@smartmemory/compose 0.2.51 → 0.2.53-beta
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/skills/compose/SKILL.md +0 -8
- package/README.md +11 -0
- package/bin/compose.js +335 -3
- package/dist/assets/App-Bu9KMtTa.js +891 -0
- package/dist/assets/abnfDiagram-VRR7QNED-6zp3w9rx.js +1 -0
- package/dist/assets/arc-CnLxxuah.js +1 -0
- package/dist/assets/architectureDiagram-ZJ3FMSHR-COM46S9k.js +36 -0
- package/dist/assets/blockDiagram-677ZJIJ3-wKzgOwF8.js +132 -0
- package/dist/assets/{browser-CnKiSnlr.js → browser-Cr0recrN.js} +6 -6
- package/dist/assets/{c4Diagram-AHTNJAMY-nBIRLJTv.js → c4Diagram-LMCZKHZV-DIXC_mR6.js} +1 -1
- package/dist/assets/channel-DQJPWkb_.js +1 -0
- package/dist/assets/{chunk-QZHKN3VN-B4Wfybww.js → chunk-2Q5K7J3B-BqLqZX6m.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-DE6_5aqh.js → chunk-32BRIVSS-CarSmVlX.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-tYY8xcjr.js → chunk-5VM5RSS4-D-f_Jn3G.js} +1 -1
- package/dist/assets/chunk-EX3LRPZG-DT42xo8E.js +231 -0
- package/dist/assets/{chunk-4BX2VUAB-CNmGyhrp.js → chunk-JWPE2WC7-CwY1aZc4.js} +1 -1
- package/dist/assets/chunk-MOJQB5TN-DBbZU_KX.js +88 -0
- package/dist/assets/chunk-RYQCIY6F-DqxtcLJ3.js +1 -0
- package/dist/assets/chunk-V7JOEXUC-Cy4Ixww6.js +206 -0
- package/dist/assets/{chunk-EDXVE4YY-TPlt1bS2.js → chunk-VR4S4FIN-CFexfU_a.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-D8KuKVLa.js → chunk-XXDRQBXY-uE-zN_vc.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-CTLbAiUK.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-CTLbAiUK.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-DzzRyJtB.js → cose-bilkent-JH36ORCC-DIjpekos.js} +1 -1
- package/dist/assets/cynefin-VYW2F7L2-Ba2gbQow.js +178 -0
- package/dist/assets/cynefinDiagram-TSTJHNR4-njGzb7Tg.js +62 -0
- package/dist/assets/dagre-VKFMJZFB-DuyZREZ3.js +4 -0
- package/dist/assets/diagram-FQU43EPY-Npq-5o3a.js +3 -0
- package/dist/assets/diagram-G47NLZAW-fm4k3axC.js +24 -0
- package/dist/assets/diagram-NH7WQ7WH-B8EFGrTG.js +24 -0
- package/dist/assets/diagram-OA4YK3LP-BC7UHx9Q.js +30 -0
- package/dist/assets/diagram-WEI45ONY-BqjHLcCX.js +41 -0
- package/dist/assets/ebnfDiagram-CCIWWBDH-D5zXd4Qg.js +1 -0
- package/dist/assets/erDiagram-Q63AITRT-DC2FMcra.js +85 -0
- package/dist/assets/flowDiagram-23GEKE2U-D7Dx7JU4.js +156 -0
- package/dist/assets/ganttDiagram-NO4QXBWP-8sQH2y5K.js +292 -0
- package/dist/assets/gitGraphDiagram-IHSO6WYX-Ci_rhxur.js +106 -0
- package/dist/assets/graph-C9eacEi8.js +1 -0
- package/dist/assets/graph-xkel59g2.js +331 -0
- package/dist/assets/index-B7-HQenC.js +119 -0
- package/dist/assets/index-BxRamj_i.css +1 -0
- package/dist/assets/infoDiagram-FWYZ7A6U-B9FGQ0Cb.js +2 -0
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-DWx-7BBy.js → ishikawaDiagram-FXEZZL3T-BEKLyH6A.js} +5 -5
- package/dist/assets/{journeyDiagram-VCZTEJTY-C42hXNha.js → journeyDiagram-5HDEW3XC-BM-IDVRo.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-CEq930ew.js → kanban-definition-HUTT4EX6-Beb2k3pt.js} +7 -7
- package/dist/assets/katex-C5jXJg4s.js +257 -0
- package/dist/assets/layout-DEXfKzaS.js +1 -0
- package/dist/assets/{linear-D158F7OT.js → linear-Dwg7dTpz.js} +1 -1
- package/dist/assets/map-Czzmt4hB.js +1 -0
- package/dist/assets/{mindmap-definition-QFDTVHPH-d9SSh2nG.js → mindmap-definition-LN4V7U3C-CeaynfcE.js} +7 -7
- package/dist/assets/{mobile-Chw8RWyH.js → mobile-CNLMhdFP.js} +2 -2
- package/dist/assets/pegDiagram-2B236MQR-7SVdAVqj.js +1 -0
- package/dist/assets/pieDiagram-ENE6RG2P-CzqQ4TaE.js +39 -0
- package/dist/assets/quadrantDiagram-ABIIQ3AL-B9B6W80N.js +7 -0
- package/dist/assets/railroadDiagram-RFXS5EU6-BRNLawsr.js +1 -0
- package/dist/assets/{requirementDiagram-MS252O5E-B1ZFIotK.js → requirementDiagram-TGXJPOKE-CmASubeG.js} +3 -3
- package/dist/assets/sankeyDiagram-HTMAVEWB-DiReR0OB.js +40 -0
- package/dist/assets/sequenceDiagram-DBY2YBRQ-DVn5iZKR.js +162 -0
- package/dist/assets/sizeCapture-X5ZJPWSS-C8CuDOdp.js +1 -0
- package/dist/assets/stateDiagram-2N3HPSRC-BIdkHglY.js +1 -0
- package/dist/assets/stateDiagram-v2-6OUMAXLB-B9yje3Er.js +1 -0
- package/dist/assets/swimlanes-5IMT3BWC-BlVsYyyN.js +2 -0
- package/dist/assets/swimlanesDiagram-G3AALYLV-BTLpU45n.js +8 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-k_0vJQAK.js → timeline-definition-FHXFAJF6-CnTqJmQ2.js} +3 -3
- package/dist/assets/vennDiagram-L72KCM5P-D_qZxRhe.js +34 -0
- package/dist/assets/wardleyDiagram-EHGQE667-CT-WHFEO.js +78 -0
- package/dist/assets/xychartDiagram-FW5EYKEG-CVRFHwUQ.js +7 -0
- package/dist/index.html +3 -3
- package/lib/agent-string.js +34 -0
- package/lib/build.js +542 -148
- package/lib/experiment-judge.js +145 -0
- package/lib/experiment-metrics.js +265 -0
- package/lib/experiment-pricing.js +60 -0
- package/lib/experiment-report.js +306 -0
- package/lib/experiment-sandbox.js +147 -0
- package/lib/experiment.js +539 -0
- package/lib/feature-json.js +1 -1
- package/lib/feature-writer.js +30 -0
- package/lib/flow-state.js +36 -0
- package/lib/gate-prompt.js +66 -6
- package/lib/lifecycle-modes.js +213 -0
- package/lib/new.js +6 -2
- package/lib/roadmap-graph/index.js +26 -28
- package/lib/roadmap-graph/vision-adapter.js +126 -0
- package/lib/stratum-mcp-client.js +9 -0
- package/lib/triage.js +7 -1
- package/lib/vision-writer.js +30 -6
- package/package.json +1 -1
- package/pipelines/plan.stratum.yaml +161 -0
- package/scripts/release-manual.sh +98 -0
- package/server/artifact-manager.js +30 -3
- package/server/compose-mcp-tools.js +11 -9
- package/server/compose-mcp.js +4 -0
- package/server/feature-scan.js +87 -173
- package/server/file-watcher.js +33 -0
- package/server/graph-export.js +0 -0
- package/server/index.js +10 -5
- package/server/lifecycle-guard.js +62 -25
- package/server/pipeline-routes.js +773 -3
- package/server/roadmap-graph-vision.js +138 -0
- package/server/status-snapshot.js +8 -7
- package/server/vision-routes.js +64 -33
- package/server/vision-store.js +13 -4
- package/.claude/skills/compose/references/hermes-tools.md +0 -80
- package/dist/assets/App-CT0vXPgd.js +0 -724
- package/dist/assets/_baseUniq-CsOHc_iS.js +0 -1
- package/dist/assets/arc-BXht3LyE.js +0 -1
- package/dist/assets/architectureDiagram-Q4EWVU46-BV1r86-w.js +0 -36
- package/dist/assets/blockDiagram-DXYQGD6D-BgcMJjAR.js +0 -132
- package/dist/assets/channel-Dd4XaiYv.js +0 -1
- package/dist/assets/chunk-4TB4RGXK-MLT7I7w4.js +0 -206
- package/dist/assets/chunk-OYMX7WX6-yBPwT1AT.js +0 -231
- package/dist/assets/classDiagram-6PBFFD2Q-DshSQDF9.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-DshSQDF9.js +0 -1
- package/dist/assets/clone-CBsbNGAa.js +0 -1
- package/dist/assets/dagre-KV5264BT-CTcTVGdU.js +0 -4
- package/dist/assets/diagram-5BDNPKRD-qR6mc8kJ.js +0 -10
- package/dist/assets/diagram-G4DWMVQ6-CUcGXyNb.js +0 -24
- package/dist/assets/diagram-MMDJMWI5-B68cPonH.js +0 -43
- package/dist/assets/diagram-TYMM5635-q8Id8eFG.js +0 -24
- package/dist/assets/erDiagram-SMLLAGMA-CtxJi57K.js +0 -85
- package/dist/assets/flowDiagram-DWJPFMVM-BzzO4JYU.js +0 -162
- package/dist/assets/ganttDiagram-T4ZO3ILL-Dn0wIyhR.js +0 -292
- package/dist/assets/gitGraphDiagram-UUTBAWPF-tnNFXfMM.js +0 -106
- package/dist/assets/graph-DZe55uk8.js +0 -331
- package/dist/assets/graph-Tq_bs_r0.js +0 -1
- package/dist/assets/index-8UhRLbGq.js +0 -123
- package/dist/assets/index-CRqB9els.css +0 -1
- package/dist/assets/infoDiagram-42DDH7IO-BXLvLeS0.js +0 -2
- package/dist/assets/katex-DkKDou_j.js +0 -257
- package/dist/assets/layout-qtUgN9BC.js +0 -1
- package/dist/assets/min-hESxN-c0.js +0 -1
- package/dist/assets/pieDiagram-DEJITSTG-gglmeMav.js +0 -30
- package/dist/assets/quadrantDiagram-34T5L4WZ-CqFC_htU.js +0 -7
- package/dist/assets/sankeyDiagram-XADWPNL6-BvItvZt5.js +0 -10
- package/dist/assets/sequenceDiagram-FGHM5R23-CAIn5Z3U.js +0 -157
- package/dist/assets/stateDiagram-FHFEXIEX-CyJrGzib.js +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DprhF_Ho.js +0 -1
- package/dist/assets/vennDiagram-DHZGUBPP-TZAW60b0.js +0 -34
- package/dist/assets/wardley-RL74JXVD-DzQEoG6X.js +0 -162
- package/dist/assets/wardleyDiagram-NUSXRM2D-BAoVVIjZ.js +0 -20
- package/dist/assets/xychartDiagram-5P7HB3ND-CynkZcUb.js +0 -7
- package/lib/roadmap-graph/collect.js +0 -178
package/lib/gate-prompt.js
CHANGED
|
@@ -107,8 +107,59 @@ function buildRecommendation(gateDispatch, gateExtras) {
|
|
|
107
107
|
};
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
// COMP-PLAN-GATE-LOOP (secondary): default deadline for a gate prompt that is
|
|
111
|
+
// reading the real stdin in a non-interactive context. Long enough for a piped
|
|
112
|
+
// producer to answer, short enough that a backgrounded/no-input runner fails
|
|
113
|
+
// fast instead of blocking forever.
|
|
114
|
+
export const GATE_NONINTERACTIVE_TIMEOUT_MS = 120_000;
|
|
115
|
+
|
|
116
|
+
const GATE_NONINTERACTIVE_HELP =
|
|
117
|
+
'stdin is not a TTY and the Compose server is unreachable, so the gate cannot ' +
|
|
118
|
+
'be answered. Start the :4001 server, run the gate interactively, or pass --all ' +
|
|
119
|
+
'to auto-approve. The flow state is preserved — re-run with --resume.';
|
|
120
|
+
|
|
121
|
+
export class GateInputUnavailableError extends Error {
|
|
122
|
+
constructor(reason) {
|
|
123
|
+
super(`Gate decision required but ${reason}. ${GATE_NONINTERACTIVE_HELP}`);
|
|
124
|
+
this.name = 'GateInputUnavailableError';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Read one line from the readline interface.
|
|
130
|
+
*
|
|
131
|
+
* When `armGuard` is set (real stdin, no TTY — see promptGate) the read is
|
|
132
|
+
* bounded: it rejects with GateInputUnavailableError if the input stream ends
|
|
133
|
+
* (EOF, e.g. stdin is /dev/null) or no line arrives within `deadlineMs`, so a
|
|
134
|
+
* non-interactive runner fails fast instead of blocking forever. Piped input
|
|
135
|
+
* still resolves normally; an unguarded read keeps the original behavior.
|
|
136
|
+
*
|
|
137
|
+
* @param {import('node:readline').Interface} rl
|
|
138
|
+
* @param {string} question
|
|
139
|
+
* @param {{ armGuard?: boolean, deadlineMs?: number }} [opts]
|
|
140
|
+
* @returns {Promise<string>}
|
|
141
|
+
*/
|
|
142
|
+
export function ask(rl, question, { armGuard = false, deadlineMs = GATE_NONINTERACTIVE_TIMEOUT_MS } = {}) {
|
|
143
|
+
if (!armGuard) {
|
|
144
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
145
|
+
}
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
let settled = false;
|
|
148
|
+
const onEnd = () => finish(() => reject(new GateInputUnavailableError('stdin was closed before a decision')));
|
|
149
|
+
const timer = setTimeout(
|
|
150
|
+
() => finish(() => reject(new GateInputUnavailableError(`no input arrived within ${Math.round(deadlineMs / 1000)}s`))),
|
|
151
|
+
deadlineMs,
|
|
152
|
+
);
|
|
153
|
+
function finish(act) {
|
|
154
|
+
if (settled) return;
|
|
155
|
+
settled = true;
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
rl.input.removeListener('end', onEnd);
|
|
158
|
+
act();
|
|
159
|
+
}
|
|
160
|
+
rl.input.once('end', onEnd);
|
|
161
|
+
rl.question(question, answer => finish(() => resolve(answer)));
|
|
162
|
+
});
|
|
112
163
|
}
|
|
113
164
|
|
|
114
165
|
/**
|
|
@@ -179,7 +230,7 @@ function drawGatePanel(out, gateDispatch, { artifact, gateExtras } = {}) {
|
|
|
179
230
|
* @param {object} [options.gateExtras] - { fromPhase, toPhase } for panel display
|
|
180
231
|
* @returns {Promise<{ outcome: string, rationale: string }>}
|
|
181
232
|
*/
|
|
182
|
-
export async function promptGate(gateDispatch, { input, output, artifact, askAgent, gateExtras, nonInteractive } = {}) {
|
|
233
|
+
export async function promptGate(gateDispatch, { input, output, artifact, askAgent, gateExtras, nonInteractive, timeoutMs } = {}) {
|
|
183
234
|
if (nonInteractive) {
|
|
184
235
|
return { outcome: 'approve', rationale: 'auto-approved (--all mode)' };
|
|
185
236
|
}
|
|
@@ -189,6 +240,15 @@ export async function promptGate(gateDispatch, { input, output, artifact, askAge
|
|
|
189
240
|
output: output ?? process.stdout,
|
|
190
241
|
});
|
|
191
242
|
|
|
243
|
+
// COMP-PLAN-GATE-LOOP (secondary): only bound the read when there is no
|
|
244
|
+
// interactive terminal AND we're on the real stdin. A real TTY (a human who
|
|
245
|
+
// may take their time) and an injected `input` (tests, piped/programmatic
|
|
246
|
+
// callers) keep the original block-until-answered behavior.
|
|
247
|
+
const askGuard = {
|
|
248
|
+
armGuard: input === undefined && !process.stdin.isTTY,
|
|
249
|
+
deadlineMs: timeoutMs ?? GATE_NONINTERACTIVE_TIMEOUT_MS,
|
|
250
|
+
};
|
|
251
|
+
|
|
192
252
|
try {
|
|
193
253
|
const { step_id, on_approve, on_revise, on_kill } = gateDispatch;
|
|
194
254
|
|
|
@@ -219,7 +279,7 @@ export async function promptGate(gateDispatch, { input, output, artifact, askAge
|
|
|
219
279
|
let outcome;
|
|
220
280
|
|
|
221
281
|
while (!outcome) {
|
|
222
|
-
const raw = await ask(rl, '\n> ');
|
|
282
|
+
const raw = await ask(rl, '\n> ', askGuard);
|
|
223
283
|
const trimmed = raw.trim();
|
|
224
284
|
|
|
225
285
|
// COMP-UX-3b: Enter alone → use recommended default action (only if recommendation was shown)
|
|
@@ -266,7 +326,7 @@ export async function promptGate(gateDispatch, { input, output, artifact, askAge
|
|
|
266
326
|
let rationale;
|
|
267
327
|
if (notes.length > 0) {
|
|
268
328
|
rationale = notes.join('\n');
|
|
269
|
-
const addMore = await ask(rl, ' Additional rationale (or Enter to use notes): ');
|
|
329
|
+
const addMore = await ask(rl, ' Additional rationale (or Enter to use notes): ', askGuard);
|
|
270
330
|
if (addMore.trim()) {
|
|
271
331
|
rationale += '\n' + addMore.trim();
|
|
272
332
|
}
|
|
@@ -274,7 +334,7 @@ export async function promptGate(gateDispatch, { input, output, artifact, askAge
|
|
|
274
334
|
rationale = 'approved';
|
|
275
335
|
} else {
|
|
276
336
|
while (!rationale) {
|
|
277
|
-
const raw = await ask(rl, 'Rationale: ');
|
|
337
|
+
const raw = await ask(rl, 'Rationale: ', askGuard);
|
|
278
338
|
const trimmed = raw.trim();
|
|
279
339
|
if (trimmed) {
|
|
280
340
|
rationale = trimmed;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// lib/lifecycle-modes.js
|
|
2
|
+
//
|
|
3
|
+
// COMP-ROADMAP-MODES — the mode-keyed lifecycle registry (keystone).
|
|
4
|
+
//
|
|
5
|
+
// Compose's lifecycle data (phase graph, skippable/terminal sets, the
|
|
6
|
+
// completion edge, per-phase artifacts, edge evidence, phase ordering, and the
|
|
7
|
+
// runner's per-mode behavioral switches) was hard-coded to the BUILD graph and
|
|
8
|
+
// scattered across server/lifecycle-guard.js, server/artifact-manager.js, and
|
|
9
|
+
// lib/build.js. This module lifts that data into a single source of truth keyed
|
|
10
|
+
// by lifecycle mode (`build | fix | plan`), so a third lifecycle (`plan`) is a
|
|
11
|
+
// data add, not a code fork, and so integration ports can later be pluggable.
|
|
12
|
+
//
|
|
13
|
+
// INVARIANT: the `build` entry reproduces the legacy hard-coded data VERBATIM —
|
|
14
|
+
// `build` behavior must stay byte-identical. test/lifecycle-modes.test.js pins
|
|
15
|
+
// the build entry against the legacy exports.
|
|
16
|
+
//
|
|
17
|
+
// Pure data + accessors. No imports from server/ (lib is the lower layer; the
|
|
18
|
+
// runner and server consume THIS, never the reverse). Loaders/functions stay in
|
|
19
|
+
// their owning modules, keyed here by a string the consumer interprets.
|
|
20
|
+
//
|
|
21
|
+
// See docs/features/COMP-ROADMAP-MODES/{design,blueprint,plan}.md.
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} ModeRunnerConfig
|
|
25
|
+
* @property {'features'|'docs/bugs'|'docs/plans'} artifactRoot token the runner maps to a dir
|
|
26
|
+
* @property {boolean} runsTriage whether the runner runs feature triage
|
|
27
|
+
* @property {boolean} tracksFeatureJson whether the runner writes feature.json lifecycle status
|
|
28
|
+
* @property {'feature'|'bug'|'plan'} descriptionLoader which description loader the runner uses
|
|
29
|
+
* @property {'feature'|'bug'|'plan'} planInputs which Stratum plan-input shape to build
|
|
30
|
+
* @property {string} defaultTemplate default Stratum template name for the mode
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** The mode registry. Keys are canonical modes; `build` data is verbatim-legacy. */
|
|
34
|
+
export const LIFECYCLE_MODES = {
|
|
35
|
+
// -------------------------------------------------------------------------
|
|
36
|
+
// BUILD — verbatim from server/lifecycle-guard.js (BASE_TRANSITIONS/SKIPPABLE/
|
|
37
|
+
// TERMINAL) + server/artifact-manager.js (PHASE_ARTIFACTS) + the runner's
|
|
38
|
+
// current feature behavior. MUST stay byte-identical.
|
|
39
|
+
// -------------------------------------------------------------------------
|
|
40
|
+
build: {
|
|
41
|
+
transitions: {
|
|
42
|
+
explore_design: ['prd', 'architecture', 'blueprint'],
|
|
43
|
+
prd: ['architecture', 'blueprint'],
|
|
44
|
+
architecture: ['blueprint'],
|
|
45
|
+
blueprint: ['verification'],
|
|
46
|
+
verification: ['plan', 'blueprint'],
|
|
47
|
+
plan: ['execute'],
|
|
48
|
+
execute: ['report', 'docs'],
|
|
49
|
+
report: ['docs'],
|
|
50
|
+
docs: ['ship'],
|
|
51
|
+
ship: [],
|
|
52
|
+
},
|
|
53
|
+
skippable: ['prd', 'architecture', 'report'],
|
|
54
|
+
terminal: ['complete', 'killed'],
|
|
55
|
+
genesis: 'explore_design',
|
|
56
|
+
completablePhase: 'ship', // the phase that → complete (was hard-coded by name)
|
|
57
|
+
phaseArtifacts: ['design.md', 'prd.md', 'architecture.md', 'blueprint.md', 'plan.md', 'report.md'],
|
|
58
|
+
edgeEvidence: {
|
|
59
|
+
'explore_design->blueprint': 'design.md',
|
|
60
|
+
'blueprint->verification': 'blueprint.md',
|
|
61
|
+
'plan->execute': 'plan.md',
|
|
62
|
+
},
|
|
63
|
+
phaseOrder: ['explore_design', 'prd', 'architecture', 'blueprint', 'verification', 'plan', 'execute', 'report', 'docs', 'ship'],
|
|
64
|
+
runner: {
|
|
65
|
+
artifactRoot: 'features',
|
|
66
|
+
runsTriage: true,
|
|
67
|
+
tracksFeatureJson: true,
|
|
68
|
+
descriptionLoader: 'feature',
|
|
69
|
+
planInputs: 'feature',
|
|
70
|
+
defaultTemplate: 'build',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// -------------------------------------------------------------------------
|
|
75
|
+
// FIX — describes today's bug-fix lifecycle. Data parity only: the bug flow
|
|
76
|
+
// registers NO guard today and MODES does not change that, so the graph here
|
|
77
|
+
// is informational (exercised by registry tests, not wired into a live guard).
|
|
78
|
+
// The `runner` block IS load-bearing: it reproduces today's bug runner
|
|
79
|
+
// behavior byte-identically (docs/bugs root, no triage, no feature.json).
|
|
80
|
+
// phaseArtifacts is [] → assess() falls back to the global default set, which
|
|
81
|
+
// is exactly how bug items are assessed today (no change).
|
|
82
|
+
// -------------------------------------------------------------------------
|
|
83
|
+
fix: {
|
|
84
|
+
transitions: {
|
|
85
|
+
reproduce: ['diagnose'],
|
|
86
|
+
diagnose: ['scope_check', 'fix'],
|
|
87
|
+
scope_check: ['fix'],
|
|
88
|
+
fix: ['test'],
|
|
89
|
+
test: ['verify', 'fix'],
|
|
90
|
+
verify: ['retro_check', 'ship'],
|
|
91
|
+
retro_check: ['ship'],
|
|
92
|
+
ship: [],
|
|
93
|
+
},
|
|
94
|
+
skippable: [],
|
|
95
|
+
terminal: ['complete', 'killed'],
|
|
96
|
+
genesis: 'reproduce',
|
|
97
|
+
completablePhase: 'ship',
|
|
98
|
+
phaseArtifacts: [], // empty → assess uses the global default (preserves today's bug behavior)
|
|
99
|
+
edgeEvidence: {},
|
|
100
|
+
phaseOrder: ['reproduce', 'diagnose', 'scope_check', 'fix', 'test', 'verify', 'retro_check', 'ship'],
|
|
101
|
+
runner: {
|
|
102
|
+
artifactRoot: 'docs/bugs',
|
|
103
|
+
runsTriage: false,
|
|
104
|
+
tracksFeatureJson: false,
|
|
105
|
+
descriptionLoader: 'bug',
|
|
106
|
+
planInputs: 'bug',
|
|
107
|
+
defaultTemplate: 'bug-fix',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
// -------------------------------------------------------------------------
|
|
112
|
+
// PLAN — minimal SEED graph. COMP-ROADMAP-PLAN owns the real plan lifecycle
|
|
113
|
+
// (framing → research → ideation → convergence → build-handshake) and the
|
|
114
|
+
// `compose plan` command. MODES ships only the mechanism: a coherent seed so
|
|
115
|
+
// a third mode is proven to be a data add. Not run by any CLI command yet.
|
|
116
|
+
// -------------------------------------------------------------------------
|
|
117
|
+
plan: {
|
|
118
|
+
transitions: {
|
|
119
|
+
explore_design: ['plan'],
|
|
120
|
+
plan: ['ship'],
|
|
121
|
+
ship: [],
|
|
122
|
+
},
|
|
123
|
+
skippable: [],
|
|
124
|
+
terminal: ['complete', 'killed'],
|
|
125
|
+
genesis: 'explore_design',
|
|
126
|
+
completablePhase: 'ship',
|
|
127
|
+
phaseArtifacts: ['design.md', 'plan.md'],
|
|
128
|
+
edgeEvidence: { 'explore_design->plan': 'design.md' },
|
|
129
|
+
phaseOrder: ['explore_design', 'plan', 'ship'],
|
|
130
|
+
runner: {
|
|
131
|
+
artifactRoot: 'docs/plans',
|
|
132
|
+
runsTriage: false,
|
|
133
|
+
tracksFeatureJson: false,
|
|
134
|
+
descriptionLoader: 'plan',
|
|
135
|
+
planInputs: 'plan',
|
|
136
|
+
defaultTemplate: 'plan',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Map any runtime or canonical mode token to a canonical registry key. Any key
|
|
143
|
+
* present in LIFECYCLE_MODES resolves to itself — so adding a new mode is a
|
|
144
|
+
* DATA-ONLY change (no edit here). The legacy runtime aliases (feature→build,
|
|
145
|
+
* bug→fix) are honored for back-compat; everything else falls back to build.
|
|
146
|
+
*/
|
|
147
|
+
export function resolveMode(raw) {
|
|
148
|
+
if (raw && Object.prototype.hasOwnProperty.call(LIFECYCLE_MODES, raw)) return raw;
|
|
149
|
+
if (raw === 'bug') return 'fix';
|
|
150
|
+
if (raw === 'feature') return 'build';
|
|
151
|
+
return 'build'; // undefined / null / unknown → build (legacy default)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Get the mode entry, normalizing the input. Unknown → build. */
|
|
155
|
+
export function getMode(mode) {
|
|
156
|
+
return LIFECYCLE_MODES[resolveMode(mode)];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function genesisOf(mode) {
|
|
160
|
+
return getMode(mode).genesis;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function completablePhaseOf(mode) {
|
|
164
|
+
return getMode(mode).completablePhase;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function transitionsOf(mode) {
|
|
168
|
+
return getMode(mode).transitions;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function skippableOf(mode) {
|
|
172
|
+
return getMode(mode).skippable;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function terminalOf(mode) {
|
|
176
|
+
return getMode(mode).terminal;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function phaseOrderOf(mode) {
|
|
180
|
+
return getMode(mode).phaseOrder;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Per-edge ("from->to" → artifact filename) evidence map for the mode. */
|
|
184
|
+
export function edgeEvidenceOf(mode) {
|
|
185
|
+
return getMode(mode).edgeEvidence;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The union of every mode's phase vocabulary — each mode's `phaseOrder` plus its
|
|
190
|
+
* terminal phases. Read-side recognizers (e.g. status-snapshot's KNOWN_PHASES)
|
|
191
|
+
* derive from this so a non-build-mode phase isn't flagged "unrecognized".
|
|
192
|
+
* MUST include the terminal phases (complete/killed): the snapshot's
|
|
193
|
+
* unknown-phase guard runs BEFORE its terminal branches.
|
|
194
|
+
*/
|
|
195
|
+
export function allKnownPhases() {
|
|
196
|
+
const set = new Set();
|
|
197
|
+
for (const key of Object.keys(LIFECYCLE_MODES)) {
|
|
198
|
+
const m = LIFECYCLE_MODES[key];
|
|
199
|
+
for (const p of m.phaseOrder) set.add(p);
|
|
200
|
+
for (const t of m.terminal) set.add(t);
|
|
201
|
+
}
|
|
202
|
+
return [...set];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The artifact filenames a mode declares. An EMPTY list means "no narrowing" —
|
|
207
|
+
* the artifact manager falls back to the global ARTIFACT_SCHEMAS key set (which
|
|
208
|
+
* is exactly today's behavior for build and bug items). A non-empty list scopes
|
|
209
|
+
* assessment to that subset.
|
|
210
|
+
*/
|
|
211
|
+
export function artifactsOf(mode) {
|
|
212
|
+
return getMode(mode).phaseArtifacts;
|
|
213
|
+
}
|
package/lib/new.js
CHANGED
|
@@ -16,6 +16,7 @@ import { installFactoryShim } from './connector-factory-shim.js';
|
|
|
16
16
|
import { buildStepPrompt, buildRetryPrompt, buildGateContext } from './step-prompt.js';
|
|
17
17
|
import { promptGate } from './gate-prompt.js';
|
|
18
18
|
import { VisionWriter } from './vision-writer.js';
|
|
19
|
+
import { readFlowRound } from './flow-state.js';
|
|
19
20
|
|
|
20
21
|
import { validateStep } from './step-validator.js';
|
|
21
22
|
|
|
@@ -197,7 +198,9 @@ export async function runNew(intent, opts = {}) {
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
|
|
201
|
+
// COMP-PLAN-GATE-LOOP: round-aware gate id so a revise re-entry mints a
|
|
202
|
+
// fresh gate instead of reusing the prior resolved one.
|
|
203
|
+
const gateId = await visionWriter.createGate(flowId, stepId, itemId, { round: readFlowRound(flowId) });
|
|
201
204
|
|
|
202
205
|
// Resolve artifact path for this gate
|
|
203
206
|
const gateArtifact = priorValConfig?.artifact
|
|
@@ -340,7 +343,8 @@ async function executeChildFlow(
|
|
|
340
343
|
|
|
341
344
|
} else if (resp.status === 'await_gate') {
|
|
342
345
|
console.log(` [${childFlowName}] Gate: ${resp.step_id}`);
|
|
343
|
-
|
|
346
|
+
// COMP-PLAN-GATE-LOOP: round-aware gate id (child flow has its own round).
|
|
347
|
+
const gateId = await visionWriter.createGate(childFlowId, resp.step_id, itemId, { round: readFlowRound(childFlowId) });
|
|
344
348
|
const { outcome, rationale } = await promptGate(resp, gateOpts);
|
|
345
349
|
await visionWriter.resolveGate(gateId, outcome);
|
|
346
350
|
resp = await stratum.gateResolve(childFlowId, resp.step_id, outcome, rationale, 'human');
|
|
@@ -1,45 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* index.js —
|
|
3
|
-
* (COMP-ROADMAP-GRAPH-1
|
|
2
|
+
* index.js — shared render/write/check helpers for the roadmap dependency
|
|
3
|
+
* graph (COMP-ROADMAP-GRAPH-1 renderer; COMP-ROADMAP-GRAPH-2 made it
|
|
4
|
+
* source-agnostic). The collection of inputs lives with the caller — today the
|
|
5
|
+
* sole collector is the vision projection in server/roadmap-graph-vision.js.
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
+
* buildArtifactFromInputs(cwd, inputs) — buildGraph + render (no write)
|
|
8
|
+
* writeArtifact(cwd, built, opts) — atomic-write the HTML
|
|
9
|
+
* checkArtifact(cwd, built, opts) — diff vs on-disk
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
* unknown feature — the Cytoscape-crash bug class this feature kills.
|
|
11
|
+
* buildGraph throws DanglingEdgeError (code DANGLING_EDGE) when an edge points
|
|
12
|
+
* at an unknown feature — the Cytoscape-crash bug class this feature kills.
|
|
10
13
|
*/
|
|
11
14
|
import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
12
15
|
import { join, isAbsolute, dirname } from 'node:path';
|
|
13
|
-
import { resolveFeaturesPath } from '../project-paths.js';
|
|
14
|
-
import { collectGraphInputs } from './collect.js';
|
|
15
16
|
import { buildGraph } from './model.js';
|
|
16
17
|
import { renderGraphHtml } from './render.js';
|
|
17
18
|
import { loadGraphConfig } from './config.js';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
|
-
* Build the HTML +
|
|
21
|
+
* Build the HTML + stats from already-collected graph inputs. The inputs may
|
|
22
|
+
* come from any collector (the static feature.json/deps.yaml path, or the
|
|
23
|
+
* vision-store canonical projection) — this is the single render seam.
|
|
21
24
|
* @param {string} cwd
|
|
22
|
-
* @
|
|
25
|
+
* @param {{ nodes: object[], rawEdges: object[], knownCodes: Set<string>, warnings?: string[] }} inputs
|
|
23
26
|
*/
|
|
24
|
-
export function
|
|
25
|
-
const featuresDir = resolveFeaturesPath(cwd);
|
|
26
|
-
const inputs = collectGraphInputs(cwd, featuresDir);
|
|
27
|
+
export function buildArtifactFromInputs(cwd, inputs) {
|
|
27
28
|
const graph = buildGraph(inputs); // throws DanglingEdgeError
|
|
28
29
|
const config = loadGraphConfig(cwd);
|
|
29
30
|
const html = renderGraphHtml({ nodes: graph.nodes, edges: graph.edges, config });
|
|
30
|
-
return { html, nodes: graph.nodes, edges: graph.edges, dropped: graph.dropped, warnings: inputs.warnings, config };
|
|
31
|
+
return { html, nodes: graph.nodes, edges: graph.edges, dropped: graph.dropped, warnings: inputs.warnings || [], config };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveOutPath(cwd, built, opts) {
|
|
35
|
+
const outRel = opts.out || built.config.out;
|
|
36
|
+
return isAbsolute(outRel) ? outRel : join(cwd, outRel);
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
/**
|
|
34
|
-
*
|
|
35
|
-
* @param {string} cwd project root
|
|
36
|
-
* @param {{ out?: string }} [opts] out path (relative to cwd unless absolute); defaults to config.out
|
|
40
|
+
* Atomically write a built artifact's HTML and return the summary.
|
|
37
41
|
* @returns {{ path: string, nodeCount: number, edgeCount: number, droppedCount: number, warnings: string[] }}
|
|
38
42
|
*/
|
|
39
|
-
export function
|
|
40
|
-
const
|
|
41
|
-
const outRel = opts.out || built.config.out;
|
|
42
|
-
const outPath = isAbsolute(outRel) ? outRel : join(cwd, outRel);
|
|
43
|
+
export function writeArtifact(cwd, built, opts = {}) {
|
|
44
|
+
const outPath = resolveOutPath(cwd, built, opts);
|
|
43
45
|
atomicWrite(outPath, built.html);
|
|
44
46
|
return {
|
|
45
47
|
path: outPath,
|
|
@@ -51,15 +53,11 @@ export function generateRoadmapGraph(cwd, opts = {}) {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
|
-
*
|
|
55
|
-
* @param {string} cwd
|
|
56
|
-
* @param {{ out?: string }} [opts]
|
|
56
|
+
* Compare a built artifact's HTML to the on-disk file.
|
|
57
57
|
* @returns {{ matches: boolean, path: string, exists: boolean, diffSummary: string, nodeCount: number, edgeCount: number, warnings: string[] }}
|
|
58
58
|
*/
|
|
59
|
-
export function
|
|
60
|
-
const
|
|
61
|
-
const outRel = opts.out || built.config.out;
|
|
62
|
-
const outPath = isAbsolute(outRel) ? outRel : join(cwd, outRel);
|
|
59
|
+
export function checkArtifact(cwd, built, opts = {}) {
|
|
60
|
+
const outPath = resolveOutPath(cwd, built, opts);
|
|
63
61
|
const exists = existsSync(outPath);
|
|
64
62
|
const onDisk = exists ? readFileSync(outPath, 'utf-8') : null;
|
|
65
63
|
const matches = exists && onDisk === built.html;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vision-adapter.js — COMP-ROADMAP-GRAPH-2.
|
|
3
|
+
*
|
|
4
|
+
* Converts a VisionStore's items + connections into the
|
|
5
|
+
* { nodes, rawEdges, knownCodes, warnings } shape that buildGraph() consumes
|
|
6
|
+
* (the same shape collect.js produces from feature.json/deps.yaml). This is the
|
|
7
|
+
* single seam that lets the cockpit/vision model drive the one canonical
|
|
8
|
+
* renderer (buildGraph -> renderGraphHtml).
|
|
9
|
+
*
|
|
10
|
+
* The adapter is source-agnostic: it is fed the LIVE in-memory store (cockpit
|
|
11
|
+
* export routes) or a freshly-seeded throwaway store (canonical headless
|
|
12
|
+
* generation). Both go through this one function.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Vision connection vocab -> graph edge vocab. Mirrors the dependency/parallel
|
|
16
|
+
// split the static path encodes in deps.yaml (depends_on/blocks -> dep,
|
|
17
|
+
// concurrent_with -> concurrent).
|
|
18
|
+
const VISION_EDGE_TYPE_MAP = {
|
|
19
|
+
blocks: 'dep',
|
|
20
|
+
informs: 'dep',
|
|
21
|
+
implements: 'dep',
|
|
22
|
+
supports: 'concurrent',
|
|
23
|
+
contradicts: 'concurrent',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Vision status (lowercase, 9 values) -> buildGraph input status (UPPERCASE).
|
|
27
|
+
// COMPLETE/KILLED/SUPERSEDED are DROP_STATUSES in model.js and get dropped there.
|
|
28
|
+
const VISION_STATUS_MAP = {
|
|
29
|
+
planned: 'PLANNED',
|
|
30
|
+
ready: 'PLANNED',
|
|
31
|
+
in_progress: 'IN_PROGRESS',
|
|
32
|
+
review: 'IN_PROGRESS',
|
|
33
|
+
complete: 'COMPLETE',
|
|
34
|
+
blocked: 'BLOCKED',
|
|
35
|
+
parked: 'PARKED',
|
|
36
|
+
killed: 'KILLED',
|
|
37
|
+
superseded: 'SUPERSEDED',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function toUpperStatus(status) {
|
|
41
|
+
return VISION_STATUS_MAP[status] || String(status || '').toUpperCase() || 'PLANNED';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Remove `Track:` / `Priority:` annotation lines from a description so the
|
|
46
|
+
* graph `desc` is clean prose. The static path reads these from structured
|
|
47
|
+
* metadata; the cockpit historically encoded them inline.
|
|
48
|
+
*/
|
|
49
|
+
function stripTrackPriority(description) {
|
|
50
|
+
if (!description) return '';
|
|
51
|
+
return String(description)
|
|
52
|
+
.split('\n')
|
|
53
|
+
.filter((line) => !/^\s*(track|priority)\s*:/i.test(line))
|
|
54
|
+
.join('\n')
|
|
55
|
+
.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {Array} items - vision items (e.g. [...store.items.values()])
|
|
60
|
+
* @param {Array} connections - vision connections (e.g. [...store.connections.values()])
|
|
61
|
+
* @param {{externalPrefixes?: string[], includeProseEdges?: boolean}} [opts]
|
|
62
|
+
* includeProseEdges (default true): the LIVE projection renders every
|
|
63
|
+
* relationship, including prose-derived `informs` edges (shipped cockpit
|
|
64
|
+
* behavior). The CANONICAL projection passes false so only structural
|
|
65
|
+
* (deps.yaml-derived blocks/supports/...) edges remain, matching the static
|
|
66
|
+
* collector which only ever drew deps.yaml edges.
|
|
67
|
+
* @returns {{nodes: object[], rawEdges: object[], knownCodes: Set<string>, warnings: string[]}}
|
|
68
|
+
*/
|
|
69
|
+
export function visionToGraphInputs(items, connections, { externalPrefixes = [], includeProseEdges = true } = {}) {
|
|
70
|
+
const isExternal = (code) => externalPrefixes.some((p) => code && code.startsWith(p));
|
|
71
|
+
const warnings = [];
|
|
72
|
+
const nodes = [];
|
|
73
|
+
const knownCodes = new Set();
|
|
74
|
+
const idToCode = new Map();
|
|
75
|
+
|
|
76
|
+
for (const it of items || []) {
|
|
77
|
+
if (it.type !== 'feature') continue;
|
|
78
|
+
// The live store carries the canonical code in lifecycle.featureCode
|
|
79
|
+
// (set via updateLifecycle); the top-level featureCode and title are
|
|
80
|
+
// fallbacks. Mirror the precedence the export route used historically.
|
|
81
|
+
const code = it.lifecycle?.featureCode || it.featureCode || it.title;
|
|
82
|
+
if (!code) continue;
|
|
83
|
+
idToCode.set(it.id, code);
|
|
84
|
+
knownCodes.add(code);
|
|
85
|
+
// External-prefixed codes are known (so edges to them don't dangle) but are
|
|
86
|
+
// not rendered as nodes — buildGraph silently drops their edges.
|
|
87
|
+
if (isExternal(code)) continue;
|
|
88
|
+
nodes.push({
|
|
89
|
+
id: code,
|
|
90
|
+
status: toUpperStatus(it.status),
|
|
91
|
+
name: it.title || code,
|
|
92
|
+
priority: it.priority || 'medium',
|
|
93
|
+
track: it.group || 'standalone',
|
|
94
|
+
desc: stripTrackPriority(it.description),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const rawEdges = [];
|
|
99
|
+
for (const c of connections || []) {
|
|
100
|
+
const fromCode = idToCode.get(c.fromId);
|
|
101
|
+
const toCode = idToCode.get(c.toId);
|
|
102
|
+
if (!fromCode || !toCode) {
|
|
103
|
+
const bad = !fromCode ? c.fromId : c.toId;
|
|
104
|
+
warnings.push(`connection ${c.id}: endpoint ${bad} is not a feature node (unresolved) — dropped`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// `informs` is the prose-derived relationship type. The canonical
|
|
108
|
+
// projection excludes it so it matches the deps.yaml-only static collector.
|
|
109
|
+
if (!includeProseEdges && c.type === 'informs') continue;
|
|
110
|
+
const type = VISION_EDGE_TYPE_MAP[c.type];
|
|
111
|
+
if (!type) {
|
|
112
|
+
warnings.push(`connection ${c.id}: unmapped connection type "${c.type}" — dropped`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
rawEdges.push({ from: fromCode, to: toCode, type });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Deterministic node order (independent of readdirSync / store insertion order)
|
|
119
|
+
// so the rendered HTML is byte-stable for --check. buildGraph preserves this
|
|
120
|
+
// order; edges are sorted inside buildGraph.
|
|
121
|
+
nodes.sort((a, b) => a.id.localeCompare(b.id));
|
|
122
|
+
|
|
123
|
+
return { nodes, rawEdges, knownCodes, warnings };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { VISION_EDGE_TYPE_MAP, VISION_STATUS_MAP };
|
|
@@ -139,6 +139,15 @@ export class StratumMcpClient {
|
|
|
139
139
|
|
|
140
140
|
const transportOpts = { command, args, stderr: 'pipe' };
|
|
141
141
|
if (opts.cwd) transportOpts.cwd = opts.cwd;
|
|
142
|
+
// Inherit the parent environment. Given no `env`, StdioClientTransport
|
|
143
|
+
// supplies only the MCP SDK's 6-var default allowlist
|
|
144
|
+
// (HOME/LOGNAME/PATH/SHELL/TERM/USER), which strips the auth context the
|
|
145
|
+
// spawned stratum-mcp → claude agent needs. Once claude credentials moved
|
|
146
|
+
// to the macOS keychain, that stripped env stopped authenticating and every
|
|
147
|
+
// agent step failed with `403 forbidden / "Request not allowed"`. Pass the
|
|
148
|
+
// full env through; the stratum connector scrubs SENSITIVE_ENV_VARS
|
|
149
|
+
// (API keys, CLAUDECODE) before it spawns the agent.
|
|
150
|
+
transportOpts.env = opts.env ?? { ...process.env };
|
|
142
151
|
this.#transport = new StdioClientTransport(transportOpts);
|
|
143
152
|
|
|
144
153
|
this.#client = new Client(
|
package/lib/triage.js
CHANGED
|
@@ -232,7 +232,9 @@ export async function runTriage(featureCode, opts = {}) {
|
|
|
232
232
|
*
|
|
233
233
|
* Returns true if:
|
|
234
234
|
* - feature.json has no triageTimestamp
|
|
235
|
-
* - any file in the feature folder has an mtime
|
|
235
|
+
* - any file in the feature folder (EXCEPT feature.json itself) has an mtime
|
|
236
|
+
* newer than triageTimestamp. feature.json is excluded because the stamp it
|
|
237
|
+
* is compared against lives inside it, so its own mtime would self-stale.
|
|
236
238
|
*
|
|
237
239
|
* @param {string} cwd - Project root
|
|
238
240
|
* @param {string} featureCode - Feature code
|
|
@@ -263,6 +265,10 @@ export function isTriageStale(cwd, featureCode, featuresDir = resolveFeaturesPat
|
|
|
263
265
|
const entries = readdirSync(featureDir, { withFileTypes: true });
|
|
264
266
|
for (const entry of entries) {
|
|
265
267
|
if (!entry.isFile()) continue;
|
|
268
|
+
// feature.json holds the triageTimestamp we compare against — counting its
|
|
269
|
+
// own mtime is circular: writing the stamp would self-invalidate the cache
|
|
270
|
+
// (COMP-ROADMAP-PLAN C14). Exclude it from the staleness scan.
|
|
271
|
+
if (entry.name === 'feature.json') continue;
|
|
266
272
|
const filePath = join(featureDir, entry.name);
|
|
267
273
|
try {
|
|
268
274
|
const stat = statSync(filePath);
|