qualia-framework 6.14.0 → 6.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/AGENTS.md +8 -5
  2. package/CHANGELOG.md +130 -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/branch-hygiene.js +135 -0
  7. package/bin/command-surface.js +1 -0
  8. package/bin/compile-instructions.js +82 -0
  9. package/bin/eval-runner.js +218 -0
  10. package/bin/host-adapters.js +72 -12
  11. package/bin/install.js +21 -13
  12. package/bin/last-report.js +207 -0
  13. package/bin/project-sync.js +315 -0
  14. package/bin/runtime-manifest.js +6 -0
  15. package/bin/state.js +112 -1
  16. package/bin/verify-panel.js +294 -0
  17. package/bin/wave-plan.js +211 -0
  18. package/docs/erp-contract.md +145 -0
  19. package/package.json +3 -2
  20. package/rules/codex-goal.md +28 -26
  21. package/rules/infrastructure.md +1 -1
  22. package/skills/qualia/SKILL.md +6 -0
  23. package/skills/qualia-build/SKILL.md +12 -9
  24. package/skills/qualia-eval/SKILL.md +83 -0
  25. package/skills/qualia-feature/SKILL.md +20 -4
  26. package/skills/qualia-fix/SKILL.md +13 -1
  27. package/skills/qualia-milestone/SKILL.md +12 -6
  28. package/skills/qualia-new/REFERENCE.md +6 -4
  29. package/skills/qualia-new/SKILL.md +27 -15
  30. package/skills/qualia-plan/SKILL.md +2 -2
  31. package/skills/qualia-report/SKILL.md +10 -0
  32. package/skills/qualia-scope/SKILL.md +3 -3
  33. package/skills/qualia-ship/SKILL.md +34 -4
  34. package/skills/qualia-update/SKILL.md +4 -0
  35. package/skills/qualia-verify/SKILL.md +45 -24
  36. package/templates/instructions.md +32 -0
  37. package/templates/journey.md +1 -1
  38. package/templates/project-discovery.md +30 -23
  39. package/templates/requirements.md +7 -7
  40. package/tests/agent-status.test.sh +15 -0
  41. package/tests/branch-hygiene.test.sh +93 -0
  42. package/tests/eval-runner.test.sh +147 -0
  43. package/tests/instructions.test.sh +109 -0
  44. package/tests/last-report.test.sh +156 -0
  45. package/tests/lib.test.sh +2 -2
  46. package/tests/project-sync.test.sh +175 -0
  47. package/tests/run-all.sh +7 -0
  48. package/tests/state.test.sh +92 -0
  49. package/tests/verify-panel.test.sh +162 -0
  50. package/tests/wave-plan.test.sh +153 -0
@@ -0,0 +1,147 @@
1
+ #!/bin/bash
2
+ # eval-runner.test.sh — bin/eval-runner.js (layered AI-feature eval, R7)
3
+ # Run: bash tests/eval-runner.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
8
+ NODE="${NODE:-node}"
9
+ ER="$BIN_DIR/eval-runner.js"
10
+
11
+ assert_exit() {
12
+ local name="$1" expected="$2" actual="$3"
13
+ if [ "$expected" = "$actual" ]; then echo " ✓ $name"; PASS=$((PASS+1));
14
+ else echo " ✗ $name (expected exit $expected, got $actual)"; FAIL=$((FAIL+1)); fi
15
+ }
16
+ assert_contains() {
17
+ local name="$1" hay="$2" needle="$3"
18
+ if echo "$hay" | grep -qF "$needle"; then echo " ✓ $name"; PASS=$((PASS+1));
19
+ else echo " ✗ $name (missing '$needle' in: $hay)"; FAIL=$((FAIL+1)); fi
20
+ }
21
+
22
+ echo "eval-runner.test.sh — bin/eval-runner.js"
23
+ echo ""
24
+
25
+ $NODE -c "$ER" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
26
+
27
+ # --- all deterministic assertions pass → PASS ---
28
+ TMP=$(mktemp -d)
29
+ cat > "$TMP/s.json" <<'EOF'
30
+ { "feature":"support-chat", "cases":[
31
+ { "name":"refund", "output":"We refund within 30 days.", "latency_ms":1200, "cost_usd":0.008,
32
+ "assert":[
33
+ {"type":"contains","value":"30 days"},
34
+ {"type":"not_contains","value":"I cannot help"},
35
+ {"type":"regex","value":"\\b30\\b"},
36
+ {"type":"min_length","value":5},
37
+ {"type":"max_latency_ms","value":2000},
38
+ {"type":"max_cost_usd","value":0.02}
39
+ ] } ] }
40
+ EOF
41
+ $NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
42
+ assert_exit "all deterministic pass → exit 0" 0 $?
43
+ OUT=$($NODE "$ER" "$TMP/s.json" 2>&1)
44
+ assert_contains "reports EVAL PASS" "$OUT" "EVAL PASS"
45
+ rm -rf "$TMP"
46
+
47
+ # --- a failing contains → FAIL with the failing assertion shown ---
48
+ TMP=$(mktemp -d)
49
+ cat > "$TMP/s.json" <<'EOF'
50
+ { "feature":"chat", "cases":[
51
+ { "name":"refusal", "output":"I cannot help with that.",
52
+ "assert":[ {"type":"contains","value":"30 days"}, {"type":"not_contains","value":"I cannot help"} ] } ] }
53
+ EOF
54
+ $NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
55
+ assert_exit "failing assertion → exit 1" 1 $?
56
+ OUT=$($NODE "$ER" "$TMP/s.json" 2>&1)
57
+ assert_contains "shows failing case" "$OUT" "refusal"
58
+ rm -rf "$TMP"
59
+
60
+ # --- latency over budget → FAIL ---
61
+ TMP=$(mktemp -d)
62
+ cat > "$TMP/s.json" <<'EOF'
63
+ { "feature":"chat", "cases":[
64
+ { "name":"slow", "output":"ok", "latency_ms":5000, "assert":[ {"type":"max_latency_ms","value":2000} ] } ] }
65
+ EOF
66
+ $NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
67
+ assert_exit "latency over budget → exit 1" 1 $?
68
+ # missing latency metric but asserted → fail (no silent pass)
69
+ cat > "$TMP/s2.json" <<'EOF'
70
+ { "feature":"chat", "cases":[ { "name":"no-metric", "output":"ok", "assert":[ {"type":"max_latency_ms","value":2000} ] } ] }
71
+ EOF
72
+ $NODE "$ER" "$TMP/s2.json" >/dev/null 2>&1
73
+ assert_exit "asserting latency with none recorded → exit 1" 1 $?
74
+ rm -rf "$TMP"
75
+
76
+ # --- json_path + json_valid ---
77
+ TMP=$(mktemp -d)
78
+ cat > "$TMP/s.json" <<'EOF'
79
+ { "feature":"rag", "cases":[
80
+ { "name":"structured", "output":"{\"answer\":\"yes\",\"sources\":[{\"id\":\"doc1\"}]}",
81
+ "assert":[
82
+ {"type":"json_valid"},
83
+ {"type":"json_path","path":"answer","equals":"yes"},
84
+ {"type":"json_path","path":"sources.0.id","contains":"doc"}
85
+ ] } ] }
86
+ EOF
87
+ $NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
88
+ assert_exit "json_path + json_valid pass → exit 0" 0 $?
89
+ # invalid json under json_valid → fail
90
+ cat > "$TMP/bad.json" <<'EOF'
91
+ { "feature":"rag", "cases":[ { "name":"notjson", "output":"not json", "assert":[ {"type":"json_valid"} ] } ] }
92
+ EOF
93
+ $NODE "$ER" "$TMP/bad.json" >/dev/null 2>&1
94
+ assert_exit "json_valid on non-json → exit 1" 1 $?
95
+ rm -rf "$TMP"
96
+
97
+ # --- llm_rubric: verdict pass / fail / pending ---
98
+ TMP=$(mktemp -d)
99
+ cat > "$TMP/pass.json" <<'EOF'
100
+ { "feature":"chat", "cases":[ { "name":"r", "output":"grounded answer", "assert":[ {"type":"llm_rubric","rubric":"grounded","verdict":"pass"} ] } ] }
101
+ EOF
102
+ $NODE "$ER" "$TMP/pass.json" >/dev/null 2>&1
103
+ assert_exit "llm_rubric verdict=pass → exit 0" 0 $?
104
+ cat > "$TMP/fail.json" <<'EOF'
105
+ { "feature":"chat", "cases":[ { "name":"r", "output":"hallucination", "assert":[ {"type":"llm_rubric","rubric":"grounded","verdict":"fail"} ] } ] }
106
+ EOF
107
+ $NODE "$ER" "$TMP/fail.json" >/dev/null 2>&1
108
+ assert_exit "llm_rubric verdict=fail → exit 1" 1 $?
109
+ cat > "$TMP/pending.json" <<'EOF'
110
+ { "feature":"chat", "cases":[ { "name":"r", "output":"x", "assert":[ {"type":"llm_rubric","rubric":"grounded"} ] } ] }
111
+ EOF
112
+ $NODE "$ER" "$TMP/pending.json" >/dev/null 2>&1
113
+ assert_exit "unjudged llm_rubric → PENDING → exit 1 (no silent pass)" 1 $?
114
+ OUT=$($NODE "$ER" "$TMP/pending.json" 2>&1)
115
+ assert_contains "reports unjudged rubric count" "$OUT" "unjudged"
116
+ rm -rf "$TMP"
117
+
118
+ # --- output_file resolution + --write artifact ---
119
+ TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
120
+ echo -n "the answer is 42" > "$TMP/out.txt"
121
+ cat > "$TMP/s.json" <<'EOF'
122
+ { "feature":"file-feature", "cases":[ { "name":"fromfile", "output_file":"out.txt", "assert":[ {"type":"contains","value":"42"} ] } ] }
123
+ EOF
124
+ (cd "$TMP" && $NODE "$ER" s.json --write >/dev/null 2>&1)
125
+ assert_exit "output_file case passes → exit 0" 0 $?
126
+ [ -f "$TMP/.planning/evals/eval-file-feature.json" ] && { echo " ✓ --write emits eval artifact"; PASS=$((PASS+1)); } || { echo " ✗ no eval artifact"; FAIL=$((FAIL+1)); }
127
+ # missing output_file → fail, not crash
128
+ cat > "$TMP/miss.json" <<'EOF'
129
+ { "feature":"x", "cases":[ { "name":"gone", "output_file":"nope.txt", "assert":[ {"type":"contains","value":"y"} ] } ] }
130
+ EOF
131
+ (cd "$TMP" && $NODE "$ER" miss.json >/dev/null 2>&1)
132
+ assert_exit "missing output_file → exit 1 (graceful)" 1 $?
133
+ rm -rf "$TMP"
134
+
135
+ # --- library: runAssertion unit ---
136
+ U=$($NODE -e "const e=require('$ER'); console.log(e.runAssertion({type:'contains',value:'hi'},{output:'oh hi there'}).ok)" 2>&1)
137
+ assert_contains "runAssertion contains true" "$U" "true"
138
+ U=$($NODE -e "const e=require('$ER'); console.log(e.getPath({a:{b:[{c:9}]}},'a.b.0.c'))" 2>&1)
139
+ assert_contains "getPath nested+index" "$U" "9"
140
+
141
+ # --- malformed suite → exit 2 ---
142
+ $NODE "$ER" /nonexistent-suite.json >/dev/null 2>&1
143
+ assert_exit "missing suite → exit 2" 2 $?
144
+
145
+ echo ""
146
+ echo "=== Results: $PASS passed, $FAIL failed ==="
147
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -0,0 +1,109 @@
1
+ #!/bin/bash
2
+ # instructions.test.sh — single-source instruction compile (R4) + host adapter
3
+ # contract (R5). Run: bash tests/instructions.test.sh
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ FRAMEWORK_DIR="$(cd "$(dirname "$0")/.." && pwd)"
8
+ NODE="${NODE:-node}"
9
+ HA="$FRAMEWORK_DIR/bin/host-adapters.js"
10
+ CI="$FRAMEWORK_DIR/bin/compile-instructions.js"
11
+
12
+ assert_exit() {
13
+ local name="$1" expected="$2" actual="$3"
14
+ if [ "$expected" = "$actual" ]; then echo " ✓ $name"; PASS=$((PASS+1));
15
+ else echo " ✗ $name (expected exit $expected, got $actual)"; FAIL=$((FAIL+1)); fi
16
+ }
17
+ assert_eq() {
18
+ local name="$1" expected="$2" actual="$3"
19
+ if [ "$expected" = "$actual" ]; then echo " ✓ $name"; PASS=$((PASS+1));
20
+ else echo " ✗ $name (expected '$expected', got '$actual')"; FAIL=$((FAIL+1)); fi
21
+ }
22
+ assert_contains() {
23
+ local name="$1" hay="$2" needle="$3"
24
+ if echo "$hay" | grep -qF "$needle"; then echo " ✓ $name"; PASS=$((PASS+1));
25
+ else echo " ✗ $name (missing '$needle')"; FAIL=$((FAIL+1)); fi
26
+ }
27
+ refute_contains() {
28
+ local name="$1" hay="$2" needle="$3"
29
+ if echo "$hay" | grep -qF "$needle"; then echo " ✗ $name (found '$needle')"; FAIL=$((FAIL+1));
30
+ else echo " ✓ $name"; PASS=$((PASS+1)); fi
31
+ }
32
+
33
+ echo "instructions.test.sh — single-source compile (R4) + host contract (R5)"
34
+ echo ""
35
+
36
+ # syntax
37
+ $NODE -c "$HA" 2>/dev/null && { echo " ✓ host-adapters syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ host-adapters syntax"; FAIL=$((FAIL+1)); }
38
+ $NODE -c "$CI" 2>/dev/null && { echo " ✓ compile-instructions syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ compile-instructions syntax"; FAIL=$((FAIL+1)); }
39
+
40
+ # --- R4: drift guard — committed files match the canonical ---
41
+ $NODE "$CI" --check >/dev/null 2>&1
42
+ assert_exit "compile-instructions --check passes (no drift)" 0 $?
43
+
44
+ # A canonical edit not reflected in the artifacts is caught by --check.
45
+ TMPCANON=$(mktemp)
46
+ cp "$FRAMEWORK_DIR/templates/instructions.md" "$TMPCANON"
47
+ printf '\nDRIFT-MARKER\n' >> "$FRAMEWORK_DIR/templates/instructions.md"
48
+ $NODE "$CI" --check >/dev/null 2>&1
49
+ RC=$?
50
+ cp "$TMPCANON" "$FRAMEWORK_DIR/templates/instructions.md" # restore immediately
51
+ rm -f "$TMPCANON"
52
+ assert_exit "drift guard fails (exit 1) when canonical edited but not recompiled" 1 $RC
53
+ # confirm restore worked
54
+ $NODE "$CI" --check >/dev/null 2>&1
55
+ assert_exit "canonical restored, --check green again" 0 $?
56
+
57
+ # --- R4: both artifacts share an identical body, differ only in footer ---
58
+ CLAUDE_BODY=$(sed -n '/# Qualia Framework/,/state router tells you the next command/p' "$FRAMEWORK_DIR/CLAUDE.md")
59
+ AGENTS_BODY=$(sed -n '/# Qualia Framework/,/state router tells you the next command/p' "$FRAMEWORK_DIR/AGENTS.md")
60
+ if [ "$CLAUDE_BODY" = "$AGENTS_BODY" ]; then
61
+ echo " ✓ CLAUDE.md and AGENTS.md bodies are identical (no drift)"; PASS=$((PASS+1));
62
+ else
63
+ echo " ✗ CLAUDE.md and AGENTS.md bodies diverge"; FAIL=$((FAIL+1));
64
+ fi
65
+ # host-specific footer survives
66
+ assert_contains "CLAUDE.md keeps Claude footer" "$(cat "$FRAMEWORK_DIR/CLAUDE.md")" "this file stays under 25 lines"
67
+ assert_contains "AGENTS.md keeps cross-vendor footer" "$(cat "$FRAMEWORK_DIR/AGENTS.md")" "cross-vendor compatibility"
68
+ refute_contains "AGENTS.md does NOT carry the Claude-only footer" "$(cat "$FRAMEWORK_DIR/AGENTS.md")" "this file stays under 25 lines"
69
+ # both carry the generated header + role placeholder for install to fill
70
+ assert_contains "CLAUDE.md has generated header" "$(head -1 "$FRAMEWORK_DIR/CLAUDE.md")" "GENERATED from templates/instructions.md"
71
+ assert_contains "AGENTS.md keeps {{ROLE}} for install" "$(cat "$FRAMEWORK_DIR/AGENTS.md")" "{{ROLE}}"
72
+
73
+ # --- R5: adapter contract is the single source of per-host facts ---
74
+ assert_eq "claude instructionFile" "CLAUDE.md" "$($NODE -e "console.log(require('$HA').adapter('claude').instructionFile)")"
75
+ assert_eq "codex instructionFile" "AGENTS.md" "$($NODE -e "console.log(require('$HA').adapter('codex').instructionFile)")"
76
+ assert_eq "claude configFile" "settings.json" "$($NODE -e "console.log(require('$HA').adapter('claude').configFile)")"
77
+ assert_eq "codex configFile" "config.toml" "$($NODE -e "console.log(require('$HA').adapter('codex').configFile)")"
78
+ assert_eq "codex agentExt" ".toml" "$($NODE -e "console.log(require('$HA').adapter('codex').agentExt)")"
79
+ assert_eq "unknown host throws" "throws" "$($NODE -e "try{require('$HA').adapter('cursor');console.log('no')}catch(e){console.log('throws')}")"
80
+
81
+ # --- R5: stripHostBlocks keeps only the matching host's block ---
82
+ SRC='before
83
+ <!--QUALIA-HOST claude-->
84
+ CLAUDE_ONLY
85
+ <!--/QUALIA-HOST-->
86
+ <!--QUALIA-HOST codex-->
87
+ CODEX_ONLY
88
+ <!--/QUALIA-HOST-->
89
+ after'
90
+ CLAUDE_OUT=$($NODE -e "console.log(require('$HA').stripHostBlocks(process.argv[1],'claude'))" "$SRC")
91
+ assert_contains "claude strip keeps CLAUDE_ONLY" "$CLAUDE_OUT" "CLAUDE_ONLY"
92
+ refute_contains "claude strip drops CODEX_ONLY" "$CLAUDE_OUT" "CODEX_ONLY"
93
+ CODEX_OUT=$($NODE -e "console.log(require('$HA').stripHostBlocks(process.argv[1],'codex'))" "$SRC")
94
+ assert_contains "codex strip keeps CODEX_ONLY" "$CODEX_OUT" "CODEX_ONLY"
95
+ refute_contains "codex strip drops CLAUDE_ONLY" "$CODEX_OUT" "CLAUDE_ONLY"
96
+
97
+ # --- R5: compileInstructions applies naming but leaves tokens/role for install ---
98
+ NAMED=$($NODE -e "console.log(require('$HA').compileInstructions('Use Claude Code now. Role: {{ROLE}} at \${QUALIA_BIN}','codex'))")
99
+ assert_contains "codex compile swaps the product name" "$NAMED" "Use Codex now"
100
+ assert_contains "codex compile leaves {{ROLE}} for install" "$NAMED" "{{ROLE}}"
101
+ assert_contains "codex compile leaves \${QUALIA_BIN} token for install" "$NAMED" '${QUALIA_BIN}'
102
+
103
+ # --- R5: renderText still resolves paths (regression) ---
104
+ RENDERED=$($NODE -e "console.log(require('$HA').renderText('bin at \${QUALIA_BIN}','claude'))")
105
+ assert_contains "renderText resolves QUALIA_BIN" "$RENDERED" "/.claude/bin"
106
+
107
+ echo ""
108
+ echo "=== Results: $PASS passed, $FAIL failed ==="
109
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -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
@@ -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 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
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 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 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
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 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
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 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 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
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
@@ -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
package/tests/run-all.sh CHANGED
@@ -20,6 +20,13 @@ 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"
23
30
  )
24
31
 
25
32
  FAILED=()