qualia-framework 6.9.2 → 6.22.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.
Files changed (64) hide show
  1. package/AGENTS.md +8 -5
  2. package/CHANGELOG.md +208 -0
  3. package/CLAUDE.md +3 -1
  4. package/agents/roadmapper.md +16 -14
  5. package/agents/verifier.md +1 -1
  6. package/bin/agent-status.js +264 -0
  7. package/bin/analyze-gate.js +318 -0
  8. package/bin/branch-hygiene.js +135 -0
  9. package/bin/command-surface.js +2 -0
  10. package/bin/compile-instructions.js +82 -0
  11. package/bin/eval-runner.js +218 -0
  12. package/bin/host-adapters.js +72 -12
  13. package/bin/install.js +27 -17
  14. package/bin/last-report.js +207 -0
  15. package/bin/project-sync.js +315 -0
  16. package/bin/report-payload.js +7 -0
  17. package/bin/runtime-manifest.js +8 -0
  18. package/bin/state.js +257 -12
  19. package/bin/verify-panel.js +294 -0
  20. package/bin/wave-plan.js +211 -0
  21. package/docs/EMPLOYEE-QUICKSTART.md +3 -3
  22. package/docs/erp-contract.md +168 -0
  23. package/docs/qualia-manual.html +5 -5
  24. package/hooks/branch-guard.js +133 -63
  25. package/hooks/pre-deploy-gate.js +38 -0
  26. package/hooks/task-write-guard.js +165 -0
  27. package/package.json +3 -2
  28. package/rules/codex-goal.md +28 -26
  29. package/rules/infrastructure.md +1 -1
  30. package/skills/qualia/SKILL.md +6 -0
  31. package/skills/qualia-build/SKILL.md +39 -7
  32. package/skills/qualia-eval/SKILL.md +83 -0
  33. package/skills/qualia-feature/SKILL.md +20 -4
  34. package/skills/qualia-fix/SKILL.md +13 -1
  35. package/skills/qualia-milestone/SKILL.md +12 -6
  36. package/skills/qualia-new/REFERENCE.md +6 -4
  37. package/skills/qualia-new/SKILL.md +27 -15
  38. package/skills/qualia-plan/SKILL.md +2 -2
  39. package/skills/qualia-report/SKILL.md +10 -0
  40. package/skills/qualia-scope/SKILL.md +3 -3
  41. package/skills/qualia-ship/SKILL.md +37 -4
  42. package/skills/qualia-update/SKILL.md +100 -0
  43. package/skills/qualia-verify/SKILL.md +51 -24
  44. package/templates/instructions.md +32 -0
  45. package/templates/journey.md +2 -2
  46. package/templates/project-discovery.md +30 -23
  47. package/templates/requirements.md +7 -7
  48. package/tests/agent-status.test.sh +153 -0
  49. package/tests/analyze-gate.test.sh +170 -0
  50. package/tests/bin.test.sh +5 -4
  51. package/tests/branch-hygiene.test.sh +93 -0
  52. package/tests/eval-runner.test.sh +147 -0
  53. package/tests/hooks.test.sh +218 -17
  54. package/tests/install-smoke.test.sh +4 -3
  55. package/tests/instructions.test.sh +109 -0
  56. package/tests/last-report.test.sh +156 -0
  57. package/tests/lib.test.sh +2 -2
  58. package/tests/project-sync.test.sh +175 -0
  59. package/tests/run-all.sh +9 -0
  60. package/tests/runner.js +3 -2
  61. package/tests/state.test.sh +187 -0
  62. package/tests/verify-panel.test.sh +162 -0
  63. package/tests/wave-plan.test.sh +153 -0
  64. package/skills/qualia-discuss/SKILL.md +0 -222
@@ -75,17 +75,17 @@ Fixed scope for every project. Do not reassign these elsewhere.
75
75
 
76
76
  ## Post-Handoff (v2)
77
77
 
78
- Features acknowledged but deferred past initial handoff. Not in current journey.
78
+ **Only** capabilities the client EXPLICITLY deferred in discovery §8 (Out of Scope) belong here. This is NOT an overflow bucket for a milestone cap — there is no cap; everything in the capability inventory (discovery §9) must be a REQ-ID mapped to a milestone above. If a capability is agreed but absent from the arc, that's a roadmap bug, not a v2 item.
79
79
 
80
80
  ### {Category}
81
81
 
82
- - **{CAT}-XX**: {capability}
82
+ - **{CAT}-XX**: {capability} — deferred by client (discovery §8)
83
83
 
84
84
  ---
85
85
 
86
86
  ## Out of Scope
87
87
 
88
- Explicit exclusions with reasoning. Prevents scope creep.
88
+ Explicit exclusions with reasoning, drawn from discovery §8. Prevents scope creep.
89
89
 
90
90
  | Feature | Reason |
91
91
  |---------|--------|
@@ -95,16 +95,16 @@ Explicit exclusions with reasoning. Prevents scope creep.
95
95
 
96
96
  ## Traceability
97
97
 
98
- Populated during roadmap creation. Every v1 requirement maps to exactly one milestone + phase.
98
+ Populated during roadmap creation. Every capability from discovery §9 maps to exactly one milestone + phase. Coverage of the full inventory — not a v1 slice — is the gate.
99
99
 
100
100
  | Requirement | Milestone | Phase | Status |
101
101
  |-------------|-----------|-------|--------|
102
102
  | {CAT}-01 | M1: {name} | Phase {N} | Pending |
103
103
 
104
- **Coverage:**
105
- - v1 requirements (all feature milestones + Handoff): {X} total
104
+ **Coverage (must be 100% of the §9 capability inventory):**
105
+ - Agreed capabilities (discovery §9, whole project): {X} total
106
106
  - Mapped to milestones + phases: {Y}
107
- - Unmapped: {Z}
107
+ - Unmapped: {Z} ← MUST be 0 before the journey-approval gate passes
108
108
 
109
109
  ---
110
110
 
@@ -0,0 +1,153 @@
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
+ # --- barrier --tasks (explicit batch gate, no contract needed; R16 wave-plan) ---
118
+ TMP=$(mktemp -d)
119
+ $NODE "$AS" write T1 DONE --commit a --cwd "$TMP" >/dev/null 2>&1
120
+ $NODE "$AS" write T2 RUNNING --cwd "$TMP" >/dev/null 2>&1
121
+ $NODE "$AS" write T5 DONE --commit b --cwd "$TMP" >/dev/null 2>&1
122
+ # batch {T1,T5} both DONE → pass, with no contract file present
123
+ $NODE "$AS" barrier --tasks T1,T5 --cwd "$TMP" >/dev/null 2>&1
124
+ assert_exit "barrier --tasks all DONE → pass (no contract)" 0 $?
125
+ # batch {T1,T2} → T2 RUNNING → hold
126
+ $NODE "$AS" barrier --tasks T1,T2 --cwd "$TMP" >/dev/null 2>&1
127
+ assert_exit "barrier --tasks with a RUNNING member → hold" 1 $?
128
+ OUT=$($NODE "$AS" barrier --tasks T1,T2 --cwd "$TMP" 2>&1)
129
+ assert_contains "barrier --tasks scope label" "$OUT" "batch T1,T2"
130
+ rm -rf "$TMP"
131
+
132
+ # --- list + clear ---
133
+ TMP=$(mktemp -d)
134
+ $NODE "$AS" write T1 DONE --cwd "$TMP" >/dev/null 2>&1
135
+ $NODE "$AS" write T2 RUNNING --cwd "$TMP" >/dev/null 2>&1
136
+ OUT=$($NODE "$AS" list --cwd "$TMP" 2>&1)
137
+ assert_contains "list shows T1" "$OUT" "T1"
138
+ assert_contains "list shows T2" "$OUT" "T2"
139
+ $NODE "$AS" clear --cwd "$TMP" >/dev/null 2>&1
140
+ [ ! -d "$TMP/.agent-status" ] && { echo " ✓ clear removes status dir"; PASS=$((PASS+1)); } || { echo " ✗ clear left status dir"; FAIL=$((FAIL+1)); }
141
+ rm -rf "$TMP"
142
+
143
+ # --- buildActive library signal (used by R1's pre-write guard) ---
144
+ TMP=$(mktemp -d)
145
+ ACTIVE=$($NODE -e "const a=require('$AS'); a.writeStatus('$TMP',{task:'T1',status:'RUNNING'}); console.log(a.buildActive('$TMP'))" 2>&1)
146
+ assert_contains "buildActive true while RUNNING" "$ACTIVE" "true"
147
+ IDLE=$($NODE -e "const a=require('$AS'); a.writeStatus('$TMP',{task:'T1',status:'DONE'}); console.log(a.buildActive('$TMP'))" 2>&1)
148
+ assert_contains "buildActive false when none RUNNING" "$IDLE" "false"
149
+ rm -rf "$TMP"
150
+
151
+ echo ""
152
+ echo "=== Results: $PASS passed, $FAIL failed ==="
153
+ [ "$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
@@ -0,0 +1,93 @@
1
+ #!/bin/bash
2
+ # branch-hygiene.test.sh — bin/branch-hygiene.js (clock-out stranded-branch sweep)
3
+ # Run: bash tests/branch-hygiene.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ BH="$BIN_DIR/branch-hygiene.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 -qF "$needle"; then echo " ✓ $name"; PASS=$((PASS+1));
19
+ else echo " ✗ $name (missing '$needle' in: $hay)"; FAIL=$((FAIL+1)); fi
20
+ }
21
+
22
+ # fresh repo on main with one commit; prints the dir (caller rm -rf)
23
+ setup_repo() {
24
+ local tmp
25
+ tmp=$(mktemp -d)
26
+ (cd "$tmp" \
27
+ && git init -q \
28
+ && git checkout -q -b main 2>/dev/null \
29
+ && git config user.email t@t.com && git config user.name T \
30
+ && echo seed > seed.txt && git add seed.txt && git commit -q -m seed)
31
+ echo "$tmp"
32
+ }
33
+
34
+ echo "branch-hygiene.test.sh — bin/branch-hygiene.js"
35
+ echo ""
36
+
37
+ $NODE -c "$BH" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
38
+
39
+ # --- not a git repo → exit 2 ---
40
+ TMP=$(mktemp -d)
41
+ (cd "$TMP" && $NODE "$BH" >/dev/null 2>&1)
42
+ assert_exit "not a git repo → exit 2" 2 $?
43
+ rm -rf "$TMP"
44
+
45
+ # --- clean: only main → exit 0 ---
46
+ TMP=$(setup_repo)
47
+ (cd "$TMP" && $NODE "$BH" >/dev/null 2>&1)
48
+ assert_exit "clean repo (main only) → exit 0" 0 $?
49
+ OUT=$(cd "$TMP" && $NODE "$BH" 2>&1)
50
+ assert_contains "reports clean" "$OUT" "clean"
51
+ rm -rf "$TMP"
52
+
53
+ # --- stranded feature branch ahead of main → exit 1, listed ---
54
+ TMP=$(setup_repo)
55
+ (cd "$TMP" && git checkout -q -b feat/stranded && echo work > w.txt && git add w.txt && git commit -q -m "wip work")
56
+ (cd "$TMP" && $NODE "$BH" >/dev/null 2>&1)
57
+ assert_exit "branch ahead of main → exit 1" 1 $?
58
+ OUT=$(cd "$TMP" && $NODE "$BH" 2>&1)
59
+ assert_contains "lists the stranded branch" "$OUT" "feat/stranded"
60
+ assert_contains "shows commits ahead" "$OUT" "+1 commit"
61
+ # json shape
62
+ OUT=$(cd "$TMP" && $NODE "$BH" --json 2>&1)
63
+ assert_contains "json stranded entry" "$OUT" '"branch": "feat/stranded"'
64
+ assert_contains "json ahead count" "$OUT" '"ahead": 1'
65
+ rm -rf "$TMP"
66
+
67
+ # --- once integrated (ff-merged) into main → no longer stranded → exit 0 ---
68
+ TMP=$(setup_repo)
69
+ (cd "$TMP" && git checkout -q -b feat/done && echo x > x.txt && git add x.txt && git commit -q -m "done work")
70
+ (cd "$TMP" && git checkout -q main && git merge -q --ff-only feat/done)
71
+ (cd "$TMP" && $NODE "$BH" >/dev/null 2>&1)
72
+ assert_exit "ff-merged branch no longer stranded → exit 0" 0 $?
73
+ rm -rf "$TMP"
74
+
75
+ # --- master as the base branch is detected ---
76
+ TMP=$(mktemp -d)
77
+ (cd "$TMP" && git init -q && git checkout -q -b master 2>/dev/null && git config user.email t@t.com && git config user.name T && echo s > s.txt && git add s.txt && git commit -q -m s)
78
+ (cd "$TMP" && git checkout -q -b feature && echo y > y.txt && git add y.txt && git commit -q -m y)
79
+ OUT=$(cd "$TMP" && $NODE "$BH" --json 2>&1)
80
+ assert_contains "detects master as base" "$OUT" '"base": "master"'
81
+ assert_contains "stranded vs master" "$OUT" '"branch": "feature"'
82
+ rm -rf "$TMP"
83
+
84
+ # --- library: analyze() returns structured result ---
85
+ TMP=$(setup_repo)
86
+ (cd "$TMP" && git checkout -q -b b1 && echo a>a && git add a && git commit -q -m a)
87
+ RES=$($NODE -e "console.log(JSON.stringify(require('$BH').analyze('$TMP').stranded.length))" 2>&1)
88
+ assert_contains "analyze() finds 1 stranded" "$RES" "1"
89
+ rm -rf "$TMP"
90
+
91
+ echo ""
92
+ echo "=== Results: $PASS passed, $FAIL failed ==="
93
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,147 @@
1
+ #!/bin/bash
2
+ # eval-runner.test.sh — bin/eval-runner.js (layered AI-feature eval, R7)
3
+ # Run: bash tests/eval-runner.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ ER="$BIN_DIR/eval-runner.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 -qF "$needle"; then echo " ✓ $name"; PASS=$((PASS+1));
19
+ else echo " ✗ $name (missing '$needle' in: $hay)"; FAIL=$((FAIL+1)); fi
20
+ }
21
+
22
+ echo "eval-runner.test.sh — bin/eval-runner.js"
23
+ echo ""
24
+
25
+ $NODE -c "$ER" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
26
+
27
+ # --- all deterministic assertions pass → PASS ---
28
+ TMP=$(mktemp -d)
29
+ cat > "$TMP/s.json" <<'EOF'
30
+ { "feature":"support-chat", "cases":[
31
+ { "name":"refund", "output":"We refund within 30 days.", "latency_ms":1200, "cost_usd":0.008,
32
+ "assert":[
33
+ {"type":"contains","value":"30 days"},
34
+ {"type":"not_contains","value":"I cannot help"},
35
+ {"type":"regex","value":"\\b30\\b"},
36
+ {"type":"min_length","value":5},
37
+ {"type":"max_latency_ms","value":2000},
38
+ {"type":"max_cost_usd","value":0.02}
39
+ ] } ] }
40
+ EOF
41
+ $NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
42
+ assert_exit "all deterministic pass → exit 0" 0 $?
43
+ OUT=$($NODE "$ER" "$TMP/s.json" 2>&1)
44
+ assert_contains "reports EVAL PASS" "$OUT" "EVAL PASS"
45
+ rm -rf "$TMP"
46
+
47
+ # --- a failing contains → FAIL with the failing assertion shown ---
48
+ TMP=$(mktemp -d)
49
+ cat > "$TMP/s.json" <<'EOF'
50
+ { "feature":"chat", "cases":[
51
+ { "name":"refusal", "output":"I cannot help with that.",
52
+ "assert":[ {"type":"contains","value":"30 days"}, {"type":"not_contains","value":"I cannot help"} ] } ] }
53
+ EOF
54
+ $NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
55
+ assert_exit "failing assertion → exit 1" 1 $?
56
+ OUT=$($NODE "$ER" "$TMP/s.json" 2>&1)
57
+ assert_contains "shows failing case" "$OUT" "refusal"
58
+ rm -rf "$TMP"
59
+
60
+ # --- latency over budget → FAIL ---
61
+ TMP=$(mktemp -d)
62
+ cat > "$TMP/s.json" <<'EOF'
63
+ { "feature":"chat", "cases":[
64
+ { "name":"slow", "output":"ok", "latency_ms":5000, "assert":[ {"type":"max_latency_ms","value":2000} ] } ] }
65
+ EOF
66
+ $NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
67
+ assert_exit "latency over budget → exit 1" 1 $?
68
+ # missing latency metric but asserted → fail (no silent pass)
69
+ cat > "$TMP/s2.json" <<'EOF'
70
+ { "feature":"chat", "cases":[ { "name":"no-metric", "output":"ok", "assert":[ {"type":"max_latency_ms","value":2000} ] } ] }
71
+ EOF
72
+ $NODE "$ER" "$TMP/s2.json" >/dev/null 2>&1
73
+ assert_exit "asserting latency with none recorded → exit 1" 1 $?
74
+ rm -rf "$TMP"
75
+
76
+ # --- json_path + json_valid ---
77
+ TMP=$(mktemp -d)
78
+ cat > "$TMP/s.json" <<'EOF'
79
+ { "feature":"rag", "cases":[
80
+ { "name":"structured", "output":"{\"answer\":\"yes\",\"sources\":[{\"id\":\"doc1\"}]}",
81
+ "assert":[
82
+ {"type":"json_valid"},
83
+ {"type":"json_path","path":"answer","equals":"yes"},
84
+ {"type":"json_path","path":"sources.0.id","contains":"doc"}
85
+ ] } ] }
86
+ EOF
87
+ $NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
88
+ assert_exit "json_path + json_valid pass → exit 0" 0 $?
89
+ # invalid json under json_valid → fail
90
+ cat > "$TMP/bad.json" <<'EOF'
91
+ { "feature":"rag", "cases":[ { "name":"notjson", "output":"not json", "assert":[ {"type":"json_valid"} ] } ] }
92
+ EOF
93
+ $NODE "$ER" "$TMP/bad.json" >/dev/null 2>&1
94
+ assert_exit "json_valid on non-json → exit 1" 1 $?
95
+ rm -rf "$TMP"
96
+
97
+ # --- llm_rubric: verdict pass / fail / pending ---
98
+ TMP=$(mktemp -d)
99
+ cat > "$TMP/pass.json" <<'EOF'
100
+ { "feature":"chat", "cases":[ { "name":"r", "output":"grounded answer", "assert":[ {"type":"llm_rubric","rubric":"grounded","verdict":"pass"} ] } ] }
101
+ EOF
102
+ $NODE "$ER" "$TMP/pass.json" >/dev/null 2>&1
103
+ assert_exit "llm_rubric verdict=pass → exit 0" 0 $?
104
+ cat > "$TMP/fail.json" <<'EOF'
105
+ { "feature":"chat", "cases":[ { "name":"r", "output":"hallucination", "assert":[ {"type":"llm_rubric","rubric":"grounded","verdict":"fail"} ] } ] }
106
+ EOF
107
+ $NODE "$ER" "$TMP/fail.json" >/dev/null 2>&1
108
+ assert_exit "llm_rubric verdict=fail → exit 1" 1 $?
109
+ cat > "$TMP/pending.json" <<'EOF'
110
+ { "feature":"chat", "cases":[ { "name":"r", "output":"x", "assert":[ {"type":"llm_rubric","rubric":"grounded"} ] } ] }
111
+ EOF
112
+ $NODE "$ER" "$TMP/pending.json" >/dev/null 2>&1
113
+ assert_exit "unjudged llm_rubric → PENDING → exit 1 (no silent pass)" 1 $?
114
+ OUT=$($NODE "$ER" "$TMP/pending.json" 2>&1)
115
+ assert_contains "reports unjudged rubric count" "$OUT" "unjudged"
116
+ rm -rf "$TMP"
117
+
118
+ # --- output_file resolution + --write artifact ---
119
+ TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
120
+ echo -n "the answer is 42" > "$TMP/out.txt"
121
+ cat > "$TMP/s.json" <<'EOF'
122
+ { "feature":"file-feature", "cases":[ { "name":"fromfile", "output_file":"out.txt", "assert":[ {"type":"contains","value":"42"} ] } ] }
123
+ EOF
124
+ (cd "$TMP" && $NODE "$ER" s.json --write >/dev/null 2>&1)
125
+ assert_exit "output_file case passes → exit 0" 0 $?
126
+ [ -f "$TMP/.planning/evals/eval-file-feature.json" ] && { echo " ✓ --write emits eval artifact"; PASS=$((PASS+1)); } || { echo " ✗ no eval artifact"; FAIL=$((FAIL+1)); }
127
+ # missing output_file → fail, not crash
128
+ cat > "$TMP/miss.json" <<'EOF'
129
+ { "feature":"x", "cases":[ { "name":"gone", "output_file":"nope.txt", "assert":[ {"type":"contains","value":"y"} ] } ] }
130
+ EOF
131
+ (cd "$TMP" && $NODE "$ER" miss.json >/dev/null 2>&1)
132
+ assert_exit "missing output_file → exit 1 (graceful)" 1 $?
133
+ rm -rf "$TMP"
134
+
135
+ # --- library: runAssertion unit ---
136
+ U=$($NODE -e "const e=require('$ER'); console.log(e.runAssertion({type:'contains',value:'hi'},{output:'oh hi there'}).ok)" 2>&1)
137
+ assert_contains "runAssertion contains true" "$U" "true"
138
+ U=$($NODE -e "const e=require('$ER'); console.log(e.getPath({a:{b:[{c:9}]}},'a.b.0.c'))" 2>&1)
139
+ assert_contains "getPath nested+index" "$U" "9"
140
+
141
+ # --- malformed suite → exit 2 ---
142
+ $NODE "$ER" /nonexistent-suite.json >/dev/null 2>&1
143
+ assert_exit "missing suite → exit 2" 2 $?
144
+
145
+ echo ""
146
+ echo "=== Results: $PASS passed, $FAIL failed ==="
147
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1