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.
- package/README.md +15 -11
- package/agents/builder.md +28 -0
- package/agents/research-synthesizer.md +7 -0
- package/bin/agent-runs.js +233 -0
- package/bin/cli.js +355 -16
- package/bin/install.js +87 -6
- package/bin/knowledge-flush.js +164 -0
- package/bin/knowledge.js +317 -0
- package/bin/plan-contract.js +220 -0
- package/bin/state.js +15 -9
- package/docs/agent-runs.md +273 -0
- package/docs/journey-demo.html +1008 -0
- package/docs/plan-contract.md +321 -0
- package/docs/reviews/v4.1.0-audit.html +1488 -0
- package/docs/reviews/v4.1.0-audit.md +263 -0
- package/hooks/auto-update.js +3 -7
- package/hooks/git-guardrails.js +167 -0
- package/hooks/pre-compact.js +22 -11
- package/hooks/pre-deploy-gate.js +16 -2
- package/hooks/pre-push.js +22 -2
- package/hooks/stop-session-log.js +180 -0
- package/package.json +8 -2
- package/skills/qualia-build/SKILL.md +5 -5
- package/skills/qualia-debug/SKILL.md +1 -1
- package/skills/qualia-design/SKILL.md +15 -0
- package/skills/qualia-flush/SKILL.md +200 -0
- package/skills/qualia-learn/SKILL.md +47 -37
- package/skills/qualia-new/SKILL.md +1 -1
- package/skills/qualia-plan/SKILL.md +3 -2
- package/skills/qualia-postmortem/SKILL.md +238 -0
- package/skills/qualia-quick/SKILL.md +1 -1
- package/skills/qualia-report/SKILL.md +1 -1
- package/skills/qualia-review/SKILL.md +3 -2
- package/skills/qualia-ship/SKILL.md +12 -10
- package/skills/qualia-verify/SKILL.md +60 -0
- package/templates/help.html +13 -7
- package/templates/knowledge/agents.md +71 -0
- package/templates/knowledge/index.md +47 -0
- package/tests/bin.test.sh +322 -12
- package/tests/hooks.test.sh +131 -20
- package/tests/lib.test.sh +217 -0
- package/tests/runner.js +103 -77
- 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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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("
|
|
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,
|
|
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(
|
|
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 },
|