Skip to content

Commit 9ce4fca

Browse files
sdsrssclaude
andcommitted
release: v0.2.4 — audit batch 6 (round-0 progress signal, moa config validation, contract/test hardening)
Second-audit functional/contract follow-up. Each code fix is RED-first. Full suite ALL GREEN (MoA 30, stop-gate 26, driver-limits 17, codex-limits 18). Version bumped in all three manifest spots. - fix(drivers): the no-progress fingerprint could not observe round-0 progress (round 0 fills PLAN/FEATURE_MATRIX before any runs/round-N.md), so a multi-session round 0 false-tripped NO_PROGRESS. Fingerprint now includes round-0 bootstrap bytes; identical across both drivers. PL-2. - fix(moa): config reference_models that is [] (silently aggregator-only) or a non-array typo (silent DEFAULT fallback) is now a clean error (exit 1). Absent reference_models still uses DEFAULT. MO-2 / MO-3. - docs(moa-decision): documents the exit-1 contract + full exit-code semantics + the capture-stdout-on-output-write-failure rule. PL-3. - test: covers F6 coordinator-mode env sanitization, the codex driver.log writability guard, and LOOP_TESTING_GATE_STALE_SECONDS=0 disabling stale-disarm. Behavior note: an empty/non-array reference_models now errors instead of silently degrading — omit it for DEFAULT or give a non-empty array. Deferred (tracked in roadmap): driver concurrency lock (P3, portable-lock + trap composition), skill/references install-dir location (P2, LLM-visible metadata needing a real-loop run). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1a1a576 commit 9ce4fca

13 files changed

Lines changed: 197 additions & 23 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
},
66
"metadata": {
77
"description": "loop-testing plugin marketplace",
8-
"version": "0.2.3"
8+
"version": "0.2.4"
99
},
1010
"plugins": [
1111
{
1212
"name": "loop-testing",
1313
"source": "./",
1414
"description": "Autonomous self-test / self-fix / self-iterate QA loop that drives a finished project to convergence as a simulated real user in a sandbox (自测·验收·QA 循环).",
15-
"version": "0.2.3",
15+
"version": "0.2.4",
1616
"category": "development"
1717
}
1818
]

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "loop-testing",
3-
"version": "0.2.3",
3+
"version": "0.2.4",
44
"description": "Autonomous QA self-test loop for a finished project: Claude acts as a real user in a sandbox, exercising every feature to surface bugs, logic/flow/UX/security/hidden issues — fixing the safe low-risk ones with regression tests and atomic commits, and recording the rest with MoA multi-model decision advice — looping self-test → self-fix → self-iterate until two consecutive low-risk rounds converge. Use when the user wants hands-off acceptance testing / 自测 / 自我测试 / 验收 / QA 循环 / self-test loop / 自动测试并修复 of their app (CLI, API, Web, or library). Runs fully autonomous, pausing only for keys/payment/network permission, a suspected security vuln to report, or a total blocker.",
55
"author": {
66
"name": "sds"

CHANGELOG.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,44 @@
11
# Changelog
22

3+
## 0.2.4 — 2026-07-12
4+
5+
Audit batch 6 (second-audit follow-up, functional/contract hardening). Lands the
6+
remaining actionable P2/P3 findings from the second production-readiness audit,
7+
each RED-first. Full suite `ALL GREEN` (MoA 30, stop-gate 26, driver-limits 17,
8+
codex-limits 18). Two findings are deferred with rationale (see below).
9+
10+
**Behavior change to note:** an `moa.config.json` whose `reference_models` is an
11+
empty array or a non-array value now fails with a clean `error:` (exit 1) instead
12+
of silently running aggregator-only / falling back to the DEFAULT models. If you
13+
relied on that silent fallback, either omit `reference_models` (to use the DEFAULT
14+
set) or give it a non-empty array.
15+
16+
- **fix(drivers)**: the no-progress fingerprint (`round | issues | converged_streak
17+
| runs count+bytes`) could not observe round-0 progress — round 0 fills PLAN.md +
18+
FEATURE_MATRIX.md before any `runs/round-N.md` exists, so a round 0 spanning ≥3
19+
sessions on a large target fingerprinted as static and false-tripped NO_PROGRESS
20+
(exit 5). The fingerprint now includes round-0 bootstrap bytes (PLAN +
21+
FEATURE_MATRIX). Kept identical across both drivers. (+2 driver tests: a round-0
22+
bootstrap run reaches --max-sessions instead of NO_PROGRESS.) PL-2.
23+
- **fix(moa)**: a config `reference_models` that is an empty array (silently ran
24+
aggregator-only) or a non-array typo (silently fell back to the paid DEFAULT
25+
models) now surfaces as a clean `error:` (exit 1) — aligning with the "zero
26+
criteria → refuse" discipline. Absent `reference_models` still uses the DEFAULT.
27+
(+2 MoA tests.) MO-2 / MO-3.
28+
- **docs(moa-decision)**: the reference now documents the exit-1 contract (user-side
29+
config/argument/input/output-write errors) alongside 0 and 2, with the full exit-
30+
code semantics and the "capture the stdout decision on an --output write failure"
31+
rule. PL-3.
32+
- **test**: closed three previously-uncovered paths — the F6 coordinator-mode env
33+
sanitization (the child must not inherit orchestration-only mode vars), the codex
34+
driver's driver.log writability guard (unwritable → die exit 2 before any session),
35+
and `LOOP_TESTING_GATE_STALE_SECONDS=0` disabling the stale-sentinel auto-disarm.
36+
37+
Deferred (tracked): a driver concurrency lock (needs a portable mkdir-lock + PID
38+
liveness + trap composition across the two drivers' differing traps; P3), and making
39+
the skill/references state how to locate the installed script dir (LLM-visible
40+
metadata requiring a real-loop run to verify; P2).
41+
342
## 0.2.3 — 2026-07-12
443

544
Second full production-readiness audit (5-track parallel review + per-finding

skills/loop-testing/references/moa-decision.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,15 @@ provider 解析:config 可为每个模型单独指定 `provider`;未指定
8383

8484
| 情况 | 引擎行为 | 退出码 |
8585
|------|---------|--------|
86-
| 部分参考模型失败(超时 / HTTP 错误 / 响应异常)| 用其余模型继续,元数据记 `partial-references` | 0 |
86+
| 正常 / 部分参考模型失败(超时 / HTTP 错误 / 响应异常)| 产出决策文件;部分失败时元数据记 `partial-references` | 0 |
8787
| 全部参考模型失败 | 仅聚合模型出建议,标 `degraded: no-references` | 0 |
8888
| 聚合模型失败,或根本没有可用 key | 报错到 stderr(已脱敏),**不产出决策文件** | **2** |
89+
| 用户侧错误:未知 CLI flag、`--config` 不可读 / 非法 JSON / `reference_models` 非非空数组、非法 provider、`--input` 缺失 / 不可读 / 为空、`--output` 写盘失败 | 打干净 `error: <msg>`**** `fatal:` 栈),不产出文件;写盘失败时决策已先打到 stdout | **1** |
8990

90-
当引擎 **exit 2** 时(MoA 不可用):编排 agent 自行用**单模型推理**给出建议,写入 `DEC-NNN.md`,并在文件与元数据中**显式标注 `degraded: single-model`**,说明缺少多模型委员会视角的局限。循环照常继续,绝不因 MoA 不可用而谎报或停摆。
91+
**退出码语义**`0` = 成功(含优雅降级);`1` = 用户侧错误(配置/参数/输入/写盘),修正后重试;`2` = MoA 不可用(付费聚合链断)。
92+
93+
- 引擎 **exit 2** 时(MoA 不可用):编排 agent 自行用**单模型推理**给出建议,写入 `DEC-NNN.md`,并在文件与元数据中**显式标注 `degraded: single-model`**,说明缺少多模型委员会视角的局限。循环照常继续,绝不因 MoA 不可用而谎报或停摆。
94+
- 引擎 **exit 1** 时(用户侧错误):先按 stderr 的 `error:` 修正配置/参数/输入再重试;**若错误是 `--output` 写盘失败,完整决策块已打到 stdout**——把它捕获落盘为 `DEC-NNN.md`,不要丢弃已付费的结论。
9195

9296
真实 key 冒烟属**付费接口 / 外网调用**,按 FR-6.7 需先获授权,不在自主推进范围内。
9397

skills/loop-testing/scripts/moa.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,14 @@ async function resolveConfig(args, env) {
179179
} catch (e) {
180180
throw new Error(`config file is not valid JSON: ${configPath}: ${e.message}`);
181181
}
182-
if (Array.isArray(parsed.reference_models)) {
182+
// reference_models absent -> keep DEFAULT. Present -> must be a non-empty array:
183+
// a non-array typo silently fell back to the DEFAULT (wrong, paid-for) models,
184+
// and [] silently ran aggregator-only — both contradict "zero criteria -> refuse"
185+
// and are now clean errors instead of silent degradation (audit MO-2 / MO-3).
186+
if (parsed.reference_models !== undefined) {
187+
if (!Array.isArray(parsed.reference_models) || parsed.reference_models.length === 0) {
188+
throw new Error('config "reference_models" must be a non-empty array of model names');
189+
}
183190
cfg.reference_models = parsed.reference_models.map(normalizeModelEntry);
184191
}
185192
if (parsed.aggregator != null) cfg.aggregator = normalizeModelEntry(parsed.aggregator);

skills/loop-testing/scripts/unattended-codex.sh

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
# 3 hit --max-sessions before terminal (driver-declared INCOMPLETE).
3030
# 4 hit --max-minutes before terminal (driver-declared INCOMPLETE).
3131
# 5 NO_PROGRESS: two consecutive sessions with no change in the composite
32-
# progress fingerprint (round | issues | converged_streak | runs count+bytes).
32+
# progress fingerprint (round | issues | converged_streak | runs count+bytes |
33+
# round-0 bootstrap bytes).
3334
set -u
3435

3536
PROJECT=""
@@ -92,9 +93,15 @@ runs_sig() { # "<file-count>:<total-bytes>" of runs/*.md — evidence-growth sig
9293
b=$(cat "$d"/*.md 2>/dev/null | wc -c | tr -d ' ')
9394
echo "$n:$b"
9495
}
95-
progress_sig() { # composite fingerprint: round|issues|streak|runsN:runsB
96+
bootstrap_sig() { # bytes of round-0 artifacts (PLAN + FEATURE_MATRIX)
97+
# Round 0 fills PLAN.md + FEATURE_MATRIX.md BEFORE any runs/round-N.md exists, so
98+
# without this a round 0 that spans sessions on a large target fingerprints as
99+
# static (round/issues/streak/runs all 0) and false-trips NO_PROGRESS (audit PL-2).
100+
cat "$LT/PLAN.md" "$LT/FEATURE_MATRIX.md" 2>/dev/null | wc -c | tr -d ' '
101+
}
102+
progress_sig() { # composite fingerprint: round|issues|streak|runsN:runsB|bootstrapB
96103
local s; s="$(state_field converged_streak)"; [ -n "$s" ] || s=-1
97-
printf '%s|%s|%s|%s' "$(round_of)" "$(issue_count)" "$s" "$(runs_sig)"
104+
printf '%s|%s|%s|%s|%s' "$(round_of)" "$(issue_count)" "$s" "$(runs_sig)" "$(bootstrap_sig)"
98105
}
99106

100107
log() { echo "$*" >> "$DLOG"; }
@@ -166,9 +173,10 @@ while true; do
166173
fi
167174

168175
# No-progress breaker: ANY change in the composite signal (round, issue count,
169-
# converged_streak, runs/ evidence bytes+count) counts as progress — catches a
170-
# long round spanning sessions and convergence progress the old round+issues
171-
# signal misread as stuck (audit A3). Kept identical to unattended-loop.sh.
176+
# converged_streak, runs/ evidence bytes+count, round-0 bootstrap bytes) counts as
177+
# progress — catches a long round spanning sessions, convergence progress, and a
178+
# round 0 filling PLAN/FEATURE_MATRIX before any runs/ file, all of which the old
179+
# round+issues signal misread as stuck (audit A3 / PL-2). Identical to unattended-loop.sh.
172180
if [ "$cur_sig" = "$prev_sig" ]; then
173181
noprog=$(( noprog + 1 ))
174182
else
@@ -177,6 +185,6 @@ while true; do
177185
prev_sig="$cur_sig"
178186

179187
if [ "$noprog" -ge 2 ]; then
180-
summary_exit 5 "NO_PROGRESS: two consecutive sessions with no change in round/issues/streak/runs"
188+
summary_exit 5 "NO_PROGRESS: two consecutive sessions with no change in round/issues/streak/runs/bootstrap"
181189
fi
182190
done

skills/loop-testing/scripts/unattended-loop.sh

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
# 3 hit --max-sessions before terminal (driver-declared INCOMPLETE).
2525
# 4 hit --max-minutes before terminal (driver-declared INCOMPLETE).
2626
# 5 NO_PROGRESS: two consecutive sessions with no change in the composite
27-
# progress fingerprint (round | issues | converged_streak | runs count+bytes).
27+
# progress fingerprint (round | issues | converged_streak | runs count+bytes |
28+
# round-0 bootstrap bytes).
2829
set -u
2930

3031
PROJECT=""
@@ -90,10 +91,16 @@ runs_sig() { # "<file-count>:<total-bytes>" of runs/*.md — evidence-growth sig
9091
b=$(cat "$d"/*.md 2>/dev/null | wc -c | tr -d ' ')
9192
echo "$n:$b"
9293
}
93-
progress_sig() { # composite progress fingerprint: round|issues|streak|runsN:runsB
94+
bootstrap_sig() { # bytes of round-0 artifacts (PLAN + FEATURE_MATRIX)
95+
# Round 0 fills PLAN.md + FEATURE_MATRIX.md BEFORE any runs/round-N.md exists, so
96+
# without this a round 0 that spans sessions on a large target fingerprints as
97+
# static (round/issues/streak/runs all 0) and false-trips NO_PROGRESS (audit PL-2).
98+
cat "$LT/PLAN.md" "$LT/FEATURE_MATRIX.md" 2>/dev/null | wc -c | tr -d ' '
99+
}
100+
progress_sig() { # composite fingerprint: round|issues|streak|runsN:runsB|bootstrapB
94101
local s
95102
s="$(state_field converged_streak)"; [ -n "$s" ] || s=-1
96-
printf '%s|%s|%s|%s' "$(round_of)" "$(issue_count)" "$s" "$(runs_sig)"
103+
printf '%s|%s|%s|%s|%s' "$(round_of)" "$(issue_count)" "$s" "$(runs_sig)" "$(bootstrap_sig)"
97104
}
98105

99106
mkdir -p "$LT"
@@ -182,17 +189,18 @@ while true; do
182189
fi
183190

184191
# 4. No-progress circuit breaker. Progress = ANY change in the composite signal
185-
# (round, issue count, converged_streak, or runs/ evidence bytes+count). The
186-
# last two catch a long round that spans sessions appending evidence before
187-
# `round` ticks, and progress made by advancing convergence — cases the old
188-
# round+issues-only signal misread as stuck (audit A3).
192+
# (round, issue count, converged_streak, runs/ evidence bytes+count, or round-0
193+
# bootstrap bytes). These catch a long round that spans sessions appending
194+
# evidence before `round` ticks, progress made by advancing convergence, and a
195+
# round 0 filling PLAN/FEATURE_MATRIX before any runs/ file exists — cases the
196+
# old round+issues-only signal misread as stuck (audit A3 / PL-2).
189197
if [ "$cur_sig" = "$prev_sig" ]; then
190198
no_progress=$(( no_progress + 1 ))
191199
else
192200
no_progress=0
193201
fi
194202
prev_sig="$cur_sig"
195203
if [ "$no_progress" -ge 2 ]; then
196-
summary_exit 5 "NO_PROGRESS: 2 consecutive sessions with no change in round/issues/streak/runs (stuck)"
204+
summary_exit 5 "NO_PROGRESS: 2 consecutive sessions with no change in round/issues/streak/runs/bootstrap (stuck)"
197205
fi
198206
done

tests/driver/codex-lib.sh

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,15 @@ round=-1
4848
[ -f "$STATE" ] && round=$(grep -aE '^round:' "$STATE" | head -1 | sed 's/[^0-9-]//g')
4949
case "$round" in ''|*[!0-9-]*) round=-1 ;; esac
5050
streak=0
51-
if [ "${STUB_STREAK_ONLY:-0}" = "1" ]; then
51+
if [ "${STUB_BOOTSTRAP:-0}" = "1" ]; then
52+
# Round-0 progress: round / issue count / converged_streak stay static and NO
53+
# runs/round-N.md is created, but PLAN.md + FEATURE_MATRIX.md grow each call
54+
# (the project-analysis phase before the loop proper). Exercises PL-2.
55+
new_round=$round; [ "$new_round" -lt 0 ] && new_round=0
56+
printf 'plan detail line for session\n' >> "$LT/PLAN.md"
57+
printf '| feature | entry | scenario | PASS | R0 |\n' >> "$LT/FEATURE_MATRIX.md"
58+
status=RUNNING
59+
elif [ "${STUB_STREAK_ONLY:-0}" = "1" ]; then
5260
# Progress via convergence + evidence only: round and issue count stay static,
5361
# but converged_streak advances and a runs/ evidence file grows each call.
5462
new_round=$round; [ "$new_round" -lt 0 ] && new_round=0

tests/driver/codex-limits.test.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,22 @@ STUB_NO_STATE=1 bash "$CODEX_DRIVER" --project "$WS7" --codex-bin "$stub" --no-p
7878
assert_rc $? 5 "absent STATE.md -> exit 5"
7979
assert_eq "1" "$(sessions_in_log "$WS7")" "exits after exactly 1 STATE-less session (not 2)"
8080

81+
# J. Round-0 progress: round/issues/streak static and NO runs/ file, but PLAN.md +
82+
# FEATURE_MATRIX.md grow each session. Must NOT trip NO_PROGRESS (audit PL-2).
83+
# Mirrors loop driver H.
84+
WS8=$(mk_proj); trap 'rm -rf "$WS" "$WS2" "$WS3" "$WS4" "$WS5" "$WS6" "$WS7" "$WS8"' EXIT
85+
stub=$(write_stub "$WS8")
86+
write_state "$WS8" RUNNING 0
87+
STUB_BOOTSTRAP=1 bash "$CODEX_DRIVER" --project "$WS8" --codex-bin "$stub" --no-protect --max-sessions 3 >/dev/null 2>&1
88+
assert_rc $? 3 "round-0 bootstrap progress (PLAN/FEATURE_MATRIX grow) -> max-sessions, not NO_PROGRESS"
89+
90+
# K. driver.log not writable -> die exit 2 BEFORE any session (parity with the loop
91+
# driver's writability guard; R16 gained this for codex but had no dedicated test).
92+
WS9=$(mk_proj); trap 'rm -rf "$WS" "$WS2" "$WS3" "$WS4" "$WS5" "$WS6" "$WS7" "$WS8" "$WS9"' EXIT
93+
mkdir "$WS9/docs/looptesting/driver.log" # append to a directory fails -> guard fires
94+
stub=$(write_stub "$WS9")
95+
bash "$CODEX_DRIVER" --project "$WS9" --codex-bin "$stub" --no-protect --max-sessions 1 >/dev/null 2>&1
96+
assert_rc $? 2 "unwritable driver.log -> die exit 2"
97+
assert_eq "0" "$(sessions_in_log "$WS9")" "no session launched when driver.log is unwritable"
98+
8199
report "codex-limits.test.sh"

tests/driver/driver-limits.test.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,39 @@ STUB_NO_STATE=1 bash "$DRIVER" --project "$WS5" --claude-bin "$stub" --max-sessi
5858
assert_rc $? 5 "absent STATE.md -> exit 5"
5959
assert_eq "1" "$(sessions_in_log "$WS5")" "exits after exactly 1 STATE-less session (not 2)"
6060

61+
# H. Round-0 progress: round/issues/streak static and NO runs/ file, but PLAN.md +
62+
# FEATURE_MATRIX.md grow each session (project-analysis phase). Must NOT trip
63+
# NO_PROGRESS — the round+issues+streak+runs signal alone misread a multi-session
64+
# round 0 as stuck (audit PL-2).
65+
WS6=$(mk_proj); trap 'rm -rf "$WS" "$WS2" "$WS3" "$WS4" "$WS5" "$WS6"' EXIT
66+
stub=$(write_stub "$WS6")
67+
write_state "$WS6" RUNNING 0
68+
STUB_BOOTSTRAP=1 bash "$DRIVER" --project "$WS6" --claude-bin "$stub" --max-sessions 3 >/dev/null 2>&1
69+
assert_rc $? 3 "round-0 bootstrap progress (PLAN/FEATURE_MATRIX grow) -> runs to max-sessions, not NO_PROGRESS"
70+
71+
# I. F6 coordinator-mode env sanitization: vars that would boot the child session in
72+
# orchestration-only mode (Agent/SendMessage/TaskStop/Workflow, no Read/Bash/Edit/
73+
# Write) must NOT reach the child. The stub records which survived into its own env.
74+
WS7=$(mk_proj); trap 'rm -rf "$WS" "$WS2" "$WS3" "$WS4" "$WS5" "$WS6" "$WS7"' EXIT
75+
cat > "$WS7/envstub.sh" <<'ESTUB'
76+
#!/usr/bin/env bash
77+
set -u
78+
env | grep -aE '^CLAUDE_CODE_(COORDINATOR_MODE|EXPERIMENTAL_AGENT_TEAMS|CHILD_SESSION|SESSION_ID)=' \
79+
> docs/looptesting/child-env.txt || true
80+
cat > docs/looptesting/STATE.md <<EOS
81+
# STATE
82+
round: 0
83+
converged_streak: 2
84+
status: CONVERGED
85+
max_rounds: 12
86+
EOS
87+
exit 0
88+
ESTUB
89+
chmod +x "$WS7/envstub.sh"
90+
CLAUDE_CODE_COORDINATOR_MODE=1 CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 \
91+
CLAUDE_CODE_CHILD_SESSION=1 CLAUDE_CODE_SESSION_ID=sess-abc \
92+
bash "$DRIVER" --project "$WS7" --claude-bin "$WS7/envstub.sh" --max-sessions 1 >/dev/null 2>&1
93+
assert_rc $? 0 "converged after a sanitized session -> exit 0"
94+
assert_eq "" "$(cat "$WS7/docs/looptesting/child-env.txt" 2>/dev/null)" "coordinator-mode env vars unset for the child (F6)"
95+
6196
report "driver-limits.test.sh"

0 commit comments

Comments
 (0)