qualia-framework 6.9.2 → 6.22.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/AGENTS.md +8 -5
- package/CHANGELOG.md +208 -0
- package/CLAUDE.md +3 -1
- package/agents/roadmapper.md +16 -14
- package/agents/verifier.md +1 -1
- package/bin/agent-status.js +264 -0
- package/bin/analyze-gate.js +318 -0
- package/bin/branch-hygiene.js +135 -0
- package/bin/command-surface.js +2 -0
- package/bin/compile-instructions.js +82 -0
- package/bin/eval-runner.js +218 -0
- package/bin/host-adapters.js +72 -12
- package/bin/install.js +27 -17
- package/bin/last-report.js +207 -0
- package/bin/project-sync.js +315 -0
- package/bin/report-payload.js +7 -0
- package/bin/runtime-manifest.js +8 -0
- package/bin/state.js +257 -12
- package/bin/verify-panel.js +294 -0
- package/bin/wave-plan.js +211 -0
- package/docs/EMPLOYEE-QUICKSTART.md +3 -3
- package/docs/erp-contract.md +168 -0
- package/docs/qualia-manual.html +5 -5
- package/hooks/branch-guard.js +133 -63
- package/hooks/pre-deploy-gate.js +38 -0
- package/hooks/task-write-guard.js +165 -0
- package/package.json +3 -2
- package/rules/codex-goal.md +28 -26
- package/rules/infrastructure.md +1 -1
- package/skills/qualia/SKILL.md +6 -0
- package/skills/qualia-build/SKILL.md +39 -7
- package/skills/qualia-eval/SKILL.md +83 -0
- package/skills/qualia-feature/SKILL.md +20 -4
- package/skills/qualia-fix/SKILL.md +13 -1
- package/skills/qualia-milestone/SKILL.md +12 -6
- package/skills/qualia-new/REFERENCE.md +6 -4
- package/skills/qualia-new/SKILL.md +27 -15
- package/skills/qualia-plan/SKILL.md +2 -2
- package/skills/qualia-report/SKILL.md +10 -0
- package/skills/qualia-scope/SKILL.md +3 -3
- package/skills/qualia-ship/SKILL.md +37 -4
- package/skills/qualia-update/SKILL.md +100 -0
- package/skills/qualia-verify/SKILL.md +51 -24
- package/templates/instructions.md +32 -0
- package/templates/journey.md +2 -2
- package/templates/project-discovery.md +30 -23
- package/templates/requirements.md +7 -7
- package/tests/agent-status.test.sh +153 -0
- package/tests/analyze-gate.test.sh +170 -0
- package/tests/bin.test.sh +5 -4
- package/tests/branch-hygiene.test.sh +93 -0
- package/tests/eval-runner.test.sh +147 -0
- package/tests/hooks.test.sh +218 -17
- package/tests/install-smoke.test.sh +4 -3
- package/tests/instructions.test.sh +109 -0
- package/tests/last-report.test.sh +156 -0
- package/tests/lib.test.sh +2 -2
- package/tests/project-sync.test.sh +175 -0
- package/tests/run-all.sh +9 -0
- package/tests/runner.js +3 -2
- package/tests/state.test.sh +187 -0
- package/tests/verify-panel.test.sh +162 -0
- package/tests/wave-plan.test.sh +153 -0
- package/skills/qualia-discuss/SKILL.md +0 -222
package/bin/state.js
CHANGED
|
@@ -607,9 +607,28 @@ function cmdTransitionIncrement(opts, target) {
|
|
|
607
607
|
const c = parseInt(opts.tasks_done) || 0;
|
|
608
608
|
if (c > 0) t.lifetime.tasks_completed += c;
|
|
609
609
|
}
|
|
610
|
+
// Scope tagging (anti-drift): /qualia-feature + /qualia-fix declare whether
|
|
611
|
+
// the work served the active milestone (--scope in --ref CORE-03) or was
|
|
612
|
+
// off-road (--scope off). Off-road work is COUNTED + ledgered so it can't
|
|
613
|
+
// drift invisibly — the OWNER + ERP see the tally, mirroring branch-guard.
|
|
614
|
+
const scope = String(opts.scope || "").toLowerCase();
|
|
615
|
+
if (scope === "off" || scope === "in") {
|
|
616
|
+
if (typeof t.lifetime.offroad_count !== "number") t.lifetime.offroad_count = 0;
|
|
617
|
+
if (scope === "off") {
|
|
618
|
+
t.lifetime.offroad_count += 1;
|
|
619
|
+
if (!Array.isArray(t.offroad)) t.offroad = [];
|
|
620
|
+
t.offroad.push({
|
|
621
|
+
at: new Date().toISOString(),
|
|
622
|
+
milestone: parseInt(t.milestone, 10) || null,
|
|
623
|
+
ref: opts.ref || null,
|
|
624
|
+
note: opts.notes || null,
|
|
625
|
+
});
|
|
626
|
+
if (t.offroad.length > 50) t.offroad = t.offroad.slice(-50); // keep recent
|
|
627
|
+
}
|
|
628
|
+
}
|
|
610
629
|
writeTracking(t);
|
|
611
630
|
regenerateViews(opts.notes || "Activity logged");
|
|
612
|
-
return output({ ok: true, action: target, layout: "increments" });
|
|
631
|
+
return output({ ok: true, action: target, layout: "increments", scope: scope || undefined, offroad_count: t.lifetime.offroad_count });
|
|
613
632
|
}
|
|
614
633
|
|
|
615
634
|
// Resolve the target increment: explicit --id, else --phase N (back-compat
|
|
@@ -706,6 +725,9 @@ function ensureLifetime(t) {
|
|
|
706
725
|
total_phases: 0,
|
|
707
726
|
};
|
|
708
727
|
}
|
|
728
|
+
// v7 lifecycle (backward compat): pre-v7 tracking.json predates these fields.
|
|
729
|
+
if (t.lifecycle !== "operate" && t.lifecycle !== "build") t.lifecycle = "build";
|
|
730
|
+
if (typeof t.lifetime.updates_completed !== "number") t.lifetime.updates_completed = 0;
|
|
709
731
|
return t;
|
|
710
732
|
}
|
|
711
733
|
|
|
@@ -756,6 +778,31 @@ function normalizeMilestoneName(name) {
|
|
|
756
778
|
return String(name == null ? "" : name).trim().toLowerCase();
|
|
757
779
|
}
|
|
758
780
|
|
|
781
|
+
// Parse the REQUIREMENTS.md traceability table for one milestone's REQ-IDs and
|
|
782
|
+
// their status. Rows look like: `| CORE-01 | M2: Name | Phase 3 | Complete |`.
|
|
783
|
+
// Returns { tracked: bool, total, incomplete: [{id, status}] }. tracked=false
|
|
784
|
+
// when REQUIREMENTS.md is absent or has no rows for this milestone (→ gate
|
|
785
|
+
// skips, like analyze-gate's no-scope-file: can't enforce what isn't declared).
|
|
786
|
+
function readMilestoneRequirements(milestoneNum) {
|
|
787
|
+
let md;
|
|
788
|
+
try { md = fs.readFileSync(path.join(PLANNING, "REQUIREMENTS.md"), "utf8"); }
|
|
789
|
+
catch { return { tracked: false, total: 0, incomplete: [] }; }
|
|
790
|
+
const num = parseInt(milestoneNum, 10);
|
|
791
|
+
const rows = [];
|
|
792
|
+
for (const line of md.split(/\r?\n/)) {
|
|
793
|
+
if (!/^\s*\|/.test(line)) continue;
|
|
794
|
+
const cells = line.split("|").map((c) => c.trim()).filter((c, i, a) => !(i === 0 && c === "") && !(i === a.length - 1 && c === ""));
|
|
795
|
+
if (cells.length < 4) continue;
|
|
796
|
+
const [id, milestone, , status] = cells;
|
|
797
|
+
if (!/^[A-Z]+-\d+$/.test(id)) continue; // skip header + non-REQ rows
|
|
798
|
+
const m = milestone.match(/M(\d+)\b/);
|
|
799
|
+
if (!m || parseInt(m[1], 10) !== num) continue;
|
|
800
|
+
rows.push({ id, status: status || "" });
|
|
801
|
+
}
|
|
802
|
+
const incomplete = rows.filter((r) => r.status.trim().toLowerCase() !== "complete");
|
|
803
|
+
return { tracked: rows.length > 0, total: rows.length, incomplete };
|
|
804
|
+
}
|
|
805
|
+
|
|
759
806
|
function readState() {
|
|
760
807
|
try {
|
|
761
808
|
return fs.readFileSync(STATE_FILE, "utf8");
|
|
@@ -970,6 +1017,25 @@ function checkPreconditions(current, target, opts) {
|
|
|
970
1017
|
const anchors = doneWhenCount + acCount;
|
|
971
1018
|
if (anchors < taskHeaders.length)
|
|
972
1019
|
return fail("INVALID_PLAN", `${taskHeaders.length} tasks but only ${anchors} 'Done when:' or 'Acceptance Criteria:' anchors`);
|
|
1020
|
+
// v7 kernel: a phase cannot reach `planned` without a machine contract.
|
|
1021
|
+
// checkMachineEvidence() at the `verified` gate only engages when
|
|
1022
|
+
// phase-N-contract.json exists; if a contract could be skipped here, the
|
|
1023
|
+
// entire evidence requirement is bypassable by omission and the prose
|
|
1024
|
+
// verifier (which can PASS on inaction) would govern. Requiring it here is
|
|
1025
|
+
// what makes "I built it" insufficient and "the contract ran clean" required.
|
|
1026
|
+
const contractFile = path.join(PLANNING, `phase-${phase}-contract.json`);
|
|
1027
|
+
if (!fs.existsSync(contractFile))
|
|
1028
|
+
return fail(
|
|
1029
|
+
"MISSING_CONTRACT",
|
|
1030
|
+
`Machine contract not found: ${contractFile}. /qualia-plan must compile the plan into a JSON contract before 'planned'. Regenerate with /qualia-plan, or build it via bin/plan-contract.js.`
|
|
1031
|
+
);
|
|
1032
|
+
try {
|
|
1033
|
+
const c = JSON.parse(fs.readFileSync(contractFile, "utf8"));
|
|
1034
|
+
if (!c || !Array.isArray(c.tasks) || c.tasks.length === 0)
|
|
1035
|
+
return fail("INVALID_CONTRACT", `${contractFile} has no tasks[]; not a valid phase contract.`);
|
|
1036
|
+
} catch (e) {
|
|
1037
|
+
return fail("INVALID_CONTRACT", `Could not parse ${contractFile}: ${e.message}`);
|
|
1038
|
+
}
|
|
973
1039
|
}
|
|
974
1040
|
|
|
975
1041
|
if (target === "verified") {
|
|
@@ -994,9 +1060,17 @@ function checkPreconditions(current, target, opts) {
|
|
|
994
1060
|
}
|
|
995
1061
|
|
|
996
1062
|
if (target === "handed_off") {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1063
|
+
// v7 lifecycle: the HANDOFF.md requirement is a BUILD-mode convention, not a
|
|
1064
|
+
// universal law. An "operate" project (a launched, repeatedly-shipping
|
|
1065
|
+
// product or retainer) has no single handoff moment, so it must not be
|
|
1066
|
+
// forced to produce one. opts.lifecycle is threaded from tracking by the
|
|
1067
|
+
// caller; absent/"build" keeps the original requirement.
|
|
1068
|
+
const lifecycle = opts.lifecycle || "build";
|
|
1069
|
+
if (lifecycle === "build") {
|
|
1070
|
+
const hFile = path.join(PLANNING, "HANDOFF.md");
|
|
1071
|
+
if (!fs.existsSync(hFile))
|
|
1072
|
+
return fail("MISSING_FILE", `Handoff file not found: ${hFile}`);
|
|
1073
|
+
}
|
|
1000
1074
|
}
|
|
1001
1075
|
|
|
1002
1076
|
// Gap-closure circuit breaker (configurable limit)
|
|
@@ -1055,7 +1129,14 @@ function recordLedgerEvent(meta) {
|
|
|
1055
1129
|
}
|
|
1056
1130
|
|
|
1057
1131
|
// ─── Next Command Logic ──────────────────────────────────
|
|
1058
|
-
|
|
1132
|
+
// `lifecycle` (default "build") changes the route once a project has launched.
|
|
1133
|
+
// In "operate" the project is an UPDATE STREAM, not a milestone journey: there is
|
|
1134
|
+
// no polish → ship → handoff terminal chain. After the last phase verifies, the
|
|
1135
|
+
// next move is the next update (/qualia-update), and the project never gets
|
|
1136
|
+
// dragged to a handoff it has outgrown. This is the v7 thesis in miniature —
|
|
1137
|
+
// a behavior that was hard-coded in prose is now a branch on explicit state.
|
|
1138
|
+
function nextCommand(status, phase, totalPhases, verification, lifecycle) {
|
|
1139
|
+
const operate = lifecycle === "operate";
|
|
1059
1140
|
switch (status) {
|
|
1060
1141
|
case "setup":
|
|
1061
1142
|
return `/qualia-plan ${phase}`;
|
|
@@ -1066,15 +1147,17 @@ function nextCommand(status, phase, totalPhases, verification) {
|
|
|
1066
1147
|
case "verified":
|
|
1067
1148
|
if (verification === "fail") return `/qualia-plan ${phase} --gaps`;
|
|
1068
1149
|
if (phase < totalPhases) return `/qualia-plan ${phase + 1}`;
|
|
1069
|
-
return "/qualia-polish";
|
|
1150
|
+
return operate ? "/qualia-update" : "/qualia-polish";
|
|
1070
1151
|
case "polished":
|
|
1071
1152
|
return "/qualia-ship";
|
|
1072
1153
|
case "shipped":
|
|
1073
|
-
|
|
1154
|
+
// In build mode a shipped project hands off once. In operate it loops:
|
|
1155
|
+
// the deploy was just another update.
|
|
1156
|
+
return operate ? "/qualia-update" : "/qualia-handoff";
|
|
1074
1157
|
case "handed_off":
|
|
1075
1158
|
return "/qualia-report";
|
|
1076
1159
|
case "done":
|
|
1077
|
-
return "Done.";
|
|
1160
|
+
return operate ? "/qualia-update" : "Done.";
|
|
1078
1161
|
default:
|
|
1079
1162
|
return `/qualia`;
|
|
1080
1163
|
}
|
|
@@ -1202,11 +1285,14 @@ function cmdCheck(opts) {
|
|
|
1202
1285
|
tasks_done: t.tasks_done || 0,
|
|
1203
1286
|
tasks_total: t.tasks_total || 0,
|
|
1204
1287
|
deployed_url: t.deployed_url || "",
|
|
1288
|
+
lifecycle: t.lifecycle || "build",
|
|
1289
|
+
launched_at: t.launched_at || "",
|
|
1205
1290
|
next_command: nextCommand(
|
|
1206
1291
|
s.status,
|
|
1207
1292
|
s.phase,
|
|
1208
1293
|
s.total_phases,
|
|
1209
|
-
t.verification
|
|
1294
|
+
t.verification,
|
|
1295
|
+
t.lifecycle
|
|
1210
1296
|
),
|
|
1211
1297
|
schema_errors: s.schema_errors && s.schema_errors.length ? s.schema_errors : undefined,
|
|
1212
1298
|
});
|
|
@@ -1233,6 +1319,28 @@ function applyNoteOrActivity(target, s, t, opts) {
|
|
|
1233
1319
|
t.lifetime.tasks_completed += count;
|
|
1234
1320
|
}
|
|
1235
1321
|
}
|
|
1322
|
+
// Scope tagging (anti-drift): /qualia-feature + /qualia-fix declare whether the
|
|
1323
|
+
// work served the active milestone (--scope in --ref CORE-03) or was off-road
|
|
1324
|
+
// (--scope off). Off-road work is COUNTED + ledgered so it can't drift
|
|
1325
|
+
// invisibly — the OWNER + ERP see the tally, mirroring branch-guard.
|
|
1326
|
+
const scope = String(opts.scope || "").toLowerCase();
|
|
1327
|
+
let offroadCount;
|
|
1328
|
+
if (scope === "in" || scope === "off") {
|
|
1329
|
+
ensureLifetime(t);
|
|
1330
|
+
if (typeof t.lifetime.offroad_count !== "number") t.lifetime.offroad_count = 0;
|
|
1331
|
+
if (scope === "off") {
|
|
1332
|
+
t.lifetime.offroad_count += 1;
|
|
1333
|
+
if (!Array.isArray(t.offroad)) t.offroad = [];
|
|
1334
|
+
t.offroad.push({
|
|
1335
|
+
at: new Date().toISOString(),
|
|
1336
|
+
milestone: parseInt(t.milestone, 10) || null,
|
|
1337
|
+
ref: opts.ref || null,
|
|
1338
|
+
note: opts.notes || null,
|
|
1339
|
+
});
|
|
1340
|
+
if (t.offroad.length > 50) t.offroad = t.offroad.slice(-50);
|
|
1341
|
+
}
|
|
1342
|
+
offroadCount = t.lifetime.offroad_count;
|
|
1343
|
+
}
|
|
1236
1344
|
t.last_updated = new Date().toISOString();
|
|
1237
1345
|
writeTracking(t);
|
|
1238
1346
|
s.last_activity = opts.notes || "Activity logged";
|
|
@@ -1242,6 +1350,8 @@ function applyNoteOrActivity(target, s, t, opts) {
|
|
|
1242
1350
|
phase: s.phase,
|
|
1243
1351
|
status: s.status,
|
|
1244
1352
|
action: target,
|
|
1353
|
+
scope: scope || undefined,
|
|
1354
|
+
offroad_count: offroadCount,
|
|
1245
1355
|
};
|
|
1246
1356
|
}
|
|
1247
1357
|
|
|
@@ -1400,8 +1510,8 @@ function cmdTransition(opts) {
|
|
|
1400
1510
|
|
|
1401
1511
|
const phase = parseInt(opts.phase) || s.phase;
|
|
1402
1512
|
|
|
1403
|
-
// Precondition check
|
|
1404
|
-
const check = checkPreconditions({ ...s, phase }, target, { ...opts, phase });
|
|
1513
|
+
// Precondition check (lifecycle threaded so the handoff gate can relax in operate)
|
|
1514
|
+
const check = checkPreconditions({ ...s, phase }, target, { ...opts, phase, lifecycle: t.lifecycle });
|
|
1405
1515
|
if (!check.ok) {
|
|
1406
1516
|
// --force bypasses status-ordering and plan-content errors. The use case
|
|
1407
1517
|
// is retroactive bookkeeping: a phase was built without /qualia-plan and
|
|
@@ -1433,6 +1543,18 @@ function cmdTransition(opts) {
|
|
|
1433
1543
|
if (target === "polished") applyPolishedTransition(s);
|
|
1434
1544
|
if (target === "shipped") applyShippedTransition(t, opts);
|
|
1435
1545
|
|
|
1546
|
+
// v7: in operate mode, a verified(pass) on the final phase completes one UPDATE.
|
|
1547
|
+
// This is the operate-mode analogue of closing a milestone in build mode.
|
|
1548
|
+
if (
|
|
1549
|
+
target === "verified" &&
|
|
1550
|
+
opts.verification === "pass" &&
|
|
1551
|
+
t.lifecycle === "operate" &&
|
|
1552
|
+
phase >= (parseInt(s.total_phases) || phase)
|
|
1553
|
+
) {
|
|
1554
|
+
ensureLifetime(t);
|
|
1555
|
+
t.lifetime.updates_completed = (t.lifetime.updates_completed || 0) + 1;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1436
1558
|
// Atomic commit
|
|
1437
1559
|
const writeError = commitTransitionAtomic(s, t);
|
|
1438
1560
|
if (writeError) return output(writeError);
|
|
@@ -1471,7 +1593,8 @@ function cmdTransition(opts) {
|
|
|
1471
1593
|
previous_status: prevStatus,
|
|
1472
1594
|
verification: t.verification,
|
|
1473
1595
|
gap_cycles: (t.gap_cycles || {})[String(s.phase)] || 0,
|
|
1474
|
-
|
|
1596
|
+
lifecycle: t.lifecycle || "build",
|
|
1597
|
+
next_command: nextCommand(s.status, s.phase, s.total_phases, t.verification, t.lifecycle),
|
|
1475
1598
|
};
|
|
1476
1599
|
if (ledger.ok) {
|
|
1477
1600
|
result.ledger_event_id = ledger.event_id;
|
|
@@ -1579,6 +1702,7 @@ function cmdInit(opts) {
|
|
|
1579
1702
|
milestones_completed: 0,
|
|
1580
1703
|
total_phases: 0,
|
|
1581
1704
|
last_closed_milestone: 0,
|
|
1705
|
+
updates_completed: 0,
|
|
1582
1706
|
};
|
|
1583
1707
|
const lifetime = prevLife
|
|
1584
1708
|
? { ...defaultLifetime, ...(prevLife.lifetime || {}) }
|
|
@@ -1607,6 +1731,11 @@ function cmdInit(opts) {
|
|
|
1607
1731
|
total_phases: totalPhases,
|
|
1608
1732
|
status: "setup",
|
|
1609
1733
|
profile,
|
|
1734
|
+
// v7 lifecycle: a new project starts in "build" (milestone journey).
|
|
1735
|
+
// `launch` flips it to "operate" (update stream). Preserved across re-init.
|
|
1736
|
+
lifecycle: prevLife ? (prevLife.lifecycle || "build") : "build",
|
|
1737
|
+
launched_at: prevLife ? (prevLife.launched_at || "") : "",
|
|
1738
|
+
launch_source: prevLife ? (prevLife.launch_source || "") : "",
|
|
1610
1739
|
wave: 0,
|
|
1611
1740
|
tasks_done: 0,
|
|
1612
1741
|
tasks_total: 0,
|
|
@@ -1665,6 +1794,76 @@ function cmdInit(opts) {
|
|
|
1665
1794
|
output(result);
|
|
1666
1795
|
}
|
|
1667
1796
|
|
|
1797
|
+
// v7: the one-time launch event. Flips the project from "build" (milestone
|
|
1798
|
+
// journey) to "operate" (update stream). This is the discrete transition the
|
|
1799
|
+
// ERP can drive when it detects a project is live (is_live / status:Launched),
|
|
1800
|
+
// so "the product is launched" becomes explicit state instead of a milestone the
|
|
1801
|
+
// team is forced to call "handoff". Idempotent: launching an operate project is
|
|
1802
|
+
// a no-op. lifecycle is canonical in tracking.json; cmdCheck surfaces it.
|
|
1803
|
+
function cmdLaunch(opts) {
|
|
1804
|
+
const beforeStateRaw = readState();
|
|
1805
|
+
const beforeTrackingRaw = readTrackingRaw();
|
|
1806
|
+
const t = parseTrackingRaw(beforeTrackingRaw);
|
|
1807
|
+
if (!t) return output(fail("NO_PROJECT", "No .planning/ found. Run /qualia-new."));
|
|
1808
|
+
ensureLifetime(t);
|
|
1809
|
+
|
|
1810
|
+
if (t.lifecycle === "operate") {
|
|
1811
|
+
return output({
|
|
1812
|
+
ok: true,
|
|
1813
|
+
action: "launch",
|
|
1814
|
+
already_launched: true,
|
|
1815
|
+
lifecycle: "operate",
|
|
1816
|
+
launched_at: t.launched_at || "",
|
|
1817
|
+
launch_source: t.launch_source || "",
|
|
1818
|
+
next_command: "/qualia-update",
|
|
1819
|
+
message: "Already launched (operate lifecycle). Nothing to do.",
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
t.lifecycle = "operate";
|
|
1824
|
+
t.launched_at = new Date().toISOString();
|
|
1825
|
+
t.launch_source = opts.source === "erp" ? "erp" : "manual";
|
|
1826
|
+
if (opts.deployed_url) t.deployed_url = opts.deployed_url;
|
|
1827
|
+
t.last_updated = new Date().toISOString();
|
|
1828
|
+
writeTracking(t);
|
|
1829
|
+
|
|
1830
|
+
const ledger = recordLedgerEvent({
|
|
1831
|
+
action: "launch",
|
|
1832
|
+
phase_before: t.phase || null,
|
|
1833
|
+
phase_after: t.phase || null,
|
|
1834
|
+
status_before: t.status || null,
|
|
1835
|
+
status_after: t.status || null,
|
|
1836
|
+
state_before: parseStateMd(beforeStateRaw),
|
|
1837
|
+
state_after: parseStateMd(readState()),
|
|
1838
|
+
tracking_before: parseTrackingRaw(beforeTrackingRaw),
|
|
1839
|
+
tracking_after: t,
|
|
1840
|
+
state_raw_before: beforeStateRaw,
|
|
1841
|
+
state_raw_after: readState(),
|
|
1842
|
+
tracking_raw_before: beforeTrackingRaw,
|
|
1843
|
+
tracking_raw_after: readTrackingRaw(),
|
|
1844
|
+
evidence_refs: opts.deployed_url ? [opts.deployed_url] : [],
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
const result = {
|
|
1848
|
+
ok: true,
|
|
1849
|
+
action: "launch",
|
|
1850
|
+
lifecycle: "operate",
|
|
1851
|
+
launched_at: t.launched_at,
|
|
1852
|
+
launch_source: t.launch_source,
|
|
1853
|
+
deployed_url: t.deployed_url || "",
|
|
1854
|
+
next_command: "/qualia-update",
|
|
1855
|
+
message:
|
|
1856
|
+
"Launched. The project is now an UPDATE STREAM (operate): no forced polish → ship → handoff. Ship updates with the build/verify/ship loop; the forced handoff is gone.",
|
|
1857
|
+
};
|
|
1858
|
+
if (ledger.ok) {
|
|
1859
|
+
result.ledger_event_id = ledger.event_id;
|
|
1860
|
+
result.ledger_event_hash = ledger.event_hash;
|
|
1861
|
+
} else {
|
|
1862
|
+
result.ledger_error = ledger.error;
|
|
1863
|
+
}
|
|
1864
|
+
output(result);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1668
1867
|
function cmdFix(opts) {
|
|
1669
1868
|
const beforeStateRaw = readState();
|
|
1670
1869
|
const beforeTrackingRaw = readTrackingRaw();
|
|
@@ -2183,6 +2382,27 @@ function cmdValidatePlan(opts) {
|
|
|
2183
2382
|
// recently closed milestone so re-running close-milestone (e.g., after a
|
|
2184
2383
|
// hiccup) does NOT double-count. To re-close a milestone deliberately, pass
|
|
2185
2384
|
// --force.
|
|
2385
|
+
// Report REQ-ID completion for a milestone (defaults to the current one). The
|
|
2386
|
+
// same check close-milestone gates on — exposed so /qualia-milestone can show
|
|
2387
|
+
// coverage before closing, and so it's directly testable. Exit 0 = all complete
|
|
2388
|
+
// (or untracked), 1 = incomplete requirements remain.
|
|
2389
|
+
function cmdReqsCheck(opts) {
|
|
2390
|
+
const t = readTracking();
|
|
2391
|
+
const milestone = opts.milestone != null ? parseInt(opts.milestone, 10) : (t ? parseInt(t.milestone, 10) || 1 : 1);
|
|
2392
|
+
const reqs = readMilestoneRequirements(milestone);
|
|
2393
|
+
const ok = !reqs.tracked || reqs.incomplete.length === 0;
|
|
2394
|
+
output({
|
|
2395
|
+
ok,
|
|
2396
|
+
action: "reqs-check",
|
|
2397
|
+
milestone,
|
|
2398
|
+
tracked: reqs.tracked,
|
|
2399
|
+
total: reqs.total,
|
|
2400
|
+
complete: reqs.total - reqs.incomplete.length,
|
|
2401
|
+
incomplete: reqs.incomplete,
|
|
2402
|
+
});
|
|
2403
|
+
process.exitCode = ok ? 0 : 1;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2186
2406
|
function cmdCloseMilestone(opts) {
|
|
2187
2407
|
const beforeStateRaw = readState();
|
|
2188
2408
|
const beforeTrackingRaw = readTrackingRaw();
|
|
@@ -2192,6 +2412,7 @@ function cmdCloseMilestone(opts) {
|
|
|
2192
2412
|
return output(fail("NO_PROJECT", "No .planning/ found."));
|
|
2193
2413
|
}
|
|
2194
2414
|
ensureLifetime(t);
|
|
2415
|
+
const closeWarnings = [];
|
|
2195
2416
|
|
|
2196
2417
|
// parseInt — legacy tracking.json files carry milestone as a string ("9"),
|
|
2197
2418
|
// which would corrupt `closedMilestone + 1` ("91") and break num dedupe.
|
|
@@ -2236,6 +2457,23 @@ function cmdCloseMilestone(opts) {
|
|
|
2236
2457
|
)
|
|
2237
2458
|
);
|
|
2238
2459
|
}
|
|
2460
|
+
// REQ-ID coverage gate: a milestone isn't "done" just because its phases
|
|
2461
|
+
// verified — its agreed requirements must actually be Complete. Stops
|
|
2462
|
+
// "finishing a milestone with scope left open". strict blocks; standard warns.
|
|
2463
|
+
const reqs = readMilestoneRequirements(closedMilestone);
|
|
2464
|
+
if (reqs.tracked && reqs.incomplete.length > 0) {
|
|
2465
|
+
const profile = resolveProfile(s, t);
|
|
2466
|
+
const list = reqs.incomplete.map((r) => `${r.id}:${r.status || "Pending"}`).join(", ");
|
|
2467
|
+
if (profile === "strict") {
|
|
2468
|
+
return output(
|
|
2469
|
+
fail(
|
|
2470
|
+
"MILESTONE_REQS_INCOMPLETE",
|
|
2471
|
+
`Milestone ${closedMilestone} has ${reqs.incomplete.length}/${reqs.total} requirement(s) not Complete in REQUIREMENTS.md: ${list}. Finish or explicitly defer them (move to Out of Scope), or use --force.`
|
|
2472
|
+
)
|
|
2473
|
+
);
|
|
2474
|
+
}
|
|
2475
|
+
closeWarnings.push(`${reqs.incomplete.length}/${reqs.total} requirement(s) not Complete: ${list} (standard profile — proceeding; record why in the report).`);
|
|
2476
|
+
}
|
|
2239
2477
|
}
|
|
2240
2478
|
|
|
2241
2479
|
// ─── Append a summary to milestones[] so the ERP can render the tree ──
|
|
@@ -2354,6 +2592,7 @@ function cmdCloseMilestone(opts) {
|
|
|
2354
2592
|
} else {
|
|
2355
2593
|
result.ledger_error = ledger.error;
|
|
2356
2594
|
}
|
|
2595
|
+
if (closeWarnings.length) result.warnings = closeWarnings;
|
|
2357
2596
|
output(result);
|
|
2358
2597
|
}
|
|
2359
2598
|
|
|
@@ -2775,6 +3014,9 @@ try {
|
|
|
2775
3014
|
case "transition":
|
|
2776
3015
|
cmdTransition(opts);
|
|
2777
3016
|
break;
|
|
3017
|
+
case "launch":
|
|
3018
|
+
cmdLaunch(opts);
|
|
3019
|
+
break;
|
|
2778
3020
|
case "init":
|
|
2779
3021
|
cmdInit(opts);
|
|
2780
3022
|
break;
|
|
@@ -2787,6 +3029,9 @@ try {
|
|
|
2787
3029
|
case "close-milestone":
|
|
2788
3030
|
cmdCloseMilestone(opts);
|
|
2789
3031
|
break;
|
|
3032
|
+
case "reqs-check":
|
|
3033
|
+
cmdReqsCheck(opts);
|
|
3034
|
+
break;
|
|
2790
3035
|
case "backfill-lifetime":
|
|
2791
3036
|
cmdBackfillLifetime(opts);
|
|
2792
3037
|
break;
|