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.
@@ -96,29 +96,78 @@ TMP=$(setup_guard_repo main OWNER)
96
96
  assert_exit "OWNER on main → allowed" 0 $?
97
97
  rm -rf "$TMP"
98
98
 
99
- # EMPLOYEE on main → blocked (exit 2)
99
+ # EMPLOYEE on main → ALLOWED + counted + notice (v6.10 policy: accountability, not block)
100
100
  TMP=$(setup_guard_repo main EMPLOYEE)
101
101
  OUT=$(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" 2>&1)
102
102
  RC=$?
103
- if [ "$RC" -eq 2 ] && echo "$OUT" | grep -q "BLOCKED" && echo "$OUT" | grep -q "main"; then
104
- echo " EMPLOYEE on main blocked (BLOCKED in stdout)"
103
+ if [ "$RC" -eq 0 ] \
104
+ && echo "$OUT" | grep -q "NOTICE" \
105
+ && grep -q "employee_main_push" "$TMP/.claude/.main-push-events.json" 2>/dev/null \
106
+ && grep -q '"total": 1' "$TMP/.claude/.main-push-events.json" 2>/dev/null; then
107
+ echo " ✓ EMPLOYEE on main → allowed + recorded + notice"
108
+ PASS=$((PASS + 1))
109
+ else
110
+ echo " ✗ EMPLOYEE on main → allowed+recorded (exit=$RC out=$OUT)"
111
+ FAIL=$((FAIL + 1))
112
+ fi
113
+ rm -rf "$TMP"
114
+
115
+ # EMPLOYEE repeated main push → per-employee count increments (framework counts)
116
+ TMP=$(setup_guard_repo main EMPLOYEE)
117
+ (cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
118
+ (cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
119
+ if grep -q '"total": 2' "$TMP/.claude/.main-push-events.json" 2>/dev/null; then
120
+ echo " ✓ EMPLOYEE repeated main push → count increments to 2"
121
+ PASS=$((PASS + 1))
122
+ else
123
+ echo " ✗ EMPLOYEE repeated main push → count increments"
124
+ FAIL=$((FAIL + 1))
125
+ fi
126
+ rm -rf "$TMP"
127
+
128
+ # EMPLOYEE refspec push to main from a feature branch → recorded
129
+ TMP=$(setup_guard_repo feature/xyz EMPLOYEE)
130
+ OUT=$(cd "$TMP/proj" && printf '%s' '{"tool_input":{"command":"git push origin feature/xyz:main"}}' | HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" 2>&1)
131
+ RC=$?
132
+ if [ "$RC" -eq 0 ] && grep -q "employee_main_push" "$TMP/.claude/.main-push-events.json" 2>/dev/null; then
133
+ echo " ✓ EMPLOYEE refspec push to :main → allowed + recorded"
105
134
  PASS=$((PASS + 1))
106
135
  else
107
- echo " ✗ EMPLOYEE on main → blocked (exit=$RC)"
136
+ echo " ✗ EMPLOYEE refspec push to :main → recorded (exit=$RC)"
108
137
  FAIL=$((FAIL + 1))
109
138
  fi
110
139
  rm -rf "$TMP"
111
140
 
112
- # EMPLOYEE on master → blocked
141
+ # EMPLOYEE on master → allowed + recorded
113
142
  TMP=$(setup_guard_repo master EMPLOYEE)
114
143
  (cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
115
- assert_exit "EMPLOYEE on master → blocked" 2 $?
144
+ assert_exit "EMPLOYEE on master → allowed (recorded)" 0 $?
116
145
  rm -rf "$TMP"
117
146
 
118
- # EMPLOYEE on feature branch → allowed
147
+ # EMPLOYEE on feature branch → allowed, NOT recorded
119
148
  TMP=$(setup_guard_repo feature/xyz EMPLOYEE)
120
149
  (cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
121
- assert_exit "EMPLOYEE on feature/xyz → allowed" 0 $?
150
+ RC=$?
151
+ if [ "$RC" -eq 0 ] && [ ! -f "$TMP/.claude/.main-push-events.json" ]; then
152
+ echo " ✓ EMPLOYEE on feature/xyz → allowed, not recorded"
153
+ PASS=$((PASS + 1))
154
+ else
155
+ echo " ✗ EMPLOYEE on feature/xyz → allowed/not-recorded (exit=$RC)"
156
+ FAIL=$((FAIL + 1))
157
+ fi
158
+ rm -rf "$TMP"
159
+
160
+ # OWNER on main → allowed, NOT recorded (OWNER pushes are unremarkable)
161
+ TMP=$(setup_guard_repo main OWNER)
162
+ (cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
163
+ RC=$?
164
+ if [ "$RC" -eq 0 ] && [ ! -f "$TMP/.claude/.main-push-events.json" ]; then
165
+ echo " ✓ OWNER on main → allowed, not recorded"
166
+ PASS=$((PASS + 1))
167
+ else
168
+ echo " ✗ OWNER on main → allowed/not-recorded (exit=$RC)"
169
+ FAIL=$((FAIL + 1))
170
+ fi
122
171
  rm -rf "$TMP"
123
172
 
124
173
  # OWNER on feature branch → allowed
@@ -127,31 +176,38 @@ TMP=$(setup_guard_repo feature/xyz OWNER)
127
176
  assert_exit "OWNER on feature/xyz → allowed" 0 $?
128
177
  rm -rf "$TMP"
129
178
 
130
- # Missing config → fails closed (block, exit 2)
179
+ # Missing config → allowed (the hook never blocks now)
131
180
  TMP=$(mktemp -d)
132
181
  mkdir -p "$TMP/proj"
133
- (cd "$TMP/proj" && git init -q && git checkout -b feature/x -q 2>/dev/null)
182
+ (cd "$TMP/proj" && git init -q && git checkout -b main -q 2>/dev/null)
134
183
  # NO .claude/.qualia-config.json
135
184
  (cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
136
- assert_exit "missing config → blocked (fails closed)" 2 $?
185
+ assert_exit "missing config → allowed (never blocks)" 0 $?
137
186
  rm -rf "$TMP"
138
187
 
139
- # Malformed config JSON → fails closed
188
+ # Malformed config JSON → allowed
140
189
  TMP=$(mktemp -d)
141
190
  mkdir -p "$TMP/proj" "$TMP/.claude"
142
- (cd "$TMP/proj" && git init -q && git checkout -b feature/x -q 2>/dev/null)
191
+ (cd "$TMP/proj" && git init -q && git checkout -b main -q 2>/dev/null)
143
192
  echo 'not json{' > "$TMP/.claude/.qualia-config.json"
144
193
  (cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
145
- assert_exit "malformed config JSON → blocked" 2 $?
194
+ assert_exit "malformed config JSON → allowed" 0 $?
146
195
  rm -rf "$TMP"
147
196
 
148
- # Empty role field → fails closed
197
+ # Empty role field → allowed, not recorded (not a known EMPLOYEE)
149
198
  TMP=$(mktemp -d)
150
199
  mkdir -p "$TMP/proj" "$TMP/.claude"
151
- (cd "$TMP/proj" && git init -q && git checkout -b feature/x -q 2>/dev/null)
200
+ (cd "$TMP/proj" && git init -q && git checkout -b main -q 2>/dev/null)
152
201
  echo '{"role":""}' > "$TMP/.claude/.qualia-config.json"
153
202
  (cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
154
- assert_exit "empty role field → blocked" 2 $?
203
+ RC=$?
204
+ if [ "$RC" -eq 0 ] && [ ! -f "$TMP/.claude/.main-push-events.json" ]; then
205
+ echo " ✓ empty role → allowed, not recorded"
206
+ PASS=$((PASS + 1))
207
+ else
208
+ echo " ✗ empty role → allowed/not-recorded (exit=$RC)"
209
+ FAIL=$((FAIL + 1))
210
+ fi
155
211
  rm -rf "$TMP"
156
212
 
157
213
  # --- fawzi-approval-guard.js ---
@@ -382,6 +438,72 @@ RC=$?
382
438
  assert_exit "regular page.tsx with service_role → blocked (exit 2)" 2 $RC
383
439
  rm -rf "$TMP"
384
440
 
441
+ # --- pre-deploy-gate: anti-slop gate (slop-detect.mjs wired into ship) ---
442
+ # Build a tmp QUALIA_HOME carrying bin/slop-detect.mjs so the gate can find it.
443
+ SLOP_SRC="$(cd "$(dirname "$0")/../bin" && pwd)/slop-detect.mjs"
444
+
445
+ # Purple-blue gradient (CRITICAL design tell) → blocked with anti-slop diagnostic
446
+ TMP=$(mktemp -d)
447
+ QH=$(mktemp -d)
448
+ mkdir -p "$QH/bin" "$TMP/app"
449
+ cp "$SLOP_SRC" "$QH/bin/slop-detect.mjs"
450
+ echo 'export default function P(){return <div className="bg-gradient-to-r from-blue-500 to-purple-600">hi</div>;}' > "$TMP/app/page.tsx"
451
+ OUT=$( (cd "$TMP" && QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
452
+ RC=$?
453
+ if [ "$RC" -eq 2 ] && echo "$OUT" | grep -qi "anti-slop"; then
454
+ echo " ✓ purple-blue gradient → blocked by anti-slop gate (exit 2)"
455
+ PASS=$((PASS + 1))
456
+ else
457
+ echo " ✗ purple-blue gradient → anti-slop block (exit=$RC out=$OUT)"
458
+ FAIL=$((FAIL + 1))
459
+ fi
460
+ rm -rf "$TMP" "$QH"
461
+
462
+ # Clean frontend file → anti-slop passes → exit 0 with "Anti-slop" check shown
463
+ TMP=$(mktemp -d)
464
+ QH=$(mktemp -d)
465
+ mkdir -p "$QH/bin" "$TMP/app"
466
+ cp "$SLOP_SRC" "$QH/bin/slop-detect.mjs"
467
+ echo 'export default function P(){return <div className="bg-stone-50 text-ink">hi</div>;}' > "$TMP/app/page.tsx"
468
+ OUT=$( (cd "$TMP" && QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
469
+ RC=$?
470
+ if [ "$RC" -eq 0 ] && echo "$OUT" | grep -q "Anti-slop"; then
471
+ echo " ✓ clean frontend → anti-slop passes (exit 0)"
472
+ PASS=$((PASS + 1))
473
+ else
474
+ echo " ✗ clean frontend → anti-slop passes (exit=$RC out=$OUT)"
475
+ FAIL=$((FAIL + 1))
476
+ fi
477
+ rm -rf "$TMP" "$QH"
478
+
479
+ # QUALIA_SKIP_SLOP=1 by non-OWNER → blocked (OWNER-only escape)
480
+ TMP=$(mktemp -d)
481
+ QH=$(mktemp -d)
482
+ mkdir -p "$QH/bin" "$TMP/app"
483
+ cp "$SLOP_SRC" "$QH/bin/slop-detect.mjs"
484
+ echo '{"role":"EMPLOYEE"}' > "$QH/.qualia-config.json"
485
+ echo 'export default function P(){return <div className="bg-gradient-to-r from-blue-500 to-purple-600">x</div>;}' > "$TMP/app/page.tsx"
486
+ OUT=$( (cd "$TMP" && QUALIA_SKIP_SLOP=1 QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
487
+ RC=$?
488
+ if [ "$RC" -eq 2 ] && echo "$OUT" | grep -q "OWNER-only"; then
489
+ echo " ✓ QUALIA_SKIP_SLOP by non-OWNER → blocked (OWNER-only)"
490
+ PASS=$((PASS + 1))
491
+ else
492
+ echo " ✗ QUALIA_SKIP_SLOP non-OWNER block (exit=$RC out=$OUT)"
493
+ FAIL=$((FAIL + 1))
494
+ fi
495
+ rm -rf "$TMP" "$QH"
496
+
497
+ # Scanner absent (older install) → gate skips silently → exit 0
498
+ TMP=$(mktemp -d)
499
+ QH=$(mktemp -d)
500
+ mkdir -p "$TMP/app"
501
+ echo 'export default function P(){return <div className="bg-gradient-to-r from-blue-500 to-purple-600">x</div>;}' > "$TMP/app/page.tsx"
502
+ OUT=$( (cd "$TMP" && QUALIA_HOME="$QH" $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
503
+ RC=$?
504
+ assert_exit "anti-slop gate skips when scanner absent (exit 0)" 0 $RC
505
+ rm -rf "$TMP" "$QH"
506
+
385
507
  # --- session-start.js — must exit 0 always ---
386
508
  echo ""
387
509
  echo "session-start:"
@@ -595,6 +717,85 @@ else
595
717
  fi
596
718
  rm -rf "$TMP"
597
719
 
720
+ # --- task-write-guard.js (R1 — runtime plan-contract file-scope guard) ---
721
+ echo ""
722
+ echo "task-write-guard:"
723
+
724
+ AS_BIN="$(cd "$(dirname "$0")/../bin" && pwd)/agent-status.js"
725
+
726
+ # setup_twg_project → tmp dir with a phase-1 contract (T1 declares src/a.ts +
727
+ # src/b.ts) and an active RUNNING status for T1. Prints the dir; caller rm -rf.
728
+ setup_twg_project() {
729
+ local tmp
730
+ tmp=$(mktemp -d)
731
+ mkdir -p "$tmp/.planning"
732
+ cat > "$tmp/.planning/phase-1-contract.json" <<'EOF'
733
+ {
734
+ "version": 1,
735
+ "phase": 1,
736
+ "tasks": [
737
+ { "id": "T1", "wave": 1, "files_modify": ["src/a.ts"], "files_create": ["src/b.ts"] }
738
+ ]
739
+ }
740
+ EOF
741
+ echo "$tmp"
742
+ }
743
+ twg() { echo "{\"tool_input\":{\"file_path\":\"$2\"}}" | (cd "$1" && $NODE "$HOOKS_DIR/task-write-guard.js" >/dev/null 2>&1); }
744
+
745
+ # No active build → guard is a no-op even on an undeclared file
746
+ TMP=$(setup_twg_project)
747
+ twg "$TMP" "src/evil.ts"
748
+ assert_exit "no active build → allowed (no-op)" 0 $?
749
+ rm -rf "$TMP"
750
+
751
+ # Active build (T1 RUNNING) + write to a DECLARED file → allowed
752
+ TMP=$(setup_twg_project)
753
+ $NODE "$AS_BIN" write T1 RUNNING --phase 1 --wave 1 --cwd "$TMP" >/dev/null 2>&1
754
+ twg "$TMP" "src/a.ts"
755
+ assert_exit "active build + declared file → allowed" 0 $?
756
+
757
+ # Active build + write to an UNDECLARED file → blocked (exit 2)
758
+ OUT=$(echo '{"tool_input":{"file_path":"src/evil.ts"}}' | (cd "$TMP" && $NODE "$HOOKS_DIR/task-write-guard.js" 2>&1))
759
+ RC=$?
760
+ if [ "$RC" -eq 2 ] && echo "$OUT" | grep -q "task-write-guard" && echo "$OUT" | grep -q "evil.ts"; then
761
+ echo " ✓ active build + undeclared file → blocked with diagnostic"
762
+ PASS=$((PASS + 1))
763
+ else
764
+ echo " ✗ active build + undeclared file → blocked (exit=$RC out=$OUT)"
765
+ FAIL=$((FAIL + 1))
766
+ fi
767
+
768
+ # files_create entries count as declared
769
+ twg "$TMP" "src/b.ts"
770
+ assert_exit "active build + declared files_create → allowed" 0 $?
771
+
772
+ # .planning/ and .agent-status/ paths are always writable during a build
773
+ twg "$TMP" ".planning/phase-1-verification.md"
774
+ assert_exit "active build + .planning path → allowed" 0 $?
775
+ twg "$TMP" ".agent-status/T1.json"
776
+ assert_exit "active build + .agent-status path → allowed" 0 $?
777
+
778
+ # OWNER escape hatch overrides the block
779
+ echo '{"tool_input":{"file_path":"src/evil.ts"}}' | (cd "$TMP" && QUALIA_ALLOW_OUTSIDE_CONTRACT=1 $NODE "$HOOKS_DIR/task-write-guard.js" >/dev/null 2>&1)
780
+ assert_exit "QUALIA_ALLOW_OUTSIDE_CONTRACT=1 → allowed despite undeclared" 0 $?
781
+
782
+ # Absolute path to an undeclared file still blocks
783
+ echo "{\"tool_input\":{\"file_path\":\"$TMP/src/evil.ts\"}}" | (cd "$TMP" && $NODE "$HOOKS_DIR/task-write-guard.js" >/dev/null 2>&1)
784
+ assert_exit "active build + absolute undeclared path → blocked" 2 $?
785
+
786
+ # Once T1 flips to DONE (no RUNNING), the guard goes quiet again
787
+ $NODE "$AS_BIN" write T1 DONE --commit abc --cwd "$TMP" >/dev/null 2>&1
788
+ twg "$TMP" "src/evil.ts"
789
+ assert_exit "build finished (no RUNNING) → allowed" 0 $?
790
+ rm -rf "$TMP"
791
+
792
+ # Active build but no contract present → fail open (allowed)
793
+ TMP=$(mktemp -d)
794
+ $NODE "$AS_BIN" write T1 RUNNING --phase 1 --cwd "$TMP" >/dev/null 2>&1
795
+ twg "$TMP" "src/anything.ts"
796
+ assert_exit "active build + no contract → allowed (fail open)" 0 $?
797
+ rm -rf "$TMP"
798
+
598
799
  echo ""
599
800
  echo "=== Results: $PASS passed, $FAIL failed ==="
600
801
  [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -124,11 +124,12 @@ else
124
124
  fi
125
125
 
126
126
  if [ -d "$HOME_DIR/.claude/hooks" ] \
127
- && [ "$(find "$HOME_DIR/.claude/hooks" -maxdepth 1 -name '*.js' | wc -l | tr -d ' ')" = "14" ] \
127
+ && [ "$(find "$HOME_DIR/.claude/hooks" -maxdepth 1 -name '*.js' | wc -l | tr -d ' ')" = "15" ] \
128
128
  && [ -f "$HOME_DIR/.claude/hooks/fawzi-approval-guard.js" ] \
129
129
  && [ -f "$HOME_DIR/.claude/hooks/pre-compact.js" ] \
130
- && [ -f "$HOME_DIR/.claude/hooks/usage-capture.js" ]; then
131
- pass "packaged install has 14 hooks including usage-capture (v6.9.1 telemetry)"
130
+ && [ -f "$HOME_DIR/.claude/hooks/usage-capture.js" ] \
131
+ && [ -f "$HOME_DIR/.claude/hooks/task-write-guard.js" ]; then
132
+ pass "packaged install has 15 hooks including task-write-guard (v6.13)"
132
133
  else
133
134
  fail_case "packaged hook set mismatch"
134
135
  fi
package/tests/lib.test.sh CHANGED
@@ -507,7 +507,7 @@ TMP=$(mktmp)
507
507
  mkdir -p "$TMP/home/.claude/bin" "$TMP/home/.claude/hooks" "$TMP/home/.claude/knowledge/daily-log" "$TMP/home/.claude/qualia-design" "$TMP/home/.claude/agents" "$TMP/home/.claude/qualia-templates" "$TMP/project"
508
508
  echo '{"installed_by":"Test","role":"OWNER","version":"6.3.0","erp":{"enabled":false}}' > "$TMP/home/.claude/.qualia-config.json"
509
509
  touch "$TMP/home/.claude/CLAUDE.md" "$TMP/home/.claude/settings.json"
510
- for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js state-ledger.js plan-contract.js contract-runner.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js work-packet.js report-payload.js project-snapshot.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
510
+ for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js work-packet.js report-payload.js project-snapshot.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
511
511
  touch "$TMP/home/.claude/bin/$f"
512
512
  done
513
513
  for h in session-start.js auto-update.js branch-guard.js pre-push.js pre-deploy-gate.js migration-guard.js git-guardrails.js stop-session-log.js fawzi-approval-guard.js vercel-account-guard.js env-empty-guard.js supabase-destructive-guard.js; do
@@ -622,7 +622,7 @@ TMP=$(mktmp)
622
622
  mkdir -p "$TMP/.claude/bin" "$TMP/.claude/hooks" "$TMP/.claude/knowledge/daily-log" "$TMP/.claude/qualia-design" "$TMP/.claude/agents" "$TMP/.claude/qualia-templates" "$TMP/project/.planning"
623
623
  echo '{"installed_by":"Test","role":"OWNER","erp":{"enabled":false}}' > "$TMP/.claude/.qualia-config.json"
624
624
  touch "$TMP/.claude/CLAUDE.md" "$TMP/.claude/settings.json"
625
- for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js state-ledger.js plan-contract.js contract-runner.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js work-packet.js report-payload.js project-snapshot.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
625
+ for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js work-packet.js report-payload.js project-snapshot.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
626
626
  touch "$TMP/.claude/bin/$f"
627
627
  done
628
628
  for h in session-start.js auto-update.js branch-guard.js pre-push.js pre-deploy-gate.js migration-guard.js git-guardrails.js stop-session-log.js fawzi-approval-guard.js vercel-account-guard.js env-empty-guard.js supabase-destructive-guard.js; do
package/tests/run-all.sh CHANGED
@@ -18,6 +18,8 @@ SUITES=(
18
18
  "refs"
19
19
  "install-smoke"
20
20
  "slop-detect"
21
+ "agent-status"
22
+ "analyze-gate"
21
23
  )
22
24
 
23
25
  FAILED=()
package/tests/runner.js CHANGED
@@ -2578,14 +2578,15 @@ describe("install.js", () => {
2578
2578
  }
2579
2579
  });
2580
2580
 
2581
- it("13 hooks installed (v6.3.2: pre-compact reintroduced with sidecar-snapshot mechanism)", () => {
2581
+ it("15 hooks installed (v6.13: task-write-guard added R1 plan-contract file-scope guard)", () => {
2582
2582
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2583
2583
  try {
2584
2584
  runInstall("QS-FAWZI-11", tmpHome);
2585
2585
  const hooks = fs.readdirSync(path.join(tmpHome, ".claude", "hooks")).filter(f => f.endsWith(".js"));
2586
- assert.equal(hooks.length, 13, `expected 13 hooks, got ${hooks.length}: ${hooks.join(", ")}`);
2586
+ assert.equal(hooks.length, 15, `expected 15 hooks, got ${hooks.length}: ${hooks.join(", ")}`);
2587
2587
  assert.ok(hooks.includes("fawzi-approval-guard.js"), "fawzi-approval-guard.js must be installed");
2588
2588
  assert.ok(hooks.includes("pre-compact.js"), "pre-compact.js must be installed (v6.3.2 sidecar-snapshot mechanism)");
2589
+ assert.ok(hooks.includes("task-write-guard.js"), "task-write-guard.js must be installed (v6.13 R1 guard)");
2589
2590
  } finally {
2590
2591
  fs.rmSync(tmpHome, { recursive: true, force: true });
2591
2592
  }
@@ -104,6 +104,12 @@ Goal: Test goal
104
104
  ## Success Criteria
105
105
  - [ ] Test passes
106
106
  PLAN
107
+ # v7 kernel: `planned` now requires a machine contract and `verified(pass)`
108
+ # requires passing machine evidence. Emit both so happy-path setups satisfy the
109
+ # preconditions; the few cases that exercise their ABSENCE delete them explicitly.
110
+ make_valid_contract "$dir" "$phase"
111
+ mkdir -p "$dir/.planning/evidence"
112
+ printf '{"ok":true,"checks":[]}\n' > "$dir/.planning/evidence/phase-${phase}-contract-run.json"
107
113
  }
108
114
 
109
115
  make_valid_contract() {
@@ -289,6 +295,9 @@ fi
289
295
  TMP=$(make_project)
290
296
  make_valid_plan "$TMP" 1
291
297
  make_valid_contract "$TMP" 1
298
+ # This case asserts the missing-evidence guard, so strip the evidence that
299
+ # make_valid_plan now writes by default.
300
+ rm -f "$TMP/.planning/evidence/phase-1-contract-run.json"
292
301
  (cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
293
302
  (cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
294
303
  echo "result: PASS" > "$TMP/.planning/phase-1-verification.md"
@@ -735,6 +744,9 @@ else
735
744
  fail_case "validate well-formed plan" "exit=$EXIT out=$OUT"
736
745
  fi
737
746
 
747
+ # This case asserts the missing-contract path, so strip the contract that
748
+ # make_valid_plan now writes by default.
749
+ rm -f "$TMP/.planning/phase-1-contract.json"
738
750
  OUT=$(cd "$TMP" && $NODE "$STATE_JS" validate-plan --phase 1 --require-contract 2>&1)
739
751
  EXIT=$?
740
752
  if [ "$EXIT" -eq 1 ] \
@@ -1417,6 +1429,89 @@ else
1417
1429
  fail_case "id traversal guard" "out=$TRAV"
1418
1430
  fi
1419
1431
 
1432
+ # ─── v7 lifecycle (build → operate) ──────────────────────
1433
+ echo ""
1434
+ echo "lifecycle (build/operate):"
1435
+
1436
+ # Helper: drive a fresh single-phase project to verified(pass).
1437
+ make_verified() {
1438
+ local dir="$1"
1439
+ make_valid_plan "$dir" 1
1440
+ (cd "$dir" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
1441
+ (cd "$dir" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
1442
+ echo "result: PASS" > "$dir/.planning/phase-1-verification.md"
1443
+ }
1444
+
1445
+ # 1. A new project defaults to lifecycle=build
1446
+ TMP=$(_mktemp_native); TMP_DIRS+=("$TMP")
1447
+ (cd "$TMP" && git init -q 2>/dev/null; $NODE "$STATE_JS" init --project acme --total-phases 1 --phases '[{"name":"Core","goal":"x"}]' --force >/dev/null 2>&1)
1448
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" check 2>&1)
1449
+ if echo "$OUT" | grep -q '"lifecycle": "build"'; then
1450
+ pass "init defaults to lifecycle=build"
1451
+ else
1452
+ fail_case "init lifecycle default" "out=$OUT"
1453
+ fi
1454
+
1455
+ # 2. launch flips to operate, stamps launched_at + source, routes to /qualia-update
1456
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" launch --deployed-url https://acme.app --source erp 2>&1)
1457
+ if echo "$OUT" | grep -q '"lifecycle": "operate"' \
1458
+ && echo "$OUT" | grep -q '"launch_source": "erp"' \
1459
+ && echo "$OUT" | grep -q '"next_command": "/qualia-update"' \
1460
+ && echo "$OUT" | grep -q '"launched_at"'; then
1461
+ pass "launch → operate (stamped, routes to /qualia-update)"
1462
+ else
1463
+ fail_case "launch to operate" "out=$OUT"
1464
+ fi
1465
+
1466
+ # 3. launch is idempotent
1467
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" launch 2>&1)
1468
+ if echo "$OUT" | grep -q '"already_launched": true'; then
1469
+ pass "launch is idempotent (already_launched)"
1470
+ else
1471
+ fail_case "launch idempotent" "out=$OUT"
1472
+ fi
1473
+
1474
+ # 4. operate: verified(pass) on the last phase routes to /qualia-update + bumps updates_completed
1475
+ TMP=$(_mktemp_native); TMP_DIRS+=("$TMP")
1476
+ (cd "$TMP" && git init -q 2>/dev/null; $NODE "$STATE_JS" init --project beta --total-phases 1 --phases '[{"name":"Core","goal":"x"}]' --force >/dev/null 2>&1)
1477
+ (cd "$TMP" && $NODE "$STATE_JS" launch >/dev/null 2>&1)
1478
+ make_verified "$TMP"
1479
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
1480
+ if echo "$OUT" | grep -q '"next_command": "/qualia-update"'; then
1481
+ pass "operate verified(pass) → /qualia-update"
1482
+ else
1483
+ fail_case "operate verified routing" "out=$OUT"
1484
+ fi
1485
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" check 2>&1)
1486
+ if echo "$OUT" | grep -q '"updates_completed": 1'; then
1487
+ pass "operate verified(pass) bumps lifetime.updates_completed"
1488
+ else
1489
+ fail_case "updates_completed bump" "out=$OUT"
1490
+ fi
1491
+
1492
+ # 5. BUILD mode still REQUIRES HANDOFF.md (regression: forced handoff intact in build)
1493
+ TMP=$(_mktemp_native); TMP_DIRS+=("$TMP")
1494
+ (cd "$TMP" && git init -q 2>/dev/null; $NODE "$STATE_JS" init --project gamma --total-phases 1 --phases '[{"name":"Core","goal":"x"}]' --force >/dev/null 2>&1)
1495
+ make_verified "$TMP"
1496
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
1497
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to polished >/dev/null 2>&1)
1498
+ (cd "$TMP" && $NODE "$STATE_JS" transition --to shipped --deployed-url https://x.app >/dev/null 2>&1)
1499
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to handed_off 2>&1)
1500
+ if echo "$OUT" | grep -q '"error": "MISSING_FILE"'; then
1501
+ pass "build mode → handed_off still requires HANDOFF.md"
1502
+ else
1503
+ fail_case "build handoff still gated" "out=$OUT"
1504
+ fi
1505
+
1506
+ # 6. OPERATE mode: handed_off allowed WITHOUT HANDOFF.md (forced handoff removed)
1507
+ (cd "$TMP" && $NODE "$STATE_JS" launch >/dev/null 2>&1) # same project → flip to operate
1508
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to handed_off 2>&1)
1509
+ if echo "$OUT" | grep -q '"status": "handed_off"' && echo "$OUT" | grep -q '"ok": true'; then
1510
+ pass "operate mode → handed_off allowed without HANDOFF.md"
1511
+ else
1512
+ fail_case "operate handoff ungated" "out=$OUT"
1513
+ fi
1514
+
1420
1515
  # ─── Summary ─────────────────────────────────────────────
1421
1516
  echo ""
1422
1517
  echo "=== Results: $PASS passed, $FAIL failed ==="