@structured-vibe-coding/core 0.12.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/defaults/memory/index.md +3 -0
- package/defaults/templates/features.md +42 -0
- package/defaults/templates/greenfield-architecture.md +39 -0
- package/defaults/templates/requirements.md +29 -0
- package/defaults/templates/vision.md +34 -0
- package/defaults/workflows/greenfield.yaml +92 -0
- package/dist/{agents-R4RMNXOS.js → agents-WL3PNTJL.js} +5 -2
- package/dist/change-context-W4HDO2IK.js +23 -0
- package/dist/{chunk-KZHNYGD6.js → chunk-67AUAGNV.js} +1 -38
- package/dist/chunk-A752UVPF.js +317 -0
- package/dist/chunk-C725DWSB.js +122 -0
- package/dist/chunk-OJFMJHNL.js +42 -0
- package/dist/{chunk-24S2YWWL.js → chunk-S7SHVKG4.js} +22 -435
- package/dist/chunk-SM734TFW.js +784 -0
- package/dist/{config-4QZMRSL3.js → config-ZYEEIRXJ.js} +2 -1
- package/dist/greenfield-NTLW6XBL.js +23 -0
- package/dist/index.d.ts +66 -1
- package/dist/index.js +103 -537
- package/package.json +1 -1
|
@@ -1,413 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ChangeMetaSchema,
|
|
3
|
-
WorkflowSchema,
|
|
4
2
|
loadConfig
|
|
5
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-OJFMJHNL.js";
|
|
6
4
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
buildAgentPrompt,
|
|
6
|
+
formatStatus
|
|
7
|
+
} from "./chunk-C725DWSB.js";
|
|
8
|
+
import {
|
|
9
|
+
buildContext,
|
|
10
|
+
getActiveChangeId
|
|
11
|
+
} from "./chunk-A752UVPF.js";
|
|
12
|
+
import {
|
|
13
|
+
sddRoot
|
|
13
14
|
} from "./chunk-7MJYPJMR.js";
|
|
14
15
|
import {
|
|
15
|
-
ensureDir,
|
|
16
|
-
listDirs,
|
|
17
|
-
listFiles,
|
|
18
16
|
pathExists,
|
|
19
17
|
readText,
|
|
20
|
-
readYaml,
|
|
21
18
|
removePath,
|
|
22
|
-
writeText
|
|
23
|
-
writeYaml
|
|
19
|
+
writeText
|
|
24
20
|
} from "./chunk-EPFURATC.js";
|
|
25
21
|
|
|
26
22
|
// src/agents.ts
|
|
27
|
-
import { join as join3 } from "pathe";
|
|
28
|
-
|
|
29
|
-
// src/slug.ts
|
|
30
|
-
function slugify(input) {
|
|
31
|
-
return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
|
|
32
|
-
}
|
|
33
|
-
function todayIsoDate(d = /* @__PURE__ */ new Date()) {
|
|
34
|
-
return d.toISOString().slice(0, 10);
|
|
35
|
-
}
|
|
36
|
-
function nowIso(d = /* @__PURE__ */ new Date()) {
|
|
37
|
-
return d.toISOString();
|
|
38
|
-
}
|
|
39
|
-
function changeDirName(title, d = /* @__PURE__ */ new Date()) {
|
|
40
|
-
const slug = slugify(title) || "change";
|
|
41
|
-
return `${todayIsoDate(d)}-${slug}`;
|
|
42
|
-
}
|
|
43
|
-
function uniqueChangeDirName(base, taken) {
|
|
44
|
-
if (!taken.has(base)) return base;
|
|
45
|
-
let n = 2;
|
|
46
|
-
while (taken.has(`${base}-${n}`)) n += 1;
|
|
47
|
-
return `${base}-${n}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// src/workflow.ts
|
|
51
23
|
import { join } from "pathe";
|
|
52
|
-
async function listWorkflowNames(projectRoot) {
|
|
53
|
-
const dir = workflowsDir(projectRoot);
|
|
54
|
-
const files = await listFiles(dir);
|
|
55
|
-
return files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => f.replace(/\.ya?ml$/, "")).sort();
|
|
56
|
-
}
|
|
57
|
-
async function loadWorkflow(projectRoot, name) {
|
|
58
|
-
const dir = workflowsDir(projectRoot);
|
|
59
|
-
const candidates = [`${name}.yaml`, `${name}.yml`];
|
|
60
|
-
for (const file of candidates) {
|
|
61
|
-
const path = join(dir, file);
|
|
62
|
-
if (await pathExists(path)) {
|
|
63
|
-
const raw = await readYaml(path);
|
|
64
|
-
const workflow = WorkflowSchema.parse(raw);
|
|
65
|
-
if (workflow.name !== name) {
|
|
66
|
-
}
|
|
67
|
-
return workflow;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
throw new Error(
|
|
71
|
-
`Workflow "${name}" not found in ${dir}. Available: ${(await listWorkflowNames(projectRoot)).join(", ") || "(none)"}`
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
function resolveStages(workflow, meta) {
|
|
75
|
-
const base = [...workflow.stages];
|
|
76
|
-
const extra = meta.overrides?.extra_stages ?? [];
|
|
77
|
-
const stages = [...base, ...extra];
|
|
78
|
-
return stages.map((stage) => {
|
|
79
|
-
const gateOverride = meta.overrides?.gates?.[stage.id];
|
|
80
|
-
if (!gateOverride) return stage;
|
|
81
|
-
return {
|
|
82
|
-
...stage,
|
|
83
|
-
gate: {
|
|
84
|
-
...stage.gate,
|
|
85
|
-
type: gateOverride
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
function isStageSkipped(meta, stageId) {
|
|
91
|
-
if (meta.overrides?.skip_stages?.includes(stageId)) return true;
|
|
92
|
-
if (meta.skipped?.some((s) => s.stage === stageId)) return true;
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
function shouldAutoSkip(stage, meta) {
|
|
96
|
-
const flags = stage.skip_when?.flags ?? [];
|
|
97
|
-
for (const flag of flags) {
|
|
98
|
-
if (meta.flags?.[flag] === true || meta.flags?.[flag] === "true") {
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
function activeStages(workflow, meta) {
|
|
105
|
-
return resolveStages(workflow, meta).filter((s) => {
|
|
106
|
-
if (isStageSkipped(meta, s.id)) return false;
|
|
107
|
-
if (shouldAutoSkip(s, meta)) return false;
|
|
108
|
-
return true;
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
function getStage(workflow, meta, stageId) {
|
|
112
|
-
return resolveStages(workflow, meta).find((s) => s.id === stageId);
|
|
113
|
-
}
|
|
114
|
-
function nextStageId(workflow, meta) {
|
|
115
|
-
const all = resolveStages(workflow, meta);
|
|
116
|
-
const activeIds = new Set(activeStages(workflow, meta).map((s) => s.id));
|
|
117
|
-
let start = all.findIndex((s) => s.id === meta.stage);
|
|
118
|
-
if (start === -1) start = -1;
|
|
119
|
-
for (let i = start + 1; i < all.length; i++) {
|
|
120
|
-
const id = all[i].id;
|
|
121
|
-
if (activeIds.has(id)) return id;
|
|
122
|
-
}
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
function firstActiveStageId(workflow, meta) {
|
|
126
|
-
return activeStages(workflow, meta)[0]?.id ?? null;
|
|
127
|
-
}
|
|
128
|
-
function inferComplexity(title, hints) {
|
|
129
|
-
const text = `${title} ${(hints?.keywords ?? []).join(" ")}`.toLowerCase();
|
|
130
|
-
const hotfixHints = ["typo", "hotfix", "nit", "rename", "bump", "chore", "readme", "docs"];
|
|
131
|
-
const highHints = [
|
|
132
|
-
"architecture",
|
|
133
|
-
"migration",
|
|
134
|
-
"auth",
|
|
135
|
-
"payment",
|
|
136
|
-
"billing",
|
|
137
|
-
"redesign",
|
|
138
|
-
"platform",
|
|
139
|
-
"arb",
|
|
140
|
-
"epic"
|
|
141
|
-
];
|
|
142
|
-
if (hotfixHints.some((h) => text.includes(h))) return "low";
|
|
143
|
-
if (highHints.some((h) => text.includes(h))) return "high";
|
|
144
|
-
if (text.length > 80) return "medium";
|
|
145
|
-
return "medium";
|
|
146
|
-
}
|
|
147
|
-
async function recommendWorkflow(projectRoot, title, config, opts) {
|
|
148
|
-
const available = await listWorkflowNames(projectRoot);
|
|
149
|
-
const allowed = config.allowed_workflows?.length ? available.filter((n) => config.allowed_workflows.includes(n)) : available;
|
|
150
|
-
if (opts?.preferred) {
|
|
151
|
-
if (!available.includes(opts.preferred)) {
|
|
152
|
-
throw new Error(
|
|
153
|
-
`Workflow "${opts.preferred}" is not available. Available: ${available.join(", ") || "(none)"}`
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
if (config.allowed_workflows?.length && !config.allowed_workflows.includes(opts.preferred)) {
|
|
157
|
-
throw new Error(
|
|
158
|
-
`Workflow "${opts.preferred}" is not in allowed_workflows: ${config.allowed_workflows.join(", ")}`
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
return {
|
|
162
|
-
name: opts.preferred,
|
|
163
|
-
reason: "Explicitly selected",
|
|
164
|
-
alternatives: allowed.filter((n) => n !== opts.preferred)
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
if (config.default_workflow !== "recommend" && allowed.includes(config.default_workflow)) {
|
|
168
|
-
return {
|
|
169
|
-
name: config.default_workflow,
|
|
170
|
-
reason: `Repo default_workflow=${config.default_workflow}`,
|
|
171
|
-
alternatives: allowed.filter((n) => n !== config.default_workflow)
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
const complexity = opts?.complexity ?? inferComplexity(title);
|
|
175
|
-
const scored = [];
|
|
176
|
-
for (const name of allowed) {
|
|
177
|
-
try {
|
|
178
|
-
const wf = await loadWorkflow(projectRoot, name);
|
|
179
|
-
let score = wf.recommendation?.priority ?? 0;
|
|
180
|
-
let reason = `workflow priority ${score}`;
|
|
181
|
-
const when = wf.recommendation?.when;
|
|
182
|
-
if (complexity === "low" && name === "hotfix") {
|
|
183
|
-
score += 50;
|
|
184
|
-
reason = "Low complexity \u2192 hotfix";
|
|
185
|
-
} else if (complexity === "low" && name === "patch") {
|
|
186
|
-
score += 40;
|
|
187
|
-
reason = "Low complexity \u2192 patch";
|
|
188
|
-
} else if (complexity === "medium" && name === "feature") {
|
|
189
|
-
score += 45;
|
|
190
|
-
reason = "Medium complexity \u2192 feature";
|
|
191
|
-
} else if (complexity === "medium" && name === "patch") {
|
|
192
|
-
score += 30;
|
|
193
|
-
reason = "Medium complexity \u2192 patch";
|
|
194
|
-
} else if (complexity === "high" && name === "enterprise-feature") {
|
|
195
|
-
score += 50;
|
|
196
|
-
reason = "High complexity \u2192 enterprise-feature";
|
|
197
|
-
} else if (complexity === "high" && name === "feature") {
|
|
198
|
-
score += 35;
|
|
199
|
-
reason = "High complexity \u2192 feature";
|
|
200
|
-
}
|
|
201
|
-
const keywords = when?.keywords ?? [];
|
|
202
|
-
const titleLower = title.toLowerCase();
|
|
203
|
-
if (keywords.some((k) => titleLower.includes(k.toLowerCase()))) {
|
|
204
|
-
score += 20;
|
|
205
|
-
reason = `Keyword match for ${name}`;
|
|
206
|
-
}
|
|
207
|
-
scored.push({ name, score, reason });
|
|
208
|
-
} catch {
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
scored.sort((a, b) => b.score - a.score);
|
|
212
|
-
const best = scored[0];
|
|
213
|
-
if (!best) {
|
|
214
|
-
throw new Error("No workflows available. Run `sdd init` or add files under .sdd/workflows/");
|
|
215
|
-
}
|
|
216
|
-
return {
|
|
217
|
-
name: best.name,
|
|
218
|
-
reason: best.reason,
|
|
219
|
-
alternatives: scored.slice(1).map((s) => s.name)
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// src/change-context.ts
|
|
224
|
-
async function loadChangeMeta(projectRoot, config, changeId) {
|
|
225
|
-
const path = changeMetaPath(projectRoot, config, changeId);
|
|
226
|
-
if (!await pathExists(path)) {
|
|
227
|
-
throw new Error(`Change not found: ${changeId}`);
|
|
228
|
-
}
|
|
229
|
-
return ChangeMetaSchema.parse(await readYaml(path));
|
|
230
|
-
}
|
|
231
|
-
async function saveChangeMeta(projectRoot, config, meta) {
|
|
232
|
-
meta.updated = nowIso();
|
|
233
|
-
await writeYaml(changeMetaPath(projectRoot, config, meta.id), ChangeMetaSchema.parse(meta));
|
|
234
|
-
}
|
|
235
|
-
async function buildContext(projectRoot, config, changeId) {
|
|
236
|
-
const meta = await loadChangeMeta(projectRoot, config, changeId);
|
|
237
|
-
const workflow = await loadWorkflow(projectRoot, meta.workflow);
|
|
238
|
-
return {
|
|
239
|
-
id: changeId,
|
|
240
|
-
path: changePath(projectRoot, config, changeId),
|
|
241
|
-
meta,
|
|
242
|
-
workflow,
|
|
243
|
-
stages: resolveStages(workflow, meta),
|
|
244
|
-
active: activeStages(workflow, meta)
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
async function listChanges(projectRoot, config) {
|
|
248
|
-
return listDirs(changesDir(projectRoot, config));
|
|
249
|
-
}
|
|
250
|
-
async function getActiveChangeId(projectRoot, config) {
|
|
251
|
-
const pointer = activePointerPath(projectRoot, config);
|
|
252
|
-
if (await pathExists(pointer)) {
|
|
253
|
-
const id = (await readText(pointer)).trim();
|
|
254
|
-
if (id && await pathExists(changePath(projectRoot, config, id))) {
|
|
255
|
-
return id;
|
|
256
|
-
}
|
|
257
|
-
if (id) {
|
|
258
|
-
await writeText(pointer, "");
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
const all = await listChanges(projectRoot, config);
|
|
262
|
-
const inProgress = [];
|
|
263
|
-
for (const id of all) {
|
|
264
|
-
try {
|
|
265
|
-
const meta = await loadChangeMeta(projectRoot, config, id);
|
|
266
|
-
if (meta.status === "in_progress" || meta.status === "blocked") {
|
|
267
|
-
inProgress.push({ id, updated: meta.updated ?? meta.created });
|
|
268
|
-
}
|
|
269
|
-
} catch {
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
if (!inProgress.length) return null;
|
|
273
|
-
inProgress.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
274
|
-
return inProgress[0].id;
|
|
275
|
-
}
|
|
276
|
-
async function clearActiveIf(projectRoot, config, changeId) {
|
|
277
|
-
const pointer = activePointerPath(projectRoot, config);
|
|
278
|
-
if (!await pathExists(pointer)) return;
|
|
279
|
-
const id = (await readText(pointer)).trim();
|
|
280
|
-
if (id === changeId) {
|
|
281
|
-
await writeText(pointer, "");
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
async function setActiveChange(projectRoot, config, changeId) {
|
|
285
|
-
await ensureDir(changesDir(projectRoot, config));
|
|
286
|
-
await writeText(activePointerPath(projectRoot, config), `${changeId}
|
|
287
|
-
`);
|
|
288
|
-
}
|
|
289
|
-
async function resolveChangeId(projectRoot, config, changeId) {
|
|
290
|
-
if (changeId) return changeId;
|
|
291
|
-
const active = await getActiveChangeId(projectRoot, config);
|
|
292
|
-
if (!active) {
|
|
293
|
-
throw new Error("No active change. Pass --change <id> or run `sdd new`.");
|
|
294
|
-
}
|
|
295
|
-
return active;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// src/agent-handoff.ts
|
|
299
|
-
import { join as join2 } from "pathe";
|
|
300
|
-
function formatStatus(ctx) {
|
|
301
|
-
const lines = [];
|
|
302
|
-
lines.push(`Change: ${ctx.meta.title}`);
|
|
303
|
-
lines.push(`ID: ${ctx.id}`);
|
|
304
|
-
lines.push(`Status: ${ctx.meta.status}`);
|
|
305
|
-
lines.push(`Workflow: ${ctx.meta.workflow}`);
|
|
306
|
-
lines.push(`Path: ${ctx.path}`);
|
|
307
|
-
lines.push("");
|
|
308
|
-
lines.push("Stages:");
|
|
309
|
-
for (const stage of ctx.stages) {
|
|
310
|
-
const skipped = isStageSkipped(ctx.meta, stage.id) || shouldAutoSkip(stage, ctx.meta);
|
|
311
|
-
let mark = " ";
|
|
312
|
-
if (skipped) mark = "\xB7";
|
|
313
|
-
else if (stage.id === ctx.meta.stage) mark = "\u25CF";
|
|
314
|
-
else {
|
|
315
|
-
const order = ctx.active.map((s) => s.id);
|
|
316
|
-
const cur = order.indexOf(ctx.meta.stage);
|
|
317
|
-
const idx = order.indexOf(stage.id);
|
|
318
|
-
if (idx >= 0 && cur >= 0 && idx < cur) mark = "\u2713";
|
|
319
|
-
}
|
|
320
|
-
const gate = ctx.meta.gates?.[stage.id];
|
|
321
|
-
const gateInfo = gate ? ` [${gate.status}]` : "";
|
|
322
|
-
const skipInfo = skipped ? " (skipped)" : "";
|
|
323
|
-
lines.push(
|
|
324
|
-
` [${mark}] ${stage.id}${stage.title ? ` \u2014 ${stage.title}` : ""}${gateInfo}${skipInfo}`
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
if (ctx.meta.overrides?.skip_stages?.length) {
|
|
328
|
-
lines.push("");
|
|
329
|
-
lines.push(`Overrides: skip=${ctx.meta.overrides.skip_stages.join(", ")}`);
|
|
330
|
-
}
|
|
331
|
-
return lines.join("\n");
|
|
332
|
-
}
|
|
333
|
-
async function buildAgentPrompt(ctx, config, projectRoot) {
|
|
334
|
-
const stage = getStage(ctx.workflow, ctx.meta, ctx.meta.stage);
|
|
335
|
-
const parts = [];
|
|
336
|
-
parts.push(`# SDD Agent Handoff`);
|
|
337
|
-
parts.push("");
|
|
338
|
-
parts.push(`You are implementing a change under Structured Vibe Coding (local SDD).`);
|
|
339
|
-
parts.push("");
|
|
340
|
-
parts.push(`## Change`);
|
|
341
|
-
parts.push(`- Title: ${ctx.meta.title}`);
|
|
342
|
-
parts.push(`- ID: ${ctx.id}`);
|
|
343
|
-
parts.push(`- Workflow: ${ctx.meta.workflow}`);
|
|
344
|
-
parts.push(
|
|
345
|
-
`- Current stage: ${ctx.meta.stage}${stage?.title ? ` (${stage.title})` : ""}`
|
|
346
|
-
);
|
|
347
|
-
parts.push(`- Change path: ${ctx.path}`);
|
|
348
|
-
parts.push("");
|
|
349
|
-
if (stage?.summary) {
|
|
350
|
-
parts.push(`## Stage goal`);
|
|
351
|
-
parts.push(stage.summary);
|
|
352
|
-
parts.push("");
|
|
353
|
-
}
|
|
354
|
-
if (stage?.agent_context?.instructions) {
|
|
355
|
-
parts.push(`## Instructions`);
|
|
356
|
-
parts.push(stage.agent_context.instructions);
|
|
357
|
-
parts.push("");
|
|
358
|
-
}
|
|
359
|
-
parts.push(`## Constraints`);
|
|
360
|
-
parts.push(`- Follow artifacts in the change directory as source of truth.`);
|
|
361
|
-
parts.push(`- Honor \`memory/constitution.md\` when present (non-negotiables).`);
|
|
362
|
-
parts.push(`- Do not skip ARB/decision constraints if present.`);
|
|
363
|
-
parts.push(`- Local development only \u2014 verify on this machine.`);
|
|
364
|
-
parts.push(`- Prefer small, reviewable commits.`);
|
|
365
|
-
parts.push("");
|
|
366
|
-
parts.push(`## Artifacts to read`);
|
|
367
|
-
const memoryHint = join2(projectRoot, config.memory_path);
|
|
368
|
-
parts.push(`- Documentation map (if present): ${memoryHint}/index.md`);
|
|
369
|
-
parts.push(`- Memory: ${memoryHint}/`);
|
|
370
|
-
for (const s of ctx.stages) {
|
|
371
|
-
for (const a of s.artifacts) {
|
|
372
|
-
parts.push(`- ${join2(ctx.path, a.path)}`);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
parts.push("");
|
|
376
|
-
parts.push(`## Current stage artifacts`);
|
|
377
|
-
if (stage) {
|
|
378
|
-
for (const a of stage.artifacts) {
|
|
379
|
-
parts.push(`- ${a.path}${a.required ? " (required)" : ""}`);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
parts.push("");
|
|
383
|
-
const stageId = ctx.meta.stage;
|
|
384
|
-
if (stageId === "implement" || stageId === "local_verify") {
|
|
385
|
-
parts.push(`## Code context (product code)`);
|
|
386
|
-
parts.push("");
|
|
387
|
-
parts.push(
|
|
388
|
-
`Process context above is not enough for product code structure.`
|
|
389
|
-
);
|
|
390
|
-
parts.push(
|
|
391
|
-
`When you need symbols, callers, or package-boundary context:`
|
|
392
|
-
);
|
|
393
|
-
parts.push(
|
|
394
|
-
`1. Run: \`sdd context\` (optional: \`--path \u2026\`, \`--symbol \u2026\`, \`--out change\`)`
|
|
395
|
-
);
|
|
396
|
-
parts.push(
|
|
397
|
-
`2. If present, read: \`changes/${ctx.id}/code-context.md\` (regenerate if stale)`
|
|
398
|
-
);
|
|
399
|
-
parts.push(
|
|
400
|
-
`Do not dump unbounded source into the prompt; prefer the sliced output.`
|
|
401
|
-
);
|
|
402
|
-
parts.push("");
|
|
403
|
-
}
|
|
404
|
-
parts.push(
|
|
405
|
-
`When done with this stage, the human will run \`sdd next\` or \`sdd verify\` / \`sdd complete\`.`
|
|
406
|
-
);
|
|
407
|
-
return parts.join("\n");
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// src/agents.ts
|
|
411
24
|
var AGENT_INTEGRATIONS = [
|
|
412
25
|
{
|
|
413
26
|
id: "copilot",
|
|
@@ -512,7 +125,7 @@ async function removeOtherAgentHosts(projectRoot, keep) {
|
|
|
512
125
|
for (const integ of AGENT_INTEGRATIONS) {
|
|
513
126
|
if (integ.id === keep) continue;
|
|
514
127
|
for (const rel of agentHostPaths(integ.id)) {
|
|
515
|
-
const full =
|
|
128
|
+
const full = join(projectRoot, rel);
|
|
516
129
|
if (await pathExists(full)) {
|
|
517
130
|
await removePath(full);
|
|
518
131
|
removed.push(rel);
|
|
@@ -520,7 +133,7 @@ async function removeOtherAgentHosts(projectRoot, keep) {
|
|
|
520
133
|
}
|
|
521
134
|
}
|
|
522
135
|
for (const rel of LEGACY_AGENT_PATHS) {
|
|
523
|
-
const full =
|
|
136
|
+
const full = join(projectRoot, rel);
|
|
524
137
|
if (await pathExists(full)) {
|
|
525
138
|
await removePath(full);
|
|
526
139
|
removed.push(rel);
|
|
@@ -529,7 +142,7 @@ async function removeOtherAgentHosts(projectRoot, keep) {
|
|
|
529
142
|
for (const parent of [".claude", ".idea", ".grok", ".ollama"]) {
|
|
530
143
|
if (keep === "grok" && parent === ".grok") continue;
|
|
531
144
|
if (keep === "ollama" && parent === ".ollama") continue;
|
|
532
|
-
const full =
|
|
145
|
+
const full = join(projectRoot, parent);
|
|
533
146
|
if (!await pathExists(full)) continue;
|
|
534
147
|
try {
|
|
535
148
|
const { readdir } = await import("fs/promises");
|
|
@@ -624,7 +237,7 @@ async function installAgentIntegration(opts) {
|
|
|
624
237
|
const force = opts.force ?? false;
|
|
625
238
|
await removeOtherAgentHosts(root, opts.target);
|
|
626
239
|
const write = async (rel, content) => {
|
|
627
|
-
const full =
|
|
240
|
+
const full = join(root, rel);
|
|
628
241
|
if (await pathExists(full) && !force) {
|
|
629
242
|
skipped.push(rel);
|
|
630
243
|
return;
|
|
@@ -632,7 +245,7 @@ async function installAgentIntegration(opts) {
|
|
|
632
245
|
await writeText(full, content);
|
|
633
246
|
created.push(rel);
|
|
634
247
|
};
|
|
635
|
-
await write(
|
|
248
|
+
await write(join(".sdd", "protocol.md"), PROTOCOL_MD);
|
|
636
249
|
await refreshActiveAgentContext(root);
|
|
637
250
|
const roles = integ.rolesToInstall?.length ? SDD_AGENT_ROLES.filter((r) => integ.rolesToInstall.includes(r.id)) : SDD_AGENT_ROLES;
|
|
638
251
|
for (const role of roles) {
|
|
@@ -640,7 +253,7 @@ async function installAgentIntegration(opts) {
|
|
|
640
253
|
}
|
|
641
254
|
await write("AGENTS.md", renderAgentsMd(integ));
|
|
642
255
|
await write(
|
|
643
|
-
|
|
256
|
+
join(".sdd", "agents.json"),
|
|
644
257
|
JSON.stringify(
|
|
645
258
|
{
|
|
646
259
|
version: 3,
|
|
@@ -708,7 +321,7 @@ Do not claim the change is complete without local verification when the workflow
|
|
|
708
321
|
`;
|
|
709
322
|
}
|
|
710
323
|
async function loadInstalledAgent(projectRoot) {
|
|
711
|
-
const path =
|
|
324
|
+
const path = join(sddRoot(projectRoot), "agents.json");
|
|
712
325
|
if (!await pathExists(path)) return null;
|
|
713
326
|
try {
|
|
714
327
|
const raw = JSON.parse(await readText(path));
|
|
@@ -723,7 +336,7 @@ async function loadInstalledAgent(projectRoot) {
|
|
|
723
336
|
async function writeAgentHandoff(projectRoot, config, changeId) {
|
|
724
337
|
const ctx = await buildContext(projectRoot, config, changeId);
|
|
725
338
|
const body = await buildAgentPrompt(ctx, config, projectRoot);
|
|
726
|
-
const outPath =
|
|
339
|
+
const outPath = join(sddRoot(projectRoot), "handoff.md");
|
|
727
340
|
const header = `# SDD handoff (auto)
|
|
728
341
|
|
|
729
342
|
> Generated for the AI coding agent configured at init.
|
|
@@ -760,16 +373,16 @@ function agentKickoffMessage(opts) {
|
|
|
760
373
|
}
|
|
761
374
|
async function refreshActiveAgentContext(projectRoot) {
|
|
762
375
|
const markerDir = sddRoot(projectRoot);
|
|
763
|
-
if (!await pathExists(
|
|
376
|
+
if (!await pathExists(join(markerDir, "config.yaml"))) {
|
|
764
377
|
return null;
|
|
765
378
|
}
|
|
766
|
-
const protocolPath =
|
|
379
|
+
const protocolPath = join(markerDir, "protocol.md");
|
|
767
380
|
if (!await pathExists(protocolPath)) {
|
|
768
381
|
await writeText(protocolPath, PROTOCOL_MD);
|
|
769
382
|
}
|
|
770
383
|
const config = await loadConfig(projectRoot);
|
|
771
384
|
const activeId = await getActiveChangeId(projectRoot, config);
|
|
772
|
-
const outPath =
|
|
385
|
+
const outPath = join(markerDir, "active-context.md");
|
|
773
386
|
if (!activeId) {
|
|
774
387
|
const empty = `# SDD active context
|
|
775
388
|
|
|
@@ -892,32 +505,6 @@ Refresh context: \`sdd agents refresh\`.
|
|
|
892
505
|
}
|
|
893
506
|
|
|
894
507
|
export {
|
|
895
|
-
listWorkflowNames,
|
|
896
|
-
loadWorkflow,
|
|
897
|
-
resolveStages,
|
|
898
|
-
isStageSkipped,
|
|
899
|
-
shouldAutoSkip,
|
|
900
|
-
activeStages,
|
|
901
|
-
getStage,
|
|
902
|
-
nextStageId,
|
|
903
|
-
firstActiveStageId,
|
|
904
|
-
inferComplexity,
|
|
905
|
-
recommendWorkflow,
|
|
906
|
-
slugify,
|
|
907
|
-
todayIsoDate,
|
|
908
|
-
nowIso,
|
|
909
|
-
changeDirName,
|
|
910
|
-
uniqueChangeDirName,
|
|
911
|
-
loadChangeMeta,
|
|
912
|
-
saveChangeMeta,
|
|
913
|
-
buildContext,
|
|
914
|
-
listChanges,
|
|
915
|
-
getActiveChangeId,
|
|
916
|
-
clearActiveIf,
|
|
917
|
-
setActiveChange,
|
|
918
|
-
resolveChangeId,
|
|
919
|
-
formatStatus,
|
|
920
|
-
buildAgentPrompt,
|
|
921
508
|
AGENT_INTEGRATIONS,
|
|
922
509
|
ALL_AGENT_TARGETS,
|
|
923
510
|
DEFAULT_INIT_INTEGRATION,
|