qualia-framework 5.9.1 → 6.2.7

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 (81) hide show
  1. package/AGENTS.md +2 -1
  2. package/CLAUDE.md +2 -1
  3. package/README.md +45 -29
  4. package/agents/builder.md +1 -5
  5. package/agents/plan-checker.md +1 -1
  6. package/agents/planner.md +2 -6
  7. package/agents/qa-browser.md +3 -3
  8. package/agents/roadmapper.md +2 -2
  9. package/agents/verifier.md +7 -9
  10. package/agents/visual-evaluator.md +1 -3
  11. package/bin/cli.js +370 -205
  12. package/bin/erp-retry.js +11 -3
  13. package/bin/install.js +383 -55
  14. package/bin/knowledge-flush.js +25 -13
  15. package/bin/knowledge.js +11 -1
  16. package/bin/project-snapshot.js +293 -0
  17. package/bin/qualia-ui.js +13 -2
  18. package/bin/report-payload.js +137 -0
  19. package/bin/slop-detect.mjs +81 -9
  20. package/bin/state.js +8 -1
  21. package/bin/statusline.js +14 -2
  22. package/docs/archive/CHANGELOG-pre-v4.md +855 -0
  23. package/docs/changelog-v6.html +864 -0
  24. package/docs/ecosystem-operating-model.md +121 -0
  25. package/docs/erp-contract.md +74 -21
  26. package/docs/onboarding.html +2 -2
  27. package/docs/release.md +44 -0
  28. package/docs/reviews/v6.2.1-revival-audit.md +53 -0
  29. package/docs/reviews/v6.2.2-memory-erp-audit.md +41 -0
  30. package/docs/reviews/v6.2.3-erp-id-guard.md +15 -0
  31. package/guide.md +28 -3
  32. package/hooks/auto-update.js +20 -10
  33. package/hooks/branch-guard.js +10 -2
  34. package/hooks/env-empty-guard.js +15 -5
  35. package/hooks/git-guardrails.js +10 -1
  36. package/hooks/migration-guard.js +4 -1
  37. package/hooks/pre-deploy-gate.js +11 -1
  38. package/hooks/pre-push.js +43 -106
  39. package/hooks/session-start.js +22 -14
  40. package/hooks/stop-session-log.js +11 -3
  41. package/hooks/supabase-destructive-guard.js +11 -1
  42. package/hooks/vercel-account-guard.js +12 -3
  43. package/package.json +4 -3
  44. package/qualia-design/design-reference.md +2 -1
  45. package/qualia-design/frontend.md +4 -4
  46. package/rules/one-opinion.md +59 -0
  47. package/rules/trust-boundary.md +35 -0
  48. package/skills/qualia-feature/SKILL.md +5 -5
  49. package/skills/qualia-flush/SKILL.md +5 -7
  50. package/skills/qualia-hook-gen/SKILL.md +1 -1
  51. package/skills/qualia-learn/SKILL.md +1 -0
  52. package/skills/qualia-map/SKILL.md +2 -1
  53. package/skills/qualia-milestone/SKILL.md +2 -2
  54. package/skills/qualia-new/SKILL.md +6 -6
  55. package/skills/qualia-optimize/SKILL.md +1 -1
  56. package/skills/qualia-plan/SKILL.md +1 -1
  57. package/skills/qualia-polish/REFERENCE.md +8 -6
  58. package/skills/qualia-polish/SKILL.md +11 -9
  59. package/skills/qualia-polish/scripts/loop.mjs +18 -6
  60. package/skills/qualia-postmortem/SKILL.md +1 -1
  61. package/skills/qualia-report/SKILL.md +6 -42
  62. package/skills/qualia-road/SKILL.md +17 -5
  63. package/skills/qualia-verify/SKILL.md +3 -3
  64. package/skills/qualia-vibe/SKILL.md +226 -0
  65. package/skills/qualia-vibe/scripts/extract.mjs +141 -0
  66. package/skills/qualia-vibe/scripts/tokens.mjs +342 -0
  67. package/templates/help.html +10 -3
  68. package/templates/knowledge/agents.md +3 -3
  69. package/templates/knowledge/index.md +1 -1
  70. package/templates/tracking.json +3 -0
  71. package/templates/work-packet.md +46 -0
  72. package/tests/bin.test.sh +423 -25
  73. package/tests/hooks.test.sh +1 -8
  74. package/tests/install-smoke.test.sh +137 -0
  75. package/tests/published-install-smoke.test.sh +126 -0
  76. package/tests/refs.test.sh +43 -1
  77. package/tests/run-all.sh +49 -0
  78. package/tests/runner.js +19 -33
  79. package/tests/slop-detect.test.sh +11 -5
  80. package/tests/state.test.sh +4 -1
  81. package/hooks/pre-compact.js +0 -125
package/tests/bin.test.sh CHANGED
@@ -454,6 +454,7 @@ if [ "$EXIT" -eq 0 ] \
454
454
  && [ -f "$TMP/.claude/bin/state.js" ] \
455
455
  && [ -f "$TMP/.claude/bin/qualia-ui.js" ] \
456
456
  && [ -f "$TMP/.claude/bin/statusline.js" ] \
457
+ && [ -f "$TMP/.claude/bin/project-snapshot.js" ] \
457
458
  && [ -f "$TMP/.claude/.qualia-config.json" ]; then
458
459
  pass "QS-FAWZI-01 → installs skills, hooks, bin/, config"
459
460
  else
@@ -477,12 +478,13 @@ else
477
478
  fail_case "CLAUDE.md role substitution"
478
479
  fi
479
480
 
480
- # 31. All 12 hooks installed (block-env-edit removed in v3.2.0;
481
+ # 31. All 11 hooks installed (block-env-edit removed in v3.2.0;
481
482
  # git-guardrails + stop-session-log added in v4.2.0;
482
- # vercel-account-guard + env-empty-guard + supabase-destructive-guard added in v5.0.0)
483
+ # vercel-account-guard + env-empty-guard + supabase-destructive-guard added in v5.0.0;
484
+ # pre-compact removed in v6.2.0)
483
485
  HOOK_COUNT=$(ls "$TMP/.claude/hooks/"*.js 2>/dev/null | wc -l)
484
- if [ "$HOOK_COUNT" -eq 12 ]; then
485
- pass "12 hooks installed in hooks/"
486
+ if [ "$HOOK_COUNT" -eq 11 ]; then
487
+ pass "11 hooks installed in hooks/"
486
488
  else
487
489
  fail_case "hook count" "got $HOOK_COUNT"
488
490
  fi
@@ -498,22 +500,23 @@ else
498
500
  fail_case "settings.json contents"
499
501
  fi
500
502
 
501
- # 33. settings.json contains all 12 hooks wired correctly
503
+ # 33. settings.json contains all 11 hooks wired correctly
504
+ # pre-compact.js was removed in v6.2.0 — verify it's NOT in settings.json.
502
505
  if grep -q 'branch-guard.js' "$TMP/.claude/settings.json" \
503
506
  && grep -q 'migration-guard.js' "$TMP/.claude/settings.json" \
504
507
  && grep -q 'pre-push.js' "$TMP/.claude/settings.json" \
505
508
  && grep -q 'pre-deploy-gate.js' "$TMP/.claude/settings.json" \
506
509
  && grep -q 'auto-update.js' "$TMP/.claude/settings.json" \
507
510
  && grep -q 'session-start.js' "$TMP/.claude/settings.json" \
508
- && grep -q 'pre-compact.js' "$TMP/.claude/settings.json" \
509
511
  && grep -q 'git-guardrails.js' "$TMP/.claude/settings.json" \
510
512
  && grep -q 'stop-session-log.js' "$TMP/.claude/settings.json" \
511
513
  && grep -q 'vercel-account-guard.js' "$TMP/.claude/settings.json" \
512
514
  && grep -q 'env-empty-guard.js' "$TMP/.claude/settings.json" \
513
- && grep -q 'supabase-destructive-guard.js' "$TMP/.claude/settings.json"; then
514
- pass "settings.json has all 12 hooks wired"
515
+ && grep -q 'supabase-destructive-guard.js' "$TMP/.claude/settings.json" \
516
+ && ! grep -q 'pre-compact.js' "$TMP/.claude/settings.json"; then
517
+ pass "settings.json has all 11 hooks wired (no pre-compact)"
515
518
  else
516
- fail_case "settings.json missing hooks"
519
+ fail_case "settings.json hooks misconfigured (check for stale pre-compact entry)"
517
520
  fi
518
521
 
519
522
  # 34. Lowercase code works (resolveTeamCode normalizes)
@@ -1225,11 +1228,11 @@ else
1225
1228
  fail_case "qualia-road missing /qualia-polish --loop reference"
1226
1229
  fi
1227
1230
 
1228
- # 108. package.json version is 5.x (5.1+ accepted; v5.1 / v5.2 share the v5 line)
1229
- if grep -qE '"5\.([1-9]|[1-9][0-9])\.' "$FRAMEWORK_DIR/package.json"; then
1230
- pass "package.json version is 5.x"
1231
+ # 108. package.json version is v5.1+ or v6+ (5.1+ accepted; v6 acceptable from v6.0.0)
1232
+ if grep -qE '"(5\.([1-9]|[1-9][0-9])|[6-9]|[1-9][0-9])\.' "$FRAMEWORK_DIR/package.json"; then
1233
+ pass "package.json version is v5.1+ or v6+"
1231
1234
  else
1232
- fail_case "package.json version not 5.x"
1235
+ fail_case "package.json version not v5.1+ or v6+"
1233
1236
  fi
1234
1237
 
1235
1238
  # 109. loop.mjs installs (orchestrator)
@@ -1340,20 +1343,49 @@ else
1340
1343
  fail_case "target=1 Claude-only" "exit=$EXIT codex_exists=$(test -d "$TMP/.codex" && echo yes || echo no)"
1341
1344
  fi
1342
1345
 
1343
- # 119. Target=2 (Codex only) writes ~/.codex/AGENTS.md, skips ~/.claude/
1346
+ # 119. Target=2 (Codex only) writes native Codex runtime files, skips ~/.claude/
1344
1347
  TMP=$(mktmp)
1345
1348
  printf 'QS-FAWZI-01\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1346
1349
  EXIT=$?
1347
1350
  if [ "$EXIT" -eq 0 ] \
1348
1351
  && [ -f "$TMP/.codex/AGENTS.md" ] \
1352
+ && [ -f "$TMP/.codex/.qualia-config.json" ] \
1353
+ && [ -f "$TMP/.codex/config.toml" ] \
1354
+ && [ -f "$TMP/.codex/hooks.json" ] \
1355
+ && [ -f "$TMP/.codex/bin/statusline.js" ] \
1356
+ && [ -f "$TMP/.codex/bin/project-snapshot.js" ] \
1357
+ && [ -f "$TMP/.codex/agents/planner.toml" ] \
1358
+ && [ -f "$TMP/.codex/skills/qualia-new/SKILL.md" ] \
1359
+ && [ -f "$TMP/.codex/qualia-references/questioning.md" ] \
1349
1360
  && [ ! -d "$TMP/.claude" ] \
1350
1361
  && grep -q "Role: OWNER" "$TMP/.codex/AGENTS.md" \
1351
- && ! grep -q "{{ROLE}}" "$TMP/.codex/AGENTS.md"; then
1352
- pass "target=2 ~/.codex/AGENTS.md with Role: OWNER, ~/.claude/ skipped"
1362
+ && ! grep -q "{{ROLE}}" "$TMP/.codex/AGENTS.md" \
1363
+ && grep -q "Qualia deploy gate" "$TMP/.codex/hooks.json" \
1364
+ && grep -q "developer_instructions" "$TMP/.codex/agents/planner.toml" \
1365
+ && ! grep -R "\.claude/bin" "$TMP/.codex/skills" >/dev/null 2>&1; then
1366
+ pass "target=2 → Codex runtime files with Role: OWNER, ~/.claude/ skipped"
1353
1367
  else
1354
1368
  fail_case "target=2 Codex-only" "exit=$EXIT claude_exists=$(test -d "$TMP/.claude" && echo yes || echo no)"
1355
1369
  fi
1356
1370
 
1371
+ EXIT=0; HOME="$TMP" $NODE "$CLI_JS" doctor > "$TMP/doctor.log" 2>&1 || EXIT=$?
1372
+ if [ "$EXIT" -eq 0 ] \
1373
+ && grep -q "Codex hooks.json PreToolUse" "$TMP/doctor.log" \
1374
+ && grep -q "All checks passed" "$TMP/doctor.log"; then
1375
+ pass "doctor passes for Codex-only install"
1376
+ else
1377
+ fail_case "doctor Codex-only" "exit=$EXIT log=$(tail -20 "$TMP/doctor.log" 2>/dev/null)"
1378
+ fi
1379
+
1380
+ EXIT=0; HOME="$TMP" $NODE "$CLI_JS" migrate > "$TMP/migrate.log" 2>&1 || EXIT=$?
1381
+ if [ "$EXIT" -eq 0 ] \
1382
+ && grep -q "Codex install uses" "$TMP/migrate.log" \
1383
+ && grep -q "hooks.json" "$TMP/migrate.log"; then
1384
+ pass "migrate is Codex-aware on Codex-only install"
1385
+ else
1386
+ fail_case "migrate Codex-only" "exit=$EXIT log=$(cat "$TMP/migrate.log" 2>/dev/null)"
1387
+ fi
1388
+
1357
1389
  # 120. Target=3 (Both) populates both directories with the right artifacts
1358
1390
  TMP=$(mktmp)
1359
1391
  printf 'QS-FAWZI-01\n3\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
@@ -1361,8 +1393,13 @@ EXIT=$?
1361
1393
  if [ "$EXIT" -eq 0 ] \
1362
1394
  && [ -f "$TMP/.claude/.qualia-config.json" ] \
1363
1395
  && [ -f "$TMP/.codex/AGENTS.md" ] \
1396
+ && [ -f "$TMP/.codex/config.toml" ] \
1397
+ && [ -f "$TMP/.codex/hooks.json" ] \
1398
+ && [ -f "$TMP/.codex/bin/knowledge.js" ] \
1399
+ && [ -f "$TMP/.codex/agents/builder.toml" ] \
1400
+ && [ -f "$TMP/.codex/qualia-references/questioning.md" ] \
1364
1401
  && grep -q "Role: OWNER" "$TMP/.codex/AGENTS.md"; then
1365
- pass "target=3 → both ~/.claude/ and ~/.codex/AGENTS.md populated"
1402
+ pass "target=3 → both ~/.claude/ and Codex runtime populated"
1366
1403
  else
1367
1404
  fail_case "target=3 Both" "exit=$EXIT"
1368
1405
  fi
@@ -1410,6 +1447,80 @@ else
1410
1447
  fail_case "Codex backup over-zealous" "bak_count=$BAK_COUNT"
1411
1448
  fi
1412
1449
 
1450
+ # 123b. Codex hooks.json merge preserves non-Qualia hook commands.
1451
+ TMP=$(mktmp)
1452
+ mkdir -p "$TMP/.codex"
1453
+ cat > "$TMP/.codex/hooks.json" <<'JSON'
1454
+ {
1455
+ "hooks": {
1456
+ "SessionStart": [
1457
+ {
1458
+ "matcher": ".*",
1459
+ "hooks": [
1460
+ { "type": "command", "command": "node /tmp/custom-session-hook.js", "timeout": 1 }
1461
+ ]
1462
+ }
1463
+ ]
1464
+ }
1465
+ }
1466
+ JSON
1467
+ printf 'QS-FAWZI-01\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1468
+ if grep -q "custom-session-hook.js" "$TMP/.codex/hooks.json" \
1469
+ && grep -q "session-start.js" "$TMP/.codex/hooks.json"; then
1470
+ pass "Codex hooks.json merge preserves non-Qualia hooks"
1471
+ else
1472
+ fail_case "Codex hooks.json merge dropped custom hooks"
1473
+ fi
1474
+
1475
+ # 123c. Codex uninstall removes Qualia runtime files while preserving user-owned
1476
+ # AGENTS.md, config.toml, knowledge, and non-Qualia hooks.json entries.
1477
+ TMP=$(mktmp)
1478
+ mkdir -p "$TMP/.codex"
1479
+ cat > "$TMP/.codex/hooks.json" <<'JSON'
1480
+ {
1481
+ "hooks": {
1482
+ "SessionStart": [
1483
+ {
1484
+ "matcher": ".*",
1485
+ "hooks": [
1486
+ { "type": "command", "command": "node /tmp/custom-session-hook.js", "timeout": 1 }
1487
+ ]
1488
+ }
1489
+ ]
1490
+ }
1491
+ }
1492
+ JSON
1493
+ printf 'QS-FAWZI-01\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1494
+ EXIT=0; HOME="$TMP" $NODE "$CLI_JS" uninstall --yes > "$TMP/uninstall.log" 2>&1 || EXIT=$?
1495
+ if [ "$EXIT" -eq 0 ] \
1496
+ && [ -f "$TMP/.codex/AGENTS.md" ] \
1497
+ && [ -f "$TMP/.codex/config.toml" ] \
1498
+ && [ -d "$TMP/.codex/knowledge" ] \
1499
+ && [ ! -f "$TMP/.codex/.qualia-config.json" ] \
1500
+ && [ ! -f "$TMP/.codex/bin/statusline.js" ] \
1501
+ && [ ! -f "$TMP/.codex/hooks/session-start.js" ] \
1502
+ && [ ! -f "$TMP/.codex/agents/planner.toml" ] \
1503
+ && [ ! -d "$TMP/.codex/skills/qualia-new" ] \
1504
+ && grep -q "custom-session-hook.js" "$TMP/.codex/hooks.json" \
1505
+ && ! grep -q "session-start.js" "$TMP/.codex/hooks.json"; then
1506
+ pass "Codex uninstall removes Qualia runtime and preserves user-owned files"
1507
+ else
1508
+ fail_case "Codex uninstall cleanup" "exit=$EXIT log=$(tail -20 "$TMP/uninstall.log" 2>/dev/null)"
1509
+ fi
1510
+
1511
+ # 123d. Team management writes to Codex-only installs without creating ~/.claude.
1512
+ TMP=$(mktmp)
1513
+ printf 'QS-FAWZI-01\n2\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/log.txt" 2>&1
1514
+ EXIT=0; HOME="$TMP" $NODE "$CLI_JS" team add --code QS-TEST-99 --name "Test User" > "$TMP/team.log" 2>&1 || EXIT=$?
1515
+ if [ "$EXIT" -eq 0 ] \
1516
+ && [ -f "$TMP/.codex/.qualia-team.json" ] \
1517
+ && [ ! -d "$TMP/.claude" ] \
1518
+ && grep -q "QS-TEST-99" "$TMP/.codex/.qualia-team.json"; then
1519
+ pass "team add writes to Codex-only install home"
1520
+ else
1521
+ fail_case "Codex team management" "exit=$EXIT log=$(cat "$TMP/team.log" 2>/dev/null)"
1522
+ fi
1523
+
1413
1524
  # 124. Non-TTY install log is free of cursor-control / line-clear escape
1414
1525
  # sequences (\r, \x1b[?25l/h hide-cursor, \x1b[2K clear-line). Spinner +
1415
1526
  # overwrite primitives must degrade cleanly when output is piped.
@@ -1453,12 +1564,12 @@ else
1453
1564
  fail_case "qualia-ui CLI broke"
1454
1565
  fi
1455
1566
 
1456
- # 128. package.json bumped to 5.x (5.1+ accepted; 5.2 is the v5.2 release)
1567
+ # 128. package.json bumped to v5.1+ or v6+
1457
1568
  PKG_V=$($NODE -e 'console.log(require("'"$FRAMEWORK_DIR"'/package.json").version)')
1458
- if echo "$PKG_V" | grep -qE "^5\.([1-9]|[1-9][0-9])\."; then
1459
- pass "package.json version bumped to 5.x ($PKG_V)"
1569
+ if echo "$PKG_V" | grep -qE "^(5\.([1-9]|[1-9][0-9])|[6-9]|[1-9][0-9])\."; then
1570
+ pass "package.json version bumped ($PKG_V)"
1460
1571
  else
1461
- fail_case "package.json version not 5.x" "got=$PKG_V"
1572
+ fail_case "package.json version not v5.1+ or v6+" "got=$PKG_V"
1462
1573
  fi
1463
1574
 
1464
1575
  echo ""
@@ -1605,12 +1716,12 @@ else
1605
1716
  fail_case "qualia-optimize REFERENCE.md missing parallel-interface template"
1606
1717
  fi
1607
1718
 
1608
- # 143. package.json version is 5.x (5.1+ accepted; v5.3 is the v5.3 release)
1719
+ # 143. package.json version is v5.1+ or v6+
1609
1720
  PKG_V=$($NODE -e 'console.log(require("'"$FRAMEWORK_DIR"'/package.json").version)')
1610
- if echo "$PKG_V" | grep -qE "^5\.([1-9]|[1-9][0-9])\."; then
1611
- pass "package.json version is 5.x ($PKG_V) — v5.3 accepted"
1721
+ if echo "$PKG_V" | grep -qE "^(5\.([1-9]|[1-9][0-9])|[6-9]|[1-9][0-9])\."; then
1722
+ pass "package.json version is shipping range ($PKG_V)"
1612
1723
  else
1613
- fail_case "package.json version not 5.x" "got=$PKG_V"
1724
+ fail_case "package.json version not v5.1+ or v6+" "got=$PKG_V"
1614
1725
  fi
1615
1726
 
1616
1727
  echo ""
@@ -1643,6 +1754,293 @@ else
1643
1754
  fail_case "CONTEXT template missing framework-task vs ERP-workflow distinction"
1644
1755
  fi
1645
1756
 
1757
+ # 147. report-payload omits slug-like ERP UUID fields before POST
1758
+ TMP_PAYLOAD=$(mktmp)
1759
+ OUT=$(FRAMEWORK_DIR="$FRAMEWORK_DIR" TMP_PAYLOAD="$TMP_PAYLOAD" $NODE <<'NODE' 2>&1
1760
+ const assert = require("assert/strict");
1761
+ const fs = require("fs");
1762
+ const path = require("path");
1763
+ const { buildPayload } = require(path.join(process.env.FRAMEWORK_DIR, "bin", "report-payload.js"));
1764
+
1765
+ const root = process.env.TMP_PAYLOAD;
1766
+ const cwd = path.join(root, "project");
1767
+ const home = path.join(root, "home");
1768
+ fs.mkdirSync(path.join(cwd, ".planning"), { recursive: true });
1769
+ fs.mkdirSync(path.join(home, ".claude"), { recursive: true });
1770
+ fs.writeFileSync(path.join(home, ".claude", ".qualia-config.json"), JSON.stringify({ version: "test-version" }));
1771
+ fs.writeFileSync(path.join(cwd, ".planning", "tracking.json"), JSON.stringify({
1772
+ project: "acme-portal",
1773
+ project_id: "qs-acme-portal",
1774
+ team_id: "qualia-solutions",
1775
+ git_remote: "github.com/QualiasolutionsCY/acme-portal",
1776
+ erp_project_id: "project-slug",
1777
+ client_id: "acme",
1778
+ workspace_id: "qualia-solutions",
1779
+ phase: 2,
1780
+ phase_name: "ERP reporting",
1781
+ total_phases: 4,
1782
+ status: "built",
1783
+ gap_cycles: { "2": 1 },
1784
+ session_started_at: "2026-05-21T00:00:00Z"
1785
+ }));
1786
+ const reportFile = path.join(root, "report.md");
1787
+ fs.writeFileSync(reportFile, "Shift notes");
1788
+
1789
+ const payload = buildPayload({
1790
+ cwd,
1791
+ home,
1792
+ reportFile,
1793
+ env: {
1794
+ CLIENT_REPORT_ID: "QS-REPORT-01",
1795
+ SUBMITTED_BY: "Fawzi Goussous",
1796
+ SUBMITTED_AT: "2026-05-21T01:30:00Z",
1797
+ },
1798
+ });
1799
+ assert.equal(payload.project_id, "qs-acme-portal");
1800
+ assert.equal(payload.team_id, "qualia-solutions");
1801
+ assert.equal(payload.framework_version, "test-version");
1802
+ assert.equal(payload.gap_cycles, 1);
1803
+ assert.equal(payload.session_duration_minutes, 90);
1804
+ assert.equal(payload.notes, "Shift notes");
1805
+ assert.equal(Object.prototype.hasOwnProperty.call(payload, "erp_project_id"), false);
1806
+ assert.equal(Object.prototype.hasOwnProperty.call(payload, "client_id"), false);
1807
+ assert.equal(Object.prototype.hasOwnProperty.call(payload, "workspace_id"), false);
1808
+ NODE
1809
+ )
1810
+ EXIT=$?
1811
+ if [ "$EXIT" -eq 0 ]; then
1812
+ pass "report-payload omits slug-like ERP UUID fields"
1813
+ else
1814
+ fail_case "report-payload slug guard failed" "$OUT"
1815
+ fi
1816
+
1817
+ # 148. report-payload sends canonical ERP UUID fields when present
1818
+ TMP_PAYLOAD=$(mktmp)
1819
+ OUT=$(FRAMEWORK_DIR="$FRAMEWORK_DIR" TMP_PAYLOAD="$TMP_PAYLOAD" $NODE <<'NODE' 2>&1
1820
+ const assert = require("assert/strict");
1821
+ const fs = require("fs");
1822
+ const path = require("path");
1823
+ const { buildPayload } = require(path.join(process.env.FRAMEWORK_DIR, "bin", "report-payload.js"));
1824
+
1825
+ const root = process.env.TMP_PAYLOAD;
1826
+ const cwd = path.join(root, "project");
1827
+ const home = path.join(root, "home");
1828
+ fs.mkdirSync(path.join(cwd, ".planning"), { recursive: true });
1829
+ fs.mkdirSync(path.join(home, ".claude"), { recursive: true });
1830
+ fs.writeFileSync(path.join(home, ".claude", ".qualia-config.json"), JSON.stringify({ version: "test-version" }));
1831
+ fs.writeFileSync(path.join(cwd, ".planning", "tracking.json"), JSON.stringify({
1832
+ project: "acme-portal",
1833
+ project_id: "qs-acme-portal",
1834
+ team_id: "qualia-solutions",
1835
+ erp_project_id: "7b5d3b4e-2b8a-4de4-91a1-9b2f3182f5ef",
1836
+ client_id: "5f5a8d8e-8c58-4c30-9b76-13a08f0d0d8a",
1837
+ workspace_id: "2af02a2d-6f1f-4d43-a6cb-6a1e7e09ac43",
1838
+ phase: 1,
1839
+ status: "verified"
1840
+ }));
1841
+ const payload = buildPayload({
1842
+ cwd,
1843
+ home,
1844
+ reportFile: path.join(root, "missing-report.md"),
1845
+ env: {
1846
+ CLIENT_REPORT_ID: "QS-REPORT-02",
1847
+ SUBMITTED_BY: "Fawzi Goussous",
1848
+ SUBMITTED_AT: "2026-05-21T01:30:00Z",
1849
+ },
1850
+ });
1851
+ assert.equal(payload.erp_project_id, "7b5d3b4e-2b8a-4de4-91a1-9b2f3182f5ef");
1852
+ assert.equal(payload.client_id, "5f5a8d8e-8c58-4c30-9b76-13a08f0d0d8a");
1853
+ assert.equal(payload.workspace_id, "2af02a2d-6f1f-4d43-a6cb-6a1e7e09ac43");
1854
+ assert.equal(payload.client_report_id, "QS-REPORT-02");
1855
+ assert.equal(payload.notes, "");
1856
+ NODE
1857
+ )
1858
+ EXIT=$?
1859
+ if [ "$EXIT" -eq 0 ]; then
1860
+ pass "report-payload sends canonical ERP UUID fields"
1861
+ else
1862
+ fail_case "report-payload UUID pass-through failed" "$OUT"
1863
+ fi
1864
+
1865
+ # 149. Installer ships the payload builder used by /qualia-report
1866
+ TMP_INSTALL=$(mktmp)
1867
+ echo "QS-FAWZI-01" | HOME="$TMP_INSTALL" $NODE "$INSTALL_JS" > "$TMP_INSTALL/out.log" 2>&1
1868
+ EXIT=$?
1869
+ if [ "$EXIT" -eq 0 ] \
1870
+ && [ -f "$TMP_INSTALL/.claude/bin/report-payload.js" ] \
1871
+ && [ -f "$TMP_INSTALL/.claude/bin/project-snapshot.js" ] \
1872
+ && grep -q "report-payload.js" "$TMP_INSTALL/.claude/skills/qualia-report/SKILL.md"; then
1873
+ pass "installer ships ERP report/snapshot helpers"
1874
+ else
1875
+ fail_case "installer missing ERP report/snapshot helpers" "exit=$EXIT"
1876
+ fi
1877
+
1878
+ # 150. project-snapshot exports a 0-to-100 admin progress object
1879
+ TMP_SNAPSHOT=$(mktmp)
1880
+ OUT=$(FRAMEWORK_DIR="$FRAMEWORK_DIR" TMP_SNAPSHOT="$TMP_SNAPSHOT" $NODE <<'NODE' 2>&1
1881
+ const assert = require("assert/strict");
1882
+ const fs = require("fs");
1883
+ const path = require("path");
1884
+ const { buildSnapshot, writeSnapshot } = require(path.join(process.env.FRAMEWORK_DIR, "bin", "project-snapshot.js"));
1885
+
1886
+ const root = process.env.TMP_SNAPSHOT;
1887
+ const cwd = path.join(root, "project");
1888
+ const home = path.join(root, "home");
1889
+ fs.mkdirSync(path.join(cwd, ".planning"), { recursive: true });
1890
+ fs.mkdirSync(path.join(home, ".claude"), { recursive: true });
1891
+ fs.writeFileSync(path.join(home, ".claude", ".qualia-config.json"), JSON.stringify({ version: "test-version" }));
1892
+ fs.writeFileSync(path.join(cwd, ".planning", "JOURNEY.md"), [
1893
+ "# Journey",
1894
+ "## Milestone 1 · Foundation",
1895
+ "## Milestone 2 · Product",
1896
+ "## Milestone 3 · Handoff",
1897
+ ].join("\n"));
1898
+ fs.writeFileSync(path.join(cwd, ".planning", "tracking.json"), JSON.stringify({
1899
+ project: "acme-portal",
1900
+ project_id: "qs-acme-portal",
1901
+ erp_project_id: "7b5d3b4e-2b8a-4de4-91a1-9b2f3182f5ef",
1902
+ client_id: "5f5a8d8e-8c58-4c30-9b76-13a08f0d0d8a",
1903
+ team_id: "qualia-solutions",
1904
+ git_remote: "github.com/QualiasolutionsCY/acme-portal",
1905
+ client: "Acme",
1906
+ milestone: 2,
1907
+ milestone_name: "Product",
1908
+ phase: 2,
1909
+ phase_name: "Dashboard",
1910
+ total_phases: 4,
1911
+ status: "built",
1912
+ tasks_done: 3,
1913
+ tasks_total: 5,
1914
+ verification: "pending",
1915
+ gap_cycles: { "2": 1 },
1916
+ build_count: 4,
1917
+ deploy_count: 1,
1918
+ milestones: [{ num: 1, name: "Foundation", closed_at: "2026-05-01T00:00:00Z" }],
1919
+ lifetime: {
1920
+ tasks_completed: 12,
1921
+ phases_completed: 4,
1922
+ milestones_completed: 1,
1923
+ total_phases: 4,
1924
+ last_closed_milestone: 1
1925
+ }
1926
+ }));
1927
+
1928
+ const snapshot = buildSnapshot({ cwd, home, now: "2026-05-21T00:00:00.000Z" });
1929
+ assert.equal(snapshot.snapshot_version, 1);
1930
+ assert.equal(snapshot.framework_version, "test-version");
1931
+ assert.equal(snapshot.identifiers.project_id, "qs-acme-portal");
1932
+ assert.equal(snapshot.identifiers.erp_project_id, "7b5d3b4e-2b8a-4de4-91a1-9b2f3182f5ef");
1933
+ assert.equal(snapshot.project.progress_percent, 42);
1934
+ assert.equal(snapshot.current.gap_cycles, 1);
1935
+ assert.equal(snapshot.journey.total_milestones, 3);
1936
+ assert.deepEqual(snapshot.journey.milestones.map((m) => m.status), ["closed", "active", "pending"]);
1937
+ assert.equal(snapshot.lifetime.tasks_completed, 12);
1938
+
1939
+ const file = writeSnapshot(snapshot, { cwd });
1940
+ assert.ok(file.endsWith(".planning/snapshots/project-snapshot-2026-05-21T00-00-00-000Z.json"));
1941
+ assert.equal(JSON.parse(fs.readFileSync(file, "utf8")).project.progress_percent, 42);
1942
+ NODE
1943
+ )
1944
+ EXIT=$?
1945
+ if [ "$EXIT" -eq 0 ]; then
1946
+ pass "project-snapshot exports ERP/admin progress object"
1947
+ else
1948
+ fail_case "project-snapshot progress export failed" "$OUT"
1949
+ fi
1950
+
1951
+ # 151. project-snapshot uploads the progress object to ERP intake
1952
+ TMP_SNAPSHOT_UPLOAD=$(mktmp)
1953
+ OUT=$(FRAMEWORK_DIR="$FRAMEWORK_DIR" TMP_SNAPSHOT_UPLOAD="$TMP_SNAPSHOT_UPLOAD" $NODE <<'NODE' 2>&1
1954
+ const assert = require("assert/strict");
1955
+ const { EventEmitter } = require("events");
1956
+ const fs = require("fs");
1957
+ const http = require("http");
1958
+ const path = require("path");
1959
+ const { buildSnapshot, uploadSnapshot } = require(path.join(process.env.FRAMEWORK_DIR, "bin", "project-snapshot.js"));
1960
+
1961
+ const root = process.env.TMP_SNAPSHOT_UPLOAD;
1962
+ const cwd = path.join(root, "project");
1963
+ const home = path.join(root, "home");
1964
+ fs.mkdirSync(path.join(cwd, ".planning"), { recursive: true });
1965
+ fs.mkdirSync(path.join(home, ".claude"), { recursive: true });
1966
+ fs.writeFileSync(path.join(home, ".claude", ".qualia-config.json"), JSON.stringify({ version: "test-version" }));
1967
+ fs.writeFileSync(path.join(cwd, ".planning", "JOURNEY.md"), [
1968
+ "# Journey",
1969
+ "## Milestone 1 · Foundation",
1970
+ "## Milestone 2 · Product",
1971
+ "## Milestone 3 · Handoff",
1972
+ ].join("\n"));
1973
+ fs.writeFileSync(path.join(cwd, ".planning", "tracking.json"), JSON.stringify({
1974
+ project: "acme-portal",
1975
+ project_id: "qs-acme-portal",
1976
+ erp_project_id: "7b5d3b4e-2b8a-4de4-91a1-9b2f3182f5ef",
1977
+ client_id: "5f5a8d8e-8c58-4c30-9b76-13a08f0d0d8a",
1978
+ milestone: 2,
1979
+ phase: 2,
1980
+ total_phases: 4,
1981
+ milestones: [{ num: 1, name: "Foundation" }]
1982
+ }));
1983
+
1984
+ let seen = null;
1985
+ const originalRequest = http.request;
1986
+ http.request = (endpoint, options, callback) => {
1987
+ let raw = "";
1988
+ const req = new EventEmitter();
1989
+ req.write = (chunk) => { raw += chunk; };
1990
+ req.end = () => {
1991
+ seen = {
1992
+ method: options.method,
1993
+ url: endpoint.pathname,
1994
+ authorization: options.headers.Authorization,
1995
+ body: JSON.parse(raw)
1996
+ };
1997
+ const res = new EventEmitter();
1998
+ res.statusCode = 200;
1999
+ res.setEncoding = () => {};
2000
+ callback(res);
2001
+ process.nextTick(() => {
2002
+ res.emit("data", JSON.stringify({
2003
+ ok: true,
2004
+ project_id: "erp-project",
2005
+ progress_percent: seen.body.project.progress_percent
2006
+ }));
2007
+ res.emit("end");
2008
+ });
2009
+ };
2010
+ req.destroy = (error) => req.emit("error", error);
2011
+ return req;
2012
+ };
2013
+
2014
+ (async () => {
2015
+ try {
2016
+ const snapshot = buildSnapshot({ cwd, home, now: "2026-05-21T00:00:00.000Z" });
2017
+ const response = await uploadSnapshot(snapshot, {
2018
+ erp: { enabled: true, url: "http://erp.local", key: "qlt_test" }
2019
+ });
2020
+ assert.equal(response.status, 200);
2021
+ assert.equal(response.body.ok, true);
2022
+ assert.equal(seen.method, "POST");
2023
+ assert.equal(seen.url, "/api/v1/project-snapshots");
2024
+ assert.equal(seen.authorization, "Bearer qlt_test");
2025
+ assert.equal(seen.body.snapshot_version, 1);
2026
+ assert.equal(seen.body.project.progress_percent, 42);
2027
+ assert.equal(seen.body.identifiers.erp_project_id, "7b5d3b4e-2b8a-4de4-91a1-9b2f3182f5ef");
2028
+ } finally {
2029
+ http.request = originalRequest;
2030
+ }
2031
+ })().catch((error) => {
2032
+ console.error(error);
2033
+ process.exit(1);
2034
+ });
2035
+ NODE
2036
+ )
2037
+ EXIT=$?
2038
+ if [ "$EXIT" -eq 0 ]; then
2039
+ pass "project-snapshot uploads ERP/admin progress object"
2040
+ else
2041
+ fail_case "project-snapshot upload failed" "$OUT"
2042
+ fi
2043
+
1646
2044
  echo ""
1647
2045
  echo "=== Results: $PASS passed, $FAIL failed ==="
1648
2046
  [ "$FAIL" -eq 0 ] && exit 0 || exit 1
@@ -340,14 +340,7 @@ EOF
340
340
  assert_exit "exits 0 with STATE.md" 0 $?
341
341
  rm -rf "$TMP"
342
342
 
343
- # --- pre-compact.js ---
344
- echo ""
345
- echo "pre-compact:"
346
-
347
- TMP=$(mktemp -d)
348
- (cd "$TMP" && $NODE "$HOOKS_DIR/pre-compact.js" >/dev/null 2>&1)
349
- assert_exit "exits 0 with no STATE.md" 0 $?
350
- rm -rf "$TMP"
343
+ # pre-compact.js removed in v6.2.0 — state.js journal provides crash safety.
351
344
 
352
345
  # --- auto-update.js ---
353
346
  echo ""