qualia-framework 4.1.1 → 4.4.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 (43) hide show
  1. package/README.md +15 -11
  2. package/agents/builder.md +28 -0
  3. package/agents/research-synthesizer.md +7 -0
  4. package/bin/agent-runs.js +233 -0
  5. package/bin/cli.js +355 -16
  6. package/bin/install.js +87 -6
  7. package/bin/knowledge-flush.js +164 -0
  8. package/bin/knowledge.js +317 -0
  9. package/bin/plan-contract.js +220 -0
  10. package/bin/state.js +15 -9
  11. package/docs/agent-runs.md +273 -0
  12. package/docs/journey-demo.html +1008 -0
  13. package/docs/plan-contract.md +321 -0
  14. package/docs/reviews/v4.1.0-audit.html +1488 -0
  15. package/docs/reviews/v4.1.0-audit.md +263 -0
  16. package/hooks/auto-update.js +3 -7
  17. package/hooks/git-guardrails.js +167 -0
  18. package/hooks/pre-compact.js +22 -11
  19. package/hooks/pre-deploy-gate.js +16 -2
  20. package/hooks/pre-push.js +22 -2
  21. package/hooks/stop-session-log.js +180 -0
  22. package/package.json +8 -2
  23. package/skills/qualia-build/SKILL.md +5 -5
  24. package/skills/qualia-debug/SKILL.md +1 -1
  25. package/skills/qualia-design/SKILL.md +15 -0
  26. package/skills/qualia-flush/SKILL.md +200 -0
  27. package/skills/qualia-learn/SKILL.md +47 -37
  28. package/skills/qualia-new/SKILL.md +1 -1
  29. package/skills/qualia-plan/SKILL.md +3 -2
  30. package/skills/qualia-postmortem/SKILL.md +238 -0
  31. package/skills/qualia-quick/SKILL.md +1 -1
  32. package/skills/qualia-report/SKILL.md +1 -1
  33. package/skills/qualia-review/SKILL.md +3 -2
  34. package/skills/qualia-ship/SKILL.md +12 -10
  35. package/skills/qualia-verify/SKILL.md +60 -0
  36. package/templates/help.html +13 -7
  37. package/templates/knowledge/agents.md +71 -0
  38. package/templates/knowledge/index.md +47 -0
  39. package/tests/bin.test.sh +322 -12
  40. package/tests/hooks.test.sh +131 -20
  41. package/tests/lib.test.sh +217 -0
  42. package/tests/runner.js +103 -77
  43. package/tests/state.test.sh +4 -3
package/tests/runner.js CHANGED
@@ -12,10 +12,11 @@ const os = require("os");
12
12
  const ROOT = path.resolve(__dirname, "..");
13
13
  const BIN = path.join(ROOT, "bin");
14
14
  const HOOKS = path.join(ROOT, "hooks");
15
+ const NODE = process.env.NODE || "node";
15
16
 
16
17
  // Helper: run a bin/ script and return {stdout, stderr, status}
17
18
  function run(script, args = [], opts = {}) {
18
- const result = spawnSync(process.execPath, [path.join(BIN, script), ...args], {
19
+ const result = spawnSync(NODE, [path.join(BIN, script), ...args], {
19
20
  encoding: "utf8",
20
21
  timeout: 10000,
21
22
  cwd: opts.cwd || ROOT,
@@ -29,7 +30,7 @@ function run(script, args = [], opts = {}) {
29
30
  // Helper: run a hook with JSON input on stdin
30
31
  function runHook(hookFile, jsonInput) {
31
32
  const hookPath = path.join(HOOKS, hookFile);
32
- const result = spawnSync(process.execPath, [hookPath], {
33
+ const result = spawnSync(NODE, [hookPath], {
33
34
  encoding: "utf8",
34
35
  timeout: 5000,
35
36
  input: JSON.stringify(jsonInput),
@@ -41,7 +42,7 @@ function runHook(hookFile, jsonInput) {
41
42
 
42
43
  // Helper: run state.js with args in a given cwd
43
44
  function runState(args, cwd) {
44
- const result = spawnSync(process.execPath, [path.join(BIN, "state.js"), ...args], {
45
+ const result = spawnSync(NODE, [path.join(BIN, "state.js"), ...args], {
45
46
  encoding: "utf8",
46
47
  timeout: 5000,
47
48
  cwd,
@@ -65,7 +66,7 @@ function withTempPlanning(fn) {
65
66
  // Helper: create a full temp project (init with 2 phases)
66
67
  function makeProject() {
67
68
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-proj-"));
68
- const r = spawnSync(process.execPath, [
69
+ const r = spawnSync(NODE, [
69
70
  path.join(BIN, "state.js"), "init",
70
71
  "--project", "TestProject",
71
72
  "--phases", '[{"name":"Foundation","goal":"Auth"},{"name":"Core","goal":"Features"}]',
@@ -144,6 +145,10 @@ describe("CLI", () => {
144
145
  assert.match(clean, /team/);
145
146
  assert.match(clean, /traces/);
146
147
  assert.match(clean, /analytics/);
148
+ assert.match(clean, /set-erp-key/);
149
+ assert.match(clean, /erp-ping/);
150
+ assert.match(clean, /doctor/);
151
+ assert.match(clean, /flush/);
147
152
  } finally {
148
153
  fs.rmSync(tmpHome, { recursive: true, force: true });
149
154
  }
@@ -264,6 +269,22 @@ describe("CLI", () => {
264
269
  fs.rmSync(tmpHome, { recursive: true, force: true });
265
270
  }
266
271
  });
272
+
273
+ it("set-erp-key saves key and enables ERP", () => {
274
+ const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-cli-"));
275
+ try {
276
+ const r = run("cli.js", ["set-erp-key", "test-erp-key-12345"], {
277
+ env: { HOME: tmpHome, USERPROFILE: tmpHome },
278
+ });
279
+ assert.equal(r.status, 0);
280
+ const keyPath = path.join(tmpHome, ".claude", ".erp-api-key");
281
+ assert.equal(fs.readFileSync(keyPath, "utf8"), "test-erp-key-12345");
282
+ const cfg = JSON.parse(fs.readFileSync(path.join(tmpHome, ".claude", ".qualia-config.json"), "utf8"));
283
+ assert.equal(cfg.erp.enabled, true);
284
+ } finally {
285
+ fs.rmSync(tmpHome, { recursive: true, force: true });
286
+ }
287
+ });
267
288
  });
268
289
 
269
290
  // ═══════════════════════════════════════════════════════════
@@ -286,7 +307,7 @@ describe("State Machine", () => {
286
307
 
287
308
  it("init creates state and tracking files", () => {
288
309
  withTempPlanning((tmpDir) => {
289
- const r = spawnSync(process.execPath, [
310
+ const r = spawnSync(NODE, [
290
311
  path.join(BIN, "state.js"), "init",
291
312
  "--project", "test-proj",
292
313
  "--phases", '[{"name":"Foundation","goal":"Auth"}]',
@@ -819,7 +840,7 @@ waves: 1
819
840
  it("init refuses to clobber an existing project (no --force)", () => {
820
841
  const tmpDir = makeProject();
821
842
  try {
822
- const r = spawnSync(process.execPath, [
843
+ const r = spawnSync(NODE, [
823
844
  path.join(BIN, "state.js"), "init",
824
845
  "--project", "TestProject",
825
846
  "--phases", '[{"name":"X","goal":"Y"}]',
@@ -838,14 +859,14 @@ waves: 1
838
859
  // Seed lifetime via close-milestone first. --force bypasses the v4
839
860
  // readiness guards (MILESTONE_NOT_READY) since this test doesn't
840
861
  // exercise the verification flow — it's focused on lifetime preservation.
841
- const c = spawnSync(process.execPath, [
862
+ const c = spawnSync(NODE, [
842
863
  path.join(BIN, "state.js"), "close-milestone", "--force",
843
864
  ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
844
865
  assert.equal(c.status, 0);
845
866
  const tBefore = JSON.parse(fs.readFileSync(path.join(tmpDir, ".planning", "tracking.json"), "utf8"));
846
867
  assert.ok(tBefore.lifetime.milestones_completed >= 1);
847
868
 
848
- const r = spawnSync(process.execPath, [
869
+ const r = spawnSync(NODE, [
849
870
  path.join(BIN, "state.js"), "init",
850
871
  "--project", "TestProject",
851
872
  "--phases", '[{"name":"NewFoundation","goal":"X"}]',
@@ -867,7 +888,7 @@ waves: 1
867
888
  try {
868
889
  // First close uses --force to bypass v4 readiness guards — this test
869
890
  // focuses on the ALREADY_CLOSED sentinel, not phase-verification gates.
870
- const r1 = spawnSync(process.execPath, [
891
+ const r1 = spawnSync(NODE, [
871
892
  path.join(BIN, "state.js"), "close-milestone", "--force",
872
893
  ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
873
894
  assert.equal(r1.status, 0);
@@ -884,7 +905,7 @@ waves: 1
884
905
 
885
906
  // Second close (without --force) must fail with ALREADY_CLOSED, which
886
907
  // is checked BEFORE the readiness guards in cmdCloseMilestone.
887
- const r2 = spawnSync(process.execPath, [
908
+ const r2 = spawnSync(NODE, [
888
909
  path.join(BIN, "state.js"), "close-milestone",
889
910
  ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
890
911
  assert.equal(r2.status, 1);
@@ -898,7 +919,7 @@ waves: 1
898
919
  it("close-milestone --force allows re-close", () => {
899
920
  const tmpDir = makeProject();
900
921
  try {
901
- const r1 = spawnSync(process.execPath, [
922
+ const r1 = spawnSync(NODE, [
902
923
  path.join(BIN, "state.js"), "close-milestone", "--force",
903
924
  ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
904
925
  assert.equal(r1.status, 0);
@@ -908,7 +929,7 @@ waves: 1
908
929
  t.milestone = JSON.parse(r1.stdout).closed_milestone;
909
930
  fs.writeFileSync(tFile, JSON.stringify(t, null, 2) + "\n");
910
931
 
911
- const r2 = spawnSync(process.execPath, [
932
+ const r2 = spawnSync(NODE, [
912
933
  path.join(BIN, "state.js"), "close-milestone", "--force",
913
934
  ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
914
935
  assert.equal(r2.status, 0);
@@ -931,7 +952,7 @@ waves: 1
931
952
  fs.writeFileSync(tFile, JSON.stringify(t, null, 2) + "\n");
932
953
 
933
954
  // Backfill on a project with NO completed phases would compute 0/0
934
- const r = spawnSync(process.execPath, [
955
+ const r = spawnSync(NODE, [
935
956
  path.join(BIN, "state.js"), "backfill-lifetime",
936
957
  ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
937
958
  assert.equal(r.status, 0);
@@ -987,7 +1008,7 @@ waves: 1
987
1008
  t.lifetime = { tasks_completed: 5 }; // partial — missing other keys
988
1009
  fs.writeFileSync(tFile, JSON.stringify(t, null, 2) + "\n");
989
1010
 
990
- const r = spawnSync(process.execPath, [
1011
+ const r = spawnSync(NODE, [
991
1012
  path.join(BIN, "state.js"), "init",
992
1013
  "--project", "TestProject",
993
1014
  "--phases", '[{"name":"X","goal":"Y"}]',
@@ -1013,7 +1034,7 @@ waves: 1
1013
1034
  const tmpDir = makeProject();
1014
1035
  try {
1015
1036
  // No phases verified yet — close-milestone (without --force) must refuse.
1016
- const r = spawnSync(process.execPath, [
1037
+ const r = spawnSync(NODE, [
1017
1038
  path.join(BIN, "state.js"), "close-milestone",
1018
1039
  ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1019
1040
  assert.equal(r.status, 1);
@@ -1027,7 +1048,7 @@ waves: 1
1027
1048
  it("close-milestone refuses single-phase milestones (MILESTONE_TOO_SMALL)", () => {
1028
1049
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-single-"));
1029
1050
  try {
1030
- const init = spawnSync(process.execPath, [
1051
+ const init = spawnSync(NODE, [
1031
1052
  path.join(BIN, "state.js"), "init",
1032
1053
  "--project", "SingleProject",
1033
1054
  "--phases", '[{"name":"Only","goal":"Y"}]',
@@ -1036,7 +1057,7 @@ waves: 1
1036
1057
 
1037
1058
  // Single-phase milestone — even if the phase were verified, the size
1038
1059
  // guard catches it first. A milestone needs ≥ 2 phases without --force.
1039
- const r = spawnSync(process.execPath, [
1060
+ const r = spawnSync(NODE, [
1040
1061
  path.join(BIN, "state.js"), "close-milestone",
1041
1062
  ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1042
1063
  assert.equal(r.status, 1);
@@ -1050,7 +1071,7 @@ waves: 1
1050
1071
  it("close-milestone appends a summary to milestones[]", () => {
1051
1072
  const tmpDir = makeProject();
1052
1073
  try {
1053
- const r = spawnSync(process.execPath, [
1074
+ const r = spawnSync(NODE, [
1054
1075
  path.join(BIN, "state.js"), "close-milestone", "--force",
1055
1076
  ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1056
1077
  assert.equal(r.status, 0);
@@ -1092,20 +1113,20 @@ waves: 1
1092
1113
  `);
1093
1114
  const verFile = path.join(tmpDir, ".planning", `phase-${phase}-verification.md`);
1094
1115
  // Plan → built → verified
1095
- let r = spawnSync(process.execPath, [path.join(BIN, "state.js"), "transition", "--to", "planned", "--phase", String(phase)],
1116
+ let r = spawnSync(NODE, [path.join(BIN, "state.js"), "transition", "--to", "planned", "--phase", String(phase)],
1096
1117
  { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1097
1118
  assert.equal(r.status, 0, `planned transition failed for phase ${phase}: ${r.stderr || r.stdout}`);
1098
- r = spawnSync(process.execPath, [path.join(BIN, "state.js"), "transition", "--to", "built", "--phase", String(phase), "--tasks-done", "3", "--tasks-total", "3"],
1119
+ r = spawnSync(NODE, [path.join(BIN, "state.js"), "transition", "--to", "built", "--phase", String(phase), "--tasks-done", "3", "--tasks-total", "3"],
1099
1120
  { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1100
1121
  assert.equal(r.status, 0, `built transition failed for phase ${phase}: ${r.stderr || r.stdout}`);
1101
1122
  fs.writeFileSync(verFile, "result: PASS");
1102
- r = spawnSync(process.execPath, [path.join(BIN, "state.js"), "transition", "--to", "verified", "--phase", String(phase), "--verification", "pass"],
1123
+ r = spawnSync(NODE, [path.join(BIN, "state.js"), "transition", "--to", "verified", "--phase", String(phase), "--verification", "pass"],
1103
1124
  { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1104
1125
  assert.equal(r.status, 0, `verified transition failed for phase ${phase}: ${r.stderr || r.stdout}`);
1105
1126
  }
1106
1127
 
1107
1128
  // Close milestone
1108
- const r = spawnSync(process.execPath, [path.join(BIN, "state.js"), "close-milestone"],
1129
+ const r = spawnSync(NODE, [path.join(BIN, "state.js"), "close-milestone"],
1109
1130
  { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1110
1131
  assert.equal(r.status, 0, `close-milestone failed: ${r.stderr || r.stdout}`);
1111
1132
 
@@ -1139,9 +1160,9 @@ waves: 1
1139
1160
  **Acceptance Criteria:**
1140
1161
  - ok
1141
1162
  `);
1142
- spawnSync(process.execPath, [path.join(BIN, "state.js"), "transition", "--to", "planned", "--phase", "1"],
1163
+ spawnSync(NODE, [path.join(BIN, "state.js"), "transition", "--to", "planned", "--phase", "1"],
1143
1164
  { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1144
- const r = spawnSync(process.execPath, [path.join(BIN, "state.js"), "transition", "--to", "built", "--phase", "1", "--tasks-done", "1", "--tasks-total", "1"],
1165
+ const r = spawnSync(NODE, [path.join(BIN, "state.js"), "transition", "--to", "built", "--phase", "1", "--tasks-done", "1", "--tasks-total", "1"],
1145
1166
  { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1146
1167
  assert.equal(r.status, 0);
1147
1168
 
@@ -1155,7 +1176,7 @@ waves: 1
1155
1176
  it("check exposes milestones[] and milestone_name in output", () => {
1156
1177
  const tmpDir = makeProject();
1157
1178
  try {
1158
- const r = spawnSync(process.execPath, [
1179
+ const r = spawnSync(NODE, [
1159
1180
  path.join(BIN, "state.js"), "check",
1160
1181
  ], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1161
1182
  assert.equal(r.status, 0);
@@ -1293,7 +1314,7 @@ waves: 1
1293
1314
  it("next-report-id returns QS-REPORT-01 on fresh project and increments", () => {
1294
1315
  const tmpDir = makeProject();
1295
1316
  try {
1296
- const r1 = spawnSync(process.execPath,
1317
+ const r1 = spawnSync(NODE,
1297
1318
  [path.join(BIN, "state.js"), "next-report-id"],
1298
1319
  { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1299
1320
  assert.equal(r1.status, 0, `next-report-id failed: ${r1.stderr || r1.stdout}`);
@@ -1302,7 +1323,7 @@ waves: 1
1302
1323
  assert.equal(j1.report_seq, 1);
1303
1324
  assert.equal(j1.peeked, false);
1304
1325
 
1305
- const r2 = spawnSync(process.execPath,
1326
+ const r2 = spawnSync(NODE,
1306
1327
  [path.join(BIN, "state.js"), "next-report-id"],
1307
1328
  { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1308
1329
  const j2 = JSON.parse(r2.stdout);
@@ -1316,7 +1337,7 @@ waves: 1
1316
1337
  it("next-report-id --peek does NOT increment the counter", () => {
1317
1338
  const tmpDir = makeProject();
1318
1339
  try {
1319
- const r1 = spawnSync(process.execPath,
1340
+ const r1 = spawnSync(NODE,
1320
1341
  [path.join(BIN, "state.js"), "next-report-id", "--peek"],
1321
1342
  { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1322
1343
  const j1 = JSON.parse(r1.stdout);
@@ -1324,7 +1345,7 @@ waves: 1
1324
1345
  assert.equal(j1.peeked, true);
1325
1346
 
1326
1347
  // Peek again — should still return QS-REPORT-01 since nothing incremented
1327
- const r2 = spawnSync(process.execPath,
1348
+ const r2 = spawnSync(NODE,
1328
1349
  [path.join(BIN, "state.js"), "next-report-id", "--peek"],
1329
1350
  { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1330
1351
  const j2 = JSON.parse(r2.stdout);
@@ -1356,7 +1377,7 @@ Exit: auth + dashboard
1356
1377
  ## Milestone 3 · Handoff [FINAL]
1357
1378
  Exit: client takeover
1358
1379
  `);
1359
- const r = spawnSync(process.execPath,
1380
+ const r = spawnSync(NODE,
1360
1381
  [path.join(BIN, "state.js"), "close-milestone", "--force"],
1361
1382
  { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1362
1383
  assert.equal(r.status, 0, `close-milestone failed: ${r.stderr || r.stdout}`);
@@ -1374,7 +1395,7 @@ Exit: client takeover
1374
1395
  const tmpDir = makeProject();
1375
1396
  try {
1376
1397
  // No JOURNEY.md — milestone_name should fall back to blank (legacy behavior)
1377
- const r = spawnSync(process.execPath,
1398
+ const r = spawnSync(NODE,
1378
1399
  [path.join(BIN, "state.js"), "close-milestone", "--force"],
1379
1400
  { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1380
1401
  assert.equal(r.status, 0);
@@ -1397,7 +1418,7 @@ describe("Hooks", () => {
1397
1418
  const hooks = fs.readdirSync(HOOKS).filter(f => f.endsWith(".js"));
1398
1419
  assert.ok(hooks.length >= 7, `Expected 7+ hooks, found ${hooks.length}`);
1399
1420
  for (const hook of hooks) {
1400
- const r = spawnSync(process.execPath, ["--check", path.join(HOOKS, hook)], {
1421
+ const r = spawnSync(NODE, ["--check", path.join(HOOKS, hook)], {
1401
1422
  encoding: "utf8", timeout: 5000,
1402
1423
  });
1403
1424
  assert.equal(r.status, 0, `Syntax error in ${hook}: ${r.stderr}`);
@@ -1516,7 +1537,7 @@ describe("Hooks", () => {
1516
1537
  const headBefore = spawnSync("git", ["rev-parse", "HEAD"], gitOpts).stdout.trim();
1517
1538
 
1518
1539
  // Run the hook
1519
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-push.js")], {
1540
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-push.js")], {
1520
1541
  encoding: "utf8", cwd: tmpDir, timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
1521
1542
  });
1522
1543
  assert.equal(r.status, 0, `pre-push exited ${r.status}: ${r.stderr}`);
@@ -1542,7 +1563,7 @@ describe("Hooks", () => {
1542
1563
  it("pre-push.js exits 0 with no tracking.json", () => {
1543
1564
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-push-"));
1544
1565
  try {
1545
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-push.js")], {
1566
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-push.js")], {
1546
1567
  encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
1547
1568
  });
1548
1569
  assert.equal(r.status, 0);
@@ -1556,7 +1577,7 @@ describe("Hooks", () => {
1556
1577
  it("session-start.js exits 0 with no project", () => {
1557
1578
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-ss-"));
1558
1579
  try {
1559
- const r = spawnSync(process.execPath, [path.join(HOOKS, "session-start.js")], {
1580
+ const r = spawnSync(NODE, [path.join(HOOKS, "session-start.js")], {
1560
1581
  encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
1561
1582
  });
1562
1583
  assert.equal(r.status, 0);
@@ -1571,7 +1592,7 @@ describe("Hooks", () => {
1571
1592
  const planningDir = path.join(tmpDir, ".planning");
1572
1593
  fs.mkdirSync(planningDir, { recursive: true });
1573
1594
  fs.writeFileSync(path.join(planningDir, "STATE.md"), "# Project State\nPhase: 1 of 3 — Foundation\nStatus: setup\n");
1574
- const r = spawnSync(process.execPath, [path.join(HOOKS, "session-start.js")], {
1595
+ const r = spawnSync(NODE, [path.join(HOOKS, "session-start.js")], {
1575
1596
  encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
1576
1597
  });
1577
1598
  assert.equal(r.status, 0);
@@ -1585,7 +1606,7 @@ describe("Hooks", () => {
1585
1606
  it("pre-compact.js exits 0 with no STATE.md", () => {
1586
1607
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-pc-"));
1587
1608
  try {
1588
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-compact.js")], {
1609
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-compact.js")], {
1589
1610
  encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
1590
1611
  });
1591
1612
  assert.equal(r.status, 0);
@@ -1603,7 +1624,7 @@ describe("Hooks", () => {
1603
1624
  fs.writeFileSync(path.join(tmpHome, ".claude", ".qualia-config.json"), JSON.stringify({
1604
1625
  code: "QS-FAWZI-01", version: "99.99.99",
1605
1626
  }));
1606
- const r = spawnSync(process.execPath, [path.join(HOOKS, "auto-update.js")], {
1627
+ const r = spawnSync(NODE, [path.join(HOOKS, "auto-update.js")], {
1607
1628
  encoding: "utf8", timeout: 5000,
1608
1629
  env: { ...process.env, HOME: tmpHome, USERPROFILE: tmpHome },
1609
1630
  stdio: ["pipe", "pipe", "pipe"],
@@ -1620,7 +1641,7 @@ describe("Hooks", () => {
1620
1641
  it("pre-deploy-gate: empty project exits 0", () => {
1621
1642
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-pdg-"));
1622
1643
  try {
1623
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-deploy-gate.js")], {
1644
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-deploy-gate.js")], {
1624
1645
  encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
1625
1646
  });
1626
1647
  assert.equal(r.status, 0);
@@ -1634,7 +1655,7 @@ describe("Hooks", () => {
1634
1655
  try {
1635
1656
  fs.mkdirSync(path.join(tmpDir, "src"), { recursive: true });
1636
1657
  fs.writeFileSync(path.join(tmpDir, "src", "app.ts"), "export const x = 1;");
1637
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-deploy-gate.js")], {
1658
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-deploy-gate.js")], {
1638
1659
  encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
1639
1660
  });
1640
1661
  assert.equal(r.status, 0);
@@ -1648,7 +1669,7 @@ describe("Hooks", () => {
1648
1669
  try {
1649
1670
  fs.mkdirSync(path.join(tmpDir, "app"), { recursive: true });
1650
1671
  fs.writeFileSync(path.join(tmpDir, "app", "page.tsx"), 'const key = "service_role_literal_leak";\nexport default function P(){return null}');
1651
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-deploy-gate.js")], {
1672
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-deploy-gate.js")], {
1652
1673
  encoding: "utf8", cwd: tmpDir, timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
1653
1674
  });
1654
1675
  assert.equal(r.status, 2, "PreToolUse hook must exit 2 to block");
@@ -1665,7 +1686,7 @@ describe("Hooks", () => {
1665
1686
  try {
1666
1687
  fs.mkdirSync(path.join(tmpDir, "components"), { recursive: true });
1667
1688
  fs.writeFileSync(path.join(tmpDir, "components", "Widget.tsx"), 'const key = "service_role_literal_leak";');
1668
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-deploy-gate.js")], {
1689
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-deploy-gate.js")], {
1669
1690
  encoding: "utf8", cwd: tmpDir, timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
1670
1691
  });
1671
1692
  assert.equal(r.status, 2, "PreToolUse hook must exit 2 to block");
@@ -1679,7 +1700,7 @@ describe("Hooks", () => {
1679
1700
  try {
1680
1701
  fs.mkdirSync(path.join(tmpDir, "app", "api"), { recursive: true });
1681
1702
  fs.writeFileSync(path.join(tmpDir, "app", "api", "route.server.ts"), 'const key = "service_role_legit_server_key";');
1682
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-deploy-gate.js")], {
1703
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-deploy-gate.js")], {
1683
1704
  encoding: "utf8", cwd: tmpDir, timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
1684
1705
  });
1685
1706
  assert.equal(r.status, 0);
@@ -1693,7 +1714,7 @@ describe("Hooks", () => {
1693
1714
  try {
1694
1715
  fs.mkdirSync(path.join(tmpDir, "app", "server"), { recursive: true });
1695
1716
  fs.writeFileSync(path.join(tmpDir, "app", "server", "admin.ts"), 'const key = "service_role_legit_server_dir";');
1696
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-deploy-gate.js")], {
1717
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-deploy-gate.js")], {
1697
1718
  encoding: "utf8", cwd: tmpDir, timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
1698
1719
  });
1699
1720
  assert.equal(r.status, 0);
@@ -1707,7 +1728,7 @@ describe("Hooks", () => {
1707
1728
  try {
1708
1729
  fs.mkdirSync(path.join(tmpDir, "app", "node_modules", "evil"), { recursive: true });
1709
1730
  fs.writeFileSync(path.join(tmpDir, "app", "node_modules", "evil", "index.ts"), 'const key = "service_role_in_node_modules";');
1710
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-deploy-gate.js")], {
1731
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-deploy-gate.js")], {
1711
1732
  encoding: "utf8", cwd: tmpDir, timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
1712
1733
  });
1713
1734
  assert.equal(r.status, 0);
@@ -1725,7 +1746,7 @@ describe("Hooks", () => {
1725
1746
  fs.writeFileSync(path.join(tmpDir, "app", "page.tsx"), "export const a = 1;");
1726
1747
  fs.writeFileSync(path.join(tmpDir, "components", "Widget.tsx"), "export const b = 2;");
1727
1748
  fs.writeFileSync(path.join(tmpDir, "lib", "util.ts"), "export const c = 3;");
1728
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-deploy-gate.js")], {
1749
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-deploy-gate.js")], {
1729
1750
  encoding: "utf8", cwd: tmpDir, timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
1730
1751
  });
1731
1752
  assert.equal(r.status, 0);
@@ -1741,7 +1762,7 @@ describe("Hooks", () => {
1741
1762
  fs.mkdirSync(path.join(tmpDir, "app", "api", "auth"), { recursive: true });
1742
1763
  fs.writeFileSync(path.join(tmpDir, "app", "api", "auth", "route.ts"),
1743
1764
  'const key = process.env.SUPABASE_SERVICE_ROLE_KEY; export async function POST() {}');
1744
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-deploy-gate.js")], {
1765
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-deploy-gate.js")], {
1745
1766
  encoding: "utf8", cwd: tmpDir, timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
1746
1767
  });
1747
1768
  assert.equal(r.status, 0);
@@ -1755,7 +1776,7 @@ describe("Hooks", () => {
1755
1776
  try {
1756
1777
  fs.writeFileSync(path.join(tmpDir, "middleware.ts"),
1757
1778
  'import { service_role } from "./config"; export function middleware() {}');
1758
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-deploy-gate.js")], {
1779
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-deploy-gate.js")], {
1759
1780
  encoding: "utf8", cwd: tmpDir, timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
1760
1781
  });
1761
1782
  assert.equal(r.status, 0);
@@ -1770,7 +1791,7 @@ describe("Hooks", () => {
1770
1791
  fs.mkdirSync(path.join(tmpDir, "app", "api", "webhook"), { recursive: true });
1771
1792
  fs.writeFileSync(path.join(tmpDir, "app", "api", "webhook", "route.js"),
1772
1793
  'const sr = "service_role"; export async function GET() { return new Response(sr); }');
1773
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-deploy-gate.js")], {
1794
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-deploy-gate.js")], {
1774
1795
  encoding: "utf8", cwd: tmpDir, timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
1775
1796
  });
1776
1797
  assert.equal(r.status, 0);
@@ -1785,7 +1806,7 @@ describe("Hooks", () => {
1785
1806
  fs.mkdirSync(path.join(tmpDir, "app", "admin"), { recursive: true });
1786
1807
  fs.writeFileSync(path.join(tmpDir, "app", "admin", "actions.ts"),
1787
1808
  '"use server"\nconst key = process.env.SUPABASE_SERVICE_ROLE_KEY;\nexport async function deleteUser() {}\n');
1788
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-deploy-gate.js")], {
1809
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-deploy-gate.js")], {
1789
1810
  encoding: "utf8", cwd: tmpDir, timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
1790
1811
  });
1791
1812
  assert.equal(r.status, 0);
@@ -1800,7 +1821,7 @@ describe("Hooks", () => {
1800
1821
  fs.mkdirSync(path.join(tmpDir, "app", "admin"), { recursive: true });
1801
1822
  fs.writeFileSync(path.join(tmpDir, "app", "admin", "page.tsx"),
1802
1823
  'const key = "service_role"; export default function Page() { return <div>{key}</div>; }');
1803
- const r = spawnSync(process.execPath, [path.join(HOOKS, "pre-deploy-gate.js")], {
1824
+ const r = spawnSync(NODE, [path.join(HOOKS, "pre-deploy-gate.js")], {
1804
1825
  encoding: "utf8", cwd: tmpDir, timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
1805
1826
  });
1806
1827
  assert.equal(r.status, 2, "PreToolUse hook must exit 2 to block");
@@ -1820,7 +1841,7 @@ describe("Hooks", () => {
1820
1841
  spawnSync("git", ["init", "-q"], { cwd: projDir });
1821
1842
  spawnSync("git", ["checkout", "-b", "main", "-q"], { cwd: projDir, stdio: "pipe" });
1822
1843
  fs.writeFileSync(path.join(tmpDir, ".claude", ".qualia-config.json"), JSON.stringify({ role: "OWNER" }));
1823
- const r = spawnSync(process.execPath, [path.join(HOOKS, "branch-guard.js")], {
1844
+ const r = spawnSync(NODE, [path.join(HOOKS, "branch-guard.js")], {
1824
1845
  encoding: "utf8", cwd: projDir, timeout: 5000,
1825
1846
  env: { ...process.env, HOME: tmpDir, USERPROFILE: tmpDir },
1826
1847
  stdio: ["pipe", "pipe", "pipe"],
@@ -1840,7 +1861,7 @@ describe("Hooks", () => {
1840
1861
  spawnSync("git", ["init", "-q"], { cwd: projDir });
1841
1862
  spawnSync("git", ["checkout", "-b", "main", "-q"], { cwd: projDir, stdio: "pipe" });
1842
1863
  fs.writeFileSync(path.join(tmpDir, ".claude", ".qualia-config.json"), JSON.stringify({ role: "EMPLOYEE" }));
1843
- const r = spawnSync(process.execPath, [path.join(HOOKS, "branch-guard.js")], {
1864
+ const r = spawnSync(NODE, [path.join(HOOKS, "branch-guard.js")], {
1844
1865
  encoding: "utf8", cwd: projDir, timeout: 5000,
1845
1866
  env: { ...process.env, HOME: tmpDir, USERPROFILE: tmpDir },
1846
1867
  stdio: ["pipe", "pipe", "pipe"],
@@ -1861,7 +1882,7 @@ describe("Hooks", () => {
1861
1882
  spawnSync("git", ["init", "-q"], { cwd: projDir });
1862
1883
  spawnSync("git", ["checkout", "-b", "feature/xyz", "-q"], { cwd: projDir, stdio: "pipe" });
1863
1884
  fs.writeFileSync(path.join(tmpDir, ".claude", ".qualia-config.json"), JSON.stringify({ role: "EMPLOYEE" }));
1864
- const r = spawnSync(process.execPath, [path.join(HOOKS, "branch-guard.js")], {
1885
+ const r = spawnSync(NODE, [path.join(HOOKS, "branch-guard.js")], {
1865
1886
  encoding: "utf8", cwd: projDir, timeout: 5000,
1866
1887
  env: { ...process.env, HOME: tmpDir, USERPROFILE: tmpDir },
1867
1888
  stdio: ["pipe", "pipe", "pipe"],
@@ -1880,7 +1901,7 @@ describe("Hooks", () => {
1880
1901
  spawnSync("git", ["init", "-q"], { cwd: projDir });
1881
1902
  spawnSync("git", ["checkout", "-b", "feature/x", "-q"], { cwd: projDir, stdio: "pipe" });
1882
1903
  // No .claude/.qualia-config.json
1883
- const r = spawnSync(process.execPath, [path.join(HOOKS, "branch-guard.js")], {
1904
+ const r = spawnSync(NODE, [path.join(HOOKS, "branch-guard.js")], {
1884
1905
  encoding: "utf8", cwd: projDir, timeout: 5000,
1885
1906
  env: { ...process.env, HOME: tmpDir, USERPROFILE: tmpDir },
1886
1907
  stdio: ["pipe", "pipe", "pipe"],
@@ -1900,7 +1921,7 @@ describe("Hooks", () => {
1900
1921
  spawnSync("git", ["init", "-q"], { cwd: projDir });
1901
1922
  spawnSync("git", ["checkout", "-b", "feature/x", "-q"], { cwd: projDir, stdio: "pipe" });
1902
1923
  fs.writeFileSync(path.join(tmpDir, ".claude", ".qualia-config.json"), "not json{");
1903
- const r = spawnSync(process.execPath, [path.join(HOOKS, "branch-guard.js")], {
1924
+ const r = spawnSync(NODE, [path.join(HOOKS, "branch-guard.js")], {
1904
1925
  encoding: "utf8", cwd: projDir, timeout: 5000,
1905
1926
  env: { ...process.env, HOME: tmpDir, USERPROFILE: tmpDir },
1906
1927
  stdio: ["pipe", "pipe", "pipe"],
@@ -1920,7 +1941,7 @@ describe("Hooks", () => {
1920
1941
  spawnSync("git", ["init", "-q"], { cwd: projDir });
1921
1942
  spawnSync("git", ["checkout", "-b", "feature/x", "-q"], { cwd: projDir, stdio: "pipe" });
1922
1943
  fs.writeFileSync(path.join(tmpDir, ".claude", ".qualia-config.json"), JSON.stringify({ role: "" }));
1923
- const r = spawnSync(process.execPath, [path.join(HOOKS, "branch-guard.js")], {
1944
+ const r = spawnSync(NODE, [path.join(HOOKS, "branch-guard.js")], {
1924
1945
  encoding: "utf8", cwd: projDir, timeout: 5000,
1925
1946
  env: { ...process.env, HOME: tmpDir, USERPROFILE: tmpDir },
1926
1947
  stdio: ["pipe", "pipe", "pipe"],
@@ -1946,7 +1967,7 @@ describe("Hooks", () => {
1946
1967
  const payload = JSON.stringify({
1947
1968
  tool_input: { command: "git push origin feature/x:main" },
1948
1969
  });
1949
- const r = spawnSync(process.execPath, [path.join(HOOKS, "branch-guard.js")], {
1970
+ const r = spawnSync(NODE, [path.join(HOOKS, "branch-guard.js")], {
1950
1971
  encoding: "utf8", cwd: projDir, timeout: 5000,
1951
1972
  env: { ...process.env, HOME: tmpDir, USERPROFILE: tmpDir },
1952
1973
  input: payload,
@@ -1970,7 +1991,7 @@ describe("Hooks", () => {
1970
1991
  const payload = JSON.stringify({
1971
1992
  tool_input: { command: "git push origin HEAD:master" },
1972
1993
  });
1973
- const r = spawnSync(process.execPath, [path.join(HOOKS, "branch-guard.js")], {
1994
+ const r = spawnSync(NODE, [path.join(HOOKS, "branch-guard.js")], {
1974
1995
  encoding: "utf8", cwd: projDir, timeout: 5000,
1975
1996
  env: { ...process.env, HOME: tmpDir, USERPROFILE: tmpDir },
1976
1997
  input: payload,
@@ -1994,7 +2015,7 @@ describe("Hooks", () => {
1994
2015
  const payload = JSON.stringify({
1995
2016
  tool_input: { command: "git push origin feature/x:main" },
1996
2017
  });
1997
- const r = spawnSync(process.execPath, [path.join(HOOKS, "branch-guard.js")], {
2018
+ const r = spawnSync(NODE, [path.join(HOOKS, "branch-guard.js")], {
1998
2019
  encoding: "utf8", cwd: projDir, timeout: 5000,
1999
2020
  env: { ...process.env, HOME: tmpDir, USERPROFILE: tmpDir },
2000
2021
  input: payload,
@@ -2089,14 +2110,14 @@ describe("Hooks", () => {
2089
2110
 
2090
2111
  describe("Statusline", () => {
2091
2112
  it("statusline.js passes syntax check", () => {
2092
- const r = spawnSync(process.execPath, ["--check", path.join(BIN, "statusline.js")], {
2113
+ const r = spawnSync(NODE, ["--check", path.join(BIN, "statusline.js")], {
2093
2114
  encoding: "utf8", timeout: 5000,
2094
2115
  });
2095
2116
  assert.equal(r.status, 0);
2096
2117
  });
2097
2118
 
2098
2119
  it("statusline.js runs without crashing", () => {
2099
- const r = spawnSync(process.execPath, [path.join(BIN, "statusline.js")], {
2120
+ const r = spawnSync(NODE, [path.join(BIN, "statusline.js")], {
2100
2121
  encoding: "utf8", timeout: 5000,
2101
2122
  env: { ...process.env, HOME: os.tmpdir(), USERPROFILE: os.tmpdir() },
2102
2123
  stdio: ["pipe", "pipe", "pipe"],
@@ -2105,7 +2126,7 @@ describe("Statusline", () => {
2105
2126
  });
2106
2127
 
2107
2128
  it("qualia-ui.js passes syntax check", () => {
2108
- const r = spawnSync(process.execPath, ["--check", path.join(BIN, "qualia-ui.js")], {
2129
+ const r = spawnSync(NODE, ["--check", path.join(BIN, "qualia-ui.js")], {
2109
2130
  encoding: "utf8", timeout: 5000,
2110
2131
  });
2111
2132
  assert.equal(r.status, 0);
@@ -2121,7 +2142,7 @@ describe("Statusline", () => {
2121
2142
  agent: {},
2122
2143
  worktree: {},
2123
2144
  });
2124
- const r = spawnSync(process.execPath, [path.join(BIN, "statusline.js")], {
2145
+ const r = spawnSync(NODE, [path.join(BIN, "statusline.js")], {
2125
2146
  encoding: "utf8", timeout: 5000,
2126
2147
  input: json,
2127
2148
  stdio: ["pipe", "pipe", "pipe"],
@@ -2143,7 +2164,7 @@ describe("Statusline", () => {
2143
2164
  agent: {},
2144
2165
  worktree: {},
2145
2166
  });
2146
- const r = spawnSync(process.execPath, [path.join(BIN, "statusline.js")], {
2167
+ const r = spawnSync(NODE, [path.join(BIN, "statusline.js")], {
2147
2168
  encoding: "utf8", timeout: 5000,
2148
2169
  input: json,
2149
2170
  stdio: ["pipe", "pipe", "pipe"],
@@ -2162,7 +2183,7 @@ describe("Statusline", () => {
2162
2183
  agent: {},
2163
2184
  worktree: {},
2164
2185
  });
2165
- const r = spawnSync(process.execPath, [path.join(BIN, "statusline.js")], {
2186
+ const r = spawnSync(NODE, [path.join(BIN, "statusline.js")], {
2166
2187
  encoding: "utf8", timeout: 5000,
2167
2188
  input: json,
2168
2189
  stdio: ["pipe", "pipe", "pipe"],
@@ -2181,7 +2202,7 @@ describe("Statusline", () => {
2181
2202
  agent: {},
2182
2203
  worktree: {},
2183
2204
  });
2184
- const r = spawnSync(process.execPath, [path.join(BIN, "statusline.js")], {
2205
+ const r = spawnSync(NODE, [path.join(BIN, "statusline.js")], {
2185
2206
  encoding: "utf8", timeout: 5000,
2186
2207
  input: json,
2187
2208
  stdio: ["pipe", "pipe", "pipe"],
@@ -2200,7 +2221,7 @@ describe("Statusline", () => {
2200
2221
  agent: { name: "qualia-planner" },
2201
2222
  worktree: {},
2202
2223
  });
2203
- const r = spawnSync(process.execPath, [path.join(BIN, "statusline.js")], {
2224
+ const r = spawnSync(NODE, [path.join(BIN, "statusline.js")], {
2204
2225
  encoding: "utf8", timeout: 5000,
2205
2226
  input: json,
2206
2227
  stdio: ["pipe", "pipe", "pipe"],
@@ -2210,7 +2231,7 @@ describe("Statusline", () => {
2210
2231
  });
2211
2232
 
2212
2233
  it("statusline handles empty stdin gracefully", () => {
2213
- const r = spawnSync(process.execPath, [path.join(BIN, "statusline.js")], {
2234
+ const r = spawnSync(NODE, [path.join(BIN, "statusline.js")], {
2214
2235
  encoding: "utf8", timeout: 5000,
2215
2236
  input: "",
2216
2237
  stdio: ["pipe", "pipe", "pipe"],
@@ -2221,7 +2242,7 @@ describe("Statusline", () => {
2221
2242
  });
2222
2243
 
2223
2244
  it("statusline handles invalid JSON gracefully", () => {
2224
- const r = spawnSync(process.execPath, [path.join(BIN, "statusline.js")], {
2245
+ const r = spawnSync(NODE, [path.join(BIN, "statusline.js")], {
2225
2246
  encoding: "utf8", timeout: 5000,
2226
2247
  input: "not json{",
2227
2248
  stdio: ["pipe", "pipe", "pipe"],
@@ -2245,7 +2266,7 @@ describe("Statusline", () => {
2245
2266
  agent: {},
2246
2267
  worktree: {},
2247
2268
  });
2248
- const r = spawnSync(process.execPath, [path.join(BIN, "statusline.js")], {
2269
+ const r = spawnSync(NODE, [path.join(BIN, "statusline.js")], {
2249
2270
  encoding: "utf8", timeout: 5000,
2250
2271
  input: json,
2251
2272
  stdio: ["pipe", "pipe", "pipe"],
@@ -2271,7 +2292,7 @@ describe("Statusline", () => {
2271
2292
  agent: {},
2272
2293
  worktree: {},
2273
2294
  });
2274
- const r = spawnSync(process.execPath, [path.join(BIN, "statusline.js")], {
2295
+ const r = spawnSync(NODE, [path.join(BIN, "statusline.js")], {
2275
2296
  encoding: "utf8", timeout: 5000,
2276
2297
  input: json,
2277
2298
  stdio: ["pipe", "pipe", "pipe"],
@@ -2294,7 +2315,7 @@ describe("qualia-ui.js", () => {
2294
2315
 
2295
2316
  function runUI(args, opts = {}) {
2296
2317
  const tmpHome = opts.home || os.tmpdir();
2297
- const r = spawnSync(process.execPath, [UI, ...args], {
2318
+ const r = spawnSync(NODE, [UI, ...args], {
2298
2319
  encoding: "utf8", timeout: 5000,
2299
2320
  cwd: opts.cwd || os.tmpdir(),
2300
2321
  env: { ...process.env, HOME: tmpHome, USERPROFILE: tmpHome },
@@ -2517,7 +2538,7 @@ describe("install.js", () => {
2517
2538
  const INSTALL = path.join(BIN, "install.js");
2518
2539
 
2519
2540
  function runInstall(code, home) {
2520
- const r = spawnSync(process.execPath, [INSTALL], {
2541
+ const r = spawnSync(NODE, [INSTALL], {
2521
2542
  encoding: "utf8", timeout: 15000,
2522
2543
  input: code + "\n",
2523
2544
  env: { ...process.env, HOME: home, USERPROFILE: home },
@@ -2536,7 +2557,12 @@ describe("install.js", () => {
2536
2557
  assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "bin", "state.js")));
2537
2558
  assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "bin", "qualia-ui.js")));
2538
2559
  assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "bin", "statusline.js")));
2560
+ assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "bin", "knowledge.js")));
2539
2561
  assert.ok(fs.existsSync(path.join(tmpHome, ".claude", ".qualia-config.json")));
2562
+ // v4.2.0 — knowledge layer must be initialized
2563
+ assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "knowledge", "agents.md")));
2564
+ assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "knowledge", "index.md")));
2565
+ assert.ok(fs.existsSync(path.join(tmpHome, ".claude", "knowledge", "daily-log")));
2540
2566
  } finally {
2541
2567
  fs.rmSync(tmpHome, { recursive: true, force: true });
2542
2568
  }
@@ -2567,12 +2593,12 @@ describe("install.js", () => {
2567
2593
  }
2568
2594
  });
2569
2595
 
2570
- it("7 hooks installed (block-env-edit removed in v3.2.0)", () => {
2596
+ it("9 hooks installed (block-env-edit removed in v3.2.0; git-guardrails + stop-session-log added in v4.2.0)", () => {
2571
2597
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2572
2598
  try {
2573
2599
  runInstall("QS-FAWZI-01", tmpHome);
2574
2600
  const hooks = fs.readdirSync(path.join(tmpHome, ".claude", "hooks")).filter(f => f.endsWith(".js"));
2575
- assert.equal(hooks.length, 7);
2601
+ assert.equal(hooks.length, 9);
2576
2602
  } finally {
2577
2603
  fs.rmSync(tmpHome, { recursive: true, force: true });
2578
2604
  }
@@ -2644,7 +2670,7 @@ describe("install.js", () => {
2644
2670
  it("empty code exits 1", () => {
2645
2671
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
2646
2672
  try {
2647
- const r = spawnSync(process.execPath, [INSTALL], {
2673
+ const r = spawnSync(NODE, [INSTALL], {
2648
2674
  encoding: "utf8", timeout: 15000,
2649
2675
  input: "\n",
2650
2676
  env: { ...process.env, HOME: tmpHome, USERPROFILE: tmpHome },