qualia-framework 6.2.9 → 6.3.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 (93) hide show
  1. package/AGENTS.md +1 -0
  2. package/CLAUDE.md +1 -0
  3. package/README.md +26 -30
  4. package/agents/builder.md +7 -7
  5. package/agents/planner.md +39 -3
  6. package/agents/research-synthesizer.md +1 -1
  7. package/agents/researcher.md +3 -3
  8. package/agents/roadmapper.md +7 -7
  9. package/agents/verifier.md +18 -6
  10. package/agents/visual-evaluator.md +8 -7
  11. package/bin/cli.js +160 -16
  12. package/bin/command-surface.js +71 -0
  13. package/bin/contract-runner.js +219 -0
  14. package/bin/harness-eval.js +296 -0
  15. package/bin/host-adapters.js +66 -0
  16. package/bin/install.js +116 -172
  17. package/bin/knowledge-flush.js +21 -10
  18. package/bin/knowledge.js +1 -1
  19. package/bin/plan-contract.js +99 -2
  20. package/bin/planning-hygiene.js +262 -0
  21. package/bin/project-snapshot.js +20 -0
  22. package/bin/report-payload.js +18 -0
  23. package/bin/runtime-manifest.js +35 -0
  24. package/bin/state-ledger.js +184 -0
  25. package/bin/state.js +330 -20
  26. package/bin/trust-score.js +268 -0
  27. package/bin/work-packet.js +228 -0
  28. package/docs/erp-contract.md +81 -1
  29. package/docs/onboarding.html +4 -14
  30. package/guide.md +16 -16
  31. package/hooks/fawzi-approval-guard.js +143 -0
  32. package/hooks/pre-deploy-gate.js +74 -1
  33. package/hooks/session-start.js +29 -1
  34. package/package.json +1 -1
  35. package/qualia-design/design-rubric.md +17 -5
  36. package/qualia-design/frontend.md +6 -2
  37. package/qualia-design/graphics.md +47 -0
  38. package/rules/codex-goal.md +1 -1
  39. package/rules/command-output.md +35 -0
  40. package/rules/one-opinion.md +2 -2
  41. package/rules/speed.md +0 -1
  42. package/skills/qualia/SKILL.md +12 -12
  43. package/skills/qualia-build/SKILL.md +20 -14
  44. package/skills/qualia-discuss/SKILL.md +10 -10
  45. package/skills/qualia-doctor/SKILL.md +140 -0
  46. package/skills/qualia-feature/SKILL.md +24 -22
  47. package/skills/qualia-fix/SKILL.md +216 -0
  48. package/skills/qualia-handoff/SKILL.md +9 -9
  49. package/skills/qualia-learn/SKILL.md +11 -11
  50. package/skills/qualia-map/SKILL.md +2 -2
  51. package/skills/qualia-milestone/SKILL.md +15 -15
  52. package/skills/qualia-new/REFERENCE.md +9 -9
  53. package/skills/qualia-new/SKILL.md +14 -14
  54. package/skills/qualia-optimize/REFERENCE.md +1 -1
  55. package/skills/qualia-optimize/SKILL.md +23 -16
  56. package/skills/qualia-plan/SKILL.md +23 -13
  57. package/skills/qualia-polish/REFERENCE.md +15 -15
  58. package/skills/qualia-polish/SKILL.md +81 -21
  59. package/skills/qualia-polish/scripts/loop.mjs +3 -3
  60. package/skills/qualia-polish/scripts/score.mjs +9 -3
  61. package/skills/{qualia-vibe/scripts/extract.mjs → qualia-polish/scripts/vibe-extract.mjs} +5 -5
  62. package/skills/{qualia-vibe/scripts/tokens.mjs → qualia-polish/scripts/vibe-tokens.mjs} +6 -6
  63. package/skills/qualia-postmortem/SKILL.md +9 -9
  64. package/skills/qualia-report/SKILL.md +23 -23
  65. package/skills/qualia-research/SKILL.md +5 -5
  66. package/skills/qualia-review/SKILL.md +28 -12
  67. package/skills/qualia-road/SKILL.md +30 -22
  68. package/skills/qualia-ship/SKILL.md +31 -24
  69. package/skills/qualia-test/SKILL.md +5 -5
  70. package/skills/qualia-verify/SKILL.md +45 -23
  71. package/skills/zoho-workflow/SKILL.md +1 -1
  72. package/templates/help.html +11 -20
  73. package/tests/bin.test.sh +178 -76
  74. package/tests/hooks.test.sh +81 -1
  75. package/tests/install-smoke.test.sh +35 -5
  76. package/tests/lib.test.sh +432 -0
  77. package/tests/published-install-smoke.test.sh +4 -3
  78. package/tests/refs.test.sh +9 -4
  79. package/tests/runner.js +32 -28
  80. package/tests/skills.test.sh +4 -4
  81. package/tests/state.test.sh +133 -3
  82. package/skills/qualia-debug/SKILL.md +0 -185
  83. package/skills/qualia-flush/SKILL.md +0 -198
  84. package/skills/qualia-help/SKILL.md +0 -74
  85. package/skills/qualia-hook-gen/SKILL.md +0 -206
  86. package/skills/qualia-idk/SKILL.md +0 -166
  87. package/skills/qualia-issues/SKILL.md +0 -151
  88. package/skills/qualia-pause/SKILL.md +0 -68
  89. package/skills/qualia-resume/SKILL.md +0 -52
  90. package/skills/qualia-skill-new/SKILL.md +0 -173
  91. package/skills/qualia-triage/SKILL.md +0 -152
  92. package/skills/qualia-vibe/SKILL.md +0 -226
  93. package/skills/qualia-zoom/SKILL.md +0 -51
package/bin/cli.js CHANGED
@@ -5,6 +5,7 @@ const path = require("path");
5
5
  const fs = require("fs");
6
6
  const readline = require("readline");
7
7
  const os = require("os");
8
+ const { binFiles } = require("./runtime-manifest.js");
8
9
 
9
10
  const TEAL = "\x1b[38;2;0;206;209m";
10
11
  const TG = "\x1b[38;2;0;170;175m";
@@ -204,6 +205,7 @@ const QUALIA_HOOK_FILES = [
204
205
  "pre-deploy-gate.js",
205
206
  "git-guardrails.js",
206
207
  "stop-session-log.js",
208
+ "fawzi-approval-guard.js",
207
209
  "env-empty-guard.js",
208
210
  "supabase-destructive-guard.js",
209
211
  "vercel-account-guard.js",
@@ -228,25 +230,13 @@ const QUALIA_AGENT_FILES = [
228
230
  const QUALIA_CODEX_AGENT_FILES = QUALIA_AGENT_FILES.map((f) => f.replace(/\.md$/, ".toml"));
229
231
 
230
232
  // Qualia bin scripts.
231
- const QUALIA_BIN_FILES = [
232
- "state.js",
233
- "qualia-ui.js",
234
- "statusline.js",
235
- "knowledge.js",
236
- "knowledge-flush.js",
237
- "plan-contract.js",
238
- "agent-runs.js",
239
- "slop-detect.mjs",
240
- "erp-retry.js",
241
- "report-payload.js",
242
- "project-snapshot.js",
243
- ];
233
+ const QUALIA_BIN_FILES = binFiles();
244
234
 
245
235
  // Qualia rules — security, deployment, infra, grounding, plus the v4.5.0 design substrate.
246
236
  // frontend.md and design-reference.md are kept for backward compat; new projects use design-laws/brand/product/rubric.
247
237
  const QUALIA_RULE_FILES = [
248
238
  "security.md", "deployment.md", "infrastructure.md", "grounding.md",
249
- "speed.md", "architecture.md", "trust-boundary.md", "one-opinion.md",
239
+ "speed.md", "architecture.md", "trust-boundary.md", "codex-goal.md", "one-opinion.md", "command-output.md",
250
240
  "frontend.md", "design-reference.md",
251
241
  "design-laws.md", "design-brand.md", "design-product.md", "design-rubric.md",
252
242
  ];
@@ -510,7 +500,7 @@ async function cmdUninstall() {
510
500
 
511
501
  function getDefaultTeam() {
512
502
  return {
513
- "QS-FAWZI-01": { name: "Fawzi Goussous", role: "OWNER", description: "Company owner. Full access. Can push to main, approve deploys, edit secrets." },
503
+ "QS-FAWZI-11": { name: "Fawzi Goussous", role: "OWNER", description: "Company owner. Full access. Can push to main, approve deploys, edit secrets." },
514
504
  "QS-HASAN-02": { name: "Hasan", role: "EMPLOYEE", description: "Developer. Feature branches only. Cannot push to main or edit .env files." },
515
505
  "QS-MOAYAD-03": { name: "Moayad", role: "EMPLOYEE", description: "Developer. Feature branches only. Cannot push to main or edit .env files." },
516
506
  "QS-RAMA-04": { name: "Rama", role: "EMPLOYEE", description: "Developer. Feature branches only. Cannot push to main or edit .env files." },
@@ -1217,6 +1207,74 @@ function cmdProjectSnapshot() {
1217
1207
  process.exit(typeof r.status === "number" ? r.status : 1);
1218
1208
  }
1219
1209
 
1210
+ function cmdWorkPacket() {
1211
+ const installedScript = path.join(primaryInstallHome(), "bin", "work-packet.js");
1212
+ const localScript = path.join(__dirname, "work-packet.js");
1213
+ const script = fs.existsSync(installedScript) ? installedScript : localScript;
1214
+ if (!fs.existsSync(script)) {
1215
+ console.log(` ${RED}✗${RESET} work-packet.js not available`);
1216
+ console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
1217
+ process.exit(1);
1218
+ }
1219
+ const args = process.argv.slice(3);
1220
+ const r = spawnSync(process.execPath, [script, ...args], {
1221
+ stdio: "inherit",
1222
+ shell: false,
1223
+ });
1224
+ process.exit(typeof r.status === "number" ? r.status : 1);
1225
+ }
1226
+
1227
+ function cmdPlanningHygiene() {
1228
+ const installedScript = path.join(primaryInstallHome(), "bin", "planning-hygiene.js");
1229
+ const localScript = path.join(__dirname, "planning-hygiene.js");
1230
+ const script = fs.existsSync(installedScript) ? installedScript : localScript;
1231
+ if (!fs.existsSync(script)) {
1232
+ console.log(` ${RED}✗${RESET} planning-hygiene.js not available`);
1233
+ console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
1234
+ process.exit(1);
1235
+ }
1236
+ const args = process.argv.slice(3);
1237
+ const r = spawnSync(process.execPath, [script, ...args], {
1238
+ stdio: "inherit",
1239
+ shell: false,
1240
+ });
1241
+ process.exit(typeof r.status === "number" ? r.status : 1);
1242
+ }
1243
+
1244
+ function cmdTrust() {
1245
+ const trustScript = path.join(primaryInstallHome(), "bin", "trust-score.js");
1246
+ const localTrustScript = path.join(__dirname, "trust-score.js");
1247
+ const script = fs.existsSync(trustScript) ? trustScript : localTrustScript;
1248
+ if (!fs.existsSync(script)) {
1249
+ console.log(` ${RED}✗${RESET} trust-score.js not available`);
1250
+ console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
1251
+ process.exit(1);
1252
+ }
1253
+ const args = process.argv.slice(3);
1254
+ const r = spawnSync(process.execPath, [script, ...args], {
1255
+ stdio: "inherit",
1256
+ shell: false,
1257
+ });
1258
+ process.exit(typeof r.status === "number" ? r.status : 1);
1259
+ }
1260
+
1261
+ function cmdHarnessEval() {
1262
+ const installedScript = path.join(primaryInstallHome(), "bin", "harness-eval.js");
1263
+ const localScript = path.join(__dirname, "harness-eval.js");
1264
+ const script = fs.existsSync(installedScript) ? installedScript : localScript;
1265
+ if (!fs.existsSync(script)) {
1266
+ console.log(` ${RED}✗${RESET} harness-eval.js not available`);
1267
+ console.log(` ${DIM}Run: npx qualia-framework@latest install${RESET}`);
1268
+ process.exit(1);
1269
+ }
1270
+ const args = process.argv.slice(3);
1271
+ const r = spawnSync(process.execPath, [script, ...args], {
1272
+ stdio: "inherit",
1273
+ shell: false,
1274
+ });
1275
+ process.exit(typeof r.status === "number" ? r.status : 1);
1276
+ }
1277
+
1220
1278
  function cmdFlush() {
1221
1279
  const flushScript = path.join(primaryInstallHome(), "bin", "knowledge-flush.js");
1222
1280
  if (!fs.existsSync(flushScript)) {
@@ -1255,14 +1313,22 @@ function cmdDoctor() {
1255
1313
  "rules/grounding.md",
1256
1314
  "rules/security.md",
1257
1315
  "rules/deployment.md",
1316
+ "rules/command-output.md",
1258
1317
  "bin/state.js",
1259
1318
  "bin/qualia-ui.js",
1260
1319
  "bin/statusline.js",
1320
+ "bin/command-surface.js",
1261
1321
  "bin/knowledge.js",
1262
1322
  "bin/knowledge-flush.js",
1323
+ "bin/state-ledger.js",
1324
+ "bin/plan-contract.js",
1325
+ "bin/contract-runner.js",
1326
+ "bin/trust-score.js",
1327
+ "bin/harness-eval.js",
1263
1328
  "bin/erp-retry.js",
1264
1329
  "bin/report-payload.js",
1265
1330
  "bin/project-snapshot.js",
1331
+ "bin/planning-hygiene.js",
1266
1332
  "knowledge/agents.md",
1267
1333
  "knowledge/index.md",
1268
1334
  "knowledge/daily-log",
@@ -1309,6 +1375,23 @@ function cmdDoctor() {
1309
1375
  check("Codex hooks.json parseable", false, e.message);
1310
1376
  }
1311
1377
  }
1378
+ if (label === "Codex" && fs.existsSync(path.join(home, "config.toml"))) {
1379
+ try {
1380
+ const configToml = fs.readFileSync(path.join(home, "config.toml"), "utf8");
1381
+ const hasTui = /^\[tui\]\s*$/m.test(configToml);
1382
+ const hasStatusLine = /^\s*status_line\s*=\s*\[/m.test(configToml);
1383
+ const hasCoreSegments = [
1384
+ "model-with-reasoning",
1385
+ "task-progress",
1386
+ "current-dir",
1387
+ "git-branch",
1388
+ "context-used",
1389
+ ].every((segment) => configToml.includes(`"${segment}"`));
1390
+ check("Codex config.toml TUI status_line", hasTui && hasStatusLine && hasCoreSegments, "reinstall to wire Codex bottom status line");
1391
+ } catch (e) {
1392
+ check("Codex config.toml status_line parseable", false, e.message);
1393
+ }
1394
+ }
1312
1395
  }
1313
1396
 
1314
1397
  // ── Version vs. installed ──────────────────────────────
@@ -1319,11 +1402,51 @@ function cmdDoctor() {
1319
1402
  check("config has install metadata", false, "reinstall to record");
1320
1403
  }
1321
1404
 
1405
+ // ── Project contract health (advisory, not install-failing) ─────────
1406
+ const projectAdvisories = [];
1407
+ try {
1408
+ const statePath = path.join(process.cwd(), ".planning", "STATE.md");
1409
+ if (fs.existsSync(statePath)) {
1410
+ const stateText = fs.readFileSync(statePath, "utf8");
1411
+ const phaseMatch = stateText.match(/^Phase:\s*(\d+)/m);
1412
+ const phase = phaseMatch ? Number(phaseMatch[1]) : 1;
1413
+ const planPath = path.join(process.cwd(), ".planning", `phase-${phase}-plan.md`);
1414
+ const contractPath = path.join(process.cwd(), ".planning", `phase-${phase}-contract.json`);
1415
+ if (fs.existsSync(planPath)) {
1416
+ if (!fs.existsSync(contractPath)) {
1417
+ projectAdvisories.push(`phase ${phase}: JSON contract missing (degraded trust)`);
1418
+ } else {
1419
+ const pc = require("./plan-contract.js");
1420
+ const loaded = pc.readContractFile(contractPath);
1421
+ if (!loaded.ok) {
1422
+ projectAdvisories.push(`phase ${phase}: JSON contract unreadable (${loaded.message || loaded.error})`);
1423
+ } else {
1424
+ const errs = pc.validate(loaded.contract);
1425
+ const drift = pc.checkDrift(contractPath, planPath);
1426
+ if (errs.length > 0) projectAdvisories.push(`phase ${phase}: JSON contract invalid (${errs.length} issue(s))`);
1427
+ else if (drift.ok && drift.drift) projectAdvisories.push(`phase ${phase}: JSON contract drifted from plan`);
1428
+ else projectAdvisories.push(`phase ${phase}: JSON contract valid`);
1429
+ }
1430
+ }
1431
+ }
1432
+ }
1433
+ } catch (e) {
1434
+ projectAdvisories.push(`project contract health unavailable: ${e.message}`);
1435
+ }
1436
+
1322
1437
  // ── Render ────────────────────────────────────────────
1323
1438
  for (const c of checks) {
1324
1439
  const mark = c.ok ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;
1325
1440
  console.log(` ${mark} ${c.label}`);
1326
1441
  }
1442
+ if (projectAdvisories.length > 0) {
1443
+ console.log("");
1444
+ console.log(` ${WHITE}Project trust:${RESET}`);
1445
+ for (const item of projectAdvisories) {
1446
+ const mark = item.includes("valid") && !item.includes("invalid") ? `${GREEN}✓${RESET}` : `${YELLOW}!${RESET}`;
1447
+ console.log(` ${mark} ${item}`);
1448
+ }
1449
+ }
1327
1450
  console.log("");
1328
1451
  if (issues.length === 0) {
1329
1452
  console.log(` ${GREEN}All checks passed. Framework is healthy.${RESET}`);
@@ -1432,18 +1555,23 @@ function cmdHelp() {
1432
1555
  console.log(` qualia-framework ${TEAL}set-erp-key${RESET} Save/enable the ERP API key`);
1433
1556
  console.log(` qualia-framework ${TEAL}erp-ping${RESET} Verify ERP connectivity + API key`);
1434
1557
  console.log(` qualia-framework ${TEAL}erp-flush${RESET} Retry queued ERP report uploads (${DIM}show|clear${RESET})`);
1558
+ console.log(` qualia-framework ${TEAL}work-packet${RESET} Pull/read ERP mission packet (${DIM}pull --project UUID${RESET})`);
1435
1559
  console.log(` qualia-framework ${TEAL}project-snapshot${RESET} Export/upload ERP admin project progress snapshot (${DIM}--write|--upload${RESET})`);
1560
+ console.log(` qualia-framework ${TEAL}planning-hygiene${RESET} Scan/organize .planning artifacts (${DIM}scan|organize --write${RESET})`);
1436
1561
  console.log(` qualia-framework ${TEAL}doctor${RESET} Health-check the install (files, hooks, settings)`);
1562
+ console.log(` qualia-framework ${TEAL}trust${RESET} Score install, state, contracts, memory, ERP (${DIM}--json${RESET})`);
1563
+ console.log(` qualia-framework ${TEAL}eval${RESET} Write/run project harness eval scoring (${DIM}--run --write --json${RESET})`);
1437
1564
  console.log(` qualia-framework ${TEAL}flush${RESET} Promote daily-log → curated knowledge (memory layer)`);
1438
1565
  console.log("");
1439
1566
  console.log(` ${WHITE}After install:${RESET}`);
1440
1567
  console.log(` ${TG}/qualia${RESET} What should I do next?`);
1568
+ console.log(` ${TG}/qualia-doctor${RESET} Health-check install, state, contracts, memory, ERP`);
1441
1569
  console.log(` ${TG}/qualia-new${RESET} Set up a new project`);
1442
1570
  console.log(` ${TG}/qualia-plan${RESET} Plan a phase`);
1443
1571
  console.log(` ${TG}/qualia-build${RESET} Build it (parallel tasks)`);
1444
1572
  console.log(` ${TG}/qualia-verify${RESET} Verify it works`);
1445
1573
  console.log(` ${TG}/qualia-polish${RESET} Design pass — any scope (component, route, app, redesign)`);
1446
- console.log(` ${TG}/qualia-debug${RESET} Structured debugging`);
1574
+ console.log(` ${TG}/qualia-fix${RESET} Root-cause broken behavior, patch, verify`);
1447
1575
  console.log(` ${TG}/qualia-review${RESET} Production audit`);
1448
1576
  console.log(` ${TG}/qualia-ship${RESET} Deploy to production`);
1449
1577
  console.log(` ${TG}/qualia-report${RESET} Log your work`);
@@ -1506,11 +1634,27 @@ switch (cmd) {
1506
1634
  case "snapshot":
1507
1635
  cmdProjectSnapshot();
1508
1636
  break;
1637
+ case "work-packet":
1638
+ case "packet":
1639
+ cmdWorkPacket();
1640
+ break;
1641
+ case "planning-hygiene":
1642
+ case "planning":
1643
+ cmdPlanningHygiene();
1644
+ break;
1509
1645
  case "doctor":
1510
1646
  case "health":
1511
1647
  case "health-check":
1512
1648
  cmdDoctor();
1513
1649
  break;
1650
+ case "trust":
1651
+ case "score":
1652
+ cmdTrust();
1653
+ break;
1654
+ case "eval":
1655
+ case "harness-eval":
1656
+ cmdHarnessEval();
1657
+ break;
1514
1658
  case "flush":
1515
1659
  case "knowledge-flush":
1516
1660
  cmdFlush();
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ // Canonical Qualia command surface.
3
+ //
4
+ // The repo may keep retired skills for migration/history, but installs should
5
+ // expose the small active surface below. This gives users fewer commands while
6
+ // preserving compatibility cleanup for older installs.
7
+
8
+ const ACTIVE_SKILLS = [
9
+ "qualia",
10
+ "qualia-new",
11
+ "qualia-discuss",
12
+ "qualia-map",
13
+ "qualia-research",
14
+ "qualia-plan",
15
+ "qualia-build",
16
+ "qualia-verify",
17
+ "qualia-fix",
18
+ "qualia-feature",
19
+ "qualia-review",
20
+ "qualia-optimize",
21
+ "qualia-polish",
22
+ "qualia-test",
23
+ "qualia-milestone",
24
+ "qualia-ship",
25
+ "qualia-handoff",
26
+ "qualia-report",
27
+ "qualia-doctor",
28
+ "qualia-road",
29
+ "qualia-learn",
30
+ "qualia-postmortem",
31
+ "zoho-workflow",
32
+ ];
33
+
34
+ const RETIRED_SKILLS = [
35
+ // Historical folds.
36
+ "qualia-task",
37
+ "qualia-quick",
38
+ "qualia-polish-loop",
39
+ "qualia-design",
40
+ "qualia-prd",
41
+
42
+ // v6.3 surface reduction: keep the behavior under sharper active commands.
43
+ "qualia-debug", // folded into qualia-fix for actionable repairs
44
+ "qualia-vibe", // folded into qualia-polish modes/documentation
45
+ "qualia-help", // guide/help files remain installed; no slash command
46
+ "qualia-idk", // folded into qualia router diagnostic branch
47
+ "qualia-pause", // folded into qualia router handoff branch
48
+ "qualia-resume", // folded into qualia router handoff branch
49
+ "qualia-zoom", // folded into qualia-map/qualia-review as an analysis mode
50
+ "qualia-issues", // GitHub queue externalization is not default workflow
51
+ "qualia-triage", // GitHub queue routing is not default workflow
52
+ "qualia-hook-gen", // framework-authoring utility, not employee default
53
+ "qualia-skill-new", // framework-authoring utility, not employee default
54
+ "qualia-flush", // available as qualia-framework flush / automation
55
+ ];
56
+
57
+ function activeSkills() {
58
+ return [...ACTIVE_SKILLS];
59
+ }
60
+
61
+ function retiredSkills() {
62
+ return [...RETIRED_SKILLS];
63
+ }
64
+
65
+ module.exports = {
66
+ ACTIVE_SKILLS,
67
+ RETIRED_SKILLS,
68
+ activeSkills,
69
+ retiredSkills,
70
+ };
71
+
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ // Execute Qualia phase contract checks and write evidence.
3
+ // No shell interpolation: command checks run through spawnSync(argv).
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const { spawnSync } = require("child_process");
8
+ const pc = require("./plan-contract.js");
9
+
10
+ function parseArgs(argv) {
11
+ const args = { _: [] };
12
+ for (let i = 2; i < argv.length; i++) {
13
+ const a = argv[i];
14
+ if (a === "--json") args.json = true;
15
+ else if (a === "--no-write") args.no_write = true;
16
+ else if (a === "--cwd") args.cwd = argv[++i];
17
+ else if (a.startsWith("--cwd=")) args.cwd = a.slice("--cwd=".length);
18
+ else args._.push(a);
19
+ }
20
+ return args;
21
+ }
22
+
23
+ function usage() {
24
+ console.error([
25
+ "Usage:",
26
+ " contract-runner.js <contract.json> [--cwd DIR] [--json] [--no-write]",
27
+ "",
28
+ "Runs file-exists, grep-match, command-exit, and behavioral evidence checks.",
29
+ ].join("\n"));
30
+ }
31
+
32
+ function rel(root, p) {
33
+ return path.resolve(root, p);
34
+ }
35
+
36
+ function checkFileExists(root, check) {
37
+ const file = rel(root, check.path);
38
+ if (!fs.existsSync(file)) return { ok: false, detail: `missing file: ${check.path}` };
39
+ if (check.must_contain != null) {
40
+ const content = fs.readFileSync(file, "utf8");
41
+ if (!content.includes(check.must_contain)) {
42
+ return { ok: false, detail: `file does not contain required text: ${check.path}` };
43
+ }
44
+ }
45
+ return { ok: true };
46
+ }
47
+
48
+ function checkGrepMatch(root, check) {
49
+ const file = rel(root, check.path);
50
+ if (!fs.existsSync(file)) return { ok: false, detail: `missing file: ${check.path}` };
51
+ const content = fs.readFileSync(file, "utf8");
52
+ const re = new RegExp(check.pattern);
53
+ const present = re.test(content);
54
+ if (check.expect === "present" && !present) return { ok: false, detail: `pattern absent: ${check.pattern}` };
55
+ if (check.expect === "absent" && present) return { ok: false, detail: `pattern present: ${check.pattern}` };
56
+ return { ok: true };
57
+ }
58
+
59
+ function checkCommandExit(root, check) {
60
+ const started = Date.now();
61
+ const r = spawnSync(check.command, check.args || [], {
62
+ cwd: root,
63
+ encoding: "utf8",
64
+ timeout: check.timeout_ms || 30_000,
65
+ stdio: ["ignore", "pipe", "pipe"],
66
+ shell: false,
67
+ });
68
+ const status = typeof r.status === "number" ? r.status : 1;
69
+ if (status !== check.expected_exit) {
70
+ return {
71
+ ok: false,
72
+ detail: `exit ${status}, expected ${check.expected_exit}`,
73
+ duration_ms: Date.now() - started,
74
+ stdout: (r.stdout || "").slice(-1000),
75
+ stderr: (r.stderr || r.error?.message || "").slice(-1000),
76
+ };
77
+ }
78
+ if (check.expect_stdout_match != null) {
79
+ const re = new RegExp(check.expect_stdout_match);
80
+ if (!re.test(r.stdout || "")) {
81
+ return {
82
+ ok: false,
83
+ detail: `stdout did not match: ${check.expect_stdout_match}`,
84
+ duration_ms: Date.now() - started,
85
+ stdout: (r.stdout || "").slice(-1000),
86
+ stderr: (r.stderr || "").slice(-1000),
87
+ };
88
+ }
89
+ }
90
+ return {
91
+ ok: true,
92
+ duration_ms: Date.now() - started,
93
+ stdout: (r.stdout || "").slice(-1000),
94
+ stderr: (r.stderr || "").slice(-1000),
95
+ };
96
+ }
97
+
98
+ function checkBehavioral(root, check) {
99
+ for (const ev of check.evidence_required || []) {
100
+ const file = rel(root, ev.path);
101
+ if (!fs.existsSync(file)) {
102
+ return { ok: false, detail: `missing evidence file: ${ev.path}` };
103
+ }
104
+ if (ev.matcher != null) {
105
+ const content = fs.readFileSync(file, "utf8");
106
+ const re = new RegExp(ev.matcher);
107
+ if (!re.test(content)) {
108
+ return { ok: false, detail: `evidence matcher failed for ${ev.path}: ${ev.matcher}` };
109
+ }
110
+ }
111
+ }
112
+ return { ok: true };
113
+ }
114
+
115
+ function runCheck(root, check) {
116
+ try {
117
+ if (check.type === "file-exists") return checkFileExists(root, check);
118
+ if (check.type === "grep-match") return checkGrepMatch(root, check);
119
+ if (check.type === "command-exit") return checkCommandExit(root, check);
120
+ if (check.type === "behavioral") return checkBehavioral(root, check);
121
+ return { ok: false, detail: `unknown check type: ${check.type}` };
122
+ } catch (e) {
123
+ return { ok: false, detail: e.message };
124
+ }
125
+ }
126
+
127
+ function writeEvidence(root, contract, result) {
128
+ const dir = path.join(root, ".planning", "evidence");
129
+ fs.mkdirSync(dir, { recursive: true });
130
+ const phase = Number(contract.phase || 0) || "unknown";
131
+ const file = path.join(dir, `phase-${phase}-contract-run.json`);
132
+ fs.writeFileSync(file, JSON.stringify(result, null, 2) + "\n");
133
+ return path.relative(root, file);
134
+ }
135
+
136
+ function runContract(contract, opts = {}) {
137
+ const root = path.resolve(opts.cwd || process.cwd());
138
+ const errors = pc.validate(contract);
139
+ if (errors.length > 0) {
140
+ return {
141
+ ok: false,
142
+ error: "CONTRACT_INVALID",
143
+ errors,
144
+ checked: 0,
145
+ failed: errors.length,
146
+ results: [],
147
+ };
148
+ }
149
+
150
+ const results = [];
151
+ for (const task of contract.tasks || []) {
152
+ for (let i = 0; i < (task.verification || []).length; i++) {
153
+ const check = task.verification[i];
154
+ const r = runCheck(root, check);
155
+ results.push({
156
+ task_id: task.id,
157
+ task_title: task.title,
158
+ index: i,
159
+ type: check.type,
160
+ ok: !!r.ok,
161
+ detail: r.detail || "",
162
+ duration_ms: r.duration_ms,
163
+ stdout: r.stdout,
164
+ stderr: r.stderr,
165
+ });
166
+ }
167
+ }
168
+ const failed = results.filter((r) => !r.ok).length;
169
+ const payload = {
170
+ ok: failed === 0,
171
+ phase: contract.phase,
172
+ goal: contract.goal,
173
+ checked: results.length,
174
+ failed,
175
+ generated_at: new Date().toISOString(),
176
+ results,
177
+ };
178
+ if (!opts.no_write) payload.evidence_file = writeEvidence(root, contract, payload);
179
+ return payload;
180
+ }
181
+
182
+ function main(argv) {
183
+ const args = parseArgs(argv);
184
+ const contractPath = args._[0];
185
+ if (!contractPath || contractPath === "--help" || contractPath === "-h") {
186
+ usage();
187
+ return 2;
188
+ }
189
+ const loaded = pc.readContractFile(contractPath);
190
+ if (!loaded.ok) {
191
+ const payload = { ok: false, ...loaded, path: contractPath };
192
+ if (args.json) console.log(JSON.stringify(payload, null, 2));
193
+ else console.error(`${payload.error}: ${payload.message}`);
194
+ return 2;
195
+ }
196
+ const result = runContract(loaded.contract, {
197
+ cwd: args.cwd,
198
+ no_write: args.no_write,
199
+ });
200
+ if (args.json) {
201
+ console.log(JSON.stringify(result, null, 2));
202
+ } else if (result.ok) {
203
+ console.log(`PASS phase ${result.phase}: ${result.checked} check(s)`);
204
+ if (result.evidence_file) console.log(`Evidence: ${result.evidence_file}`);
205
+ } else {
206
+ console.error(`FAIL phase ${result.phase || "?"}: ${result.failed} of ${result.checked || result.failed} check(s) failed`);
207
+ for (const r of result.results || []) {
208
+ if (!r.ok) console.error(`- ${r.task_id} ${r.type}: ${r.detail}`);
209
+ }
210
+ if (result.error) for (const e of result.errors || []) console.error(`- ${e}`);
211
+ }
212
+ return result.ok ? 0 : 1;
213
+ }
214
+
215
+ module.exports = { runContract, runCheck };
216
+
217
+ if (require.main === module) {
218
+ process.exit(main(process.argv));
219
+ }