qualia-framework 7.2.1 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +17 -0
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +56 -0
- package/CLAUDE.md +1 -1
- package/README.md +17 -4
- package/TROUBLESHOOTING.md +8 -7
- package/agents/verifier.md +1 -1
- package/bin/agent-status.js +115 -11
- package/bin/auto-report.js +15 -7
- package/bin/cli.js +173 -4
- package/bin/erp-retry.js +92 -8
- package/bin/install.js +134 -6
- package/bin/qualia-doctor.js +115 -1
- package/bin/state.js +102 -13
- package/bin/verify-panel.js +409 -0
- package/docs/onboarding.html +1 -1
- package/hooks/branch-guard.js +19 -5
- package/hooks/fawzi-approval-guard.js +16 -3
- package/hooks/hooks.json +60 -0
- package/hooks/migration-guard.js +143 -66
- package/hooks/session-start.js +27 -0
- package/package.json +3 -1
- package/skills/qualia/SKILL.md +26 -13
- package/skills/qualia-build/SKILL.md +20 -9
- package/skills/qualia-verify/SKILL.md +43 -5
- package/templates/instructions.md +2 -2
- package/tests/bin.test.sh +183 -0
- package/tests/hooks.test.sh +124 -0
- package/tests/install-smoke.test.sh +14 -0
- package/tests/instructions.test.sh +2 -2
- package/tests/lib.test.sh +149 -0
- package/tests/plugin-manifest.test.sh +168 -0
- package/tests/refs.test.sh +64 -0
- package/tests/run-all.sh +1 -0
- package/tests/state.test.sh +174 -0
- package/tests/verify-panel.test.sh +236 -0
package/bin/qualia-doctor.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// 3. wiki/_export rebuilt ≤ 2 days (export is scheduled & running)
|
|
18
18
|
// 4. export concept count == allowed source set (export covers all concepts)
|
|
19
19
|
// 5. tags:deprecated == 0 in _export (no deprecated rows leaked out)
|
|
20
|
+
// 6. doc/code coherence docs only reference real cli.js commands + skills
|
|
20
21
|
//
|
|
21
22
|
// Zero-dependency. Resolves the vault from $QUALIA_MEMORY else ~/qualia-memory,
|
|
22
23
|
// and the install home from $QUALIA_HOME else ~/.claude. Missing dirs surface
|
|
@@ -209,6 +210,118 @@ function gateNoDeprecated(VAULT) {
|
|
|
209
210
|
};
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
// ── Doc/code coherence ──────────────────────────────────────────────────
|
|
214
|
+
// Docs that promise a `qualia-framework <cmd>` CLI command or a `/qualia-*`
|
|
215
|
+
// skill must point at one that actually exists, else a new hire follows a
|
|
216
|
+
// dead reference. This gate resolves the real surface from code (cli.js switch
|
|
217
|
+
// cases + command-surface.js ACTIVE_SKILLS) and flags any doc token that
|
|
218
|
+
// doesn't resolve. Deterministic, filesystem-only, fail-soft if sources are
|
|
219
|
+
// absent (e.g. a partial install) — it WARNs rather than crashing.
|
|
220
|
+
|
|
221
|
+
const COHERENCE_DOCS = [
|
|
222
|
+
"TROUBLESHOOTING.md",
|
|
223
|
+
"README.md",
|
|
224
|
+
"AGENTS.md",
|
|
225
|
+
"CLAUDE.md",
|
|
226
|
+
"docs/EMPLOYEE-QUICKSTART.md",
|
|
227
|
+
"docs/onboarding.html",
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
// Real CLI commands = the quoted labels of the top-level `switch (cmd)` cases
|
|
231
|
+
// in bin/cli.js. We read the dispatch verbatim so the gate can never drift
|
|
232
|
+
// from the code.
|
|
233
|
+
function cliCommandSet(REPO) {
|
|
234
|
+
const file = path.join(REPO, "bin", "cli.js");
|
|
235
|
+
if (!fs.existsSync(file)) return null;
|
|
236
|
+
let txt;
|
|
237
|
+
try { txt = fs.readFileSync(file, "utf8"); } catch { return null; }
|
|
238
|
+
// Only the dispatch switch matters; bound the scan to after `switch (cmd)`.
|
|
239
|
+
const sw = txt.indexOf("switch (cmd)");
|
|
240
|
+
const body = sw >= 0 ? txt.slice(sw) : txt;
|
|
241
|
+
const set = new Set();
|
|
242
|
+
for (const m of body.matchAll(/^\s*case\s+"([a-z][a-z0-9-]*)":/gim)) {
|
|
243
|
+
set.add(m[1]);
|
|
244
|
+
}
|
|
245
|
+
return set.size ? set : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function activeSkillSet(REPO) {
|
|
249
|
+
// Prefer the canonical module; fall back to the installed copy beside us.
|
|
250
|
+
for (const candidate of [
|
|
251
|
+
path.join(REPO, "bin", "command-surface.js"),
|
|
252
|
+
path.join(__dirname, "command-surface.js"),
|
|
253
|
+
]) {
|
|
254
|
+
if (!fs.existsSync(candidate)) continue;
|
|
255
|
+
try {
|
|
256
|
+
const mod = require(candidate);
|
|
257
|
+
const list = mod.activeSkills ? mod.activeSkills() : mod.ACTIVE_SKILLS;
|
|
258
|
+
if (Array.isArray(list) && list.length) return new Set(list);
|
|
259
|
+
} catch {}
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function gateDocCoherence(REPO) {
|
|
265
|
+
const cliCmds = cliCommandSet(REPO);
|
|
266
|
+
const skills = activeSkillSet(REPO);
|
|
267
|
+
if (!cliCmds || !skills) {
|
|
268
|
+
return {
|
|
269
|
+
gate: "doc/code coherence",
|
|
270
|
+
status: "WARN",
|
|
271
|
+
detail: "cli.js or command-surface.js not found — coherence not checked",
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const offenders = [];
|
|
275
|
+
let docsScanned = 0;
|
|
276
|
+
for (const rel of COHERENCE_DOCS) {
|
|
277
|
+
const p = path.join(REPO, rel);
|
|
278
|
+
if (!fs.existsSync(p)) continue;
|
|
279
|
+
let txt;
|
|
280
|
+
try { txt = fs.readFileSync(p, "utf8"); } catch { continue; }
|
|
281
|
+
docsScanned++;
|
|
282
|
+
// (a) `qualia-framework <cmd>` — first word after the binary name.
|
|
283
|
+
for (const m of txt.matchAll(/qualia-framework\s+([a-z][a-z0-9-]*)/g)) {
|
|
284
|
+
const cmd = m[1];
|
|
285
|
+
if (!cliCmds.has(cmd)) offenders.push(`${rel}:qualia-framework ${cmd}`);
|
|
286
|
+
}
|
|
287
|
+
// (b) `/qualia-<skill>` slash-command invocations. The leading slash must
|
|
288
|
+
// be a real command slash (boundary before it — whitespace, `(`, backtick,
|
|
289
|
+
// quote, or line start), NOT a path separator. And the token must not be a
|
|
290
|
+
// file/repo path (followed by `.`, `/`, or `:` → e.g. ./qualia-manual.html,
|
|
291
|
+
// qualia-memory.git, to/qualia-memory). This avoids false positives on the
|
|
292
|
+
// HTML file link and the knowledge-repo path in the quickstart.
|
|
293
|
+
for (const m of txt.matchAll(/(^|[\s(`'">])\/(qualia-[a-z]+)(?![a-z./:-])/gim)) {
|
|
294
|
+
const skill = m[2];
|
|
295
|
+
// `qualia` (the router) and `qualia-framework` (the CLI) are valid; the
|
|
296
|
+
// CLI binary is matched without a slash, so a bare `/qualia-framework`
|
|
297
|
+
// would be wrong — but the negative lookahead already excludes `-`.
|
|
298
|
+
if (skill === "qualia") continue;
|
|
299
|
+
if (!skills.has(skill)) offenders.push(`${rel}:/${skill}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (docsScanned === 0) {
|
|
303
|
+
return { gate: "doc/code coherence", status: "WARN", detail: "no coherence docs present to scan" };
|
|
304
|
+
}
|
|
305
|
+
if (offenders.length === 0) {
|
|
306
|
+
return {
|
|
307
|
+
gate: "doc/code coherence",
|
|
308
|
+
status: "PASS",
|
|
309
|
+
detail: `${docsScanned} doc(s): all command/skill refs resolve`,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const uniq = [...new Set(offenders)];
|
|
313
|
+
return {
|
|
314
|
+
gate: "doc/code coherence",
|
|
315
|
+
status: "FAIL",
|
|
316
|
+
detail: `${uniq.length} dead ref(s): ${uniq.slice(0, 4).join(", ")}${uniq.length > 4 ? "…" : ""}`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function repoRoot() {
|
|
321
|
+
// The doctor lives in <root>/bin; the docs/cli/surface sit one level up.
|
|
322
|
+
return path.dirname(__dirname);
|
|
323
|
+
}
|
|
324
|
+
|
|
212
325
|
function runGates() {
|
|
213
326
|
const QUALIA_HOME = qualiaHome();
|
|
214
327
|
const VAULT = vaultRoot();
|
|
@@ -218,6 +331,7 @@ function runGates() {
|
|
|
218
331
|
gateExportFresh(VAULT),
|
|
219
332
|
gateExportCoverage(VAULT),
|
|
220
333
|
gateNoDeprecated(VAULT),
|
|
334
|
+
gateDocCoherence(repoRoot()),
|
|
221
335
|
];
|
|
222
336
|
}
|
|
223
337
|
|
|
@@ -245,5 +359,5 @@ function main() {
|
|
|
245
359
|
if (require.main === module) {
|
|
246
360
|
main();
|
|
247
361
|
} else {
|
|
248
|
-
module.exports = { runGates, gateDailyLog, gateFlushLog, gateExportFresh, gateExportCoverage, gateNoDeprecated, qualiaHome, vaultRoot };
|
|
362
|
+
module.exports = { runGates, gateDailyLog, gateFlushLog, gateExportFresh, gateExportCoverage, gateNoDeprecated, gateDocCoherence, repoRoot, qualiaHome, vaultRoot };
|
|
249
363
|
}
|
package/bin/state.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const stateLedger = require("./state-ledger.js");
|
|
8
|
+
// analyze-gate.js is loaded lazily inside checkScopeDrift() (require-in-function)
|
|
9
|
+
// so a missing/broken gate file can never crash state.js at load time — the
|
|
10
|
+
// scope-drift gate fails SOFT (advisory), never fail-closed on its own error.
|
|
8
11
|
|
|
9
12
|
const PLANNING = ".planning";
|
|
10
13
|
const STATE_FILE = path.join(PLANNING, "STATE.md");
|
|
@@ -529,7 +532,7 @@ function regenerateViews(activity) {
|
|
|
529
532
|
}));
|
|
530
533
|
t.releases = listReleases().map((r) => ({ id: r.id, type: r.type, status: r.status, increments: r.increments }));
|
|
531
534
|
t.next_command = active
|
|
532
|
-
? nextCommand(active.status, active.num, increments.length, active.verification)
|
|
535
|
+
? nextCommand(active.status, active.num, increments.length, active.verification, t.lifecycle)
|
|
533
536
|
: "/qualia";
|
|
534
537
|
t.last_updated = new Date().toISOString();
|
|
535
538
|
writeTracking(t);
|
|
@@ -560,10 +563,10 @@ function cmdCheckIncrement(opts) {
|
|
|
560
563
|
const totalPhases = increments.length;
|
|
561
564
|
const allDone = increments.every(isDone);
|
|
562
565
|
const next_command = mine
|
|
563
|
-
? nextCommand(mine.status, mine.num, totalPhases, mine.verification)
|
|
566
|
+
? nextCommand(mine.status, mine.num, totalPhases, mine.verification, t.lifecycle)
|
|
564
567
|
: next_unclaimed
|
|
565
|
-
? nextCommand(next_unclaimed.status, next_unclaimed.num, totalPhases, next_unclaimed.verification)
|
|
566
|
-
: (allDone ? "/qualia-polish" : "/qualia");
|
|
568
|
+
? nextCommand(next_unclaimed.status, next_unclaimed.num, totalPhases, next_unclaimed.verification, t.lifecycle)
|
|
569
|
+
: (allDone ? (t.lifecycle === "operate" ? "/qualia-update" : "/qualia-polish") : "/qualia");
|
|
567
570
|
|
|
568
571
|
output({
|
|
569
572
|
ok: true,
|
|
@@ -642,9 +645,10 @@ function cmdTransitionIncrement(opts, target) {
|
|
|
642
645
|
|
|
643
646
|
const phase = inc.num;
|
|
644
647
|
const prevStatus = inc.status;
|
|
645
|
-
const
|
|
648
|
+
const t0 = ensureLifetime(readTracking() || {});
|
|
649
|
+
const check = checkPreconditions({ phase, status: inc.status, total_phases: increments.length }, target, { ...opts, phase, lifecycle: t0.lifecycle, profile: resolveProfile(null, t0) });
|
|
646
650
|
if (!check.ok) {
|
|
647
|
-
const forceable = ["PRECONDITION_FAILED", "GAP_CYCLE_LIMIT", "INVALID_PLAN"];
|
|
651
|
+
const forceable = ["PRECONDITION_FAILED", "GAP_CYCLE_LIMIT", "INVALID_PLAN", "SCOPE_DRIFT"];
|
|
648
652
|
if (opts.force && forceable.includes(check.error)) {
|
|
649
653
|
console.error(`WARNING: Forcing transition despite: ${check.message}`);
|
|
650
654
|
} else {
|
|
@@ -698,7 +702,7 @@ function cmdTransitionIncrement(opts, target) {
|
|
|
698
702
|
previous_status: prevStatus,
|
|
699
703
|
verification: inc.verification,
|
|
700
704
|
gap_cycles: inc.gap_cycles || 0,
|
|
701
|
-
next_command: nextCommand(inc.status, inc.num, increments.length, inc.verification),
|
|
705
|
+
next_command: nextCommand(inc.status, inc.num, increments.length, inc.verification, t.lifecycle),
|
|
702
706
|
};
|
|
703
707
|
if (ledger.ok) {
|
|
704
708
|
result.ledger_event_id = ledger.event_id;
|
|
@@ -1040,6 +1044,14 @@ function checkPreconditions(current, target, opts) {
|
|
|
1040
1044
|
}
|
|
1041
1045
|
}
|
|
1042
1046
|
|
|
1047
|
+
if (target === "built") {
|
|
1048
|
+
// Scope-drift gate (ITEM #5): the planned→built seam is where /qualia-build
|
|
1049
|
+
// used to ASK the agent to run analyze-gate. Now it's enforced. strict +
|
|
1050
|
+
// HIGH finding refuses; standard is advisory; the gate's own failure is soft.
|
|
1051
|
+
const driftCheck = checkScopeDrift(phase, opts.profile);
|
|
1052
|
+
if (!driftCheck.ok) return driftCheck;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1043
1055
|
if (target === "verified") {
|
|
1044
1056
|
const vFile = path.join(PLANNING, `phase-${phase}-verification.md`);
|
|
1045
1057
|
if (!fs.existsSync(vFile))
|
|
@@ -1119,6 +1131,78 @@ function checkMachineEvidence(phase) {
|
|
|
1119
1131
|
return { ok: true };
|
|
1120
1132
|
}
|
|
1121
1133
|
|
|
1134
|
+
// Scope-drift gate (ITEM #5): enforce analyze-gate.js at the planned→built seam
|
|
1135
|
+
// instead of leaving it to the qualia-build SKILL prose ("please run analyze-gate").
|
|
1136
|
+
// "A rule worth enforcing is worth a hook" — so the highest-value catch (a plan
|
|
1137
|
+
// silently dropping a scope acceptance criterion) is now deterministic state.
|
|
1138
|
+
//
|
|
1139
|
+
// Fail-SOFT discipline: this gate must never crash a build on its OWN error. A
|
|
1140
|
+
// missing contract, missing analyze-gate.js, a thrown analyze(), or a parse
|
|
1141
|
+
// failure all return { ok: true } (advisory, build proceeds). It fail-CLOSES
|
|
1142
|
+
// only on a genuine finding: a strict-profile project with a HIGH analyze
|
|
1143
|
+
// finding (an under-covered scope AC or scope-reduction language). In
|
|
1144
|
+
// `standard` profile every finding stays advisory, mirroring the SKILL contract.
|
|
1145
|
+
function checkScopeDrift(phase, profile) {
|
|
1146
|
+
const contractFile = path.join(PLANNING, `phase-${phase}-contract.json`);
|
|
1147
|
+
// No contract → nothing to diff against. (planned→built already required a
|
|
1148
|
+
// contract at the `planned` gate, but a forced/legacy build may lack one.)
|
|
1149
|
+
if (!fs.existsSync(contractFile)) return { ok: true };
|
|
1150
|
+
|
|
1151
|
+
let analyzeGate;
|
|
1152
|
+
try {
|
|
1153
|
+
analyzeGate = require("./analyze-gate.js");
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
try { _trace("scope-drift", "skip", { phase, reason: "gate-unavailable", error: e.message }); } catch {}
|
|
1156
|
+
return { ok: true };
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Reuse plan-contract's reader so a malformed contract is a soft skip, not a throw.
|
|
1160
|
+
let result;
|
|
1161
|
+
try {
|
|
1162
|
+
const pc = require("./plan-contract.js");
|
|
1163
|
+
const loaded = pc.readContractFile(contractFile);
|
|
1164
|
+
if (!loaded.ok) {
|
|
1165
|
+
try { _trace("scope-drift", "skip", { phase, reason: "contract-unreadable", error: loaded.error }); } catch {}
|
|
1166
|
+
return { ok: true };
|
|
1167
|
+
}
|
|
1168
|
+
const scopePath = path.join(PLANNING, `phase-${phase}-context.md`);
|
|
1169
|
+
const contextPath = path.join(PLANNING, "CONTEXT.md");
|
|
1170
|
+
const readMaybe = (p) => { try { return fs.readFileSync(p, "utf8"); } catch { return null; } };
|
|
1171
|
+
result = analyzeGate.analyze({
|
|
1172
|
+
contract: loaded.contract,
|
|
1173
|
+
scopeMd: readMaybe(scopePath),
|
|
1174
|
+
contextMd: readMaybe(contextPath),
|
|
1175
|
+
});
|
|
1176
|
+
} catch (e) {
|
|
1177
|
+
// The gate's own failure must not block the build — fail soft.
|
|
1178
|
+
try { _trace("scope-drift", "error", { phase, error: e.message }); } catch {}
|
|
1179
|
+
return { ok: true };
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const highs = (result.findings || []).filter((f) => f.severity === "HIGH");
|
|
1183
|
+
if (highs.length === 0) {
|
|
1184
|
+
try { _trace("scope-drift", "allow", { phase, findings: (result.findings || []).length }); } catch {}
|
|
1185
|
+
return { ok: true };
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// standard profile: a senior may proceed past findings (advisory). The build
|
|
1189
|
+
// SKILL logs the waiver; state.js only records the trace and lets it through.
|
|
1190
|
+
if (profile === "standard") {
|
|
1191
|
+
try { _trace("scope-drift", "advisory", { phase, profile, high: highs.length }); } catch {}
|
|
1192
|
+
return { ok: true };
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// strict profile + HIGH finding → REFUSE the advance.
|
|
1196
|
+
const dropped = highs.map((f) => f.message).join("; ");
|
|
1197
|
+
try { _trace("scope-drift", "block", { phase, profile, high: highs.length, findings: highs.map((f) => f.type) }); } catch {}
|
|
1198
|
+
return fail(
|
|
1199
|
+
"SCOPE_DRIFT",
|
|
1200
|
+
`Phase ${phase} plan drifts from scope (${highs.length} HIGH): ${dropped}. ` +
|
|
1201
|
+
`Route to /qualia-plan ${phase} --gaps (plan dropped a requirement) or /qualia-scope ${phase} (scope is wrong). ` +
|
|
1202
|
+
`Override (standard profile / senior waiver) with --force.`
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1122
1206
|
function recordLedgerEvent(meta) {
|
|
1123
1207
|
try {
|
|
1124
1208
|
return stateLedger.append(process.cwd(), {
|
|
@@ -1166,7 +1250,10 @@ function nextCommand(status, phase, totalPhases, verification, lifecycle) {
|
|
|
1166
1250
|
case "done":
|
|
1167
1251
|
return operate ? "/qualia-update" : "Done.";
|
|
1168
1252
|
default:
|
|
1169
|
-
|
|
1253
|
+
// UNRECOGNIZED_STATUS — an unknown status must never recommend /qualia
|
|
1254
|
+
// itself (that's a self-recommending loop: /qualia reads state, state
|
|
1255
|
+
// points back at /qualia). Emit a diagnostic the caller can surface.
|
|
1256
|
+
return `UNRECOGNIZED_STATUS '${status}' — run /qualia-doctor`;
|
|
1170
1257
|
}
|
|
1171
1258
|
}
|
|
1172
1259
|
|
|
@@ -1517,15 +1604,16 @@ function cmdTransition(opts) {
|
|
|
1517
1604
|
|
|
1518
1605
|
const phase = parseInt(opts.phase) || s.phase;
|
|
1519
1606
|
|
|
1520
|
-
// Precondition check (lifecycle threaded so the handoff gate can relax in
|
|
1521
|
-
|
|
1607
|
+
// Precondition check (lifecycle threaded so the handoff gate can relax in
|
|
1608
|
+
// operate; profile threaded so the scope-drift gate knows strict vs standard)
|
|
1609
|
+
const check = checkPreconditions({ ...s, phase }, target, { ...opts, phase, lifecycle: t.lifecycle, profile: resolveProfile(s, t) });
|
|
1522
1610
|
if (!check.ok) {
|
|
1523
1611
|
// --force bypasses status-ordering and plan-content errors. The use case
|
|
1524
1612
|
// is retroactive bookkeeping: a phase was built without /qualia-plan and
|
|
1525
1613
|
// the user is catching STATE.md up to reality. --force never bypasses
|
|
1526
1614
|
// MISSING_FILE or MISSING_ARG — those would leave the state machine
|
|
1527
1615
|
// pointing at nothing.
|
|
1528
|
-
const forceableErrors = ["PRECONDITION_FAILED", "GAP_CYCLE_LIMIT", "INVALID_PLAN"];
|
|
1616
|
+
const forceableErrors = ["PRECONDITION_FAILED", "GAP_CYCLE_LIMIT", "INVALID_PLAN", "SCOPE_DRIFT"];
|
|
1529
1617
|
if (opts.force && forceableErrors.includes(check.error)) {
|
|
1530
1618
|
console.error(`WARNING: Forcing transition despite: ${check.message}`);
|
|
1531
1619
|
} else {
|
|
@@ -2102,7 +2190,7 @@ function cmdMigrate(opts) {
|
|
|
2102
2190
|
increment_ids: ids,
|
|
2103
2191
|
cursor: ids[(Number(t.phase || s.phase) || s.phase) - 1] || ids[0],
|
|
2104
2192
|
note: "STATE.md + tracking.json are now gitignored generated views. If they were previously committed, run `git rm --cached .planning/STATE.md .planning/tracking.json` to stop tracking them. Revert anytime with `state.js migrate --revert`.",
|
|
2105
|
-
next_command: nextCommand(s.status, s.phase, s.total_phases, t.verification),
|
|
2193
|
+
next_command: nextCommand(s.status, s.phase, s.total_phases, t.verification, t.lifecycle),
|
|
2106
2194
|
});
|
|
2107
2195
|
}
|
|
2108
2196
|
|
|
@@ -2158,7 +2246,8 @@ function cmdClaim(opts) {
|
|
|
2158
2246
|
writeCursor({ current_increment: id });
|
|
2159
2247
|
regenerateViews(`Claimed ${id} (${me})`);
|
|
2160
2248
|
recordLedgerEvent({ action: "claim", phase_before: inc.num, phase_after: inc.num, status_before: inc.status, status_after: inc.status });
|
|
2161
|
-
|
|
2249
|
+
const lifecycle = (readTracking() || {}).lifecycle;
|
|
2250
|
+
output({ ok: true, action: "claim", id, claimed_by: me, branch: inc.branch, status: inc.status, next_command: nextCommand(inc.status, inc.num, listIncrements().length, inc.verification, lifecycle) });
|
|
2162
2251
|
}
|
|
2163
2252
|
|
|
2164
2253
|
// ─── A5: release (ship) an increment + clear its claim ──
|