qualia-framework 6.2.9 → 6.2.10

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 (72) hide show
  1. package/README.md +14 -11
  2. package/agents/builder.md +7 -7
  3. package/agents/planner.md +39 -3
  4. package/agents/research-synthesizer.md +1 -1
  5. package/agents/researcher.md +3 -3
  6. package/agents/roadmapper.md +7 -7
  7. package/agents/verifier.md +18 -6
  8. package/agents/visual-evaluator.md +8 -7
  9. package/bin/cli.js +111 -14
  10. package/bin/contract-runner.js +219 -0
  11. package/bin/host-adapters.js +66 -0
  12. package/bin/install.js +99 -152
  13. package/bin/plan-contract.js +99 -2
  14. package/bin/planning-hygiene.js +262 -0
  15. package/bin/runtime-manifest.js +32 -0
  16. package/bin/state-ledger.js +184 -0
  17. package/bin/state.js +299 -20
  18. package/bin/trust-score.js +276 -0
  19. package/docs/onboarding.html +5 -4
  20. package/guide.md +3 -2
  21. package/package.json +1 -1
  22. package/qualia-design/design-rubric.md +17 -5
  23. package/qualia-design/frontend.md +5 -1
  24. package/qualia-design/graphics.md +47 -0
  25. package/rules/command-output.md +35 -0
  26. package/skills/qualia/SKILL.md +10 -10
  27. package/skills/qualia-build/SKILL.md +20 -14
  28. package/skills/qualia-debug/SKILL.md +16 -8
  29. package/skills/qualia-discuss/SKILL.md +10 -10
  30. package/skills/qualia-doctor/SKILL.md +140 -0
  31. package/skills/qualia-feature/SKILL.md +23 -21
  32. package/skills/qualia-fix/SKILL.md +216 -0
  33. package/skills/qualia-flush/SKILL.md +9 -9
  34. package/skills/qualia-handoff/SKILL.md +9 -9
  35. package/skills/qualia-help/SKILL.md +3 -3
  36. package/skills/qualia-hook-gen/SKILL.md +1 -1
  37. package/skills/qualia-idk/SKILL.md +4 -4
  38. package/skills/qualia-issues/SKILL.md +2 -2
  39. package/skills/qualia-learn/SKILL.md +10 -10
  40. package/skills/qualia-map/SKILL.md +2 -2
  41. package/skills/qualia-milestone/SKILL.md +15 -15
  42. package/skills/qualia-new/REFERENCE.md +9 -9
  43. package/skills/qualia-new/SKILL.md +14 -14
  44. package/skills/qualia-optimize/REFERENCE.md +1 -1
  45. package/skills/qualia-optimize/SKILL.md +23 -16
  46. package/skills/qualia-pause/SKILL.md +2 -2
  47. package/skills/qualia-plan/SKILL.md +23 -13
  48. package/skills/qualia-polish/REFERENCE.md +14 -14
  49. package/skills/qualia-polish/SKILL.md +64 -19
  50. package/skills/qualia-polish/scripts/loop.mjs +3 -3
  51. package/skills/qualia-polish/scripts/score.mjs +9 -3
  52. package/skills/qualia-postmortem/SKILL.md +9 -9
  53. package/skills/qualia-report/SKILL.md +23 -23
  54. package/skills/qualia-research/SKILL.md +5 -5
  55. package/skills/qualia-resume/SKILL.md +4 -4
  56. package/skills/qualia-review/SKILL.md +28 -12
  57. package/skills/qualia-road/SKILL.md +18 -5
  58. package/skills/qualia-ship/SKILL.md +22 -22
  59. package/skills/qualia-skill-new/SKILL.md +13 -13
  60. package/skills/qualia-test/SKILL.md +5 -5
  61. package/skills/qualia-triage/SKILL.md +1 -1
  62. package/skills/qualia-verify/SKILL.md +37 -23
  63. package/skills/qualia-vibe/SKILL.md +13 -10
  64. package/skills/qualia-vibe/scripts/extract.mjs +1 -1
  65. package/skills/zoho-workflow/SKILL.md +1 -1
  66. package/templates/help.html +12 -10
  67. package/tests/bin.test.sh +34 -4
  68. package/tests/install-smoke.test.sh +22 -2
  69. package/tests/lib.test.sh +290 -0
  70. package/tests/runner.js +3 -0
  71. package/tests/skills.test.sh +4 -4
  72. package/tests/state.test.sh +65 -3
package/tests/lib.test.sh CHANGED
@@ -149,6 +149,123 @@ console.log(d2.drift ? "DRIFT-DETECTED" : "FAIL2");
149
149
  ' | tail -1 > /tmp/qfdrift.out
150
150
  [ "$(cat /tmp/qfdrift.out)" = "DRIFT-DETECTED" ] && ok "drift detection" || fail "drift detection: $(cat /tmp/qfdrift.out)"
151
151
 
152
+ # CLI validate
153
+ TMP=$(mktmp)
154
+ cat > "$TMP/contract.json" <<JSON
155
+ {
156
+ "version": 1,
157
+ "phase": 1,
158
+ "goal": "contract cli",
159
+ "why": "prove validator is executable",
160
+ "generated_at": "2026-05-23T00:00:00Z",
161
+ "generated_by": "manual",
162
+ "source_plan_hash": "",
163
+ "success_criteria": ["contract validates"],
164
+ "tasks": [{
165
+ "id": "T1",
166
+ "title": "Create evidence",
167
+ "wave": 1,
168
+ "depends_on": [],
169
+ "files_modify": [],
170
+ "files_create": ["out.txt"],
171
+ "files_delete": [],
172
+ "acceptance_criteria": ["out.txt exists"],
173
+ "action": "Create out.txt",
174
+ "context_files": [],
175
+ "verification": [{ "type": "file-exists", "path": "out.txt" }]
176
+ }]
177
+ }
178
+ JSON
179
+ OUT=$($NODE "$PC" validate "$TMP/contract.json" 2>&1)
180
+ [ "$OUT" = "VALID $TMP/contract.json" ] && ok "plan-contract CLI validates contract" || fail "plan-contract CLI validate: $OUT"
181
+
182
+ # ─── contract-runner ───────────────────────────────────────
183
+
184
+ CR="$FRAMEWORK_DIR/bin/contract-runner.js"
185
+ $NODE --check "$CR" >/dev/null 2>&1 && ok "contract-runner.js parses" || fail "contract-runner.js parse"
186
+
187
+ TMP=$(mktmp)
188
+ mkdir -p "$TMP/src" "$TMP/.planning"
189
+ echo "export const wired = true;" > "$TMP/src/feature.ts"
190
+ cat > "$TMP/.planning/phase-1-contract.json" <<JSON
191
+ {
192
+ "version": 1,
193
+ "phase": 1,
194
+ "goal": "runner pass",
195
+ "why": "prove checks execute",
196
+ "generated_at": "2026-05-23T00:00:00Z",
197
+ "generated_by": "manual",
198
+ "source_plan_hash": "",
199
+ "success_criteria": ["all checks pass"],
200
+ "tasks": [{
201
+ "id": "T1",
202
+ "title": "Check feature",
203
+ "wave": 1,
204
+ "depends_on": [],
205
+ "files_modify": ["src/feature.ts"],
206
+ "files_create": [],
207
+ "files_delete": [],
208
+ "acceptance_criteria": ["feature exists and is wired"],
209
+ "action": "Validate feature",
210
+ "context_files": [],
211
+ "verification": [
212
+ { "type": "file-exists", "path": "src/feature.ts", "must_contain": "wired" },
213
+ { "type": "grep-match", "path": "src/feature.ts", "pattern": "wired\\\\s*=\\\\s*true", "expect": "present" },
214
+ { "type": "command-exit", "command": "printf", "args": ["contract-ok"], "expected_exit": 0, "expect_stdout_match": "contract-ok" },
215
+ { "type": "behavioral", "description": "evidence file includes wired flag", "evidence_required": [{ "path": "src/feature.ts", "description": "feature evidence", "matcher": "wired" }] }
216
+ ]
217
+ }]
218
+ }
219
+ JSON
220
+ OUT=$(cd "$TMP" && $NODE "$CR" .planning/phase-1-contract.json 2>&1)
221
+ if echo "$OUT" | grep -q "PASS phase 1: 4 check" && [ -f "$TMP/.planning/evidence/phase-1-contract-run.json" ]; then
222
+ ok "contract-runner executes passing contract and writes evidence"
223
+ else
224
+ fail "contract-runner pass/evidence: $OUT"
225
+ fi
226
+
227
+ OUT=$(cd "$TMP" && $NODE "$CR" .planning/phase-1-contract.json --json 2>/dev/null)
228
+ if echo "$OUT" | grep -q '"ok": true' && echo "$OUT" | grep -q '"checked": 4'; then
229
+ ok "contract-runner --json reports checked count"
230
+ else
231
+ fail "contract-runner json output: $OUT"
232
+ fi
233
+
234
+ TMP=$(mktmp)
235
+ mkdir -p "$TMP/.planning"
236
+ cat > "$TMP/.planning/phase-1-contract.json" <<JSON
237
+ {
238
+ "version": 1,
239
+ "phase": 1,
240
+ "goal": "runner fail",
241
+ "why": "prove failures block",
242
+ "generated_at": "2026-05-23T00:00:00Z",
243
+ "generated_by": "manual",
244
+ "source_plan_hash": "",
245
+ "success_criteria": ["failure is detected"],
246
+ "tasks": [{
247
+ "id": "T1",
248
+ "title": "Missing file",
249
+ "wave": 1,
250
+ "depends_on": [],
251
+ "files_modify": [],
252
+ "files_create": [],
253
+ "files_delete": [],
254
+ "acceptance_criteria": ["missing file fails"],
255
+ "action": "Validate missing file",
256
+ "context_files": [],
257
+ "verification": [{ "type": "file-exists", "path": "missing.txt" }]
258
+ }]
259
+ }
260
+ JSON
261
+ OUT=$(cd "$TMP" && $NODE "$CR" .planning/phase-1-contract.json 2>&1)
262
+ EXIT=$?
263
+ if [ "$EXIT" -eq 1 ] && echo "$OUT" | grep -q "missing file"; then
264
+ ok "contract-runner fails closed on failed check"
265
+ else
266
+ fail "contract-runner failed-check behavior: exit=$EXIT out=$OUT"
267
+ fi
268
+
152
269
  # ─── agent-runs ──────────────────────────────────────────
153
270
 
154
271
  AR="$FRAMEWORK_DIR/bin/agent-runs.js"
@@ -233,6 +350,179 @@ console.log("removed:" + r.removed);
233
350
  ')
234
351
  [ "$RES" = "removed:1" ] && ok "prune --before removes old records" || fail "prune: $RES"
235
352
 
353
+ # ─── state-ledger ────────────────────────────────────────
354
+
355
+ SL="$FRAMEWORK_DIR/bin/state-ledger.js"
356
+ $NODE --check "$SL" >/dev/null 2>&1 && ok "state-ledger.js parses" || fail "state-ledger.js parse"
357
+
358
+ HA="$FRAMEWORK_DIR/bin/host-adapters.js"
359
+ $NODE --check "$HA" >/dev/null 2>&1 && ok "host-adapters.js parses" || fail "host-adapters.js parse"
360
+ RES=$($NODE -e '
361
+ const { renderText } = require("'"$HA"'");
362
+ const c = renderText("node ${QUALIA_BIN}/state.js @${QUALIA_AGENTS}/planner.md", "codex");
363
+ const d = renderText("node ${QUALIA_BIN}/state.js @${QUALIA_AGENTS}/planner.md", "claude");
364
+ console.log(c.includes(".codex/bin/state.js") && c.includes(".codex/agents/planner.md") && d.includes(".claude/bin/state.js") ? "HOST-RENDER-OK" : c + "\n" + d);
365
+ ')
366
+ [ "$RES" = "HOST-RENDER-OK" ] && ok "host-adapters renders Qualia path tokens per host" || fail "host-adapters render: $RES"
367
+
368
+ TMP=$(mktmp)
369
+ RES=$(cd "$TMP" && $NODE -e '
370
+ const ledger = require("'"$SL"'");
371
+ const fs = require("fs");
372
+ fs.mkdirSync(".planning", { recursive: true });
373
+ ledger.append(process.cwd(), {
374
+ action: "init",
375
+ status_after: "setup",
376
+ phase_after: 1,
377
+ state_raw_after: "state-1",
378
+ tracking_raw_after: "{\"status\":\"setup\"}"
379
+ });
380
+ ledger.append(process.cwd(), {
381
+ action: "transition",
382
+ status_before: "setup",
383
+ status_after: "planned",
384
+ phase_before: 1,
385
+ phase_after: 1,
386
+ state_raw_before: "state-1",
387
+ state_raw_after: "state-2",
388
+ tracking_raw_before: "{\"status\":\"setup\"}",
389
+ tracking_raw_after: "{\"status\":\"planned\"}",
390
+ evidence_refs: [".planning/phase-1-plan.md"]
391
+ });
392
+ const v = ledger.validate(process.cwd());
393
+ const lines = fs.readFileSync(ledger.ledgerPath(process.cwd()), "utf8").trim().split(/\n/);
394
+ console.log(v.ok && v.count === 2 && lines.length === 2 ? "CHAIN-OK" : JSON.stringify(v));
395
+ ')
396
+ [ "$RES" = "CHAIN-OK" ] && ok "state-ledger appends and validates hash chain" || fail "state-ledger chain: $RES"
397
+
398
+ RES=$(cd "$TMP" && $NODE -e '
399
+ const ledger = require("'"$SL"'");
400
+ const fs = require("fs");
401
+ const file = ledger.ledgerPath(process.cwd());
402
+ const lines = fs.readFileSync(file, "utf8").trim().split(/\n/);
403
+ const first = JSON.parse(lines[0]);
404
+ first.status_after = "tampered";
405
+ lines[0] = JSON.stringify(first);
406
+ fs.writeFileSync(file, lines.join("\n") + "\n");
407
+ const v = ledger.validate(process.cwd());
408
+ console.log(!v.ok && v.errors.some(e => /event_hash/.test(e)) ? "TAMPER-DETECTED" : JSON.stringify(v));
409
+ ')
410
+ [ "$RES" = "TAMPER-DETECTED" ] && ok "state-ledger detects tampered event" || fail "state-ledger tamper: $RES"
411
+
412
+ # ─── trust-score ─────────────────────────────────────────
413
+
414
+ PH="$FRAMEWORK_DIR/bin/planning-hygiene.js"
415
+ $NODE --check "$PH" >/dev/null 2>&1 && ok "planning-hygiene.js parses" || fail "planning-hygiene.js parse"
416
+
417
+ TMP=$(mktmp)
418
+ mkdir -p "$TMP/.planning"
419
+ touch "$TMP/.planning/PROJECT.md"
420
+ touch "$TMP/.planning/phase-1-plan.md"
421
+ OUT=$(cd "$TMP" && $NODE "$PH" scan 2>&1)
422
+ EXIT=$?
423
+ if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q "clean"; then
424
+ ok "planning-hygiene clean project passes"
425
+ else
426
+ fail "planning-hygiene clean project: exit=$EXIT out=$OUT"
427
+ fi
428
+
429
+ TMP=$(mktmp)
430
+ mkdir -p "$TMP/.planning"
431
+ echo debug > "$TMP/.planning/DEBUG-2026-05-23-0100.md"
432
+ echo fix > "$TMP/.planning/FIX-2026-05-23-0101.md"
433
+ echo review > "$TMP/.planning/REVIEW.md"
434
+ echo image > "$TMP/.planning/vibe-after.png"
435
+ OUT=$(cd "$TMP" && $NODE "$PH" scan --json 2>&1)
436
+ EXIT=$?
437
+ if [ "$EXIT" -eq 1 ] \
438
+ && echo "$OUT" | grep -q '"status": "needs_organizing"' \
439
+ && echo "$OUT" | grep -q 'reports/debug/DEBUG-2026-05-23-0100.md' \
440
+ && echo "$OUT" | grep -q 'reports/fix/FIX-2026-05-23-0101.md' \
441
+ && echo "$OUT" | grep -q 'reports/review/REVIEW.md' \
442
+ && echo "$OUT" | grep -q 'assets/vibe/vibe-after.png'; then
443
+ ok "planning-hygiene routes loose artifacts"
444
+ else
445
+ fail "planning-hygiene loose routing: exit=$EXIT out=$OUT"
446
+ fi
447
+
448
+ OUT=$(cd "$TMP" && $NODE "$PH" organize --write 2>&1)
449
+ EXIT=$?
450
+ if [ "$EXIT" -eq 0 ] \
451
+ && [ -f "$TMP/.planning/reports/debug/DEBUG-2026-05-23-0100.md" ] \
452
+ && [ -f "$TMP/.planning/reports/fix/FIX-2026-05-23-0101.md" ] \
453
+ && [ -f "$TMP/.planning/reports/review/REVIEW.md" ] \
454
+ && [ -f "$TMP/.planning/assets/vibe/vibe-after.png" ]; then
455
+ ok "planning-hygiene organize moves loose artifacts"
456
+ else
457
+ fail "planning-hygiene organize: exit=$EXIT out=$OUT"
458
+ fi
459
+
460
+ TS="$FRAMEWORK_DIR/bin/trust-score.js"
461
+ $NODE --check "$TS" >/dev/null 2>&1 && ok "trust-score.js parses" || fail "trust-score.js parse"
462
+
463
+ TMP=$(mktmp)
464
+ OUT=$(HOME="$TMP" $NODE "$TS" --json 2>/dev/null)
465
+ EXIT=$?
466
+ if [ "$EXIT" -eq 1 ] && echo "$OUT" | grep -q '"status": "FAIL"'; then
467
+ ok "trust-score fails closed when no install exists"
468
+ else
469
+ fail "trust-score no-install behavior: exit=$EXIT out=$OUT"
470
+ fi
471
+
472
+ TMP=$(mktmp)
473
+ 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"
474
+ echo '{"installed_by":"Test","role":"OWNER","erp":{"enabled":false}}' > "$TMP/.claude/.qualia-config.json"
475
+ touch "$TMP/.claude/CLAUDE.md" "$TMP/.claude/settings.json"
476
+ for f in runtime-manifest.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 trust-score.js agent-runs.js slop-detect.mjs erp-retry.js report-payload.js project-snapshot.js codex-goal.js planning-hygiene.js; do
477
+ touch "$TMP/.claude/bin/$f"
478
+ done
479
+ 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 vercel-account-guard.js env-empty-guard.js supabase-destructive-guard.js; do
480
+ touch "$TMP/.claude/hooks/$h"
481
+ done
482
+ touch "$TMP/.claude/knowledge/index.md" "$TMP/.claude/knowledge/agents.md"
483
+ for f in design-laws.md design-rubric.md design-brand.md design-product.md design-reference.md frontend.md graphics.md; do
484
+ touch "$TMP/.claude/qualia-design/$f"
485
+ done
486
+ for s in qualia-doctor qualia-road qualia-resume qualia-pause qualia-report qualia-polish qualia-vibe; do
487
+ mkdir -p "$TMP/.claude/skills/$s"
488
+ touch "$TMP/.claude/skills/$s/SKILL.md"
489
+ done
490
+ touch "$TMP/.claude/agents/visual-evaluator.md" "$TMP/.claude/qualia-guide.md" "$TMP/.claude/qualia-templates/help.html"
491
+ cat > "$TMP/project/.planning/STATE.md" <<'EOF'
492
+ Phase: 1 of 1 — Trust
493
+ Status: planned
494
+ EOF
495
+ echo '{"phase":1,"status":"planned"}' > "$TMP/project/.planning/tracking.json"
496
+ cat > "$TMP/project/.planning/phase-1-plan.md" <<'EOF'
497
+ ---
498
+ goal: trust
499
+ ---
500
+ # Plan
501
+ ## Task 1
502
+ **Acceptance Criteria:**
503
+ - ok
504
+ ## Success Criteria
505
+ - [ ] ok
506
+ EOF
507
+ $NODE -e '
508
+ const ledger = require("'"$SL"'");
509
+ ledger.append("'"$TMP"'/project", {
510
+ action: "init",
511
+ status_after: "planned",
512
+ phase_after: 1,
513
+ state_raw_after: "Phase: 1 of 1\nStatus: planned\n",
514
+ tracking_raw_after: "{\"phase\":1,\"status\":\"planned\"}"
515
+ });
516
+ ' >/dev/null
517
+ OUT=$(cd "$TMP/project" && HOME="$TMP" $NODE "$TS" --json 2>/dev/null)
518
+ if echo "$OUT" | grep -q '"status": "DEGRADED"' \
519
+ && echo "$OUT" | grep -q '"score": 88' \
520
+ && echo "$OUT" | grep -q 'JSON contract missing'; then
521
+ ok "trust-score reports 88 degraded when only contract is missing"
522
+ else
523
+ fail "trust-score degraded contract score: $OUT"
524
+ fi
525
+
236
526
  echo ""
237
527
  echo "lib.test.sh: $PASS passed, $FAIL failed"
238
528
  [ "$FAIL" -eq 0 ]
package/tests/runner.js CHANGED
@@ -2801,6 +2801,9 @@ describe("install.js", () => {
2801
2801
  assert.ok(fs.existsSync(rulesDir));
2802
2802
  const files = fs.readdirSync(rulesDir);
2803
2803
  assert.ok(files.length > 0);
2804
+ assert.ok(fs.existsSync(path.join(rulesDir, "command-output.md")));
2805
+ assert.match(fs.readFileSync(path.join(rulesDir, "command-output.md"), "utf8"), /alwaysApply: true/);
2806
+ assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "qualia-design", "graphics.md")));
2804
2807
  } finally {
2805
2808
  fs.rmSync(tmpHome, { recursive: true, force: true });
2806
2809
  }
@@ -106,7 +106,7 @@ for skill_dir in "$SKILLS_DIR"/*/; do
106
106
 
107
107
  # Cache-aware spawn audit (per rules/grounding.md):
108
108
  # Every spawn to a CUSTOM (qualia-*) agent must anchor the prompt with
109
- # `@~/.claude/agents/{name}.md` (either `Role: @...` or `Read your role:
109
+ # `@${QUALIA_AGENTS}/{name}.md` (either `Role: @...` or `Read your role:
110
110
  # @...` — both forms accepted). The role file is session-stable; placing
111
111
  # it first lets Anthropic's prompt cache reuse the prefix across spawns
112
112
  # (documented 81-90% cost reduction). If task-specific content lands
@@ -125,14 +125,14 @@ for skill_dir in "$SKILLS_DIR"/*/; do
125
125
  custom_spawn_count=$((custom_spawn_count + $(grep -c 'subagent_type="qualia-' "$ref_md")))
126
126
  fi
127
127
  if [ "${custom_spawn_count:-0}" -gt 0 ]; then
128
- role_count=$(grep -cE '@~/\.claude/agents/' "$skill_md")
128
+ role_count=$(grep -cF '@${QUALIA_AGENTS}/' "$skill_md")
129
129
  if [ -f "$ref_md" ]; then
130
- role_count=$((role_count + $(grep -cE '@~/\.claude/agents/' "$ref_md")))
130
+ role_count=$((role_count + $(grep -cF '@${QUALIA_AGENTS}/' "$ref_md")))
131
131
  fi
132
132
  if [ "${role_count:-0}" -ge "$custom_spawn_count" ]; then
133
133
  pass "$name: spawn audit ($custom_spawn_count custom spawn(s), all role-anchored for cache)"
134
134
  else
135
- fail_case "$name: spawn audit" "$custom_spawn_count custom spawn(s) but only ${role_count:-0} '@~/.claude/agents/' anchors — prompt cache will miss"
135
+ fail_case "$name: spawn audit" "$custom_spawn_count custom spawn(s) but only ${role_count:-0} '@\${QUALIA_AGENTS}/' anchors — prompt cache will miss"
136
136
  fi
137
137
  fi
138
138
  done
@@ -6,6 +6,8 @@ PASS=0
6
6
  FAIL=0
7
7
  # Resolve STATE_JS to an ABSOLUTE path so `cd` inside subshells doesn't break it.
8
8
  STATE_JS="$(cd "$(dirname "$0")/../bin" && pwd)/state.js"
9
+ FRAMEWORK_DIR="$(cd "$(dirname "$0")/.." && pwd)"
10
+ STATE_LEDGER_JS="$FRAMEWORK_DIR/bin/state-ledger.js"
9
11
  NODE="${NODE:-node}"
10
12
 
11
13
  # Track tmp dirs we create so we can clean them up on exit
@@ -98,11 +100,18 @@ INIT_EXIT=$?
98
100
  if [ "$INIT_EXIT" -eq 0 ] \
99
101
  && [ -f "$TMP/.planning/tracking.json" ] \
100
102
  && [ -f "$TMP/.planning/STATE.md" ] \
103
+ && [ -f "$TMP/.planning/qualia/state.jsonl" ] \
101
104
  && grep -q '"ok": true' /tmp/qualia-state-test.out \
102
105
  && grep -q '"action": "init"' /tmp/qualia-state-test.out; then
103
- pass "cmdInit creates tracking.json + STATE.md"
106
+ pass "cmdInit creates tracking.json + STATE.md + ledger"
104
107
  else
105
- fail_case "cmdInit creates tracking.json + STATE.md" "exit=$INIT_EXIT"
108
+ fail_case "cmdInit creates tracking.json + STATE.md + ledger" "exit=$INIT_EXIT"
109
+ fi
110
+
111
+ if (cd "$TMP" && $NODE "$STATE_LEDGER_JS" validate 2>&1 | grep -q '"ok": true'); then
112
+ pass "cmdInit writes valid state ledger"
113
+ else
114
+ fail_case "cmdInit state ledger validates"
106
115
  fi
107
116
 
108
117
  # tracking.json content sanity
@@ -175,13 +184,22 @@ EXIT=$?
175
184
  if [ "$EXIT" -eq 0 ] \
176
185
  && echo "$OUT" | grep -q '"ok": true' \
177
186
  && echo "$OUT" | grep -q '"status": "built"' \
187
+ && echo "$OUT" | grep -q '"ledger_event_id":' \
178
188
  && grep -q '"tasks_done": 5' "$TMP/.planning/tracking.json" \
179
189
  && grep -q '"tasks_total": 5' "$TMP/.planning/tracking.json"; then
180
- pass "planned → built records tasks_done/tasks_total"
190
+ pass "planned → built records tasks_done/tasks_total + ledger id"
181
191
  else
182
192
  fail_case "planned → built" "exit=$EXIT"
183
193
  fi
184
194
 
195
+ LEDGER_COUNT=$(wc -l < "$TMP/.planning/qualia/state.jsonl" | tr -d ' ')
196
+ if [ "$LEDGER_COUNT" -ge 3 ] \
197
+ && (cd "$TMP" && $NODE "$STATE_LEDGER_JS" validate 2>&1 | grep -q '"ok": true'); then
198
+ pass "state ledger records init + transitions as valid chain"
199
+ else
200
+ fail_case "state ledger transition chain" "count=$LEDGER_COUNT"
201
+ fi
202
+
185
203
  # 6. built → verified(pass) auto-advances to phase 2, resets status to setup
186
204
  touch "$TMP/.planning/phase-1-verification.md"
187
205
  OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
@@ -618,6 +636,50 @@ else
618
636
  fail_case "validate well-formed plan" "exit=$EXIT out=$OUT"
619
637
  fi
620
638
 
639
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" validate-plan --phase 1 --require-contract 2>&1)
640
+ EXIT=$?
641
+ if [ "$EXIT" -eq 1 ] \
642
+ && echo "$OUT" | grep -q 'JSON contract missing'; then
643
+ pass "validate-plan --require-contract fails when JSON contract is missing"
644
+ else
645
+ fail_case "validate-plan --require-contract missing contract" "exit=$EXIT out=$OUT"
646
+ fi
647
+
648
+ HASH=$(cd "$TMP" && $NODE -e "const pc=require('$FRAMEWORK_DIR/bin/plan-contract.js'); const fs=require('fs'); process.stdout.write(pc.hashPlan(fs.readFileSync('.planning/phase-1-plan.md','utf8')))")
649
+ cat > "$TMP/.planning/phase-1-contract.json" <<JSON
650
+ {
651
+ "version": 1,
652
+ "phase": 1,
653
+ "goal": "test",
654
+ "why": "validate require-contract",
655
+ "generated_at": "2026-05-23T00:00:00Z",
656
+ "generated_by": "manual",
657
+ "source_plan_hash": "$HASH",
658
+ "success_criteria": ["ok"],
659
+ "tasks": [{
660
+ "id": "T1",
661
+ "title": "Task",
662
+ "wave": 1,
663
+ "depends_on": [],
664
+ "files_modify": [],
665
+ "files_create": [],
666
+ "files_delete": [],
667
+ "acceptance_criteria": ["ok"],
668
+ "action": "Do task",
669
+ "context_files": [],
670
+ "verification": [{ "type": "file-exists", "path": ".planning/phase-1-plan.md" }]
671
+ }]
672
+ }
673
+ JSON
674
+ OUT=$(cd "$TMP" && $NODE "$STATE_JS" validate-plan --phase 1 --require-contract 2>&1)
675
+ EXIT=$?
676
+ if [ "$EXIT" -eq 0 ] \
677
+ && echo "$OUT" | grep -q '"contract_status": "valid"'; then
678
+ pass "validate-plan --require-contract accepts valid JSON contract"
679
+ else
680
+ fail_case "validate-plan --require-contract valid contract" "exit=$EXIT out=$OUT"
681
+ fi
682
+
621
683
  # 33. validate-plan rejects empty plan
622
684
  TMP=$(make_project)
623
685
  echo "" > "$TMP/.planning/phase-1-plan.md"