qualia-framework 6.14.0 → 7.0.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 (72) hide show
  1. package/AGENTS.md +8 -5
  2. package/CHANGELOG.md +316 -0
  3. package/CLAUDE.md +3 -1
  4. package/agents/roadmapper.md +16 -14
  5. package/bin/agent-status.js +24 -11
  6. package/bin/batch-plan.js +111 -0
  7. package/bin/branch-hygiene.js +135 -0
  8. package/bin/command-surface.js +2 -0
  9. package/bin/compile-instructions.js +82 -0
  10. package/bin/design-tokens.js +131 -0
  11. package/bin/erp-event.js +177 -0
  12. package/bin/erp-retry.js +12 -1
  13. package/bin/eval-runner.js +218 -0
  14. package/bin/host-adapters.js +84 -12
  15. package/bin/install.js +44 -13
  16. package/bin/knowledge-flush.js +6 -3
  17. package/bin/last-report.js +207 -0
  18. package/bin/project-sync.js +315 -0
  19. package/bin/recall.js +172 -0
  20. package/bin/repo-map.js +188 -0
  21. package/bin/runtime-manifest.js +12 -0
  22. package/bin/state.js +112 -1
  23. package/bin/vault-access.js +82 -0
  24. package/bin/verify-panel.js +294 -0
  25. package/bin/wave-plan.js +211 -0
  26. package/docs/erp-contract.md +180 -0
  27. package/mcp/memory-mcp/server.js +257 -0
  28. package/package.json +6 -3
  29. package/qualia-design/design-dials.md +72 -0
  30. package/qualia-design/design-reference.md +24 -0
  31. package/rules/access.md +42 -0
  32. package/rules/codex-goal.md +28 -26
  33. package/rules/infrastructure.md +1 -1
  34. package/skills/qualia/SKILL.md +6 -0
  35. package/skills/qualia-build/SKILL.md +43 -9
  36. package/skills/qualia-eval/SKILL.md +83 -0
  37. package/skills/qualia-feature/SKILL.md +20 -4
  38. package/skills/qualia-fix/SKILL.md +13 -1
  39. package/skills/qualia-map/SKILL.md +15 -0
  40. package/skills/qualia-milestone/SKILL.md +12 -6
  41. package/skills/qualia-new/REFERENCE.md +6 -4
  42. package/skills/qualia-new/SKILL.md +41 -15
  43. package/skills/qualia-plan/SKILL.md +2 -2
  44. package/skills/qualia-polish/SKILL.md +3 -2
  45. package/skills/qualia-recall/SKILL.md +76 -0
  46. package/skills/qualia-report/SKILL.md +10 -0
  47. package/skills/qualia-scope/SKILL.md +3 -3
  48. package/skills/qualia-ship/SKILL.md +34 -4
  49. package/skills/qualia-update/SKILL.md +4 -0
  50. package/skills/qualia-verify/SKILL.md +53 -24
  51. package/templates/DESIGN.md +15 -0
  52. package/templates/instructions.md +32 -0
  53. package/templates/journey.md +1 -1
  54. package/templates/project-discovery.md +30 -23
  55. package/templates/requirements.md +7 -7
  56. package/tests/agent-status.test.sh +15 -0
  57. package/tests/batch-plan.test.sh +56 -0
  58. package/tests/branch-hygiene.test.sh +93 -0
  59. package/tests/design-tokens.test.sh +53 -0
  60. package/tests/erp-event.test.sh +78 -0
  61. package/tests/eval-runner.test.sh +147 -0
  62. package/tests/instructions.test.sh +109 -0
  63. package/tests/last-report.test.sh +156 -0
  64. package/tests/lib.test.sh +29 -4
  65. package/tests/project-sync.test.sh +175 -0
  66. package/tests/recall.test.sh +91 -0
  67. package/tests/repo-map.test.sh +70 -0
  68. package/tests/run-all.sh +12 -0
  69. package/tests/runner.js +363 -33
  70. package/tests/state.test.sh +92 -0
  71. package/tests/verify-panel.test.sh +162 -0
  72. package/tests/wave-plan.test.sh +153 -0
@@ -0,0 +1,156 @@
1
+ #!/bin/bash
2
+ # last-report.test.sh — bin/last-report.js (surface the latest session report)
3
+ # Run: bash tests/last-report.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ LR="$BIN_DIR/last-report.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
+ assert_not_contains() {
22
+ local name="$1" hay="$2" needle="$3"
23
+ if echo "$hay" | grep -qF "$needle"; then echo " ✗ $name (unexpected '$needle' in: $hay)"; FAIL=$((FAIL+1));
24
+ else echo " ✓ $name"; PASS=$((PASS+1)); fi
25
+ }
26
+
27
+ # Write a well-formed report at $1/.planning/reports/report-$2.md with summary $3, next $4
28
+ write_report() {
29
+ local root="$1" datepart="$2" summary="$3" next="$4" date="$5"
30
+ mkdir -p "$root/.planning/reports"
31
+ cat > "$root/.planning/reports/report-$datepart.md" <<EOF
32
+ # Session Report — $date
33
+
34
+ **Project:** demo
35
+ **Employee:** T
36
+ **Branch:** main
37
+ **Date:** $date
38
+
39
+ ## What Was Done
40
+ - $summary
41
+
42
+ ## Blockers
43
+ None.
44
+
45
+ ## Next Steps
46
+ 1. $next
47
+
48
+ ## Commits (1)
49
+ - abc123 seed
50
+ EOF
51
+ }
52
+
53
+ echo "last-report.test.sh — bin/last-report.js"
54
+ echo ""
55
+
56
+ $NODE -c "$LR" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
57
+
58
+ # --- no reports dir → exit 1, found:false ---
59
+ TMP=$(mktemp -d)
60
+ (cd "$TMP" && $NODE "$LR" >/dev/null 2>&1)
61
+ assert_exit "no reports dir → exit 1" 1 $?
62
+ OUT=$(cd "$TMP" && $NODE "$LR" --json 2>&1)
63
+ assert_contains "json found:false when none" "$OUT" '"found": false'
64
+ OUT=$(cd "$TMP" && $NODE "$LR" 2>&1)
65
+ assert_contains "human: nothing to surface" "$OUT" "No session reports"
66
+ rm -rf "$TMP"
67
+
68
+ # --- single report → extracts date + summary + next, exit 0 ---
69
+ TMP=$(mktemp -d)
70
+ write_report "$TMP" "2026-05-28" "Built the login flow" "Wire the dashboard" "2026-05-28"
71
+ (cd "$TMP" && $NODE "$LR" >/dev/null 2>&1)
72
+ assert_exit "single report → exit 0" 0 $?
73
+ OUT=$(cd "$TMP" && $NODE "$LR" 2>&1)
74
+ assert_contains "human shows date" "$OUT" "2026-05-28"
75
+ assert_contains "human shows summary" "$OUT" "Built the login flow"
76
+ assert_contains "human shows next" "$OUT" "Wire the dashboard"
77
+ assert_contains "human has 'Last session' prefix" "$OUT" "Last session"
78
+ rm -rf "$TMP"
79
+
80
+ # --- json shape ---
81
+ TMP=$(mktemp -d)
82
+ write_report "$TMP" "2026-05-28" "Built the login flow" "Wire the dashboard" "2026-05-28"
83
+ OUT=$(cd "$TMP" && $NODE "$LR" --json 2>&1)
84
+ assert_contains "json found:true" "$OUT" '"found": true'
85
+ assert_contains "json file field" "$OUT" '"file": "report-2026-05-28.md"'
86
+ assert_contains "json date field" "$OUT" '"date": "2026-05-28"'
87
+ assert_contains "json summary field" "$OUT" '"summary": "Built the login flow"'
88
+ assert_contains "json next field" "$OUT" '"next": "Wire the dashboard"'
89
+ rm -rf "$TMP"
90
+
91
+ # --- multiple reports → picks the newest BY DATE (not file order) ---
92
+ TMP=$(mktemp -d)
93
+ write_report "$TMP" "2026-05-28" "Older work" "Old next" "2026-05-28"
94
+ write_report "$TMP" "2026-06-15" "Newest work" "New next" "2026-06-15"
95
+ write_report "$TMP" "2026-06-01" "Middle work" "Mid next" "2026-06-01"
96
+ OUT=$(cd "$TMP" && $NODE "$LR" --json 2>&1)
97
+ assert_contains "picks newest date file" "$OUT" '"file": "report-2026-06-15.md"'
98
+ assert_contains "picks newest summary" "$OUT" "Newest work"
99
+ assert_not_contains "does not pick older summary" "$OUT" "Older work"
100
+ rm -rf "$TMP"
101
+
102
+ # --- dated suffix filenames (report-2026-06-20-session2.md) parse correctly ---
103
+ TMP=$(mktemp -d)
104
+ write_report "$TMP" "2026-06-10" "Earlier" "e" "2026-06-10"
105
+ write_report "$TMP" "2026-06-20-session2" "Suffixed newest" "s" "2026-06-20"
106
+ OUT=$(cd "$TMP" && $NODE "$LR" --json 2>&1)
107
+ assert_contains "suffixed filename wins by date" "$OUT" "Suffixed newest"
108
+ assert_contains "suffixed date extracted" "$OUT" '"date": "2026-06-20"'
109
+ rm -rf "$TMP"
110
+
111
+ # --- --now makes age_days deterministic ---
112
+ TMP=$(mktemp -d)
113
+ write_report "$TMP" "2026-06-01" "Work" "Next" "2026-06-01"
114
+ OUT=$(cd "$TMP" && $NODE "$LR" --json --now 2026-06-11T00:00:00Z 2>&1)
115
+ assert_contains "age_days deterministic (10)" "$OUT" '"age_days": 10'
116
+ OUT=$(cd "$TMP" && $NODE "$LR" --now 2026-06-11T00:00:00Z 2>&1)
117
+ assert_contains "human age uses --now" "$OUT" "10d ago"
118
+ rm -rf "$TMP"
119
+
120
+ # --- library: latestReport() returns structured object ---
121
+ TMP=$(mktemp -d)
122
+ write_report "$TMP" "2026-06-01" "Lib work" "Lib next" "2026-06-01"
123
+ RES=$($NODE -e "console.log(require('$LR').latestReport('$TMP').summary)" 2>&1)
124
+ assert_contains "latestReport() returns summary" "$RES" "Lib work"
125
+ RES=$($NODE -e "console.log(require('$LR').latestReport('$TMP',{now:'2026-06-06T00:00:00Z'}).age_days)" 2>&1)
126
+ assert_contains "latestReport() honors now opt (5)" "$RES" "5"
127
+ rm -rf "$TMP"
128
+
129
+ # --- malformed / empty report handled gracefully (no crash, still exit 0) ---
130
+ TMP=$(mktemp -d)
131
+ mkdir -p "$TMP/.planning/reports"
132
+ : > "$TMP/.planning/reports/report-2026-06-02.md" # completely empty
133
+ (cd "$TMP" && $NODE "$LR" >/dev/null 2>&1)
134
+ assert_exit "empty report → exit 0 (found by filename)" 0 $?
135
+ OUT=$(cd "$TMP" && $NODE "$LR" --json 2>&1)
136
+ assert_contains "empty report found:true" "$OUT" '"found": true'
137
+ assert_contains "empty report date from filename" "$OUT" '"date": "2026-06-02"'
138
+ rm -rf "$TMP"
139
+
140
+ # --- malformed report (no headings, just prose) still yields a summary ---
141
+ TMP=$(mktemp -d)
142
+ mkdir -p "$TMP/.planning/reports"
143
+ printf 'just some freeform notes about the day\n' > "$TMP/.planning/reports/report-2026-06-03.md"
144
+ OUT=$(cd "$TMP" && $NODE "$LR" --json 2>&1)
145
+ assert_contains "malformed report falls back to first line" "$OUT" "just some freeform notes"
146
+ rm -rf "$TMP"
147
+
148
+ # --- bad argument → exit 2 ---
149
+ TMP=$(mktemp -d)
150
+ (cd "$TMP" && $NODE "$LR" --bogus >/dev/null 2>&1)
151
+ assert_exit "unknown flag → exit 2" 2 $?
152
+ rm -rf "$TMP"
153
+
154
+ echo ""
155
+ echo "=== Results: $PASS passed, $FAIL failed ==="
156
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
package/tests/lib.test.sh CHANGED
@@ -365,6 +365,31 @@ console.log(c.includes(".codex/bin/state.js") && c.includes(".codex/agents/plann
365
365
  ')
366
366
  [ "$RES" = "HOST-RENDER-OK" ] && ok "host-adapters renders Qualia path tokens per host" || fail "host-adapters render: $RES"
367
367
 
368
+ RES=$($NODE -e '
369
+ const { adapter, hostForHome } = require("'"$HA"'");
370
+ const c = adapter("claude"), x = adapter("codex");
371
+ const ok =
372
+ c.agentCli === "claude" && JSON.stringify(c.agentExec("P")) === JSON.stringify(["-p","P"]) &&
373
+ x.agentCli === "codex" && JSON.stringify(x.agentExec("P")) === JSON.stringify(["exec","P"]) &&
374
+ hostForHome("/x/.codex") === "codex" && hostForHome("/x/.claude") === "claude";
375
+ console.log(ok ? "ADAPTER-EXEC-OK" : JSON.stringify({c:c.agentExec("P"),x:x.agentExec("P")}));
376
+ ')
377
+ [ "$RES" = "ADAPTER-EXEC-OK" ] && ok "host-adapters owns agent-CLI invocation (agentCli/agentExec) + hostForHome" || fail "host-adapters agent-exec: $RES"
378
+
379
+ # ─── R9 design dials: the 3 dials exist in the reference AND the DESIGN template ─
380
+ DIALS="$FRAMEWORK_DIR/qualia-design/design-dials.md"
381
+ DTMPL="$FRAMEWORK_DIR/templates/DESIGN.md"
382
+ RES=$($NODE -e '
383
+ const fs = require("fs");
384
+ const ref = fs.readFileSync("'"$DIALS"'", "utf8");
385
+ const tpl = fs.readFileSync("'"$DTMPL"'", "utf8");
386
+ const dials = ["DESIGN_VARIANCE", "MOTION_INTENSITY", "VISUAL_DENSITY"];
387
+ const refOk = dials.every(d => ref.includes(d)) && /ban\b/i.test(ref) && /do instead/i.test(ref);
388
+ const tplOk = dials.every(d => tpl.includes(d));
389
+ console.log(refOk && tplOk ? "DIALS-OK" : "FAIL ref="+refOk+" tpl="+tplOk);
390
+ ')
391
+ [ "$RES" = "DIALS-OK" ] && ok "design dials + ban→do-instead present in design-dials.md AND DESIGN.md template" || fail "design dials: $RES"
392
+
368
393
  CS="$FRAMEWORK_DIR/bin/command-surface.js"
369
394
  $NODE --check "$CS" >/dev/null 2>&1 && ok "command-surface.js parses" || fail "command-surface.js parse"
370
395
  RES=$($NODE -e '
@@ -507,14 +532,14 @@ TMP=$(mktmp)
507
532
  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
533
  echo '{"installed_by":"Test","role":"OWNER","version":"6.3.0","erp":{"enabled":false}}' > "$TMP/home/.claude/.qualia-config.json"
509
534
  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 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
535
+ for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
511
536
  touch "$TMP/home/.claude/bin/$f"
512
537
  done
513
538
  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
514
539
  touch "$TMP/home/.claude/hooks/$h"
515
540
  done
516
541
  touch "$TMP/home/.claude/knowledge/index.md" "$TMP/home/.claude/knowledge/agents.md" "$TMP/home/.claude/agents/visual-evaluator.md" "$TMP/home/.claude/qualia-guide.md" "$TMP/home/.claude/qualia-templates/help.html"
517
- for f in design-laws.md design-rubric.md design-brand.md design-product.md design-reference.md frontend.md graphics.md; do
542
+ for f in design-laws.md design-rubric.md design-brand.md design-product.md design-reference.md design-dials.md frontend.md graphics.md; do
518
543
  touch "$TMP/home/.claude/qualia-design/$f"
519
544
  done
520
545
  for s in $($NODE -e 'console.log(require(process.argv[1]).ACTIVE_SKILLS.join(" "))' "$CS"); do
@@ -622,14 +647,14 @@ TMP=$(mktmp)
622
647
  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
648
  echo '{"installed_by":"Test","role":"OWNER","erp":{"enabled":false}}' > "$TMP/.claude/.qualia-config.json"
624
649
  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 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
650
+ for f in runtime-manifest.js command-surface.js host-adapters.js state.js qualia-ui.js statusline.js knowledge.js knowledge-flush.js recall.js vault-access.js repo-map.js design-tokens.js batch-plan.js state-ledger.js plan-contract.js contract-runner.js agent-status.js analyze-gate.js verify-panel.js wave-plan.js eval-runner.js branch-hygiene.js last-report.js harness-eval.js trust-score.js agent-runs.js slop-detect.mjs erp-retry.js erp-event.js work-packet.js report-payload.js project-snapshot.js project-sync.js codex-goal.js planning-hygiene.js prune-deprecated.js learning-candidates.js status-snapshot.js security-scan.js auto-report.js; do
626
651
  touch "$TMP/.claude/bin/$f"
627
652
  done
628
653
  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
629
654
  touch "$TMP/.claude/hooks/$h"
630
655
  done
631
656
  touch "$TMP/.claude/knowledge/index.md" "$TMP/.claude/knowledge/agents.md"
632
- for f in design-laws.md design-rubric.md design-brand.md design-product.md design-reference.md frontend.md graphics.md; do
657
+ for f in design-laws.md design-rubric.md design-brand.md design-product.md design-reference.md design-dials.md frontend.md graphics.md; do
633
658
  touch "$TMP/.claude/qualia-design/$f"
634
659
  done
635
660
  for s in $($NODE -e 'console.log(require(process.argv[1]).ACTIVE_SKILLS.join(" "))' "$CS"); do
@@ -0,0 +1,175 @@
1
+ #!/bin/bash
2
+ # project-sync.test.sh — bin/project-sync.js (Framework→ERP project-sync payload)
3
+ # Run: bash tests/project-sync.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ PS="$BIN_DIR/project-sync.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 output)"; FAIL=$((FAIL+1)); fi
20
+ }
21
+ assert_not_contains() {
22
+ local name="$1" hay="$2" needle="$3"
23
+ if echo "$hay" | grep -qF "$needle"; then echo " ✗ $name (unexpected '$needle')"; FAIL=$((FAIL+1));
24
+ else echo " ✓ $name"; PASS=$((PASS+1)); fi
25
+ }
26
+
27
+ # A fully-populated .planning fixture: milestones, REQ-IDs, offroad, lifetime.
28
+ setup_full() {
29
+ local tmp
30
+ tmp=$(mktemp -d)
31
+ mkdir -p "$tmp/.planning"
32
+ cat > "$tmp/.planning/tracking.json" <<'EOF'
33
+ {
34
+ "project": "acme-portal",
35
+ "project_id": "qs-acme-portal",
36
+ "client": "Acme",
37
+ "milestone": 2,
38
+ "milestone_name": "Product",
39
+ "milestones": [
40
+ { "num": 1, "name": "Foundation", "closed_at": "2026-04-10T18:00:00Z", "phases_completed": 3, "tasks_completed": 12, "deployed_url": "https://m1.vercel.app" }
41
+ ],
42
+ "phase": 2,
43
+ "phase_name": "Dashboard",
44
+ "total_phases": 4,
45
+ "status": "built",
46
+ "tasks_done": 3,
47
+ "tasks_total": 5,
48
+ "verification": "pending",
49
+ "gap_cycles": { "2": 1 },
50
+ "build_count": 4,
51
+ "deploy_count": 1,
52
+ "deployed_url": "https://client.vercel.app",
53
+ "lifecycle": "build",
54
+ "lifetime": { "tasks_completed": 15, "phases_completed": 4, "milestones_completed": 1, "total_phases": 7, "offroad_count": 2 },
55
+ "offroad": [
56
+ { "at": "2026-06-01T10:00:00Z", "milestone": 2, "ref": "BUG-7", "note": "hotfix login" },
57
+ { "at": "2026-06-02T10:00:00Z", "milestone": 2, "ref": null, "note": "tweak" }
58
+ ]
59
+ }
60
+ EOF
61
+ cat > "$tmp/.planning/JOURNEY.md" <<'EOF'
62
+ ## Milestone 1 · Foundation
63
+ ## Milestone 2 · Product [CURRENT]
64
+ ## Milestone 3 · Handoff [FINAL]
65
+ EOF
66
+ cat > "$tmp/.planning/REQUIREMENTS.md" <<'EOF'
67
+ | ID | Milestone | Phase | Status |
68
+ |----|-----------|-------|--------|
69
+ | CORE-01 | M1: Foundation | Phase 1 | Complete |
70
+ | CORE-02 | M2: Product | Phase 2 | Complete |
71
+ | CORE-03 | M2: Product | Phase 3 | Incomplete |
72
+ EOF
73
+ echo "$tmp"
74
+ }
75
+
76
+ echo "project-sync.test.sh — bin/project-sync.js"
77
+ echo ""
78
+
79
+ # --- syntax ---
80
+ $NODE -c "$PS" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
81
+
82
+ NOW="2026-06-21T00:00:00.000Z"
83
+
84
+ # --- no .planning → exit 2 ---
85
+ TMP=$(mktemp -d)
86
+ (cd "$TMP" && $NODE "$PS" >/dev/null 2>&1)
87
+ assert_exit "no .planning → exit 2" 2 $?
88
+ rm -rf "$TMP"
89
+
90
+ # --- full fixture → exit 0 + valid JSON ---
91
+ TMP=$(setup_full)
92
+ OUT=$($NODE "$PS" --cwd "$TMP" --json --now "$NOW" 2>&1)
93
+ (cd "$TMP" && $NODE "$PS" --json --now "$NOW" >/dev/null 2>&1)
94
+ assert_exit "full fixture → exit 0" 0 $?
95
+ echo "$OUT" | $NODE -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{JSON.parse(s);process.exit(0)})" 2>/dev/null \
96
+ && { echo " ✓ --json emits valid JSON"; PASS=$((PASS+1)); } \
97
+ || { echo " ✗ --json invalid JSON"; FAIL=$((FAIL+1)); }
98
+
99
+ # --- schema_version + payload marker ---
100
+ assert_contains "carries schema_version" "$OUT" '"schema_version": 1'
101
+ assert_contains "payload marker is project-sync" "$OUT" '"payload": "project-sync"'
102
+
103
+ # --- lifecycle + launched_at handling ---
104
+ assert_contains "lifecycle present (build)" "$OUT" '"lifecycle": "build"'
105
+
106
+ # --- milestones[] with status + REQ completion ---
107
+ assert_contains "milestone 1 closed" "$OUT" '"status": "closed"'
108
+ assert_contains "milestone 2 current" "$OUT" '"status": "current"'
109
+ assert_contains "milestone 3 future" "$OUT" '"status": "future"'
110
+ assert_contains "closed milestone phases count" "$OUT" '"phases": 3'
111
+ assert_contains "closed milestone tasks_completed" "$OUT" '"tasks_completed": 12'
112
+ assert_contains "REQ total tracked for M2" "$OUT" '"total": 2'
113
+ assert_contains "REQ complete count" "$OUT" '"complete": 1'
114
+ assert_contains "incomplete REQ id surfaced" "$OUT" '"id": "CORE-03"'
115
+ assert_contains "future milestone REQ untracked" "$OUT" '"tracked": false'
116
+ assert_contains "per-milestone deployed_url" "$OUT" '"deployed_url": "https://m1.vercel.app"'
117
+ assert_contains "total_milestones from journey" "$OUT" '"total_milestones": 3'
118
+
119
+ # --- current position ---
120
+ assert_contains "current phase" "$OUT" '"phase": 2'
121
+ assert_contains "current verification" "$OUT" '"verification": "pending"'
122
+
123
+ # --- task rollup ---
124
+ assert_contains "rollup tasks_completed" "$OUT" '"tasks_completed": 15'
125
+ assert_contains "rollup build_count" "$OUT" '"build_count": 4'
126
+ assert_contains "rollup deploy_count" "$OUT" '"deploy_count": 1'
127
+ assert_contains "rollup gap_cycles (flattened to number)" "$OUT" '"current_phase_gap_cycles": 1'
128
+
129
+ # --- accountability / offroad ---
130
+ assert_contains "offroad_count" "$OUT" '"offroad_count": 2'
131
+ assert_contains "recent offroad entry" "$OUT" '"ref": "BUG-7"'
132
+
133
+ # --- integration / merge model ---
134
+ assert_contains "trunk integration model" "$OUT" '"model": "trunk"'
135
+ assert_contains "integrates at ship" "$OUT" '"integrates_at": "/qualia-ship"'
136
+ assert_contains "main-push event type" "$OUT" '"main_push_event_type": "employee_main_push"'
137
+
138
+ rm -rf "$TMP"
139
+
140
+ # --- graceful when fields absent: minimal tracking.json, no JOURNEY/REQUIREMENTS ---
141
+ TMP=$(mktemp -d)
142
+ mkdir -p "$TMP/.planning"
143
+ echo '{ "project": "bare", "milestone": 1, "phase": 1 }' > "$TMP/.planning/tracking.json"
144
+ OUT=$($NODE "$PS" --cwd "$TMP" --json --now "$NOW" 2>&1)
145
+ (cd "$TMP" && $NODE "$PS" --json --now "$NOW" >/dev/null 2>&1)
146
+ assert_exit "bare fixture → exit 0" 0 $?
147
+ assert_contains "bare still has schema_version" "$OUT" '"schema_version": 1'
148
+ assert_contains "bare offroad_count defaults to 0" "$OUT" '"offroad_count": 0'
149
+ assert_contains "bare emits a current milestone" "$OUT" '"status": "current"'
150
+ assert_contains "bare REQ untracked (no REQUIREMENTS.md)" "$OUT" '"tracked": false'
151
+ rm -rf "$TMP"
152
+
153
+ # --- library: buildProjectSync() returns the object ---
154
+ TMP=$(setup_full)
155
+ RES=$($NODE -e "const {buildProjectSync}=require('$PS');const o=buildProjectSync({cwd:'$TMP',now:'$NOW'});console.log(o.schema_version, o.milestones.length, o.milestones[1].requirements.incomplete[0].id)" 2>&1)
156
+ assert_contains "buildProjectSync() returns enriched object" "$RES" "1 3 CORE-03"
157
+ rm -rf "$TMP"
158
+
159
+ # --- library: milestoneRequirements() parses a fixture dir ---
160
+ TMP=$(setup_full)
161
+ RES=$($NODE -e "const {milestoneRequirements}=require('$PS');const r=milestoneRequirements('$TMP/.planning',2);console.log(r.tracked, r.total, r.complete, r.incomplete.length)" 2>&1)
162
+ assert_contains "milestoneRequirements() counts REQ completion" "$RES" "true 2 1 1"
163
+ rm -rf "$TMP"
164
+
165
+ # --- --write persists a snapshot file ---
166
+ TMP=$(setup_full)
167
+ FILE=$($NODE "$PS" --cwd "$TMP" --write --now "$NOW" 2>&1)
168
+ if [ -f "$FILE" ]; then echo " ✓ --write persists a file"; PASS=$((PASS+1));
169
+ else echo " ✗ --write did not persist ($FILE)"; FAIL=$((FAIL+1)); fi
170
+ assert_contains "written file is project-sync-*" "$FILE" "project-sync-"
171
+ rm -rf "$TMP"
172
+
173
+ echo ""
174
+ echo "=== Results: $PASS passed, $FAIL failed ==="
175
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,91 @@
1
+ #!/bin/bash
2
+ # recall.test.sh — bin/recall.js (read-side memory recall, role-filtered vault)
3
+ # Run: bash tests/recall.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ RECALL="$BIN_DIR/recall.js"
10
+
11
+ assert_exit() { if [ "$2" = "$3" ]; then echo " ✓ $1"; PASS=$((PASS+1)); else echo " ✗ $1 (expected exit $2, got $3)"; FAIL=$((FAIL+1)); fi; }
12
+ assert_contains() { if echo "$2" | grep -qF "$3"; then echo " ✓ $1"; PASS=$((PASS+1)); else echo " ✗ $1 (missing '$3')"; FAIL=$((FAIL+1)); fi; }
13
+ assert_absent() { if echo "$2" | grep -qF "$3"; then echo " ✗ $1 (LEAK: found '$3')"; FAIL=$((FAIL+1)); else echo " ✓ $1"; PASS=$((PASS+1)); fi; }
14
+
15
+ echo "recall.test.sh — bin/recall.js"
16
+ echo ""
17
+
18
+ $NODE -c "$RECALL" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
19
+
20
+ # ── Fixture: a temp knowledge home + a temp vault with an access manifest ──
21
+ HOME_DIR=$(mktemp -d)
22
+ VAULT=$(mktemp -d)
23
+ mkdir -p "$HOME_DIR/knowledge" "$VAULT/wiki/concepts" "$VAULT/wiki/_meta"
24
+
25
+ # knowledge layer hit
26
+ cat > "$HOME_DIR/knowledge/learned-patterns.md" <<'EOF'
27
+ # Patterns
28
+ - The zebrafish pattern: batch writes before fanout.
29
+ EOF
30
+ # vault ALL_ROLES hit
31
+ cat > "$VAULT/wiki/concepts/notes.md" <<'EOF'
32
+ A lesson about the zebrafish approach to retries.
33
+ EOF
34
+ # vault OWNER_ONLY hit (the access manifest itself names wiki/_meta/access.md)
35
+ cat > "$VAULT/wiki/_meta/access.md" <<'EOF'
36
+ # Vault Access Manifest
37
+ ## OWNER_ONLY
38
+ | Path | Why |
39
+ |---|---|
40
+ | `Clients/*.md` | commercial |
41
+ | `wiki/_meta/access.md` | this file (zebrafish marker) |
42
+ ## ALL_ROLES
43
+ | `wiki/concepts/` | lessons |
44
+ EOF
45
+
46
+ export QUALIA_MEMORY_ROOT="$VAULT"
47
+ RUN() { QUALIA_HOME="$HOME_DIR" "$@"; }
48
+
49
+ # ── Invocation guards ──
50
+ RUN $NODE "$RECALL" >/dev/null 2>&1; assert_exit "no query → exit 2" 2 $?
51
+ RUN $NODE "$RECALL" zebrafish --scope bogus >/dev/null 2>&1; assert_exit "bad scope → exit 2" 2 $?
52
+
53
+ # ── Hits across both stores ──
54
+ OUT=$(QUALIA_ROLE=OWNER RUN $NODE "$RECALL" zebrafish 2>&1); assert_exit "valid query → exit 0" 0 $?
55
+ assert_contains "knowledge-layer hit" "$OUT" "learned-patterns.md"
56
+ assert_contains "vault ALL_ROLES hit" "$OUT" "concepts/notes.md"
57
+
58
+ # ── Role enforcement (the security boundary) ──
59
+ OWNER_OUT=$(QUALIA_ROLE=OWNER RUN $NODE "$RECALL" zebrafish --scope vault 2>&1)
60
+ assert_contains "OWNER sees OWNER_ONLY path" "$OWNER_OUT" "_meta/access.md"
61
+ EMP_OUT=$(QUALIA_ROLE=EMPLOYEE RUN $NODE "$RECALL" zebrafish --scope vault 2>&1)
62
+ assert_contains "EMPLOYEE still sees ALL_ROLES" "$EMP_OUT" "concepts/notes.md"
63
+ assert_absent "EMPLOYEE blocked from OWNER_ONLY" "$EMP_OUT" "_meta/access.md"
64
+ # fail-closed: no role config + no QUALIA_ROLE → RESTRICTED
65
+ NOROLE_OUT=$(QUALIA_HOME="$VAULT" env -u QUALIA_ROLE $NODE "$RECALL" zebrafish --scope vault 2>&1)
66
+ assert_absent "RESTRICTED (fail-closed) blocked from OWNER_ONLY" "$NOROLE_OUT" "_meta/access.md"
67
+
68
+ # ── JSON shape ──
69
+ JSON=$(QUALIA_ROLE=OWNER RUN $NODE "$RECALL" zebrafish --json 2>&1)
70
+ echo "$JSON" | $NODE -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const j=JSON.parse(s);if(j.role&&j.scope==='all'&&typeof j.total==='number'&&Array.isArray(j.knowledge)&&Array.isArray(j.vault))process.exit(0);process.exit(1)})" \
71
+ && { echo " ✓ --json valid shape (role/scope/total/knowledge/vault)"; PASS=$((PASS+1)); } \
72
+ || { echo " ✗ --json shape"; FAIL=$((FAIL+1)); }
73
+
74
+ # ── Scope isolation ──
75
+ KONLY=$(QUALIA_ROLE=OWNER RUN $NODE "$RECALL" zebrafish --scope knowledge 2>&1)
76
+ assert_contains "scope=knowledge includes knowledge" "$KONLY" "learned-patterns.md"
77
+ assert_absent "scope=knowledge excludes vault" "$KONLY" "concepts/notes.md"
78
+ VONLY=$(QUALIA_ROLE=OWNER RUN $NODE "$RECALL" zebrafish --scope vault 2>&1)
79
+ assert_absent "scope=vault excludes knowledge" "$VONLY" "learned-patterns.md"
80
+
81
+ # ── Graceful degradation ──
82
+ NOVAULT=$(QUALIA_HOME="$HOME_DIR" QUALIA_MEMORY_ROOT="/tmp/qualia-no-such-vault-$$" QUALIA_ROLE=OWNER $NODE "$RECALL" zebrafish 2>&1); RC=$?
83
+ assert_exit "missing vault → still exit 0" 0 $RC
84
+ assert_contains "missing vault → knowledge still searched" "$NOVAULT" "learned-patterns.md"
85
+ ZERO=$(QUALIA_ROLE=OWNER RUN $NODE "$RECALL" "noyzzqxmatch" 2>&1); assert_exit "zero hits → exit 0" 0 $?
86
+ assert_contains "zero hits → friendly message" "$ZERO" "no matches"
87
+
88
+ rm -rf "$HOME_DIR" "$VAULT"
89
+ echo ""
90
+ echo "=== Results: $PASS passed, $FAIL failed ==="
91
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,70 @@
1
+ #!/bin/bash
2
+ # repo-map.test.sh — bin/repo-map.js (zero-dep symbol map for /qualia-map)
3
+ # Run: bash tests/repo-map.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ RM="$BIN_DIR/repo-map.js"
10
+
11
+ assert_exit() { if [ "$2" = "$3" ]; then echo " ✓ $1"; PASS=$((PASS+1)); else echo " ✗ $1 (expected exit $2, got $3)"; FAIL=$((FAIL+1)); fi; }
12
+ assert_contains() { if echo "$2" | grep -qF "$3"; then echo " ✓ $1"; PASS=$((PASS+1)); else echo " ✗ $1 (missing '$3')"; FAIL=$((FAIL+1)); fi; }
13
+ assert_absent() { if echo "$2" | grep -qF "$3"; then echo " ✗ $1 (unexpected '$3')"; FAIL=$((FAIL+1)); else echo " ✓ $1"; PASS=$((PASS+1)); fi; }
14
+
15
+ echo "repo-map.test.sh — bin/repo-map.js"
16
+ echo ""
17
+
18
+ $NODE -c "$RM" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
19
+
20
+ # ── Fixture repo ──
21
+ REPO=$(mktemp -d)
22
+ mkdir -p "$REPO/src" "$REPO/node_modules/pkg" "$REPO/api"
23
+ cat > "$REPO/src/widget.ts" <<'EOF'
24
+ export function makeWidget() {}
25
+ export class WidgetStore {}
26
+ export const WIDGET_LIMIT = 5;
27
+ export type WidgetId = string;
28
+ function internalHelper() {}
29
+ EOF
30
+ cat > "$REPO/api/handler.py" <<'EOF'
31
+ class Handler:
32
+ pass
33
+ def serve():
34
+ pass
35
+ async def serve_async():
36
+ pass
37
+ EOF
38
+ cat > "$REPO/node_modules/pkg/index.js" <<'EOF'
39
+ export function shouldBeIgnored() {}
40
+ EOF
41
+
42
+ # ── Symbol extraction ──
43
+ OUT=$($NODE "$RM" "$REPO" 2>&1); assert_exit "valid dir → exit 0" 0 $?
44
+ assert_contains "extracts JS export function" "$OUT" "makeWidget"
45
+ assert_contains "extracts JS export class" "$OUT" "WidgetStore"
46
+ assert_contains "extracts JS export const" "$OUT" "WIDGET_LIMIT"
47
+ assert_contains "extracts JS export type" "$OUT" "WidgetId"
48
+ assert_contains "extracts Python def" "$OUT" "serve"
49
+ assert_contains "extracts Python class" "$OUT" "Handler"
50
+ assert_absent "ignores node_modules" "$OUT" "shouldBeIgnored"
51
+
52
+ # ── JSON shape ──
53
+ JSON=$($NODE "$RM" "$REPO" --json 2>&1)
54
+ echo "$JSON" | $NODE -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const j=JSON.parse(s);if(typeof j.total_files==='number'&&typeof j.total_symbols==='number'&&Array.isArray(j.files)&&j.files[0].symbols)process.exit(0);process.exit(1)})" \
55
+ && { echo " ✓ --json valid shape (total_files/total_symbols/files[].symbols)"; PASS=$((PASS+1)); } \
56
+ || { echo " ✗ --json shape"; FAIL=$((FAIL+1)); }
57
+ # densest file first (widget.ts has 5 symbols, handler.py has 3)
58
+ FIRST=$(echo "$JSON" | $NODE -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{console.log(JSON.parse(s).files[0].file)})")
59
+ [ "$FIRST" = "src/widget.ts" ] && { echo " ✓ ranks densest file first"; PASS=$((PASS+1)); } || { echo " ✗ ranking (got '$FIRST')"; FAIL=$((FAIL+1)); }
60
+
61
+ # ── Edge cases ──
62
+ $NODE "$RM" /no/such/dir-xyz >/dev/null 2>&1; assert_exit "missing dir → exit 2" 2 $?
63
+ EMPTY=$(mktemp -d)
64
+ EOUT=$($NODE "$RM" "$EMPTY" 2>&1); assert_exit "empty dir → exit 0" 0 $?
65
+ assert_contains "empty dir → 0 source files" "$EOUT" "0 source files"
66
+
67
+ rm -rf "$REPO" "$EMPTY"
68
+ echo ""
69
+ echo "=== Results: $PASS passed, $FAIL failed ==="
70
+ [ "$FAIL" -eq 0 ]
package/tests/run-all.sh CHANGED
@@ -20,6 +20,18 @@ SUITES=(
20
20
  "slop-detect"
21
21
  "agent-status"
22
22
  "analyze-gate"
23
+ "instructions"
24
+ "verify-panel"
25
+ "wave-plan"
26
+ "eval-runner"
27
+ "branch-hygiene"
28
+ "last-report"
29
+ "project-sync"
30
+ "erp-event"
31
+ "recall"
32
+ "repo-map"
33
+ "design-tokens"
34
+ "batch-plan"
23
35
  )
24
36
 
25
37
  FAILED=()