qualia-framework 3.6.0 → 4.0.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 +96 -51
- package/agents/builder.md +25 -14
- package/agents/plan-checker.md +29 -16
- package/agents/planner.md +33 -24
- package/agents/research-synthesizer.md +25 -12
- package/agents/roadmapper.md +89 -84
- package/agents/verifier.md +11 -2
- package/bin/qualia-ui.js +267 -1
- package/bin/state.js +90 -5
- package/guide.md +84 -21
- package/package.json +1 -1
- package/skills/qualia/SKILL.md +3 -1
- package/skills/qualia-build/SKILL.md +39 -4
- package/skills/qualia-handoff/SKILL.md +87 -12
- package/skills/qualia-idk/SKILL.md +160 -0
- package/skills/qualia-milestone/SKILL.md +122 -79
- package/skills/qualia-new/SKILL.md +151 -230
- package/skills/qualia-plan/SKILL.md +14 -9
- package/skills/qualia-report/SKILL.md +12 -0
- package/skills/qualia-verify/SKILL.md +57 -3
- package/templates/journey.md +113 -0
- package/templates/plan.md +56 -11
- package/templates/requirements.md +82 -22
- package/templates/roadmap.md +41 -14
- package/templates/tracking.json +2 -0
- package/tests/runner.js +169 -4
package/tests/runner.js
CHANGED
|
@@ -835,9 +835,11 @@ waves: 1
|
|
|
835
835
|
it("init --force overwrites an existing project (preserves lifetime)", () => {
|
|
836
836
|
const tmpDir = makeProject();
|
|
837
837
|
try {
|
|
838
|
-
// Seed lifetime via close-milestone first
|
|
838
|
+
// Seed lifetime via close-milestone first. --force bypasses the v4
|
|
839
|
+
// readiness guards (MILESTONE_NOT_READY) since this test doesn't
|
|
840
|
+
// exercise the verification flow — it's focused on lifetime preservation.
|
|
839
841
|
const c = spawnSync(process.execPath, [
|
|
840
|
-
path.join(BIN, "state.js"), "close-milestone",
|
|
842
|
+
path.join(BIN, "state.js"), "close-milestone", "--force",
|
|
841
843
|
], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
842
844
|
assert.equal(c.status, 0);
|
|
843
845
|
const tBefore = JSON.parse(fs.readFileSync(path.join(tmpDir, ".planning", "tracking.json"), "utf8"));
|
|
@@ -863,8 +865,10 @@ waves: 1
|
|
|
863
865
|
it("close-milestone refuses double-close (idempotency)", () => {
|
|
864
866
|
const tmpDir = makeProject();
|
|
865
867
|
try {
|
|
868
|
+
// First close uses --force to bypass v4 readiness guards — this test
|
|
869
|
+
// focuses on the ALREADY_CLOSED sentinel, not phase-verification gates.
|
|
866
870
|
const r1 = spawnSync(process.execPath, [
|
|
867
|
-
path.join(BIN, "state.js"), "close-milestone",
|
|
871
|
+
path.join(BIN, "state.js"), "close-milestone", "--force",
|
|
868
872
|
], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
869
873
|
assert.equal(r1.status, 0);
|
|
870
874
|
const out1 = JSON.parse(r1.stdout);
|
|
@@ -878,6 +882,8 @@ waves: 1
|
|
|
878
882
|
t.milestone = out1.closed_milestone; // rewind
|
|
879
883
|
fs.writeFileSync(tFile, JSON.stringify(t, null, 2) + "\n");
|
|
880
884
|
|
|
885
|
+
// Second close (without --force) must fail with ALREADY_CLOSED, which
|
|
886
|
+
// is checked BEFORE the readiness guards in cmdCloseMilestone.
|
|
881
887
|
const r2 = spawnSync(process.execPath, [
|
|
882
888
|
path.join(BIN, "state.js"), "close-milestone",
|
|
883
889
|
], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -893,7 +899,7 @@ waves: 1
|
|
|
893
899
|
const tmpDir = makeProject();
|
|
894
900
|
try {
|
|
895
901
|
const r1 = spawnSync(process.execPath, [
|
|
896
|
-
path.join(BIN, "state.js"), "close-milestone",
|
|
902
|
+
path.join(BIN, "state.js"), "close-milestone", "--force",
|
|
897
903
|
], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
898
904
|
assert.equal(r1.status, 0);
|
|
899
905
|
|
|
@@ -1002,6 +1008,165 @@ waves: 1
|
|
|
1002
1008
|
}
|
|
1003
1009
|
});
|
|
1004
1010
|
|
|
1011
|
+
// ─── v4.0.0: milestone readiness guards + milestones[] summary ─
|
|
1012
|
+
it("close-milestone refuses unverified phases (MILESTONE_NOT_READY)", () => {
|
|
1013
|
+
const tmpDir = makeProject();
|
|
1014
|
+
try {
|
|
1015
|
+
// No phases verified yet — close-milestone (without --force) must refuse.
|
|
1016
|
+
const r = spawnSync(process.execPath, [
|
|
1017
|
+
path.join(BIN, "state.js"), "close-milestone",
|
|
1018
|
+
], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1019
|
+
assert.equal(r.status, 1);
|
|
1020
|
+
const out = JSON.parse(r.stdout);
|
|
1021
|
+
assert.equal(out.error, "MILESTONE_NOT_READY");
|
|
1022
|
+
} finally {
|
|
1023
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it("close-milestone refuses single-phase milestones (MILESTONE_TOO_SMALL)", () => {
|
|
1028
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-single-"));
|
|
1029
|
+
try {
|
|
1030
|
+
const init = spawnSync(process.execPath, [
|
|
1031
|
+
path.join(BIN, "state.js"), "init",
|
|
1032
|
+
"--project", "SingleProject",
|
|
1033
|
+
"--phases", '[{"name":"Only","goal":"Y"}]',
|
|
1034
|
+
], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1035
|
+
assert.equal(init.status, 0);
|
|
1036
|
+
|
|
1037
|
+
// Single-phase milestone — even if the phase were verified, the size
|
|
1038
|
+
// guard catches it first. A milestone needs ≥ 2 phases without --force.
|
|
1039
|
+
const r = spawnSync(process.execPath, [
|
|
1040
|
+
path.join(BIN, "state.js"), "close-milestone",
|
|
1041
|
+
], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1042
|
+
assert.equal(r.status, 1);
|
|
1043
|
+
const out = JSON.parse(r.stdout);
|
|
1044
|
+
assert.equal(out.error, "MILESTONE_TOO_SMALL");
|
|
1045
|
+
} finally {
|
|
1046
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
it("close-milestone appends a summary to milestones[]", () => {
|
|
1051
|
+
const tmpDir = makeProject();
|
|
1052
|
+
try {
|
|
1053
|
+
const r = spawnSync(process.execPath, [
|
|
1054
|
+
path.join(BIN, "state.js"), "close-milestone", "--force",
|
|
1055
|
+
], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1056
|
+
assert.equal(r.status, 0);
|
|
1057
|
+
|
|
1058
|
+
const tFile = path.join(tmpDir, ".planning", "tracking.json");
|
|
1059
|
+
const t = JSON.parse(fs.readFileSync(tFile, "utf8"));
|
|
1060
|
+
assert.ok(Array.isArray(t.milestones), "milestones[] must exist");
|
|
1061
|
+
assert.equal(t.milestones.length, 1);
|
|
1062
|
+
const m1 = t.milestones[0];
|
|
1063
|
+
assert.equal(m1.num, 1);
|
|
1064
|
+
assert.ok(m1.total_phases >= 2, "total_phases should reflect seeded phases");
|
|
1065
|
+
assert.ok(typeof m1.closed_at === "string" && m1.closed_at.length > 0);
|
|
1066
|
+
} finally {
|
|
1067
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
it("milestone summary captures cumulative tasks_completed, not current phase only", () => {
|
|
1072
|
+
const tmpDir = makeProject();
|
|
1073
|
+
try {
|
|
1074
|
+
// Simulate 2 phases each with 3 tasks verified pass. This bumps
|
|
1075
|
+
// lifetime.tasks_completed to 6. The milestone close summary should
|
|
1076
|
+
// reflect 6, not just 3 (the last phase's tasks_done).
|
|
1077
|
+
for (const phase of [1, 2]) {
|
|
1078
|
+
const planFile = path.join(tmpDir, ".planning", `phase-${phase}-plan.md`);
|
|
1079
|
+
fs.writeFileSync(planFile, `---
|
|
1080
|
+
phase: ${phase}
|
|
1081
|
+
goal: "x"
|
|
1082
|
+
tasks: 1
|
|
1083
|
+
waves: 1
|
|
1084
|
+
---
|
|
1085
|
+
|
|
1086
|
+
## Task 1 — x
|
|
1087
|
+
**Wave:** 1
|
|
1088
|
+
**Files:** x.ts
|
|
1089
|
+
**Depends on:** none
|
|
1090
|
+
**Acceptance Criteria:**
|
|
1091
|
+
- ok
|
|
1092
|
+
`);
|
|
1093
|
+
const verFile = path.join(tmpDir, ".planning", `phase-${phase}-verification.md`);
|
|
1094
|
+
// Plan → built → verified
|
|
1095
|
+
let r = spawnSync(process.execPath, [path.join(BIN, "state.js"), "transition", "--to", "planned", "--phase", String(phase)],
|
|
1096
|
+
{ encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1097
|
+
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"],
|
|
1099
|
+
{ encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1100
|
+
assert.equal(r.status, 0, `built transition failed for phase ${phase}: ${r.stderr || r.stdout}`);
|
|
1101
|
+
fs.writeFileSync(verFile, "result: PASS");
|
|
1102
|
+
r = spawnSync(process.execPath, [path.join(BIN, "state.js"), "transition", "--to", "verified", "--phase", String(phase), "--verification", "pass"],
|
|
1103
|
+
{ encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1104
|
+
assert.equal(r.status, 0, `verified transition failed for phase ${phase}: ${r.stderr || r.stdout}`);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Close milestone
|
|
1108
|
+
const r = spawnSync(process.execPath, [path.join(BIN, "state.js"), "close-milestone"],
|
|
1109
|
+
{ encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1110
|
+
assert.equal(r.status, 0, `close-milestone failed: ${r.stderr || r.stdout}`);
|
|
1111
|
+
|
|
1112
|
+
const t = JSON.parse(fs.readFileSync(path.join(tmpDir, ".planning", "tracking.json"), "utf8"));
|
|
1113
|
+
assert.equal(t.lifetime.tasks_completed, 6, "lifetime should have 6 tasks (2 phases × 3 tasks)");
|
|
1114
|
+
assert.equal(t.milestones.length, 1);
|
|
1115
|
+
assert.equal(t.milestones[0].tasks_completed, 6, "milestone summary should cumulate all 6 tasks, not just the last phase's 3");
|
|
1116
|
+
assert.equal(t.milestones[0].phases_completed, 2);
|
|
1117
|
+
} finally {
|
|
1118
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
it("build_count bumps on each 'built' transition", () => {
|
|
1123
|
+
const tmpDir = makeProject();
|
|
1124
|
+
try {
|
|
1125
|
+
const tFile = path.join(tmpDir, ".planning", "tracking.json");
|
|
1126
|
+
const before = JSON.parse(fs.readFileSync(tFile, "utf8")).build_count || 0;
|
|
1127
|
+
|
|
1128
|
+
fs.writeFileSync(path.join(tmpDir, ".planning", "phase-1-plan.md"), `---
|
|
1129
|
+
phase: 1
|
|
1130
|
+
goal: "x"
|
|
1131
|
+
tasks: 1
|
|
1132
|
+
waves: 1
|
|
1133
|
+
---
|
|
1134
|
+
|
|
1135
|
+
## Task 1 — x
|
|
1136
|
+
**Wave:** 1
|
|
1137
|
+
**Files:** x.ts
|
|
1138
|
+
**Depends on:** none
|
|
1139
|
+
**Acceptance Criteria:**
|
|
1140
|
+
- ok
|
|
1141
|
+
`);
|
|
1142
|
+
spawnSync(process.execPath, [path.join(BIN, "state.js"), "transition", "--to", "planned", "--phase", "1"],
|
|
1143
|
+
{ 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"],
|
|
1145
|
+
{ encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1146
|
+
assert.equal(r.status, 0);
|
|
1147
|
+
|
|
1148
|
+
const after = JSON.parse(fs.readFileSync(tFile, "utf8")).build_count || 0;
|
|
1149
|
+
assert.equal(after, before + 1, "build_count should bump on 'built' transition");
|
|
1150
|
+
} finally {
|
|
1151
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
it("check exposes milestones[] and milestone_name in output", () => {
|
|
1156
|
+
const tmpDir = makeProject();
|
|
1157
|
+
try {
|
|
1158
|
+
const r = spawnSync(process.execPath, [
|
|
1159
|
+
path.join(BIN, "state.js"), "check",
|
|
1160
|
+
], { encoding: "utf8", cwd: tmpDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1161
|
+
assert.equal(r.status, 0);
|
|
1162
|
+
const out = JSON.parse(r.stdout);
|
|
1163
|
+
assert.ok(Array.isArray(out.milestones), "check must expose milestones[]");
|
|
1164
|
+
assert.ok(typeof out.milestone_name === "string", "check must expose milestone_name");
|
|
1165
|
+
} finally {
|
|
1166
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1005
1170
|
// ─── v3.5.0: CRLF tolerance in parseStateMd ────────────
|
|
1006
1171
|
it("parseStateMd tolerates CRLF line endings (Windows-edited STATE.md)", () => {
|
|
1007
1172
|
const tmpDir = makeProject();
|