@smartmemory/compose 0.1.28-beta → 0.1.29-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/bin/compose.js +27 -0
- package/contracts/task-result.json +60 -0
- package/contracts/taskgraph-gsd.json +94 -0
- package/dist/assets/{App-Dj7XWWxC.js → App-DyRUFvbx.js} +67 -67
- package/dist/assets/{_baseUniq-tNOA7dYy.js → _baseUniq-6fxo8lI4.js} +1 -1
- package/dist/assets/{arc-BAmAJ19S.js → arc-ElMd2B94.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BPWGVKHW.js → architectureDiagram-Q4EWVU46-CYkxf4MU.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-CVlFbWKF.js → blockDiagram-DXYQGD6D-BEbnyY6z.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-CqzLpnSp.js → c4Diagram-AHTNJAMY-Bo5yMBT9.js} +1 -1
- package/dist/assets/channel-BZuHuJYj.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-D27n9FGy.js → chunk-4BX2VUAB-BETV09jq.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-BNXf8s1x.js → chunk-4TB4RGXK-Dbuohq0Y.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-kGd4Gwx6.js → chunk-55IACEB6-CDVuU3Ew.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-Ci9gWeIv.js → chunk-EDXVE4YY-DjkbltSG.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-B-C0Qn-h.js → chunk-FMBD7UC4-Dz2qczuu.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-CosYsxuv.js → chunk-OYMX7WX6-XJH4cbpv.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-DV2v0Qii.js → chunk-QZHKN3VN-DPhBd22Q.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-BlgKQRCn.js → chunk-YZCP3GAM-C-DPngtC.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DTItsUL2.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DTItsUL2.js +1 -0
- package/dist/assets/clone-BM9Hd7mq.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-CSkzhGHO.js → cose-bilkent-S5V4N54A-CN-sdzRq.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-zp76534d.js → dagre-KV5264BT-HcBmaeC1.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-CAsORZBT.js → diagram-5BDNPKRD-DeMZN1_c.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Da2z6fvR.js → diagram-G4DWMVQ6-DAVSapDS.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-R9NZEWPF.js → diagram-MMDJMWI5-WiLMNWTz.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-DXabRna8.js → diagram-TYMM5635-CjfaWKT0.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-B1zsRPqn.js → erDiagram-SMLLAGMA-DUT_laNT.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-AvlZ6pTE.js → flowDiagram-DWJPFMVM-J9uyMWkp.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-Bnj-jTcM.js → ganttDiagram-T4ZO3ILL-Cee_YjtS.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-82ysfuqG.js → gitGraphDiagram-UUTBAWPF-B1jNNoiW.js} +1 -1
- package/dist/assets/{graph-6nRhlKgL.js → graph-DV5DY72d.js} +1 -1
- package/dist/assets/{index-CF7jc-By.js → index-D5Mh04yh.js} +2 -2
- package/dist/assets/{infoDiagram-42DDH7IO-DSGEEGYr.js → infoDiagram-42DDH7IO-m1jIMAlx.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-COnZHJuM.js → ishikawaDiagram-UXIWVN3A-BC7DwQNb.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-Bsssj2jr.js → journeyDiagram-VCZTEJTY-BOCgv7m4.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-1o5Em0Ia.js → kanban-definition-6JOO6SKY-Fu0eFxr1.js} +1 -1
- package/dist/assets/{layout-C6Mitjz_.js → layout-C-R3-tDf.js} +1 -1
- package/dist/assets/{linear-DoGSpDWQ.js → linear-NOMW_E2I.js} +1 -1
- package/dist/assets/{min-BqH4I4oK.js → min-B7SuZW29.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-79V6zmXV.js → mindmap-definition-QFDTVHPH-BPwx_SxA.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-nXz4HiT6.js → pieDiagram-DEJITSTG-DdBjaXTu.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-BuwKLcii.js → quadrantDiagram-34T5L4WZ-D_K_ZMnx.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-9YEiDjlT.js → requirementDiagram-MS252O5E-XlROD_VH.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-gX8lhWn5.js → sankeyDiagram-XADWPNL6-Sl0FllYQ.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-CkssrD67.js → sequenceDiagram-FGHM5R23-D22asQ-_.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DPmEy2eV.js → stateDiagram-FHFEXIEX-BnCepkBg.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DD_YjrTQ.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-DGpUOjs3.js → timeline-definition-GMOUNBTQ-gdlx6TEB.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-CIvkd661.js → vennDiagram-DHZGUBPP-wy4YzhrG.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-BqRTpa3K.js → wardley-RL74JXVD-D0T_uQ79.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-B93hOd7R.js → wardleyDiagram-NUSXRM2D-Ds2dLjs8.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-C6hwimqo.js → xychartDiagram-5P7HB3ND-CLgTTXkj.js} +1 -1
- package/dist/index.html +1 -1
- package/lib/gsd-blackboard.js +135 -0
- package/lib/gsd-decompose-enrich.js +171 -0
- package/lib/gsd-prompt.js +82 -0
- package/lib/gsd.js +364 -0
- package/package.json +1 -1
- package/pipelines/gsd.stratum.yaml +141 -0
- package/dist/assets/channel-Ddcaj0fR.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DfGxrNIN.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-DfGxrNIN.js +0 -1
- package/dist/assets/clone-DVujR_lO.js +0 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BGZzYkLq.js +0 -1
package/lib/gsd.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// lib/gsd.js
|
|
2
|
+
//
|
|
3
|
+
// COMP-GSD-2 T6: runGsd lifecycle entry — `compose gsd <featureCode>`.
|
|
4
|
+
//
|
|
5
|
+
// Self-contained status loop. Does NOT modify lib/build.js. Reuses primitives:
|
|
6
|
+
// - StratumMcpClient (lib/stratum-mcp-client.js) for plan/stepDone/runAgentText
|
|
7
|
+
// - executeParallelDispatchServer (lib/build.js) for the execute step
|
|
8
|
+
// - validateBoundaryMap (lib/boundary-map.js) for precondition check
|
|
9
|
+
// - enrichTaskGraph (lib/gsd-decompose-enrich.js) for decompose validation
|
|
10
|
+
// - buildTaskDescription (lib/gsd-prompt.js) for description repair fallback
|
|
11
|
+
// - gsd-blackboard.writeAll for post-step finalization
|
|
12
|
+
//
|
|
13
|
+
// V1 limitation: runtime task-to-task handoff is not implemented; tasks see
|
|
14
|
+
// only spec-level upstream context (Boundary Map declarations) per blueprint.
|
|
15
|
+
|
|
16
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
17
|
+
import { join, resolve, dirname } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
20
|
+
|
|
21
|
+
import { StratumMcpClient } from './stratum-mcp-client.js';
|
|
22
|
+
import { validateBoundaryMap } from './boundary-map.js';
|
|
23
|
+
import { enrichTaskGraph } from './gsd-decompose-enrich.js';
|
|
24
|
+
import { buildTaskDescription } from './gsd-prompt.js';
|
|
25
|
+
import { writeAll, validate as validateTaskResult } from './gsd-blackboard.js';
|
|
26
|
+
import { executeParallelDispatchServer, executeShipStep } from './build.js';
|
|
27
|
+
|
|
28
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const PACKAGE_ROOT = resolve(__dirname, '..');
|
|
30
|
+
|
|
31
|
+
const DEFAULT_GATE_COMMANDS = ['pnpm lint', 'pnpm build', 'pnpm test'];
|
|
32
|
+
|
|
33
|
+
// ---------- Public API ----------
|
|
34
|
+
|
|
35
|
+
export async function runGsd(featureCode, opts = {}) {
|
|
36
|
+
if (!featureCode || typeof featureCode !== 'string') {
|
|
37
|
+
throw new Error('runGsd: featureCode required');
|
|
38
|
+
}
|
|
39
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
40
|
+
|
|
41
|
+
// 1. Validate preconditions: blueprint exists + Boundary Map ok
|
|
42
|
+
const blueprintPath = join(cwd, 'docs', 'features', featureCode, 'blueprint.md');
|
|
43
|
+
if (!existsSync(blueprintPath)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`runGsd: blueprint missing at ${blueprintPath}. ` +
|
|
46
|
+
`Run \`compose build ${featureCode}\` to generate it, or author it by hand.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const blueprintText = readFileSync(blueprintPath, 'utf-8');
|
|
50
|
+
const bmResult = validateBoundaryMap({
|
|
51
|
+
blueprintText,
|
|
52
|
+
blueprintPath,
|
|
53
|
+
repoRoot: cwd,
|
|
54
|
+
});
|
|
55
|
+
if (!bmResult.ok) {
|
|
56
|
+
const summary = bmResult.violations
|
|
57
|
+
.slice(0, 5)
|
|
58
|
+
.map((v) => `${v.kind}: ${v.message}`)
|
|
59
|
+
.join('\n - ');
|
|
60
|
+
throw new Error(
|
|
61
|
+
`runGsd: Boundary Map invalid in ${blueprintPath}:\n - ${summary}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. Refuse to start in a dirty workspace BEFORE any Stratum side effects.
|
|
66
|
+
// v1 rationale: alternatives (baseline subtract + post-execute delta) drop
|
|
67
|
+
// legitimate edits to pre-existing dirty files. Refuse-if-dirty makes
|
|
68
|
+
// post-execute dirty set unambiguous: every entry is GSD-produced.
|
|
69
|
+
if (!opts.allowDirtyWorkspace) {
|
|
70
|
+
const startingDirty = collectChangedFiles(cwd);
|
|
71
|
+
if (startingDirty.length > 0) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`runGsd: working tree must be clean to ensure ship_gsd stages only GSD-produced changes. ` +
|
|
74
|
+
`Dirty files: ${startingDirty.slice(0, 5).join(', ')}${startingDirty.length > 5 ? `, +${startingDirty.length - 5} more` : ''}. ` +
|
|
75
|
+
`Commit or stash and re-run, or pass {allowDirtyWorkspace: true} (advanced; risks staging unrelated edits).`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. Resolve gateCommands. loadProjectConfig() does not merge defaults, so
|
|
81
|
+
// explicit fallback here.
|
|
82
|
+
const gateCommands = resolveGateCommands(cwd, opts.gateCommands);
|
|
83
|
+
|
|
84
|
+
// 4. Load pipeline spec
|
|
85
|
+
const specPath = join(PACKAGE_ROOT, 'pipelines', 'gsd.stratum.yaml');
|
|
86
|
+
const specYaml = readFileSync(specPath, 'utf-8');
|
|
87
|
+
|
|
88
|
+
// 5. Connect Stratum + plan (only after preconditions pass)
|
|
89
|
+
const stratum = opts.stratum ?? new StratumMcpClient();
|
|
90
|
+
const ownsStratum = !opts.stratum;
|
|
91
|
+
if (ownsStratum) await stratum.connect();
|
|
92
|
+
try {
|
|
93
|
+
let response = await stratum.plan(specYaml, 'gsd', {
|
|
94
|
+
featureCode,
|
|
95
|
+
gateCommands,
|
|
96
|
+
});
|
|
97
|
+
const flowId = response.flow_id;
|
|
98
|
+
|
|
99
|
+
// Track files merged into the base cwd by the execute step so ship_gsd
|
|
100
|
+
// can stage them. executeShipStep's default filter only stages feature
|
|
101
|
+
// docs unless context.filesChanged is provided.
|
|
102
|
+
const stepCtx = {
|
|
103
|
+
stratum, cwd, featureCode, blueprintText, gateCommands,
|
|
104
|
+
filesChanged: [],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// 5. Status loop
|
|
108
|
+
while (response.status !== 'complete' && response.status !== 'killed') {
|
|
109
|
+
response = await runOneStep(response, stepCtx);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 6. Post-step blackboard finalization — read each task's TaskResult JSON
|
|
113
|
+
// and write the consolidated blackboard.
|
|
114
|
+
const blackboard = collectBlackboard(cwd, featureCode);
|
|
115
|
+
if (Object.keys(blackboard).length > 0) {
|
|
116
|
+
await writeAll(featureCode, blackboard, { cwd });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
status: response.status,
|
|
121
|
+
flowId,
|
|
122
|
+
blackboardEntries: Object.keys(blackboard).length,
|
|
123
|
+
};
|
|
124
|
+
} finally {
|
|
125
|
+
if (ownsStratum) {
|
|
126
|
+
try { await stratum.disconnect?.(); } catch { /* best-effort */ }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------- Internals ----------
|
|
132
|
+
|
|
133
|
+
export function resolveGateCommands(cwd, override) {
|
|
134
|
+
if (Array.isArray(override) && override.length > 0) return override;
|
|
135
|
+
// loadProjectConfig() returns raw .compose/compose.json — does NOT merge
|
|
136
|
+
// defaults — so we must do our own fallback.
|
|
137
|
+
const configPath = join(cwd, '.compose', 'compose.json');
|
|
138
|
+
if (existsSync(configPath)) {
|
|
139
|
+
try {
|
|
140
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
141
|
+
if (Array.isArray(cfg.gateCommands) && cfg.gateCommands.length > 0) {
|
|
142
|
+
return cfg.gateCommands;
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
/* fall through to default */
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return [...DEFAULT_GATE_COMMANDS];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function runOneStep(response, ctx) {
|
|
152
|
+
const { stratum, cwd, featureCode, blueprintText, gateCommands } = ctx;
|
|
153
|
+
const flowId = response.flow_id;
|
|
154
|
+
const stepId = response.step_id;
|
|
155
|
+
const stepType = response.type ?? response.step_type;
|
|
156
|
+
|
|
157
|
+
if (response.status === 'execute_step') {
|
|
158
|
+
// parallel_dispatch step (the `execute` step)
|
|
159
|
+
if (stepType === 'parallel_dispatch' || response.tasks) {
|
|
160
|
+
const outcome = await executeParallelDispatchServer(
|
|
161
|
+
response,
|
|
162
|
+
stratum,
|
|
163
|
+
{ cwd, featureCode },
|
|
164
|
+
null, // progress
|
|
165
|
+
{ write: () => {} }, // streamWriter — no-op for v1
|
|
166
|
+
cwd,
|
|
167
|
+
);
|
|
168
|
+
// After diffs are merged, capture the touched files for ship_gsd
|
|
169
|
+
// staging. The clean-workspace precondition above guarantees every
|
|
170
|
+
// file in the post-execute dirty set is genuinely a GSD-produced change.
|
|
171
|
+
ctx.filesChanged = collectChangedFiles(cwd);
|
|
172
|
+
// executeParallelDispatchServer returns the next-step dispatch envelope
|
|
173
|
+
return outcome;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ship_gsd: delegate to executeShipStep with filesChanged from execute step
|
|
177
|
+
// so source files are staged. Agent sandbox blocks git, so commit must
|
|
178
|
+
// run in-process (mirrors runBuild's special case at lib/build.js:963-981).
|
|
179
|
+
if (stepId === 'ship_gsd') {
|
|
180
|
+
const shipResult = await executeShipStep(
|
|
181
|
+
featureCode,
|
|
182
|
+
cwd,
|
|
183
|
+
cwd,
|
|
184
|
+
{ cwd, featureCode, mode: 'feature', filesChanged: ctx.filesChanged ?? [] },
|
|
185
|
+
'',
|
|
186
|
+
null,
|
|
187
|
+
);
|
|
188
|
+
// executeShipStep stages + commits but does NOT push. Push is a
|
|
189
|
+
// user-facing operation deferred to the user in v1; runBuild's ship
|
|
190
|
+
// step doesn't auto-push either. Document via ship intent later.
|
|
191
|
+
return await stratum.stepDone(flowId, stepId, shipResult);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Single-agent step: dispatch via runAgentText. The agent returns text;
|
|
195
|
+
// we expect JSON matching the step's output_contract.
|
|
196
|
+
const prompt = response.intent ?? '';
|
|
197
|
+
const text = await stratum.runAgentText(response.agent ?? 'claude', prompt, { cwd });
|
|
198
|
+
let result;
|
|
199
|
+
try {
|
|
200
|
+
result = parseJsonFromText(text);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`runGsd: step ${stepId} agent did not return parseable JSON: ${err.message}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// T6 step 7: validate decompose_gsd output and repair missing descriptions.
|
|
208
|
+
if (stepId === 'decompose_gsd') {
|
|
209
|
+
result = validateAndRepairTaskGraph(result, blueprintText, gateCommands);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return await stratum.stepDone(flowId, stepId, result);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (response.status === 'await_gate') {
|
|
216
|
+
// GSD has no gates in v1. If we hit one, surface it.
|
|
217
|
+
throw new Error(
|
|
218
|
+
`runGsd: unexpected gate at step ${stepId}. v1 has no gates in the gsd flow.`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
throw new Error(`runGsd: unknown response status: ${response.status}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function validateAndRepairTaskGraph(taskGraph, blueprintText, gateCommands) {
|
|
226
|
+
// Structural check via enrichTaskGraph. Throws on orphan slice/task —
|
|
227
|
+
// that's a "fail loudly" case (no reliable repair path).
|
|
228
|
+
const enriched = enrichTaskGraph(taskGraph, blueprintText);
|
|
229
|
+
|
|
230
|
+
// Per-task description check. The agent must produce a description with
|
|
231
|
+
// all six required sections (per T4 prompt contract). If ANY section
|
|
232
|
+
// marker is missing, repair via buildTaskDescription. Length-only would
|
|
233
|
+
// miss long-but-malformed strings.
|
|
234
|
+
const enrichedById = new Map(enriched.tasks.map((t) => [t.id, t]));
|
|
235
|
+
const repairedTasks = enriched.tasks.map((task) => {
|
|
236
|
+
if (typeof task.description === 'string' && hasAllRequiredSections(task.description)) {
|
|
237
|
+
return task;
|
|
238
|
+
}
|
|
239
|
+
// Repair: synthesize a fresh description.
|
|
240
|
+
const sliceText = extractSliceTextForTask(blueprintText, task);
|
|
241
|
+
const upstream = (task.depends_on || [])
|
|
242
|
+
.map((dep) => enrichedById.get(dep))
|
|
243
|
+
.filter(Boolean);
|
|
244
|
+
const fresh = buildTaskDescription({
|
|
245
|
+
task,
|
|
246
|
+
slice: sliceText,
|
|
247
|
+
upstreamTasks: upstream,
|
|
248
|
+
gateCommands,
|
|
249
|
+
});
|
|
250
|
+
return { ...task, description: fresh };
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return { tasks: repairedTasks };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const REQUIRED_DESCRIPTION_SECTIONS = [
|
|
257
|
+
'Symbols you must produce',
|
|
258
|
+
'Symbols you may consume from upstream tasks',
|
|
259
|
+
'Boundary Map slice',
|
|
260
|
+
'Upstream tasks',
|
|
261
|
+
'GATES',
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
function hasAllRequiredSections(description) {
|
|
265
|
+
for (const marker of REQUIRED_DESCRIPTION_SECTIONS) {
|
|
266
|
+
if (!description.includes(marker)) return false;
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function extractSliceTextForTask(blueprintText, task) {
|
|
272
|
+
// Find any Boundary Map slice whose File Plan files match the task's
|
|
273
|
+
// files_owned. We don't have a sliceId here, so we scan slice blocks for
|
|
274
|
+
// the first one whose File Plan ⊆ task.files_owned. Best-effort — only
|
|
275
|
+
// used in the description-repair path.
|
|
276
|
+
const lines = blueprintText.split(/\r?\n/);
|
|
277
|
+
const owned = new Set(task.files_owned || []);
|
|
278
|
+
const blocks = [];
|
|
279
|
+
let cur = null;
|
|
280
|
+
for (let i = 0; i < lines.length; i++) {
|
|
281
|
+
const m = lines[i].match(/^### (S\d{2,})/);
|
|
282
|
+
if (m) {
|
|
283
|
+
if (cur) blocks.push(cur);
|
|
284
|
+
cur = { id: m[1], start: i, end: lines.length };
|
|
285
|
+
} else if (cur && /^### S\d/.test(lines[i])) {
|
|
286
|
+
cur.end = i;
|
|
287
|
+
blocks.push(cur);
|
|
288
|
+
cur = null;
|
|
289
|
+
} else if (cur && /^## /.test(lines[i])) {
|
|
290
|
+
cur.end = i;
|
|
291
|
+
blocks.push(cur);
|
|
292
|
+
cur = null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (cur) blocks.push(cur);
|
|
296
|
+
for (const b of blocks) {
|
|
297
|
+
const block = lines.slice(b.start, b.end).join('\n');
|
|
298
|
+
const fpMatch = block.match(/^File Plan\s*:\s*(.+)$/m);
|
|
299
|
+
if (!fpMatch) continue;
|
|
300
|
+
const files = [...fpMatch[1].matchAll(/`([^`]+)`/g)].map((mm) => mm[1].trim());
|
|
301
|
+
if (files.length > 0 && files.every((f) => owned.has(f))) {
|
|
302
|
+
return block;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return '';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function parseJsonFromText(text) {
|
|
309
|
+
// Strip code fences if present.
|
|
310
|
+
const trimmed = text.trim();
|
|
311
|
+
const fenced = trimmed.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
312
|
+
const body = fenced ? fenced[1] : trimmed;
|
|
313
|
+
return JSON.parse(body);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function collectChangedFiles(cwd) {
|
|
317
|
+
try {
|
|
318
|
+
const tracked = execSync('git diff --name-only HEAD', {
|
|
319
|
+
cwd, encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
320
|
+
}).trim();
|
|
321
|
+
const untracked = execSync('git ls-files --others --exclude-standard', {
|
|
322
|
+
cwd, encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
323
|
+
}).trim();
|
|
324
|
+
const all = [
|
|
325
|
+
...tracked.split('\n').filter(Boolean),
|
|
326
|
+
...untracked.split('\n').filter(Boolean),
|
|
327
|
+
];
|
|
328
|
+
return [...new Set(all)];
|
|
329
|
+
} catch {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function collectBlackboard(cwd, featureCode) {
|
|
335
|
+
const dir = join(cwd, '.compose', 'gsd', featureCode, 'results');
|
|
336
|
+
if (!existsSync(dir)) return {};
|
|
337
|
+
const files = readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
338
|
+
const out = {};
|
|
339
|
+
const failures = [];
|
|
340
|
+
for (const f of files) {
|
|
341
|
+
const taskId = f.replace(/\.json$/, '');
|
|
342
|
+
let parsed;
|
|
343
|
+
try {
|
|
344
|
+
parsed = JSON.parse(readFileSync(join(dir, f), 'utf-8'));
|
|
345
|
+
} catch (err) {
|
|
346
|
+
failures.push(`${f}: unreadable JSON (${err.message})`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const v = validateTaskResult(parsed);
|
|
350
|
+
if (v.ok) {
|
|
351
|
+
out[taskId] = parsed;
|
|
352
|
+
} else {
|
|
353
|
+
failures.push(`${f}: ${v.errors.join('; ')}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (failures.length > 0) {
|
|
357
|
+
// Plan T6 acceptance: blackboard must contain one VALIDATED entry per task.
|
|
358
|
+
// A partial blackboard is worse than no blackboard — fail loudly.
|
|
359
|
+
throw new Error(
|
|
360
|
+
`runGsd: ${failures.length} TaskResult file(s) failed validation; refusing to write partial blackboard:\n - ${failures.join('\n - ')}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/compose",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29-beta",
|
|
4
4
|
"description": "Structured AI dev pipeline — goal-to-product orchestration with gates, iteration loops, and feature lifecycle management.",
|
|
5
5
|
"author": "SmartMemory",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# metadata:
|
|
2
|
+
# id: gsd
|
|
3
|
+
# label: "GSD — Per-Task Fresh-Context Dispatch"
|
|
4
|
+
# description: "Long-run autonomous mode. Decompose blueprint+Boundary Map into tasks, dispatch each as a fresh-context worktree-isolated agent, sequential by default. Reads existing blueprint.md."
|
|
5
|
+
# category: development
|
|
6
|
+
# steps: 3
|
|
7
|
+
# estimated_minutes: 60
|
|
8
|
+
|
|
9
|
+
version: "0.3"
|
|
10
|
+
|
|
11
|
+
workflow:
|
|
12
|
+
name: gsd
|
|
13
|
+
description: "Per-task fresh-context dispatch from existing blueprint+Boundary Map (COMP-GSD-2)"
|
|
14
|
+
input:
|
|
15
|
+
featureCode:
|
|
16
|
+
type: string
|
|
17
|
+
required: true
|
|
18
|
+
gateCommands:
|
|
19
|
+
type: array
|
|
20
|
+
required: true # injected by runGsd from loadProjectConfig() with default fallback
|
|
21
|
+
|
|
22
|
+
contracts:
|
|
23
|
+
PhaseResult:
|
|
24
|
+
phase: {type: string}
|
|
25
|
+
artifact: {type: string}
|
|
26
|
+
outcome: {type: string, values: [complete, skipped, failed]}
|
|
27
|
+
summary: {type: string}
|
|
28
|
+
|
|
29
|
+
# TaskGraph is a v0.3 built-in contract — must NOT be redeclared.
|
|
30
|
+
# The richer TaskGraphGsd shape (per-task produces/consumes) is enforced
|
|
31
|
+
# post-decompose by lib/gsd-decompose-enrich.js + contracts/taskgraph-gsd.json
|
|
32
|
+
# rather than via Stratum's contract layer.
|
|
33
|
+
|
|
34
|
+
flows:
|
|
35
|
+
gsd:
|
|
36
|
+
input:
|
|
37
|
+
featureCode: {type: string}
|
|
38
|
+
gateCommands: {type: array}
|
|
39
|
+
output: PhaseResult
|
|
40
|
+
max_rounds: 10
|
|
41
|
+
steps:
|
|
42
|
+
# Phase: Decompose blueprint + Boundary Map into TaskGraphGsd
|
|
43
|
+
- id: decompose_gsd
|
|
44
|
+
type: decompose
|
|
45
|
+
agent: claude
|
|
46
|
+
intent: >
|
|
47
|
+
Read docs/features/{input.featureCode}/blueprint.md including its
|
|
48
|
+
## Boundary Map section. Decompose into independent tasks.
|
|
49
|
+
|
|
50
|
+
For each task emit: id, files_owned, files_read, depends_on, AND a
|
|
51
|
+
rich `description` string that bundles all the spec context Stratum's
|
|
52
|
+
parallel_dispatch interpolation cannot carry on its own.
|
|
53
|
+
|
|
54
|
+
Each task's `description` MUST include these labeled sections:
|
|
55
|
+
- "Symbols you must produce:" + the matching Boundary Map slice's
|
|
56
|
+
produces entries (file → symbols (kind))
|
|
57
|
+
- "Symbols you may consume from upstream tasks:" + the slice's
|
|
58
|
+
consumes entries
|
|
59
|
+
- "Boundary Map slice (the contract for this task):" + the verbatim
|
|
60
|
+
slice block from blueprint.md
|
|
61
|
+
- "Upstream tasks (spec-level summary; their code lands at end-of-step
|
|
62
|
+
merge):" + one line per upstream task listing its declared produces
|
|
63
|
+
- "GATES — you MUST run each command and they MUST pass before
|
|
64
|
+
declaring done:" + each command from the gateCommands array
|
|
65
|
+
- "Fix and re-run within this invocation. Do NOT declare done while
|
|
66
|
+
gates are red."
|
|
67
|
+
|
|
68
|
+
Stratum's parallel_dispatch only interpolates {task.id},
|
|
69
|
+
{task.description}, {task.files_owned}, {task.files_read},
|
|
70
|
+
{task.depends_on}, {task.index}, {input.<field>}. Pack everything else
|
|
71
|
+
into description.
|
|
72
|
+
|
|
73
|
+
Each Boundary Map slice maps to exactly one task whose files_owned ⊇
|
|
74
|
+
the slice's File Plan files. The decomposition is invalid if a slice
|
|
75
|
+
has no matching task or its File Plan files are split across
|
|
76
|
+
multiple tasks.
|
|
77
|
+
|
|
78
|
+
gateCommands to embed: {input.gateCommands}
|
|
79
|
+
inputs:
|
|
80
|
+
featureCode: "$.input.featureCode"
|
|
81
|
+
gateCommands: "$.input.gateCommands"
|
|
82
|
+
output_contract: TaskGraph
|
|
83
|
+
ensure:
|
|
84
|
+
- "no_file_conflicts(result.tasks)"
|
|
85
|
+
- "len(result.tasks) >= 1"
|
|
86
|
+
retries: 2
|
|
87
|
+
|
|
88
|
+
# Phase: Execute — sequential per-task fresh-context dispatch
|
|
89
|
+
- id: execute
|
|
90
|
+
type: parallel_dispatch
|
|
91
|
+
source: "$.steps.decompose_gsd.output.tasks"
|
|
92
|
+
agent: claude
|
|
93
|
+
max_concurrent: 1
|
|
94
|
+
isolation: worktree
|
|
95
|
+
capture_diff: true
|
|
96
|
+
require: all
|
|
97
|
+
merge: sequential_apply
|
|
98
|
+
retries: 2
|
|
99
|
+
intent_template: >
|
|
100
|
+
Implement task {task.id} using TDD.
|
|
101
|
+
|
|
102
|
+
Files you own (write): {task.files_owned}
|
|
103
|
+
Files you may read: {task.files_read}
|
|
104
|
+
Depends on: {task.depends_on}
|
|
105
|
+
|
|
106
|
+
{task.description}
|
|
107
|
+
|
|
108
|
+
When done, write TaskResult JSON to
|
|
109
|
+
.compose/gsd/{input.featureCode}/results/{task.id}.json
|
|
110
|
+
(schema: contracts/task-result.json) and INCLUDE that file in your diff.
|
|
111
|
+
depends_on: [decompose_gsd]
|
|
112
|
+
|
|
113
|
+
# Phase: Ship — verify, update docs, commit, push
|
|
114
|
+
- id: ship_gsd
|
|
115
|
+
agent: "claude::critical"
|
|
116
|
+
intent: >
|
|
117
|
+
Final verification and ship for feature {input.featureCode}.
|
|
118
|
+
|
|
119
|
+
You MUST:
|
|
120
|
+
1. Run the project test suite. If tests fail, fix them.
|
|
121
|
+
2. Run the project build. Must pass.
|
|
122
|
+
3. Update ROADMAP.md — find COMP-GSD-2's row and mark it COMPLETE.
|
|
123
|
+
If this completes a parent feature, update the parent header.
|
|
124
|
+
4. Update CHANGELOG.md — add an entry under today's date with
|
|
125
|
+
COMP-GSD-2 and a bullet list of what was built. Read the
|
|
126
|
+
existing format first.
|
|
127
|
+
5. Update CLAUDE.md ONLY if a new convention/command/architecture
|
|
128
|
+
surface emerged. Skip otherwise.
|
|
129
|
+
6. Stage changed files explicitly (no worktrees, pycache, build
|
|
130
|
+
artifacts). Commit with message:
|
|
131
|
+
"feat(COMP-GSD-2): per-task fresh-context dispatch"
|
|
132
|
+
(Note: push is deferred to the user — `compose gsd` does not
|
|
133
|
+
auto-push, mirroring `compose build`'s ship behavior.)
|
|
134
|
+
7. Report commit hash and files_changed in result JSON.
|
|
135
|
+
inputs:
|
|
136
|
+
featureCode: "$.input.featureCode"
|
|
137
|
+
output_contract: PhaseResult
|
|
138
|
+
ensure:
|
|
139
|
+
- "result.outcome == 'complete'"
|
|
140
|
+
retries: 2
|
|
141
|
+
depends_on: [execute]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{aq as o,ar as n}from"./App-Dj7XWWxC.js";const t=(r,a)=>o.lang.round(n.parse(r)[a]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-4TB4RGXK-BNXf8s1x.js";import{_ as i}from"./App-Dj7XWWxC.js";import"./chunk-FMBD7UC4-B-C0Qn-h.js";import"./chunk-YZCP3GAM-BlgKQRCn.js";import"./chunk-55IACEB6-kGd4Gwx6.js";import"./chunk-EDXVE4YY-Ci9gWeIv.js";import"./mobile-BOyZ87uL.js";import"./index-CF7jc-By.js";import"./graph-CfEl_ohV.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-4TB4RGXK-BNXf8s1x.js";import{_ as i}from"./App-Dj7XWWxC.js";import"./chunk-FMBD7UC4-B-C0Qn-h.js";import"./chunk-YZCP3GAM-BlgKQRCn.js";import"./chunk-55IACEB6-kGd4Gwx6.js";import"./chunk-EDXVE4YY-Ci9gWeIv.js";import"./mobile-BOyZ87uL.js";import"./index-CF7jc-By.js";import"./graph-CfEl_ohV.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{b as r}from"./graph-6nRhlKgL.js";var e=4;function a(o){return r(o,e)}export{a as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as r,b as e,a,S as s}from"./chunk-OYMX7WX6-CosYsxuv.js";import{_ as i}from"./App-Dj7XWWxC.js";import"./chunk-55IACEB6-kGd4Gwx6.js";import"./chunk-EDXVE4YY-Ci9gWeIv.js";import"./mobile-BOyZ87uL.js";import"./index-CF7jc-By.js";import"./graph-CfEl_ohV.js";var n={parser:a,get db(){return new s(2)},renderer:e,styles:r,init:i(t=>{t.state||(t.state={}),t.state.arrowMarkerAbsolute=t.arrowMarkerAbsolute},"init")};export{n as diagram};
|