qualia-framework 6.9.0 → 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.
@@ -38,6 +38,20 @@ node ${QUALIA_BIN}/plan-contract.js validate .planning/phase-{N}-contract.json
38
38
 
39
39
  Parse tasks, waves, file refs. Prefer the JSON contract for task ids, dependencies, file lists, and verification checks; use the Markdown plan as the human-readable context.
40
40
 
41
+ ### 1a. Analyze Gate (scope ↔ plan, before any build)
42
+
43
+ `plan-contract.js` proves the contract is internally well-formed; this gate diffs it **against intent** — scope acceptance criteria (`phase-{N}-context.md`) + the CONTEXT.md glossary — to catch requirements the plan silently dropped or contradicted. This is the plan→build seam Spec-Kit calls `/analyze`.
44
+
45
+ ```bash
46
+ node ${QUALIA_BIN}/analyze-gate.js {N}
47
+ ```
48
+
49
+ Exit 0 → consistent, proceed. Non-zero → it lists under-covered scope criteria, orphan success criteria, glossary violations, and scope-reduction language. **Profile-aware** (the `profile` field from `state.js check`):
50
+ - **strict** → a HIGH finding is a stop. Route to `/qualia-plan {N} --gaps` (plan dropped a requirement) or `/qualia-scope {N}` (scope itself is wrong). Do not build.
51
+ - **standard** → surface findings to the operator and proceed only with an explicit ack; log the waiver reason to `.planning/decisions/` if you proceed past a HIGH.
52
+
53
+ (No scope file = scope-coverage check is skipped, not a failure — `/qualia-feature` trivia and scope-less phases still build.)
54
+
41
55
  ### 1b. Recovery Reference
42
56
 
43
57
  Tag HEAD before executing. Reference only, no auto-rollback.
@@ -117,7 +131,13 @@ Parallel tasks Wave {W} (do NOT touch their files):
117
131
  </task_contract>
118
132
 
119
133
  Context tags already loaded. Only Read project code you modify.
120
- Execute. Commit. Return DONE/BLOCKED/PARTIAL.
134
+
135
+ Status protocol (machine-readable fan-in — do this, do not skip):
136
+ - First action: `node ${QUALIA_BIN}/agent-status.js write {task_id} RUNNING --phase {N} --wave {W}`
137
+ - Last action, after committing: `node ${QUALIA_BIN}/agent-status.js write {task_id} DONE --commit $(git rev-parse --short HEAD)`
138
+ (use BLOCKED or PARTIAL with `--note \"why\"` instead of DONE if you could not finish)
139
+
140
+ Execute. Commit. Write your DONE/BLOCKED/PARTIAL status. Return DONE/BLOCKED/PARTIAL.
121
141
  ", subagent_type="qualia-builder", description="Task {N}: {title}")
122
142
  ```
123
143
 
@@ -130,6 +150,14 @@ Execute. Commit. Return DONE/BLOCKED/PARTIAL.
130
150
  node ${QUALIA_BIN}/qualia-ui.js done {task_num} "{title}" {commit_hash}
131
151
  ```
132
152
 
153
+ **After each wave — fan-in barrier (deterministic, not "did the model notice"):**
154
+
155
+ ```bash
156
+ node ${QUALIA_BIN}/agent-status.js barrier .planning/phase-{N}-contract.json --wave {W}
157
+ ```
158
+
159
+ Exit 0 ⇔ every task in the wave wrote `DONE`. Non-zero → the barrier lists which tasks are RUNNING/BLOCKED/PARTIAL/MISSING. Do NOT advance to the next wave until the barrier passes; a BLOCKED/PARTIAL task is a wave failure (§4). `agent-status.js list` shows the live wave view.
160
+
133
161
  **After each wave:** move to next, show summary.
134
162
 
135
163
  ### 3. Wave Completion
@@ -141,6 +169,7 @@ node ${QUALIA_BIN}/qualia-ui.js divider
141
169
  node ${QUALIA_BIN}/qualia-ui.js ok "Tasks: {done}/{total}"
142
170
  node ${QUALIA_BIN}/qualia-ui.js ok "Commits: {count}"
143
171
  node ${QUALIA_BIN}/qualia-ui.js ok "Waves: {count}"
172
+ node ${QUALIA_BIN}/agent-status.js clear # drop ephemeral .agent-status/ scratch
144
173
  ```
145
174
 
146
175
  ### 4. Handle Failures
@@ -230,6 +230,9 @@ if [ "$ERP_ENABLED" = "true" ]; then
230
230
  if [ "$HTTP_CODE" = "200" ]; then
231
231
  ERP_REPORT_ID=$(echo "$BODY" | node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).report_id||'')}catch{}")
232
232
  node ${QUALIA_BIN}/qualia-ui.js ok "Uploaded as $CLIENT_REPORT_ID (ERP: ${ERP_REPORT_ID:-none})"
233
+ # Reset per-session usage telemetry (command_usage + prompt_samples) now
234
+ # that it has been delivered, so the next shift starts from a clean slate.
235
+ rm -f .planning/.session-usage.json 2>/dev/null
233
236
  break
234
237
  fi
235
238
  if [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "422" ]; then
@@ -65,8 +65,11 @@ npx tsc --noEmit # TypeScript — must pass
65
65
  if node -e "const p=require('./package.json');process.exit(p.scripts&&p.scripts.lint?0:1)"; then npm run lint; fi
66
66
  if node -e "const p=require('./package.json');process.exit(p.scripts&&p.scripts.test?0:1)"; then npm test; fi
67
67
  npm run build # Build — must succeed
68
+ node ${QUALIA_BIN}/slop-detect.mjs --severity=critical # Anti-slop — CRITICAL design tells block ship
68
69
  ```
69
70
 
71
+ The `pre-deploy-gate.js` hook re-runs the anti-slop scan at `vercel --prod` time as the hard, non-bypassable gate (OWNER-only `QUALIA_SKIP_SLOP=1` escape). This step surfaces failures early so they're fixed before the deploy command.
72
+
70
73
  On failure:
71
74
  1. Summarize what failed in plain language
72
75
  2. Auto-fix
@@ -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.
@@ -8,3 +8,6 @@ tracking.json
8
8
  .state.journal
9
9
  .backup/
10
10
  migration-manifest.json
11
+ # Performance-audit telemetry buffer. Holds the engineer's real prompt samples
12
+ # until clock-out — must NEVER be committed (raw prompt text, may contain PII).
13
+ .session-usage.json
@@ -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,14 +487,16 @@ else
487
487
  fail_case "CLAUDE.md role substitution"
488
488
  fi
489
489
 
490
- # 31. All 13 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
- # REINTRODUCED in v6.3.2 with sidecar-snapshot mechanism)
494
+ # REINTRODUCED in v6.3.2 with sidecar-snapshot mechanism;
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)
495
497
  HOOK_COUNT=$(ls "$TMP/.claude/hooks/"*.js 2>/dev/null | wc -l)
496
- if [ "$HOOK_COUNT" -eq 13 ]; then
497
- pass "13 hooks installed in hooks/ (incl. pre-compact v6.3.2)"
498
+ if [ "$HOOK_COUNT" -eq 15 ]; then
499
+ pass "15 hooks installed in hooks/ (incl. task-write-guard v6.13)"
498
500
  else
499
501
  fail_case "hook count" "got $HOOK_COUNT"
500
502
  fi