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.
- package/CHANGELOG.md +78 -0
- package/agents/verifier.md +1 -1
- package/bin/agent-status.js +251 -0
- package/bin/analyze-gate.js +318 -0
- package/bin/command-surface.js +1 -0
- package/bin/install.js +6 -4
- package/bin/report-payload.js +7 -0
- package/bin/runtime-manifest.js +2 -0
- package/bin/state.js +145 -11
- package/docs/EMPLOYEE-QUICKSTART.md +3 -3
- package/docs/erp-contract.md +23 -0
- package/docs/qualia-manual.html +5 -5
- package/hooks/branch-guard.js +133 -63
- package/hooks/pre-deploy-gate.js +38 -0
- package/hooks/task-write-guard.js +165 -0
- package/package.json +1 -1
- package/skills/qualia-build/SKILL.md +30 -1
- package/skills/qualia-ship/SKILL.md +3 -0
- package/skills/qualia-update/SKILL.md +96 -0
- package/skills/qualia-verify/SKILL.md +7 -1
- package/templates/journey.md +1 -1
- package/tests/agent-status.test.sh +138 -0
- package/tests/analyze-gate.test.sh +170 -0
- package/tests/bin.test.sh +5 -4
- package/tests/hooks.test.sh +218 -17
- package/tests/install-smoke.test.sh +4 -3
- package/tests/lib.test.sh +2 -2
- package/tests/run-all.sh +2 -0
- package/tests/runner.js +3 -2
- package/tests/state.test.sh +95 -0
- package/skills/qualia-discuss/SKILL.md +0 -222
|
@@ -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
|
-
|
|
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
|
package/templates/journey.md
CHANGED
|
@@ -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. **
|
|
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
|
|
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
|
|
498
|
-
pass "
|
|
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
|