qualia-framework 6.9.2 → 6.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: qualia-update
3
+ description: "Ship one update to a LAUNCHED project (operate lifecycle). The post-launch counterpart to /qualia-milestone — a lean plan → build → verify → ship loop with no milestone-journey or forced-handoff machinery. Triggers: 'ship an update', 'make a change to the live site', 'the project is live, I need to add X', 'operate mode', 'post-launch change', 'patch the launched product'."
4
+ allowed-tools:
5
+ - Bash
6
+ - Read
7
+ - Write
8
+ - Edit
9
+ - Grep
10
+ - Glob
11
+ - Agent
12
+ ---
13
+
14
+ # /qualia-update — ship one update to a launched project
15
+
16
+ The operate-lifecycle counterpart to `/qualia-milestone`. Once a project has
17
+ **launched** (`state.js launch` flips `lifecycle` from `build` to `operate`), it
18
+ is an **update stream**, not a milestone journey: there is no forced
19
+ polish → ship → **handoff** terminal. `/qualia-update` runs one lean change
20
+ cycle and loops. Each completed update bumps `lifetime.updates_completed`.
21
+
22
+ ## Output contract
23
+
24
+ ```
25
+ Command: /qualia-update {change}
26
+ Scope: {files/routes touched}
27
+ Intent: build
28
+ Mutation: planned → active
29
+ Evidence: state.js check, contract-runner output
30
+ Output: shipped update + deployed URL
31
+ Next: /qualia-update (next change) or done
32
+ ```
33
+
34
+ ## Process
35
+
36
+ ### 1. Confirm the project is launched
37
+
38
+ ```bash
39
+ node ${QUALIA_BIN}/state.js check
40
+ ```
41
+
42
+ - `lifecycle: "operate"` → proceed.
43
+ - `lifecycle: "build"` and the product is actually live → launch it first:
44
+ ```bash
45
+ node ${QUALIA_BIN}/state.js launch --deployed-url {url} --source manual
46
+ ```
47
+ (The ERP can also drive this automatically when it detects the project is live.)
48
+ - Still mid-build (never launched, not live) → this is `/qualia-feature` or a
49
+ normal phase, not an update. Route there instead.
50
+
51
+ ### 2. Scope the change (one update = one coherent change)
52
+
53
+ Keep it small and shippable. A bug fix, a copy change, a new section, a single
54
+ feature. If it's larger than ~5 files or needs its own milestone arc, it's a new
55
+ `build`-mode milestone — use `/qualia-milestone`, not an update.
56
+
57
+ ### 3. Run the lean loop
58
+
59
+ Reuse the real lifecycle skills, scoped to this one change:
60
+
61
+ ```
62
+ /qualia-plan {N} # compile the change into a phase + machine contract
63
+ /qualia-build {N} # builders execute, atomic commits
64
+ /qualia-verify {N} # contract-runner evidence + goal-backward check (must PASS)
65
+ ```
66
+
67
+ Verification evidence is mandatory exactly as in build mode — a contract runs and
68
+ its evidence must be clean before PASS. On the final phase, a `verified(pass)`
69
+ in operate routes here again (`next_command: /qualia-update`) and increments
70
+ `lifetime.updates_completed`.
71
+
72
+ ### 4. Ship
73
+
74
+ ```bash
75
+ /qualia-ship # quality gates → commit → deploy → verify live
76
+ ```
77
+
78
+ No handoff. No "final milestone." The deploy is just this update going live. Then
79
+ `/qualia-report` as usual — the report carries `lifecycle: operate` and the
80
+ updated `updates_completed` so the ERP counts updates, not milestones.
81
+
82
+ ### 5. Loop or stop
83
+
84
+ ```bash
85
+ node ${QUALIA_BIN}/qualia-ui.js end "UPDATE SHIPPED" "/qualia-update"
86
+ ```
87
+
88
+ Run `/qualia-update` again for the next change, or stop — an operate project has
89
+ no terminal state to reach.
90
+
91
+ ## Why this exists
92
+
93
+ Hard-coding "every project ends in Handoff" forced launched products and
94
+ retainers into a state machine that fought them. `/qualia-update` + the `operate`
95
+ lifecycle make "this project is live and keeps shipping" explicit state instead
96
+ of a milestone the team is forced to mislabel. See `templates/journey.md` rule 3.
@@ -175,7 +175,13 @@ Write the deterministic eval artifact before changing state:
175
175
  node ${QUALIA_BIN}/harness-eval.js --phase {N} --run --write
176
176
  ```
177
177
 
178
- If the eval status is `FAIL`, do not mark the phase PASS. The state machine also refuses PASS when a contract exists but `.planning/evidence/phase-{N}-contract-run.json` is missing/failing, or when the verification report contains `INSUFFICIENT EVIDENCE`.
178
+ Run the zero-token anti-slop scan as a deterministic gate (same role as `migration-guard`/`branch-guard` the scanner exits non-zero on CRITICAL design tells). A CRITICAL finding is a verification FAIL, not a soft note:
179
+
180
+ ```bash
181
+ node ${QUALIA_BIN}/slop-detect.mjs --severity=critical
182
+ ```
183
+
184
+ If the eval status is `FAIL` or anti-slop exits non-zero, do not mark the phase PASS. The state machine also refuses PASS when a contract exists but `.planning/evidence/phase-{N}-contract-run.json` is missing/failing, or when the verification report contains `INSUFFICIENT EVIDENCE`.
179
185
 
180
186
  ```bash
181
187
  node ${QUALIA_BIN}/state.js transition --to verified --phase {N} --verification {pass|fail} --evidence .planning/evals/harness-eval-*.json
@@ -102,7 +102,7 @@ M1 ─── M2 ─── M3 ─── ... ─── M{N} (Handoff)
102
102
 
103
103
  1. **Hard ceiling: 5 milestones.** If the project needs more, defer to a v2 release after handoff.
104
104
  2. **Hard floor: 2 milestones.** Anything smaller should use `/qualia-new --quick` instead.
105
- 3. **The final milestone is always Handoff.** Same 4 phases every project. Never negotiable.
105
+ 3. **In BUILD mode, the final milestone is Handoff.** This is the convention for a one-shot client build that ends with a handoff. It is **not** a universal law: once a project launches and enters the `operate` lifecycle (`state.js launch`), it becomes an *update stream* with no forced Handoff — it ships updates indefinitely. Don't author a Handoff milestone for a product/retainer that will keep shipping; launch it instead.
106
106
  4. **Milestones ≥ 2 phases OR are a shipped release gate.** A 1-phase milestone is a phase, not a milestone.
107
107
  5. **Numbering is contiguous.** No skipped milestone numbers.
108
108
  6. **Progressive detail is OK.** M1 is fully detailed (ready for `/qualia-plan`). M2..M{N-1} have phase names + one-line goals. Full phase detail gets written by roadmapper when the milestone opens.
@@ -0,0 +1,138 @@
1
+ #!/bin/bash
2
+ # agent-status.test.sh — bin/agent-status.js (per-task status + fan-in barrier)
3
+ # Run: bash tests/agent-status.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ AS="$BIN_DIR/agent-status.js"
10
+
11
+ assert_exit() {
12
+ local name="$1" expected="$2" actual="$3"
13
+ if [ "$expected" = "$actual" ]; then
14
+ echo " ✓ $name"; PASS=$((PASS + 1))
15
+ else
16
+ echo " ✗ $name (expected exit $expected, got $actual)"; FAIL=$((FAIL + 1))
17
+ fi
18
+ }
19
+
20
+ assert_contains() {
21
+ local name="$1" haystack="$2" needle="$3"
22
+ if echo "$haystack" | grep -q "$needle"; then
23
+ echo " ✓ $name"; PASS=$((PASS + 1))
24
+ else
25
+ echo " ✗ $name (missing '$needle' in: $haystack)"; FAIL=$((FAIL + 1))
26
+ fi
27
+ }
28
+
29
+ # A minimal contract with two tasks in wave 1, one in wave 2.
30
+ write_contract() {
31
+ cat > "$1" <<'EOF'
32
+ {
33
+ "version": 1,
34
+ "tasks": [
35
+ { "id": "T1", "wave": 1 },
36
+ { "id": "T2", "wave": 1 },
37
+ { "id": "T3", "wave": 2 }
38
+ ]
39
+ }
40
+ EOF
41
+ }
42
+
43
+ echo "agent-status.test.sh — bin/agent-status.js"
44
+ echo ""
45
+
46
+ # syntax
47
+ $NODE -c "$AS" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
48
+
49
+ # --- write / read round-trip ---
50
+ TMP=$(mktemp -d)
51
+ $NODE "$AS" write T1 RUNNING --phase 3 --wave 1 --cwd "$TMP" >/dev/null 2>&1
52
+ assert_exit "write RUNNING → exit 0" 0 $?
53
+ [ -f "$TMP/.agent-status/T1.json" ] && { echo " ✓ status file created"; PASS=$((PASS+1)); } || { echo " ✗ status file missing"; FAIL=$((FAIL+1)); }
54
+ OUT=$($NODE "$AS" read T1 --cwd "$TMP" --json 2>&1)
55
+ assert_contains "read shows RUNNING" "$OUT" '"status":"RUNNING"'
56
+ $NODE "$AS" write T1 DONE --commit abc1234 --cwd "$TMP" >/dev/null 2>&1
57
+ OUT=$($NODE "$AS" read T1 --cwd "$TMP" --json 2>&1)
58
+ assert_contains "overwrite to DONE" "$OUT" '"status":"DONE"'
59
+ assert_contains "records commit hash" "$OUT" 'abc1234'
60
+ rm -rf "$TMP"
61
+
62
+ # --- validation: bad task id and bad status are rejected (exit 2) ---
63
+ TMP=$(mktemp -d)
64
+ $NODE "$AS" write nope DONE --cwd "$TMP" >/dev/null 2>&1
65
+ assert_exit "invalid task id → exit 2" 2 $?
66
+ $NODE "$AS" write T1 FINISHED --cwd "$TMP" >/dev/null 2>&1
67
+ assert_exit "invalid status → exit 2" 2 $?
68
+ # traversal attempt in task id is rejected (no file written outside dir)
69
+ $NODE "$AS" write ../evil DONE --cwd "$TMP" >/dev/null 2>&1
70
+ assert_exit "path-traversal task id → exit 2" 2 $?
71
+ rm -rf "$TMP"
72
+
73
+ # --- barrier: holds until all expected tasks in the wave are DONE ---
74
+ TMP=$(mktemp -d)
75
+ write_contract "$TMP/contract.json"
76
+
77
+ # Nothing written yet → wave 1 barrier HOLDS (exit 1), T1+T2 MISSING
78
+ OUT=$($NODE "$AS" barrier "$TMP/contract.json" --wave 1 --cwd "$TMP" 2>&1)
79
+ RC=$?
80
+ assert_exit "empty wave-1 barrier holds" 1 $RC
81
+ assert_contains "barrier reports HOLD" "$OUT" "BARRIER HOLD"
82
+
83
+ # One DONE, one RUNNING → still holds
84
+ $NODE "$AS" write T1 DONE --commit aaa --cwd "$TMP" >/dev/null 2>&1
85
+ $NODE "$AS" write T2 RUNNING --cwd "$TMP" >/dev/null 2>&1
86
+ $NODE "$AS" barrier "$TMP/contract.json" --wave 1 --cwd "$TMP" >/dev/null 2>&1
87
+ assert_exit "partial wave-1 barrier holds" 1 $?
88
+
89
+ # Both DONE → wave 1 barrier PASSES (exit 0)
90
+ $NODE "$AS" write T2 DONE --commit bbb --cwd "$TMP" >/dev/null 2>&1
91
+ OUT=$($NODE "$AS" barrier "$TMP/contract.json" --wave 1 --cwd "$TMP" 2>&1)
92
+ RC=$?
93
+ assert_exit "complete wave-1 barrier passes" 0 $RC
94
+ assert_contains "barrier reports PASS" "$OUT" "BARRIER PASS"
95
+
96
+ # Wave 2 still has T3 MISSING → holds independently
97
+ $NODE "$AS" barrier "$TMP/contract.json" --wave 2 --cwd "$TMP" >/dev/null 2>&1
98
+ assert_exit "wave-2 barrier still holds (T3 missing)" 1 $?
99
+
100
+ # Whole-phase barrier (no --wave) holds because T3 not done
101
+ $NODE "$AS" barrier "$TMP/contract.json" --cwd "$TMP" >/dev/null 2>&1
102
+ assert_exit "phase barrier holds while T3 open" 1 $?
103
+
104
+ # Finish T3 → phase barrier passes
105
+ $NODE "$AS" write T3 DONE --commit ccc --cwd "$TMP" >/dev/null 2>&1
106
+ $NODE "$AS" barrier "$TMP/contract.json" --cwd "$TMP" >/dev/null 2>&1
107
+ assert_exit "phase barrier passes when all done" 0 $?
108
+
109
+ # A BLOCKED task holds the barrier (BLOCKED != DONE)
110
+ $NODE "$AS" write T3 BLOCKED --note "missing dep" --cwd "$TMP" >/dev/null 2>&1
111
+ OUT=$($NODE "$AS" barrier "$TMP/contract.json" --cwd "$TMP" --json 2>&1)
112
+ RC=$?
113
+ assert_exit "BLOCKED task holds barrier" 1 $RC
114
+ assert_contains "barrier json counts blocked" "$OUT" '"blocked": 1'
115
+ rm -rf "$TMP"
116
+
117
+ # --- list + clear ---
118
+ TMP=$(mktemp -d)
119
+ $NODE "$AS" write T1 DONE --cwd "$TMP" >/dev/null 2>&1
120
+ $NODE "$AS" write T2 RUNNING --cwd "$TMP" >/dev/null 2>&1
121
+ OUT=$($NODE "$AS" list --cwd "$TMP" 2>&1)
122
+ assert_contains "list shows T1" "$OUT" "T1"
123
+ assert_contains "list shows T2" "$OUT" "T2"
124
+ $NODE "$AS" clear --cwd "$TMP" >/dev/null 2>&1
125
+ [ ! -d "$TMP/.agent-status" ] && { echo " ✓ clear removes status dir"; PASS=$((PASS+1)); } || { echo " ✗ clear left status dir"; FAIL=$((FAIL+1)); }
126
+ rm -rf "$TMP"
127
+
128
+ # --- buildActive library signal (used by R1's pre-write guard) ---
129
+ TMP=$(mktemp -d)
130
+ ACTIVE=$($NODE -e "const a=require('$AS'); a.writeStatus('$TMP',{task:'T1',status:'RUNNING'}); console.log(a.buildActive('$TMP'))" 2>&1)
131
+ assert_contains "buildActive true while RUNNING" "$ACTIVE" "true"
132
+ IDLE=$($NODE -e "const a=require('$AS'); a.writeStatus('$TMP',{task:'T1',status:'DONE'}); console.log(a.buildActive('$TMP'))" 2>&1)
133
+ assert_contains "buildActive false when none RUNNING" "$IDLE" "false"
134
+ rm -rf "$TMP"
135
+
136
+ echo ""
137
+ echo "=== Results: $PASS passed, $FAIL failed ==="
138
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,170 @@
1
+ #!/bin/bash
2
+ # analyze-gate.test.sh — bin/analyze-gate.js (cross-artifact scope↔plan gate)
3
+ # Run: bash tests/analyze-gate.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ AG="$BIN_DIR/analyze-gate.js"
10
+
11
+ assert_exit() {
12
+ local name="$1" expected="$2" actual="$3"
13
+ if [ "$expected" = "$actual" ]; then echo " ✓ $name"; PASS=$((PASS+1));
14
+ else echo " ✗ $name (expected exit $expected, got $actual)"; FAIL=$((FAIL+1)); fi
15
+ }
16
+ assert_contains() {
17
+ local name="$1" hay="$2" needle="$3"
18
+ if echo "$hay" | grep -q "$needle"; then echo " ✓ $name"; PASS=$((PASS+1));
19
+ else echo " ✗ $name (missing '$needle' in: $hay)"; FAIL=$((FAIL+1)); fi
20
+ }
21
+
22
+ # A contract whose tasks cover "checkout" + "stripe webhook" + "email receipt".
23
+ write_good_contract() {
24
+ cat > "$1" <<'EOF'
25
+ {
26
+ "version": 1,
27
+ "phase": 2,
28
+ "goal": "Customers can pay for an order and receive a receipt",
29
+ "why": "revenue",
30
+ "success_criteria": ["Checkout charges the card via Stripe", "Customer receives an email receipt"],
31
+ "tasks": [
32
+ { "id": "T1", "title": "Stripe checkout charge", "action": "Implement the checkout endpoint that charges the card through the Stripe webhook handler", "acceptance_criteria": ["Checkout charges card via Stripe successfully"] },
33
+ { "id": "T2", "title": "Email receipt", "action": "Send an email receipt to the customer after a successful charge", "acceptance_criteria": ["Customer receives an email receipt after payment"] }
34
+ ]
35
+ }
36
+ EOF
37
+ }
38
+
39
+ write_scope() {
40
+ cat > "$1" <<'EOF'
41
+ ---
42
+ phase: 2
43
+ ---
44
+ # Phase 2 Context: Payments
45
+
46
+ ## Acceptance Criteria (testable)
47
+ - AC1 — Checkout charges the card through Stripe
48
+ - AC2 — Customer receives an email receipt
49
+ - AC3 — Failed payments display a retry banner with a refund option
50
+
51
+ ## DoD closure
52
+ - Security: resolved
53
+ EOF
54
+ }
55
+
56
+ echo "analyze-gate.test.sh — bin/analyze-gate.js"
57
+ echo ""
58
+
59
+ # syntax
60
+ $NODE -c "$AG" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
61
+
62
+ # --- clean: every scope AC covered, every success criterion has a task ---
63
+ TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
64
+ write_good_contract "$TMP/.planning/phase-2-contract.json"
65
+ # scope with only the two covered ACs
66
+ cat > "$TMP/.planning/phase-2-context.md" <<'EOF'
67
+ ## Acceptance Criteria (testable)
68
+ - AC1 — Checkout charges the card through Stripe
69
+ - AC2 — Customer receives an email receipt
70
+ EOF
71
+ $NODE "$AG" 2 --cwd "$TMP" >/dev/null 2>&1
72
+ assert_exit "all covered → ANALYZE PASS (exit 0)" 0 $?
73
+ OUT=$($NODE "$AG" 2 --cwd "$TMP" 2>&1)
74
+ assert_contains "reports PASS" "$OUT" "ANALYZE PASS"
75
+ rm -rf "$TMP"
76
+
77
+ # --- under-covered scope AC (the retry/refund banner is in scope, not in plan) ---
78
+ TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
79
+ write_good_contract "$TMP/.planning/phase-2-contract.json"
80
+ write_scope "$TMP/.planning/phase-2-context.md"
81
+ OUT=$($NODE "$AG" 2 --cwd "$TMP" 2>&1)
82
+ RC=$?
83
+ assert_exit "uncovered scope AC → findings (exit 1)" 1 $RC
84
+ assert_contains "flags the uncovered AC" "$OUT" "under-covered"
85
+ assert_contains "names the refund/retry requirement" "$OUT" "retry banner"
86
+ # JSON shape carries the finding type
87
+ OUT=$($NODE "$AG" 2 --cwd "$TMP" --json 2>&1)
88
+ assert_contains "json finding type" "$OUT" '"uncovered-scope-ac"'
89
+ assert_contains "json severity HIGH" "$OUT" '"severity": "HIGH"'
90
+ rm -rf "$TMP"
91
+
92
+ # --- orphan success criterion (no task covers it) ---
93
+ TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
94
+ cat > "$TMP/.planning/phase-3-contract.json" <<'EOF'
95
+ {
96
+ "version": 1,
97
+ "phase": 3,
98
+ "goal": "g",
99
+ "why": "w",
100
+ "success_criteria": ["Dashboard exports analytics to CSV"],
101
+ "tasks": [
102
+ { "id": "T1", "title": "Login form", "action": "Build the login form with password reset", "acceptance_criteria": ["User can log in"] }
103
+ ]
104
+ }
105
+ EOF
106
+ OUT=$($NODE "$AG" 3 --cwd "$TMP" --json 2>&1)
107
+ RC=$?
108
+ assert_exit "orphan success criterion → exit 1" 1 $RC
109
+ assert_contains "flags orphan success criterion" "$OUT" '"uncovered-success-criterion"'
110
+ rm -rf "$TMP"
111
+
112
+ # --- glossary violation: plan uses a banned alias ---
113
+ TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
114
+ cat > "$TMP/.planning/phase-1-contract.json" <<'EOF'
115
+ {
116
+ "version": 1,
117
+ "phase": 1,
118
+ "goal": "Manage the AuthUser session lifecycle",
119
+ "why": "w",
120
+ "success_criteria": ["Sessions expire after 24h"],
121
+ "tasks": [
122
+ { "id": "T1", "title": "Session expiry", "action": "Expire AuthUser sessions after twentyfour hours of inactivity", "acceptance_criteria": ["Sessions expire after the configured window"] }
123
+ ]
124
+ }
125
+ EOF
126
+ cat > "$TMP/.planning/CONTEXT.md" <<'EOF'
127
+ # Glossary
128
+ ## Language
129
+ ### Customer
130
+ The paying account holder.
131
+ **Avoid:** AuthUser vs Customer (unless disambiguated)
132
+ EOF
133
+ OUT=$($NODE "$AG" 1 --cwd "$TMP" --json 2>&1)
134
+ RC=$?
135
+ assert_exit "banned glossary alias → exit 1" 1 $RC
136
+ assert_contains "flags glossary violation" "$OUT" '"glossary-violation"'
137
+ assert_contains "names the banned term" "$OUT" "AuthUser"
138
+ rm -rf "$TMP"
139
+
140
+ # --- no scope file → scope-coverage skipped, not a failure ---
141
+ TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
142
+ write_good_contract "$TMP/.planning/phase-2-contract.json"
143
+ # no phase-2-context.md, no CONTEXT.md
144
+ OUT=$($NODE "$AG" 2 --cwd "$TMP" 2>&1)
145
+ RC=$?
146
+ assert_exit "no scope file → exit 0 (skipped)" 0 $RC
147
+ assert_contains "notes the skip" "$OUT" "scope-coverage check skipped"
148
+ rm -rf "$TMP"
149
+
150
+ # --- missing contract → invocation error (exit 2) ---
151
+ TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
152
+ $NODE "$AG" 9 --cwd "$TMP" >/dev/null 2>&1
153
+ assert_exit "missing contract → exit 2" 2 $?
154
+ rm -rf "$TMP"
155
+
156
+ # --- library: coverage() unit behavior ---
157
+ COV=$($NODE -e "const a=require('$AG'); const t=a.tokenize('Checkout charges the card via Stripe'); const set=new Set(t); console.log(a.coverage('Checkout charges card Stripe', set).covered)" 2>&1)
158
+ assert_contains "coverage true when terms overlap" "$COV" "true"
159
+ COV=$($NODE -e "const a=require('$AG'); console.log(a.coverage('Quantum teleportation export pipeline', new Set(['login','password'])).covered)" 2>&1)
160
+ assert_contains "coverage false when disjoint" "$COV" "false"
161
+
162
+ # --- library: scope AC parser strips the AC label ---
163
+ ACS=$($NODE -e "const a=require('$AG'); console.log(JSON.stringify(a.parseScopeAcceptanceCriteria('## Acceptance Criteria (testable)\n- AC1 — Checkout works\n- AC2 — Receipt sent\n## Next\n- ignored')))" 2>&1)
164
+ assert_contains "parses AC1 without label" "$ACS" "Checkout works"
165
+ assert_contains "stops at next heading" "$ACS" "Receipt sent"
166
+ if echo "$ACS" | grep -q "ignored"; then echo " ✗ parser leaked past section"; FAIL=$((FAIL+1)); else echo " ✓ parser stops at next ## heading"; PASS=$((PASS+1)); fi
167
+
168
+ echo ""
169
+ echo "=== Results: $PASS passed, $FAIL failed ==="
170
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
package/tests/bin.test.sh CHANGED
@@ -487,15 +487,16 @@ else
487
487
  fail_case "CLAUDE.md role substitution"
488
488
  fi
489
489
 
490
- # 31. All 14 hooks installed (block-env-edit removed in v3.2.0;
490
+ # 31. All 15 hooks installed (block-env-edit removed in v3.2.0;
491
491
  # git-guardrails + stop-session-log added in v4.2.0;
492
492
  # vercel-account-guard + env-empty-guard + supabase-destructive-guard added in v5.0.0;
493
493
  # fawzi-approval-guard added in v6.2.11; pre-compact removed in v6.2.0 and
494
494
  # REINTRODUCED in v6.3.2 with sidecar-snapshot mechanism;
495
- # usage-capture added in v6.9.1 — UserPromptSubmit telemetry capture)
495
+ # usage-capture added in v6.9.1 — UserPromptSubmit telemetry capture;
496
+ # task-write-guard added in v6.13 — R1 runtime plan-contract file-scope guard)
496
497
  HOOK_COUNT=$(ls "$TMP/.claude/hooks/"*.js 2>/dev/null | wc -l)
497
- if [ "$HOOK_COUNT" -eq 14 ]; then
498
- pass "14 hooks installed in hooks/ (incl. usage-capture v6.9.1)"
498
+ if [ "$HOOK_COUNT" -eq 15 ]; then
499
+ pass "15 hooks installed in hooks/ (incl. task-write-guard v6.13)"
499
500
  else
500
501
  fail_case "hook count" "got $HOOK_COUNT"
501
502
  fi