@smartmemory/compose 0.1.27-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.
Files changed (69) hide show
  1. package/.claude/skills/compose/SKILL.md +5 -2
  2. package/.claude/skills/compose/templates/boundary-map.md +94 -0
  3. package/bin/compose.js +27 -0
  4. package/contracts/task-result.json +60 -0
  5. package/contracts/taskgraph-gsd.json +94 -0
  6. package/dist/assets/{App-Dj7XWWxC.js → App-DyRUFvbx.js} +67 -67
  7. package/dist/assets/{_baseUniq-tNOA7dYy.js → _baseUniq-6fxo8lI4.js} +1 -1
  8. package/dist/assets/{arc-BAmAJ19S.js → arc-ElMd2B94.js} +1 -1
  9. package/dist/assets/{architectureDiagram-Q4EWVU46-BPWGVKHW.js → architectureDiagram-Q4EWVU46-CYkxf4MU.js} +1 -1
  10. package/dist/assets/{blockDiagram-DXYQGD6D-CVlFbWKF.js → blockDiagram-DXYQGD6D-BEbnyY6z.js} +1 -1
  11. package/dist/assets/{c4Diagram-AHTNJAMY-CqzLpnSp.js → c4Diagram-AHTNJAMY-Bo5yMBT9.js} +1 -1
  12. package/dist/assets/channel-BZuHuJYj.js +1 -0
  13. package/dist/assets/{chunk-4BX2VUAB-D27n9FGy.js → chunk-4BX2VUAB-BETV09jq.js} +1 -1
  14. package/dist/assets/{chunk-4TB4RGXK-BNXf8s1x.js → chunk-4TB4RGXK-Dbuohq0Y.js} +1 -1
  15. package/dist/assets/{chunk-55IACEB6-kGd4Gwx6.js → chunk-55IACEB6-CDVuU3Ew.js} +1 -1
  16. package/dist/assets/{chunk-EDXVE4YY-Ci9gWeIv.js → chunk-EDXVE4YY-DjkbltSG.js} +1 -1
  17. package/dist/assets/{chunk-FMBD7UC4-B-C0Qn-h.js → chunk-FMBD7UC4-Dz2qczuu.js} +1 -1
  18. package/dist/assets/{chunk-OYMX7WX6-CosYsxuv.js → chunk-OYMX7WX6-XJH4cbpv.js} +1 -1
  19. package/dist/assets/{chunk-QZHKN3VN-DV2v0Qii.js → chunk-QZHKN3VN-DPhBd22Q.js} +1 -1
  20. package/dist/assets/{chunk-YZCP3GAM-BlgKQRCn.js → chunk-YZCP3GAM-C-DPngtC.js} +1 -1
  21. package/dist/assets/classDiagram-6PBFFD2Q-DTItsUL2.js +1 -0
  22. package/dist/assets/classDiagram-v2-HSJHXN6E-DTItsUL2.js +1 -0
  23. package/dist/assets/clone-BM9Hd7mq.js +1 -0
  24. package/dist/assets/{cose-bilkent-S5V4N54A-CSkzhGHO.js → cose-bilkent-S5V4N54A-CN-sdzRq.js} +1 -1
  25. package/dist/assets/{dagre-KV5264BT-zp76534d.js → dagre-KV5264BT-HcBmaeC1.js} +1 -1
  26. package/dist/assets/{diagram-5BDNPKRD-CAsORZBT.js → diagram-5BDNPKRD-DeMZN1_c.js} +1 -1
  27. package/dist/assets/{diagram-G4DWMVQ6-Da2z6fvR.js → diagram-G4DWMVQ6-DAVSapDS.js} +1 -1
  28. package/dist/assets/{diagram-MMDJMWI5-R9NZEWPF.js → diagram-MMDJMWI5-WiLMNWTz.js} +1 -1
  29. package/dist/assets/{diagram-TYMM5635-DXabRna8.js → diagram-TYMM5635-CjfaWKT0.js} +1 -1
  30. package/dist/assets/{erDiagram-SMLLAGMA-B1zsRPqn.js → erDiagram-SMLLAGMA-DUT_laNT.js} +1 -1
  31. package/dist/assets/{flowDiagram-DWJPFMVM-AvlZ6pTE.js → flowDiagram-DWJPFMVM-J9uyMWkp.js} +1 -1
  32. package/dist/assets/{ganttDiagram-T4ZO3ILL-Bnj-jTcM.js → ganttDiagram-T4ZO3ILL-Cee_YjtS.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-UUTBAWPF-82ysfuqG.js → gitGraphDiagram-UUTBAWPF-B1jNNoiW.js} +1 -1
  34. package/dist/assets/{graph-6nRhlKgL.js → graph-DV5DY72d.js} +1 -1
  35. package/dist/assets/{index-CF7jc-By.js → index-D5Mh04yh.js} +2 -2
  36. package/dist/assets/{infoDiagram-42DDH7IO-DSGEEGYr.js → infoDiagram-42DDH7IO-m1jIMAlx.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-UXIWVN3A-COnZHJuM.js → ishikawaDiagram-UXIWVN3A-BC7DwQNb.js} +1 -1
  38. package/dist/assets/{journeyDiagram-VCZTEJTY-Bsssj2jr.js → journeyDiagram-VCZTEJTY-BOCgv7m4.js} +1 -1
  39. package/dist/assets/{kanban-definition-6JOO6SKY-1o5Em0Ia.js → kanban-definition-6JOO6SKY-Fu0eFxr1.js} +1 -1
  40. package/dist/assets/{layout-C6Mitjz_.js → layout-C-R3-tDf.js} +1 -1
  41. package/dist/assets/{linear-DoGSpDWQ.js → linear-NOMW_E2I.js} +1 -1
  42. package/dist/assets/{min-BqH4I4oK.js → min-B7SuZW29.js} +1 -1
  43. package/dist/assets/{mindmap-definition-QFDTVHPH-79V6zmXV.js → mindmap-definition-QFDTVHPH-BPwx_SxA.js} +1 -1
  44. package/dist/assets/{pieDiagram-DEJITSTG-nXz4HiT6.js → pieDiagram-DEJITSTG-DdBjaXTu.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-34T5L4WZ-BuwKLcii.js → quadrantDiagram-34T5L4WZ-D_K_ZMnx.js} +1 -1
  46. package/dist/assets/{requirementDiagram-MS252O5E-9YEiDjlT.js → requirementDiagram-MS252O5E-XlROD_VH.js} +1 -1
  47. package/dist/assets/{sankeyDiagram-XADWPNL6-gX8lhWn5.js → sankeyDiagram-XADWPNL6-Sl0FllYQ.js} +1 -1
  48. package/dist/assets/{sequenceDiagram-FGHM5R23-CkssrD67.js → sequenceDiagram-FGHM5R23-D22asQ-_.js} +1 -1
  49. package/dist/assets/{stateDiagram-FHFEXIEX-DPmEy2eV.js → stateDiagram-FHFEXIEX-BnCepkBg.js} +1 -1
  50. package/dist/assets/stateDiagram-v2-QKLJ7IA2-DD_YjrTQ.js +1 -0
  51. package/dist/assets/{timeline-definition-GMOUNBTQ-DGpUOjs3.js → timeline-definition-GMOUNBTQ-gdlx6TEB.js} +1 -1
  52. package/dist/assets/{vennDiagram-DHZGUBPP-CIvkd661.js → vennDiagram-DHZGUBPP-wy4YzhrG.js} +1 -1
  53. package/dist/assets/{wardley-RL74JXVD-BqRTpa3K.js → wardley-RL74JXVD-D0T_uQ79.js} +1 -1
  54. package/dist/assets/{wardleyDiagram-NUSXRM2D-B93hOd7R.js → wardleyDiagram-NUSXRM2D-Ds2dLjs8.js} +1 -1
  55. package/dist/assets/{xychartDiagram-5P7HB3ND-C6hwimqo.js → xychartDiagram-5P7HB3ND-CLgTTXkj.js} +1 -1
  56. package/dist/index.html +1 -1
  57. package/lib/boundary-map.js +481 -0
  58. package/lib/gsd-blackboard.js +135 -0
  59. package/lib/gsd-decompose-enrich.js +171 -0
  60. package/lib/gsd-prompt.js +82 -0
  61. package/lib/gsd.js +364 -0
  62. package/package.json +1 -1
  63. package/pipelines/build.stratum.yaml +7 -2
  64. package/pipelines/gsd.stratum.yaml +141 -0
  65. package/dist/assets/channel-Ddcaj0fR.js +0 -1
  66. package/dist/assets/classDiagram-6PBFFD2Q-DfGxrNIN.js +0 -1
  67. package/dist/assets/classDiagram-v2-HSJHXN6E-DfGxrNIN.js +0 -1
  68. package/dist/assets/clone-DVujR_lO.js +0 -1
  69. package/dist/assets/stateDiagram-v2-QKLJ7IA2-BGZzYkLq.js +0 -1
@@ -0,0 +1,171 @@
1
+ // lib/gsd-decompose-enrich.js
2
+ //
3
+ // COMP-GSD-2 T3: enrichTaskGraph(taskGraph, blueprintText) → TaskGraphGsd.
4
+ //
5
+ // Pure function. No filesystem I/O. Takes the bare TaskGraph emitted by
6
+ // Stratum's decompose step (tasks have id/files_owned/files_read/depends_on/
7
+ // description) and the blueprint text. Calls parseBoundaryMap (pure) to get
8
+ // slices and parse violations. Maps each slice to exactly one task by
9
+ // the slice's File Plan files ⊆ task.files_owned. Attaches produces/consumes
10
+ // arrays to each task.
11
+ //
12
+ // Purity note: the blueprint says T6/runGsd should use validateBoundaryMap
13
+ // (which does filesystem checks). T3 only uses parseBoundaryMap so it stays
14
+ // pure and deterministic — runGsd separately calls validateBoundaryMap as
15
+ // its lifecycle precondition.
16
+ //
17
+ // Throws on: parseBoundaryMap parseViolations, empty Boundary Map, orphaned
18
+ // slice (slice's File Plan files not all owned by any single task), orphaned
19
+ // task (no slice maps to it).
20
+
21
+ import { parseBoundaryMap } from './boundary-map.js';
22
+
23
+ // Match per-slice File Plan lines:
24
+ // File Plan: `path/a` (new), `path/b` (modify), ...
25
+ // File Plan: path/a, path/b
26
+ const FILEPLAN_LINE_RE = /^File Plan\s*:\s*(.+)$/;
27
+ const BACKTICK_PATH_RE = /`([^`]+)`/g;
28
+ const BARE_PATH_RE = /([^\s,()`]+)/g;
29
+
30
+ function extractSliceFilePlanFiles(blueprintText, sliceId) {
31
+ // Find the slice block: from `### {sliceId}` to the next `### S` heading or
32
+ // a `## ` heading or EOF.
33
+ const lines = blueprintText.split(/\r?\n/);
34
+ const headingRe = new RegExp(`^### ${sliceId}(?::|\\s|$)`);
35
+ let start = -1;
36
+ for (let i = 0; i < lines.length; i++) {
37
+ if (headingRe.test(lines[i])) { start = i + 1; break; }
38
+ }
39
+ if (start === -1) return [];
40
+ let end = lines.length;
41
+ for (let i = start; i < lines.length; i++) {
42
+ if (/^### S\d/.test(lines[i]) || /^## /.test(lines[i])) { end = i; break; }
43
+ }
44
+ // Find File Plan line within the slice block
45
+ for (let i = start; i < end; i++) {
46
+ const m = lines[i].match(FILEPLAN_LINE_RE);
47
+ if (!m) continue;
48
+ const tail = m[1];
49
+ // Prefer backtick-quoted paths; fall back to bare comma-separated if none.
50
+ const ticked = [...tail.matchAll(BACKTICK_PATH_RE)].map((mm) => mm[1].trim());
51
+ if (ticked.length > 0) return ticked;
52
+ // Strip parenthesized actions like " (new)" before bare-token matching.
53
+ const cleaned = tail.replace(/\([^)]*\)/g, '');
54
+ const bare = [...cleaned.matchAll(BARE_PATH_RE)].map((mm) => mm[1].trim()).filter(Boolean);
55
+ return bare;
56
+ }
57
+ return [];
58
+ }
59
+
60
+ export function enrichTaskGraph(taskGraph, blueprintText) {
61
+ if (!taskGraph || !Array.isArray(taskGraph.tasks)) {
62
+ throw new Error('enrichTaskGraph: taskGraph.tasks must be an array');
63
+ }
64
+ if (typeof blueprintText !== 'string') {
65
+ throw new Error('enrichTaskGraph: blueprintText must be a string');
66
+ }
67
+
68
+ const { slices, parseViolations } = parseBoundaryMap(blueprintText);
69
+ if (parseViolations.length > 0) {
70
+ const summary = parseViolations
71
+ .slice(0, 5)
72
+ .map((v) => `${v.kind}: ${v.message}`)
73
+ .join('; ');
74
+ throw new Error(`enrichTaskGraph: Boundary Map invalid (parse violations): ${summary}`);
75
+ }
76
+
77
+ const liveSlices = slices.filter((s) => !s._duplicate);
78
+ if (liveSlices.length === 0) {
79
+ throw new Error('enrichTaskGraph: Boundary Map empty (zero slices)');
80
+ }
81
+
82
+ // For each slice, extract its File Plan files and map to a task whose
83
+ // files_owned ⊇ those files. If the slice's File Plan files are split
84
+ // across multiple tasks → ambiguous → throw. If no task owns all of
85
+ // them → orphaned slice.
86
+ const sliceToTask = new Map();
87
+ const sliceFilePlanFiles = new Map(); // sliceId → string[]
88
+ for (const slice of liveSlices) {
89
+ const filePlanFiles = extractSliceFilePlanFiles(blueprintText, slice.id);
90
+ if (filePlanFiles.length === 0) {
91
+ throw new Error(
92
+ `enrichTaskGraph: slice ${slice.id} has no File Plan entries. ` +
93
+ `Each slice must declare its File Plan files for slice→task mapping.`,
94
+ );
95
+ }
96
+ sliceFilePlanFiles.set(slice.id, filePlanFiles);
97
+ const fpSet = new Set(filePlanFiles);
98
+ let matchedTaskId = null;
99
+ let partialMatchTaskIds = [];
100
+ for (const task of taskGraph.tasks) {
101
+ const owned = new Set(task.files_owned || []);
102
+ const allOwned = [...fpSet].every((f) => owned.has(f));
103
+ const someOwned = [...fpSet].some((f) => owned.has(f));
104
+ if (allOwned) {
105
+ if (matchedTaskId) {
106
+ throw new Error(
107
+ `enrichTaskGraph: slice ${slice.id} matches multiple tasks ` +
108
+ `(${matchedTaskId}, ${task.id}); decomposition has overlapping files_owned`,
109
+ );
110
+ }
111
+ matchedTaskId = task.id;
112
+ } else if (someOwned) {
113
+ partialMatchTaskIds.push(task.id);
114
+ }
115
+ }
116
+ if (!matchedTaskId) {
117
+ if (partialMatchTaskIds.length > 0) {
118
+ throw new Error(
119
+ `enrichTaskGraph: slice ${slice.id} File Plan files ` +
120
+ `[${[...fpSet].join(', ')}] are split across multiple tasks ` +
121
+ `(partial owners: ${partialMatchTaskIds.join(', ')}). ` +
122
+ `Each slice must map to exactly one task.`,
123
+ );
124
+ }
125
+ throw new Error(
126
+ `enrichTaskGraph: slice ${slice.id} is orphaned — no task owns its File Plan files [${[...fpSet].join(', ')}]`,
127
+ );
128
+ }
129
+ sliceToTask.set(slice.id, matchedTaskId);
130
+ }
131
+
132
+ // Reverse map: each task must have ≥1 matching slice.
133
+ const taskToSlices = new Map();
134
+ for (const [sliceId, taskId] of sliceToTask.entries()) {
135
+ if (!taskToSlices.has(taskId)) taskToSlices.set(taskId, []);
136
+ taskToSlices.get(taskId).push(sliceId);
137
+ }
138
+ const orphanedTasks = taskGraph.tasks
139
+ .filter((t) => !taskToSlices.has(t.id))
140
+ .map((t) => t.id);
141
+ if (orphanedTasks.length > 0) {
142
+ throw new Error(
143
+ `enrichTaskGraph: tasks have no matching Boundary Map slice (orphaned): ${orphanedTasks.join(', ')}`,
144
+ );
145
+ }
146
+
147
+ // Build the enriched TaskGraph. For each task, concatenate produces/consumes
148
+ // from all matching slices.
149
+ const slicesById = new Map(liveSlices.map((s) => [s.id, s]));
150
+ const enrichedTasks = taskGraph.tasks.map((task) => {
151
+ const matchedSliceIds = taskToSlices.get(task.id) || [];
152
+ const produces = [];
153
+ const consumes = [];
154
+ for (const sid of matchedSliceIds) {
155
+ const s = slicesById.get(sid);
156
+ for (const p of s.produces) {
157
+ produces.push({ file: p.file, symbols: p.symbols, kind: p.kind });
158
+ }
159
+ for (const c of s.consumes) {
160
+ consumes.push({ from: c.from, file: c.file, symbols: c.symbols });
161
+ }
162
+ }
163
+ return {
164
+ ...task,
165
+ produces,
166
+ consumes,
167
+ };
168
+ });
169
+
170
+ return { tasks: enrichedTasks };
171
+ }
@@ -0,0 +1,82 @@
1
+ // lib/gsd-prompt.js
2
+ //
3
+ // COMP-GSD-2 T4: buildTaskDescription — pure string assembly.
4
+ //
5
+ // Stratum's parallel_dispatch only interpolates {task.id|description|
6
+ // files_owned|files_read|depends_on|index} and {input.<field>}. Every other
7
+ // piece of context — the per-slice produces/consumes contract, the upstream
8
+ // tasks summary, the gate commands — must be packed inside `task.description`.
9
+ //
10
+ // This module produces the rich `description` string. It is also used by
11
+ // runGsd as a deterministic fallback when the decompose_gsd agent failed to
12
+ // bake a description into a task.
13
+ //
14
+ // Pure function. No fs, no globals.
15
+
16
+ const SECTION_DIVIDER = '---';
17
+
18
+ function formatProduces(produces) {
19
+ if (!produces || produces.length === 0) return '(none)';
20
+ const lines = produces.map((p) => {
21
+ const syms = (p.symbols || []).join(', ');
22
+ return ` ${p.file} → ${syms} (${p.kind})`;
23
+ });
24
+ return lines.join('\n');
25
+ }
26
+
27
+ function formatConsumes(consumes) {
28
+ if (!consumes || consumes.length === 0) return '(none)';
29
+ const lines = consumes.map((c) => {
30
+ const syms = (c.symbols || []).join(', ');
31
+ return ` from ${c.from}: ${c.file} → ${syms}`;
32
+ });
33
+ return lines.join('\n');
34
+ }
35
+
36
+ function formatUpstream(task, upstreamTasks) {
37
+ const deps = new Set(task.depends_on || []);
38
+ const filtered = (upstreamTasks || []).filter((t) => deps.has(t.id));
39
+ if (filtered.length === 0) return '(none)';
40
+ return filtered
41
+ .map((t) => {
42
+ const producesStr = (t.produces || [])
43
+ .map((p) => `${p.file} → ${(p.symbols || []).join(', ')} (${p.kind})`)
44
+ .join('; ');
45
+ return ` ${t.id}: produces ${producesStr || '(none)'}`;
46
+ })
47
+ .join('\n');
48
+ }
49
+
50
+ function formatGates(gateCommands) {
51
+ if (!gateCommands || gateCommands.length === 0) return '(none)';
52
+ return gateCommands.map((c) => ` - ${c}`).join('\n');
53
+ }
54
+
55
+ export function buildTaskDescription({ task, slice, upstreamTasks, gateCommands }) {
56
+ if (!task || typeof task !== 'object') {
57
+ throw new Error('buildTaskDescription: task object required');
58
+ }
59
+
60
+ // The function PRODUCES task.description — it must not embed any preexisting
61
+ // description (which may itself be malformed; this is the repair path). The
62
+ // output is the fresh, canonical description.
63
+ const lines = [];
64
+ lines.push('Symbols you must produce:');
65
+ lines.push(formatProduces(task.produces));
66
+ lines.push('');
67
+ lines.push('Symbols you may consume from upstream tasks:');
68
+ lines.push(formatConsumes(task.consumes));
69
+ lines.push('');
70
+ lines.push('Boundary Map slice (the contract for this task):');
71
+ lines.push(typeof slice === 'string' ? slice : '(slice text not provided)');
72
+ lines.push('');
73
+ lines.push('Upstream tasks (spec-level summary; their code lands at end-of-step merge):');
74
+ lines.push(formatUpstream(task, upstreamTasks));
75
+ lines.push('');
76
+ lines.push('GATES — you MUST run each command and they MUST pass before declaring done:');
77
+ lines.push(formatGates(gateCommands));
78
+ lines.push('');
79
+ lines.push('Fix and re-run within this invocation. Do NOT declare done while gates are red.');
80
+
81
+ return lines.join('\n');
82
+ }
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.27-beta",
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",
@@ -278,8 +278,13 @@ flows:
278
278
  agent: claude
279
279
  intent: >
280
280
  Verify every file:line reference in the blueprint against the actual codebase.
281
- Flag stale or incorrect references. Return { verified: true } only if all
282
- references are valid. Return { verified: false, staleRefs: [...] } otherwise.
281
+ Flag stale or incorrect references. If the blueprint contains a `## Boundary Map`
282
+ section, additionally invoke `validateBoundaryMap` from `lib/boundary-map.js` and
283
+ treat its violations as stale references; treat its warnings as informational.
284
+ Summarize Boundary Map results (counts of violations and warnings, plus any
285
+ must-fix detail) inside the existing `summary` string field of the PhaseResult —
286
+ do not add new top-level fields. The phase outcome is `complete` only when every
287
+ file:line reference is valid AND the Boundary Map (if present) has zero violations.
283
288
  inputs:
284
289
  featureCode: "$.input.featureCode"
285
290
  description: "$.input.description"