specwf 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -59
- package/bin/cli.d.ts +2 -0
- package/bin/cli.js +4425 -0
- package/bin/specwf.js +4424 -1
- package/dist/cli.js +3821 -1462
- package/package.json +2 -1
- package/dist/templates/agents/archiver.md +0 -112
- package/dist/templates/agents/executor.md +0 -125
- package/dist/templates/agents/planner.md +0 -114
- package/dist/templates/agents/researcher.md +0 -104
- package/dist/templates/agents/reviewer.md +0 -98
- package/dist/templates/agents/verifier.md +0 -129
- package/dist/templates/artifacts/context.md +0 -151
- package/dist/templates/artifacts/design.md +0 -224
- package/dist/templates/artifacts/goal-review.md +0 -95
- package/dist/templates/artifacts/project.yml +0 -117
- package/dist/templates/artifacts/proposal.md +0 -124
- package/dist/templates/artifacts/quality-review.md +0 -131
- package/dist/templates/artifacts/research.md +0 -127
- package/dist/templates/artifacts/spec-review.md +0 -84
- package/dist/templates/artifacts/state.md +0 -158
- package/dist/templates/artifacts/summary.md +0 -129
- package/dist/templates/artifacts/tasks.md +0 -109
- package/dist/templates/artifacts/verification.md +0 -113
- package/dist/templates/commands/adhoc.md +0 -38
- package/dist/templates/commands/apply.md +0 -20
- package/dist/templates/commands/archive.md +0 -21
- package/dist/templates/commands/continue.md +0 -27
- package/dist/templates/commands/discuss.md +0 -24
- package/dist/templates/commands/grill.md +0 -24
- package/dist/templates/commands/init.md +0 -37
- package/dist/templates/commands/milestone.md +0 -27
- package/dist/templates/commands/plan.md +0 -22
- package/dist/templates/commands/research-phase.md +0 -20
- package/dist/templates/commands/research.md +0 -24
- package/dist/templates/commands/review.md +0 -20
- package/dist/templates/commands/roadmap.md +0 -23
- package/dist/templates/commands/ship.md +0 -36
- package/dist/templates/commands/split.md +0 -16
- package/dist/templates/commands/verify.md +0 -22
- package/dist/templates/skills/adhoc.md +0 -53
- package/dist/templates/skills/apply.md +0 -134
- package/dist/templates/skills/archive.md +0 -109
- package/dist/templates/skills/continue.md +0 -97
- package/dist/templates/skills/discuss.md +0 -95
- package/dist/templates/skills/grill.md +0 -90
- package/dist/templates/skills/init.md +0 -116
- package/dist/templates/skills/milestone.md +0 -36
- package/dist/templates/skills/plan.md +0 -144
- package/dist/templates/skills/research-phase.md +0 -66
- package/dist/templates/skills/research.md +0 -76
- package/dist/templates/skills/review.md +0 -148
- package/dist/templates/skills/roadmap.md +0 -104
- package/dist/templates/skills/ship.md +0 -94
- package/dist/templates/skills/split.md +0 -96
- package/dist/templates/skills/verify.md +0 -147
package/bin/cli.js
ADDED
|
@@ -0,0 +1,4425 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
6
|
+
import { join as join19, dirname as dirname3 } from "path";
|
|
7
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8
|
+
|
|
9
|
+
// src/commands/specwf-init.ts
|
|
10
|
+
import { join as join6 } from "path";
|
|
11
|
+
|
|
12
|
+
// src/core/config.ts
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { existsSync } from "fs";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
|
|
17
|
+
// src/parser/yaml.ts
|
|
18
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
19
|
+
import { parse, parseDocument } from "yaml";
|
|
20
|
+
function readYamlDoc(path) {
|
|
21
|
+
return parseDocument(readFileSync(path, "utf-8"));
|
|
22
|
+
}
|
|
23
|
+
function writeYamlDoc(path, doc) {
|
|
24
|
+
writeFileSync(path, String(doc), "utf-8");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/types/config.ts
|
|
28
|
+
var PROFILE_MODEL_MAP = {
|
|
29
|
+
lite: {
|
|
30
|
+
research: "default",
|
|
31
|
+
plan: "default",
|
|
32
|
+
execute: "default",
|
|
33
|
+
review: "default",
|
|
34
|
+
verify: "default",
|
|
35
|
+
archive: "smol"
|
|
36
|
+
},
|
|
37
|
+
standard: {
|
|
38
|
+
research: "slow",
|
|
39
|
+
plan: "slow",
|
|
40
|
+
execute: "default",
|
|
41
|
+
review: "slow",
|
|
42
|
+
verify: "default",
|
|
43
|
+
archive: "default"
|
|
44
|
+
},
|
|
45
|
+
strict: {
|
|
46
|
+
research: "slow:high",
|
|
47
|
+
plan: "slow:high",
|
|
48
|
+
execute: "slow",
|
|
49
|
+
review: "slow:high",
|
|
50
|
+
verify: "slow",
|
|
51
|
+
archive: "default"
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/core/config.ts
|
|
56
|
+
import { Document as Document2 } from "yaml";
|
|
57
|
+
var CONFIG_FILE = "project.yml";
|
|
58
|
+
var ProjectConfigSchema = z.object({
|
|
59
|
+
version: z.number(),
|
|
60
|
+
platform: z.array(z.string()),
|
|
61
|
+
profile: z.enum(["lite", "standard", "strict"]),
|
|
62
|
+
context: z.string(),
|
|
63
|
+
workflow: z.object({
|
|
64
|
+
research: z.boolean().optional(),
|
|
65
|
+
plan_check: z.boolean().optional(),
|
|
66
|
+
tdd: z.boolean().optional(),
|
|
67
|
+
triple_review: z.boolean().optional(),
|
|
68
|
+
auto_advance: z.boolean().optional(),
|
|
69
|
+
spec_injection: z.boolean().optional()
|
|
70
|
+
}).optional().default({}),
|
|
71
|
+
review: z.object({
|
|
72
|
+
gate: z.enum(["all-pass", "severity", "report-only"]).optional(),
|
|
73
|
+
parallel: z.boolean().optional()
|
|
74
|
+
}).optional().default({}),
|
|
75
|
+
change: z.object({
|
|
76
|
+
parallel: z.enum(["serial", "dependency-graph", "pipeline"]).optional(),
|
|
77
|
+
isolation: z.boolean().optional()
|
|
78
|
+
}).optional().default({}),
|
|
79
|
+
git: z.object({
|
|
80
|
+
branching: z.enum(["none", "phase", "milestone"]).optional(),
|
|
81
|
+
create_tag: z.boolean().optional()
|
|
82
|
+
}).optional().default({}),
|
|
83
|
+
conventions: z.object({
|
|
84
|
+
inject: z.boolean().optional().default(true)
|
|
85
|
+
}).optional().default({ inject: true }),
|
|
86
|
+
models: z.record(z.string(), z.string()).optional().default({})
|
|
87
|
+
});
|
|
88
|
+
function configPath(specwfDir) {
|
|
89
|
+
return join(specwfDir, CONFIG_FILE);
|
|
90
|
+
}
|
|
91
|
+
function loadConfig(specwfDir) {
|
|
92
|
+
const doc = readYamlDoc(configPath(specwfDir));
|
|
93
|
+
const raw = doc.toJS();
|
|
94
|
+
return ProjectConfigSchema.parse(raw);
|
|
95
|
+
}
|
|
96
|
+
function saveConfig(specwfDir, config) {
|
|
97
|
+
let doc;
|
|
98
|
+
if (existsSync(configPath(specwfDir))) {
|
|
99
|
+
doc = readYamlDoc(configPath(specwfDir));
|
|
100
|
+
} else {
|
|
101
|
+
doc = new Document2({});
|
|
102
|
+
}
|
|
103
|
+
doc.set("version", config.version);
|
|
104
|
+
doc.set("platform", config.platform);
|
|
105
|
+
doc.set("profile", config.profile);
|
|
106
|
+
doc.set("context", config.context);
|
|
107
|
+
if (config.workflow) doc.set("workflow", config.workflow);
|
|
108
|
+
if (config.review) doc.set("review", config.review);
|
|
109
|
+
if (config.change) doc.set("change", config.change);
|
|
110
|
+
if (config.git) doc.set("git", config.git);
|
|
111
|
+
if (config.conventions) doc.set("conventions", config.conventions);
|
|
112
|
+
if (config.models) doc.set("models", config.models);
|
|
113
|
+
writeYamlDoc(configPath(specwfDir), doc);
|
|
114
|
+
}
|
|
115
|
+
function updateConfig(specwfDir, updater) {
|
|
116
|
+
const config = loadConfig(specwfDir);
|
|
117
|
+
updater(config);
|
|
118
|
+
saveConfig(specwfDir, config);
|
|
119
|
+
}
|
|
120
|
+
function resolveModels(config) {
|
|
121
|
+
const profile = config.profile;
|
|
122
|
+
const defaults = PROFILE_MODEL_MAP[profile];
|
|
123
|
+
return { ...defaults, ...config.models };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/core/file-tree.ts
|
|
127
|
+
import { mkdirSync, existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
128
|
+
import { join as join2 } from "path";
|
|
129
|
+
import { renameSync } from "fs";
|
|
130
|
+
var SPECWF_DIRS = [
|
|
131
|
+
"specs",
|
|
132
|
+
"conventions",
|
|
133
|
+
"research",
|
|
134
|
+
"milestones",
|
|
135
|
+
"changes",
|
|
136
|
+
"archive",
|
|
137
|
+
"workspace"
|
|
138
|
+
];
|
|
139
|
+
function createSpecwfStructure(specwfDir) {
|
|
140
|
+
mkdirSync(specwfDir, { recursive: true });
|
|
141
|
+
for (const dir of SPECWF_DIRS) {
|
|
142
|
+
mkdirSync(join2(specwfDir, dir), { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function isInitialized(specwfDir) {
|
|
146
|
+
return existsSync2(join2(specwfDir, "project.yml")) && existsSync2(join2(specwfDir, "state.md"));
|
|
147
|
+
}
|
|
148
|
+
function createAdhocChangeDir(specwfDir, changeName) {
|
|
149
|
+
const dir = join2(specwfDir, "changes", changeName);
|
|
150
|
+
mkdirSync(dir, { recursive: true });
|
|
151
|
+
mkdirSync(join2(dir, "specs"), { recursive: true });
|
|
152
|
+
return dir;
|
|
153
|
+
}
|
|
154
|
+
function archiveChangeDir(specwfDir, changeDir) {
|
|
155
|
+
const changeName = changeDir.split("/").pop() ?? "unknown";
|
|
156
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
157
|
+
const archiveRoot = join2(specwfDir, "archive", "changes");
|
|
158
|
+
mkdirSync(archiveRoot, { recursive: true });
|
|
159
|
+
const archiveDir = join2(archiveRoot, `${date}-${changeName}`);
|
|
160
|
+
if (existsSync2(changeDir)) {
|
|
161
|
+
renameSync(changeDir, archiveDir);
|
|
162
|
+
}
|
|
163
|
+
return archiveDir;
|
|
164
|
+
}
|
|
165
|
+
function archiveMilestoneDir(specwfDir, milestoneId) {
|
|
166
|
+
const sourceDir = join2(specwfDir, "milestones", milestoneId);
|
|
167
|
+
const archiveDir = join2(specwfDir, "archive", "milestones", milestoneId);
|
|
168
|
+
if (!existsSync2(sourceDir)) {
|
|
169
|
+
return archiveDir;
|
|
170
|
+
}
|
|
171
|
+
mkdirSync(join2(specwfDir, "archive", "milestones"), { recursive: true });
|
|
172
|
+
renameSync(sourceDir, archiveDir);
|
|
173
|
+
return archiveDir;
|
|
174
|
+
}
|
|
175
|
+
function listMilestones(specwfDir) {
|
|
176
|
+
const dir = join2(specwfDir, "milestones");
|
|
177
|
+
if (!existsSync2(dir)) return [];
|
|
178
|
+
return readdirSync(dir).filter((e) => {
|
|
179
|
+
const stat = statSync(join2(dir, e));
|
|
180
|
+
return stat.isDirectory();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function listPhases(specwfDir, milestoneId) {
|
|
184
|
+
const dir = join2(specwfDir, "milestones", milestoneId, "phases");
|
|
185
|
+
if (!existsSync2(dir)) return [];
|
|
186
|
+
return readdirSync(dir).filter((e) => {
|
|
187
|
+
const stat = statSync(join2(dir, e));
|
|
188
|
+
return stat.isDirectory();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function listChanges(specwfDir, milestoneId, phaseId) {
|
|
192
|
+
const dir = join2(specwfDir, "milestones", milestoneId, "phases", phaseId, "changes");
|
|
193
|
+
if (!existsSync2(dir)) return [];
|
|
194
|
+
return readdirSync(dir).filter((e) => {
|
|
195
|
+
const stat = statSync(join2(dir, e));
|
|
196
|
+
return stat.isDirectory();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function listAdhocChanges(specwfDir) {
|
|
200
|
+
const dir = join2(specwfDir, "changes");
|
|
201
|
+
if (!existsSync2(dir)) return [];
|
|
202
|
+
return readdirSync(dir).filter((e) => {
|
|
203
|
+
const stat = statSync(join2(dir, e));
|
|
204
|
+
return stat.isDirectory();
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function listArchived(specwfDir) {
|
|
208
|
+
const dir = join2(specwfDir, "archive", "changes");
|
|
209
|
+
if (!existsSync2(dir)) return [];
|
|
210
|
+
return readdirSync(dir).filter((e) => {
|
|
211
|
+
const stat = statSync(join2(dir, e));
|
|
212
|
+
return stat.isDirectory();
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/core/state-file.ts
|
|
217
|
+
import { join as join3 } from "path";
|
|
218
|
+
import { z as z2 } from "zod";
|
|
219
|
+
|
|
220
|
+
// src/parser/frontmatter.ts
|
|
221
|
+
import matter from "gray-matter";
|
|
222
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
223
|
+
function parseFrontmatter(content) {
|
|
224
|
+
const parsed = matter(content);
|
|
225
|
+
return {
|
|
226
|
+
data: parsed.data,
|
|
227
|
+
content: parsed.content
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function stringifyFrontmatter(data, body) {
|
|
231
|
+
return matter.stringify(body, data);
|
|
232
|
+
}
|
|
233
|
+
function readFrontmatterFile(path) {
|
|
234
|
+
return parseFrontmatter(readFileSync3(path, "utf-8"));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/core/state-file.ts
|
|
238
|
+
import { writeFileSync as writeFileSync3, existsSync as existsSync3 } from "fs";
|
|
239
|
+
var STATE_FILE = "state.md";
|
|
240
|
+
var ChangeStateSchema = z2.object({
|
|
241
|
+
name: z2.string(),
|
|
242
|
+
status: z2.string(),
|
|
243
|
+
depends_on: z2.array(z2.string()).optional().default([])
|
|
244
|
+
});
|
|
245
|
+
var StateFileSchema = z2.object({
|
|
246
|
+
project: z2.object({
|
|
247
|
+
name: z2.string(),
|
|
248
|
+
status: z2.string(),
|
|
249
|
+
current_milestone: z2.string().nullable(),
|
|
250
|
+
current_phase: z2.string().nullable()
|
|
251
|
+
}),
|
|
252
|
+
active_context: z2.object({
|
|
253
|
+
type: z2.enum(["project", "milestone", "phase", "change", "adhoc"]),
|
|
254
|
+
ref: z2.string().nullable(),
|
|
255
|
+
step: z2.string()
|
|
256
|
+
}),
|
|
257
|
+
changes: z2.array(ChangeStateSchema).optional().default([]),
|
|
258
|
+
adhoc: z2.array(ChangeStateSchema).optional().default([])
|
|
259
|
+
});
|
|
260
|
+
function statePath(specwfDir) {
|
|
261
|
+
return join3(specwfDir, STATE_FILE);
|
|
262
|
+
}
|
|
263
|
+
function loadState(specwfDir) {
|
|
264
|
+
const result = readFrontmatterFile(statePath(specwfDir));
|
|
265
|
+
return StateFileSchema.parse(result.data);
|
|
266
|
+
}
|
|
267
|
+
function saveState(specwfDir, state) {
|
|
268
|
+
let body;
|
|
269
|
+
try {
|
|
270
|
+
const existing = readFrontmatterFile(statePath(specwfDir));
|
|
271
|
+
body = existing.content;
|
|
272
|
+
} catch {
|
|
273
|
+
body = generateStateBody(state);
|
|
274
|
+
}
|
|
275
|
+
const output = stringifyFrontmatter(state, body);
|
|
276
|
+
writeFileSync3(statePath(specwfDir), output, "utf-8");
|
|
277
|
+
}
|
|
278
|
+
function updateState(specwfDir, updater) {
|
|
279
|
+
const state = loadState(specwfDir);
|
|
280
|
+
updater(state);
|
|
281
|
+
saveState(specwfDir, state);
|
|
282
|
+
}
|
|
283
|
+
function generateStateBody(state) {
|
|
284
|
+
const ctx = state.active_context;
|
|
285
|
+
const lines = [
|
|
286
|
+
"# State",
|
|
287
|
+
"",
|
|
288
|
+
"## \u5F53\u524D\u4F4D\u7F6E",
|
|
289
|
+
"",
|
|
290
|
+
formatContext(state),
|
|
291
|
+
"",
|
|
292
|
+
"## \u72B6\u6001\u673A",
|
|
293
|
+
"",
|
|
294
|
+
"\u9879\u76EE\u5C42\u8DEF\u5F84: `initialized \u2192 requirements-defined \u2192 researched \u2192 roadmap-defined`",
|
|
295
|
+
""
|
|
296
|
+
];
|
|
297
|
+
if (state.project) {
|
|
298
|
+
lines.push("## \u5386\u53F2", "");
|
|
299
|
+
}
|
|
300
|
+
return lines.join("\n");
|
|
301
|
+
}
|
|
302
|
+
function formatContext(state) {
|
|
303
|
+
const { type, ref, step } = state.active_context;
|
|
304
|
+
switch (type) {
|
|
305
|
+
case "project":
|
|
306
|
+
return `\u9879\u76EE\u5C42 \u2014 ${step}\u3002`;
|
|
307
|
+
case "milestone":
|
|
308
|
+
return `Milestone ${state.project.current_milestone ?? "?"} \u2014 ${step}\u3002`;
|
|
309
|
+
case "phase":
|
|
310
|
+
return `Phase ${state.project.current_phase ?? "?"} \u2014 ${step}\u3002`;
|
|
311
|
+
case "change":
|
|
312
|
+
return `Change (${ref ?? "?"}) \u2014 ${step}\u3002`;
|
|
313
|
+
case "adhoc":
|
|
314
|
+
return `\u4E34\u65F6 Change (${ref ?? "?"}) \u2014 ${step}\u3002`;
|
|
315
|
+
default:
|
|
316
|
+
return step;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/prompts/init-wizard.ts
|
|
321
|
+
async function runInitWizard(defaults) {
|
|
322
|
+
if (defaults.yes) {
|
|
323
|
+
return { profile: defaults.profile, context: "", platform: ["omp"], brownfield: false };
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const clack = await import("@clack/prompts");
|
|
327
|
+
const val = await clack.select({
|
|
328
|
+
message: "\u9009\u62E9\u5DE5\u4F5C\u6D41\u4E25\u683C\u5EA6:",
|
|
329
|
+
options: [{ value: "lite", label: "Lite" }, { value: "standard", label: "Standard\uFF08\u63A8\u8350\uFF09" }, { value: "strict", label: "Strict" }],
|
|
330
|
+
initialValue: defaults.profile
|
|
331
|
+
});
|
|
332
|
+
const profile = typeof val === "string" ? val : defaults.profile;
|
|
333
|
+
const ctxVal = await clack.text({ message: "\u9879\u76EE\u4E0A\u4E0B\u6587\u63CF\u8FF0\uFF08\u53EF\u9009\uFF09:", placeholder: "\u6280\u672F\u6808: TypeScript, Node.js..." });
|
|
334
|
+
const context = typeof ctxVal === "string" ? ctxVal : "";
|
|
335
|
+
const pfVal = await clack.multiselect({ message: "\u9009\u62E9\u76EE\u6807\u5E73\u53F0:", options: [{ value: "omp", label: "Oh My Pi" }], initialValues: ["omp"] });
|
|
336
|
+
const platform = Array.isArray(pfVal) ? pfVal : ["omp"];
|
|
337
|
+
const bfVal = await clack.confirm({ message: "\u8FD9\u662F\u4E00\u4E2A\u5B58\u91CF\u9879\u76EE\u5417\uFF1F", initialValue: false });
|
|
338
|
+
const brownfield = typeof bfVal === "boolean" ? bfVal : false;
|
|
339
|
+
return { profile, context, platform, brownfield };
|
|
340
|
+
} catch {
|
|
341
|
+
console.log("(@clack/prompts \u672A\u5B89\u88C5\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u914D\u7F6E)");
|
|
342
|
+
return { profile: defaults.profile, context: "", platform: ["omp"], brownfield: false };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/core/brownfield.ts
|
|
347
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as existsSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
348
|
+
import { join as join4 } from "path";
|
|
349
|
+
function detectProjectInfo(rootDir) {
|
|
350
|
+
const info = {
|
|
351
|
+
type: "unknown",
|
|
352
|
+
language: "unknown",
|
|
353
|
+
framework: "unknown",
|
|
354
|
+
hasPackageJson: false,
|
|
355
|
+
hasTests: false,
|
|
356
|
+
srcDirs: [],
|
|
357
|
+
structFiles: []
|
|
358
|
+
};
|
|
359
|
+
if (existsSync4(join4(rootDir, "package.json"))) {
|
|
360
|
+
info.hasPackageJson = true;
|
|
361
|
+
info.type = "node";
|
|
362
|
+
info.language = "typescript";
|
|
363
|
+
try {
|
|
364
|
+
const pkg2 = JSON.parse(readFileSync5(join4(rootDir, "package.json"), "utf-8"));
|
|
365
|
+
if (pkg2.dependencies?.next) info.framework = "next.js";
|
|
366
|
+
else if (pkg2.dependencies?.react) info.framework = "react";
|
|
367
|
+
else if (pkg2.dependencies?.vue) info.framework = "vue";
|
|
368
|
+
else if (pkg2.dependencies?.express) info.framework = "express";
|
|
369
|
+
else if (pkg2.dependencies?.fastify) info.framework = "fastify";
|
|
370
|
+
} catch {
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (existsSync4(join4(rootDir, "Cargo.toml"))) {
|
|
374
|
+
info.type = "rust";
|
|
375
|
+
info.language = "rust";
|
|
376
|
+
}
|
|
377
|
+
if (existsSync4(join4(rootDir, "go.mod"))) {
|
|
378
|
+
info.type = "go";
|
|
379
|
+
info.language = "go";
|
|
380
|
+
}
|
|
381
|
+
for (const dir of ["src", "app", "lib", "pkg", "cmd"]) {
|
|
382
|
+
if (existsSync4(join4(rootDir, dir)) && readdirSync2(join4(rootDir, dir), { withFileTypes: true }).some((e) => e.isDirectory())) {
|
|
383
|
+
info.srcDirs.push(dir);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (existsSync4(join4(rootDir, "__tests__")) || existsSync4(join4(rootDir, "tests"))) {
|
|
387
|
+
info.hasTests = true;
|
|
388
|
+
}
|
|
389
|
+
if (existsSync4(join4(rootDir, "vitest.config.ts")) || existsSync4(join4(rootDir, "jest.config.ts"))) {
|
|
390
|
+
info.hasTests = true;
|
|
391
|
+
}
|
|
392
|
+
return info;
|
|
393
|
+
}
|
|
394
|
+
function generateCodebaseReport(rootDir, info) {
|
|
395
|
+
const stack = buildStackSection(info);
|
|
396
|
+
const structure = buildStructureSection(rootDir);
|
|
397
|
+
const conventions = detectConventions(rootDir);
|
|
398
|
+
return { stack, structure, conventions };
|
|
399
|
+
}
|
|
400
|
+
function buildStackSection(info) {
|
|
401
|
+
return `# \u6280\u672F\u6808
|
|
402
|
+
|
|
403
|
+
- \u9879\u76EE\u7C7B\u578B: ${info.type}
|
|
404
|
+
- \u8BED\u8A00: ${info.language}
|
|
405
|
+
- \u6846\u67B6: ${info.framework}
|
|
406
|
+
- src \u76EE\u5F55: ${info.srcDirs.join(", ")}
|
|
407
|
+
- \u6D4B\u8BD5: ${info.hasTests ? "\u6709" : "\u65E0"}
|
|
408
|
+
`;
|
|
409
|
+
}
|
|
410
|
+
function buildStructureSection(rootDir) {
|
|
411
|
+
const lines = ["# \u9879\u76EE\u7ED3\u6784", ""];
|
|
412
|
+
try {
|
|
413
|
+
const entries = readdirSync2(rootDir, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") && !e.name.startsWith("node_modules") && e.name !== "dist").map((e) => `${e.isDirectory() ? " [dir] " : " [file] "}${e.name}`).slice(0, 30);
|
|
414
|
+
lines.push(...entries);
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
return lines.join("\n");
|
|
418
|
+
}
|
|
419
|
+
function detectConventions(rootDir) {
|
|
420
|
+
const configs = [];
|
|
421
|
+
if (existsSync4(join4(rootDir, "tsconfig.json"))) configs.push("TypeScript");
|
|
422
|
+
if (existsSync4(join4(rootDir, ".eslintrc.js")) || existsSync4(join4(rootDir, "eslint.config.js"))) configs.push("ESLint");
|
|
423
|
+
if (existsSync4(join4(rootDir, ".prettierrc"))) configs.push("Prettier");
|
|
424
|
+
return `# \u9879\u76EE\u7EA6\u5B9A
|
|
425
|
+
|
|
426
|
+
\u68C0\u6D4B\u5230: ${configs.length > 0 ? configs.join(", ") : "\u65E0"}`;
|
|
427
|
+
}
|
|
428
|
+
function bootstrapSpecs(rootDir, specwfDir) {
|
|
429
|
+
const specs = [];
|
|
430
|
+
if (existsSync4(join4(rootDir, "src"))) {
|
|
431
|
+
try {
|
|
432
|
+
for (const entry of readdirSync2(join4(rootDir, "src"), { withFileTypes: true })) {
|
|
433
|
+
if (entry.isDirectory() && !entry.name.startsWith("_")) {
|
|
434
|
+
const domainDir = join4(specwfDir, "specs", entry.name);
|
|
435
|
+
mkdirSync2(domainDir, { recursive: true });
|
|
436
|
+
writeFileSync4(
|
|
437
|
+
join4(domainDir, "spec.md"),
|
|
438
|
+
`# ${entry.name} Specification
|
|
439
|
+
|
|
440
|
+
## Purpose
|
|
441
|
+
|
|
442
|
+
[\u4ECE\u4E0A\u4F4D\u4EE3\u7801\u81EA\u52A8\u63D0\u53D6\u7684\u521D\u59CB spec \u2014 \u5F85\u4EBA\u5DE5\u5BA1\u6838]
|
|
443
|
+
|
|
444
|
+
## Requirements
|
|
445
|
+
|
|
446
|
+
`,
|
|
447
|
+
"utf-8"
|
|
448
|
+
);
|
|
449
|
+
specs.push(entry.name);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (specs.length === 0) {
|
|
456
|
+
const domainDir = join4(specwfDir, "specs", "general");
|
|
457
|
+
mkdirSync2(domainDir, { recursive: true });
|
|
458
|
+
writeFileSync4(
|
|
459
|
+
join4(domainDir, "spec.md"),
|
|
460
|
+
`# General Specification
|
|
461
|
+
|
|
462
|
+
## Purpose
|
|
463
|
+
|
|
464
|
+
[\u4ECE\u4E0A\u4F4D\u4EE3\u7801\u81EA\u52A8\u63D0\u53D6\u7684\u521D\u59CB spec \u2014 \u5F85\u4EBA\u5DE5\u5BA1\u6838]
|
|
465
|
+
|
|
466
|
+
## Requirements
|
|
467
|
+
|
|
468
|
+
`,
|
|
469
|
+
"utf-8"
|
|
470
|
+
);
|
|
471
|
+
specs.push("general");
|
|
472
|
+
}
|
|
473
|
+
return specs;
|
|
474
|
+
}
|
|
475
|
+
async function runBrownfieldInit(rootDir, specwfDir, info) {
|
|
476
|
+
const report = generateCodebaseReport(rootDir, info);
|
|
477
|
+
const researchDir = join4(specwfDir, "research", "codebase");
|
|
478
|
+
mkdirSync2(researchDir, { recursive: true });
|
|
479
|
+
writeFileSync4(join4(researchDir, "stack.md"), report.stack, "utf-8");
|
|
480
|
+
writeFileSync4(join4(researchDir, "structure.md"), report.structure, "utf-8");
|
|
481
|
+
writeFileSync4(join4(researchDir, "conventions.md"), report.conventions, "utf-8");
|
|
482
|
+
const domains = bootstrapSpecs(rootDir, specwfDir);
|
|
483
|
+
return domains;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/templates/workflows/init.ts
|
|
487
|
+
var instructions = `## Input
|
|
488
|
+
- No prior state required (this is the project entry point)
|
|
489
|
+
- Node.js 20+ must be installed
|
|
490
|
+
|
|
491
|
+
## Steps
|
|
492
|
+
|
|
493
|
+
### Step 1: Check state and get context
|
|
494
|
+
Run \`specwf context init\` \u2014 outputs JSON with state and file manifest. Read all listed files before proceeding.
|
|
495
|
+
|
|
496
|
+
### Step 1: Execute initialization
|
|
497
|
+
Run \`specwf init --yes\` to create the project skeleton:
|
|
498
|
+
|
|
499
|
+
- \`specwf/\` directory structure
|
|
500
|
+
- \`specwf/project.yml\` \u2014 project workflow configuration
|
|
501
|
+
- \`specwf/state.md\` \u2014 state machine file
|
|
502
|
+
- \`specwf/requirements.md\` \u2014 requirements document (template)
|
|
503
|
+
- \`specwf/conventions/\` \u2014 coding conventions directory
|
|
504
|
+
- \`.omp/commands/specwf-*.md\` \u2014 16 slash commands
|
|
505
|
+
- \`.omp/agents/specwf-*.md\` \u2014 8 agent definitions
|
|
506
|
+
- \`.omp/skills/specwf-*/SKILL.md\` \u2014 16 skill guides
|
|
507
|
+
|
|
508
|
+
### Step 2: Brownfield mode (existing projects)
|
|
509
|
+
For projects with existing code, use \`specwf init --yes --brownfield\`:
|
|
510
|
+
|
|
511
|
+
Dispatches two sub-agents in parallel:
|
|
512
|
+
|
|
513
|
+
**Agent 1: specwf-codebase-mapper** \u2014 analyzes existing codebase for tech stack, architecture, conventions, and pitfalls.
|
|
514
|
+
|
|
515
|
+
**Agent 2: specwf-spec-bootstrapper** \u2014 extracts behavioral contracts from existing code signatures, comments, and tests.
|
|
516
|
+
|
|
517
|
+
### Step 3: Advance
|
|
518
|
+
Run \`specwf continue\` to proceed to the requirements exploration phase (grill).
|
|
519
|
+
|
|
520
|
+
## Output
|
|
521
|
+
|
|
522
|
+
| File | Description |
|
|
523
|
+
|------|-------------|
|
|
524
|
+
| \`specwf/\` directory | Project skeleton |
|
|
525
|
+
| \`specwf/project.yml\` | Workflow configuration |
|
|
526
|
+
| \`specwf/state.md\` | State machine |
|
|
527
|
+
| \`specwf/requirements.md\` | Requirements template |
|
|
528
|
+
| \`.omp/commands/*.md\` | Generated slash commands |
|
|
529
|
+
| \`.omp/agents/*.md\` | Generated agent definitions |
|
|
530
|
+
| \`.omp/skills/*/SKILL.md\` | Generated skill guides |
|
|
531
|
+
|
|
532
|
+
Brownfield extras: \`codebase/stack.md\`, \`codebase/architecture.md\`, \`codebase/pitfalls.md\`, \`conventions/codebase-conventions.md\`, \`specs/<domain>/spec.md\`.
|
|
533
|
+
|
|
534
|
+
## Guardrails
|
|
535
|
+
|
|
536
|
+
- Run \`specwf init\` only once per project \u2014 re-running overwrites generated files
|
|
537
|
+
- Use \`--yes\` to skip interactive prompts in CI/non-interactive environments
|
|
538
|
+
- Brownfield mode is read-only analysis \u2014 it never modifies source code
|
|
539
|
+
- After initialization, fill in \`requirements.md\` before advancing`;
|
|
540
|
+
function getInitSkillTemplate() {
|
|
541
|
+
return {
|
|
542
|
+
name: "specwf-init",
|
|
543
|
+
description: "Initialize specwf project structure and generate platform files",
|
|
544
|
+
instructions
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function getInitCommandTemplate() {
|
|
548
|
+
return {
|
|
549
|
+
name: "SpecWF: Init",
|
|
550
|
+
description: "Initialize specwf project structure and generate platform files",
|
|
551
|
+
category: "Setup",
|
|
552
|
+
tags: ["specwf", "init", "setup"],
|
|
553
|
+
content: instructions
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/templates/workflows/grill.ts
|
|
558
|
+
var instructions2 = `## Input
|
|
559
|
+
|
|
560
|
+
### Parameters
|
|
561
|
+
- **No parameters**: operate on the current project from \`specwf state\`
|
|
562
|
+
|
|
563
|
+
### Prerequisites
|
|
564
|
+
- \`specwf/requirements.md\` must exist (created by init)
|
|
565
|
+
- \`specwf/project.yml\` for project context
|
|
566
|
+
|
|
567
|
+
## Steps
|
|
568
|
+
|
|
569
|
+
### Step 1: Get context
|
|
570
|
+
Run \`specwf context grill\` \u2014 outputs JSON with state and requirements.md path. Read requirements.md.
|
|
571
|
+
|
|
572
|
+
### Step 2: Explore requirements \u2014 one question at a time
|
|
573
|
+
Interview the user relentlessly until shared understanding is reached. Follow these rules:
|
|
574
|
+
|
|
575
|
+
1. **One question at a time.** Never batch multiple questions.
|
|
576
|
+
2. **Provide your recommended answer** for each question, then ask if they agree.
|
|
577
|
+
3. **Explore the codebase first** if the question can be answered by reading existing code or docs.
|
|
578
|
+
4. **Walk each branch of the decision tree** \u2014 resolve dependencies between decisions one by one.
|
|
579
|
+
|
|
580
|
+
Use the 5W1H framework:
|
|
581
|
+
- **What** \u2014 core goals and value proposition
|
|
582
|
+
- **Who** \u2014 target users, their roles and workflows
|
|
583
|
+
- **Where** \u2014 deployment environment, platform constraints
|
|
584
|
+
- **When** \u2014 timeline, phases, priorities
|
|
585
|
+
- **Why** \u2014 business motivation, success metrics
|
|
586
|
+
- **How** \u2014 technical preferences, non-functional requirements
|
|
587
|
+
|
|
588
|
+
### Step 3: Record decisions incrementally
|
|
589
|
+
After each consensus point, write it to \`specwf/requirements.md\` immediately \u2014 do not batch at the end.
|
|
590
|
+
|
|
591
|
+
### Step 4: Confirm consensus
|
|
592
|
+
Review requirements.md with the user. Ensure no ambiguity remains. Mark unresolved items as \`[TODO: decide]\`.
|
|
593
|
+
|
|
594
|
+
### Step 5: Advance
|
|
595
|
+
Run \`specwf continue\` to proceed to the research phase.
|
|
596
|
+
|
|
597
|
+
## Output
|
|
598
|
+
- \`specwf/requirements.md\` \u2014 populated with agreed requirements
|
|
599
|
+
|
|
600
|
+
## Guardrails
|
|
601
|
+
- No code is written during grill \u2014 this is pure exploration
|
|
602
|
+
- Do not skip questions that seem obvious \u2014 hidden assumptions cause rework
|
|
603
|
+
- Record decisions as they are made, not at the end
|
|
604
|
+
- If the user cannot answer, mark \`[TODO: decide]\` and move on`;
|
|
605
|
+
function getGrillSkillTemplate() {
|
|
606
|
+
return {
|
|
607
|
+
name: "specwf-grill",
|
|
608
|
+
description: "Requirements exploration \u2014 one question at a time, provide recommendations",
|
|
609
|
+
instructions: instructions2
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function getGrillCommandTemplate() {
|
|
613
|
+
return {
|
|
614
|
+
name: "SpecWF: Grill",
|
|
615
|
+
description: "Requirements exploration \u2014 one question at a time, provide recommendations",
|
|
616
|
+
category: "Discovery",
|
|
617
|
+
tags: ["specwf", "grill", "requirements", "discovery"],
|
|
618
|
+
content: instructions2
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/templates/workflows/research.ts
|
|
623
|
+
var instructions3 = `## Input
|
|
624
|
+
- \`specwf/requirements.md\` must be complete (grill phase done)
|
|
625
|
+
- \`specwf/project.yml\` for technical constraints
|
|
626
|
+
|
|
627
|
+
## Steps
|
|
628
|
+
|
|
629
|
+
### Step 1: Check state and get context
|
|
630
|
+
Run \`specwf context research\` \u2014 outputs JSON with state and file manifest. Read all listed files before proceeding.
|
|
631
|
+
|
|
632
|
+
### Step 1: Dispatch research sub-agents
|
|
633
|
+
**You are the orchestrator \u2014 dispatch, do not research yourself.** Spawn \`specwf-researcher\` sub-agents in parallel, one per technical direction (stack, architecture, pitfalls).
|
|
634
|
+
|
|
635
|
+
Construct each sub-agent prompt:
|
|
636
|
+
|
|
637
|
+
\`\`\`text
|
|
638
|
+
Sub-agent: specwf-researcher
|
|
639
|
+
Task: Research <direction> for project <project-name>
|
|
640
|
+
|
|
641
|
+
[Context]
|
|
642
|
+
- Read requirements.md from specwf/requirements.md
|
|
643
|
+
- Extract constraints from specwf/project.yml
|
|
644
|
+
- Research scope: <stack | architecture | pitfalls>
|
|
645
|
+
|
|
646
|
+
[Responsibilities]
|
|
647
|
+
- Compare at least 2 candidate solutions
|
|
648
|
+
- Assess feasibility, risk, and trade-offs
|
|
649
|
+
- Produce a recommended approach with rationale
|
|
650
|
+
- Output to specwf/research/<stack.md | architecture.md | pitfalls.md>
|
|
651
|
+
|
|
652
|
+
[Constraints]
|
|
653
|
+
- Read-only \u2014 do not modify code or config
|
|
654
|
+
- Mark speculative findings with confidence levels
|
|
655
|
+
\`\`\`
|
|
656
|
+
|
|
657
|
+
### Step 2: Verify sub-agent output
|
|
658
|
+
After all sub-agents complete, verify:
|
|
659
|
+
- \`research/stack.md\` exists with tech stack comparison and recommendation
|
|
660
|
+
- \`research/architecture.md\` exists with architecture evaluation
|
|
661
|
+
- \`research/pitfalls.md\` exists with risk assessment
|
|
662
|
+
- Write \`research/summary.md\` synthesizing all findings into one recommendation
|
|
663
|
+
|
|
664
|
+
### Step 3: Advance
|
|
665
|
+
Run \`specwf continue\` to proceed to roadmap definition.
|
|
666
|
+
|
|
667
|
+
## Output
|
|
668
|
+
- \`research/stack.md\` \u2014 recommended tech stack with alternatives compared
|
|
669
|
+
- \`research/architecture.md\` \u2014 recommended architecture with rationale
|
|
670
|
+
- \`research/pitfalls.md\` \u2014 known risks and mitigation strategies
|
|
671
|
+
- \`research/summary.md\` \u2014 consolidated research conclusion
|
|
672
|
+
|
|
673
|
+
## Guardrails
|
|
674
|
+
- **You are the orchestrator** \u2014 dispatch sub-agents, do not research yourself
|
|
675
|
+
- Each sub-agent must compare at least 2 alternatives \u2014 never recommend the first option found
|
|
676
|
+
- Sub-agents are independent and run in parallel
|
|
677
|
+
- Mark speculative findings with confidence levels`;
|
|
678
|
+
function getResearchSkillTemplate() {
|
|
679
|
+
return {
|
|
680
|
+
name: "specwf-research",
|
|
681
|
+
description: "Project-level technical research \u2014 dispatch researcher sub-agents in parallel",
|
|
682
|
+
instructions: instructions3
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function getResearchCommandTemplate() {
|
|
686
|
+
return {
|
|
687
|
+
name: "SpecWF: Research",
|
|
688
|
+
description: "Project-level technical research \u2014 dispatch researcher sub-agents in parallel",
|
|
689
|
+
category: "Discovery",
|
|
690
|
+
tags: ["specwf", "research", "architecture", "tech-stack", "sub-agent"],
|
|
691
|
+
content: instructions3
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/templates/workflows/roadmap.ts
|
|
696
|
+
var instructions4 = `## Input
|
|
697
|
+
|
|
698
|
+
### Parameters
|
|
699
|
+
- **No parameters**: operate on the current project
|
|
700
|
+
|
|
701
|
+
### Prerequisites
|
|
702
|
+
- \`specwf/requirements.md\` \u2014 complete requirements
|
|
703
|
+
- \`specwf/research/\` \u2014 technical research results (if available)
|
|
704
|
+
|
|
705
|
+
## Steps
|
|
706
|
+
|
|
707
|
+
### Step 1: Get context
|
|
708
|
+
Run \`specwf context roadmap\` \u2014 outputs JSON with state, requirements.md, and research/ paths. Read all listed files.
|
|
709
|
+
|
|
710
|
+
### Step 2: Define Milestones
|
|
711
|
+
Define milestones by functional area or release cadence. Write to \`specwf/roadmap.md\`:
|
|
712
|
+
|
|
713
|
+
\`\`\`markdown
|
|
714
|
+
# Roadmap
|
|
715
|
+
|
|
716
|
+
## Milestones
|
|
717
|
+
|
|
718
|
+
| ID | Goal | Phases | Duration |
|
|
719
|
+
|----|------|--------|----------|
|
|
720
|
+
| M1-<name> | <one-sentence goal> | <count> | <duration> |
|
|
721
|
+
|
|
722
|
+
## M1-<name>: <goal>
|
|
723
|
+
|
|
724
|
+
### Success Criteria
|
|
725
|
+
- <measurable condition>
|
|
726
|
+
|
|
727
|
+
### Phases
|
|
728
|
+
|
|
729
|
+
| ID | Goal | Depends On | Changes |
|
|
730
|
+
|----|------|-----------|---------|
|
|
731
|
+
| ph.1-<name> | <goal> | - | <count> |
|
|
732
|
+
| ph.2-<name> | <goal> | ph.1 | <count> |
|
|
733
|
+
|
|
734
|
+
### ph.1-<name>
|
|
735
|
+
- **Goal**: <what this phase delivers>
|
|
736
|
+
- **Inputs**: <specs, conventions, docs>
|
|
737
|
+
- **Outputs**: <code, specs, docs>
|
|
738
|
+
\`\`\`
|
|
739
|
+
|
|
740
|
+
### Step 3: Create milestone directories
|
|
741
|
+
For each milestone, create: \`specwf/milestones/<id>/\`
|
|
742
|
+
|
|
743
|
+
### Step 4: Validate coverage
|
|
744
|
+
- All functional scope covered by milestones and phases
|
|
745
|
+
- Phase dependencies form a DAG (no cycles)
|
|
746
|
+
- Each phase has verifiable success criteria
|
|
747
|
+
- Total phase count: 3-15
|
|
748
|
+
- First phase is the minimum viable path
|
|
749
|
+
|
|
750
|
+
### Step 5: Advance
|
|
751
|
+
Run \`specwf state set-milestone <id>\` to activate the first milestone, then \`specwf continue\`.
|
|
752
|
+
|
|
753
|
+
## Output
|
|
754
|
+
- \`specwf/roadmap.md\` \u2014 structured roadmap with milestone and phase tables
|
|
755
|
+
- \`specwf/milestones/<id>/\` \u2014 per-milestone directories
|
|
756
|
+
|
|
757
|
+
## Guardrails
|
|
758
|
+
- Start with the smallest viable milestone first
|
|
759
|
+
- Phase count per milestone: 3-8
|
|
760
|
+
- Each phase must produce a demonstrable increment
|
|
761
|
+
- Use the table format above \u2014 it's structured and parseable`;
|
|
762
|
+
function getRoadmapSkillTemplate() {
|
|
763
|
+
return {
|
|
764
|
+
name: "specwf-roadmap",
|
|
765
|
+
description: "Roadmap definition \u2014 split project into Milestones x Phases with structured format",
|
|
766
|
+
instructions: instructions4
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function getRoadmapCommandTemplate() {
|
|
770
|
+
return {
|
|
771
|
+
name: "SpecWF: Roadmap",
|
|
772
|
+
description: "Roadmap definition \u2014 split project into Milestones x Phases with structured format",
|
|
773
|
+
category: "Planning",
|
|
774
|
+
tags: ["specwf", "roadmap", "planning", "milestones"],
|
|
775
|
+
content: instructions4
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/templates/workflows/milestone.ts
|
|
780
|
+
var instructions5 = `## Input
|
|
781
|
+
|
|
782
|
+
### Parameters
|
|
783
|
+
- **No parameters**: operate on the currently active milestone from \`specwf state\`
|
|
784
|
+
- **\`<milestone-id>\`**: switch to a specific milestone
|
|
785
|
+
|
|
786
|
+
### Prerequisites
|
|
787
|
+
- \`specwf/roadmap.md\` must exist with defined milestones
|
|
788
|
+
|
|
789
|
+
## Steps
|
|
790
|
+
|
|
791
|
+
### Step 1: Get context
|
|
792
|
+
Run \`specwf context milestone\` \u2014 outputs JSON with state and roadmap file paths. Read all listed files.
|
|
793
|
+
|
|
794
|
+
### Step 2: Select milestone
|
|
795
|
+
If a milestone ID was provided: run \`specwf state set-milestone <id>\`.
|
|
796
|
+
If no ID: run \`specwf state\` to see the current milestone. To switch, use \`specwf state set-milestone <id>\`.
|
|
797
|
+
|
|
798
|
+
### Step 3: Select phase (optional)
|
|
799
|
+
If you need to jump to a specific phase within the milestone: run \`specwf state set-phase <id>\`.
|
|
800
|
+
|
|
801
|
+
### Step 4: Advance
|
|
802
|
+
Run \`specwf continue\` to proceed to the discuss phase for the active milestone/phase.
|
|
803
|
+
|
|
804
|
+
## When to use milestone vs continue
|
|
805
|
+
|
|
806
|
+
| Situation | Command |
|
|
807
|
+
|-----------|---------|
|
|
808
|
+
| Just finished roadmap, want to start first milestone | \`specwf state set-milestone <id>\` then \`specwf continue\` |
|
|
809
|
+
| Currently in a milestone, want to advance | \`specwf continue\` |
|
|
810
|
+
| Want to switch to a different milestone | \`specwf state set-milestone <id>\` |
|
|
811
|
+
| Want to jump to a specific phase | \`specwf state set-phase <id>\` |
|
|
812
|
+
|
|
813
|
+
## Output
|
|
814
|
+
- Updated state.md with new active milestone/phase
|
|
815
|
+
|
|
816
|
+
## Guardrails
|
|
817
|
+
- Switching milestones archives the current one if not yet shipped
|
|
818
|
+
- Phase transitions within a milestone do not trigger archival
|
|
819
|
+
- After switching, always run \`specwf continue\` to get the next step's instructions`;
|
|
820
|
+
function getMilestoneSkillTemplate() {
|
|
821
|
+
return {
|
|
822
|
+
name: "specwf-milestone",
|
|
823
|
+
description: "Milestone management \u2014 switch/create milestones, set current phase",
|
|
824
|
+
instructions: instructions5
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function getMilestoneCommandTemplate() {
|
|
828
|
+
return {
|
|
829
|
+
name: "SpecWF: Milestone",
|
|
830
|
+
description: "Milestone management \u2014 switch/create milestones, set current phase",
|
|
831
|
+
category: "Planning",
|
|
832
|
+
tags: ["specwf", "milestone", "planning"],
|
|
833
|
+
content: instructions5
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/templates/workflows/discuss.ts
|
|
838
|
+
var instructions6 = `## Input
|
|
839
|
+
|
|
840
|
+
### Parameters
|
|
841
|
+
- **No parameters**: operate on the currently active phase from \`specwf state\`
|
|
842
|
+
|
|
843
|
+
### Prerequisites
|
|
844
|
+
- Active milestone and phase must be set
|
|
845
|
+
- \`specwf/roadmap.md\` \u2014 current phase description and boundaries
|
|
846
|
+
|
|
847
|
+
## Steps
|
|
848
|
+
|
|
849
|
+
### Step 1: Get context
|
|
850
|
+
Run \`specwf context discuss\` \u2014 outputs JSON with state and roadmap path. Read roadmap.md.
|
|
851
|
+
|
|
852
|
+
### Step 2: Discuss and record decisions
|
|
853
|
+
Walk through each topic with the user. Record every decision using this format in context.md:
|
|
854
|
+
|
|
855
|
+
\`\`\`markdown
|
|
856
|
+
## D1: <decision title>
|
|
857
|
+
- **Decision**: <what we chose>
|
|
858
|
+
- **Rationale**: <why we chose it>
|
|
859
|
+
- **Alternatives considered**: <what else we evaluated and why rejected>
|
|
860
|
+
- **Impact**: <what this affects \u2014 files, interfaces, constraints>
|
|
861
|
+
|
|
862
|
+
## D2: <decision title>
|
|
863
|
+
...
|
|
864
|
+
\`\`\`
|
|
865
|
+
|
|
866
|
+
Topics to cover:
|
|
867
|
+
- **Phase goals** \u2014 what this phase delivers
|
|
868
|
+
- **Interface contracts** \u2014 key APIs and data models
|
|
869
|
+
- **Implementation constraints** \u2014 technical limits
|
|
870
|
+
- **Change split plan** \u2014 preliminary breakdown
|
|
871
|
+
- **Non-goals** \u2014 explicitly excluded
|
|
872
|
+
|
|
873
|
+
### Step 3: Mark gray areas
|
|
874
|
+
Unresolved items: mark as \`[TODO: discuss]\` with a brief note on what's blocking.
|
|
875
|
+
|
|
876
|
+
### Step 4: Advance
|
|
877
|
+
Run \`specwf continue\` to proceed to research-phase.
|
|
878
|
+
|
|
879
|
+
## Output
|
|
880
|
+
- \`context.md\` \u2014 phase-level implementation decisions (D1, D2, ... format)
|
|
881
|
+
|
|
882
|
+
## Guardrails
|
|
883
|
+
- Every architecture decision gets a unique D-number for traceability
|
|
884
|
+
- Do not skip gray areas \u2014 mark them and revisit
|
|
885
|
+
- context.md is the single source of truth for phase implementation`;
|
|
886
|
+
function getDiscussSkillTemplate() {
|
|
887
|
+
return {
|
|
888
|
+
name: "specwf-discuss",
|
|
889
|
+
description: "Phase discussion \u2014 capture implementation decisions with D1/D2 format",
|
|
890
|
+
instructions: instructions6
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function getDiscussCommandTemplate() {
|
|
894
|
+
return {
|
|
895
|
+
name: "SpecWF: Discuss",
|
|
896
|
+
description: "Phase discussion \u2014 capture implementation decisions with D1/D2 format",
|
|
897
|
+
category: "Planning",
|
|
898
|
+
tags: ["specwf", "discuss", "context", "decisions"],
|
|
899
|
+
content: instructions6
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/templates/workflows/research-phase.ts
|
|
904
|
+
var instructions7 = `## Input
|
|
905
|
+
- \`context.md\` must exist (discuss phase done)
|
|
906
|
+
- Related specs, conventions, and external dependencies
|
|
907
|
+
|
|
908
|
+
## Steps
|
|
909
|
+
|
|
910
|
+
### Step 1: Check state and get context
|
|
911
|
+
Run \`specwf context research-phase\` \u2014 outputs JSON with state and file manifest. Read all listed files before proceeding.
|
|
912
|
+
|
|
913
|
+
### Step 1: Dispatch phase researcher
|
|
914
|
+
**You are the orchestrator \u2014 dispatch, do not research yourself.** Spawn \`specwf-phase-researcher\` sub-agent.
|
|
915
|
+
|
|
916
|
+
Construct the sub-agent prompt:
|
|
917
|
+
|
|
918
|
+
\`\`\`text
|
|
919
|
+
Sub-agent: specwf-phase-researcher
|
|
920
|
+
Task: Research implementation paths for phase <phase-id>
|
|
921
|
+
|
|
922
|
+
[Context]
|
|
923
|
+
- Read context.md for locked decisions and discretion areas
|
|
924
|
+
- Read related specs/ for existing behavioral contracts
|
|
925
|
+
- Read conventions/ for coding standards
|
|
926
|
+
|
|
927
|
+
[Responsibilities]
|
|
928
|
+
- Investigate concrete implementation approaches
|
|
929
|
+
- Identify reusable patterns from existing codebase
|
|
930
|
+
- Flag known pitfalls and edge cases
|
|
931
|
+
- Produce research.md with recommended paths and TDD implications
|
|
932
|
+
- Output to milestones/<ms>/phases/<ph>/research.md
|
|
933
|
+
|
|
934
|
+
[Constraints]
|
|
935
|
+
- Respect context.md locked decisions \u2014 they are non-negotiable
|
|
936
|
+
- Surface trade-offs explicitly \u2014 do not present one option as the only path
|
|
937
|
+
- Note confidence levels for speculative findings
|
|
938
|
+
\`\`\`
|
|
939
|
+
|
|
940
|
+
### Step 2: Verify output
|
|
941
|
+
Confirm \`research.md\` was written by the sub-agent with:
|
|
942
|
+
- Recommended implementation paths with rationale
|
|
943
|
+
- Known pitfalls and edge cases
|
|
944
|
+
- TDD implications annotated
|
|
945
|
+
|
|
946
|
+
### Step 3: Advance
|
|
947
|
+
Run \`specwf continue\` to proceed to the split phase.
|
|
948
|
+
|
|
949
|
+
## Output
|
|
950
|
+
- \`research.md\` \u2014 phase-level implementation research with recommended paths and known pitfalls
|
|
951
|
+
|
|
952
|
+
## Guardrails
|
|
953
|
+
- **You are the orchestrator** \u2014 dispatch the sub-agent, do not research yourself
|
|
954
|
+
- Research must respect context.md locked decisions
|
|
955
|
+
- Surface trade-offs explicitly`;
|
|
956
|
+
function getResearchPhaseSkillTemplate() {
|
|
957
|
+
return {
|
|
958
|
+
name: "specwf-research-phase",
|
|
959
|
+
description: "Phase research \u2014 dispatch phase-researcher sub-agent",
|
|
960
|
+
instructions: instructions7
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
function getResearchPhaseCommandTemplate() {
|
|
964
|
+
return {
|
|
965
|
+
name: "SpecWF: Research Phase",
|
|
966
|
+
description: "Phase research \u2014 dispatch phase-researcher sub-agent",
|
|
967
|
+
category: "Discovery",
|
|
968
|
+
tags: ["specwf", "research-phase", "implementation", "sub-agent"],
|
|
969
|
+
content: instructions7
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// src/templates/workflows/split.ts
|
|
974
|
+
var instructions8 = `## Input
|
|
975
|
+
|
|
976
|
+
### Parameters
|
|
977
|
+
- **No parameters**: operate on the current phase from \`specwf state\`
|
|
978
|
+
|
|
979
|
+
### Prerequisites
|
|
980
|
+
- \`context.md\` \u2014 phase decisions and constraints
|
|
981
|
+
- \`research.md\` \u2014 implementation research
|
|
982
|
+
|
|
983
|
+
## Steps
|
|
984
|
+
|
|
985
|
+
### Step 1: Get context
|
|
986
|
+
Run \`specwf context split\` \u2014 outputs JSON with state, context.md, and research.md paths. Read all listed files.
|
|
987
|
+
|
|
988
|
+
### Step 2: Split into changes
|
|
989
|
+
Decompose the phase scope into independently implementable Change units:
|
|
990
|
+
- Each change is a vertical slice (not layer-by-layer)
|
|
991
|
+
- Identify dependencies \u2192 dependency graph (must be a DAG)
|
|
992
|
+
- Each change gets a descriptive kebab-case name
|
|
993
|
+
|
|
994
|
+
Create each change:
|
|
995
|
+
\`\`\`bash
|
|
996
|
+
specwf change new <name> --phase <phase-id>
|
|
997
|
+
\`\`\`
|
|
998
|
+
|
|
999
|
+
### Step 3: Document dependency graph
|
|
1000
|
+
Record in state.md:
|
|
1001
|
+
\`\`\`yaml
|
|
1002
|
+
changes:
|
|
1003
|
+
- name: <change-1>
|
|
1004
|
+
status: planned
|
|
1005
|
+
depends_on: []
|
|
1006
|
+
- name: <change-2>
|
|
1007
|
+
status: planned
|
|
1008
|
+
depends_on: [<change-1>]
|
|
1009
|
+
\`\`\`
|
|
1010
|
+
|
|
1011
|
+
### Step 4: Validation checklist
|
|
1012
|
+
Before advancing, verify:
|
|
1013
|
+
- [ ] Each change is a vertical slice (delivers end-to-end value)
|
|
1014
|
+
- [ ] Dependency graph is a DAG (no cycles)
|
|
1015
|
+
- [ ] Change count: 3-8
|
|
1016
|
+
- [ ] No purely sequential changes (merge them into one)
|
|
1017
|
+
- [ ] Every change has a directory with proposal.md template
|
|
1018
|
+
|
|
1019
|
+
### Step 5: Advance
|
|
1020
|
+
Run \`specwf continue\` to proceed to change planning.
|
|
1021
|
+
|
|
1022
|
+
## Output
|
|
1023
|
+
- \`specwf/changes/<name>/\` directories \u2014 one per change
|
|
1024
|
+
- Updated \`state.md\` with change dependency graph
|
|
1025
|
+
|
|
1026
|
+
## Guardrails
|
|
1027
|
+
- Changes must be vertical slices \u2014 each delivers end-to-end value
|
|
1028
|
+
- Dependency graph must be a DAG \u2014 no cycles
|
|
1029
|
+
- 3-8 changes per phase is the sweet spot
|
|
1030
|
+
- Merge purely sequential changes into one`;
|
|
1031
|
+
function getSplitSkillTemplate() {
|
|
1032
|
+
return {
|
|
1033
|
+
name: "specwf-split",
|
|
1034
|
+
description: "Change splitting \u2014 dependency graph + N changes with validation",
|
|
1035
|
+
instructions: instructions8
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function getSplitCommandTemplate() {
|
|
1039
|
+
return {
|
|
1040
|
+
name: "SpecWF: Split",
|
|
1041
|
+
description: "Change splitting \u2014 dependency graph + N changes with validation",
|
|
1042
|
+
category: "Planning",
|
|
1043
|
+
tags: ["specwf", "split", "changes", "dependency-graph"],
|
|
1044
|
+
content: instructions8
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/templates/workflows/adhoc.ts
|
|
1049
|
+
var instructions9 = `## Input
|
|
1050
|
+
|
|
1051
|
+
### Parameters
|
|
1052
|
+
- **\`<change-name>\`** (required) \u2014 kebab-case name for the new adhoc change.
|
|
1053
|
+
- If no name is provided, ask the user: "What should we call this change? (kebab-case, e.g. fix-login-timeout)"
|
|
1054
|
+
|
|
1055
|
+
### Prerequisites
|
|
1056
|
+
- specwf project must be initialized
|
|
1057
|
+
|
|
1058
|
+
## Steps
|
|
1059
|
+
|
|
1060
|
+
### Step 1: Resolve change name
|
|
1061
|
+
If a name was provided: use it directly.
|
|
1062
|
+
If no name: ask the user for a descriptive kebab-case name.
|
|
1063
|
+
|
|
1064
|
+
### Step 2: Create the adhoc change
|
|
1065
|
+
Run \`specwf change new <name>\` to create:
|
|
1066
|
+
- \`specwf/changes/<name>/proposal.md\` \u2014 proposal template
|
|
1067
|
+
- \`specwf/changes/<name>/design.md\` \u2014 design template
|
|
1068
|
+
- \`specwf/changes/<name>/tasks.md\` \u2014 tasks template
|
|
1069
|
+
- \`specwf/changes/<name>/specs/\` \u2014 delta-specs directory
|
|
1070
|
+
|
|
1071
|
+
The change is registered in \`state.md\` under the adhoc list with status \`proposal\`.
|
|
1072
|
+
|
|
1073
|
+
### Step 1: Fill the proposal
|
|
1074
|
+
Edit \`proposal.md\` \u2014 describe the intent, scope, approach, and must-haves.
|
|
1075
|
+
|
|
1076
|
+
### Step 2: Advance
|
|
1077
|
+
Run \`specwf continue change <name>\` to proceed through the standard change cycle.
|
|
1078
|
+
|
|
1079
|
+
## Output
|
|
1080
|
+
- \`specwf/changes/<name>/\` \u2014 change directory with template files
|
|
1081
|
+
- Updated \`state.md\` with new adhoc entry
|
|
1082
|
+
|
|
1083
|
+
## Guardrails
|
|
1084
|
+
- Adhoc changes do NOT go through milestone/phase discuss/research-phase/split flow
|
|
1085
|
+
- To associate with a phase, use \`specwf change new --phase <id>\`
|
|
1086
|
+
- Archived adhoc changes are stored under \`specwf/archive/\`
|
|
1087
|
+
- Adhoc changes follow the same plan->apply->review->verify->archive cycle as phase changes`;
|
|
1088
|
+
function getAdhocSkillTemplate() {
|
|
1089
|
+
return {
|
|
1090
|
+
name: "specwf-adhoc",
|
|
1091
|
+
description: "Create adhoc change \u2014 independent change unrelated to milestone/phase",
|
|
1092
|
+
instructions: instructions9
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
function getAdhocCommandTemplate() {
|
|
1096
|
+
return {
|
|
1097
|
+
name: "SpecWF: Adhoc",
|
|
1098
|
+
description: "Create adhoc change \u2014 independent change unrelated to milestone/phase",
|
|
1099
|
+
category: "Workflow",
|
|
1100
|
+
tags: ["specwf", "adhoc", "change"],
|
|
1101
|
+
content: instructions9
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// src/templates/workflows/plan.ts
|
|
1106
|
+
var instructions10 = `## Input
|
|
1107
|
+
|
|
1108
|
+
### Parameters
|
|
1109
|
+
- **\`<change-name>\`** (required) \u2014 the change to plan. Provided by \`specwf continue\` output or user.
|
|
1110
|
+
- If no change name is available, check the \`pending\` array from \`specwf context <step>\` JSON output, then ask the user which to work on.
|
|
1111
|
+
|
|
1112
|
+
### Prerequisites
|
|
1113
|
+
- Change \`proposal.md\` must be confirmed (not template)
|
|
1114
|
+
- \`context.md\` \u2014 phase-level implementation decisions (for phase changes)
|
|
1115
|
+
- Related \`specs/\` and \`conventions/\` files
|
|
1116
|
+
|
|
1117
|
+
## Steps
|
|
1118
|
+
|
|
1119
|
+
### Step 1: Resolve change name and get context
|
|
1120
|
+
If a change name was provided: use it directly. If not: run \`specwf state\`, list pending changes with status \`planned\` or \`proposal\`, ask the user to pick. Then run \`specwf context plan\` to get the file manifest. Read all listed files.
|
|
1121
|
+
|
|
1122
|
+
### Step 2: Dispatch planner sub-agent
|
|
1123
|
+
**You are the orchestrator \u2014 dispatch, do not design yourself.** Spawn \`specwf-planner\` sub-agent with:
|
|
1124
|
+
|
|
1125
|
+
\`\`\`text
|
|
1126
|
+
Sub-agent: specwf-planner
|
|
1127
|
+
Change: <change-name> (from specwf/changes/<change-name>/)
|
|
1128
|
+
|
|
1129
|
+
Task: Produce design.md, tasks.md, and delta-specs for this change.
|
|
1130
|
+
Read proposal.md, context.md, related specs/, and conventions/ first.
|
|
1131
|
+
Use templates: specwf template design, specwf template tasks.
|
|
1132
|
+
Write all output to specwf/changes/<change-name>/.
|
|
1133
|
+
|
|
1134
|
+
Completion: write completion.md to specwf/changes/<change-name>/ listing files created and decisions made.
|
|
1135
|
+
\`\`\`
|
|
1136
|
+
|
|
1137
|
+
### Step 3: Verify output and completion report
|
|
1138
|
+
After the planner finishes, check that \`completion.md\` exists and confirm:
|
|
1139
|
+
- \`design.md\` \u2014 architecture, data flow, alternatives
|
|
1140
|
+
- \`tasks.md\` \u2014 type annotations, TDD triples, wave grouping
|
|
1141
|
+
- \`specs/<domain>/spec.md\` \u2014 SHALL/MUST with scenarios
|
|
1142
|
+
- All must_haves from proposal.md covered
|
|
1143
|
+
- No contradictions with context.md
|
|
1144
|
+
|
|
1145
|
+
### Step 4: Advance
|
|
1146
|
+
Run \`specwf continue\` to proceed to the apply phase.
|
|
1147
|
+
|
|
1148
|
+
## Output
|
|
1149
|
+
- \`design.md\` \u2014 technical design document
|
|
1150
|
+
- \`tasks.md\` \u2014 implementation task checklist
|
|
1151
|
+
- \`specs/<domain>/spec.md\` \u2014 delta-specs
|
|
1152
|
+
- \`completion.md\` \u2014 sub-agent completion report
|
|
1153
|
+
|
|
1154
|
+
## Guardrails
|
|
1155
|
+
- **You are the orchestrator** \u2014 dispatch specwf-planner, do not design yourself
|
|
1156
|
+
- type:behavior tasks MUST have RED->GREEN->REFACTOR triples
|
|
1157
|
+
- Delta-specs describe behavior, not implementation details
|
|
1158
|
+
- If a change is too large to split into clear tasks, return to split phase`;
|
|
1159
|
+
function getPlanSkillTemplate() {
|
|
1160
|
+
return {
|
|
1161
|
+
name: "specwf-plan",
|
|
1162
|
+
description: "Change design \u2014 dispatch planner sub-agent for design + tasks + delta-specs",
|
|
1163
|
+
instructions: instructions10
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
function getPlanCommandTemplate() {
|
|
1167
|
+
return {
|
|
1168
|
+
name: "SpecWF: Plan",
|
|
1169
|
+
description: "Change design \u2014 dispatch planner sub-agent for design + tasks + delta-specs",
|
|
1170
|
+
category: "Workflow",
|
|
1171
|
+
tags: ["specwf", "plan", "design", "tasks", "delta-specs", "sub-agent"],
|
|
1172
|
+
content: instructions10
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/templates/workflows/apply.ts
|
|
1177
|
+
var instructions11 = `## Input
|
|
1178
|
+
|
|
1179
|
+
### Parameters
|
|
1180
|
+
- **\`<change-name>\`** (required) \u2014 the change to implement. Provided by \`specwf continue\` output or user.
|
|
1181
|
+
- If no change name is available, check the \`pending\` array from \`specwf context <step>\` JSON output, then ask the user which to work on.
|
|
1182
|
+
|
|
1183
|
+
### Prerequisites
|
|
1184
|
+
- Plan phase complete: \`design.md\`, \`tasks.md\`, delta-specs ready
|
|
1185
|
+
|
|
1186
|
+
## Steps
|
|
1187
|
+
|
|
1188
|
+
### Step 1: Resolve change name and get context
|
|
1189
|
+
Run \`specwf context apply\` \u2014 outputs JSON with state (including pending changes) and file manifest. If a change name was provided, use it directly. If not, read the \`pending\` array from the JSON, filter by status \`planning\`, and ask the user to pick. Then read all files listed in \`specs\`, \`conventions\`, and \`artifacts\`.
|
|
1190
|
+
|
|
1191
|
+
### Step 2: Dispatch executor sub-agent
|
|
1192
|
+
**You are the orchestrator \u2014 dispatch, do not implement yourself.** Spawn \`specwf-executor\` sub-agent with:
|
|
1193
|
+
|
|
1194
|
+
\`\`\`text
|
|
1195
|
+
Sub-agent: specwf-executor
|
|
1196
|
+
Change: <change-name> (from specwf/changes/<change-name>/)
|
|
1197
|
+
|
|
1198
|
+
Task: Implement all tasks in tasks.md following TDD protocol.
|
|
1199
|
+
Read design.md for approach, delta-specs for constraints.
|
|
1200
|
+
type:behavior tasks: RED->GREEN->REFACTOR (mandatory).
|
|
1201
|
+
All commits atomic, Conventional Commits format.
|
|
1202
|
+
Run type check and tests after each wave.
|
|
1203
|
+
|
|
1204
|
+
Completion: write completion.md listing tasks done, tests passed, commits made.
|
|
1205
|
+
\`\`\`
|
|
1206
|
+
|
|
1207
|
+
### Step 3: Verify output and completion report
|
|
1208
|
+
After the executor finishes, check \`completion.md\` exists and confirm:
|
|
1209
|
+
- All tasks.md checkboxes checked
|
|
1210
|
+
- Type check passes (\`tsc --noEmit\`)
|
|
1211
|
+
- All tests pass (\`vitest run\`)
|
|
1212
|
+
- Each delta-spec SHALL/MUST has test coverage
|
|
1213
|
+
|
|
1214
|
+
### Step 4: Generate change summary
|
|
1215
|
+
Run \`specwf template change-summary --name <change-name> --dir specwf/changes/<change-name>\`, then fill it with actual details. Do NOT skip \u2014 the summary is the handoff artifact for review.
|
|
1216
|
+
|
|
1217
|
+
### Step 5: Pre-advance checklist
|
|
1218
|
+
- [ ] All wave tasks complete
|
|
1219
|
+
- [ ] Type check passes
|
|
1220
|
+
- [ ] All tests pass
|
|
1221
|
+
- [ ] Change summary written and filled (not template)
|
|
1222
|
+
- [ ] completion.md from executor exists and verified
|
|
1223
|
+
|
|
1224
|
+
### Step 6: Advance
|
|
1225
|
+
Run \`specwf continue\` to proceed to review.
|
|
1226
|
+
|
|
1227
|
+
## Guardrails
|
|
1228
|
+
- **You are the orchestrator** \u2014 dispatch specwf-executor, never implement yourself
|
|
1229
|
+
- GREEN phase writes ONLY enough code to pass \u2014 save refactoring for REFACTOR
|
|
1230
|
+
- Never skip RED \u2014 always write the failing test first
|
|
1231
|
+
- **Summary is mandatory**: advancing without a filled change-summary.md is a process violation`;
|
|
1232
|
+
function getApplySkillTemplate() {
|
|
1233
|
+
return {
|
|
1234
|
+
name: "specwf-apply",
|
|
1235
|
+
description: "Code implementation \u2014 dispatch executor sub-agent for TDD RED->GREEN->REFACTOR",
|
|
1236
|
+
instructions: instructions11
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
function getApplyCommandTemplate() {
|
|
1240
|
+
return {
|
|
1241
|
+
name: "SpecWF: Apply",
|
|
1242
|
+
description: "Code implementation \u2014 dispatch executor sub-agent for TDD RED->GREEN->REFACTOR",
|
|
1243
|
+
category: "Workflow",
|
|
1244
|
+
tags: ["specwf", "apply", "implementation", "tdd", "sub-agent"],
|
|
1245
|
+
content: instructions11
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// src/templates/workflows/review.ts
|
|
1250
|
+
var instructions12 = `## Input
|
|
1251
|
+
|
|
1252
|
+
### Parameters
|
|
1253
|
+
- **\`<change-name>\`** (required) \u2014 the change to review. Provided by \`specwf continue\` output or user.
|
|
1254
|
+
- If no change name is available, check the \`pending\` array from \`specwf context <step>\` JSON output, then ask the user.
|
|
1255
|
+
|
|
1256
|
+
### Prerequisites
|
|
1257
|
+
- Apply phase complete: implementation code, tests, summary.md
|
|
1258
|
+
|
|
1259
|
+
## Steps
|
|
1260
|
+
|
|
1261
|
+
### Step 1: Resolve change name and get context
|
|
1262
|
+
If a change name was provided: use it directly. If not: run \`specwf state\`, list pending changes with status \`applying\`, ask the user to pick. Then run \`specwf context review\` to get the file manifest. Read all listed files.
|
|
1263
|
+
|
|
1264
|
+
### Step 2: Dispatch parallel review sub-agents
|
|
1265
|
+
**You are the orchestrator \u2014 dispatch, do not review yourself.** Spawn three \`specwf-reviewer\` sub-agents in parallel, each with a different role:
|
|
1266
|
+
|
|
1267
|
+
\`\`\`text
|
|
1268
|
+
Sub-agent: specwf-reviewer
|
|
1269
|
+
Change: <change-name>
|
|
1270
|
+
Role: spec-review | quality-review | goal-review (pick one per agent)
|
|
1271
|
+
|
|
1272
|
+
Task: Review the change according to your assigned role.
|
|
1273
|
+
Read proposal.md, delta-specs, design.md, and the implementation.
|
|
1274
|
+
Cite specific file:line references for every finding.
|
|
1275
|
+
|
|
1276
|
+
Output: write spec-review.md | quality-review.md | goal-review.md to specwf/changes/<change-name>/
|
|
1277
|
+
\`\`\`
|
|
1278
|
+
|
|
1279
|
+
### Step 3: Aggregate results
|
|
1280
|
+
After all three complete, check each report:
|
|
1281
|
+
- \`spec-review.md\`: all SHALL/MUST covered? BLOCKERs?
|
|
1282
|
+
- \`quality-review.md\`: any BLOCKERs or MAJOR issues?
|
|
1283
|
+
- \`goal-review.md\`: all goals ACHIEVED?
|
|
1284
|
+
|
|
1285
|
+
### Step 4: Handle findings
|
|
1286
|
+
- Auto-fixable issues \u2192 fix and re-verify
|
|
1287
|
+
- Architecture-level issues \u2192 pause and ask user
|
|
1288
|
+
- No BLOCKERs \u2192 advance
|
|
1289
|
+
|
|
1290
|
+
### Step 5: Advance
|
|
1291
|
+
Run \`specwf continue\` to proceed to verify.
|
|
1292
|
+
|
|
1293
|
+
## Guardrails
|
|
1294
|
+
- **You are the orchestrator** \u2014 dispatch reviewers, do not review yourself
|
|
1295
|
+
- All three reviews run in parallel \u2014 no inter-dependencies
|
|
1296
|
+
- Gate behavior depends on project.yml \`review.gate\` setting
|
|
1297
|
+
- Findings classified: BLOCKER (must fix), FLAG (should fix), NOTE (informational)`;
|
|
1298
|
+
function getReviewSkillTemplate() {
|
|
1299
|
+
return {
|
|
1300
|
+
name: "specwf-review",
|
|
1301
|
+
description: "Triple review \u2014 dispatch reviewer sub-agents in parallel",
|
|
1302
|
+
instructions: instructions12
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
function getReviewCommandTemplate() {
|
|
1306
|
+
return {
|
|
1307
|
+
name: "SpecWF: Review",
|
|
1308
|
+
description: "Triple review \u2014 dispatch reviewer sub-agents in parallel",
|
|
1309
|
+
category: "Workflow",
|
|
1310
|
+
tags: ["specwf", "review", "quality", "specs", "sub-agent"],
|
|
1311
|
+
content: instructions12
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/templates/workflows/verify.ts
|
|
1316
|
+
var instructions13 = `## Input
|
|
1317
|
+
|
|
1318
|
+
### Parameters
|
|
1319
|
+
- **\`<change-name>\`** (required) \u2014 the change to verify. Provided by \`specwf continue\` output or user.
|
|
1320
|
+
- If no change name, check the \`pending\` array from \`specwf context verify\` JSON, filter by status \`reviewing\`, ask the user.
|
|
1321
|
+
|
|
1322
|
+
### Prerequisites
|
|
1323
|
+
- Review phase complete: spec-review.md, quality-review.md, goal-review.md
|
|
1324
|
+
- All review blockers resolved
|
|
1325
|
+
|
|
1326
|
+
## Steps
|
|
1327
|
+
|
|
1328
|
+
### Step 1: Resolve change name and get context
|
|
1329
|
+
Run \`specwf context verify\` \u2014 outputs JSON with state and file manifest. If a change name was provided, use it directly. If not, read the \`pending\` array, filter by status \`reviewing\`, ask the user.
|
|
1330
|
+
|
|
1331
|
+
### Step 2: Dispatch verifier sub-agent
|
|
1332
|
+
**You are the orchestrator \u2014 dispatch, do not verify yourself.** Spawn \`specwf-verifier\` with:
|
|
1333
|
+
|
|
1334
|
+
\`\`\`text
|
|
1335
|
+
Sub-agent: specwf-verifier
|
|
1336
|
+
Change: <change-name> (from specwf/changes/<change-name>/)
|
|
1337
|
+
|
|
1338
|
+
## Goal-Backward Verification
|
|
1339
|
+
|
|
1340
|
+
Start from what the change SHOULD deliver, then verify it actually exists and works. Do NOT trust summary.md claims \u2014 they document what was SAID, not what EXISTS.
|
|
1341
|
+
|
|
1342
|
+
## Step 1: Establish truths
|
|
1343
|
+
Read proposal.md must-haves and delta-specs. For each must-have, ask:
|
|
1344
|
+
1. What must be TRUE for this to be achieved?
|
|
1345
|
+
2. What must EXIST in the codebase for those truths?
|
|
1346
|
+
3. What must be WIRED for those artifacts to function?
|
|
1347
|
+
|
|
1348
|
+
## Step 2: Run automated checks
|
|
1349
|
+
- Full test suite (diagnose any failures to root cause)
|
|
1350
|
+
- Type check (tsc --noEmit or equivalent)
|
|
1351
|
+
- Lint check
|
|
1352
|
+
- Each delta-spec SHALL/MUST must have passing test coverage
|
|
1353
|
+
|
|
1354
|
+
## Step 3: Adversarial stance
|
|
1355
|
+
Assume each must-have is NOT achieved until codebase evidence proves it. Classify:
|
|
1356
|
+
- **VERIFIED**: code evidence confirms the must-have is delivered
|
|
1357
|
+
- **FAILED (BLOCKER)**: must-have not achieved; change must not proceed
|
|
1358
|
+
- **UNCERTAIN (WARNING)**: insufficient evidence; human decision required
|
|
1359
|
+
|
|
1360
|
+
## Step 4: UAT \u2014 User Acceptance Testing
|
|
1361
|
+
For each must-have that describes user-observable behavior:
|
|
1362
|
+
- If a UI exists: open it, verify the behavior matches the spec
|
|
1363
|
+
- If an API exists: call it, verify the response matches the spec
|
|
1364
|
+
- If a CLI command exists: run it, verify the output matches the spec
|
|
1365
|
+
- Record what was tested and the result
|
|
1366
|
+
|
|
1367
|
+
## Step 5: Output VERIFICATION.md
|
|
1368
|
+
Write to specwf/changes/<change-name>/verification.md:
|
|
1369
|
+
- Status: passed | gaps_found | human_needed
|
|
1370
|
+
- Truth table: each must-have \u2192 VERIFIED / FAILED / UNCERTAIN with evidence
|
|
1371
|
+
- UAT results: what was tested manually, what passed
|
|
1372
|
+
- BLOCKERs: list of must-haves that failed (blocks advancement)
|
|
1373
|
+
- WARNINGs: items needing human review
|
|
1374
|
+
|
|
1375
|
+
## Routing
|
|
1376
|
+
- passed \u2192 archive
|
|
1377
|
+
- gaps_found \u2192 BLOCKERs listed \u2192 route back to apply (reapply)
|
|
1378
|
+
- human_needed \u2192 surface to user with specific questions
|
|
1379
|
+
\`\`\`
|
|
1380
|
+
|
|
1381
|
+
### Step 3: Handle results
|
|
1382
|
+
- \`passed\` \u2192 advance to archive
|
|
1383
|
+
- \`gaps_found\` \u2192 route back to apply (reapply) or plan (replan)
|
|
1384
|
+
- \`human_needed\` \u2192 surface to user with specific blocking questions
|
|
1385
|
+
|
|
1386
|
+
### Step 4: Advance
|
|
1387
|
+
Run \`specwf continue\` to proceed to archive (if passed).
|
|
1388
|
+
|
|
1389
|
+
## Output
|
|
1390
|
+
- \`verification.md\` \u2014 verification report with truth table, UAT results, BLOCKERs/WARNINGs
|
|
1391
|
+
|
|
1392
|
+
## Guardrails
|
|
1393
|
+
- **You are the orchestrator** \u2014 dispatch verifier, do not verify yourself
|
|
1394
|
+
- Goal-backward: what should be delivered \u2192 is it actually there?
|
|
1395
|
+
- Adversarial stance: assume NOT achieved until evidence proves it
|
|
1396
|
+
- BLOCKERs block advancement \u2014 do not advance with unresolved BLOCKERs`;
|
|
1397
|
+
function getVerifySkillTemplate() {
|
|
1398
|
+
return {
|
|
1399
|
+
name: "specwf-verify",
|
|
1400
|
+
description: "Test verification \u2014 goal-backward analysis + UAT, dispatch verifier sub-agent",
|
|
1401
|
+
instructions: instructions13
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
function getVerifyCommandTemplate() {
|
|
1405
|
+
return {
|
|
1406
|
+
name: "SpecWF: Verify",
|
|
1407
|
+
description: "Test verification \u2014 goal-backward analysis + UAT, dispatch verifier sub-agent",
|
|
1408
|
+
category: "Workflow",
|
|
1409
|
+
tags: ["specwf", "verify", "testing", "uat", "sub-agent"],
|
|
1410
|
+
content: instructions13
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// src/templates/workflows/archive.ts
|
|
1415
|
+
var instructions14 = `## Input
|
|
1416
|
+
|
|
1417
|
+
### Parameters
|
|
1418
|
+
- **\`<change-name>\`** (required) \u2014 the change to archive. Provided by \`specwf continue\` output or user.
|
|
1419
|
+
- If no change name, check the \`pending\` array from \`specwf context archive\` JSON, filter by status \`verifying\`, and ask the user.
|
|
1420
|
+
|
|
1421
|
+
### Prerequisites
|
|
1422
|
+
- Verify phase passed (verification.md status: passed)
|
|
1423
|
+
- All changes committed and pushed
|
|
1424
|
+
|
|
1425
|
+
## Steps
|
|
1426
|
+
|
|
1427
|
+
### Step 1: Resolve change name and get context
|
|
1428
|
+
Run \`specwf context archive\` \u2014 outputs JSON with state (pending list) and file manifest. If a change name was provided, use it directly. If not, read the \`pending\` array, filter by status \`verifying\`, ask the user to pick. Then read files from \`specs\` and \`artifacts\`.
|
|
1429
|
+
|
|
1430
|
+
### Step 2: Dispatch archiver sub-agent
|
|
1431
|
+
**You are the orchestrator \u2014 dispatch, do not archive yourself.** Spawn \`specwf-archiver\` with:
|
|
1432
|
+
|
|
1433
|
+
\`\`\`text
|
|
1434
|
+
Sub-agent: specwf-archiver
|
|
1435
|
+
Change: <change-name>
|
|
1436
|
+
|
|
1437
|
+
Task: Archive the completed change.
|
|
1438
|
+
- Merge delta-specs from specwf/changes/<change-name>/specs/ into specwf/specs/
|
|
1439
|
+
- Run code cognition backfill (update context.md)
|
|
1440
|
+
- Move change to specwf/archive/<YYYY-MM-DD>-<change-name>/
|
|
1441
|
+
- Update state.md: mark as archived
|
|
1442
|
+
|
|
1443
|
+
Output: write completion.md with merge results and archived path.
|
|
1444
|
+
\`\`\`
|
|
1445
|
+
|
|
1446
|
+
### Step 3: Verify archival
|
|
1447
|
+
Check \`completion.md\` and confirm:
|
|
1448
|
+
- Global \`specwf/specs/\` updated with delta-specs
|
|
1449
|
+
- Change directory moved to \`specwf/archive/<date>-<name>/\`
|
|
1450
|
+
- \`state.md\` reflects archived status
|
|
1451
|
+
|
|
1452
|
+
### Step 4: Advance
|
|
1453
|
+
Run \`specwf continue\` \u2014 if all phase changes are archived, routes to ship-phase.
|
|
1454
|
+
|
|
1455
|
+
## Guardrails
|
|
1456
|
+
- **You are the orchestrator** \u2014 dispatch archiver, do not archive yourself
|
|
1457
|
+
- Delta-spec merge must resolve conflicts, not overwrite
|
|
1458
|
+
- Archived changes are never deleted
|
|
1459
|
+
- If archival fails, the change stays in place`;
|
|
1460
|
+
function getArchiveSkillTemplate() {
|
|
1461
|
+
return {
|
|
1462
|
+
name: "specwf-archive",
|
|
1463
|
+
description: "Archive \u2014 dispatch archiver sub-agent for delta-spec merge + backfill",
|
|
1464
|
+
instructions: instructions14
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
function getArchiveCommandTemplate() {
|
|
1468
|
+
return {
|
|
1469
|
+
name: "SpecWF: Archive",
|
|
1470
|
+
description: "Archive \u2014 dispatch archiver sub-agent for delta-spec merge + backfill",
|
|
1471
|
+
category: "Workflow",
|
|
1472
|
+
tags: ["specwf", "archive", "specs", "merge", "sub-agent"],
|
|
1473
|
+
content: instructions14
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/templates/workflows/ship.ts
|
|
1478
|
+
var instructions15 = `## Input
|
|
1479
|
+
|
|
1480
|
+
### Parameters
|
|
1481
|
+
- **No parameters**: auto-detect ship context from \`specwf state\`
|
|
1482
|
+
|
|
1483
|
+
### Prerequisites
|
|
1484
|
+
- Phase ship: all phase changes archived
|
|
1485
|
+
- Milestone ship: all phases shipped
|
|
1486
|
+
- Git remote configured (for PR creation)
|
|
1487
|
+
|
|
1488
|
+
## Steps
|
|
1489
|
+
|
|
1490
|
+
### Step 1: Get context
|
|
1491
|
+
Run \`specwf context ship\` \u2014 outputs JSON with state info. Determine ship context (phase or milestone).
|
|
1492
|
+
|
|
1493
|
+
### Step 2: Dry-run first
|
|
1494
|
+
Always preview before executing:
|
|
1495
|
+
|
|
1496
|
+
\`\`\`bash
|
|
1497
|
+
specwf ship --dry-run
|
|
1498
|
+
\`\`\`
|
|
1499
|
+
|
|
1500
|
+
Review the output \u2014 what will be created, what state will change.
|
|
1501
|
+
|
|
1502
|
+
### Step 3: Phase ship
|
|
1503
|
+
Creates a PR summarizing all changes in the phase:
|
|
1504
|
+
\`\`\`bash
|
|
1505
|
+
specwf ship phase
|
|
1506
|
+
\`\`\`
|
|
1507
|
+
- Generates phase summary from archived changes
|
|
1508
|
+
- Creates PR via \`gh pr create\`
|
|
1509
|
+
- Updates state.md: marks phase as shipped
|
|
1510
|
+
|
|
1511
|
+
### Step 4: Milestone ship
|
|
1512
|
+
Publishes a release tag:
|
|
1513
|
+
\`\`\`bash
|
|
1514
|
+
specwf ship milestone
|
|
1515
|
+
\`\`\`
|
|
1516
|
+
- Creates release tag (e.g. v0.1.0)
|
|
1517
|
+
- Updates project.md version
|
|
1518
|
+
- Updates state.md: marks milestone as shipped
|
|
1519
|
+
|
|
1520
|
+
### Step 5: Advance
|
|
1521
|
+
Run \`specwf continue\` to start the next phase or milestone.
|
|
1522
|
+
|
|
1523
|
+
## Output
|
|
1524
|
+
- PR on GitHub (phase ship)
|
|
1525
|
+
- Release tag (milestone ship)
|
|
1526
|
+
- Updated \`state.md\` and \`project.md\`
|
|
1527
|
+
|
|
1528
|
+
## Guardrails
|
|
1529
|
+
- Always run \`--dry-run\` first \u2014 never ship without previewing
|
|
1530
|
+
- Phase ship requires all changes archived
|
|
1531
|
+
- Milestone ship requires all phases shipped`;
|
|
1532
|
+
function getShipSkillTemplate() {
|
|
1533
|
+
return {
|
|
1534
|
+
name: "specwf-ship",
|
|
1535
|
+
description: "Ship \u2014 dry-run first, then create PR or release tag",
|
|
1536
|
+
instructions: instructions15
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
function getShipCommandTemplate() {
|
|
1540
|
+
return {
|
|
1541
|
+
name: "SpecWF: Ship",
|
|
1542
|
+
description: "Ship \u2014 dry-run first, then create PR or release tag",
|
|
1543
|
+
category: "Workflow",
|
|
1544
|
+
tags: ["specwf", "ship", "release", "pr"],
|
|
1545
|
+
content: instructions15
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// src/templates/workflows/continue.ts
|
|
1550
|
+
var instructions16 = `## Input
|
|
1551
|
+
|
|
1552
|
+
### Two commands \u2014 two scopes
|
|
1553
|
+
|
|
1554
|
+
| Command | Scope | When to use |
|
|
1555
|
+
|---------|-------|------------|
|
|
1556
|
+
| \`specwf continue\` | Project-level / Phase-level | After init, grill, research, roadmap, discuss, research-phase, split, ship |
|
|
1557
|
+
| \`specwf continue change <name>\` | Single change | Advance a specific change through plan \u2192 apply \u2192 review \u2192 verify \u2192 archive |
|
|
1558
|
+
|
|
1559
|
+
### Prerequisites
|
|
1560
|
+
- \`specwf/state.md\` must exist and be valid
|
|
1561
|
+
- Previous step must be complete (exit conditions met)
|
|
1562
|
+
|
|
1563
|
+
## Steps
|
|
1564
|
+
|
|
1565
|
+
### Scenario A: Project-level advancement (\`specwf continue\`)
|
|
1566
|
+
|
|
1567
|
+
Use this after project-level steps (init, grill, research, roadmap, discuss, research-phase, split, ship).
|
|
1568
|
+
|
|
1569
|
+
Run \`specwf continue\`. The CLI:
|
|
1570
|
+
1. Reads \`active_context\` from state.md to determine what's current
|
|
1571
|
+
2. Validates the current step's exit conditions
|
|
1572
|
+
3. Advances state to the next step
|
|
1573
|
+
4. Outputs the next step's inline instructions
|
|
1574
|
+
|
|
1575
|
+
Examples:
|
|
1576
|
+
- After \`specwf init\` \u2192 \`specwf continue\` routes to grill
|
|
1577
|
+
- After grill \u2192 routes to research
|
|
1578
|
+
- After research \u2192 routes to roadmap
|
|
1579
|
+
- After roadmap \u2192 routes to discuss (first phase)
|
|
1580
|
+
- After discuss \u2192 routes to research-phase
|
|
1581
|
+
- After research-phase \u2192 routes to split
|
|
1582
|
+
- After split \u2192 routes to plan (for first change)
|
|
1583
|
+
|
|
1584
|
+
If \`specwf continue\` says "No available next step" but the project has pending changes, switch to Scenario B for those changes.
|
|
1585
|
+
|
|
1586
|
+
### Scenario B: Change-level advancement (\`specwf continue change <name>\`)
|
|
1587
|
+
|
|
1588
|
+
Use this to advance an individual change through its cycle. The change name comes from:
|
|
1589
|
+
- The \`specwf continue\` output (which tells you which change to work on)
|
|
1590
|
+
- \`specwf state\` output (lists pending changes with their status)
|
|
1591
|
+
- User explicitly naming a change
|
|
1592
|
+
|
|
1593
|
+
**With a name**: run \`specwf continue change <name>\`. The CLI advances that change's state and outputs the next step's inline instructions.
|
|
1594
|
+
|
|
1595
|
+
**Without a name**: run \`specwf state\`, read the pending changes list, and present options:
|
|
1596
|
+
|
|
1597
|
+
\`\`\`
|
|
1598
|
+
Which change should I advance?
|
|
1599
|
+
|
|
1600
|
+
1. <change-name> [planning] \u2014 ready for apply
|
|
1601
|
+
2. <change-name> [applying] \u2014 ready for review
|
|
1602
|
+
3. <change-name> [proposal] \u2014 ready for plan
|
|
1603
|
+
|
|
1604
|
+
Pick a number or name.
|
|
1605
|
+
\`\`\`
|
|
1606
|
+
|
|
1607
|
+
Then run \`specwf continue change <selected-name>\`.
|
|
1608
|
+
|
|
1609
|
+
### Scenario C: No pending work
|
|
1610
|
+
|
|
1611
|
+
If both \`specwf continue\` and all pending changes report "No available next step":
|
|
1612
|
+
|
|
1613
|
+
\`\`\`
|
|
1614
|
+
All work is complete or blocked.
|
|
1615
|
+
|
|
1616
|
+
Current state:
|
|
1617
|
+
- Project status: <status>
|
|
1618
|
+
- Active milestone: <name>
|
|
1619
|
+
- Pending changes: <list or "none">
|
|
1620
|
+
- Adhoc changes: <list or "none">
|
|
1621
|
+
|
|
1622
|
+
Run \`specwf state\` to inspect. To start new work, use:
|
|
1623
|
+
- \`specwf change new <name>\` for an adhoc change
|
|
1624
|
+
- \`specwf continue\` after completing blocking prerequisites
|
|
1625
|
+
\`\`\`
|
|
1626
|
+
|
|
1627
|
+
## Output
|
|
1628
|
+
Inline instructions for the next workflow step, including:
|
|
1629
|
+
- Current position and context
|
|
1630
|
+
- Next command name and slash command
|
|
1631
|
+
- Whether sub-agents are needed
|
|
1632
|
+
- Full step instructions (Input, Steps, Output, Guardrails)
|
|
1633
|
+
|
|
1634
|
+
## Guardrails
|
|
1635
|
+
- \`specwf continue\` (no args) = project/phase level; \`specwf continue change <name>\` = change level \u2014 do not mix them
|
|
1636
|
+
- Continue does NOT advance state if exit conditions are not met \u2014 it reports what's blocking
|
|
1637
|
+
- The output instructions are self-contained \u2014 execute them directly, no need to read another file
|
|
1638
|
+
- When multiple changes are pending, always present choices \u2014 do not silently pick one`;
|
|
1639
|
+
function getContinueSkillTemplate() {
|
|
1640
|
+
return {
|
|
1641
|
+
name: "specwf-continue",
|
|
1642
|
+
description: "Auto-advance \u2014 project-level or change-level, present pending work, route to next step",
|
|
1643
|
+
instructions: instructions16
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
function getContinueCommandTemplate() {
|
|
1647
|
+
return {
|
|
1648
|
+
name: "SpecWF: Continue",
|
|
1649
|
+
description: "Auto-advance \u2014 project-level or change-level, present pending work, route to next step",
|
|
1650
|
+
category: "Workflow",
|
|
1651
|
+
tags: ["specwf", "continue", "state-machine"],
|
|
1652
|
+
content: instructions16
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/templates/workflows/registry.ts
|
|
1657
|
+
var WORKFLOW_REGISTRY = {
|
|
1658
|
+
init: { skill: getInitSkillTemplate, command: getInitCommandTemplate },
|
|
1659
|
+
grill: { skill: getGrillSkillTemplate, command: getGrillCommandTemplate },
|
|
1660
|
+
research: { skill: getResearchSkillTemplate, command: getResearchCommandTemplate },
|
|
1661
|
+
roadmap: { skill: getRoadmapSkillTemplate, command: getRoadmapCommandTemplate },
|
|
1662
|
+
milestone: { skill: getMilestoneSkillTemplate, command: getMilestoneCommandTemplate },
|
|
1663
|
+
discuss: { skill: getDiscussSkillTemplate, command: getDiscussCommandTemplate },
|
|
1664
|
+
"research-phase": { skill: getResearchPhaseSkillTemplate, command: getResearchPhaseCommandTemplate },
|
|
1665
|
+
split: { skill: getSplitSkillTemplate, command: getSplitCommandTemplate },
|
|
1666
|
+
adhoc: { skill: getAdhocSkillTemplate, command: getAdhocCommandTemplate },
|
|
1667
|
+
plan: { skill: getPlanSkillTemplate, command: getPlanCommandTemplate },
|
|
1668
|
+
apply: { skill: getApplySkillTemplate, command: getApplyCommandTemplate },
|
|
1669
|
+
review: { skill: getReviewSkillTemplate, command: getReviewCommandTemplate },
|
|
1670
|
+
verify: { skill: getVerifySkillTemplate, command: getVerifyCommandTemplate },
|
|
1671
|
+
archive: { skill: getArchiveSkillTemplate, command: getArchiveCommandTemplate },
|
|
1672
|
+
ship: { skill: getShipSkillTemplate, command: getShipCommandTemplate },
|
|
1673
|
+
continue: { skill: getContinueSkillTemplate, command: getContinueCommandTemplate }
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
// src/integrations/omp/commands.ts
|
|
1677
|
+
var STEP_DEFS = [
|
|
1678
|
+
{ step: "init", name: "specwf:init", description: "Initialize specwf project structure and generate platform files", usesAgent: true, agents: ["researcher"] },
|
|
1679
|
+
{ step: "grill", name: "specwf:grill", description: "Requirements exploration \u2014 detailed questioning until shared understanding", usesAgent: false, agents: [] },
|
|
1680
|
+
{ step: "research", name: "specwf:research", description: "Project-level technical research \u2014 parallel multi-direction investigation", usesAgent: true, agents: ["researcher"] },
|
|
1681
|
+
{ step: "roadmap", name: "specwf:roadmap", description: "Roadmap definition \u2014 split project into Milestones \xD7 Phases", usesAgent: false, agents: [] },
|
|
1682
|
+
{ step: "milestone", name: "specwf:milestone", description: "Milestone management \u2014 switch/create milestones, set current phase", usesAgent: false, agents: [] },
|
|
1683
|
+
{ step: "discuss", name: "specwf:discuss", description: "Phase discussion \u2014 capture implementation decisions into context.md", usesAgent: false, agents: [] },
|
|
1684
|
+
{ step: "research-phase", name: "specwf:research-phase", description: "Phase research \u2014 implementation path investigation", usesAgent: true, agents: ["researcher"] },
|
|
1685
|
+
{ step: "split", name: "specwf:split", description: "Change splitting \u2014 dependency graph + N changes", usesAgent: false, agents: [] },
|
|
1686
|
+
{ step: "adhoc", name: "specwf:adhoc", description: "Create adhoc change \u2014 independent change unrelated to milestone/phase", usesAgent: false, agents: [] },
|
|
1687
|
+
{ step: "plan", name: "specwf:plan", description: "Change design \u2014 technical design + task breakdown + delta-specs", usesAgent: true, agents: ["planner"] },
|
|
1688
|
+
{ step: "apply", name: "specwf:apply", description: "Code implementation \u2014 TDD RED\u2192GREEN\u2192REFACTOR", usesAgent: true, agents: ["executor"] },
|
|
1689
|
+
{ step: "review", name: "specwf:review", description: "Triple review \u2014 spec/quality/goal reviews in parallel", usesAgent: true, agents: ["reviewer"] },
|
|
1690
|
+
{ step: "verify", name: "specwf:verify", description: "Test verification \u2014 diagnose root cause + route loopback", usesAgent: true, agents: ["verifier"] },
|
|
1691
|
+
{ step: "archive", name: "specwf:archive", description: "Archive \u2014 delta-spec merge + code cognition backfill", usesAgent: false, agents: [] },
|
|
1692
|
+
{ step: "ship", name: "specwf:ship", description: "Ship \u2014 create PR + update state / release tag", usesAgent: false, agents: [] },
|
|
1693
|
+
{ step: "continue", name: "specwf:continue", description: "Auto-advance \u2014 read STATE and route to next step", usesAgent: false, agents: [] }
|
|
1694
|
+
];
|
|
1695
|
+
function generateSlashCommand(def, _config) {
|
|
1696
|
+
const entry = WORKFLOW_REGISTRY[def.step];
|
|
1697
|
+
const body = entry ? entry.command().content : fallbackBody(def);
|
|
1698
|
+
return `---
|
|
1699
|
+
name: ${def.name}
|
|
1700
|
+
description: ${def.description}
|
|
1701
|
+
---
|
|
1702
|
+
|
|
1703
|
+
${body}
|
|
1704
|
+
`;
|
|
1705
|
+
}
|
|
1706
|
+
function fallbackBody(def) {
|
|
1707
|
+
const agentsSection = def.usesAgent && def.agents.length > 0 ? `Dispatch \`specwf-${def.agents[0]}\` sub-agent via task tool.` : "This step does not use sub-agents.";
|
|
1708
|
+
return `# ${def.description}
|
|
1709
|
+
|
|
1710
|
+
## Input
|
|
1711
|
+
|
|
1712
|
+
- state.md status is correct
|
|
1713
|
+
- All prerequisite steps are complete
|
|
1714
|
+
|
|
1715
|
+
## Steps
|
|
1716
|
+
|
|
1717
|
+
### Step 1: Check state
|
|
1718
|
+
Run \`specwf state\` to verify current position.
|
|
1719
|
+
|
|
1720
|
+
### Step 2: Get context
|
|
1721
|
+
Run \`specwf context ${def.step}\` to read the file manifest.
|
|
1722
|
+
|
|
1723
|
+
### Step 3: Execute
|
|
1724
|
+
Run \`specwf ${def.step}\` to perform the step.
|
|
1725
|
+
|
|
1726
|
+
## Sub-agents
|
|
1727
|
+
|
|
1728
|
+
${agentsSection}
|
|
1729
|
+
|
|
1730
|
+
## Output
|
|
1731
|
+
|
|
1732
|
+
Check \`specwf state\` for updated status.
|
|
1733
|
+
|
|
1734
|
+
## Advance
|
|
1735
|
+
|
|
1736
|
+
Run \`specwf continue\` to proceed to the next step.
|
|
1737
|
+
`;
|
|
1738
|
+
}
|
|
1739
|
+
function generateAllCommands(config) {
|
|
1740
|
+
return STEP_DEFS.map((def) => ({
|
|
1741
|
+
path: `.omp/commands/specwf-${def.step}.md`,
|
|
1742
|
+
content: generateSlashCommand(def, config)
|
|
1743
|
+
}));
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// src/integrations/omp/agents.ts
|
|
1747
|
+
import { dirname } from "path";
|
|
1748
|
+
import { fileURLToPath } from "url";
|
|
1749
|
+
|
|
1750
|
+
// src/templates/agents/index.ts
|
|
1751
|
+
var PLANNER_PROMPT = `## Role
|
|
1752
|
+
|
|
1753
|
+
You are a **Change Design Specialist** for specwf.
|
|
1754
|
+
|
|
1755
|
+
Your core responsibility is to analyze proposals, design technical solutions, create executable task checklists, and pre-write delta-specs as quality contracts. Your output directly drives the executor's implementation.
|
|
1756
|
+
|
|
1757
|
+
- Design complete technical solutions including architecture, data flow, and component trees
|
|
1758
|
+
- Break changes into independently committable task granularity
|
|
1759
|
+
- Annotate TDD protocol requirements for each type:behavior task
|
|
1760
|
+
- Pre-write delta-specs to ensure specification consistency
|
|
1761
|
+
- NEVER reduce or simplify the user's decision scope
|
|
1762
|
+
|
|
1763
|
+
## Core Constraints
|
|
1764
|
+
|
|
1765
|
+
- All artifacts written to the specwf/ directory
|
|
1766
|
+
- Use bash to invoke specwf CLI for state management
|
|
1767
|
+
- Respect project.yml context field
|
|
1768
|
+
- Follow conventions/ for coding standards
|
|
1769
|
+
- All output files use English
|
|
1770
|
+
|
|
1771
|
+
## Execution Flow
|
|
1772
|
+
|
|
1773
|
+
### Step 1: Read project context and proposal
|
|
1774
|
+
- Read specwf/project.yml for profile and workflow configuration
|
|
1775
|
+
- Read the change's proposal.md for intent, scope, and must-haves
|
|
1776
|
+
- Read specwf/specs/ for existing global specs
|
|
1777
|
+
- Read specwf/conventions/ for coding standards
|
|
1778
|
+
|
|
1779
|
+
### Step 2: Design technical solution
|
|
1780
|
+
- Design overall architecture based on proposal and context
|
|
1781
|
+
- Consider at least 2 alternatives and compare
|
|
1782
|
+
- Document the complete design in design.md using \`specwf template design\`
|
|
1783
|
+
|
|
1784
|
+
### Step 3: Break down into executable tasks
|
|
1785
|
+
- Use tracer-bullet vertical slice principle
|
|
1786
|
+
- First wave is typically an end-to-end skeleton
|
|
1787
|
+
- Annotate each task's type and dependencies
|
|
1788
|
+
|
|
1789
|
+
### Step 4: Pre-write delta-specs
|
|
1790
|
+
- Create spec files under specs/<domain>/
|
|
1791
|
+
- Use SHALL/MUST/SHOULD/MAY keywords
|
|
1792
|
+
- Ensure each spec item is testable
|
|
1793
|
+
|
|
1794
|
+
## Deviation Rules
|
|
1795
|
+
|
|
1796
|
+
1. **Scope reduction prohibition**: NEVER reduce user decision points to simplify implementation
|
|
1797
|
+
2. **Spec gap fill**: Annotate missing specs as SPEC_GAP_FILL
|
|
1798
|
+
3. **Task granularity**: behavior task \u2264 50 lines, refactor task \u2264 200 lines changed
|
|
1799
|
+
4. **Alternative archiving**: Record rejected alternatives in design.md
|
|
1800
|
+
|
|
1801
|
+
## Output Requirements
|
|
1802
|
+
|
|
1803
|
+
- design.md \u2014 technical design with architecture, data flow, alternatives
|
|
1804
|
+
- tasks.md \u2014 implementation checklist with TDD annotations
|
|
1805
|
+
- specs/<domain>/spec.md \u2014 delta behavioral contracts
|
|
1806
|
+
|
|
1807
|
+
## Verification Criteria
|
|
1808
|
+
|
|
1809
|
+
- tasks.md covers all must_haves from proposal.md
|
|
1810
|
+
- Each type:behavior task has a RED test description
|
|
1811
|
+
- Delta-spec SHALL/MUST constraints are testable
|
|
1812
|
+
- No circular dependencies between tasks`;
|
|
1813
|
+
var EXECUTOR_PROMPT = `## Role
|
|
1814
|
+
|
|
1815
|
+
You are a **Code Implementation Specialist** for specwf.
|
|
1816
|
+
|
|
1817
|
+
Your core responsibility is to implement code according to tasks.md, strictly following TDD protocol (RED\u2192GREEN\u2192REFACTOR), and ensuring each commit is atomic and verifiable.
|
|
1818
|
+
|
|
1819
|
+
- Execute tasks in strict order, never skipping any task
|
|
1820
|
+
- Follow TDD protocol: write failing test first, then implement, then refactor
|
|
1821
|
+
- Ensure each commit is an independent atomic change
|
|
1822
|
+
- Auto-fix bugs or missing code when discovered
|
|
1823
|
+
- Pause and ask when encountering architecture-level changes
|
|
1824
|
+
|
|
1825
|
+
## Core Constraints
|
|
1826
|
+
|
|
1827
|
+
- All artifacts written to the specwf/ directory
|
|
1828
|
+
- Use bash to invoke specwf CLI for state management
|
|
1829
|
+
- Respect project.yml context field
|
|
1830
|
+
- Follow conventions/ for project conventions
|
|
1831
|
+
- All output files use English
|
|
1832
|
+
|
|
1833
|
+
## Execution Flow
|
|
1834
|
+
|
|
1835
|
+
### Step 1: Read task list
|
|
1836
|
+
- Read tasks.md for current wave task list and order
|
|
1837
|
+
- Read design.md for technical approach
|
|
1838
|
+
- Read delta-specs for specification constraints
|
|
1839
|
+
|
|
1840
|
+
### Step 2: Execute by type
|
|
1841
|
+
|
|
1842
|
+
**type:behavior \u2192 TDD three-step protocol**
|
|
1843
|
+
1. **RED**: Write a failing test \u2014 test must be runnable and fail on assertion
|
|
1844
|
+
Commit: \`test(<scope>): RED - <description>\`
|
|
1845
|
+
2. **GREEN**: Write minimal implementation to pass the test \u2014 only what's needed
|
|
1846
|
+
Commit: \`feat(<scope>): GREEN - <description>\`
|
|
1847
|
+
3. **REFACTOR**: Improve code quality without changing behavior
|
|
1848
|
+
Commit: \`refactor(<scope>): REFACTOR - <description>\`
|
|
1849
|
+
|
|
1850
|
+
**type:config** \u2014 direct implementation, single commit: \`config(<scope>): <description>\`
|
|
1851
|
+
**type:refactor** \u2014 verify tests pass first, then refactor: \`refactor(<scope>): <description>\`
|
|
1852
|
+
**type:docs** \u2014 documentation update: \`docs(<scope>): <description>\`
|
|
1853
|
+
**type:scaffolding** \u2014 skeleton code: \`chore(<scope>): <description>\`
|
|
1854
|
+
|
|
1855
|
+
### Step 3: Per-task verification
|
|
1856
|
+
- Run related tests, confirm no regressions
|
|
1857
|
+
- Confirm delta-spec constraints are satisfied
|
|
1858
|
+
|
|
1859
|
+
### Step 4: Wave completion
|
|
1860
|
+
- Confirm all wave tasks complete
|
|
1861
|
+
- Run full test suite
|
|
1862
|
+
|
|
1863
|
+
## Deviation Rules
|
|
1864
|
+
|
|
1865
|
+
1. **auto-fix**: Auto-fix bugs discovered in code, annotate [auto-fix]
|
|
1866
|
+
2. **auto-add**: Auto-add missing helper code, annotate [auto-add]
|
|
1867
|
+
3. **auto-fix-blocking**: Attempt auto-fix for build/dependency issues up to 3 times, then pause
|
|
1868
|
+
4. **ask-architectural**: Pause and describe architectural changes for confirmation
|
|
1869
|
+
|
|
1870
|
+
**Analysis paralysis guard**: After 5 consecutive reads without a write, stop and diagnose what's blocking.
|
|
1871
|
+
|
|
1872
|
+
## Output
|
|
1873
|
+
- Code changes per tasks.md
|
|
1874
|
+
- Tests co-located with source files (*.test.ts)
|
|
1875
|
+
- Atomic git commits in Conventional Commits format
|
|
1876
|
+
|
|
1877
|
+
## Verification
|
|
1878
|
+
- All type:behavior tests pass (RED\u2192GREEN\u2192REFACTOR complete)
|
|
1879
|
+
- Implementation matches delta-spec SHALL/MUST
|
|
1880
|
+
- Each commit is atomic, commit messages conform to spec`;
|
|
1881
|
+
var REVIEWER_PROMPT = `## Role
|
|
1882
|
+
|
|
1883
|
+
You are a **Triple Review Specialist** for specwf.
|
|
1884
|
+
|
|
1885
|
+
Your orchestrator will assign you one of three roles: **spec-review**, **quality-review**, or **goal-review**. Execute only the assigned role.
|
|
1886
|
+
|
|
1887
|
+
## Core Constraints
|
|
1888
|
+
- All output files use English
|
|
1889
|
+
- Every finding must cite specific file:line references
|
|
1890
|
+
|
|
1891
|
+
## Role: spec-review
|
|
1892
|
+
Cross-reference delta-spec SHALL/MUST constraints against implementation:
|
|
1893
|
+
- Read delta-specs from specwf/changes/<change-name>/specs/
|
|
1894
|
+
- Use grep/ast_grep to find corresponding implementation
|
|
1895
|
+
- Annotate each constraint: PASS / FAIL / NOT_APPLICABLE with file:line
|
|
1896
|
+
- Check edge cases for each constraint
|
|
1897
|
+
- Output to specwf/changes/<change-name>/spec-review.md
|
|
1898
|
+
|
|
1899
|
+
## Role: quality-review
|
|
1900
|
+
Audit code for bugs, security, conventions, and AI mistakes:
|
|
1901
|
+
- Bug patterns: null pointer, resource leak, race condition, type error
|
|
1902
|
+
- Security: injection, XSS, auth bypass, sensitive data exposure
|
|
1903
|
+
- Conventions: naming, directory structure, import style vs conventions/
|
|
1904
|
+
- AI mistakes: hallucinated APIs, over-abstraction, missing error handling, hard-coded values
|
|
1905
|
+
- Severity: BLOCKER / MAJOR / MINOR / INFO
|
|
1906
|
+
- Output to specwf/changes/<change-name>/quality-review.md
|
|
1907
|
+
|
|
1908
|
+
## Role: goal-review
|
|
1909
|
+
Verify the change achieves what it promised:
|
|
1910
|
+
- Read proposal.md for goals and must_haves
|
|
1911
|
+
- Cross-reference each goal against implementation
|
|
1912
|
+
- Annotate: ACHIEVED / PARTIAL / NOT_ACHIEVED with evidence
|
|
1913
|
+
- Assess overall completeness
|
|
1914
|
+
- Output to specwf/changes/<change-name>/goal-review.md
|
|
1915
|
+
|
|
1916
|
+
## Output Format
|
|
1917
|
+
Every review report must include:
|
|
1918
|
+
- Overall verdict: PASS / FAIL / NEEDS_REVISION
|
|
1919
|
+
- Numbered findings with file:line references
|
|
1920
|
+
- NO_ISSUES_FOUND if nothing found (never leave a review blank)`;
|
|
1921
|
+
var VERIFIER_PROMPT = `## Role
|
|
1922
|
+
|
|
1923
|
+
You are a **Test Verification Specialist** for specwf.
|
|
1924
|
+
|
|
1925
|
+
Your core responsibility is to verify that implemented changes meet their goals. Run the full test suite, diagnose failures to root cause, and verify TDD commit integrity.
|
|
1926
|
+
|
|
1927
|
+
## Core Constraints
|
|
1928
|
+
|
|
1929
|
+
- All artifacts written to the specwf/ directory
|
|
1930
|
+
- All output files use English
|
|
1931
|
+
|
|
1932
|
+
## Execution Flow
|
|
1933
|
+
|
|
1934
|
+
### Step 1: Read context
|
|
1935
|
+
- Read delta-specs, tasks.md, review reports
|
|
1936
|
+
|
|
1937
|
+
### Step 2: Run test suite
|
|
1938
|
+
- Execute all tests, diagnose any failures to root cause
|
|
1939
|
+
|
|
1940
|
+
### Step 3: Verify coverage
|
|
1941
|
+
- Each delta-spec SHALL/MUST has a passing test
|
|
1942
|
+
- TDD commit integrity: RED\u2192GREEN\u2192REFACTOR sequence for type:behavior
|
|
1943
|
+
|
|
1944
|
+
### Step 4: Output verification.md
|
|
1945
|
+
Status: passed | gaps_found | human_needed
|
|
1946
|
+
|
|
1947
|
+
## Routing
|
|
1948
|
+
- passed \u2192 archive
|
|
1949
|
+
- gaps_found \u2192 reapply or replan
|
|
1950
|
+
- human_needed \u2192 surface to user with specific questions`;
|
|
1951
|
+
var ARCHIVER_PROMPT = `## Role
|
|
1952
|
+
|
|
1953
|
+
You are an **Archive Specialist** for specwf.
|
|
1954
|
+
|
|
1955
|
+
Your core responsibility is to merge delta-specs into global specs, run code cognition backfill, and move completed changes to the archive.
|
|
1956
|
+
|
|
1957
|
+
## Core Constraints
|
|
1958
|
+
|
|
1959
|
+
- All artifacts written to the specwf/ directory
|
|
1960
|
+
- All output files use English
|
|
1961
|
+
|
|
1962
|
+
## Execution Flow
|
|
1963
|
+
|
|
1964
|
+
### Step 1: Read context
|
|
1965
|
+
- Read the change directory and global specs/
|
|
1966
|
+
|
|
1967
|
+
### Step 2: Merge delta-specs
|
|
1968
|
+
- Merge changes/<name>/specs/ into global specs/
|
|
1969
|
+
- New specs append, modified specs update, removed specs archive
|
|
1970
|
+
|
|
1971
|
+
### Step 3: Code cognition backfill
|
|
1972
|
+
- Update context.md with learned patterns from this change
|
|
1973
|
+
|
|
1974
|
+
### Step 4: Move to archive
|
|
1975
|
+
- Move change to specwf/archive/<date>-<name>/
|
|
1976
|
+
- Update state.md: mark change as archived
|
|
1977
|
+
|
|
1978
|
+
## Guardrails
|
|
1979
|
+
- Delta-spec merge must resolve conflicts, not overwrite
|
|
1980
|
+
- Archived changes are never deleted \u2014 they form project decision history`;
|
|
1981
|
+
var RESEARCHER_PROMPT = `## Role
|
|
1982
|
+
|
|
1983
|
+
You are a **Technical Researcher** for specwf.
|
|
1984
|
+
|
|
1985
|
+
Your core responsibility is to investigate technical directions, compare alternatives, and produce structured research outputs.
|
|
1986
|
+
|
|
1987
|
+
## Core Constraints
|
|
1988
|
+
|
|
1989
|
+
- All artifacts written to the specwf/ directory
|
|
1990
|
+
- Read-only analysis \u2014 never modify source code
|
|
1991
|
+
- All output files use English
|
|
1992
|
+
|
|
1993
|
+
## Execution Flow
|
|
1994
|
+
|
|
1995
|
+
### Step 1: Read context
|
|
1996
|
+
- Read requirements.md for research scope
|
|
1997
|
+
- Read project.yml for technical constraints
|
|
1998
|
+
|
|
1999
|
+
### Step 2: Research
|
|
2000
|
+
- Compare at least 2 candidate solutions per direction
|
|
2001
|
+
- Assess feasibility, risk, and trade-offs
|
|
2002
|
+
- Produce a recommended approach with rationale
|
|
2003
|
+
|
|
2004
|
+
### Step 3: Output
|
|
2005
|
+
- stack.md \u2014 tech stack recommendations
|
|
2006
|
+
- architecture.md \u2014 architecture approach
|
|
2007
|
+
- pitfalls.md \u2014 known risks and mitigations
|
|
2008
|
+
|
|
2009
|
+
## Guardrails
|
|
2010
|
+
- Never recommend the first option found without comparison
|
|
2011
|
+
- Mark speculative findings with confidence levels`;
|
|
2012
|
+
var PHASE_RESEARCHER_PROMPT = `## Role
|
|
2013
|
+
|
|
2014
|
+
You are a **Phase Researcher** for specwf.
|
|
2015
|
+
|
|
2016
|
+
Your core responsibility is to investigate implementation paths for a specific phase, building on context.md decisions and parent project research.
|
|
2017
|
+
|
|
2018
|
+
## Core Constraints
|
|
2019
|
+
|
|
2020
|
+
- All artifacts written to the specwf/ directory
|
|
2021
|
+
- All output files use English
|
|
2022
|
+
|
|
2023
|
+
## Execution Flow
|
|
2024
|
+
|
|
2025
|
+
### Step 1: Read context
|
|
2026
|
+
- Read context.md for locked decisions and discretion areas
|
|
2027
|
+
- Read related specs/ for existing behavioral contracts
|
|
2028
|
+
|
|
2029
|
+
### Step 2: Research
|
|
2030
|
+
- Investigate concrete implementation approaches
|
|
2031
|
+
- Identify reusable patterns from existing codebase
|
|
2032
|
+
- Flag known pitfalls and edge cases
|
|
2033
|
+
|
|
2034
|
+
### Step 3: Output research.md
|
|
2035
|
+
- Recommended paths with rationale
|
|
2036
|
+
- Known pitfalls and TDD implications`;
|
|
2037
|
+
var CODEBASE_MAPPER_PROMPT = `## Role
|
|
2038
|
+
|
|
2039
|
+
You are a **Codebase Mapper** for specwf.
|
|
2040
|
+
|
|
2041
|
+
Your core responsibility is to analyze existing (brownfield) codebases and produce structured technical reports.
|
|
2042
|
+
|
|
2043
|
+
## Core Constraints
|
|
2044
|
+
|
|
2045
|
+
- Read-only analysis \u2014 never modify source code
|
|
2046
|
+
- All output files use English
|
|
2047
|
+
|
|
2048
|
+
## Execution Flow
|
|
2049
|
+
|
|
2050
|
+
### Step 1: Scan codebase
|
|
2051
|
+
- Analyze directory structure, package.json, config files
|
|
2052
|
+
- Identify tech stack, frameworks, and dependencies
|
|
2053
|
+
|
|
2054
|
+
### Step 2: Analyze architecture
|
|
2055
|
+
- Map module structure and dependencies
|
|
2056
|
+
- Identify architectural patterns in use
|
|
2057
|
+
|
|
2058
|
+
### Step 3: Extract conventions
|
|
2059
|
+
- Naming patterns, code style, directory conventions
|
|
2060
|
+
|
|
2061
|
+
### Step 4: Identify pitfalls
|
|
2062
|
+
- Anti-patterns, technical debt, risky areas
|
|
2063
|
+
|
|
2064
|
+
### Step 5: Output
|
|
2065
|
+
- codebase/stack.md, codebase/architecture.md
|
|
2066
|
+
- conventions/codebase-conventions.md
|
|
2067
|
+
- codebase/pitfalls.md`;
|
|
2068
|
+
var SPEC_BOOTSTRAPPER_PROMPT = `## Role
|
|
2069
|
+
|
|
2070
|
+
You are a **Spec Bootstrapper** for specwf.
|
|
2071
|
+
|
|
2072
|
+
Your core responsibility is to extract behavioral contracts from existing code \u2014 code signatures, comments, and tests \u2014 and produce initial spec files.
|
|
2073
|
+
|
|
2074
|
+
## Core Constraints
|
|
2075
|
+
|
|
2076
|
+
- Read-only analysis \u2014 never modify source code
|
|
2077
|
+
- All output files use English
|
|
2078
|
+
|
|
2079
|
+
## Execution Flow
|
|
2080
|
+
|
|
2081
|
+
### Step 1: Scan codebase
|
|
2082
|
+
- Scan src/ to identify core modules
|
|
2083
|
+
- Read function signatures, JSDoc comments, and existing tests
|
|
2084
|
+
|
|
2085
|
+
### Step 2: Extract behavioral contracts
|
|
2086
|
+
- Infer SHALL/MUST constraints from tests and signatures
|
|
2087
|
+
- Annotate with confidence levels (HIGH/MEDIUM/LOW)
|
|
2088
|
+
|
|
2089
|
+
### Step 3: Output specs/<domain>/spec.md
|
|
2090
|
+
- Mark all entries as BOOTSTRAPPED
|
|
2091
|
+
- Low-confidence entries flagged for human review`;
|
|
2092
|
+
var AGENT_PROMPTS = {
|
|
2093
|
+
planner: PLANNER_PROMPT,
|
|
2094
|
+
executor: EXECUTOR_PROMPT,
|
|
2095
|
+
reviewer: REVIEWER_PROMPT,
|
|
2096
|
+
verifier: VERIFIER_PROMPT,
|
|
2097
|
+
archiver: ARCHIVER_PROMPT,
|
|
2098
|
+
researcher: RESEARCHER_PROMPT,
|
|
2099
|
+
"phase-researcher": PHASE_RESEARCHER_PROMPT,
|
|
2100
|
+
"codebase-mapper": CODEBASE_MAPPER_PROMPT,
|
|
2101
|
+
"spec-bootstrapper": SPEC_BOOTSTRAPPER_PROMPT
|
|
2102
|
+
};
|
|
2103
|
+
|
|
2104
|
+
// src/integrations/omp/agents.ts
|
|
2105
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
2106
|
+
var AGENT_DEFS = [
|
|
2107
|
+
// specwf-researcher
|
|
2108
|
+
{
|
|
2109
|
+
role: "researcher",
|
|
2110
|
+
description: "Technical research \u2014 produce STACK/ARCH/PITFALLS/RESEARCH docs",
|
|
2111
|
+
tools: ["read", "grep", "glob", "lsp", "web_search", "write", "bash"],
|
|
2112
|
+
spawns: "*"
|
|
2113
|
+
},
|
|
2114
|
+
// specwf-planner
|
|
2115
|
+
{
|
|
2116
|
+
role: "planner",
|
|
2117
|
+
description: "Change design \u2014 produce proposal/design/tasks/delta-specs",
|
|
2118
|
+
tools: ["read", "grep", "glob", "lsp", "write", "bash"],
|
|
2119
|
+
spawns: "*"
|
|
2120
|
+
},
|
|
2121
|
+
// specwf-executor
|
|
2122
|
+
{
|
|
2123
|
+
role: "executor",
|
|
2124
|
+
description: "Code implementation \u2014 TDD RED\u2192GREEN\u2192REFACTOR",
|
|
2125
|
+
tools: ["read", "edit", "write", "bash", "grep", "glob", "lsp", "ast_grep", "ast_edit"],
|
|
2126
|
+
spawns: "*"
|
|
2127
|
+
},
|
|
2128
|
+
// specwf-reviewer
|
|
2129
|
+
{
|
|
2130
|
+
role: "reviewer",
|
|
2131
|
+
description: "Triple review \u2014 spec review + quality review + goal review",
|
|
2132
|
+
tools: ["read", "grep", "glob", "lsp", "ast_grep", "bash"],
|
|
2133
|
+
spawns: "*"
|
|
2134
|
+
},
|
|
2135
|
+
// specwf-verifier
|
|
2136
|
+
{
|
|
2137
|
+
role: "verifier",
|
|
2138
|
+
description: "Test verification \u2014 diagnose + route loopback",
|
|
2139
|
+
tools: ["read", "bash", "grep", "glob", "lsp", "edit", "write"],
|
|
2140
|
+
spawns: "*"
|
|
2141
|
+
},
|
|
2142
|
+
// specwf-archiver
|
|
2143
|
+
{
|
|
2144
|
+
role: "archiver",
|
|
2145
|
+
description: "Archive \u2014 delta-spec merge + code cognition backfill",
|
|
2146
|
+
tools: ["read", "write", "bash", "grep", "glob", "lsp"],
|
|
2147
|
+
spawns: "*"
|
|
2148
|
+
},
|
|
2149
|
+
// specwf-phase-researcher
|
|
2150
|
+
{
|
|
2151
|
+
role: "phase-researcher",
|
|
2152
|
+
description: "Phase research \u2014 produce RESEARCH.md for planner",
|
|
2153
|
+
tools: ["read", "grep", "glob", "lsp", "write", "bash"],
|
|
2154
|
+
spawns: "*"
|
|
2155
|
+
},
|
|
2156
|
+
// specwf-codebase-mapper + specwf-spec-bootstrapper (combined as aux agents)
|
|
2157
|
+
{
|
|
2158
|
+
role: "codebase-mapper",
|
|
2159
|
+
description: "Codebase mapping \u2014 analyze existing code, produce technical reports",
|
|
2160
|
+
tools: ["read", "grep", "glob", "lsp", "write", "bash"],
|
|
2161
|
+
spawns: "*"
|
|
2162
|
+
}
|
|
2163
|
+
];
|
|
2164
|
+
function resolveAgentModel(role, config) {
|
|
2165
|
+
return resolveModels(config)[role] ?? "default";
|
|
2166
|
+
}
|
|
2167
|
+
function resolveThinkingLevel(role) {
|
|
2168
|
+
const highThinkingRoles = ["planner", "researcher", "reviewer"];
|
|
2169
|
+
return highThinkingRoles.includes(role) ? "high" : "medium";
|
|
2170
|
+
}
|
|
2171
|
+
function generateAgent(def, model) {
|
|
2172
|
+
const thinkingLevel = resolveThinkingLevel(def.role);
|
|
2173
|
+
const body = AGENT_PROMPTS[def.role] ?? `# ${def.description}
|
|
2174
|
+
|
|
2175
|
+
Agent system prompt for specwf-${def.role}.`;
|
|
2176
|
+
return `---
|
|
2177
|
+
name: specwf-${def.role}
|
|
2178
|
+
description: ${def.description}
|
|
2179
|
+
tools:
|
|
2180
|
+
${def.tools.map((t) => ` - ${t}`).join("\n")}
|
|
2181
|
+
model: ${model}
|
|
2182
|
+
thinkingLevel: ${thinkingLevel}
|
|
2183
|
+
spawns: "${def.spawns}"
|
|
2184
|
+
blocking: false
|
|
2185
|
+
autoloadSkills: false
|
|
2186
|
+
readSummarize: true
|
|
2187
|
+
---
|
|
2188
|
+
|
|
2189
|
+
${body}
|
|
2190
|
+
`;
|
|
2191
|
+
}
|
|
2192
|
+
function generateAllAgents(config) {
|
|
2193
|
+
return AGENT_DEFS.map((def) => ({
|
|
2194
|
+
path: `.omp/agents/specwf-${def.role}.md`,
|
|
2195
|
+
content: generateAgent(def, resolveAgentModel(def.role, config))
|
|
2196
|
+
}));
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// src/integrations/omp/skills.ts
|
|
2200
|
+
function skillName(step) {
|
|
2201
|
+
return `specwf-${step}`;
|
|
2202
|
+
}
|
|
2203
|
+
function skillDescription(step) {
|
|
2204
|
+
const map = {
|
|
2205
|
+
init: "Initialize specwf project structure, generate platform files",
|
|
2206
|
+
grill: "Requirements exploration \u2014 detailed questioning until shared understanding is reached",
|
|
2207
|
+
research: "Project-level technical research \u2014 parallel multi-direction investigation",
|
|
2208
|
+
roadmap: "Roadmap definition \u2014 split project into Milestones \xD7 Phases",
|
|
2209
|
+
milestone: "Milestone management \u2014 switch/create milestones, set current phase",
|
|
2210
|
+
discuss: "Phase discussion \u2014 capture implementation decisions into context.md",
|
|
2211
|
+
"research-phase": "Phase research \u2014 implementation path investigation",
|
|
2212
|
+
split: "Change splitting \u2014 dependency graph + N changes",
|
|
2213
|
+
adhoc: "Create adhoc change \u2014 independent change unrelated to milestone/phase",
|
|
2214
|
+
plan: "Change design \u2014 technical design + task breakdown + delta-specs",
|
|
2215
|
+
apply: "Code implementation \u2014 TDD RED\u2192GREEN\u2192REFACTOR",
|
|
2216
|
+
review: "Triple review \u2014 spec review, quality review, goal review in parallel",
|
|
2217
|
+
verify: "Test verification \u2014 diagnose root cause + route loopback",
|
|
2218
|
+
archive: "Archive \u2014 delta-spec merge + code cognition backfill",
|
|
2219
|
+
ship: "Ship \u2014 create PR + update state / release tag",
|
|
2220
|
+
continue: "Auto-advance \u2014 read STATE and route to next step"
|
|
2221
|
+
};
|
|
2222
|
+
return map[step] ?? "";
|
|
2223
|
+
}
|
|
2224
|
+
var STEPS = ["init", "grill", "research", "roadmap", "milestone", "discuss", "research-phase", "split", "adhoc", "plan", "apply", "review", "verify", "archive", "ship", "continue"];
|
|
2225
|
+
var SKILL_DEFS = STEPS.map((step) => ({
|
|
2226
|
+
step,
|
|
2227
|
+
name: skillName(step),
|
|
2228
|
+
description: skillDescription(step)
|
|
2229
|
+
}));
|
|
2230
|
+
function generateSkill(def) {
|
|
2231
|
+
const entry = WORKFLOW_REGISTRY[def.step];
|
|
2232
|
+
const body = entry ? entry.skill().instructions : `# ${def.description}
|
|
2233
|
+
|
|
2234
|
+
Workflow guide for the \`${def.step}\` step.`;
|
|
2235
|
+
return `---
|
|
2236
|
+
name: ${def.name}
|
|
2237
|
+
description: ${def.description}
|
|
2238
|
+
hide: false
|
|
2239
|
+
---
|
|
2240
|
+
|
|
2241
|
+
${body}
|
|
2242
|
+
`;
|
|
2243
|
+
}
|
|
2244
|
+
function generateAllSkills(_config) {
|
|
2245
|
+
return SKILL_DEFS.map((def) => ({
|
|
2246
|
+
path: `.omp/skills/specwf-${def.step}/SKILL.md`,
|
|
2247
|
+
content: generateSkill(def)
|
|
2248
|
+
}));
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
// src/generators/index.ts
|
|
2252
|
+
function generateAll(config) {
|
|
2253
|
+
return [
|
|
2254
|
+
...generateAllCommands(config),
|
|
2255
|
+
...generateAllAgents(config),
|
|
2256
|
+
...generateAllSkills(config)
|
|
2257
|
+
];
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
// src/commands/_utils.ts
|
|
2261
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
2262
|
+
function writeGeneratedFiles(files) {
|
|
2263
|
+
for (const file of files) {
|
|
2264
|
+
const dir = file.path.split("/").slice(0, -1).join("/");
|
|
2265
|
+
if (dir) mkdirSync3(dir, { recursive: true });
|
|
2266
|
+
writeFileSync5(file.path, file.content, "utf-8");
|
|
2267
|
+
console.log(` \u2713 ${file.path}`);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
// src/commands/specwf-init.ts
|
|
2272
|
+
function register(program2) {
|
|
2273
|
+
program2.command("init").description("\u521D\u59CB\u5316 specwf \u9879\u76EE\u7ED3\u6784").option("--dir <path>", "\u76EE\u6807\u76EE\u5F55", ".").option("--profile <profile>", "\u5DE5\u4F5C\u6D41\u4E25\u683C\u5EA6 (lite|standard|strict)", "standard").option("--brownfield", "\u5B58\u91CF\u9879\u76EE\u6A21\u5F0F\uFF08codebase mapping + spec bootstrap\uFF09").option("--yes", "\u8DF3\u8FC7\u786E\u8BA4\u4F7F\u7528\u9ED8\u8BA4\u503C").action(initHandler);
|
|
2274
|
+
}
|
|
2275
|
+
async function initHandler(options) {
|
|
2276
|
+
const baseDir = options.dir.startsWith("/") ? options.dir : join6(process.cwd(), options.dir);
|
|
2277
|
+
const specwfDir = join6(baseDir, "specwf");
|
|
2278
|
+
if (isInitialized(specwfDir)) {
|
|
2279
|
+
console.error("specwf \u5DF2\u521D\u59CB\u5316\u3002\u8FD0\u884C `specwf update` \u66F4\u65B0\u5E73\u53F0\u6587\u4EF6\u3002");
|
|
2280
|
+
process.exit(1);
|
|
2281
|
+
}
|
|
2282
|
+
const wizard = await runInitWizard({ profile: options.profile, yes: options.yes });
|
|
2283
|
+
const profile = wizard.profile;
|
|
2284
|
+
const platform = wizard.platform;
|
|
2285
|
+
const isBrownfield = options.brownfield || wizard.brownfield;
|
|
2286
|
+
createSpecwfStructure(specwfDir);
|
|
2287
|
+
console.log("\u2713 \u521B\u5EFA specwf/ \u76EE\u5F55\u7ED3\u6784");
|
|
2288
|
+
saveConfig(specwfDir, {
|
|
2289
|
+
version: 1,
|
|
2290
|
+
platform,
|
|
2291
|
+
profile,
|
|
2292
|
+
context: wizard.context,
|
|
2293
|
+
workflow: {},
|
|
2294
|
+
review: {},
|
|
2295
|
+
change: {},
|
|
2296
|
+
git: { branching: "none", create_tag: true },
|
|
2297
|
+
conventions: { inject: true },
|
|
2298
|
+
models: {}
|
|
2299
|
+
});
|
|
2300
|
+
console.log("\u2713 \u521B\u5EFA project.yml (profile: " + profile + ")");
|
|
2301
|
+
saveState(specwfDir, {
|
|
2302
|
+
project: {
|
|
2303
|
+
name: baseDir.split("/").pop() || "project",
|
|
2304
|
+
status: "initialized",
|
|
2305
|
+
current_milestone: null,
|
|
2306
|
+
current_phase: null
|
|
2307
|
+
},
|
|
2308
|
+
active_context: {
|
|
2309
|
+
type: "project",
|
|
2310
|
+
ref: null,
|
|
2311
|
+
step: "init"
|
|
2312
|
+
},
|
|
2313
|
+
changes: [],
|
|
2314
|
+
adhoc: []
|
|
2315
|
+
});
|
|
2316
|
+
console.log("\u2713 \u521B\u5EFA state.md");
|
|
2317
|
+
if (isBrownfield) {
|
|
2318
|
+
const info = detectProjectInfo(process.cwd());
|
|
2319
|
+
const domains = await runBrownfieldInit(process.cwd(), specwfDir, info);
|
|
2320
|
+
console.log("\u2713 \u5DF2\u626B\u63CF\u9879\u76EE\u7ED3\u6784\u3002\u8BF7\u6D3E\u53D1 specwf-codebase-mapper \u548C specwf-spec-bootstrapper \u5B50\u4EE3\u7406\u5B8C\u6210\u5B8C\u6574\u5206\u6790\u3002");
|
|
2321
|
+
}
|
|
2322
|
+
console.log("specwf \u521D\u59CB\u5316\u5B8C\u6210\u3002");
|
|
2323
|
+
try {
|
|
2324
|
+
const files = generateAll({ version: 1, platform, profile, context: wizard.context, workflow: {}, review: {}, change: {}, git: { branching: "none", create_tag: true }, conventions: { inject: true }, models: {} });
|
|
2325
|
+
writeGeneratedFiles(files);
|
|
2326
|
+
console.log(`\u2713 \u5E73\u53F0\u6587\u4EF6\u5DF2\u751F\u6210 (${files.length} \u4E2A)`);
|
|
2327
|
+
} catch {
|
|
2328
|
+
console.log("\u26A0 \u5E73\u53F0\u6587\u4EF6\u751F\u6210\u5931\u8D25\uFF0C\u53EF\u7A0D\u540E\u8FD0\u884C `specwf update` \u91CD\u8BD5");
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// src/commands/specwf-update.ts
|
|
2333
|
+
import { join as join7 } from "path";
|
|
2334
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
2335
|
+
var HOOK_TEMPLATE = `/**
|
|
2336
|
+
* specwf OMP Hook \u2014 automated spec injection and workflow guidance.
|
|
2337
|
+
* Generated by specwf update. Do not edit manually.
|
|
2338
|
+
*/
|
|
2339
|
+
import { existsSync } from "node:fs";
|
|
2340
|
+
import { join } from "node:path";
|
|
2341
|
+
import { execSync } from "node:child_process";
|
|
2342
|
+
|
|
2343
|
+
export default function specwfHook(api: any): void {
|
|
2344
|
+
api.on("session_start", async (_event: any, ctx: any) => {
|
|
2345
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
2346
|
+
if (!existsSync(join(cwd, "specwf", "state.md"))) return;
|
|
2347
|
+
try {
|
|
2348
|
+
const output = execSync("specwf context current", { cwd, encoding: "utf-8", timeout: 5000 });
|
|
2349
|
+
const data = JSON.parse(output);
|
|
2350
|
+
const lines: string[] = [];
|
|
2351
|
+
lines.push(\`[specwf] Project: \${data.project} | Status: \${data.status}\`);
|
|
2352
|
+
if (data.specs?.length) {
|
|
2353
|
+
lines.push("\\n\u2500\u2500\u2500 Specs \u2500\u2500\u2500");
|
|
2354
|
+
for (const spec of data.specs) {
|
|
2355
|
+
if (spec.content) lines.push(\`\\n### \${spec.path}\\n\${spec.content.slice(0, 4096)}\`);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
if (data.conventions?.length) {
|
|
2359
|
+
lines.push("\\n\u2500\u2500\u2500 Conventions \u2500\u2500\u2500");
|
|
2360
|
+
for (const conv of data.conventions) {
|
|
2361
|
+
if (conv.content) lines.push(\`\\n### \${conv.path}\\n\${conv.content.slice(0, 2048)}\`);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
api.sendMessage({ role: "custom", customType: "specwf-session", content: [{ type: "text", text: lines.join("\\n") }], timestamp: Date.now() });
|
|
2365
|
+
} catch { /* specwf CLI not available */ }
|
|
2366
|
+
});
|
|
2367
|
+
|
|
2368
|
+
// \u2500\u2500 Quality gate: warn on test/lint/typecheck failures \u2500\u2500
|
|
2369
|
+
api.on("tool_result", async (event: any) => {
|
|
2370
|
+
if (event.isError || event.toolName !== "bash") return;
|
|
2371
|
+
const cmd = String(event.input?.command ?? "");
|
|
2372
|
+
const isCheck = /\b(vitest|jest|tsc\b|typecheck|eslint|prettier.*--check)\b/.test(cmd);
|
|
2373
|
+
if (!isCheck) return;
|
|
2374
|
+
const text = String(event.content?.[0]?.text ?? "");
|
|
2375
|
+
const m = text.match(/exited with code (d+)/i);
|
|
2376
|
+
if (m && parseInt(m[1]) !== 0) {
|
|
2377
|
+
api.sendMessage({ role: "custom", customType: "specwf-quality-gate", content: [{ type: "text", text: \`[specwf] Quality gate FAILED: \${cmd.trim().split("\\n")[0]}\\nFix failures before running specwf continue.\` }], timestamp: Date.now() }, { deliverAs: "followUp" });
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
|
|
2381
|
+
api.on("before_agent_start", async (_event: any, ctx: any) => {
|
|
2382
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
2383
|
+
if (!existsSync(join(cwd, "specwf", "state.md"))) return;
|
|
2384
|
+
try {
|
|
2385
|
+
const output = execSync("specwf state", { cwd, encoding: "utf-8", timeout: 3000 });
|
|
2386
|
+
const state = JSON.parse(output);
|
|
2387
|
+
const pending = state.pending || [];
|
|
2388
|
+
const hint = pending.length > 0
|
|
2389
|
+
? \`[specwf] Pending: \${pending.map((p: any) => \`\${p.name}[\${p.status}]\`).join(", ")}. Run specwf continue.\`
|
|
2390
|
+
: \`[specwf] Status: \${state.status}. Run specwf continue.\`;
|
|
2391
|
+
ctx.ui?.setStatus?.("specwf", hint);
|
|
2392
|
+
} catch { /* skip */ }
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
`;
|
|
2396
|
+
function register2(program2) {
|
|
2397
|
+
program2.command("update").description("Regenerate platform files (commands + agents + skills + hooks)").option("--dir <path>", "specwf directory", "specwf").action(updateHandler);
|
|
2398
|
+
}
|
|
2399
|
+
function updateHandler(options) {
|
|
2400
|
+
const specwfDir = join7(process.cwd(), options.dir);
|
|
2401
|
+
const config = loadConfig(specwfDir);
|
|
2402
|
+
const files = generateAll(config);
|
|
2403
|
+
console.log("Regenerating platform files...");
|
|
2404
|
+
writeGeneratedFiles(files);
|
|
2405
|
+
const hookDir = join7(process.cwd(), ".omp", "hooks", "pre");
|
|
2406
|
+
mkdirSync4(hookDir, { recursive: true });
|
|
2407
|
+
writeFileSync6(join7(hookDir, "specwf.ts"), HOOK_TEMPLATE, "utf-8");
|
|
2408
|
+
console.log(" \u2713 .omp/hooks/pre/specwf.ts");
|
|
2409
|
+
console.log(`\u2713 Update complete (${files.length + 1} files)`);
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// src/commands/specwf-config.ts
|
|
2413
|
+
import { join as join8 } from "path";
|
|
2414
|
+
function register3(program2) {
|
|
2415
|
+
const cmd = program2.command("config").description("\u67E5\u770B/\u4FEE\u6539\u914D\u7F6E\u9879\u76EE");
|
|
2416
|
+
cmd.command("list").description("\u67E5\u770B\u5F53\u524D\u914D\u7F6E").action(configList);
|
|
2417
|
+
cmd.command("set <key> <value>").description("\u4FEE\u6539\u914D\u7F6E\u9879").action(configSet);
|
|
2418
|
+
cmd.action(configList);
|
|
2419
|
+
}
|
|
2420
|
+
function configList(options, cmd) {
|
|
2421
|
+
if (cmd?.parent?.args?.length > 1) return;
|
|
2422
|
+
const specwfDir = findSpecwfDir();
|
|
2423
|
+
const config = loadConfig(specwfDir);
|
|
2424
|
+
console.log(JSON.stringify(config, null, 2));
|
|
2425
|
+
}
|
|
2426
|
+
function configSet(key, value) {
|
|
2427
|
+
const specwfDir = findSpecwfDir();
|
|
2428
|
+
updateConfig(specwfDir, (config) => {
|
|
2429
|
+
const parts = key.split(".");
|
|
2430
|
+
let target = config;
|
|
2431
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
2432
|
+
if (!target[parts[i]]) target[parts[i]] = {};
|
|
2433
|
+
target = target[parts[i]];
|
|
2434
|
+
}
|
|
2435
|
+
const lastKey = parts[parts.length - 1];
|
|
2436
|
+
const typedValue = parseTypedValue(value);
|
|
2437
|
+
target[lastKey] = typedValue;
|
|
2438
|
+
});
|
|
2439
|
+
console.log(`\u2713 ${key} = ${value}`);
|
|
2440
|
+
}
|
|
2441
|
+
function parseTypedValue(value) {
|
|
2442
|
+
if (value === "true") return true;
|
|
2443
|
+
if (value === "false") return false;
|
|
2444
|
+
if (/^\d+$/.test(value)) return parseInt(value, 10);
|
|
2445
|
+
if (value === "null") return null;
|
|
2446
|
+
return value;
|
|
2447
|
+
}
|
|
2448
|
+
function findSpecwfDir() {
|
|
2449
|
+
return join8(process.cwd(), "specwf");
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
// src/commands/specwf-state.ts
|
|
2453
|
+
import { join as join9 } from "path";
|
|
2454
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
|
|
2455
|
+
function register4(program2) {
|
|
2456
|
+
const cmd = program2.command("state").description("View/modify current state");
|
|
2457
|
+
cmd.command("show").description("Show current state as JSON").action(showState);
|
|
2458
|
+
cmd.command("set-milestone <id>").description("Switch to specified milestone").action(setMilestone);
|
|
2459
|
+
cmd.command("set-phase <id>").description("Switch to specified phase").action(setPhase);
|
|
2460
|
+
cmd.command("set-step <step>").description("Set current step").action(setStep);
|
|
2461
|
+
cmd.action(showState);
|
|
2462
|
+
}
|
|
2463
|
+
function findSpecwfDir2() {
|
|
2464
|
+
return join9(process.cwd(), "specwf");
|
|
2465
|
+
}
|
|
2466
|
+
function showState() {
|
|
2467
|
+
const specwfDir = findSpecwfDir2();
|
|
2468
|
+
const state = loadState(specwfDir);
|
|
2469
|
+
const { project, active_context } = state;
|
|
2470
|
+
const pendingChanges = state.changes.filter((c) => c.status !== "archived");
|
|
2471
|
+
const pendingAdhoc = state.adhoc.filter((c) => c.status !== "archived");
|
|
2472
|
+
const withProgress = (items, type) => items.map((c) => {
|
|
2473
|
+
const tasksPath = join9(specwfDir, "changes", c.name, "tasks.md");
|
|
2474
|
+
let tasks = null;
|
|
2475
|
+
try {
|
|
2476
|
+
const content = readFileSync6(tasksPath, "utf-8");
|
|
2477
|
+
const total = (content.match(/^\s*-\s*\[[ x]\]/gm) || []).length;
|
|
2478
|
+
const completed = (content.match(/^\s*-\s*\[x\]/gm) || []).length;
|
|
2479
|
+
if (total > 0) tasks = { total, completed };
|
|
2480
|
+
} catch {
|
|
2481
|
+
}
|
|
2482
|
+
return { type, name: c.name, status: c.status, tasks };
|
|
2483
|
+
});
|
|
2484
|
+
let roadmap = null;
|
|
2485
|
+
try {
|
|
2486
|
+
const roadmapPath = join9(specwfDir, "roadmap.md");
|
|
2487
|
+
if (existsSync6(roadmapPath)) {
|
|
2488
|
+
const content = readFileSync6(roadmapPath, "utf-8");
|
|
2489
|
+
const msMatches = content.match(/^##\s+(M\d+[^\n]*)/gm);
|
|
2490
|
+
if (msMatches) {
|
|
2491
|
+
roadmap = msMatches.map((m) => {
|
|
2492
|
+
const name = m.replace(/^##\s+/, "").trim();
|
|
2493
|
+
const id = name.split(/\s*[-–—]\s*/)[0].trim();
|
|
2494
|
+
const isActive = project.current_milestone === id;
|
|
2495
|
+
return { name, id, active: isActive };
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
} catch {
|
|
2500
|
+
}
|
|
2501
|
+
const output = {
|
|
2502
|
+
project: project.name,
|
|
2503
|
+
status: project.status,
|
|
2504
|
+
milestone: project.current_milestone,
|
|
2505
|
+
phase: project.current_phase,
|
|
2506
|
+
context: {
|
|
2507
|
+
type: active_context.type,
|
|
2508
|
+
step: active_context.step,
|
|
2509
|
+
ref: active_context.ref || null
|
|
2510
|
+
},
|
|
2511
|
+
pending: withProgress(pendingChanges, "change").concat(withProgress(pendingAdhoc, "adhoc"))
|
|
2512
|
+
};
|
|
2513
|
+
if (roadmap) output.roadmap = roadmap;
|
|
2514
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2515
|
+
}
|
|
2516
|
+
function setMilestone(id) {
|
|
2517
|
+
const specwfDir = findSpecwfDir2();
|
|
2518
|
+
updateState(specwfDir, (state) => {
|
|
2519
|
+
const current = state.project.current_milestone;
|
|
2520
|
+
if (current && state.project.status !== "milestone-shipped") {
|
|
2521
|
+
archiveMilestoneDir(specwfDir, current);
|
|
2522
|
+
}
|
|
2523
|
+
state.project.current_milestone = id;
|
|
2524
|
+
state.project.current_phase = null;
|
|
2525
|
+
state.active_context = { type: "milestone", ref: `milestones/${id}`, step: "active" };
|
|
2526
|
+
});
|
|
2527
|
+
console.log(JSON.stringify({ ok: true, milestone: id }));
|
|
2528
|
+
}
|
|
2529
|
+
function setPhase(id) {
|
|
2530
|
+
const specwfDir = findSpecwfDir2();
|
|
2531
|
+
updateState(specwfDir, (state) => {
|
|
2532
|
+
state.project.current_phase = id;
|
|
2533
|
+
state.active_context = { type: "phase", ref: `milestones/${state.project.current_milestone}/phases/${id}`, step: "discuss" };
|
|
2534
|
+
});
|
|
2535
|
+
console.log(JSON.stringify({ ok: true, phase: id }));
|
|
2536
|
+
}
|
|
2537
|
+
function setStep(step) {
|
|
2538
|
+
const specwfDir = findSpecwfDir2();
|
|
2539
|
+
updateState(specwfDir, (state) => {
|
|
2540
|
+
state.active_context.step = step;
|
|
2541
|
+
});
|
|
2542
|
+
console.log(JSON.stringify({ ok: true, step }));
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// src/commands/specwf-context.ts
|
|
2546
|
+
import { join as join11 } from "path";
|
|
2547
|
+
|
|
2548
|
+
// src/core/spec-injector.ts
|
|
2549
|
+
import { join as join10 } from "path";
|
|
2550
|
+
import { readdirSync as readdirSync3, existsSync as existsSync7, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
2551
|
+
var PROJECT_STEPS = ["init", "grill", "research", "roadmap"];
|
|
2552
|
+
var PHASE_STEPS = ["discuss", "research-phase", "split"];
|
|
2553
|
+
var CHANGE_STEPS = ["plan", "apply", "review", "verify", "archive"];
|
|
2554
|
+
function isProjectStep(step) {
|
|
2555
|
+
return PROJECT_STEPS.includes(step);
|
|
2556
|
+
}
|
|
2557
|
+
function isPhaseStep(step) {
|
|
2558
|
+
return PHASE_STEPS.includes(step);
|
|
2559
|
+
}
|
|
2560
|
+
function isChangeStep(step) {
|
|
2561
|
+
return CHANGE_STEPS.includes(step);
|
|
2562
|
+
}
|
|
2563
|
+
function generateContext(specwfDir, step) {
|
|
2564
|
+
const state = loadState(specwfDir);
|
|
2565
|
+
const ctx = state.active_context;
|
|
2566
|
+
const result = {
|
|
2567
|
+
step,
|
|
2568
|
+
scope: { type: ctx.type, ref: ctx.ref },
|
|
2569
|
+
specs: [],
|
|
2570
|
+
conventions: [],
|
|
2571
|
+
changeArtifacts: [],
|
|
2572
|
+
requirements: []
|
|
2573
|
+
};
|
|
2574
|
+
result.conventions = getAllConventions(specwfDir).map(withContent(specwfDir));
|
|
2575
|
+
if (existsSync7(join10(specwfDir, "requirements.md"))) {
|
|
2576
|
+
const reqPath = join10(specwfDir, "requirements.md");
|
|
2577
|
+
result.requirements.push({
|
|
2578
|
+
path: "requirements.md",
|
|
2579
|
+
description: "Requirements specification",
|
|
2580
|
+
content: readContent(reqPath)
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
if (isProjectStep(step)) {
|
|
2584
|
+
result.specs = getAllSpecs(specwfDir).map(withContent(specwfDir));
|
|
2585
|
+
} else if (isPhaseStep(step)) {
|
|
2586
|
+
result.specs = getRelatedSpecs(specwfDir, state).map(withContent(specwfDir));
|
|
2587
|
+
} else if (isChangeStep(step)) {
|
|
2588
|
+
result.specs = getRelatedSpecs(specwfDir, state).map(withContent(specwfDir));
|
|
2589
|
+
result.changeArtifacts = getChangeArtifacts(specwfDir, state).map(withContent(specwfDir));
|
|
2590
|
+
}
|
|
2591
|
+
return result;
|
|
2592
|
+
}
|
|
2593
|
+
function readContent(absPath) {
|
|
2594
|
+
try {
|
|
2595
|
+
const content = readFileSync7(absPath, "utf-8");
|
|
2596
|
+
return content.length > 8192 ? content.slice(0, 8192) + "\n... [truncated]" : content;
|
|
2597
|
+
} catch {
|
|
2598
|
+
return void 0;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
function withContent(specwfDir) {
|
|
2602
|
+
return (ref) => ({
|
|
2603
|
+
...ref,
|
|
2604
|
+
content: readContent(join10(specwfDir, ref.path))
|
|
2605
|
+
});
|
|
2606
|
+
}
|
|
2607
|
+
function getAllSpecs(specwfDir) {
|
|
2608
|
+
const specsDir = join10(specwfDir, "specs");
|
|
2609
|
+
return listSpecFiles(specsDir, "specs");
|
|
2610
|
+
}
|
|
2611
|
+
function getRelatedSpecs(specwfDir, state) {
|
|
2612
|
+
const allSpecs = getAllSpecs(specwfDir);
|
|
2613
|
+
if (allSpecs.length === 0) return [];
|
|
2614
|
+
const ref = state.active_context.ref ?? "";
|
|
2615
|
+
const changeName = ref.split("/").pop() ?? "";
|
|
2616
|
+
const related = allSpecs.filter((spec) => {
|
|
2617
|
+
const domain = spec.path.split("/")[1] ?? "";
|
|
2618
|
+
return changeName.toLowerCase().includes(domain.toLowerCase());
|
|
2619
|
+
});
|
|
2620
|
+
return related.length > 0 ? related : allSpecs;
|
|
2621
|
+
}
|
|
2622
|
+
function getAllConventions(specwfDir) {
|
|
2623
|
+
const convDir = join10(specwfDir, "conventions");
|
|
2624
|
+
if (!existsSync7(convDir)) return [];
|
|
2625
|
+
return readdirSync3(convDir).filter((f) => f.endsWith(".md")).map((f) => ({ path: `conventions/${f}`, description: "\u9879\u76EE\u7EA6\u5B9A" }));
|
|
2626
|
+
}
|
|
2627
|
+
function getChangeArtifacts(specwfDir, state) {
|
|
2628
|
+
const ref = state.active_context.ref;
|
|
2629
|
+
if (!ref) return [];
|
|
2630
|
+
const changeDir = join10(specwfDir, ref);
|
|
2631
|
+
if (!existsSync7(changeDir)) return [];
|
|
2632
|
+
const artifacts = [];
|
|
2633
|
+
for (const file of ["proposal.md", "design.md", "tasks.md", ".specwf.yaml"]) {
|
|
2634
|
+
const fullPath = join10(changeDir, file);
|
|
2635
|
+
if (existsSync7(fullPath)) {
|
|
2636
|
+
artifacts.push({ path: `${ref}/${file}`, description: "change \u4EA7\u7269" });
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
const specsDir = join10(changeDir, "specs");
|
|
2640
|
+
if (existsSync7(specsDir)) {
|
|
2641
|
+
const deltaSpecs = listSpecFiles(specsDir, `${ref}/specs`);
|
|
2642
|
+
artifacts.push(...deltaSpecs);
|
|
2643
|
+
}
|
|
2644
|
+
return artifacts;
|
|
2645
|
+
}
|
|
2646
|
+
function listSpecFiles(dir, prefix) {
|
|
2647
|
+
if (!existsSync7(dir)) return [];
|
|
2648
|
+
const results = [];
|
|
2649
|
+
for (const entry of readdirSync3(dir)) {
|
|
2650
|
+
const fullPath = join10(dir, entry);
|
|
2651
|
+
const stat = statSync2(fullPath);
|
|
2652
|
+
if (stat.isDirectory()) {
|
|
2653
|
+
results.push(...listSpecFiles(fullPath, `${prefix}/${entry}`));
|
|
2654
|
+
} else if (entry.endsWith(".md")) {
|
|
2655
|
+
results.push({ path: `${prefix}/${entry}`, description: "\u884C\u4E3A\u5951\u7EA6" });
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
return results;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
// src/commands/specwf-context.ts
|
|
2662
|
+
function register5(program2) {
|
|
2663
|
+
program2.command("context <step>").description("Output state + file manifest as JSON").action(contextHandler);
|
|
2664
|
+
}
|
|
2665
|
+
function contextHandler(step) {
|
|
2666
|
+
const specwfDir = join11(process.cwd(), "specwf");
|
|
2667
|
+
const state = loadState(specwfDir);
|
|
2668
|
+
const { project, active_context } = state;
|
|
2669
|
+
const pendingChanges = state.changes.filter((c) => c.status !== "archived");
|
|
2670
|
+
const pendingAdhoc = state.adhoc.filter((c) => c.status !== "archived");
|
|
2671
|
+
const result = generateContext(specwfDir, step);
|
|
2672
|
+
const output = {
|
|
2673
|
+
project: project.name,
|
|
2674
|
+
status: project.status,
|
|
2675
|
+
milestone: project.current_milestone,
|
|
2676
|
+
phase: project.current_phase,
|
|
2677
|
+
context: {
|
|
2678
|
+
type: active_context.type,
|
|
2679
|
+
step: active_context.step,
|
|
2680
|
+
ref: active_context.ref || null
|
|
2681
|
+
},
|
|
2682
|
+
pending: [
|
|
2683
|
+
...pendingChanges.map((c) => ({ type: "change", name: c.name, status: c.status })),
|
|
2684
|
+
...pendingAdhoc.map((c) => ({ type: "adhoc", name: c.name, status: c.status }))
|
|
2685
|
+
],
|
|
2686
|
+
specs: result.specs,
|
|
2687
|
+
conventions: result.conventions,
|
|
2688
|
+
artifacts: result.changeArtifacts,
|
|
2689
|
+
requirements: result.requirements
|
|
2690
|
+
};
|
|
2691
|
+
console.log(JSON.stringify(output));
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
// src/commands/specwf-continue.ts
|
|
2695
|
+
import { join as join13 } from "path";
|
|
2696
|
+
|
|
2697
|
+
// src/types/state.ts
|
|
2698
|
+
var STATE_TRANSITIONS = [
|
|
2699
|
+
// 项目层路径
|
|
2700
|
+
{ from: "initialized", command: "grill", to: "requirements-defined", slashCommand: "/specwf:grill" },
|
|
2701
|
+
{ from: "requirements-defined", command: "research", to: "researching", slashCommand: "/specwf:research", subagent: true },
|
|
2702
|
+
{ from: "researching", command: "research-done", to: "researched", slashCommand: "" },
|
|
2703
|
+
{ from: "researched", command: "roadmap", to: "roadmap-defined", slashCommand: "/specwf:roadmap" },
|
|
2704
|
+
{ from: "roadmap-defined", command: "discuss", to: "phase-discuss", slashCommand: "/specwf:discuss" },
|
|
2705
|
+
// Phase 路径
|
|
2706
|
+
{ from: "phase-discuss", command: "research-phase", to: "phase-research", slashCommand: "/specwf:research-phase", subagent: true },
|
|
2707
|
+
{ from: "phase-research", command: "split", to: "phase-split", slashCommand: "/specwf:split" },
|
|
2708
|
+
{ from: "phase-split", command: "plan", to: "change-planning", slashCommand: "/specwf:plan", subagent: true },
|
|
2709
|
+
{ from: "change-planning", command: "apply", to: "change-applying", slashCommand: "/specwf:apply", subagent: true },
|
|
2710
|
+
{ from: "change-applying", command: "review", to: "change-reviewing", slashCommand: "/specwf:review", subagent: true },
|
|
2711
|
+
{ from: "change-reviewing", command: "verify", to: "change-verifying", slashCommand: "/specwf:verify", subagent: true },
|
|
2712
|
+
{ from: "change-verifying", command: "archive", to: "change-archiving", slashCommand: "/specwf:archive", subagent: true },
|
|
2713
|
+
{ from: "change-archiving", command: "archive-done", to: "change-archived", slashCommand: "" },
|
|
2714
|
+
// 回环
|
|
2715
|
+
{ from: "change-verifying", command: "replan", to: "change-planning", slashCommand: "/specwf:plan", subagent: true },
|
|
2716
|
+
{ from: "change-verifying", command: "reapply", to: "change-applying", slashCommand: "/specwf:apply", subagent: true },
|
|
2717
|
+
{ from: "change-reviewing", command: "fix", to: "change-applying", slashCommand: "/specwf:apply", subagent: true },
|
|
2718
|
+
// Milestone 层(新里程碑 = 项目流程 - init)
|
|
2719
|
+
{ from: "milestone-active", command: "grill", to: "requirements-defined", slashCommand: "/specwf:grill" },
|
|
2720
|
+
// Ship
|
|
2721
|
+
{ from: "change-archived", command: "ship-phase", to: "phase-shipped", slashCommand: "/specwf:ship" },
|
|
2722
|
+
{ from: "phase-shipped", command: "next-phase", to: "phase-discuss", slashCommand: "/specwf:discuss" },
|
|
2723
|
+
{ from: "phase-shipped", command: "ship-milestone", to: "milestone-shipped", slashCommand: "/specwf:ship" },
|
|
2724
|
+
// 临时 change
|
|
2725
|
+
{ from: "adhoc-proposal", command: "plan", to: "change-planning", slashCommand: "/specwf:plan", subagent: true },
|
|
2726
|
+
{ from: "change-archived", command: "adhoc-done", to: "adhoc-archived", slashCommand: "" },
|
|
2727
|
+
{ from: "adhoc-archived", command: "new-change", to: "adhoc-proposal", slashCommand: "" }
|
|
2728
|
+
];
|
|
2729
|
+
|
|
2730
|
+
// src/core/state-machine.ts
|
|
2731
|
+
function getTransition(from, command) {
|
|
2732
|
+
return STATE_TRANSITIONS.find(
|
|
2733
|
+
(t) => t.from === from && t.command === command
|
|
2734
|
+
) ?? null;
|
|
2735
|
+
}
|
|
2736
|
+
function getNextSteps(from) {
|
|
2737
|
+
return STATE_TRANSITIONS.filter((t) => t.from === from);
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
// src/core/continue.ts
|
|
2741
|
+
function determineNextStep(specwfDir) {
|
|
2742
|
+
return determineFromState(loadState(specwfDir));
|
|
2743
|
+
}
|
|
2744
|
+
function determineChangeNextStep(specwfDir, changeName) {
|
|
2745
|
+
const state = loadState(specwfDir);
|
|
2746
|
+
const change = state.changes.find((c) => c.name === changeName);
|
|
2747
|
+
if (change) {
|
|
2748
|
+
return determineFromChangeStatus(changeName, `change-${change.status}`, "change");
|
|
2749
|
+
}
|
|
2750
|
+
const adhoc = state.adhoc.find((c) => c.name === changeName);
|
|
2751
|
+
if (adhoc) {
|
|
2752
|
+
const prefix = adhoc.status === "proposal" ? "adhoc" : "change";
|
|
2753
|
+
return determineFromChangeStatus(
|
|
2754
|
+
changeName,
|
|
2755
|
+
`${prefix}-${adhoc.status}`,
|
|
2756
|
+
"adhoc"
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
return {
|
|
2760
|
+
error: `change \u4E0D\u5B58\u5728: ${changeName}\u3002\u53EF\u7528: ${listAvailableChanges(state)}`
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
var STEP_INFO = {
|
|
2764
|
+
grill: {
|
|
2765
|
+
command: "grill",
|
|
2766
|
+
description: "Requirements exploration \u2014 5W1H questioning, output requirements.md",
|
|
2767
|
+
artifacts: ["specwf/requirements.md"],
|
|
2768
|
+
fileRef: ""
|
|
2769
|
+
},
|
|
2770
|
+
research: {
|
|
2771
|
+
command: "research",
|
|
2772
|
+
description: "Parallel technical research \u2014 dispatch researcher sub-agents",
|
|
2773
|
+
artifacts: ["specwf/research/stack.md", "specwf/research/architecture.md", "specwf/research/pitfalls.md", "specwf/research/summary.md"],
|
|
2774
|
+
fileRef: ""
|
|
2775
|
+
},
|
|
2776
|
+
"research-done": {
|
|
2777
|
+
command: "research-done",
|
|
2778
|
+
description: "Mark research complete, proceed to roadmap",
|
|
2779
|
+
artifacts: [],
|
|
2780
|
+
fileRef: ""
|
|
2781
|
+
},
|
|
2782
|
+
roadmap: {
|
|
2783
|
+
command: "roadmap",
|
|
2784
|
+
description: "Split project into Milestones x Phases",
|
|
2785
|
+
artifacts: ["specwf/roadmap.md"],
|
|
2786
|
+
fileRef: ""
|
|
2787
|
+
},
|
|
2788
|
+
discuss: {
|
|
2789
|
+
command: "discuss",
|
|
2790
|
+
description: "Phase discussion \u2014 capture implementation decisions into context.md",
|
|
2791
|
+
artifacts: ["context.md"],
|
|
2792
|
+
fileRef: ""
|
|
2793
|
+
},
|
|
2794
|
+
"research-phase": {
|
|
2795
|
+
command: "research-phase",
|
|
2796
|
+
description: "Phase-level technical research \u2014 dispatch phase-researcher sub-agent",
|
|
2797
|
+
artifacts: ["research.md"],
|
|
2798
|
+
fileRef: ""
|
|
2799
|
+
},
|
|
2800
|
+
split: {
|
|
2801
|
+
command: "split",
|
|
2802
|
+
description: "Split phase into changes with dependency graph",
|
|
2803
|
+
artifacts: ["specwf/changes/<name>/"],
|
|
2804
|
+
fileRef: ""
|
|
2805
|
+
},
|
|
2806
|
+
plan: {
|
|
2807
|
+
command: "plan",
|
|
2808
|
+
description: "Change design \u2014 dispatch planner sub-agent for design + tasks + delta-specs",
|
|
2809
|
+
artifacts: ["design.md", "tasks.md", "specs/<domain>/spec.md"],
|
|
2810
|
+
fileRef: ""
|
|
2811
|
+
},
|
|
2812
|
+
apply: {
|
|
2813
|
+
command: "apply",
|
|
2814
|
+
description: "Code implementation \u2014 dispatch executor sub-agent for TDD",
|
|
2815
|
+
artifacts: ["code changes", "tests", "change-summary.md"],
|
|
2816
|
+
fileRef: ""
|
|
2817
|
+
},
|
|
2818
|
+
review: {
|
|
2819
|
+
command: "review",
|
|
2820
|
+
description: "Triple review \u2014 dispatch three reviewer sub-agents in parallel",
|
|
2821
|
+
artifacts: ["spec-review.md", "quality-review.md", "goal-review.md"],
|
|
2822
|
+
fileRef: ""
|
|
2823
|
+
},
|
|
2824
|
+
verify: {
|
|
2825
|
+
command: "verify",
|
|
2826
|
+
description: "Test verification \u2014 dispatch verifier sub-agent",
|
|
2827
|
+
artifacts: ["verification.md"],
|
|
2828
|
+
fileRef: ""
|
|
2829
|
+
},
|
|
2830
|
+
archive: {
|
|
2831
|
+
command: "archive",
|
|
2832
|
+
description: "Archive \u2014 dispatch archiver sub-agent for delta-spec merge + backfill",
|
|
2833
|
+
artifacts: ["archive/<change-id>/"],
|
|
2834
|
+
fileRef: ""
|
|
2835
|
+
},
|
|
2836
|
+
"ship-phase": {
|
|
2837
|
+
command: "ship-phase",
|
|
2838
|
+
description: "Phase ship \u2014 create PR, update state.md",
|
|
2839
|
+
artifacts: ["GitHub PR"],
|
|
2840
|
+
fileRef: ""
|
|
2841
|
+
},
|
|
2842
|
+
"ship-milestone": {
|
|
2843
|
+
command: "ship-milestone",
|
|
2844
|
+
description: "Milestone ship \u2014 release tag, update version",
|
|
2845
|
+
artifacts: ["git tag", "RELEASE.md"],
|
|
2846
|
+
fileRef: ""
|
|
2847
|
+
}
|
|
2848
|
+
};
|
|
2849
|
+
var STEP_TO_WORKFLOW = {
|
|
2850
|
+
grill: "grill",
|
|
2851
|
+
research: "research",
|
|
2852
|
+
roadmap: "roadmap",
|
|
2853
|
+
discuss: "discuss",
|
|
2854
|
+
"research-phase": "research-phase",
|
|
2855
|
+
split: "split",
|
|
2856
|
+
plan: "plan",
|
|
2857
|
+
apply: "apply",
|
|
2858
|
+
review: "review",
|
|
2859
|
+
verify: "verify",
|
|
2860
|
+
archive: "archive",
|
|
2861
|
+
"ship-phase": "ship",
|
|
2862
|
+
"ship-milestone": "ship",
|
|
2863
|
+
init: "init",
|
|
2864
|
+
adhoc: "adhoc",
|
|
2865
|
+
continue: "continue",
|
|
2866
|
+
milestone: "milestone"
|
|
2867
|
+
};
|
|
2868
|
+
function getStepInfo(command) {
|
|
2869
|
+
const info = STEP_INFO[command];
|
|
2870
|
+
if (!info) return void 0;
|
|
2871
|
+
const wfStep = STEP_TO_WORKFLOW[command];
|
|
2872
|
+
if (wfStep && WORKFLOW_REGISTRY[wfStep]) {
|
|
2873
|
+
return {
|
|
2874
|
+
...info,
|
|
2875
|
+
instructions: WORKFLOW_REGISTRY[wfStep].command().content
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
return info;
|
|
2879
|
+
}
|
|
2880
|
+
function determineFromChangeStatus(name, statusKey, type) {
|
|
2881
|
+
const available = getNextSteps(statusKey);
|
|
2882
|
+
const availableSteps = available.map((t) => ({
|
|
2883
|
+
command: t.command,
|
|
2884
|
+
slashCommand: t.slashCommand,
|
|
2885
|
+
subagent: t.subagent ?? false
|
|
2886
|
+
}));
|
|
2887
|
+
const first = available[0];
|
|
2888
|
+
const stepInfo = first ? getStepInfo(first.command) : void 0;
|
|
2889
|
+
return {
|
|
2890
|
+
currentStep: statusKey,
|
|
2891
|
+
context: `${type === "adhoc" ? "Adhoc Change" : "Change"} (${name})`,
|
|
2892
|
+
nextCommand: first?.command ?? null,
|
|
2893
|
+
slashCommand: first?.slashCommand || null,
|
|
2894
|
+
needsSubagent: first?.subagent ?? false,
|
|
2895
|
+
availableSteps,
|
|
2896
|
+
hint: available.length === 0 ? "This change has no available next steps. Create a new change to continue." : null,
|
|
2897
|
+
nextStepInfo: stepInfo,
|
|
2898
|
+
instructions: stepInfo?.instructions
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
function listAvailableChanges(state) {
|
|
2902
|
+
const names = [
|
|
2903
|
+
...state.changes.map((c) => c.name),
|
|
2904
|
+
...state.adhoc.map((c) => c.name)
|
|
2905
|
+
];
|
|
2906
|
+
return names.join(", ") || "(\u65E0)";
|
|
2907
|
+
}
|
|
2908
|
+
function determineFromState(state) {
|
|
2909
|
+
const ctx = state.active_context;
|
|
2910
|
+
const currentStatus = resolveStatus(state);
|
|
2911
|
+
const available = getNextSteps(currentStatus);
|
|
2912
|
+
const availableSteps = available.map((t) => ({
|
|
2913
|
+
command: t.command,
|
|
2914
|
+
slashCommand: t.slashCommand,
|
|
2915
|
+
subagent: t.subagent ?? false
|
|
2916
|
+
}));
|
|
2917
|
+
const first = available[0];
|
|
2918
|
+
const hint = available.length === 0 ? generateHint(state) : null;
|
|
2919
|
+
const stepInfo = first ? getStepInfo(first.command) : void 0;
|
|
2920
|
+
return {
|
|
2921
|
+
currentStep: ctx.step,
|
|
2922
|
+
context: formatContext2(state),
|
|
2923
|
+
nextCommand: first?.command ?? null,
|
|
2924
|
+
slashCommand: first?.slashCommand || null,
|
|
2925
|
+
needsSubagent: first?.subagent ?? false,
|
|
2926
|
+
availableSteps,
|
|
2927
|
+
hint,
|
|
2928
|
+
nextStepInfo: stepInfo,
|
|
2929
|
+
instructions: stepInfo?.instructions
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
function resolveStatus(state) {
|
|
2933
|
+
const ctx = state.active_context;
|
|
2934
|
+
switch (ctx.type) {
|
|
2935
|
+
case "project":
|
|
2936
|
+
return state.project.status;
|
|
2937
|
+
case "milestone":
|
|
2938
|
+
return state.project.status === "milestone-shipped" ? "milestone-shipped" : "milestone-active";
|
|
2939
|
+
case "phase":
|
|
2940
|
+
return `phase-${ctx.step}`;
|
|
2941
|
+
case "change":
|
|
2942
|
+
return `change-${ctx.step}`;
|
|
2943
|
+
case "adhoc":
|
|
2944
|
+
return `adhoc-${ctx.step}`;
|
|
2945
|
+
default:
|
|
2946
|
+
return state.project.status;
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
function formatContext2(state) {
|
|
2950
|
+
const { type, ref, step } = state.active_context;
|
|
2951
|
+
switch (type) {
|
|
2952
|
+
case "project":
|
|
2953
|
+
return `\u9879\u76EE\u5C42 \u2014 ${step}`;
|
|
2954
|
+
case "milestone":
|
|
2955
|
+
return `Milestone ${state.project.current_milestone ?? "?"} \u2014 ${step}`;
|
|
2956
|
+
case "phase":
|
|
2957
|
+
return `Phase ${state.project.current_phase ?? "?"} \u2014 ${step}`;
|
|
2958
|
+
case "change":
|
|
2959
|
+
return `Change (${ref ?? "?"}) \u2014 ${step}`;
|
|
2960
|
+
case "adhoc":
|
|
2961
|
+
return `\u4E34\u65F6 Change (${ref ?? "?"}) \u2014 ${step}`;
|
|
2962
|
+
default:
|
|
2963
|
+
return step;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
function generateHint(state) {
|
|
2967
|
+
const status = state.project.status;
|
|
2968
|
+
if (status === "milestone-shipped") {
|
|
2969
|
+
const pendingAdhoc = state.adhoc.filter((c) => c.status !== "archived");
|
|
2970
|
+
const hintParts = ["\u5F53\u524D milestone \u5DF2\u5B8C\u6210\u3002\u521B\u5EFA\u65B0 milestone: specwf state set-milestone <id>"];
|
|
2971
|
+
if (pendingAdhoc.length > 0) {
|
|
2972
|
+
hintParts.push(
|
|
2973
|
+
`\u5F85\u5904\u7406\u7684\u4E34\u65F6 change: ${pendingAdhoc.map((c) => c.name).join(", ")}\u3002\u4F7F\u7528: specwf continue change <name>`
|
|
2974
|
+
);
|
|
2975
|
+
}
|
|
2976
|
+
return hintParts.join("\n ");
|
|
2977
|
+
}
|
|
2978
|
+
if (status === "phase-shipped") {
|
|
2979
|
+
return "\u5F53\u524D phase \u5DF2\u5B8C\u6210\u3002\u521B\u5EFA\u65B0 phase \u6216\u5207\u6362: specwf state set-milestone <id>";
|
|
2980
|
+
}
|
|
2981
|
+
return null;
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// src/core/state-validator.ts
|
|
2985
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "fs";
|
|
2986
|
+
import { join as join12 } from "path";
|
|
2987
|
+
var EXIT_CRITERIA = [
|
|
2988
|
+
// milestone/active → 必须有 requirements.md(grill 产出后才能推进)
|
|
2989
|
+
{
|
|
2990
|
+
type: "milestone",
|
|
2991
|
+
step: "active",
|
|
2992
|
+
checks: [
|
|
2993
|
+
{ path: "requirements.md", description: "requirements.md \u4E0D\u5B58\u5728" }
|
|
2994
|
+
]
|
|
2995
|
+
},
|
|
2996
|
+
// project/requirements-defined → 必须有完整的 requirements.md
|
|
2997
|
+
{
|
|
2998
|
+
type: "project",
|
|
2999
|
+
step: "requirements-defined",
|
|
3000
|
+
checks: [
|
|
3001
|
+
{ path: "requirements.md", description: "requirements.md \u5185\u5BB9\u4E3A\u6A21\u677F\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5" }
|
|
3002
|
+
]
|
|
3003
|
+
},
|
|
3004
|
+
// project/researched → 必须有调研产出
|
|
3005
|
+
{
|
|
3006
|
+
type: "project",
|
|
3007
|
+
step: "researched",
|
|
3008
|
+
checks: [
|
|
3009
|
+
{ path: "research/summary.md", description: "research/summary.md \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u5B8C\u6210\u8C03\u7814" }
|
|
3010
|
+
]
|
|
3011
|
+
},
|
|
3012
|
+
// phase/discuss → 必须先产出 context.md
|
|
3013
|
+
{
|
|
3014
|
+
type: "phase",
|
|
3015
|
+
step: "discuss",
|
|
3016
|
+
checks: [
|
|
3017
|
+
{ path: "context.md", description: "context.md \u4E0D\u5B58\u5728\u6216\u4E3A\u6A21\u677F\u7A7A\u58F3\u3002\u8BF7\u5148\u5B8C\u6210 discuss \u6B65\u9AA4\u3002" }
|
|
3018
|
+
]
|
|
3019
|
+
},
|
|
3020
|
+
// phase/research → 必须有 phase 调研报告
|
|
3021
|
+
{
|
|
3022
|
+
type: "phase",
|
|
3023
|
+
step: "research",
|
|
3024
|
+
checks: [
|
|
3025
|
+
{ path: "research.md", description: "research.md \u4E0D\u5B58\u5728\u6216\u4E3A\u6A21\u677F\u7A7A\u58F3\u3002\u8BF7\u5148\u5B8C\u6210 research-phase \u6B65\u9AA4\u3002" }
|
|
3026
|
+
]
|
|
3027
|
+
},
|
|
3028
|
+
// adhoc/proposal → proposal.md 不能是模板
|
|
3029
|
+
{
|
|
3030
|
+
type: "adhoc",
|
|
3031
|
+
step: "proposal",
|
|
3032
|
+
checks: [
|
|
3033
|
+
{ path: "changes/", description: "change \u7684 proposal.md \u4E3A\u6A21\u677F\u7A7A\u58F3\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5" }
|
|
3034
|
+
]
|
|
3035
|
+
},
|
|
3036
|
+
// change/planning → design.md + tasks.md 不能是模板
|
|
3037
|
+
{
|
|
3038
|
+
type: "change",
|
|
3039
|
+
step: "planning",
|
|
3040
|
+
checks: [
|
|
3041
|
+
{ path: "changes/", description: "design.md \u6216 tasks.md \u4E3A\u6A21\u677F\u7A7A\u58F3\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5" }
|
|
3042
|
+
]
|
|
3043
|
+
}
|
|
3044
|
+
];
|
|
3045
|
+
function isTemplateFile(filePath) {
|
|
3046
|
+
try {
|
|
3047
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
3048
|
+
const placeholders = content.match(/\{\{[a-zA-Z_-]+\}\}/g);
|
|
3049
|
+
return (placeholders?.length ?? 0) > 3;
|
|
3050
|
+
} catch {
|
|
3051
|
+
return false;
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
function findChangeDir(specwfDir) {
|
|
3055
|
+
const changesDir = join12(specwfDir, "changes");
|
|
3056
|
+
if (!existsSync8(changesDir)) return [];
|
|
3057
|
+
try {
|
|
3058
|
+
return readdirSync4(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
3059
|
+
} catch {
|
|
3060
|
+
return [];
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
function checkExitCondition(specwfDir, check, resolvedPath) {
|
|
3064
|
+
const fullPath = resolvedPath ?? join12(specwfDir, check.path);
|
|
3065
|
+
if (check.path.endsWith("/") || check.description.includes("\u7684 ")) {
|
|
3066
|
+
const changes = findChangeDir(specwfDir);
|
|
3067
|
+
for (const change of changes) {
|
|
3068
|
+
for (const doc of ["proposal.md", "design.md", "tasks.md"]) {
|
|
3069
|
+
const docPath = join12(specwfDir, "changes", change, doc);
|
|
3070
|
+
if (existsSync8(docPath) && isTemplateFile(docPath)) {
|
|
3071
|
+
return `changes/${change}/${doc} \u4ECD\u4E3A\u6A21\u677F\u7A7A\u58F3\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5`;
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
return null;
|
|
3076
|
+
}
|
|
3077
|
+
if (!existsSync8(fullPath)) {
|
|
3078
|
+
return check.description;
|
|
3079
|
+
}
|
|
3080
|
+
if (isTemplateFile(fullPath)) {
|
|
3081
|
+
return check.description;
|
|
3082
|
+
}
|
|
3083
|
+
return null;
|
|
3084
|
+
}
|
|
3085
|
+
function validateStepAdvance(contextType, contextStep, ref, cwd) {
|
|
3086
|
+
const specwfDir = join12(cwd, "specwf");
|
|
3087
|
+
const criteria = EXIT_CRITERIA.find(
|
|
3088
|
+
(c) => c.type === contextType && c.step === contextStep
|
|
3089
|
+
);
|
|
3090
|
+
if (!criteria) {
|
|
3091
|
+
return { valid: true, errors: [] };
|
|
3092
|
+
}
|
|
3093
|
+
const errors = [];
|
|
3094
|
+
for (const check of criteria.checks) {
|
|
3095
|
+
const resolvedPath = ref && !check.path.startsWith("changes/") && existsSync8(join12(specwfDir, ref, check.path)) ? join12(specwfDir, ref, check.path) : join12(specwfDir, check.path);
|
|
3096
|
+
const error = checkExitCondition(specwfDir, check, resolvedPath);
|
|
3097
|
+
if (error) {
|
|
3098
|
+
errors.push(error);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
return { valid: errors.length === 0, errors };
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
// src/commands/specwf-continue.ts
|
|
3105
|
+
function register6(program2) {
|
|
3106
|
+
const cmd = program2.command("continue").description("Auto-advance to next step");
|
|
3107
|
+
cmd.command("change <name>").description("Advance a specific change").action(continueChangeHandler);
|
|
3108
|
+
cmd.action(continueHandler);
|
|
3109
|
+
}
|
|
3110
|
+
function formatContinueResult(result) {
|
|
3111
|
+
const output = {
|
|
3112
|
+
current: {
|
|
3113
|
+
context: result.context,
|
|
3114
|
+
step: result.currentStep
|
|
3115
|
+
}
|
|
3116
|
+
};
|
|
3117
|
+
if (result.nextCommand) {
|
|
3118
|
+
output.next = {
|
|
3119
|
+
command: result.nextCommand,
|
|
3120
|
+
slash: result.slashCommand || null,
|
|
3121
|
+
subagent: result.needsSubagent,
|
|
3122
|
+
description: result.nextStepInfo?.description || null,
|
|
3123
|
+
outputs: result.nextStepInfo?.artifacts || []
|
|
3124
|
+
};
|
|
3125
|
+
if (result.instructions) {
|
|
3126
|
+
output.next.instructions = result.instructions;
|
|
3127
|
+
}
|
|
3128
|
+
} else {
|
|
3129
|
+
output.next = null;
|
|
3130
|
+
if (result.hint) output.hint = result.hint;
|
|
3131
|
+
}
|
|
3132
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3133
|
+
}
|
|
3134
|
+
function resolveStatusKey(type, step, projectStatus) {
|
|
3135
|
+
switch (type) {
|
|
3136
|
+
case "project":
|
|
3137
|
+
return projectStatus;
|
|
3138
|
+
case "milestone":
|
|
3139
|
+
return projectStatus === "milestone-shipped" ? "milestone-shipped" : "milestone-active";
|
|
3140
|
+
case "phase":
|
|
3141
|
+
return `phase-${step}`;
|
|
3142
|
+
case "change":
|
|
3143
|
+
return `change-${step}`;
|
|
3144
|
+
case "adhoc":
|
|
3145
|
+
return `adhoc-${step}`;
|
|
3146
|
+
default:
|
|
3147
|
+
return projectStatus;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
function continueHandler() {
|
|
3151
|
+
const specwfDir = join13(process.cwd(), "specwf");
|
|
3152
|
+
const cwd = process.cwd();
|
|
3153
|
+
const state = loadState(specwfDir);
|
|
3154
|
+
const validation = validateStepAdvance(state.active_context.type, state.active_context.step, state.active_context.ref, cwd);
|
|
3155
|
+
if (!validation.valid) {
|
|
3156
|
+
console.log(JSON.stringify({ error: "exit_conditions_not_met", details: validation.errors }));
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
const result = determineNextStep(specwfDir);
|
|
3160
|
+
if (result.nextCommand) {
|
|
3161
|
+
const currentStatus = resolveStatusKey(state.active_context.type, state.active_context.step, state.project.status);
|
|
3162
|
+
const transition = getTransition(currentStatus, result.nextCommand);
|
|
3163
|
+
if (transition) {
|
|
3164
|
+
updateState(specwfDir, (s) => {
|
|
3165
|
+
s.active_context.step = transition.to;
|
|
3166
|
+
if (s.active_context.type === "project" || s.active_context.type === "milestone") {
|
|
3167
|
+
s.project.status = transition.to;
|
|
3168
|
+
}
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
formatContinueResult(result);
|
|
3173
|
+
}
|
|
3174
|
+
function continueChangeHandler(name) {
|
|
3175
|
+
const specwfDir = join13(process.cwd(), "specwf");
|
|
3176
|
+
const result = determineChangeNextStep(specwfDir, name);
|
|
3177
|
+
if ("error" in result) {
|
|
3178
|
+
console.log(JSON.stringify({ error: "not_found", message: result.error }));
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
if (result.nextCommand) {
|
|
3182
|
+
const state = loadState(specwfDir);
|
|
3183
|
+
const transition = getTransition(result.currentStep, result.nextCommand);
|
|
3184
|
+
if (transition) {
|
|
3185
|
+
const shortStatus = transition.to.replace(/^(change|adhoc)-/, "");
|
|
3186
|
+
updateState(specwfDir, (s) => {
|
|
3187
|
+
const adhoc = s.adhoc.find((c) => c.name === name);
|
|
3188
|
+
if (adhoc) {
|
|
3189
|
+
adhoc.status = shortStatus;
|
|
3190
|
+
s.active_context = { type: "adhoc", ref: `changes/${name}`, step: shortStatus };
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
const change = s.changes.find((c) => c.name === name);
|
|
3194
|
+
if (change) {
|
|
3195
|
+
change.status = shortStatus;
|
|
3196
|
+
s.active_context = { type: "change", ref: `changes/${name}`, step: shortStatus };
|
|
3197
|
+
}
|
|
3198
|
+
});
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
formatContinueResult(result);
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
// src/commands/specwf-archive.ts
|
|
3205
|
+
import { join as join15 } from "path";
|
|
3206
|
+
import { existsSync as existsSync11, readdirSync as readdirSync6, mkdirSync as mkdirSync6, copyFileSync } from "fs";
|
|
3207
|
+
import { execSync as execSync2 } from "child_process";
|
|
3208
|
+
|
|
3209
|
+
// src/core/delta-merge.ts
|
|
3210
|
+
import { createHash } from "crypto";
|
|
3211
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync9 } from "fs";
|
|
3212
|
+
|
|
3213
|
+
// src/parser/heading-tree.ts
|
|
3214
|
+
function parseHeadings(markdown) {
|
|
3215
|
+
const lines = markdown.split("\n");
|
|
3216
|
+
const nodes = [];
|
|
3217
|
+
const stack = [];
|
|
3218
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3219
|
+
const line = lines[i];
|
|
3220
|
+
const match = line.match(/^(#{1,6})\s+(.+)$/);
|
|
3221
|
+
if (!match) continue;
|
|
3222
|
+
const level = match[1].length;
|
|
3223
|
+
const text = match[2].trim();
|
|
3224
|
+
const lineNum = i + 1;
|
|
3225
|
+
const contentLines = [];
|
|
3226
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
3227
|
+
const nextMatch = lines[j].match(/^(#{1,6})\s+(.+)$/);
|
|
3228
|
+
if (nextMatch) break;
|
|
3229
|
+
contentLines.push(lines[j]);
|
|
3230
|
+
}
|
|
3231
|
+
const content = contentLines.join("\n").trim();
|
|
3232
|
+
const node = { level, text, line: lineNum, children: [], content };
|
|
3233
|
+
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
|
|
3234
|
+
stack.pop();
|
|
3235
|
+
}
|
|
3236
|
+
if (stack.length === 0) {
|
|
3237
|
+
nodes.push(node);
|
|
3238
|
+
} else {
|
|
3239
|
+
stack[stack.length - 1].node.children.push(node);
|
|
3240
|
+
}
|
|
3241
|
+
stack.push({ node, level });
|
|
3242
|
+
}
|
|
3243
|
+
return nodes;
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
// src/core/delta-merge.ts
|
|
3247
|
+
function fingerprint(content) {
|
|
3248
|
+
return createHash("sha256").update(content).digest("hex");
|
|
3249
|
+
}
|
|
3250
|
+
function mergeDeltaSpec(baseSpec, deltaSpec, baseFingerprint) {
|
|
3251
|
+
if (baseFingerprint) {
|
|
3252
|
+
const liveFingerprint = fingerprint(baseSpec);
|
|
3253
|
+
if (liveFingerprint === baseFingerprint) {
|
|
3254
|
+
return { type: "ok", merged: deltaSpec };
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
const baseTree = parseHeadings(baseSpec);
|
|
3258
|
+
const deltaTree = parseHeadings(deltaSpec);
|
|
3259
|
+
const merged = mergeTrees(baseTree, deltaTree);
|
|
3260
|
+
if (merged.conflicts.length > 0) {
|
|
3261
|
+
return { type: "conflict", conflicts: merged.conflicts };
|
|
3262
|
+
}
|
|
3263
|
+
return { type: "ok", merged: renderTree(merged.nodes) };
|
|
3264
|
+
}
|
|
3265
|
+
function mergeTrees(base, delta) {
|
|
3266
|
+
const conflicts = [];
|
|
3267
|
+
const nodes = [];
|
|
3268
|
+
const baseIndex = indexNodes(base);
|
|
3269
|
+
const deltaIndex = indexNodes(delta);
|
|
3270
|
+
const allKeys = /* @__PURE__ */ new Set([...baseIndex.keys(), ...deltaIndex.keys()]);
|
|
3271
|
+
for (const key of allKeys) {
|
|
3272
|
+
const b = baseIndex.get(key);
|
|
3273
|
+
const d = deltaIndex.get(key);
|
|
3274
|
+
if (b && !d) {
|
|
3275
|
+
nodes.push({ node: b, children: b.children.map((c) => ({ node: c, children: [] })) });
|
|
3276
|
+
} else if (!b && d) {
|
|
3277
|
+
nodes.push({ node: d, children: d.children.map((c) => ({ node: c, children: [] })) });
|
|
3278
|
+
} else if (b && d) {
|
|
3279
|
+
const childMerge = mergeTrees(b.children, d.children);
|
|
3280
|
+
if (b.content === d.content) {
|
|
3281
|
+
nodes.push({ node: b, children: childMerge.nodes });
|
|
3282
|
+
} else {
|
|
3283
|
+
const lineMerge = tryLineMerge(b.content, d.content);
|
|
3284
|
+
if (lineMerge !== null) {
|
|
3285
|
+
nodes.push({ node: { ...b, content: lineMerge }, children: childMerge.nodes });
|
|
3286
|
+
} else {
|
|
3287
|
+
conflicts.push({
|
|
3288
|
+
section: b.text,
|
|
3289
|
+
message: `Content conflict in section: ${b.text}`,
|
|
3290
|
+
baseContent: b.content,
|
|
3291
|
+
deltaContent: d.content
|
|
3292
|
+
});
|
|
3293
|
+
nodes.push({ node: b, children: childMerge.nodes });
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
conflicts.push(...childMerge.conflicts);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
return { nodes, conflicts };
|
|
3300
|
+
}
|
|
3301
|
+
function indexNodes(nodes) {
|
|
3302
|
+
const map = /* @__PURE__ */ new Map();
|
|
3303
|
+
for (const node of nodes) {
|
|
3304
|
+
map.set(`${node.level}:${node.text}`, node);
|
|
3305
|
+
}
|
|
3306
|
+
return map;
|
|
3307
|
+
}
|
|
3308
|
+
function tryLineMerge(baseText, deltaText) {
|
|
3309
|
+
const baseLines = baseText.split("\n");
|
|
3310
|
+
const deltaLines = deltaText.split("\n");
|
|
3311
|
+
const baseSet = new Set(baseLines);
|
|
3312
|
+
const removedFromBase = baseLines.filter(
|
|
3313
|
+
(l) => l.trim() && !deltaLines.includes(l)
|
|
3314
|
+
);
|
|
3315
|
+
if (removedFromBase.length === 0) {
|
|
3316
|
+
const result = [...baseLines];
|
|
3317
|
+
for (const line of deltaLines) {
|
|
3318
|
+
if (!baseSet.has(line)) {
|
|
3319
|
+
result.push(line);
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
return result.join("\n");
|
|
3323
|
+
}
|
|
3324
|
+
return null;
|
|
3325
|
+
}
|
|
3326
|
+
function renderTree(nodes) {
|
|
3327
|
+
const lines = [];
|
|
3328
|
+
renderNodes(nodes, lines);
|
|
3329
|
+
return lines.join("\n").trim();
|
|
3330
|
+
}
|
|
3331
|
+
function renderNodes(nodes, lines) {
|
|
3332
|
+
for (const { node, children } of nodes) {
|
|
3333
|
+
lines.push(`${"#".repeat(node.level)} ${node.text}`);
|
|
3334
|
+
if (node.content) {
|
|
3335
|
+
lines.push("");
|
|
3336
|
+
lines.push(node.content);
|
|
3337
|
+
}
|
|
3338
|
+
if (children.length > 0) {
|
|
3339
|
+
lines.push("");
|
|
3340
|
+
renderNodes(children, lines);
|
|
3341
|
+
}
|
|
3342
|
+
lines.push("");
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
function mergeAndWrite(liveSpecPath, deltaSpecPath, baseFingerprint) {
|
|
3346
|
+
const baseSpec = readFileSync9(liveSpecPath, "utf-8");
|
|
3347
|
+
const deltaSpec = readFileSync9(deltaSpecPath, "utf-8");
|
|
3348
|
+
const result = mergeDeltaSpec(baseSpec, deltaSpec, baseFingerprint);
|
|
3349
|
+
if (result.type === "ok") {
|
|
3350
|
+
writeFileSync7(liveSpecPath, result.merged, "utf-8");
|
|
3351
|
+
}
|
|
3352
|
+
return result;
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
// src/core/code-extract.ts
|
|
3356
|
+
import { execSync } from "child_process";
|
|
3357
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync5 } from "fs";
|
|
3358
|
+
import { join as join14 } from "path";
|
|
3359
|
+
function extractFromGitDiff(repoDir, changeDir) {
|
|
3360
|
+
const diff = getGitDiff(repoDir);
|
|
3361
|
+
if (diff === null) {
|
|
3362
|
+
return { extractions: [], available: false };
|
|
3363
|
+
}
|
|
3364
|
+
const domains = changeDir ? detectDomains(changeDir) : ["general"];
|
|
3365
|
+
const extractions = [];
|
|
3366
|
+
for (const domain of domains) {
|
|
3367
|
+
const behaviors = extractBehaviors(diff, domain);
|
|
3368
|
+
const constraints = extractConstraints(diff, domain);
|
|
3369
|
+
if (behaviors.length > 0 || constraints.length > 0) {
|
|
3370
|
+
extractions.push({ domain, behaviors, constraints });
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
return { extractions, available: true };
|
|
3374
|
+
}
|
|
3375
|
+
function getGitDiff(repoDir) {
|
|
3376
|
+
try {
|
|
3377
|
+
const diff = execSync("git diff HEAD", {
|
|
3378
|
+
cwd: repoDir,
|
|
3379
|
+
encoding: "utf-8",
|
|
3380
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3381
|
+
});
|
|
3382
|
+
if (diff.trim()) return diff;
|
|
3383
|
+
const lastCommit = execSync("git diff HEAD~1 HEAD", {
|
|
3384
|
+
cwd: repoDir,
|
|
3385
|
+
encoding: "utf-8",
|
|
3386
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3387
|
+
});
|
|
3388
|
+
return lastCommit.trim() ? lastCommit : null;
|
|
3389
|
+
} catch {
|
|
3390
|
+
return null;
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
function detectDomains(changeDir) {
|
|
3394
|
+
const specsDir = join14(changeDir, "specs");
|
|
3395
|
+
if (!existsSync10(specsDir)) return ["general"];
|
|
3396
|
+
try {
|
|
3397
|
+
return readdirSync5(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3398
|
+
} catch {
|
|
3399
|
+
return ["general"];
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
function extractBehaviors(diff, _domain) {
|
|
3403
|
+
const behaviors = [];
|
|
3404
|
+
const lines = diff.split("\n");
|
|
3405
|
+
for (const line of lines) {
|
|
3406
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
3407
|
+
const content = line.slice(1).trim();
|
|
3408
|
+
if (/\b(SHALL|MUST|SHOULD|MAY)\b/.test(content)) {
|
|
3409
|
+
behaviors.push(content);
|
|
3410
|
+
}
|
|
3411
|
+
if (/^(export\s+)?(async\s+)?function\s+/.test(content) || /^(export\s+)?class\s+/.test(content)) {
|
|
3412
|
+
behaviors.push(`\u65B0\u589E: ${content}`);
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
return behaviors;
|
|
3417
|
+
}
|
|
3418
|
+
function extractConstraints(diff, _domain) {
|
|
3419
|
+
const constraints = [];
|
|
3420
|
+
const lines = diff.split("\n");
|
|
3421
|
+
for (const line of lines) {
|
|
3422
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
3423
|
+
const content = line.slice(1).trim();
|
|
3424
|
+
if (/^(throw|assert|if\s*\()/.test(content) && !content.startsWith("//")) {
|
|
3425
|
+
constraints.push(`\u7EA6\u675F: ${content}`);
|
|
3426
|
+
}
|
|
3427
|
+
if (/^(export\s+)?(interface|type)\s+/.test(content)) {
|
|
3428
|
+
constraints.push(`\u7C7B\u578B\u7EA6\u675F: ${content}`);
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
return constraints;
|
|
3433
|
+
}
|
|
3434
|
+
function writeExtractionToSpec(specsDir, extraction) {
|
|
3435
|
+
const domainDir = join14(specsDir, extraction.domain);
|
|
3436
|
+
const specPath = join14(domainDir, "spec.md");
|
|
3437
|
+
let existing = "";
|
|
3438
|
+
if (existsSync10(specPath)) {
|
|
3439
|
+
existing = readFileSync10(specPath, "utf-8");
|
|
3440
|
+
}
|
|
3441
|
+
const section = generateAutoExtractedSection(extraction);
|
|
3442
|
+
const updated = existing.trim() ? `${existing.trim()}
|
|
3443
|
+
|
|
3444
|
+
${section}` : section;
|
|
3445
|
+
mkdirSync5(domainDir, { recursive: true });
|
|
3446
|
+
writeFileSync8(specPath, updated, "utf-8");
|
|
3447
|
+
}
|
|
3448
|
+
function generateAutoExtractedSection(extraction) {
|
|
3449
|
+
const lines = [
|
|
3450
|
+
"<!-- AUTO-EXTRACTED: \u4EE5\u4E0B\u5185\u5BB9\u7531 code-extract \u4ECE\u4EE3\u7801 diff \u63D0\u53D6\uFF0C\u8BF7\u4EBA\u5DE5\u5BA1\u6838 -->",
|
|
3451
|
+
"",
|
|
3452
|
+
"## Auto-Extracted Behaviors",
|
|
3453
|
+
""
|
|
3454
|
+
];
|
|
3455
|
+
if (extraction.behaviors.length > 0) {
|
|
3456
|
+
lines.push("### Detected Behaviors", "");
|
|
3457
|
+
for (const b of extraction.behaviors) {
|
|
3458
|
+
lines.push(`- ${b}`);
|
|
3459
|
+
}
|
|
3460
|
+
lines.push("");
|
|
3461
|
+
}
|
|
3462
|
+
if (extraction.constraints.length > 0) {
|
|
3463
|
+
lines.push("### Detected Constraints", "");
|
|
3464
|
+
for (const c of extraction.constraints) {
|
|
3465
|
+
lines.push(`- ${c}`);
|
|
3466
|
+
}
|
|
3467
|
+
lines.push("");
|
|
3468
|
+
}
|
|
3469
|
+
lines.push("<!-- END AUTO-EXTRACTED -->");
|
|
3470
|
+
return lines.join("\n");
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
// src/commands/specwf-archive.ts
|
|
3474
|
+
function register7(program2) {
|
|
3475
|
+
program2.command("archive <change>").description("\u5F52\u6863 change\uFF08delta \u5408\u5E76 + \u4EE3\u7801\u56DE\u704C\uFF09").action(archiveHandler);
|
|
3476
|
+
}
|
|
3477
|
+
function archiveHandler(changePath) {
|
|
3478
|
+
const specwfDir = join15(process.cwd(), "specwf");
|
|
3479
|
+
const fullChangePath = join15(process.cwd(), changePath);
|
|
3480
|
+
if (!existsSync11(fullChangePath)) {
|
|
3481
|
+
console.error(`\u9519\u8BEF: change \u76EE\u5F55\u4E0D\u5B58\u5728: ${changePath}`);
|
|
3482
|
+
process.exit(1);
|
|
3483
|
+
}
|
|
3484
|
+
const specsDir = join15(fullChangePath, "specs");
|
|
3485
|
+
if (existsSync11(specsDir)) {
|
|
3486
|
+
mergeDeltaSpecs(specsDir, specwfDir);
|
|
3487
|
+
console.log("\u2713 delta-specs \u5408\u5E76\u5B8C\u6210");
|
|
3488
|
+
}
|
|
3489
|
+
const summaryPath = join15(fullChangePath, "change-summary.md");
|
|
3490
|
+
if (!existsSync11(summaryPath)) {
|
|
3491
|
+
console.warn("\u26A0 change-summary.md \u4E0D\u5B58\u5728\u3002\u5EFA\u8BAE\u5148\u4F7F\u7528 `specwf template change-summary` \u751F\u6210\u53D8\u66F4\u603B\u7ED3\u3002");
|
|
3492
|
+
}
|
|
3493
|
+
const repoDir = process.cwd();
|
|
3494
|
+
const extractResult = extractFromGitDiff(repoDir, fullChangePath);
|
|
3495
|
+
if (extractResult.available && extractResult.extractions.length > 0) {
|
|
3496
|
+
for (const extraction of extractResult.extractions) {
|
|
3497
|
+
writeExtractionToSpec(join15(specwfDir, "specs"), extraction);
|
|
3498
|
+
}
|
|
3499
|
+
if (extractResult.extractions.length > 0) {
|
|
3500
|
+
console.log(`\u2713 \u4EE3\u7801\u8BA4\u77E5\u63D0\u53D6\u5B8C\u6210 (${extractResult.extractions.length} \u4E2A\u57DF)`);
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
const archiveDir = archiveChangeDir(specwfDir, fullChangePath);
|
|
3504
|
+
console.log(`\u2713 \u5F52\u6863\u5230: ${archiveDir}`);
|
|
3505
|
+
try {
|
|
3506
|
+
execSync2(`git rm -r "${changePath}" 2>/dev/null || true`, { cwd: process.cwd() });
|
|
3507
|
+
} catch {
|
|
3508
|
+
}
|
|
3509
|
+
const changeName = changePath.split("/").pop() ?? "unknown";
|
|
3510
|
+
try {
|
|
3511
|
+
updateState(specwfDir, (state) => {
|
|
3512
|
+
const change = state.changes.find((c) => c.name === changeName);
|
|
3513
|
+
if (change) {
|
|
3514
|
+
change.status = "archived";
|
|
3515
|
+
return;
|
|
3516
|
+
}
|
|
3517
|
+
const adhoc = state.adhoc.find((c) => c.name === changeName);
|
|
3518
|
+
if (adhoc) {
|
|
3519
|
+
adhoc.status = "archived";
|
|
3520
|
+
}
|
|
3521
|
+
});
|
|
3522
|
+
console.log("\u2713 state.md \u5DF2\u66F4\u65B0");
|
|
3523
|
+
} catch {
|
|
3524
|
+
}
|
|
3525
|
+
console.log("\u5F52\u6863\u5B8C\u6210\u3002");
|
|
3526
|
+
}
|
|
3527
|
+
function mergeDeltaSpecs(deltaDir, specwfDir) {
|
|
3528
|
+
const entries = readdirSync6(deltaDir, { withFileTypes: true });
|
|
3529
|
+
for (const entry of entries) {
|
|
3530
|
+
if (!entry.isDirectory()) continue;
|
|
3531
|
+
const deltaSpecPath = join15(deltaDir, entry.name, "spec.md");
|
|
3532
|
+
const liveSpecPath = join15(specwfDir, "specs", entry.name, "spec.md");
|
|
3533
|
+
if (!existsSync11(deltaSpecPath)) continue;
|
|
3534
|
+
if (!existsSync11(liveSpecPath)) {
|
|
3535
|
+
mkdirSync6(join15(specwfDir, "specs", entry.name), { recursive: true });
|
|
3536
|
+
copyFileSync(deltaSpecPath, liveSpecPath);
|
|
3537
|
+
continue;
|
|
3538
|
+
}
|
|
3539
|
+
const result = mergeAndWrite(liveSpecPath, deltaSpecPath);
|
|
3540
|
+
if (result.type === "conflict") {
|
|
3541
|
+
console.warn(`\u26A0 \u5408\u5E76\u51B2\u7A81: ${entry.name}/spec.md`);
|
|
3542
|
+
for (const c of result.conflicts) {
|
|
3543
|
+
console.warn(` \u8282: ${c.section}`);
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
// src/commands/specwf-list.ts
|
|
3550
|
+
import { join as join16 } from "path";
|
|
3551
|
+
function register8(program2) {
|
|
3552
|
+
program2.command("list").description("\u5217\u51FA milestones/phases/changes").option("--all", "\u5305\u542B\u5F52\u6863").action(listHandler);
|
|
3553
|
+
}
|
|
3554
|
+
function listHandler(options) {
|
|
3555
|
+
const specwfDir = join16(process.cwd(), "specwf");
|
|
3556
|
+
let hasItems = false;
|
|
3557
|
+
const milestones = listMilestones(specwfDir);
|
|
3558
|
+
if (milestones.length > 0) {
|
|
3559
|
+
console.log("Milestones:");
|
|
3560
|
+
for (const ms of milestones) {
|
|
3561
|
+
console.log(` ${ms}/`);
|
|
3562
|
+
const phases = listPhases(specwfDir, ms);
|
|
3563
|
+
for (const ph of phases) {
|
|
3564
|
+
console.log(` ${ph}/`);
|
|
3565
|
+
const changes = listChanges(specwfDir, ms, ph);
|
|
3566
|
+
for (const ch of changes) {
|
|
3567
|
+
console.log(` ${ch}/`);
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
hasItems = true;
|
|
3572
|
+
}
|
|
3573
|
+
const adhoc = listAdhocChanges(specwfDir);
|
|
3574
|
+
if (adhoc.length > 0) {
|
|
3575
|
+
if (hasItems) console.log("");
|
|
3576
|
+
console.log("\u4E34\u65F6 Changes:");
|
|
3577
|
+
for (const ch of adhoc) {
|
|
3578
|
+
console.log(` ${ch}/`);
|
|
3579
|
+
}
|
|
3580
|
+
hasItems = true;
|
|
3581
|
+
}
|
|
3582
|
+
if (options.all) {
|
|
3583
|
+
const archived = listArchived(specwfDir);
|
|
3584
|
+
if (archived.length > 0) {
|
|
3585
|
+
if (hasItems) console.log("");
|
|
3586
|
+
console.log("\u5F52\u6863:");
|
|
3587
|
+
for (const a of archived) {
|
|
3588
|
+
console.log(` ${a}/`);
|
|
3589
|
+
}
|
|
3590
|
+
hasItems = true;
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
if (!hasItems) {
|
|
3594
|
+
console.log("(\u65E0\u6761\u76EE)");
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
// src/commands/specwf-template.ts
|
|
3599
|
+
import { join as join17, dirname as dirname2 } from "path";
|
|
3600
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
|
|
3601
|
+
|
|
3602
|
+
// src/templates/artifacts/index.ts
|
|
3603
|
+
var PROPOSAL_TEMPLATE = `# Proposal: {{name}}
|
|
3604
|
+
|
|
3605
|
+
> This document is a Change Proposal \u2014 align intent, scope, and approach before implementation. Complete each section; reviewers will evaluate this proposal before the design phase.
|
|
3606
|
+
|
|
3607
|
+
---
|
|
3608
|
+
|
|
3609
|
+
## Intent
|
|
3610
|
+
|
|
3611
|
+
<!--
|
|
3612
|
+
Describe why this change is needed:
|
|
3613
|
+
1. What specific problem exists or what capability is missing?
|
|
3614
|
+
2. Who is affected (users/developers/ops)? How severely?
|
|
3615
|
+
3. What happens if we don't make this change?
|
|
3616
|
+
4. Is this a bug fix / feature / tech debt / perf improvement?
|
|
3617
|
+
5. Is this linked to a known issue, user feedback, or metric? (attach issue link if available)
|
|
3618
|
+
-->
|
|
3619
|
+
|
|
3620
|
+
{{intent}}
|
|
3621
|
+
|
|
3622
|
+
---
|
|
3623
|
+
|
|
3624
|
+
## Scope
|
|
3625
|
+
|
|
3626
|
+
### In scope
|
|
3627
|
+
|
|
3628
|
+
<!--
|
|
3629
|
+
List all items covered by this change. One per line, verb-first.
|
|
3630
|
+
Example:
|
|
3631
|
+
- Add skeleton loading state on list pull-to-refresh
|
|
3632
|
+
- Add useScrollPerformance hook for scroll metrics
|
|
3633
|
+
- Memoize UserCard component
|
|
3634
|
+
-->
|
|
3635
|
+
|
|
3636
|
+
{{in-scope-items}}
|
|
3637
|
+
|
|
3638
|
+
### Out of scope
|
|
3639
|
+
|
|
3640
|
+
<!--
|
|
3641
|
+
Explicitly excluded changes to prevent scope creep. One per line with reason.
|
|
3642
|
+
Example:
|
|
3643
|
+
- Homepage skeleton screen (planned for next phase)
|
|
3644
|
+
- Server-side API pagination (unrelated to client performance)
|
|
3645
|
+
- Android list optimization (platform-specific, needs separate research)
|
|
3646
|
+
-->
|
|
3647
|
+
|
|
3648
|
+
{{out-of-scope-items}}
|
|
3649
|
+
|
|
3650
|
+
---
|
|
3651
|
+
|
|
3652
|
+
## Approach
|
|
3653
|
+
|
|
3654
|
+
<!--
|
|
3655
|
+
Describe the technical direction at a high level:
|
|
3656
|
+
1. Architecture layer: Which layer does the change touch (UI/Service/Store)? New modules needed?
|
|
3657
|
+
2. Library choices: New dependencies? Upgrades? Rationale?
|
|
3658
|
+
3. Data flow: How does data travel from source to UI? State management changes?
|
|
3659
|
+
4. Compatibility: Backward compatibility strategy? Migration needed?
|
|
3660
|
+
5. Testability: Are there injection points / mock seams for testing?
|
|
3661
|
+
|
|
3662
|
+
No detailed implementation here \u2014 the design doc handles that.
|
|
3663
|
+
-->
|
|
3664
|
+
|
|
3665
|
+
{{approach}}
|
|
3666
|
+
|
|
3667
|
+
---
|
|
3668
|
+
|
|
3669
|
+
## Must-haves
|
|
3670
|
+
|
|
3671
|
+
<!--
|
|
3672
|
+
3-7 observable, verifiable must-have behaviors.
|
|
3673
|
+
Each must be a concrete statement \u2014 no ambiguity.
|
|
3674
|
+
Reviewers should be able to judge pass/fail using these conditions.
|
|
3675
|
+
|
|
3676
|
+
Format: "MUST <condition>" or "SHALL <condition>"
|
|
3677
|
+
- Observable: visible on screen, checkable via CLI, assertable in tests
|
|
3678
|
+
- Verifiable: reviewer can confirm via action/command
|
|
3679
|
+
-->
|
|
3680
|
+
|
|
3681
|
+
{{must-haves}}
|
|
3682
|
+
|
|
3683
|
+
---
|
|
3684
|
+
|
|
3685
|
+
## Non-goals
|
|
3686
|
+
|
|
3687
|
+
<!--
|
|
3688
|
+
Explicit non-goals to prevent reviewers from asking "why wasn't X done?"
|
|
3689
|
+
Different from Out of scope (not in this change's scope).
|
|
3690
|
+
Non-goals are specific targets that might be incorrectly assumed to be in scope.
|
|
3691
|
+
Example:
|
|
3692
|
+
- Not pursuing Android list performance in this change
|
|
3693
|
+
- Not changing the existing pagination logic
|
|
3694
|
+
- Not adding new UI component library dependencies
|
|
3695
|
+
-->
|
|
3696
|
+
|
|
3697
|
+
{{non-goals}}
|
|
3698
|
+
`;
|
|
3699
|
+
var DESIGN_TEMPLATE = `# Design: {{name}}
|
|
3700
|
+
|
|
3701
|
+
> This document is the Change Design \u2014 written after proposal approval, describing how to implement. Each section has fill-in guidance. After this document, proceed to task breakdown.
|
|
3702
|
+
|
|
3703
|
+
---
|
|
3704
|
+
|
|
3705
|
+
## Context & Goals
|
|
3706
|
+
|
|
3707
|
+
<!--
|
|
3708
|
+
1. Briefly describe context \u2014 what constraints exist?
|
|
3709
|
+
2. Core design goals (no more than 3)
|
|
3710
|
+
3. Must align with proposal Intent and Must-haves
|
|
3711
|
+
-->
|
|
3712
|
+
|
|
3713
|
+
{{background-and-goals}}
|
|
3714
|
+
|
|
3715
|
+
---
|
|
3716
|
+
|
|
3717
|
+
## Technical Approach
|
|
3718
|
+
|
|
3719
|
+
### Architecture Diagram
|
|
3720
|
+
|
|
3721
|
+
<!--
|
|
3722
|
+
ASCII art showing module/component relationships:
|
|
3723
|
+
- New modules vs. existing modules
|
|
3724
|
+
- Data flow direction (arrows)
|
|
3725
|
+
- File/module boundaries
|
|
3726
|
+
Annotate: [NEW], [MODIFIED], [EXISTING]
|
|
3727
|
+
-->
|
|
3728
|
+
|
|
3729
|
+
\`\`\`text
|
|
3730
|
+
{{architecture-diagram}}
|
|
3731
|
+
\`\`\`
|
|
3732
|
+
|
|
3733
|
+
### Core Data Structures
|
|
3734
|
+
|
|
3735
|
+
<!--
|
|
3736
|
+
Key types/interfaces/data structures introduced or modified by this design.
|
|
3737
|
+
Use TypeScript interface format. Brief description per type.
|
|
3738
|
+
-->
|
|
3739
|
+
|
|
3740
|
+
{{data-structures}}
|
|
3741
|
+
|
|
3742
|
+
### Data Flow
|
|
3743
|
+
|
|
3744
|
+
<!--
|
|
3745
|
+
Step-by-step description of data flow from trigger to effect.
|
|
3746
|
+
Example:
|
|
3747
|
+
1. User scrolls list \u2192 FlatList fires onScroll
|
|
3748
|
+
2. OptimizedList reads itemHeight config \u2192 enables getItemLayout
|
|
3749
|
+
3. Layout engine skips dynamic measurement \u2192 uses fixed row height
|
|
3750
|
+
4. useScrollPerformance samples FPS every 500ms
|
|
3751
|
+
5. FPS data \u2192 Performance Reporter \u2192 backend
|
|
3752
|
+
-->
|
|
3753
|
+
|
|
3754
|
+
{{data-flow}}
|
|
3755
|
+
|
|
3756
|
+
### Interface Design
|
|
3757
|
+
|
|
3758
|
+
<!--
|
|
3759
|
+
Public API signatures exposed by this design:
|
|
3760
|
+
- Function/method names
|
|
3761
|
+
- Parameter lists (name + type + description)
|
|
3762
|
+
- Return types
|
|
3763
|
+
- sync/async
|
|
3764
|
+
-->
|
|
3765
|
+
|
|
3766
|
+
{{api-signatures}}
|
|
3767
|
+
|
|
3768
|
+
---
|
|
3769
|
+
|
|
3770
|
+
## File Manifest
|
|
3771
|
+
|
|
3772
|
+
<!--
|
|
3773
|
+
All files to create or modify, organized as a table.
|
|
3774
|
+
-->
|
|
3775
|
+
|
|
3776
|
+
| File Path | Description | Action |
|
|
3777
|
+
|-----------|-------------|--------|
|
|
3778
|
+
| \`{{file-path-1}}\` | {{description}} | Create |
|
|
3779
|
+
| \`{{file-path-2}}\` | {{description}} | Modify |
|
|
3780
|
+
|
|
3781
|
+
---
|
|
3782
|
+
|
|
3783
|
+
## Test Strategy
|
|
3784
|
+
|
|
3785
|
+
### Unit Tests
|
|
3786
|
+
- <!-- Which modules need unit tests? What needs mocking? -->
|
|
3787
|
+
|
|
3788
|
+
### Integration Tests
|
|
3789
|
+
- <!-- Which flows need integration tests? What fixtures needed? -->
|
|
3790
|
+
|
|
3791
|
+
### TDD Tasks
|
|
3792
|
+
- <!-- List type:behavior tasks requiring RED\u2192GREEN\u2192REFACTOR -->
|
|
3793
|
+
|
|
3794
|
+
---
|
|
3795
|
+
|
|
3796
|
+
## Alternatives
|
|
3797
|
+
|
|
3798
|
+
<!--
|
|
3799
|
+
Evaluated but rejected approaches, with rationale.
|
|
3800
|
+
-->
|
|
3801
|
+
|
|
3802
|
+
| Approach | Pros | Cons | Rejection Reason |
|
|
3803
|
+
|----------|------|------|-----------------|
|
|
3804
|
+
| {{alt-name-1}} | {{pros}} | {{cons}} | {{reason}} |
|
|
3805
|
+
| {{alt-name-2}} | {{pros}} | {{cons}} | {{reason}} |
|
|
3806
|
+
|
|
3807
|
+
---
|
|
3808
|
+
|
|
3809
|
+
## Risk Assessment
|
|
3810
|
+
|
|
3811
|
+
| Risk | Probability | Impact | Mitigation |
|
|
3812
|
+
|------|------------|--------|-----------|
|
|
3813
|
+
| {{risk-1}} | {{probability}} | {{impact}} | {{mitigation}} |
|
|
3814
|
+
| {{risk-2}} | {{probability}} | {{impact}} | {{mitigation}} |
|
|
3815
|
+
`;
|
|
3816
|
+
var TASKS_TEMPLATE = `# Tasks: {{name}}
|
|
3817
|
+
|
|
3818
|
+
> This document breaks the design into executable tasks grouped by wave. Each task includes description, files, acceptance criteria, optional depends_on and spec_ref. type:behavior tasks must include RED test descriptions (GIVEN/WHEN/THEN format).
|
|
3819
|
+
|
|
3820
|
+
---
|
|
3821
|
+
|
|
3822
|
+
## TDD Type Annotations
|
|
3823
|
+
|
|
3824
|
+
| type | Meaning | TDD Protocol |
|
|
3825
|
+
|------|---------|-------------|
|
|
3826
|
+
| \`behavior\` | Business behavior \u2014 implement a concrete, observable/assertable feature | **RED\u2192GREEN\u2192REFACTOR** (mandatory: test first \u2192 implement \u2192 refactor) |
|
|
3827
|
+
| \`config\` | Configuration \u2014 env vars, CI/CD, lint, tsconfig, etc. | Direct implementation, no TDD |
|
|
3828
|
+
| \`refactor\` | Refactoring \u2014 improve internal structure without changing behavior | Verify tests pass \u2192 refactor \u2192 verify again |
|
|
3829
|
+
| \`docs\` | Documentation \u2014 README, API docs, comments | Direct implementation, no TDD |
|
|
3830
|
+
| \`scaffolding\` | Skeleton code \u2014 new module shells, directory structure, templates | Direct implementation, no TDD |
|
|
3831
|
+
|
|
3832
|
+
> **Rule**: If a task's core output is "a behavior" (user-perceptible or test-assertable), use \`behavior\`. If it's just "file exists" or "config takes effect", use \`config\`/\`scaffolding\`.
|
|
3833
|
+
|
|
3834
|
+
---
|
|
3835
|
+
|
|
3836
|
+
## Wave 1: {{wave-1-theme}}
|
|
3837
|
+
|
|
3838
|
+
<!--
|
|
3839
|
+
A wave is an independently verifiable unit of work. Tasks within a wave may have dependencies but the wave is self-contained.
|
|
3840
|
+
Each wave completion enables verification (tsc + test pass).
|
|
3841
|
+
-->
|
|
3842
|
+
|
|
3843
|
+
- [ ] task-{{id-1}}: [type:{{type}}] {{title}}
|
|
3844
|
+
- **description**: {{What to do, approach, files/APIs to reference}}
|
|
3845
|
+
- **files**: {{comma-separated file paths}}
|
|
3846
|
+
- **acceptance**: {{observable, assertable acceptance criteria}}
|
|
3847
|
+
- **depends_on**: [task-{{id-x}}] <!-- optional: predecessor -->
|
|
3848
|
+
- **spec_ref**: specs/{{domain}}/spec.md <!-- optional: linked spec -->
|
|
3849
|
+
{{if behavior}}
|
|
3850
|
+
- ***RED test***:
|
|
3851
|
+
\`\`\`
|
|
3852
|
+
GIVEN {{precondition}}
|
|
3853
|
+
WHEN {{trigger action}}
|
|
3854
|
+
THEN {{expected result}}
|
|
3855
|
+
\`\`\`
|
|
3856
|
+
{{/if}}
|
|
3857
|
+
|
|
3858
|
+
---
|
|
3859
|
+
|
|
3860
|
+
## Wave 2: {{wave-2-theme}}
|
|
3861
|
+
|
|
3862
|
+
- [ ] task-{{id-3}}: [type:{{type}}] {{title}}
|
|
3863
|
+
- **description**: {{What to do}}
|
|
3864
|
+
- **files**: {{file paths}}
|
|
3865
|
+
- **acceptance**: {{acceptance criteria}}
|
|
3866
|
+
- **depends_on**: [task-{{id-1}}] <!-- optional -->
|
|
3867
|
+
{{if behavior}}
|
|
3868
|
+
- ***RED test***:
|
|
3869
|
+
\`\`\`
|
|
3870
|
+
GIVEN {{precondition}}
|
|
3871
|
+
WHEN {{trigger action}}
|
|
3872
|
+
THEN {{expected result}}
|
|
3873
|
+
\`\`\`
|
|
3874
|
+
{{/if}}
|
|
3875
|
+
|
|
3876
|
+
---
|
|
3877
|
+
|
|
3878
|
+
## Verification
|
|
3879
|
+
|
|
3880
|
+
- [ ] \`tsc --noEmit\` passes (or equivalent type check)
|
|
3881
|
+
- [ ] \`vitest run\` all test suites pass
|
|
3882
|
+
- [ ] Each wave's acceptance criteria confirmed (manual or automated)
|
|
3883
|
+
- [ ] New code passes lint check
|
|
3884
|
+
- [ ] No new type errors or warnings introduced
|
|
3885
|
+
`;
|
|
3886
|
+
var CONTEXT_TEMPLATE = `# Context: {{name}}
|
|
3887
|
+
|
|
3888
|
+
> Phase implementation decisions document. Captures architecture decisions, interface contracts, and implementation constraints for this phase. Written during the discuss phase.
|
|
3889
|
+
|
|
3890
|
+
---
|
|
3891
|
+
|
|
3892
|
+
## Phase Goals
|
|
3893
|
+
<!-- What does this phase deliver? -->
|
|
3894
|
+
|
|
3895
|
+
{{phase-goals}}
|
|
3896
|
+
|
|
3897
|
+
---
|
|
3898
|
+
|
|
3899
|
+
## Architecture Decisions
|
|
3900
|
+
|
|
3901
|
+
<!-- Numbered decisions: D1, D2, ... Each decision records what was chosen and why. -->
|
|
3902
|
+
|
|
3903
|
+
### D1: {{decision-title}}
|
|
3904
|
+
- **Decision**: {{what we decided}}
|
|
3905
|
+
- **Rationale**: {{why}}
|
|
3906
|
+
- **Alternatives considered**: {{what else we evaluated}}
|
|
3907
|
+
|
|
3908
|
+
### D2: {{decision-title}}
|
|
3909
|
+
- **Decision**: {{what we decided}}
|
|
3910
|
+
- **Rationale**: {{why}}
|
|
3911
|
+
- **Alternatives considered**: {{what else we evaluated}}
|
|
3912
|
+
|
|
3913
|
+
---
|
|
3914
|
+
|
|
3915
|
+
## Interface Contracts
|
|
3916
|
+
|
|
3917
|
+
<!-- Key APIs and data models for this phase. -->
|
|
3918
|
+
|
|
3919
|
+
{{interface-contracts}}
|
|
3920
|
+
|
|
3921
|
+
---
|
|
3922
|
+
|
|
3923
|
+
## Implementation Constraints
|
|
3924
|
+
|
|
3925
|
+
<!-- Technical limits and boundaries for this phase. -->
|
|
3926
|
+
|
|
3927
|
+
{{constraints}}
|
|
3928
|
+
|
|
3929
|
+
---
|
|
3930
|
+
|
|
3931
|
+
## Change Split Plan
|
|
3932
|
+
|
|
3933
|
+
<!-- Preliminary breakdown of this phase into changes. -->
|
|
3934
|
+
|
|
3935
|
+
{{change-split-plan}}
|
|
3936
|
+
|
|
3937
|
+
---
|
|
3938
|
+
|
|
3939
|
+
## Non-Goals
|
|
3940
|
+
|
|
3941
|
+
<!-- Explicitly excluded from this phase. -->
|
|
3942
|
+
|
|
3943
|
+
{{non-goals}}
|
|
3944
|
+
`;
|
|
3945
|
+
var RESEARCH_TEMPLATE = `# Research: {{name}}
|
|
3946
|
+
|
|
3947
|
+
> Technical research document. Compares alternatives, assesses feasibility, and produces recommendations.
|
|
3948
|
+
|
|
3949
|
+
---
|
|
3950
|
+
|
|
3951
|
+
## Research Scope
|
|
3952
|
+
|
|
3953
|
+
{{scope}}
|
|
3954
|
+
|
|
3955
|
+
---
|
|
3956
|
+
|
|
3957
|
+
## Candidate Comparison
|
|
3958
|
+
|
|
3959
|
+
| Criterion | Option A: {{name-a}} | Option B: {{name-b}} | Option C: {{name-c}} |
|
|
3960
|
+
|-----------|---------------------|---------------------|---------------------|
|
|
3961
|
+
| {{criterion-1}} | {{score}} | {{score}} | {{score}} |
|
|
3962
|
+
| {{criterion-2}} | {{score}} | {{score}} | {{score}} |
|
|
3963
|
+
|
|
3964
|
+
---
|
|
3965
|
+
|
|
3966
|
+
## Recommendation
|
|
3967
|
+
|
|
3968
|
+
**Recommended**: {{recommended-option}}
|
|
3969
|
+
|
|
3970
|
+
**Rationale**: {{rationale}}
|
|
3971
|
+
|
|
3972
|
+
---
|
|
3973
|
+
|
|
3974
|
+
## Risk Assessment
|
|
3975
|
+
|
|
3976
|
+
| Risk | Likelihood | Impact | Mitigation |
|
|
3977
|
+
|------|-----------|--------|-----------|
|
|
3978
|
+
| {{risk-1}} | {{likelihood}} | {{impact}} | {{mitigation}} |
|
|
3979
|
+
|
|
3980
|
+
---
|
|
3981
|
+
|
|
3982
|
+
## Open Questions
|
|
3983
|
+
|
|
3984
|
+
- {{question-1}}
|
|
3985
|
+
- {{question-2}}
|
|
3986
|
+
`;
|
|
3987
|
+
var SUMMARY_TEMPLATE = `# Summary: {{name}}
|
|
3988
|
+
|
|
3989
|
+
> Change completion summary. Generated after all waves are complete.
|
|
3990
|
+
|
|
3991
|
+
---
|
|
3992
|
+
|
|
3993
|
+
## Intent Recap
|
|
3994
|
+
{{intent}}
|
|
3995
|
+
|
|
3996
|
+
## Files Changed
|
|
3997
|
+
| File | Action | Lines |
|
|
3998
|
+
|------|--------|-------|
|
|
3999
|
+
| {{file-1}} | {{action}} | {{lines}} |
|
|
4000
|
+
|
|
4001
|
+
## Key Decisions
|
|
4002
|
+
- {{decision-1}}
|
|
4003
|
+
- {{decision-2}}
|
|
4004
|
+
|
|
4005
|
+
## Verification
|
|
4006
|
+
- [ ] All tests pass
|
|
4007
|
+
- [ ] Type check passes
|
|
4008
|
+
- [ ] Delta-specs covered
|
|
4009
|
+
`;
|
|
4010
|
+
var VERIFICATION_TEMPLATE = `# Verification: {{name}}
|
|
4011
|
+
|
|
4012
|
+
> Goal-backward verification report. Confirms the change delivers what it promised.
|
|
4013
|
+
|
|
4014
|
+
---
|
|
4015
|
+
|
|
4016
|
+
## Status: {{status}}
|
|
4017
|
+
|
|
4018
|
+
<!-- passed | gaps_found | human_needed -->
|
|
4019
|
+
|
|
4020
|
+
## Delta-Spec Coverage
|
|
4021
|
+
|
|
4022
|
+
| Spec Item | Test Coverage | Status |
|
|
4023
|
+
|-----------|--------------|--------|
|
|
4024
|
+
| {{spec-item-1}} | {{test}} | {{status}} |
|
|
4025
|
+
|
|
4026
|
+
## TDD Commit Integrity
|
|
4027
|
+
|
|
4028
|
+
| Task | RED | GREEN | REFACTOR | Status |
|
|
4029
|
+
|------|-----|-------|----------|--------|
|
|
4030
|
+
| {{task-1}} | {{commit}} | {{commit}} | {{commit}} | {{status}} |
|
|
4031
|
+
|
|
4032
|
+
## Test Suite
|
|
4033
|
+
|
|
4034
|
+
- Total: {{total}}
|
|
4035
|
+
- Passed: {{passed}}
|
|
4036
|
+
- Failed: {{failed}}
|
|
4037
|
+
- Skipped: {{skipped}}
|
|
4038
|
+
|
|
4039
|
+
## Findings
|
|
4040
|
+
|
|
4041
|
+
{{findings}}
|
|
4042
|
+
`;
|
|
4043
|
+
var SPEC_REVIEW_TEMPLATE = `# Spec Review: {{name}}
|
|
4044
|
+
|
|
4045
|
+
> Specification compliance review. Cross-references delta-spec SHALL/MUST constraints against implementation.
|
|
4046
|
+
|
|
4047
|
+
---
|
|
4048
|
+
|
|
4049
|
+
## Overall: {{verdict}}
|
|
4050
|
+
|
|
4051
|
+
<!-- PASS / FAIL / NEEDS_REVISION -->
|
|
4052
|
+
|
|
4053
|
+
## Constraint Checklist
|
|
4054
|
+
|
|
4055
|
+
| # | Constraint | Location | Status | Evidence |
|
|
4056
|
+
|---|-----------|----------|--------|----------|
|
|
4057
|
+
| 1 | {{constraint}} | {{file:line}} | PASS / FAIL / N/A | {{note}} |
|
|
4058
|
+
|
|
4059
|
+
## Edge Case Coverage
|
|
4060
|
+
|
|
4061
|
+
| Edge Case | Covered? | Evidence |
|
|
4062
|
+
|-----------|---------|----------|
|
|
4063
|
+
| {{edge-case}} | {{yes/no}} | {{note}} |
|
|
4064
|
+
|
|
4065
|
+
## Findings
|
|
4066
|
+
|
|
4067
|
+
{{findings}}
|
|
4068
|
+
`;
|
|
4069
|
+
var QUALITY_REVIEW_TEMPLATE = `# Quality Review: {{name}}
|
|
4070
|
+
|
|
4071
|
+
> Code quality audit. Checks for bugs, security issues, conventions, and common AI mistakes.
|
|
4072
|
+
|
|
4073
|
+
---
|
|
4074
|
+
|
|
4075
|
+
## Overall: {{verdict}}
|
|
4076
|
+
|
|
4077
|
+
<!-- PASS / FAIL / NEEDS_REVISION -->
|
|
4078
|
+
|
|
4079
|
+
## Issues
|
|
4080
|
+
|
|
4081
|
+
| # | Severity | Category | Location | Description |
|
|
4082
|
+
|---|----------|----------|----------|-------------|
|
|
4083
|
+
| 1 | BLOCKER / MAJOR / MINOR / INFO | {{category}} | {{file:line}} | {{description}} |
|
|
4084
|
+
|
|
4085
|
+
## Convention Compliance
|
|
4086
|
+
|
|
4087
|
+
| Rule | Status | Note |
|
|
4088
|
+
|------|--------|------|
|
|
4089
|
+
| {{rule}} | {{status}} | {{note}} |
|
|
4090
|
+
|
|
4091
|
+
## Findings
|
|
4092
|
+
|
|
4093
|
+
{{findings}}
|
|
4094
|
+
`;
|
|
4095
|
+
var GOAL_REVIEW_TEMPLATE = `# Goal Review: {{name}}
|
|
4096
|
+
|
|
4097
|
+
> Goal achievement review. Cross-references proposal.md goals and must_haves against implementation.
|
|
4098
|
+
|
|
4099
|
+
---
|
|
4100
|
+
|
|
4101
|
+
## Overall: {{verdict}}
|
|
4102
|
+
|
|
4103
|
+
<!-- PASS / FAIL / NEEDS_REVISION -->
|
|
4104
|
+
|
|
4105
|
+
## Goal Checklist
|
|
4106
|
+
|
|
4107
|
+
| # | Goal / Must-have | Status | Evidence |
|
|
4108
|
+
|---|-----------------|--------|----------|
|
|
4109
|
+
| 1 | {{goal}} | ACHIEVED / PARTIAL / NOT_ACHIEVED | {{note}} |
|
|
4110
|
+
|
|
4111
|
+
## Completeness Assessment
|
|
4112
|
+
|
|
4113
|
+
{{assessment}}
|
|
4114
|
+
|
|
4115
|
+
## Findings
|
|
4116
|
+
|
|
4117
|
+
{{findings}}
|
|
4118
|
+
`;
|
|
4119
|
+
var CHANGE_SUMMARY_TEMPLATE = `# Change Summary: {{name}}
|
|
4120
|
+
|
|
4121
|
+
> Auto-generated summary after all waves complete.
|
|
4122
|
+
|
|
4123
|
+
---
|
|
4124
|
+
|
|
4125
|
+
## Intent
|
|
4126
|
+
{{intent}}
|
|
4127
|
+
|
|
4128
|
+
## Output Files
|
|
4129
|
+
| File | Action |
|
|
4130
|
+
|------|--------|
|
|
4131
|
+
| {{file}} | {{action}} |
|
|
4132
|
+
|
|
4133
|
+
## Key Decisions
|
|
4134
|
+
- {{decision}}
|
|
4135
|
+
|
|
4136
|
+
## Verification Results
|
|
4137
|
+
- Type check: {{typecheck}}
|
|
4138
|
+
- Tests: {{tests}}
|
|
4139
|
+
- Lint: {{lint}}
|
|
4140
|
+
`;
|
|
4141
|
+
var CODEBASE_STACK_TEMPLATE = `# Tech Stack: {{name}}
|
|
4142
|
+
|
|
4143
|
+
> Codebase analysis \u2014 technology stack identified from brownfield scan.
|
|
4144
|
+
|
|
4145
|
+
---
|
|
4146
|
+
|
|
4147
|
+
## Runtime
|
|
4148
|
+
- **Language**: {{language}}
|
|
4149
|
+
- **Version**: {{version}}
|
|
4150
|
+
- **Package manager**: {{package-manager}}
|
|
4151
|
+
|
|
4152
|
+
## Frameworks & Libraries
|
|
4153
|
+
| Dependency | Version | Purpose |
|
|
4154
|
+
|-----------|---------|--------|
|
|
4155
|
+
| {{dep-1}} | {{version}} | {{purpose}} |
|
|
4156
|
+
|
|
4157
|
+
## Build & Tooling
|
|
4158
|
+
- **Bundler**: {{bundler}}
|
|
4159
|
+
- **Test runner**: {{test-runner}}
|
|
4160
|
+
- **Linter**: {{linter}}
|
|
4161
|
+
- **Formatter**: {{formatter}}
|
|
4162
|
+
|
|
4163
|
+
## Infrastructure
|
|
4164
|
+
- **CI/CD**: {{ci-cd}}
|
|
4165
|
+
- **Deployment**: {{deployment}}
|
|
4166
|
+
- **Database**: {{database}}
|
|
4167
|
+
`;
|
|
4168
|
+
var CODEBASE_ARCHITECTURE_TEMPLATE = `# Architecture: {{name}}
|
|
4169
|
+
|
|
4170
|
+
> Codebase analysis \u2014 architecture patterns and module structure identified from brownfield scan.
|
|
4171
|
+
|
|
4172
|
+
---
|
|
4173
|
+
|
|
4174
|
+
## Module Map
|
|
4175
|
+
|
|
4176
|
+
\`\`\`text
|
|
4177
|
+
{{module-map}}
|
|
4178
|
+
\`\`\`
|
|
4179
|
+
|
|
4180
|
+
## Architectural Patterns
|
|
4181
|
+
| Pattern | Where | Notes |
|
|
4182
|
+
|---------|-------|-------|
|
|
4183
|
+
| {{pattern-1}} | {{location}} | {{notes}} |
|
|
4184
|
+
|
|
4185
|
+
## Key Abstractions
|
|
4186
|
+
- **{{abstraction-1}}**: {{description}}
|
|
4187
|
+
|
|
4188
|
+
## Data Flow
|
|
4189
|
+
{{data-flow}}
|
|
4190
|
+
`;
|
|
4191
|
+
var CODEBASE_CONVENTIONS_TEMPLATE = `# Conventions: {{name}}
|
|
4192
|
+
|
|
4193
|
+
> Codebase analysis \u2014 coding conventions identified from brownfield scan.
|
|
4194
|
+
|
|
4195
|
+
---
|
|
4196
|
+
|
|
4197
|
+
## Naming
|
|
4198
|
+
- **Files**: {{file-naming}}
|
|
4199
|
+
- **Variables**: {{var-naming}}
|
|
4200
|
+
- **Functions**: {{func-naming}}
|
|
4201
|
+
- **Types/Interfaces**: {{type-naming}}
|
|
4202
|
+
|
|
4203
|
+
## Code Style
|
|
4204
|
+
- **Indentation**: {{indent}}
|
|
4205
|
+
- **Quotes**: {{quotes}}
|
|
4206
|
+
- **Semicolons**: {{semicolons}}
|
|
4207
|
+
- **Max line length**: {{max-line}}
|
|
4208
|
+
|
|
4209
|
+
## Import Style
|
|
4210
|
+
- **Pattern**: {{import-pattern}}
|
|
4211
|
+
- **Path aliases**: {{aliases}}
|
|
4212
|
+
|
|
4213
|
+
## Testing Conventions
|
|
4214
|
+
- **Test location**: {{test-location}}
|
|
4215
|
+
- **Test naming**: {{test-naming}}
|
|
4216
|
+
- **Test framework patterns**: {{test-patterns}}
|
|
4217
|
+
`;
|
|
4218
|
+
var CODEBASE_PITFALLS_TEMPLATE = `# Pitfalls: {{name}}
|
|
4219
|
+
|
|
4220
|
+
> Codebase analysis \u2014 known pitfalls, anti-patterns, and technical debt identified from brownfield scan.
|
|
4221
|
+
|
|
4222
|
+
---
|
|
4223
|
+
|
|
4224
|
+
## Anti-Patterns
|
|
4225
|
+
| Pattern | Location | Risk |
|
|
4226
|
+
|---------|----------|------|
|
|
4227
|
+
| {{anti-1}} | {{location}} | {{risk}} |
|
|
4228
|
+
|
|
4229
|
+
## Technical Debt
|
|
4230
|
+
| Item | Impact | Effort to Fix |
|
|
4231
|
+
|------|--------|--------------|
|
|
4232
|
+
| {{debt-1}} | {{impact}} | {{effort}} |
|
|
4233
|
+
|
|
4234
|
+
## Risky Areas
|
|
4235
|
+
| Area | Why Risky | Mitigation |
|
|
4236
|
+
|------|----------|-----------|
|
|
4237
|
+
| {{area-1}} | {{reason}} | {{mitigation}} |
|
|
4238
|
+
|
|
4239
|
+
## Dependencies at Risk
|
|
4240
|
+
| Dependency | Version | Latest | Risk |
|
|
4241
|
+
|-----------|---------|--------|------|
|
|
4242
|
+
| {{dep-1}} | {{version}} | {{latest}} | {{risk}} |
|
|
4243
|
+
`;
|
|
4244
|
+
var PHASE_RESEARCH_TEMPLATE = `# Phase Research: {{name}}
|
|
4245
|
+
|
|
4246
|
+
> Implementation path investigation for a specific phase.
|
|
4247
|
+
|
|
4248
|
+
---
|
|
4249
|
+
|
|
4250
|
+
## Research Scope
|
|
4251
|
+
{{scope}}
|
|
4252
|
+
|
|
4253
|
+
## Recommended Approach
|
|
4254
|
+
**Recommendation**: {{recommendation}}
|
|
4255
|
+
|
|
4256
|
+
**Rationale**: {{rationale}}
|
|
4257
|
+
|
|
4258
|
+
## Alternatives Considered
|
|
4259
|
+
| Approach | Pros | Cons | Verdict |
|
|
4260
|
+
|----------|------|------|--------|
|
|
4261
|
+
| {{alt-1}} | {{pros}} | {{cons}} | {{verdict}} |
|
|
4262
|
+
|
|
4263
|
+
## Known Pitfalls
|
|
4264
|
+
- {{pitfall-1}}
|
|
4265
|
+
- {{pitfall-2}}
|
|
4266
|
+
|
|
4267
|
+
## TDD Implications
|
|
4268
|
+
- {{tdd-note-1}}
|
|
4269
|
+
`;
|
|
4270
|
+
var ARTIFACT_TEMPLATES = {
|
|
4271
|
+
proposal: PROPOSAL_TEMPLATE,
|
|
4272
|
+
design: DESIGN_TEMPLATE,
|
|
4273
|
+
tasks: TASKS_TEMPLATE,
|
|
4274
|
+
context: CONTEXT_TEMPLATE,
|
|
4275
|
+
research: RESEARCH_TEMPLATE,
|
|
4276
|
+
summary: SUMMARY_TEMPLATE,
|
|
4277
|
+
verification: VERIFICATION_TEMPLATE,
|
|
4278
|
+
"spec-review": SPEC_REVIEW_TEMPLATE,
|
|
4279
|
+
"quality-review": QUALITY_REVIEW_TEMPLATE,
|
|
4280
|
+
"goal-review": GOAL_REVIEW_TEMPLATE,
|
|
4281
|
+
"change-summary": CHANGE_SUMMARY_TEMPLATE,
|
|
4282
|
+
// Codebase analysis (brownfield — produced by specwf-codebase-mapper)
|
|
4283
|
+
"codebase-stack": CODEBASE_STACK_TEMPLATE,
|
|
4284
|
+
"codebase-architecture": CODEBASE_ARCHITECTURE_TEMPLATE,
|
|
4285
|
+
"codebase-conventions": CODEBASE_CONVENTIONS_TEMPLATE,
|
|
4286
|
+
"codebase-pitfalls": CODEBASE_PITFALLS_TEMPLATE,
|
|
4287
|
+
// Phase research (produced by specwf-phase-researcher)
|
|
4288
|
+
"phase-research": PHASE_RESEARCH_TEMPLATE
|
|
4289
|
+
};
|
|
4290
|
+
var TEMPLATE_IDS = Object.keys(ARTIFACT_TEMPLATES);
|
|
4291
|
+
|
|
4292
|
+
// src/commands/specwf-template.ts
|
|
4293
|
+
function register9(program2) {
|
|
4294
|
+
program2.command("template <type>").description(`Generate template file (${TEMPLATE_IDS.join("|")})`).option("--name <name>", "change name", "my-change").option("--dir <path>", "target directory (default specwf/changes/<name>/)").action(templateHandler);
|
|
4295
|
+
}
|
|
4296
|
+
function templateHandler(type, options) {
|
|
4297
|
+
const template = ARTIFACT_TEMPLATES[type];
|
|
4298
|
+
if (!template) {
|
|
4299
|
+
console.error(`Unknown template type: ${type}. Available: ${TEMPLATE_IDS.join(", ")}`);
|
|
4300
|
+
process.exit(1);
|
|
4301
|
+
}
|
|
4302
|
+
let content = template;
|
|
4303
|
+
const name = options.name;
|
|
4304
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4305
|
+
content = content.replace(/\{\{name\}\}/g, name);
|
|
4306
|
+
content = content.replace(/\{\{date\}\}/g, date);
|
|
4307
|
+
const filenames = {
|
|
4308
|
+
proposal: "proposal.md",
|
|
4309
|
+
design: "design.md",
|
|
4310
|
+
tasks: "tasks.md",
|
|
4311
|
+
context: "context.md",
|
|
4312
|
+
research: "research.md",
|
|
4313
|
+
summary: "summary.md",
|
|
4314
|
+
verification: "verification.md",
|
|
4315
|
+
"spec-review": "spec-review.md",
|
|
4316
|
+
"quality-review": "quality-review.md",
|
|
4317
|
+
"goal-review": "goal-review.md",
|
|
4318
|
+
"change-summary": "change-summary.md",
|
|
4319
|
+
"codebase-stack": "codebase/stack.md",
|
|
4320
|
+
"codebase-architecture": "codebase/architecture.md",
|
|
4321
|
+
"codebase-conventions": "codebase/conventions.md",
|
|
4322
|
+
"codebase-pitfalls": "codebase/pitfalls.md",
|
|
4323
|
+
"phase-research": "phase-research.md"
|
|
4324
|
+
};
|
|
4325
|
+
const filename = filenames[type] ?? `${type}.md`;
|
|
4326
|
+
let targetDir;
|
|
4327
|
+
if (options.dir) {
|
|
4328
|
+
targetDir = options.dir.startsWith("/") ? options.dir : join17(process.cwd(), options.dir);
|
|
4329
|
+
} else {
|
|
4330
|
+
targetDir = join17(process.cwd(), "specwf", "changes", name);
|
|
4331
|
+
}
|
|
4332
|
+
mkdirSync7(targetDir, { recursive: true });
|
|
4333
|
+
const fullPath = join17(targetDir, filename);
|
|
4334
|
+
const fullDir = dirname2(fullPath);
|
|
4335
|
+
if (fullDir !== targetDir) {
|
|
4336
|
+
mkdirSync7(fullDir, { recursive: true });
|
|
4337
|
+
}
|
|
4338
|
+
writeFileSync9(fullPath, content, "utf-8");
|
|
4339
|
+
console.log(`\u2713 Created ${fullPath}`);
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
// src/commands/specwf-change.ts
|
|
4343
|
+
import { join as join18 } from "path";
|
|
4344
|
+
import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
|
|
4345
|
+
function register10(program2) {
|
|
4346
|
+
const cmd = program2.command("change").description("Manage changes (create/list)");
|
|
4347
|
+
cmd.command("new <name>").description("Create a change. Default: fast path (all artifacts). Use --full for full plan cycle.").option("--dir <path>", "specwf directory", "specwf").option("--full", "Full cycle: proposal only, goes through plan phase").option("--intent <text>", "one-line intent for the proposal").action(newChange);
|
|
4348
|
+
cmd.action(() => {
|
|
4349
|
+
console.log("Usage: specwf change new <name> [--full] [--intent <text>]");
|
|
4350
|
+
});
|
|
4351
|
+
}
|
|
4352
|
+
function newChange(name, options) {
|
|
4353
|
+
const specwfDir = join18(process.cwd(), options.dir);
|
|
4354
|
+
const changeDir = createAdhocChangeDir(specwfDir, name);
|
|
4355
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4356
|
+
if (options.full) {
|
|
4357
|
+
const content = (ARTIFACT_TEMPLATES["proposal"] ?? `# Proposal: ${name}
|
|
4358
|
+
`).replace(/\{\{name\}\}/g, name).replace(/\{\{date\}\}/g, date);
|
|
4359
|
+
writeFileSync10(join18(changeDir, "proposal.md"), content, "utf-8");
|
|
4360
|
+
console.log(`\u2713 Created change: changes/${name}/ (full cycle)`);
|
|
4361
|
+
console.log(` proposal.md \u2014 fill in, then plan \u2192 apply \u2192 review \u2192 verify \u2192 archive`);
|
|
4362
|
+
updateState(specwfDir, (state) => {
|
|
4363
|
+
state.adhoc.push({ name, status: "proposal", depends_on: [] });
|
|
4364
|
+
});
|
|
4365
|
+
} else {
|
|
4366
|
+
const templates = {
|
|
4367
|
+
"proposal.md": (ARTIFACT_TEMPLATES["proposal"] ?? `# Proposal: ${name}
|
|
4368
|
+
`).replace(/\{\{name\}\}/g, name).replace(/\{\{date\}\}/g, date).replace("{{intent}}", options.intent || "{{intent}}"),
|
|
4369
|
+
"design.md": (ARTIFACT_TEMPLATES["design"] ?? `# Design: ${name}
|
|
4370
|
+
`).replace(/\{\{name\}\}/g, name).replace(/\{\{date\}\}/g, date),
|
|
4371
|
+
"tasks.md": (ARTIFACT_TEMPLATES["tasks"] ?? `# Tasks: ${name}
|
|
4372
|
+
`).replace(/\{\{name\}\}/g, name).replace(/\{\{date\}\}/g, date)
|
|
4373
|
+
};
|
|
4374
|
+
for (const [filename, content] of Object.entries(templates)) {
|
|
4375
|
+
writeFileSync10(join18(changeDir, filename), content, "utf-8");
|
|
4376
|
+
}
|
|
4377
|
+
const specsDir = join18(changeDir, "specs", name);
|
|
4378
|
+
mkdirSync8(specsDir, { recursive: true });
|
|
4379
|
+
writeFileSync10(
|
|
4380
|
+
join18(specsDir, "spec.md"),
|
|
4381
|
+
`# ${name} \u2014 Delta Spec
|
|
4382
|
+
|
|
4383
|
+
## ADDED Requirements
|
|
4384
|
+
|
|
4385
|
+
### Requirement: <name>
|
|
4386
|
+
|
|
4387
|
+
The system SHALL <behavior>.
|
|
4388
|
+
|
|
4389
|
+
#### Scenario: <name>
|
|
4390
|
+
- **GIVEN** <precondition>
|
|
4391
|
+
- **WHEN** <trigger>
|
|
4392
|
+
- **THEN** <expected outcome>
|
|
4393
|
+
`,
|
|
4394
|
+
"utf-8"
|
|
4395
|
+
);
|
|
4396
|
+
console.log(`\u2713 Created change: changes/${name}/ (fast path)`);
|
|
4397
|
+
console.log(` proposal.md \u2014 fill in intent, scope, must-haves`);
|
|
4398
|
+
console.log(` design.md \u2014 fill in technical approach`);
|
|
4399
|
+
console.log(` tasks.md \u2014 fill in implementation tasks`);
|
|
4400
|
+
console.log(` specs/${name}/spec.md \u2014 fill in behavioral contracts`);
|
|
4401
|
+
updateState(specwfDir, (state) => {
|
|
4402
|
+
state.adhoc.push({ name, status: "planning", depends_on: [] });
|
|
4403
|
+
});
|
|
4404
|
+
}
|
|
4405
|
+
console.log(`\u2713 state.md updated`);
|
|
4406
|
+
console.log("");
|
|
4407
|
+
console.log(`\u2192 Next: fill in artifacts, then \`specwf continue change ${name}\``);
|
|
4408
|
+
}
|
|
4409
|
+
|
|
4410
|
+
// src/cli.ts
|
|
4411
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
4412
|
+
var pkg = JSON.parse(readFileSync12(join19(__dirname2, "..", "package.json"), "utf-8"));
|
|
4413
|
+
var version = pkg.version;
|
|
4414
|
+
program.name("specwf").description("\u89C4\u683C\u9A71\u52A8\u5F00\u53D1\u5DE5\u4F5C\u6D41 \u2014 spec-driven development workflow").version(version);
|
|
4415
|
+
register(program);
|
|
4416
|
+
register2(program);
|
|
4417
|
+
register3(program);
|
|
4418
|
+
register4(program);
|
|
4419
|
+
register5(program);
|
|
4420
|
+
register6(program);
|
|
4421
|
+
register7(program);
|
|
4422
|
+
register8(program);
|
|
4423
|
+
register9(program);
|
|
4424
|
+
register10(program);
|
|
4425
|
+
program.parse(process.argv);
|