qualia-framework 6.1.0 → 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/README.md +35 -26
- package/agents/roadmapper.md +1 -1
- package/bin/cli.js +339 -200
- 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/state.js +8 -1
- package/bin/statusline.js +14 -2
- 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 +1 -1
- 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 +16 -4
- package/hooks/auto-update.js +14 -7
- package/hooks/branch-guard.js +10 -2
- package/hooks/env-empty-guard.js +10 -1
- 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 +42 -162
- 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 +3 -2
- package/skills/qualia-map/SKILL.md +1 -1
- package/skills/qualia-milestone/SKILL.md +1 -1
- package/skills/qualia-optimize/SKILL.md +1 -1
- package/skills/qualia-polish/SKILL.md +2 -2
- package/skills/qualia-report/SKILL.md +6 -43
- package/skills/qualia-road/SKILL.md +1 -1
- package/skills/qualia-verify/SKILL.md +1 -1
- package/templates/help.html +1 -1
- 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 +411 -13
- 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 +42 -0
- package/tests/run-all.sh +1 -0
- package/tests/runner.js +19 -33
- package/tests/state.test.sh +4 -1
- package/hooks/pre-compact.js +0 -127
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Work Packet — {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
Purpose: ERP-approved context for the next Framework session. This is the handoff from operations/control into Claude Code or Codex execution.
|
|
4
|
+
|
|
5
|
+
## Source
|
|
6
|
+
|
|
7
|
+
- ERP project:
|
|
8
|
+
- ERP project ID (UUID):
|
|
9
|
+
- Client:
|
|
10
|
+
- Client ID (UUID):
|
|
11
|
+
- Workspace ID (UUID):
|
|
12
|
+
- Git remote:
|
|
13
|
+
- Assigned to:
|
|
14
|
+
- Prepared by:
|
|
15
|
+
- Prepared at:
|
|
16
|
+
|
|
17
|
+
## Current Truth
|
|
18
|
+
|
|
19
|
+
- Current milestone:
|
|
20
|
+
- Current phase:
|
|
21
|
+
- Open blocker:
|
|
22
|
+
- Latest report:
|
|
23
|
+
- Deadline:
|
|
24
|
+
- Client-visible status:
|
|
25
|
+
|
|
26
|
+
## Approved Work
|
|
27
|
+
|
|
28
|
+
1.
|
|
29
|
+
2.
|
|
30
|
+
3.
|
|
31
|
+
|
|
32
|
+
## Guardrails
|
|
33
|
+
|
|
34
|
+
- Do not change deadlines, project status, or client-visible updates without approval.
|
|
35
|
+
- Do not treat chat transcripts as final truth unless the ERP record confirms them.
|
|
36
|
+
- Use Framework `.planning/` for execution state; use ERP as the operational source of truth.
|
|
37
|
+
|
|
38
|
+
## Memory Context
|
|
39
|
+
|
|
40
|
+
- Relevant client preferences:
|
|
41
|
+
- Reusable lessons:
|
|
42
|
+
- Known mistakes to avoid:
|
|
43
|
+
|
|
44
|
+
## Next Framework Command
|
|
45
|
+
|
|
46
|
+
`/qualia`
|
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)
|
|
@@ -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.
|
|
@@ -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 ""
|