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.
- package/AGENTS.md +2 -1
- package/CLAUDE.md +2 -1
- package/README.md +45 -29
- package/agents/builder.md +1 -5
- package/agents/plan-checker.md +1 -1
- package/agents/planner.md +2 -6
- package/agents/qa-browser.md +3 -3
- package/agents/roadmapper.md +2 -2
- package/agents/verifier.md +7 -9
- package/agents/visual-evaluator.md +1 -3
- package/bin/cli.js +370 -205
- package/bin/erp-retry.js +11 -3
- package/bin/install.js +383 -55
- package/bin/knowledge-flush.js +25 -13
- package/bin/knowledge.js +11 -1
- package/bin/project-snapshot.js +293 -0
- package/bin/qualia-ui.js +13 -2
- package/bin/report-payload.js +137 -0
- package/bin/slop-detect.mjs +81 -9
- package/bin/state.js +8 -1
- package/bin/statusline.js +14 -2
- package/docs/archive/CHANGELOG-pre-v4.md +855 -0
- package/docs/changelog-v6.html +864 -0
- package/docs/ecosystem-operating-model.md +121 -0
- package/docs/erp-contract.md +74 -21
- package/docs/onboarding.html +2 -2
- package/docs/release.md +44 -0
- package/docs/reviews/v6.2.1-revival-audit.md +53 -0
- package/docs/reviews/v6.2.2-memory-erp-audit.md +41 -0
- package/docs/reviews/v6.2.3-erp-id-guard.md +15 -0
- package/guide.md +28 -3
- package/hooks/auto-update.js +20 -10
- package/hooks/branch-guard.js +10 -2
- package/hooks/env-empty-guard.js +15 -5
- package/hooks/git-guardrails.js +10 -1
- package/hooks/migration-guard.js +4 -1
- package/hooks/pre-deploy-gate.js +11 -1
- package/hooks/pre-push.js +43 -106
- package/hooks/session-start.js +22 -14
- package/hooks/stop-session-log.js +11 -3
- package/hooks/supabase-destructive-guard.js +11 -1
- package/hooks/vercel-account-guard.js +12 -3
- package/package.json +4 -3
- package/qualia-design/design-reference.md +2 -1
- package/qualia-design/frontend.md +4 -4
- package/rules/one-opinion.md +59 -0
- package/rules/trust-boundary.md +35 -0
- package/skills/qualia-feature/SKILL.md +5 -5
- package/skills/qualia-flush/SKILL.md +5 -7
- package/skills/qualia-hook-gen/SKILL.md +1 -1
- package/skills/qualia-learn/SKILL.md +1 -0
- package/skills/qualia-map/SKILL.md +2 -1
- package/skills/qualia-milestone/SKILL.md +2 -2
- package/skills/qualia-new/SKILL.md +6 -6
- package/skills/qualia-optimize/SKILL.md +1 -1
- package/skills/qualia-plan/SKILL.md +1 -1
- package/skills/qualia-polish/REFERENCE.md +8 -6
- package/skills/qualia-polish/SKILL.md +11 -9
- package/skills/qualia-polish/scripts/loop.mjs +18 -6
- package/skills/qualia-postmortem/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +6 -42
- package/skills/qualia-road/SKILL.md +17 -5
- package/skills/qualia-verify/SKILL.md +3 -3
- package/skills/qualia-vibe/SKILL.md +226 -0
- package/skills/qualia-vibe/scripts/extract.mjs +141 -0
- package/skills/qualia-vibe/scripts/tokens.mjs +342 -0
- package/templates/help.html +10 -3
- package/templates/knowledge/agents.md +3 -3
- package/templates/knowledge/index.md +1 -1
- package/templates/tracking.json +3 -0
- package/templates/work-packet.md +46 -0
- package/tests/bin.test.sh +423 -25
- package/tests/hooks.test.sh +1 -8
- package/tests/install-smoke.test.sh +137 -0
- package/tests/published-install-smoke.test.sh +126 -0
- package/tests/refs.test.sh +43 -1
- package/tests/run-all.sh +49 -0
- package/tests/runner.js +19 -33
- package/tests/slop-detect.test.sh +11 -5
- package/tests/state.test.sh +4 -1
- 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
|
|
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
|
|
485
|
-
pass "
|
|
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
|
|
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"
|
|
514
|
-
|
|
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
|
|
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
|
|
1229
|
-
if grep -qE '"5\.([1-9]|[1-9][0-9])\.' "$FRAMEWORK_DIR/package.json"; then
|
|
1230
|
-
pass "package.json version is
|
|
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
|
|
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
|
|
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"
|
|
1352
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/tests/hooks.test.sh
CHANGED
|
@@ -340,14 +340,7 @@ EOF
|
|
|
340
340
|
assert_exit "exits 0 with STATE.md" 0 $?
|
|
341
341
|
rm -rf "$TMP"
|
|
342
342
|
|
|
343
|
-
#
|
|
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 ""
|