specwf 0.2.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.
Files changed (55) hide show
  1. package/README.md +79 -0
  2. package/bin/specwf.js +2 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +1800 -0
  5. package/dist/templates/agents/archiver.md +112 -0
  6. package/dist/templates/agents/executor.md +125 -0
  7. package/dist/templates/agents/planner.md +114 -0
  8. package/dist/templates/agents/researcher.md +104 -0
  9. package/dist/templates/agents/reviewer.md +98 -0
  10. package/dist/templates/agents/verifier.md +129 -0
  11. package/dist/templates/artifacts/context.md +151 -0
  12. package/dist/templates/artifacts/design.md +224 -0
  13. package/dist/templates/artifacts/goal-review.md +95 -0
  14. package/dist/templates/artifacts/project.yml +117 -0
  15. package/dist/templates/artifacts/proposal.md +124 -0
  16. package/dist/templates/artifacts/quality-review.md +131 -0
  17. package/dist/templates/artifacts/research.md +127 -0
  18. package/dist/templates/artifacts/spec-review.md +84 -0
  19. package/dist/templates/artifacts/state.md +158 -0
  20. package/dist/templates/artifacts/summary.md +129 -0
  21. package/dist/templates/artifacts/tasks.md +109 -0
  22. package/dist/templates/artifacts/verification.md +113 -0
  23. package/dist/templates/commands/adhoc.md +38 -0
  24. package/dist/templates/commands/apply.md +20 -0
  25. package/dist/templates/commands/archive.md +21 -0
  26. package/dist/templates/commands/continue.md +27 -0
  27. package/dist/templates/commands/discuss.md +24 -0
  28. package/dist/templates/commands/grill.md +24 -0
  29. package/dist/templates/commands/init.md +20 -0
  30. package/dist/templates/commands/milestone.md +27 -0
  31. package/dist/templates/commands/plan.md +22 -0
  32. package/dist/templates/commands/research-phase.md +20 -0
  33. package/dist/templates/commands/research.md +24 -0
  34. package/dist/templates/commands/review.md +20 -0
  35. package/dist/templates/commands/roadmap.md +23 -0
  36. package/dist/templates/commands/ship.md +36 -0
  37. package/dist/templates/commands/split.md +16 -0
  38. package/dist/templates/commands/verify.md +22 -0
  39. package/dist/templates/skills/adhoc.md +53 -0
  40. package/dist/templates/skills/apply.md +134 -0
  41. package/dist/templates/skills/archive.md +109 -0
  42. package/dist/templates/skills/continue.md +97 -0
  43. package/dist/templates/skills/discuss.md +95 -0
  44. package/dist/templates/skills/grill.md +90 -0
  45. package/dist/templates/skills/init.md +116 -0
  46. package/dist/templates/skills/milestone.md +36 -0
  47. package/dist/templates/skills/plan.md +144 -0
  48. package/dist/templates/skills/research-phase.md +66 -0
  49. package/dist/templates/skills/research.md +76 -0
  50. package/dist/templates/skills/review.md +148 -0
  51. package/dist/templates/skills/roadmap.md +104 -0
  52. package/dist/templates/skills/ship.md +94 -0
  53. package/dist/templates/skills/split.md +96 -0
  54. package/dist/templates/skills/verify.md +147 -0
  55. package/package.json +56 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1800 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { program } from "commander";
5
+
6
+ // src/commands/specwf-init.ts
7
+ import { join as join5 } from "path";
8
+
9
+ // src/core/config.ts
10
+ import { join } from "path";
11
+ import { existsSync } from "fs";
12
+ import { z } from "zod";
13
+
14
+ // src/parser/yaml.ts
15
+ import { readFileSync, writeFileSync } from "fs";
16
+ import { parse, parseDocument } from "yaml";
17
+ function readYamlDoc(path) {
18
+ return parseDocument(readFileSync(path, "utf-8"));
19
+ }
20
+ function writeYamlDoc(path, doc) {
21
+ writeFileSync(path, String(doc), "utf-8");
22
+ }
23
+
24
+ // src/types/config.ts
25
+ var PROFILE_MODEL_MAP = {
26
+ lite: {
27
+ research: "default",
28
+ plan: "default",
29
+ execute: "default",
30
+ review: "default",
31
+ verify: "default",
32
+ archive: "smol"
33
+ },
34
+ standard: {
35
+ research: "slow",
36
+ plan: "slow",
37
+ execute: "default",
38
+ review: "slow",
39
+ verify: "default",
40
+ archive: "default"
41
+ },
42
+ strict: {
43
+ research: "slow:high",
44
+ plan: "slow:high",
45
+ execute: "slow",
46
+ review: "slow:high",
47
+ verify: "slow",
48
+ archive: "default"
49
+ }
50
+ };
51
+
52
+ // src/core/config.ts
53
+ import { Document as Document2 } from "yaml";
54
+ var CONFIG_FILE = "project.yml";
55
+ var ProjectConfigSchema = z.object({
56
+ version: z.number(),
57
+ platform: z.array(z.string()),
58
+ profile: z.enum(["lite", "standard", "strict"]),
59
+ context: z.string(),
60
+ workflow: z.object({
61
+ research: z.boolean().optional(),
62
+ plan_check: z.boolean().optional(),
63
+ tdd: z.boolean().optional(),
64
+ triple_review: z.boolean().optional(),
65
+ auto_advance: z.boolean().optional(),
66
+ spec_injection: z.boolean().optional()
67
+ }).optional().default({}),
68
+ review: z.object({
69
+ gate: z.enum(["all-pass", "severity", "report-only"]).optional(),
70
+ parallel: z.boolean().optional()
71
+ }).optional().default({}),
72
+ change: z.object({
73
+ parallel: z.enum(["serial", "dependency-graph", "pipeline"]).optional(),
74
+ isolation: z.boolean().optional()
75
+ }).optional().default({}),
76
+ git: z.object({
77
+ branching: z.enum(["none", "phase", "milestone"]).optional(),
78
+ create_tag: z.boolean().optional()
79
+ }).optional().default({}),
80
+ conventions: z.object({
81
+ inject: z.boolean().optional().default(true)
82
+ }).optional().default({ inject: true }),
83
+ models: z.record(z.string(), z.string()).optional().default({})
84
+ });
85
+ function configPath(specwfDir) {
86
+ return join(specwfDir, CONFIG_FILE);
87
+ }
88
+ function loadConfig(specwfDir) {
89
+ const doc = readYamlDoc(configPath(specwfDir));
90
+ const raw = doc.toJS();
91
+ return ProjectConfigSchema.parse(raw);
92
+ }
93
+ function saveConfig(specwfDir, config) {
94
+ let doc;
95
+ if (existsSync(configPath(specwfDir))) {
96
+ doc = readYamlDoc(configPath(specwfDir));
97
+ } else {
98
+ doc = new Document2({});
99
+ }
100
+ doc.set("version", config.version);
101
+ doc.set("platform", config.platform);
102
+ doc.set("profile", config.profile);
103
+ doc.set("context", config.context);
104
+ if (config.workflow) doc.set("workflow", config.workflow);
105
+ if (config.review) doc.set("review", config.review);
106
+ if (config.change) doc.set("change", config.change);
107
+ if (config.git) doc.set("git", config.git);
108
+ if (config.conventions) doc.set("conventions", config.conventions);
109
+ if (config.models) doc.set("models", config.models);
110
+ writeYamlDoc(configPath(specwfDir), doc);
111
+ }
112
+ function updateConfig(specwfDir, updater) {
113
+ const config = loadConfig(specwfDir);
114
+ updater(config);
115
+ saveConfig(specwfDir, config);
116
+ }
117
+ function resolveModels(config) {
118
+ const profile = config.profile;
119
+ const defaults = PROFILE_MODEL_MAP[profile];
120
+ return { ...defaults, ...config.models };
121
+ }
122
+
123
+ // src/core/file-tree.ts
124
+ import { mkdirSync, existsSync as existsSync2, readdirSync, statSync } from "fs";
125
+ import { join as join2 } from "path";
126
+ import { renameSync } from "fs";
127
+ var SPECWF_DIRS = [
128
+ "specs",
129
+ "conventions",
130
+ "research",
131
+ "milestones",
132
+ "changes",
133
+ "archive",
134
+ "workspace"
135
+ ];
136
+ function createSpecwfStructure(specwfDir) {
137
+ mkdirSync(specwfDir, { recursive: true });
138
+ for (const dir of SPECWF_DIRS) {
139
+ mkdirSync(join2(specwfDir, dir), { recursive: true });
140
+ }
141
+ }
142
+ function isInitialized(specwfDir) {
143
+ return existsSync2(join2(specwfDir, "project.yml")) && existsSync2(join2(specwfDir, "state.md"));
144
+ }
145
+ function createAdhocChangeDir(specwfDir, changeName) {
146
+ const dir = join2(specwfDir, "changes", changeName);
147
+ mkdirSync(dir, { recursive: true });
148
+ mkdirSync(join2(dir, "specs"), { recursive: true });
149
+ return dir;
150
+ }
151
+ function archiveChangeDir(specwfDir, changeDir) {
152
+ const changeName = changeDir.split("/").pop() ?? "unknown";
153
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
154
+ const archiveDir = join2(specwfDir, "archive", `${date}-${changeName}`);
155
+ if (existsSync2(changeDir)) {
156
+ renameSync(changeDir, archiveDir);
157
+ }
158
+ return archiveDir;
159
+ }
160
+ function listMilestones(specwfDir) {
161
+ const dir = join2(specwfDir, "milestones");
162
+ if (!existsSync2(dir)) return [];
163
+ return readdirSync(dir).filter((e) => {
164
+ const stat = statSync(join2(dir, e));
165
+ return stat.isDirectory();
166
+ });
167
+ }
168
+ function listPhases(specwfDir, milestoneId) {
169
+ const dir = join2(specwfDir, "milestones", milestoneId, "phases");
170
+ if (!existsSync2(dir)) return [];
171
+ return readdirSync(dir).filter((e) => {
172
+ const stat = statSync(join2(dir, e));
173
+ return stat.isDirectory();
174
+ });
175
+ }
176
+ function listChanges(specwfDir, milestoneId, phaseId) {
177
+ const dir = join2(specwfDir, "milestones", milestoneId, "phases", phaseId, "changes");
178
+ if (!existsSync2(dir)) return [];
179
+ return readdirSync(dir).filter((e) => {
180
+ const stat = statSync(join2(dir, e));
181
+ return stat.isDirectory();
182
+ });
183
+ }
184
+ function listAdhocChanges(specwfDir) {
185
+ const dir = join2(specwfDir, "changes");
186
+ if (!existsSync2(dir)) return [];
187
+ return readdirSync(dir).filter((e) => {
188
+ const stat = statSync(join2(dir, e));
189
+ return stat.isDirectory();
190
+ });
191
+ }
192
+ function listArchived(specwfDir) {
193
+ const dir = join2(specwfDir, "archive");
194
+ if (!existsSync2(dir)) return [];
195
+ return readdirSync(dir).filter((e) => {
196
+ const stat = statSync(join2(dir, e));
197
+ return stat.isDirectory();
198
+ });
199
+ }
200
+
201
+ // src/core/state-file.ts
202
+ import { join as join3 } from "path";
203
+ import { z as z2 } from "zod";
204
+
205
+ // src/parser/frontmatter.ts
206
+ import matter from "gray-matter";
207
+ import { readFileSync as readFileSync3 } from "fs";
208
+ function parseFrontmatter(content) {
209
+ const parsed = matter(content);
210
+ return {
211
+ data: parsed.data,
212
+ content: parsed.content
213
+ };
214
+ }
215
+ function stringifyFrontmatter(data, body) {
216
+ return matter.stringify(body, data);
217
+ }
218
+ function readFrontmatterFile(path) {
219
+ return parseFrontmatter(readFileSync3(path, "utf-8"));
220
+ }
221
+
222
+ // src/core/state-file.ts
223
+ import { writeFileSync as writeFileSync3, existsSync as existsSync3 } from "fs";
224
+ var STATE_FILE = "state.md";
225
+ var ChangeStateSchema = z2.object({
226
+ name: z2.string(),
227
+ status: z2.string(),
228
+ depends_on: z2.array(z2.string()).optional().default([])
229
+ });
230
+ var StateFileSchema = z2.object({
231
+ project: z2.object({
232
+ name: z2.string(),
233
+ status: z2.string(),
234
+ current_milestone: z2.string().nullable(),
235
+ current_phase: z2.string().nullable()
236
+ }),
237
+ active_context: z2.object({
238
+ type: z2.enum(["project", "milestone", "phase", "change", "adhoc"]),
239
+ ref: z2.string().nullable(),
240
+ step: z2.string()
241
+ }),
242
+ changes: z2.array(ChangeStateSchema).optional().default([]),
243
+ adhoc: z2.array(ChangeStateSchema).optional().default([])
244
+ });
245
+ function statePath(specwfDir) {
246
+ return join3(specwfDir, STATE_FILE);
247
+ }
248
+ function loadState(specwfDir) {
249
+ const result = readFrontmatterFile(statePath(specwfDir));
250
+ return StateFileSchema.parse(result.data);
251
+ }
252
+ function saveState(specwfDir, state) {
253
+ let body;
254
+ try {
255
+ const existing = readFrontmatterFile(statePath(specwfDir));
256
+ body = existing.content;
257
+ } catch {
258
+ body = generateStateBody(state);
259
+ }
260
+ const output = stringifyFrontmatter(state, body);
261
+ writeFileSync3(statePath(specwfDir), output, "utf-8");
262
+ }
263
+ function updateState(specwfDir, updater) {
264
+ const state = loadState(specwfDir);
265
+ updater(state);
266
+ saveState(specwfDir, state);
267
+ }
268
+ function generateStateBody(state) {
269
+ const ctx = state.active_context;
270
+ const lines = [
271
+ "# State",
272
+ "",
273
+ "## \u5F53\u524D\u4F4D\u7F6E",
274
+ "",
275
+ formatContext(state),
276
+ "",
277
+ "## \u72B6\u6001\u673A",
278
+ "",
279
+ "\u9879\u76EE\u5C42\u8DEF\u5F84: `initialized \u2192 requirements-defined \u2192 researched \u2192 roadmap-defined`",
280
+ ""
281
+ ];
282
+ if (state.project) {
283
+ lines.push("## \u5386\u53F2", "");
284
+ }
285
+ return lines.join("\n");
286
+ }
287
+ function formatContext(state) {
288
+ const { type, ref, step } = state.active_context;
289
+ switch (type) {
290
+ case "project":
291
+ return `\u9879\u76EE\u5C42 \u2014 ${step}\u3002`;
292
+ case "milestone":
293
+ return `Milestone ${state.project.current_milestone ?? "?"} \u2014 ${step}\u3002`;
294
+ case "phase":
295
+ return `Phase ${state.project.current_phase ?? "?"} \u2014 ${step}\u3002`;
296
+ case "change":
297
+ return `Change (${ref ?? "?"}) \u2014 ${step}\u3002`;
298
+ case "adhoc":
299
+ return `\u4E34\u65F6 Change (${ref ?? "?"}) \u2014 ${step}\u3002`;
300
+ default:
301
+ return step;
302
+ }
303
+ }
304
+
305
+ // src/prompts/init-wizard.ts
306
+ async function runInitWizard(defaults) {
307
+ if (defaults.yes) {
308
+ return { profile: defaults.profile, context: "", platform: ["omp"], brownfield: false };
309
+ }
310
+ try {
311
+ const clack = await import("@clack/prompts");
312
+ const val = await clack.select({
313
+ message: "\u9009\u62E9\u5DE5\u4F5C\u6D41\u4E25\u683C\u5EA6:",
314
+ options: [{ value: "lite", label: "Lite" }, { value: "standard", label: "Standard\uFF08\u63A8\u8350\uFF09" }, { value: "strict", label: "Strict" }],
315
+ initialValue: defaults.profile
316
+ });
317
+ const profile = typeof val === "string" ? val : defaults.profile;
318
+ const ctxVal = await clack.text({ message: "\u9879\u76EE\u4E0A\u4E0B\u6587\u63CF\u8FF0\uFF08\u53EF\u9009\uFF09:", placeholder: "\u6280\u672F\u6808: TypeScript, Node.js..." });
319
+ const context = typeof ctxVal === "string" ? ctxVal : "";
320
+ const pfVal = await clack.multiselect({ message: "\u9009\u62E9\u76EE\u6807\u5E73\u53F0:", options: [{ value: "omp", label: "Oh My Pi" }], initialValues: ["omp"] });
321
+ const platform = Array.isArray(pfVal) ? pfVal : ["omp"];
322
+ const bfVal = await clack.confirm({ message: "\u8FD9\u662F\u4E00\u4E2A\u5B58\u91CF\u9879\u76EE\u5417\uFF1F", initialValue: false });
323
+ const brownfield = typeof bfVal === "boolean" ? bfVal : false;
324
+ return { profile, context, platform, brownfield };
325
+ } catch {
326
+ console.log("(@clack/prompts \u672A\u5B89\u88C5\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u914D\u7F6E)");
327
+ return { profile: defaults.profile, context: "", platform: ["omp"], brownfield: false };
328
+ }
329
+ }
330
+
331
+ // src/core/brownfield.ts
332
+ import { readFileSync as readFileSync5, readdirSync as readdirSync2, existsSync as existsSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2 } from "fs";
333
+ import { join as join4 } from "path";
334
+ function detectProjectInfo(rootDir) {
335
+ const info = {
336
+ type: "unknown",
337
+ language: "unknown",
338
+ framework: "unknown",
339
+ hasPackageJson: false,
340
+ hasTests: false,
341
+ srcDirs: [],
342
+ structFiles: []
343
+ };
344
+ if (existsSync4(join4(rootDir, "package.json"))) {
345
+ info.hasPackageJson = true;
346
+ info.type = "node";
347
+ info.language = "typescript";
348
+ try {
349
+ const pkg = JSON.parse(readFileSync5(join4(rootDir, "package.json"), "utf-8"));
350
+ if (pkg.dependencies?.next) info.framework = "next.js";
351
+ else if (pkg.dependencies?.react) info.framework = "react";
352
+ else if (pkg.dependencies?.vue) info.framework = "vue";
353
+ else if (pkg.dependencies?.express) info.framework = "express";
354
+ else if (pkg.dependencies?.fastify) info.framework = "fastify";
355
+ } catch {
356
+ }
357
+ }
358
+ if (existsSync4(join4(rootDir, "Cargo.toml"))) {
359
+ info.type = "rust";
360
+ info.language = "rust";
361
+ }
362
+ if (existsSync4(join4(rootDir, "go.mod"))) {
363
+ info.type = "go";
364
+ info.language = "go";
365
+ }
366
+ for (const dir of ["src", "app", "lib", "pkg", "cmd"]) {
367
+ if (existsSync4(join4(rootDir, dir)) && readdirSync2(join4(rootDir, dir), { withFileTypes: true }).some((e) => e.isDirectory())) {
368
+ info.srcDirs.push(dir);
369
+ }
370
+ }
371
+ if (existsSync4(join4(rootDir, "__tests__")) || existsSync4(join4(rootDir, "tests"))) {
372
+ info.hasTests = true;
373
+ }
374
+ if (existsSync4(join4(rootDir, "vitest.config.ts")) || existsSync4(join4(rootDir, "jest.config.ts"))) {
375
+ info.hasTests = true;
376
+ }
377
+ return info;
378
+ }
379
+ function generateCodebaseReport(rootDir, info) {
380
+ const stack = buildStackSection(info);
381
+ const structure = buildStructureSection(rootDir);
382
+ const conventions = detectConventions(rootDir);
383
+ return { stack, structure, conventions };
384
+ }
385
+ function buildStackSection(info) {
386
+ return `# \u6280\u672F\u6808
387
+
388
+ - \u9879\u76EE\u7C7B\u578B: ${info.type}
389
+ - \u8BED\u8A00: ${info.language}
390
+ - \u6846\u67B6: ${info.framework}
391
+ - src \u76EE\u5F55: ${info.srcDirs.join(", ")}
392
+ - \u6D4B\u8BD5: ${info.hasTests ? "\u6709" : "\u65E0"}
393
+ `;
394
+ }
395
+ function buildStructureSection(rootDir) {
396
+ const lines = ["# \u9879\u76EE\u7ED3\u6784", ""];
397
+ try {
398
+ 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);
399
+ lines.push(...entries);
400
+ } catch {
401
+ }
402
+ return lines.join("\n");
403
+ }
404
+ function detectConventions(rootDir) {
405
+ const configs = [];
406
+ if (existsSync4(join4(rootDir, "tsconfig.json"))) configs.push("TypeScript");
407
+ if (existsSync4(join4(rootDir, ".eslintrc.js")) || existsSync4(join4(rootDir, "eslint.config.js"))) configs.push("ESLint");
408
+ if (existsSync4(join4(rootDir, ".prettierrc"))) configs.push("Prettier");
409
+ return `# \u9879\u76EE\u7EA6\u5B9A
410
+
411
+ \u68C0\u6D4B\u5230: ${configs.length > 0 ? configs.join(", ") : "\u65E0"}`;
412
+ }
413
+ function bootstrapSpecs(rootDir, specwfDir) {
414
+ const specs = [];
415
+ if (existsSync4(join4(rootDir, "src"))) {
416
+ try {
417
+ for (const entry of readdirSync2(join4(rootDir, "src"), { withFileTypes: true })) {
418
+ if (entry.isDirectory() && !entry.name.startsWith("_")) {
419
+ const domainDir = join4(specwfDir, "specs", entry.name);
420
+ mkdirSync2(domainDir, { recursive: true });
421
+ writeFileSync4(
422
+ join4(domainDir, "spec.md"),
423
+ `# ${entry.name} Specification
424
+
425
+ ## Purpose
426
+
427
+ [\u4ECE\u4E0A\u4F4D\u4EE3\u7801\u81EA\u52A8\u63D0\u53D6\u7684\u521D\u59CB spec \u2014 \u5F85\u4EBA\u5DE5\u5BA1\u6838]
428
+
429
+ ## Requirements
430
+
431
+ `,
432
+ "utf-8"
433
+ );
434
+ specs.push(entry.name);
435
+ }
436
+ }
437
+ } catch {
438
+ }
439
+ }
440
+ if (specs.length === 0) {
441
+ const domainDir = join4(specwfDir, "specs", "general");
442
+ mkdirSync2(domainDir, { recursive: true });
443
+ writeFileSync4(
444
+ join4(domainDir, "spec.md"),
445
+ `# General Specification
446
+
447
+ ## Purpose
448
+
449
+ [\u4ECE\u4E0A\u4F4D\u4EE3\u7801\u81EA\u52A8\u63D0\u53D6\u7684\u521D\u59CB spec \u2014 \u5F85\u4EBA\u5DE5\u5BA1\u6838]
450
+
451
+ ## Requirements
452
+
453
+ `,
454
+ "utf-8"
455
+ );
456
+ specs.push("general");
457
+ }
458
+ return specs;
459
+ }
460
+ async function runBrownfieldInit(rootDir, specwfDir, info) {
461
+ const report = generateCodebaseReport(rootDir, info);
462
+ const researchDir = join4(specwfDir, "research", "codebase");
463
+ mkdirSync2(researchDir, { recursive: true });
464
+ writeFileSync4(join4(researchDir, "stack.md"), report.stack, "utf-8");
465
+ writeFileSync4(join4(researchDir, "structure.md"), report.structure, "utf-8");
466
+ writeFileSync4(join4(researchDir, "conventions.md"), report.conventions, "utf-8");
467
+ const domains = bootstrapSpecs(rootDir, specwfDir);
468
+ return domains;
469
+ }
470
+
471
+ // src/commands/specwf-init.ts
472
+ function register(program2) {
473
+ 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);
474
+ }
475
+ async function initHandler(options) {
476
+ const baseDir = options.dir.startsWith("/") ? options.dir : join5(process.cwd(), options.dir);
477
+ const specwfDir = join5(baseDir, "specwf");
478
+ if (isInitialized(specwfDir)) {
479
+ console.error("specwf \u5DF2\u521D\u59CB\u5316\u3002\u8FD0\u884C `specwf update` \u66F4\u65B0\u5E73\u53F0\u6587\u4EF6\u3002");
480
+ process.exit(1);
481
+ }
482
+ const wizard = await runInitWizard({ profile: options.profile, yes: options.yes });
483
+ const profile = wizard.profile;
484
+ const platform = wizard.platform;
485
+ const isBrownfield = options.brownfield || wizard.brownfield;
486
+ createSpecwfStructure(specwfDir);
487
+ console.log("\u2713 \u521B\u5EFA specwf/ \u76EE\u5F55\u7ED3\u6784");
488
+ saveConfig(specwfDir, {
489
+ version: 1,
490
+ platform,
491
+ profile,
492
+ context: wizard.context,
493
+ workflow: {},
494
+ review: {},
495
+ change: {},
496
+ git: { branching: "none", create_tag: true },
497
+ conventions: { inject: true },
498
+ models: {}
499
+ });
500
+ console.log("\u2713 \u521B\u5EFA project.yml (profile: " + profile + ")");
501
+ saveState(specwfDir, {
502
+ project: {
503
+ name: baseDir.split("/").pop() || "project",
504
+ status: "initialized",
505
+ current_milestone: null,
506
+ current_phase: null
507
+ },
508
+ active_context: {
509
+ type: "project",
510
+ ref: null,
511
+ step: "init"
512
+ },
513
+ changes: [],
514
+ adhoc: []
515
+ });
516
+ console.log("\u2713 \u521B\u5EFA state.md");
517
+ if (isBrownfield) {
518
+ const info = detectProjectInfo(process.cwd());
519
+ const domains = await runBrownfieldInit(process.cwd(), specwfDir, info);
520
+ console.log("\u2713 \u5B58\u91CF\u9879\u76EE codebase mapping \u5B8C\u6210 (" + domains.length + " \u4E2A spec \u57DF)");
521
+ }
522
+ console.log("specwf \u521D\u59CB\u5316\u5B8C\u6210\u3002\u8FD0\u884C `specwf update` \u751F\u6210\u5E73\u53F0\u6587\u4EF6\u3002");
523
+ }
524
+
525
+ // src/commands/specwf-update.ts
526
+ import { join as join9 } from "path";
527
+
528
+ // src/generators/omp-commands.ts
529
+ import { readFileSync as readFileSync6 } from "fs";
530
+ import { join as join6, dirname } from "path";
531
+ import { fileURLToPath } from "url";
532
+ var STEP_DEFS = [
533
+ { step: "init", name: "specwf:init", description: "\u521D\u59CB\u5316 specwf \u9879\u76EE\u7ED3\u6784", usesAgent: true, agents: ["researcher"] },
534
+ { step: "grill", name: "specwf:grill", description: "\u9700\u6C42\u63A2\u8BA8 \u2014 \u65E0\u9650\u5236\u7EC6\u8282\u63D0\u95EE\u76F4\u5230\u8FBE\u6210\u5171\u8BC6", usesAgent: false, agents: [] },
535
+ { step: "research", name: "specwf:research", description: "\u9879\u76EE\u6280\u672F\u8C03\u7814 \u2014 \u5E76\u884C\u591A\u65B9\u5411\u8C03\u7814", usesAgent: true, agents: ["researcher"] },
536
+ { step: "roadmap", name: "specwf:roadmap", description: "\u8DEF\u7EBF\u56FE \u2014 \u62C6\u5206 Milestone \xD7 Phase", usesAgent: false, agents: [] },
537
+ { step: "milestone", name: "specwf:milestone", description: "\u91CC\u7A0B\u7891\u7BA1\u7406 \u2014 \u5207\u6362/\u521B\u5EFA Milestone", usesAgent: false, agents: [] },
538
+ { step: "discuss", name: "specwf:discuss", description: "Phase \u8BA8\u8BBA \u2014 \u6355\u83B7\u5B9E\u73B0\u51B3\u7B56", usesAgent: false, agents: [] },
539
+ { step: "research-phase", name: "specwf:research-phase", description: "Phase \u8C03\u7814 \u2014 \u5B9E\u73B0\u8DEF\u5F84\u7814\u7A76", usesAgent: true, agents: ["researcher"] },
540
+ { step: "split", name: "specwf:split", description: "Change \u62C6\u5206 \u2014 \u4F9D\u8D56\u56FE + N \u4E2A Change", usesAgent: false, agents: [] },
541
+ { step: "adhoc", name: "specwf:adhoc", description: "\u4E34\u65F6 Change \u2014 \u4E0E milestone/phase \u65E0\u5173\u7684\u72EC\u7ACB\u53D8\u66F4", usesAgent: false, agents: [] },
542
+ { step: "plan", name: "specwf:plan", description: "Change \u8BBE\u8BA1 \u2014 design+tasks+delta-specs", usesAgent: true, agents: ["planner"] },
543
+ { step: "apply", name: "specwf:apply", description: "\u4EE3\u7801\u5B9E\u73B0 \u2014 TDD RED\u2192GREEN\u2192REFACTOR", usesAgent: true, agents: ["executor"] },
544
+ { step: "review", name: "specwf:review", description: "\u4E09\u91CD\u5BA1\u67E5 \u2014 \u89C4\u683C/\u8D28\u91CF/\u76EE\u6807\u5E76\u884C", usesAgent: true, agents: ["reviewer"] },
545
+ { step: "verify", name: "specwf:verify", description: "\u6D4B\u8BD5\u9A8C\u8BC1 \u2014 \u8BCA\u65AD+\u8DEF\u7531\u56DE\u73AF", usesAgent: true, agents: ["verifier"] },
546
+ { step: "archive", name: "specwf:archive", description: "\u5F52\u6863 \u2014 delta \u5408\u5E76 + \u4EE3\u7801\u8BA4\u77E5\u56DE\u704C", usesAgent: false, agents: [] },
547
+ { step: "ship", name: "specwf:ship", description: "\u4EA4\u4ED8 \u2014 PR + STATE \u66F4\u65B0 / release tag", usesAgent: false, agents: [] },
548
+ { step: "continue", name: "specwf:continue", description: "\u81EA\u52A8\u63A8\u8FDB \u2014 \u8BFB STATE \u786E\u5B9A\u4E0B\u4E00\u6B65", usesAgent: false, agents: [] }
549
+ ];
550
+ var __dirname = dirname(fileURLToPath(import.meta.url));
551
+ var TEMPLATES_DIR = join6(__dirname, "templates", "commands");
552
+ function loadTemplate(step) {
553
+ return readFileSync6(join6(TEMPLATES_DIR, `${step}.md`), "utf-8");
554
+ }
555
+ function renderTemplate(template, vars) {
556
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
557
+ }
558
+ function generateSlashCommand(def, config) {
559
+ const body = def.bodyOverride ?? loadAndRenderTemplate(def);
560
+ return `---
561
+ name: ${def.name}
562
+ description: ${def.description}
563
+ ---
564
+
565
+ ${body}
566
+ `;
567
+ }
568
+ function loadAndRenderTemplate(def) {
569
+ try {
570
+ return renderTemplate(loadTemplate(def.step), {
571
+ step: def.step,
572
+ description: def.description,
573
+ usesAgent: String(def.usesAgent),
574
+ agents: def.agents.join(", ")
575
+ });
576
+ } catch {
577
+ return fallbackBody(def);
578
+ }
579
+ }
580
+ function fallbackBody(def) {
581
+ const agentsSection = def.usesAgent && def.agents.length > 0 ? `\u8C03\u7528 task \u5DE5\u5177 fan-out \`specwf-${def.agents[0]}\` agent\u3002` : "\u672C\u6B65\u9AA4\u4E0D\u4F7F\u7528\u5B50\u4EE3\u7406\u3002";
582
+ return `# \u5DE5\u4F5C\u6D41: ${def.description}
583
+
584
+ ## 1. \u89D2\u8272\u5B9A\u4E49
585
+
586
+ \u672C\u6B65\u9AA4\u8D1F\u8D23\u6267\u884C\u6807\u51C6\u7684 specwf \u5DE5\u4F5C\u6D41\u64CD\u4F5C\u3002
587
+ - **\u4EA7\u51FA**\uFF1A\u6309\u7167 specwf \u6807\u51C6\u6D41\u7A0B\u6267\u884C
588
+
589
+ ## 2. \u524D\u7F6E\u6761\u4EF6
590
+
591
+ - state.md \u72B6\u6001\u6B63\u786E
592
+ - \u524D\u7F6E\u6B65\u9AA4\u5DF2\u5168\u90E8\u5B8C\u6210
593
+
594
+ ## 3. \u6267\u884C\u6B65\u9AA4
595
+
596
+ \`\`\`bash
597
+ # \u83B7\u53D6\u4E0A\u4E0B\u6587
598
+ specwf context ${def.step} $@
599
+
600
+ # \u6267\u884C\u6B65\u9AA4\u547D\u4EE4
601
+ specwf ${def.step}
602
+ \`\`\`
603
+
604
+ ## 4. \u5B50\u4EE3\u7406\u4F7F\u7528
605
+
606
+ ${agentsSection}
607
+
608
+ ## 5. \u4EA7\u7269\u7BA1\u7406
609
+
610
+ \`\`\`bash
611
+ specwf state
612
+ specwf config
613
+ \`\`\`
614
+
615
+ ## 6. \u9A8C\u8BC1
616
+
617
+ \`\`\`bash
618
+ specwf state
619
+ \`\`\`
620
+
621
+ ## 7. \u4E0B\u4E00\u6B65
622
+
623
+ \`\`\`bash
624
+ specwf continue
625
+ \`\`\`
626
+ `;
627
+ }
628
+ function generateAllCommands(config) {
629
+ return STEP_DEFS.map((def) => ({
630
+ path: `.omp/commands/specwf-${def.step}.md`,
631
+ content: generateSlashCommand(def, config)
632
+ }));
633
+ }
634
+
635
+ // src/generators/omp-agents.ts
636
+ import { readFileSync as readFileSync7 } from "fs";
637
+ import { join as join7, dirname as dirname2 } from "path";
638
+ import { fileURLToPath as fileURLToPath2 } from "url";
639
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
640
+ var TEMPLATES_DIR2 = join7(__dirname2, "templates", "agents");
641
+ function loadTemplate2(role) {
642
+ return readFileSync7(join7(TEMPLATES_DIR2, `${role}.md`), "utf-8");
643
+ }
644
+ function renderTemplate2(template, vars) {
645
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
646
+ }
647
+ var AGENT_DEFS = [
648
+ // ====================================================================
649
+ // specwf-researcher
650
+ // ====================================================================
651
+ {
652
+ role: "researcher",
653
+ description: "\u6280\u672F\u8C03\u7814\uFF1A\u4EA7\u51FA STACK/ARCH/PITFALLS/RESEARCH \u6587\u6863",
654
+ tools: ["read", "grep", "glob", "lsp", "web_search", "write", "bash"],
655
+ spawns: "*"
656
+ },
657
+ // ====================================================================
658
+ // specwf-planner
659
+ // ====================================================================
660
+ {
661
+ role: "planner",
662
+ description: "Change \u8BBE\u8BA1\uFF1A\u4EA7\u51FA proposal/design/tasks/delta-specs",
663
+ tools: ["read", "grep", "glob", "lsp", "write", "bash"],
664
+ spawns: "*"
665
+ },
666
+ // ====================================================================
667
+ // specwf-executor
668
+ // ====================================================================
669
+ {
670
+ role: "executor",
671
+ description: "\u4EE3\u7801\u5B9E\u73B0\uFF1ATDD RED\u2192GREEN\u2192REFACTOR",
672
+ tools: ["read", "edit", "write", "bash", "grep", "glob", "lsp", "ast_grep", "ast_edit"],
673
+ spawns: "*"
674
+ },
675
+ // ====================================================================
676
+ // specwf-reviewer
677
+ // ====================================================================
678
+ {
679
+ role: "reviewer",
680
+ description: "\u4E09\u91CD\u5BA1\u67E5\uFF1A\u89C4\u683C\u5BA1\u67E5 + \u8D28\u91CF\u5BA1\u67E5 + \u76EE\u6807\u5BA1\u67E5",
681
+ tools: ["read", "grep", "glob", "lsp", "ast_grep", "bash"],
682
+ spawns: "*"
683
+ },
684
+ // ====================================================================
685
+ // specwf-verifier
686
+ // ====================================================================
687
+ {
688
+ role: "verifier",
689
+ description: "\u6D4B\u8BD5\u9A8C\u8BC1\uFF1A\u8BCA\u65AD + \u8DEF\u7531\u56DE\u73AF",
690
+ tools: ["read", "bash", "grep", "glob", "lsp", "edit", "write"],
691
+ spawns: "*"
692
+ },
693
+ // ====================================================================
694
+ // specwf-archiver
695
+ // ====================================================================
696
+ {
697
+ role: "archiver",
698
+ description: "\u5F52\u6863\uFF1Adelta-spec \u5408\u5E76 + \u4EE3\u7801\u8BA4\u77E5\u56DE\u704C",
699
+ tools: ["read", "grep", "glob", "write", "bash", "edit"],
700
+ spawns: "*"
701
+ }
702
+ ];
703
+ function resolveAgentModel(role, config) {
704
+ const models = resolveModels(config);
705
+ const key = role;
706
+ return models[key] ?? "default";
707
+ }
708
+ function resolveThinkingLevel(role) {
709
+ const levelMap = {
710
+ researcher: "high",
711
+ planner: "high",
712
+ executor: "medium",
713
+ reviewer: "high",
714
+ verifier: "medium",
715
+ archiver: "medium"
716
+ };
717
+ return levelMap[role] ?? "medium";
718
+ }
719
+ function generateAgent(def, model) {
720
+ const thinkingLevel = resolveThinkingLevel(def.role);
721
+ const body = renderTemplate2(loadTemplate2(def.role), {
722
+ role: def.role,
723
+ description: def.description,
724
+ tools: def.tools.map((t) => ` - ${t}`).join("\n"),
725
+ model,
726
+ spawns: def.spawns
727
+ });
728
+ return `---
729
+ name: specwf-${def.role}
730
+ description: ${def.description}
731
+ tools:
732
+ ${def.tools.map((t) => ` - ${t}`).join("\n")}
733
+ model: ${model}
734
+ thinkingLevel: ${thinkingLevel}
735
+ spawns: "${def.spawns}"
736
+ blocking: false
737
+ autoloadSkills: false
738
+ readSummarize: true
739
+ ---
740
+
741
+ ${body}
742
+ `;
743
+ }
744
+ function generateAllAgents(config) {
745
+ return AGENT_DEFS.map((def) => ({
746
+ path: `.omp/agents/specwf-${def.role}.md`,
747
+ content: generateAgent(def, resolveAgentModel(def.role, config))
748
+ }));
749
+ }
750
+
751
+ // src/generators/skills.ts
752
+ import { readFileSync as readFileSync8 } from "fs";
753
+ import { join as join8, dirname as dirname3 } from "path";
754
+ import { fileURLToPath as fileURLToPath3 } from "url";
755
+ var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
756
+ var TEMPLATES_DIR3 = join8(__dirname3, "templates", "skills");
757
+ function loadTemplate3(step) {
758
+ return readFileSync8(join8(TEMPLATES_DIR3, `${step}.md`), "utf-8");
759
+ }
760
+ function renderTemplate3(template, vars) {
761
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
762
+ }
763
+ function skillName(step) {
764
+ return `specwf-${step}`;
765
+ }
766
+ function skillDescription(step) {
767
+ const map = {
768
+ init: "\u521D\u59CB\u5316 specwf \u9879\u76EE\u7ED3\u6784\uFF0C\u751F\u6210\u5E73\u53F0\u6587\u4EF6",
769
+ grill: "\u9700\u6C42\u63A2\u8BA8 \u2014 \u65E0\u9650\u5236\u7EC6\u8282\u63D0\u95EE\u76F4\u5230\u8FBE\u6210\u5171\u8BC6",
770
+ research: "\u9879\u76EE\u6280\u672F\u8C03\u7814 \u2014 \u5E76\u884C\u591A\u65B9\u5411\u8C03\u7814",
771
+ roadmap: "\u8DEF\u7EBF\u56FE \u2014 \u62C6\u5206 Milestone \xD7 Phase",
772
+ milestone: "\u91CC\u7A0B\u7891\u7BA1\u7406 \u2014 \u5207\u6362/\u521B\u5EFA Milestone\uFF0C\u8BBE\u7F6E\u5F53\u524D\u9636\u6BB5",
773
+ discuss: "Phase \u8BA8\u8BBA \u2014 \u6355\u83B7\u5B9E\u73B0\u51B3\u7B56\uFF0C\u5F62\u6210 context.md",
774
+ "research-phase": "Phase \u8C03\u7814 \u2014 \u5B9E\u73B0\u8DEF\u5F84\u7814\u7A76",
775
+ split: "Change \u62C6\u5206 \u2014 \u4F9D\u8D56\u56FE + N \u4E2A Change",
776
+ adhoc: "\u521B\u5EFA\u4E34\u65F6 Change \u2014 \u4E0E\u9636\u6BB5\u65E0\u5173\u7684\u72EC\u7ACB\u53D8\u66F4",
777
+ plan: "Change \u8BBE\u8BA1 \u2014 \u6280\u672F\u65B9\u6848 + \u4EFB\u52A1\u62C6\u5206 + delta-specs",
778
+ apply: "\u4EE3\u7801\u5B9E\u73B0 \u2014 TDD RED\u2192GREEN\u2192REFACTOR",
779
+ review: "\u4E09\u91CD\u5BA1\u67E5 \u2014 \u89C4\u683C\u5BA1\u67E5/\u8D28\u91CF\u5BA1\u67E5/\u76EE\u6807\u5BA1\u67E5\u5E76\u884C",
780
+ verify: "\u6D4B\u8BD5\u9A8C\u8BC1 \u2014 \u8BCA\u65AD\u6839\u56E0 + \u8DEF\u7531\u56DE\u73AF",
781
+ archive: "\u5F52\u6863 \u2014 delta-spec \u5408\u5E76 + \u4EE3\u7801\u8BA4\u77E5\u56DE\u704C",
782
+ ship: "\u4EA4\u4ED8 \u2014 PR + STATE \u66F4\u65B0 / release tag",
783
+ continue: "\u81EA\u52A8\u63A8\u8FDB \u2014 \u8BFB STATE \u786E\u5B9A\u4E0B\u4E00\u6B65\u5E76\u89E6\u53D1\u5BF9\u5E94\u547D\u4EE4"
784
+ };
785
+ return map[step] ?? "";
786
+ }
787
+ var STEPS = ["init", "grill", "research", "roadmap", "milestone", "discuss", "research-phase", "split", "adhoc", "plan", "apply", "review", "verify", "archive", "ship", "continue"];
788
+ var SKILL_DEFS = STEPS.map((step) => ({
789
+ step,
790
+ name: skillName(step),
791
+ description: skillDescription(step)
792
+ }));
793
+ function generateSkill(def) {
794
+ const body = renderTemplate3(loadTemplate3(def.step), { step: def.step, description: def.description });
795
+ return `---
796
+ name: ${def.name}
797
+ description: ${def.description}
798
+ hide: false
799
+ ---
800
+
801
+ ${body}
802
+ `;
803
+ }
804
+ function generateAllSkills(config) {
805
+ return SKILL_DEFS.map((def) => ({
806
+ path: `.omp/skills/specwf-${def.step}/SKILL.md`,
807
+ content: generateSkill(def)
808
+ }));
809
+ }
810
+
811
+ // src/generators/index.ts
812
+ function generateAll(config) {
813
+ return [
814
+ ...generateAllCommands(config),
815
+ ...generateAllAgents(config),
816
+ ...generateAllSkills(config)
817
+ ];
818
+ }
819
+
820
+ // src/commands/_utils.ts
821
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
822
+ function writeGeneratedFiles(files) {
823
+ for (const file of files) {
824
+ const dir = file.path.split("/").slice(0, -1).join("/");
825
+ if (dir) mkdirSync3(dir, { recursive: true });
826
+ writeFileSync5(file.path, file.content, "utf-8");
827
+ console.log(` \u2713 ${file.path}`);
828
+ }
829
+ }
830
+
831
+ // src/commands/specwf-update.ts
832
+ function register2(program2) {
833
+ program2.command("update").description("\u66F4\u65B0\u5E73\u53F0\u6587\u4EF6\uFF08commands + agents\uFF09").option("--dir <path>", "specwf \u76EE\u5F55", "specwf").action(updateHandler);
834
+ }
835
+ function updateHandler(options) {
836
+ const specwfDir = join9(process.cwd(), options.dir);
837
+ const config = loadConfig(specwfDir);
838
+ const files = generateAll(config);
839
+ console.log("\u6B63\u5728\u66F4\u65B0\u5E73\u53F0\u6587\u4EF6...");
840
+ writeGeneratedFiles(files);
841
+ console.log(`\u2713 \u66F4\u65B0\u5B8C\u6210 (${files.length} \u4E2A\u6587\u4EF6)`);
842
+ }
843
+
844
+ // src/commands/specwf-config.ts
845
+ import { join as join10 } from "path";
846
+ function register3(program2) {
847
+ const cmd = program2.command("config").description("\u67E5\u770B/\u4FEE\u6539\u914D\u7F6E\u9879\u76EE");
848
+ cmd.command("list").description("\u67E5\u770B\u5F53\u524D\u914D\u7F6E").action(configList);
849
+ cmd.command("set <key> <value>").description("\u4FEE\u6539\u914D\u7F6E\u9879").action(configSet);
850
+ cmd.action(configList);
851
+ }
852
+ function configList(options, cmd) {
853
+ if (cmd?.parent?.args?.length > 1) return;
854
+ const specwfDir = findSpecwfDir();
855
+ const config = loadConfig(specwfDir);
856
+ console.log(JSON.stringify(config, null, 2));
857
+ }
858
+ function configSet(key, value) {
859
+ const specwfDir = findSpecwfDir();
860
+ updateConfig(specwfDir, (config) => {
861
+ const parts = key.split(".");
862
+ let target = config;
863
+ for (let i = 0; i < parts.length - 1; i++) {
864
+ if (!target[parts[i]]) target[parts[i]] = {};
865
+ target = target[parts[i]];
866
+ }
867
+ const lastKey = parts[parts.length - 1];
868
+ const typedValue = parseTypedValue(value);
869
+ target[lastKey] = typedValue;
870
+ });
871
+ console.log(`\u2713 ${key} = ${value}`);
872
+ }
873
+ function parseTypedValue(value) {
874
+ if (value === "true") return true;
875
+ if (value === "false") return false;
876
+ if (/^\d+$/.test(value)) return parseInt(value, 10);
877
+ if (value === "null") return null;
878
+ return value;
879
+ }
880
+ function findSpecwfDir() {
881
+ return join10(process.cwd(), "specwf");
882
+ }
883
+
884
+ // src/commands/specwf-state.ts
885
+ import { join as join11 } from "path";
886
+ function register4(program2) {
887
+ const cmd = program2.command("state").description("\u67E5\u770B/\u4FEE\u6539\u5F53\u524D\u72B6\u6001");
888
+ cmd.command("show").description("\u67E5\u770B\u5F53\u524D\u72B6\u6001").action(showState);
889
+ cmd.command("set-milestone <id>").description("\u5207\u6362\u5230\u6307\u5B9A milestone").action(setMilestone);
890
+ cmd.command("set-phase <id>").description("\u5207\u6362\u5230\u6307\u5B9A phase").action(setPhase);
891
+ cmd.command("set-step <step>").description("\u8BBE\u7F6E\u5F53\u524D\u6B65\u9AA4").action(setStep);
892
+ cmd.action(showState);
893
+ }
894
+ function findSpecwfDir2() {
895
+ return join11(process.cwd(), "specwf");
896
+ }
897
+ function showState() {
898
+ const specwfDir = findSpecwfDir2();
899
+ const state = loadState(specwfDir);
900
+ const { project, active_context } = state;
901
+ console.log("\u2500".repeat(50));
902
+ console.log(`\u9879\u76EE: ${project.name}`);
903
+ console.log(`\u72B6\u6001: ${project.status}`);
904
+ console.log(`Milestone: ${project.current_milestone ?? "(\u65E0)"}`);
905
+ console.log(`Phase: ${project.current_phase ?? "(\u65E0)"}`);
906
+ console.log(`\u5F53\u524D\u7C7B\u578B: ${active_context.type}`);
907
+ console.log(`\u5F53\u524D\u6B65\u9AA4: ${active_context.step}`);
908
+ if (active_context.ref) {
909
+ console.log(`\u5F15\u7528: ${active_context.ref}`);
910
+ }
911
+ console.log("\u2500".repeat(50));
912
+ }
913
+ function setMilestone(id) {
914
+ const specwfDir = findSpecwfDir2();
915
+ updateState(specwfDir, (state) => {
916
+ state.project.current_milestone = id;
917
+ state.project.current_phase = null;
918
+ state.active_context.type = "phase";
919
+ state.active_context.ref = `milestones/${id}`;
920
+ state.active_context.step = "discuss";
921
+ state.project.status = "phase-discuss";
922
+ });
923
+ console.log(`\u2713 \u5207\u6362\u5230 milestone: ${id}\uFF08\u72B6\u6001: phase-discuss\uFF09`);
924
+ console.log("\u2192 \u4E0B\u4E00\u6B65: /specwf:discuss");
925
+ }
926
+ function setPhase(id) {
927
+ const specwfDir = findSpecwfDir2();
928
+ updateState(specwfDir, (state) => {
929
+ state.project.current_phase = id;
930
+ state.active_context.type = "phase";
931
+ state.active_context.ref = `milestones/${state.project.current_milestone ?? "?"}/phases/${id}`;
932
+ state.active_context.step = "discuss";
933
+ state.project.status = "phase-discuss";
934
+ });
935
+ console.log(`\u2713 \u5207\u6362\u5230 phase: ${id}\uFF08\u72B6\u6001: phase-discuss\uFF09`);
936
+ console.log("\u2192 \u4E0B\u4E00\u6B65: /specwf:discuss");
937
+ }
938
+ function setStep(step) {
939
+ const specwfDir = findSpecwfDir2();
940
+ updateState(specwfDir, (state) => {
941
+ state.active_context.step = step;
942
+ });
943
+ console.log(`\u2713 \u5F53\u524D\u6B65\u9AA4: ${step}`);
944
+ }
945
+
946
+ // src/commands/specwf-context.ts
947
+ import { join as join13 } from "path";
948
+
949
+ // src/core/spec-injector.ts
950
+ import { join as join12 } from "path";
951
+ import { readdirSync as readdirSync3, existsSync as existsSync6, statSync as statSync2 } from "fs";
952
+ var PROJECT_STEPS = ["init", "grill", "research", "roadmap"];
953
+ var PHASE_STEPS = ["discuss", "research-phase", "split"];
954
+ var CHANGE_STEPS = ["plan", "apply", "review", "verify", "archive"];
955
+ function isProjectStep(step) {
956
+ return PROJECT_STEPS.includes(step);
957
+ }
958
+ function isPhaseStep(step) {
959
+ return PHASE_STEPS.includes(step);
960
+ }
961
+ function isChangeStep(step) {
962
+ return CHANGE_STEPS.includes(step);
963
+ }
964
+ function generateContext(specwfDir, step) {
965
+ const state = loadState(specwfDir);
966
+ const ctx = state.active_context;
967
+ const result = {
968
+ step,
969
+ scope: { type: ctx.type, ref: ctx.ref },
970
+ specs: [],
971
+ conventions: [],
972
+ changeArtifacts: [],
973
+ requirements: []
974
+ };
975
+ result.conventions = getAllConventions(specwfDir);
976
+ if (existsSync6(join12(specwfDir, "requirements.md"))) {
977
+ result.requirements.push({ path: "requirements.md", description: "\u9700\u6C42\u89C4\u683C" });
978
+ }
979
+ if (isProjectStep(step)) {
980
+ result.specs = getAllSpecs(specwfDir);
981
+ } else if (isPhaseStep(step)) {
982
+ result.specs = getRelatedSpecs(specwfDir, state);
983
+ } else if (isChangeStep(step)) {
984
+ result.specs = getRelatedSpecs(specwfDir, state);
985
+ result.changeArtifacts = getChangeArtifacts(specwfDir, state);
986
+ }
987
+ return result;
988
+ }
989
+ function getAllSpecs(specwfDir) {
990
+ const specsDir = join12(specwfDir, "specs");
991
+ return listSpecFiles(specsDir, "specs");
992
+ }
993
+ function getRelatedSpecs(specwfDir, state) {
994
+ const allSpecs = getAllSpecs(specwfDir);
995
+ if (allSpecs.length === 0) return [];
996
+ const ref = state.active_context.ref ?? "";
997
+ const changeName = ref.split("/").pop() ?? "";
998
+ const related = allSpecs.filter((spec) => {
999
+ const domain = spec.path.split("/")[1] ?? "";
1000
+ return changeName.toLowerCase().includes(domain.toLowerCase());
1001
+ });
1002
+ return related.length > 0 ? related : allSpecs;
1003
+ }
1004
+ function getAllConventions(specwfDir) {
1005
+ const convDir = join12(specwfDir, "conventions");
1006
+ if (!existsSync6(convDir)) return [];
1007
+ return readdirSync3(convDir).filter((f) => f.endsWith(".md")).map((f) => ({ path: `conventions/${f}`, description: "\u9879\u76EE\u7EA6\u5B9A" }));
1008
+ }
1009
+ function getChangeArtifacts(specwfDir, state) {
1010
+ const ref = state.active_context.ref;
1011
+ if (!ref) return [];
1012
+ const changeDir = join12(specwfDir, ref);
1013
+ if (!existsSync6(changeDir)) return [];
1014
+ const artifacts = [];
1015
+ for (const file of ["proposal.md", "design.md", "tasks.md", ".specwf.yaml"]) {
1016
+ const fullPath = join12(changeDir, file);
1017
+ if (existsSync6(fullPath)) {
1018
+ artifacts.push({ path: `${ref}/${file}`, description: "change \u4EA7\u7269" });
1019
+ }
1020
+ }
1021
+ const specsDir = join12(changeDir, "specs");
1022
+ if (existsSync6(specsDir)) {
1023
+ const deltaSpecs = listSpecFiles(specsDir, `${ref}/specs`);
1024
+ artifacts.push(...deltaSpecs);
1025
+ }
1026
+ return artifacts;
1027
+ }
1028
+ function listSpecFiles(dir, prefix) {
1029
+ if (!existsSync6(dir)) return [];
1030
+ const results = [];
1031
+ for (const entry of readdirSync3(dir)) {
1032
+ const fullPath = join12(dir, entry);
1033
+ const stat = statSync2(fullPath);
1034
+ if (stat.isDirectory()) {
1035
+ results.push(...listSpecFiles(fullPath, `${prefix}/${entry}`));
1036
+ } else if (entry.endsWith(".md")) {
1037
+ results.push({ path: `${prefix}/${entry}`, description: "\u884C\u4E3A\u5951\u7EA6" });
1038
+ }
1039
+ }
1040
+ return results;
1041
+ }
1042
+ function formatContextTerminal(result) {
1043
+ const lines = [
1044
+ `=== specwf context for step: ${result.step} ===`,
1045
+ `Scope: ${result.scope.type}${result.scope.ref ? ` (${result.scope.ref})` : ""}`,
1046
+ "\u2500".repeat(60)
1047
+ ];
1048
+ if (result.specs.length > 0) {
1049
+ lines.push("Related specs:");
1050
+ for (const spec of result.specs) {
1051
+ lines.push(` ${spec.path.padEnd(40)} # ${spec.description ?? ""}`);
1052
+ }
1053
+ lines.push("");
1054
+ }
1055
+ if (result.conventions.length > 0) {
1056
+ lines.push("Related conventions:");
1057
+ for (const conv of result.conventions) {
1058
+ lines.push(` ${conv.path.padEnd(40)} # ${conv.description ?? ""}`);
1059
+ }
1060
+ lines.push("");
1061
+ }
1062
+ if (result.changeArtifacts.length > 0) {
1063
+ lines.push("Current change artifacts:");
1064
+ for (const art of result.changeArtifacts) {
1065
+ lines.push(` ${art.path}`);
1066
+ }
1067
+ lines.push("");
1068
+ }
1069
+ if (result.requirements.length > 0) {
1070
+ lines.push("Requirements:");
1071
+ for (const req of result.requirements) {
1072
+ lines.push(` ${req.path}`);
1073
+ }
1074
+ lines.push("");
1075
+ }
1076
+ lines.push("\u2500".repeat(60));
1077
+ lines.push("Usage: use `read <path>` to load each file.");
1078
+ lines.push("Selectors: `read <path>:50-100` for ranges.");
1079
+ return lines.join("\n");
1080
+ }
1081
+
1082
+ // src/commands/specwf-context.ts
1083
+ function register5(program2) {
1084
+ program2.command("context <step>").description("\u8F93\u51FA\u5F53\u524D\u6B65\u9AA4\u4E0A\u4E0B\u6587\u6587\u4EF6\u6E05\u5355").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA").action(contextHandler);
1085
+ }
1086
+ function contextHandler(step, options) {
1087
+ const specwfDir = join13(process.cwd(), "specwf");
1088
+ const result = generateContext(specwfDir, step);
1089
+ if (options.json) {
1090
+ console.log(JSON.stringify(result, null, 2));
1091
+ } else {
1092
+ console.log(formatContextTerminal(result));
1093
+ }
1094
+ }
1095
+
1096
+ // src/commands/specwf-continue.ts
1097
+ import { join as join14 } from "path";
1098
+
1099
+ // src/types/state.ts
1100
+ var STATE_TRANSITIONS = [
1101
+ // 项目层路径
1102
+ { from: "initialized", command: "grill", to: "requirements-defined", slashCommand: "/specwf:grill" },
1103
+ { from: "requirements-defined", command: "research", to: "researching", slashCommand: "/specwf:research", subagent: true },
1104
+ { from: "researching", command: "research-done", to: "researched", slashCommand: "" },
1105
+ { from: "researched", command: "roadmap", to: "roadmap-defined", slashCommand: "/specwf:roadmap" },
1106
+ { from: "roadmap-defined", command: "discuss", to: "phase-discuss", slashCommand: "/specwf:discuss" },
1107
+ // Phase 路径
1108
+ { from: "phase-discuss", command: "research-phase", to: "phase-research", slashCommand: "/specwf:research-phase", subagent: true },
1109
+ { from: "phase-research", command: "split", to: "phase-split", slashCommand: "/specwf:split" },
1110
+ { from: "phase-split", command: "plan", to: "change-planning", slashCommand: "/specwf:plan", subagent: true },
1111
+ { from: "change-planning", command: "apply", to: "change-applying", slashCommand: "/specwf:apply", subagent: true },
1112
+ { from: "change-applying", command: "review", to: "change-reviewing", slashCommand: "/specwf:review", subagent: true },
1113
+ { from: "change-reviewing", command: "verify", to: "change-verifying", slashCommand: "/specwf:verify", subagent: true },
1114
+ { from: "change-verifying", command: "archive", to: "change-archiving", slashCommand: "/specwf:archive", subagent: true },
1115
+ { from: "change-archiving", command: "archive-done", to: "change-archived", slashCommand: "" },
1116
+ // 回环
1117
+ { from: "change-verifying", command: "replan", to: "change-planning", slashCommand: "/specwf:plan", subagent: true },
1118
+ { from: "change-verifying", command: "reapply", to: "change-applying", slashCommand: "/specwf:apply", subagent: true },
1119
+ { from: "change-reviewing", command: "fix", to: "change-applying", slashCommand: "/specwf:apply", subagent: true },
1120
+ // Ship
1121
+ { from: "change-archived", command: "ship-phase", to: "phase-shipped", slashCommand: "/specwf:ship" },
1122
+ { from: "phase-shipped", command: "next-phase", to: "phase-discuss", slashCommand: "/specwf:discuss" },
1123
+ { from: "phase-shipped", command: "ship-milestone", to: "milestone-shipped", slashCommand: "/specwf:ship" },
1124
+ // 临时 change
1125
+ { from: "adhoc-proposal", command: "plan", to: "change-planning", slashCommand: "/specwf:plan", subagent: true },
1126
+ { from: "change-archived", command: "adhoc-done", to: "adhoc-archived", slashCommand: "" },
1127
+ { from: "adhoc-archived", command: "new-change", to: "adhoc-proposal", slashCommand: "" }
1128
+ ];
1129
+
1130
+ // src/core/state-machine.ts
1131
+ function getNextSteps(from) {
1132
+ return STATE_TRANSITIONS.filter((t) => t.from === from);
1133
+ }
1134
+
1135
+ // src/core/continue.ts
1136
+ function determineNextStep(specwfDir) {
1137
+ return determineFromState(loadState(specwfDir));
1138
+ }
1139
+ function determineChangeNextStep(specwfDir, changeName) {
1140
+ const state = loadState(specwfDir);
1141
+ const change = state.changes.find((c) => c.name === changeName);
1142
+ if (change) {
1143
+ return determineFromChangeStatus(changeName, `change-${change.status}`, "change");
1144
+ }
1145
+ const adhoc = state.adhoc.find((c) => c.name === changeName);
1146
+ if (adhoc) {
1147
+ return determineFromChangeStatus(
1148
+ changeName,
1149
+ `adhoc-${adhoc.status}`,
1150
+ "adhoc"
1151
+ );
1152
+ }
1153
+ return {
1154
+ error: `change \u4E0D\u5B58\u5728: ${changeName}\u3002\u53EF\u7528: ${listAvailableChanges(state)}`
1155
+ };
1156
+ }
1157
+ function determineFromChangeStatus(name, statusKey, type) {
1158
+ const available = getNextSteps(statusKey);
1159
+ const availableSteps = available.map((t) => ({
1160
+ command: t.command,
1161
+ slashCommand: t.slashCommand,
1162
+ subagent: t.subagent ?? false
1163
+ }));
1164
+ const first = available[0];
1165
+ return {
1166
+ currentStep: statusKey,
1167
+ context: `${type === "adhoc" ? "\u4E34\u65F6 Change" : "Change"} (${name})`,
1168
+ nextCommand: first?.command ?? null,
1169
+ slashCommand: first?.slashCommand || null,
1170
+ needsSubagent: first?.subagent ?? false,
1171
+ availableSteps,
1172
+ hint: available.length === 0 ? "\u8BE5 change \u5DF2\u6CA1\u6709\u53EF\u7528\u4E0B\u4E00\u6B65\u3002\u521B\u5EFA\u65B0 change \u7EE7\u7EED\u3002" : null
1173
+ };
1174
+ }
1175
+ function listAvailableChanges(state) {
1176
+ const names = [
1177
+ ...state.changes.map((c) => c.name),
1178
+ ...state.adhoc.map((c) => c.name)
1179
+ ];
1180
+ return names.join(", ") || "(\u65E0)";
1181
+ }
1182
+ function determineFromState(state) {
1183
+ const ctx = state.active_context;
1184
+ const currentStatus = resolveStatus(state);
1185
+ const available = getNextSteps(currentStatus);
1186
+ const availableSteps = available.map((t) => ({
1187
+ command: t.command,
1188
+ slashCommand: t.slashCommand,
1189
+ subagent: t.subagent ?? false
1190
+ }));
1191
+ const first = available[0];
1192
+ const hint = available.length === 0 ? generateHint(state) : null;
1193
+ return {
1194
+ currentStep: ctx.step,
1195
+ context: formatContext2(state),
1196
+ nextCommand: first?.command ?? null,
1197
+ slashCommand: first?.slashCommand || null,
1198
+ needsSubagent: first?.subagent ?? false,
1199
+ availableSteps,
1200
+ hint
1201
+ };
1202
+ }
1203
+ function resolveStatus(state) {
1204
+ const ctx = state.active_context;
1205
+ switch (ctx.type) {
1206
+ case "project":
1207
+ return state.project.status;
1208
+ case "milestone":
1209
+ return state.project.status === "milestone-shipped" ? "milestone-shipped" : "milestone-active";
1210
+ case "phase":
1211
+ return `phase-${ctx.step}`;
1212
+ case "change":
1213
+ return `change-${ctx.step}`;
1214
+ case "adhoc":
1215
+ return `adhoc-${ctx.step}`;
1216
+ default:
1217
+ return state.project.status;
1218
+ }
1219
+ }
1220
+ function formatContext2(state) {
1221
+ const { type, ref, step } = state.active_context;
1222
+ switch (type) {
1223
+ case "project":
1224
+ return `\u9879\u76EE\u5C42 \u2014 ${step}`;
1225
+ case "milestone":
1226
+ return `Milestone ${state.project.current_milestone ?? "?"} \u2014 ${step}`;
1227
+ case "phase":
1228
+ return `Phase ${state.project.current_phase ?? "?"} \u2014 ${step}`;
1229
+ case "change":
1230
+ return `Change (${ref ?? "?"}) \u2014 ${step}`;
1231
+ case "adhoc":
1232
+ return `\u4E34\u65F6 Change (${ref ?? "?"}) \u2014 ${step}`;
1233
+ default:
1234
+ return step;
1235
+ }
1236
+ }
1237
+ function generateHint(state) {
1238
+ const status = state.project.status;
1239
+ if (status === "milestone-shipped") {
1240
+ const pendingAdhoc = state.adhoc.filter((c) => c.status !== "archived");
1241
+ const hintParts = ["\u5F53\u524D milestone \u5DF2\u5B8C\u6210\u3002\u521B\u5EFA\u65B0 milestone: specwf state set-milestone <id>"];
1242
+ if (pendingAdhoc.length > 0) {
1243
+ hintParts.push(
1244
+ `\u5F85\u5904\u7406\u7684\u4E34\u65F6 change: ${pendingAdhoc.map((c) => c.name).join(", ")}\u3002\u4F7F\u7528: specwf continue change <name>`
1245
+ );
1246
+ }
1247
+ return hintParts.join("\n ");
1248
+ }
1249
+ if (status === "phase-shipped") {
1250
+ return "\u5F53\u524D phase \u5DF2\u5B8C\u6210\u3002\u521B\u5EFA\u65B0 phase \u6216\u5207\u6362: specwf state set-milestone <id>";
1251
+ }
1252
+ return null;
1253
+ }
1254
+
1255
+ // src/commands/specwf-continue.ts
1256
+ function register6(program2) {
1257
+ const cmd = program2.command("continue").description("\u81EA\u52A8\u63A8\u8FDB\u5230\u4E0B\u4E00\u6B65\uFF08\u8BFB state.md \u2192 \u786E\u5B9A\u4E0B\u4E00\u6B65 \u2192 \u8F93\u51FA\uFF09");
1258
+ cmd.command("change <name>").description("\u67E5\u8BE2\u6307\u5B9A change \u7684\u4E0B\u4E00\u6B65").action(continueChangeHandler);
1259
+ cmd.action(continueHandler);
1260
+ }
1261
+ function formatContinueResult(result) {
1262
+ console.log("\u2500".repeat(50));
1263
+ console.log(`\u5F53\u524D\u4F4D\u7F6E: ${result.context}`);
1264
+ console.log(`\u5F53\u524D\u6B65\u9AA4: ${result.currentStep}`);
1265
+ if (result.nextCommand) {
1266
+ console.log("");
1267
+ console.log(`\u2192 \u63A8\u8350\u4E0B\u4E00\u6B65: ${result.nextCommand}`);
1268
+ if (result.slashCommand) {
1269
+ console.log(` Slash \u547D\u4EE4: ${result.slashCommand}`);
1270
+ }
1271
+ if (result.needsSubagent) {
1272
+ console.log(` \u9700\u8981\u5B50\u4EE3\u7406: \u662F`);
1273
+ }
1274
+ } else {
1275
+ console.log("");
1276
+ console.log("\u2192 \u5F53\u524D\u65E0\u53EF\u7528\u4E0B\u4E00\u6B65");
1277
+ if (result.hint) {
1278
+ console.log(` \u{1F4A1} ${result.hint}`);
1279
+ }
1280
+ }
1281
+ console.log("\u2500".repeat(50));
1282
+ }
1283
+ function continueHandler() {
1284
+ const specwfDir = join14(process.cwd(), "specwf");
1285
+ const result = determineNextStep(specwfDir);
1286
+ formatContinueResult(result);
1287
+ }
1288
+ function continueChangeHandler(name) {
1289
+ const specwfDir = join14(process.cwd(), "specwf");
1290
+ const result = determineChangeNextStep(specwfDir, name);
1291
+ if ("error" in result) {
1292
+ console.log("\u2500".repeat(50));
1293
+ console.log(result.error);
1294
+ console.log("\u2500".repeat(50));
1295
+ return;
1296
+ }
1297
+ formatContinueResult(result);
1298
+ }
1299
+
1300
+ // src/commands/specwf-archive.ts
1301
+ import { join as join16 } from "path";
1302
+ import { existsSync as existsSync9, readdirSync as readdirSync5, mkdirSync as mkdirSync5, copyFileSync } from "fs";
1303
+
1304
+ // src/core/delta-merge.ts
1305
+ import { createHash } from "crypto";
1306
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
1307
+
1308
+ // src/parser/heading-tree.ts
1309
+ function parseHeadings(markdown) {
1310
+ const lines = markdown.split("\n");
1311
+ const nodes = [];
1312
+ const stack = [];
1313
+ for (let i = 0; i < lines.length; i++) {
1314
+ const line = lines[i];
1315
+ const match = line.match(/^(#{1,6})\s+(.+)$/);
1316
+ if (!match) continue;
1317
+ const level = match[1].length;
1318
+ const text = match[2].trim();
1319
+ const lineNum = i + 1;
1320
+ const contentLines = [];
1321
+ for (let j = i + 1; j < lines.length; j++) {
1322
+ const nextMatch = lines[j].match(/^(#{1,6})\s+(.+)$/);
1323
+ if (nextMatch) break;
1324
+ contentLines.push(lines[j]);
1325
+ }
1326
+ const content = contentLines.join("\n").trim();
1327
+ const node = { level, text, line: lineNum, children: [], content };
1328
+ while (stack.length > 0 && stack[stack.length - 1].level >= level) {
1329
+ stack.pop();
1330
+ }
1331
+ if (stack.length === 0) {
1332
+ nodes.push(node);
1333
+ } else {
1334
+ stack[stack.length - 1].node.children.push(node);
1335
+ }
1336
+ stack.push({ node, level });
1337
+ }
1338
+ return nodes;
1339
+ }
1340
+
1341
+ // src/core/delta-merge.ts
1342
+ function fingerprint(content) {
1343
+ return createHash("sha256").update(content).digest("hex");
1344
+ }
1345
+ function mergeDeltaSpec(baseSpec, deltaSpec, baseFingerprint) {
1346
+ if (baseFingerprint) {
1347
+ const liveFingerprint = fingerprint(baseSpec);
1348
+ if (liveFingerprint === baseFingerprint) {
1349
+ return { type: "ok", merged: deltaSpec };
1350
+ }
1351
+ }
1352
+ const baseTree = parseHeadings(baseSpec);
1353
+ const deltaTree = parseHeadings(deltaSpec);
1354
+ const merged = mergeTrees(baseTree, deltaTree);
1355
+ if (merged.conflicts.length > 0) {
1356
+ return { type: "conflict", conflicts: merged.conflicts };
1357
+ }
1358
+ return { type: "ok", merged: renderTree(merged.nodes) };
1359
+ }
1360
+ function mergeTrees(base, delta) {
1361
+ const conflicts = [];
1362
+ const nodes = [];
1363
+ const baseIndex = indexNodes(base);
1364
+ const deltaIndex = indexNodes(delta);
1365
+ const allKeys = /* @__PURE__ */ new Set([...baseIndex.keys(), ...deltaIndex.keys()]);
1366
+ for (const key of allKeys) {
1367
+ const b = baseIndex.get(key);
1368
+ const d = deltaIndex.get(key);
1369
+ if (b && !d) {
1370
+ nodes.push({ node: b, children: b.children.map((c) => ({ node: c, children: [] })) });
1371
+ } else if (!b && d) {
1372
+ nodes.push({ node: d, children: d.children.map((c) => ({ node: c, children: [] })) });
1373
+ } else if (b && d) {
1374
+ const childMerge = mergeTrees(b.children, d.children);
1375
+ if (b.content === d.content) {
1376
+ nodes.push({ node: b, children: childMerge.nodes });
1377
+ } else {
1378
+ const lineMerge = tryLineMerge(b.content, d.content);
1379
+ if (lineMerge !== null) {
1380
+ nodes.push({ node: { ...b, content: lineMerge }, children: childMerge.nodes });
1381
+ } else {
1382
+ conflicts.push({
1383
+ section: b.text,
1384
+ message: `Content conflict in section: ${b.text}`,
1385
+ baseContent: b.content,
1386
+ deltaContent: d.content
1387
+ });
1388
+ nodes.push({ node: b, children: childMerge.nodes });
1389
+ }
1390
+ }
1391
+ conflicts.push(...childMerge.conflicts);
1392
+ }
1393
+ }
1394
+ return { nodes, conflicts };
1395
+ }
1396
+ function indexNodes(nodes) {
1397
+ const map = /* @__PURE__ */ new Map();
1398
+ for (const node of nodes) {
1399
+ map.set(`${node.level}:${node.text}`, node);
1400
+ }
1401
+ return map;
1402
+ }
1403
+ function tryLineMerge(baseText, deltaText) {
1404
+ const baseLines = baseText.split("\n");
1405
+ const deltaLines = deltaText.split("\n");
1406
+ const baseSet = new Set(baseLines);
1407
+ const removedFromBase = baseLines.filter(
1408
+ (l) => l.trim() && !deltaLines.includes(l)
1409
+ );
1410
+ if (removedFromBase.length === 0) {
1411
+ const result = [...baseLines];
1412
+ for (const line of deltaLines) {
1413
+ if (!baseSet.has(line)) {
1414
+ result.push(line);
1415
+ }
1416
+ }
1417
+ return result.join("\n");
1418
+ }
1419
+ return null;
1420
+ }
1421
+ function renderTree(nodes) {
1422
+ const lines = [];
1423
+ renderNodes(nodes, lines);
1424
+ return lines.join("\n").trim();
1425
+ }
1426
+ function renderNodes(nodes, lines) {
1427
+ for (const { node, children } of nodes) {
1428
+ lines.push(`${"#".repeat(node.level)} ${node.text}`);
1429
+ if (node.content) {
1430
+ lines.push("");
1431
+ lines.push(node.content);
1432
+ }
1433
+ if (children.length > 0) {
1434
+ lines.push("");
1435
+ renderNodes(children, lines);
1436
+ }
1437
+ lines.push("");
1438
+ }
1439
+ }
1440
+ function mergeAndWrite(liveSpecPath, deltaSpecPath, baseFingerprint) {
1441
+ const baseSpec = readFileSync10(liveSpecPath, "utf-8");
1442
+ const deltaSpec = readFileSync10(deltaSpecPath, "utf-8");
1443
+ const result = mergeDeltaSpec(baseSpec, deltaSpec, baseFingerprint);
1444
+ if (result.type === "ok") {
1445
+ writeFileSync6(liveSpecPath, result.merged, "utf-8");
1446
+ }
1447
+ return result;
1448
+ }
1449
+
1450
+ // src/core/code-extract.ts
1451
+ import { execSync } from "child_process";
1452
+ import { existsSync as existsSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync4 } from "fs";
1453
+ import { join as join15 } from "path";
1454
+ function extractFromGitDiff(repoDir, changeDir) {
1455
+ const diff = getGitDiff(repoDir);
1456
+ if (diff === null) {
1457
+ return { extractions: [], available: false };
1458
+ }
1459
+ const domains = changeDir ? detectDomains(changeDir) : ["general"];
1460
+ const extractions = [];
1461
+ for (const domain of domains) {
1462
+ const behaviors = extractBehaviors(diff, domain);
1463
+ const constraints = extractConstraints(diff, domain);
1464
+ if (behaviors.length > 0 || constraints.length > 0) {
1465
+ extractions.push({ domain, behaviors, constraints });
1466
+ }
1467
+ }
1468
+ return { extractions, available: true };
1469
+ }
1470
+ function getGitDiff(repoDir) {
1471
+ try {
1472
+ const diff = execSync("git diff HEAD", {
1473
+ cwd: repoDir,
1474
+ encoding: "utf-8",
1475
+ stdio: ["pipe", "pipe", "pipe"]
1476
+ });
1477
+ if (diff.trim()) return diff;
1478
+ const lastCommit = execSync("git diff HEAD~1 HEAD", {
1479
+ cwd: repoDir,
1480
+ encoding: "utf-8",
1481
+ stdio: ["pipe", "pipe", "pipe"]
1482
+ });
1483
+ return lastCommit.trim() ? lastCommit : null;
1484
+ } catch {
1485
+ return null;
1486
+ }
1487
+ }
1488
+ function detectDomains(changeDir) {
1489
+ const specsDir = join15(changeDir, "specs");
1490
+ if (!existsSync8(specsDir)) return ["general"];
1491
+ try {
1492
+ return readdirSync4(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1493
+ } catch {
1494
+ return ["general"];
1495
+ }
1496
+ }
1497
+ function extractBehaviors(diff, _domain) {
1498
+ const behaviors = [];
1499
+ const lines = diff.split("\n");
1500
+ for (const line of lines) {
1501
+ if (line.startsWith("+") && !line.startsWith("+++")) {
1502
+ const content = line.slice(1).trim();
1503
+ if (/\b(SHALL|MUST|SHOULD|MAY)\b/.test(content)) {
1504
+ behaviors.push(content);
1505
+ }
1506
+ if (/^(export\s+)?(async\s+)?function\s+/.test(content) || /^(export\s+)?class\s+/.test(content)) {
1507
+ behaviors.push(`\u65B0\u589E: ${content}`);
1508
+ }
1509
+ }
1510
+ }
1511
+ return behaviors;
1512
+ }
1513
+ function extractConstraints(diff, _domain) {
1514
+ const constraints = [];
1515
+ const lines = diff.split("\n");
1516
+ for (const line of lines) {
1517
+ if (line.startsWith("+") && !line.startsWith("+++")) {
1518
+ const content = line.slice(1).trim();
1519
+ if (/^(throw|assert|if\s*\()/.test(content) && !content.startsWith("//")) {
1520
+ constraints.push(`\u7EA6\u675F: ${content}`);
1521
+ }
1522
+ if (/^(export\s+)?(interface|type)\s+/.test(content)) {
1523
+ constraints.push(`\u7C7B\u578B\u7EA6\u675F: ${content}`);
1524
+ }
1525
+ }
1526
+ }
1527
+ return constraints;
1528
+ }
1529
+ function writeExtractionToSpec(specsDir, extraction) {
1530
+ const domainDir = join15(specsDir, extraction.domain);
1531
+ const specPath = join15(domainDir, "spec.md");
1532
+ let existing = "";
1533
+ if (existsSync8(specPath)) {
1534
+ existing = readFileSync11(specPath, "utf-8");
1535
+ }
1536
+ const section = generateAutoExtractedSection(extraction);
1537
+ const updated = existing.trim() ? `${existing.trim()}
1538
+
1539
+ ${section}` : section;
1540
+ mkdirSync4(domainDir, { recursive: true });
1541
+ writeFileSync7(specPath, updated, "utf-8");
1542
+ }
1543
+ function generateAutoExtractedSection(extraction) {
1544
+ const lines = [
1545
+ "<!-- AUTO-EXTRACTED: \u4EE5\u4E0B\u5185\u5BB9\u7531 code-extract \u4ECE\u4EE3\u7801 diff \u63D0\u53D6\uFF0C\u8BF7\u4EBA\u5DE5\u5BA1\u6838 -->",
1546
+ "",
1547
+ "## Auto-Extracted Behaviors",
1548
+ ""
1549
+ ];
1550
+ if (extraction.behaviors.length > 0) {
1551
+ lines.push("### Detected Behaviors", "");
1552
+ for (const b of extraction.behaviors) {
1553
+ lines.push(`- ${b}`);
1554
+ }
1555
+ lines.push("");
1556
+ }
1557
+ if (extraction.constraints.length > 0) {
1558
+ lines.push("### Detected Constraints", "");
1559
+ for (const c of extraction.constraints) {
1560
+ lines.push(`- ${c}`);
1561
+ }
1562
+ lines.push("");
1563
+ }
1564
+ lines.push("<!-- END AUTO-EXTRACTED -->");
1565
+ return lines.join("\n");
1566
+ }
1567
+
1568
+ // src/commands/specwf-archive.ts
1569
+ function register7(program2) {
1570
+ program2.command("archive <change>").description("\u5F52\u6863 change\uFF08delta \u5408\u5E76 + \u4EE3\u7801\u56DE\u704C\uFF09").action(archiveHandler);
1571
+ }
1572
+ function archiveHandler(changePath) {
1573
+ const specwfDir = join16(process.cwd(), "specwf");
1574
+ const fullChangePath = join16(process.cwd(), changePath);
1575
+ if (!existsSync9(fullChangePath)) {
1576
+ console.error(`\u9519\u8BEF: change \u76EE\u5F55\u4E0D\u5B58\u5728: ${changePath}`);
1577
+ process.exit(1);
1578
+ }
1579
+ const specsDir = join16(fullChangePath, "specs");
1580
+ if (existsSync9(specsDir)) {
1581
+ mergeDeltaSpecs(specsDir, specwfDir);
1582
+ console.log("\u2713 delta-specs \u5408\u5E76\u5B8C\u6210");
1583
+ }
1584
+ const repoDir = process.cwd();
1585
+ const extractResult = extractFromGitDiff(repoDir, fullChangePath);
1586
+ if (extractResult.available && extractResult.extractions.length > 0) {
1587
+ for (const extraction of extractResult.extractions) {
1588
+ writeExtractionToSpec(join16(specwfDir, "specs"), extraction);
1589
+ }
1590
+ if (extractResult.extractions.length > 0) {
1591
+ console.log(`\u2713 \u4EE3\u7801\u8BA4\u77E5\u63D0\u53D6\u5B8C\u6210 (${extractResult.extractions.length} \u4E2A\u57DF)`);
1592
+ }
1593
+ }
1594
+ const archiveDir = archiveChangeDir(specwfDir, fullChangePath);
1595
+ console.log(`\u2713 \u5F52\u6863\u5230: ${archiveDir}`);
1596
+ const changeName = changePath.split("/").pop() ?? "unknown";
1597
+ try {
1598
+ updateState(specwfDir, (state) => {
1599
+ const change = state.changes.find((c) => c.name === changeName);
1600
+ if (change) {
1601
+ change.status = "archived";
1602
+ return;
1603
+ }
1604
+ const adhoc = state.adhoc.find((c) => c.name === changeName);
1605
+ if (adhoc) {
1606
+ adhoc.status = "archived";
1607
+ }
1608
+ });
1609
+ console.log("\u2713 state.md \u5DF2\u66F4\u65B0");
1610
+ } catch {
1611
+ }
1612
+ console.log("\u5F52\u6863\u5B8C\u6210\u3002");
1613
+ }
1614
+ function mergeDeltaSpecs(deltaDir, specwfDir) {
1615
+ const entries = readdirSync5(deltaDir, { withFileTypes: true });
1616
+ for (const entry of entries) {
1617
+ if (!entry.isDirectory()) continue;
1618
+ const deltaSpecPath = join16(deltaDir, entry.name, "spec.md");
1619
+ const liveSpecPath = join16(specwfDir, "specs", entry.name, "spec.md");
1620
+ if (!existsSync9(deltaSpecPath)) continue;
1621
+ if (!existsSync9(liveSpecPath)) {
1622
+ mkdirSync5(join16(specwfDir, "specs", entry.name), { recursive: true });
1623
+ copyFileSync(deltaSpecPath, liveSpecPath);
1624
+ continue;
1625
+ }
1626
+ const result = mergeAndWrite(liveSpecPath, deltaSpecPath);
1627
+ if (result.type === "conflict") {
1628
+ console.warn(`\u26A0 \u5408\u5E76\u51B2\u7A81: ${entry.name}/spec.md`);
1629
+ for (const c of result.conflicts) {
1630
+ console.warn(` \u8282: ${c.section}`);
1631
+ }
1632
+ }
1633
+ }
1634
+ }
1635
+
1636
+ // src/commands/specwf-list.ts
1637
+ import { join as join17 } from "path";
1638
+ function register8(program2) {
1639
+ program2.command("list").description("\u5217\u51FA milestones/phases/changes").option("--all", "\u5305\u542B\u5F52\u6863").action(listHandler);
1640
+ }
1641
+ function listHandler(options) {
1642
+ const specwfDir = join17(process.cwd(), "specwf");
1643
+ let hasItems = false;
1644
+ const milestones = listMilestones(specwfDir);
1645
+ if (milestones.length > 0) {
1646
+ console.log("Milestones:");
1647
+ for (const ms of milestones) {
1648
+ console.log(` ${ms}/`);
1649
+ const phases = listPhases(specwfDir, ms);
1650
+ for (const ph of phases) {
1651
+ console.log(` ${ph}/`);
1652
+ const changes = listChanges(specwfDir, ms, ph);
1653
+ for (const ch of changes) {
1654
+ console.log(` ${ch}/`);
1655
+ }
1656
+ }
1657
+ }
1658
+ hasItems = true;
1659
+ }
1660
+ const adhoc = listAdhocChanges(specwfDir);
1661
+ if (adhoc.length > 0) {
1662
+ if (hasItems) console.log("");
1663
+ console.log("\u4E34\u65F6 Changes:");
1664
+ for (const ch of adhoc) {
1665
+ console.log(` ${ch}/`);
1666
+ }
1667
+ hasItems = true;
1668
+ }
1669
+ if (options.all) {
1670
+ const archived = listArchived(specwfDir);
1671
+ if (archived.length > 0) {
1672
+ if (hasItems) console.log("");
1673
+ console.log("\u5F52\u6863:");
1674
+ for (const a of archived) {
1675
+ console.log(` ${a}/`);
1676
+ }
1677
+ hasItems = true;
1678
+ }
1679
+ }
1680
+ if (!hasItems) {
1681
+ console.log("(\u65E0\u6761\u76EE)");
1682
+ }
1683
+ }
1684
+
1685
+ // src/commands/specwf-template.ts
1686
+ import { join as join18, dirname as dirname4 } from "path";
1687
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, existsSync as existsSync10, readFileSync as readFileSync13 } from "fs";
1688
+ import { fileURLToPath as fileURLToPath4 } from "url";
1689
+ var __dirname4 = dirname4(fileURLToPath4(import.meta.url));
1690
+ var TEMPLATES_DIR4 = join18(__dirname4, "templates", "artifacts");
1691
+ var TEMPLATE_TYPES = [
1692
+ "proposal",
1693
+ "design",
1694
+ "tasks",
1695
+ "context",
1696
+ "research",
1697
+ "summary",
1698
+ "verification",
1699
+ "spec-review",
1700
+ "quality-review",
1701
+ "goal-review",
1702
+ "project.yml",
1703
+ "state.md"
1704
+ ];
1705
+ function register9(program2) {
1706
+ program2.command("template <type>").description("\u751F\u6210\u6A21\u677F\u6587\u4EF6\uFF08proposal|design|tasks|context|research|summary|...\uFF09").option("--name <name>", "change \u540D\u79F0", "my-change").option("--dir <path>", "\u76EE\u6807\u76EE\u5F55\uFF08\u9ED8\u8BA4 specwf/changes/<name>/\uFF09").action(templateHandler);
1707
+ }
1708
+ function templateHandler(type, options) {
1709
+ if (!TEMPLATE_TYPES.includes(type)) {
1710
+ console.error(`\u672A\u77E5\u6A21\u677F\u7C7B\u578B: ${type}\u3002\u53EF\u9009: ${TEMPLATE_TYPES.join(", ")}`);
1711
+ process.exit(1);
1712
+ }
1713
+ const templatePath = join18(TEMPLATES_DIR4, type.endsWith(".yml") || type.endsWith(".md") ? type : `${type}.md`);
1714
+ if (!existsSync10(templatePath)) {
1715
+ console.error(`\u6A21\u677F\u6587\u4EF6\u4E0D\u5B58\u5728: ${templatePath}`);
1716
+ process.exit(1);
1717
+ }
1718
+ let content = readFileSync13(templatePath, "utf-8");
1719
+ const name = options.name;
1720
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1721
+ content = content.replace(/\{\{name\}\}/g, name);
1722
+ content = content.replace(/\{\{date\}\}/g, date);
1723
+ content = content.replace(/\{\{phase-name\}\}/g, name);
1724
+ content = content.replace(/\{\{change-name\}\}/g, name);
1725
+ let targetDir;
1726
+ let filename;
1727
+ if (options.dir) {
1728
+ targetDir = options.dir.startsWith("/") ? options.dir : join18(process.cwd(), options.dir);
1729
+ filename = type;
1730
+ } else if (type === "project.yml" || type === "state.md") {
1731
+ targetDir = join18(process.cwd(), "specwf");
1732
+ filename = type;
1733
+ } else {
1734
+ targetDir = join18(process.cwd(), "specwf", "changes", name);
1735
+ filename = type.endsWith(".yml") ? type : `${type}.md`;
1736
+ }
1737
+ mkdirSync6(targetDir, { recursive: true });
1738
+ const fullPath = join18(targetDir, filename);
1739
+ writeFileSync8(fullPath, content, "utf-8");
1740
+ console.log(`\u2713 \u521B\u5EFA ${fullPath}`);
1741
+ }
1742
+
1743
+ // src/commands/specwf-change.ts
1744
+ import { join as join19, dirname as dirname5 } from "path";
1745
+ import { existsSync as existsSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
1746
+ import { fileURLToPath as fileURLToPath5 } from "url";
1747
+ var __dirname5 = dirname5(fileURLToPath5(import.meta.url));
1748
+ function register10(program2) {
1749
+ const cmd = program2.command("change").description("\u7BA1\u7406 change\uFF08\u521B\u5EFA/\u5217\u8868\uFF09");
1750
+ cmd.command("new <name>").description("\u521B\u5EFA\u4E34\u65F6 change\uFF08\u4E0E\u9636\u6BB5\u65E0\u5173\uFF09").option("--dir <path>", "specwf \u76EE\u5F55", "specwf").action(newChange);
1751
+ cmd.action(() => {
1752
+ console.log("\u7528\u6CD5: specwf change new <name>");
1753
+ });
1754
+ }
1755
+ function newChange(name, options) {
1756
+ const specwfDir = join19(process.cwd(), options.dir);
1757
+ const changeDir = createAdhocChangeDir(specwfDir, name);
1758
+ console.log(`\u2713 \u521B\u5EFA change \u76EE\u5F55: changes/${name}/`);
1759
+ const templatesDir = join19(__dirname5, "templates", "artifacts");
1760
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1761
+ for (const file of ["proposal.md", "design.md", "tasks.md"]) {
1762
+ const tplPath = join19(templatesDir, file);
1763
+ let content;
1764
+ if (existsSync11(tplPath)) {
1765
+ content = readFileSync14(tplPath, "utf-8");
1766
+ content = content.replace(/\{\{name\}\}/g, name);
1767
+ content = content.replace(/\{\{date\}\}/g, date);
1768
+ } else {
1769
+ content = `# ${file.replace(".md", "")}: ${name}
1770
+ `;
1771
+ }
1772
+ writeFileSync9(join19(changeDir, file), content, "utf-8");
1773
+ }
1774
+ console.log("\u2713 \u521B\u5EFA\u6A21\u677F\u6587\u4EF6: proposal.md, design.md, tasks.md");
1775
+ try {
1776
+ updateState(specwfDir, (state) => {
1777
+ state.adhoc.push({ name, status: "proposal", depends_on: [] });
1778
+ });
1779
+ console.log("\u2713 state.md \u5DF2\u66F4\u65B0");
1780
+ } catch {
1781
+ console.log("\u26A0 state.md \u66F4\u65B0\u5931\u8D25\uFF08\u975E\u5173\u952E\uFF09");
1782
+ }
1783
+ console.log("");
1784
+ console.log("\u2192 \u4E0B\u4E00\u6B65: \u5B8C\u6210 proposal \u540E\uFF0C\u8FD0\u884C specwf continue \u63A8\u8FDB");
1785
+ }
1786
+
1787
+ // src/cli.ts
1788
+ var version = "0.1.0";
1789
+ program.name("specwf").description("\u89C4\u683C\u9A71\u52A8\u5F00\u53D1\u5DE5\u4F5C\u6D41 \u2014 spec-driven development workflow").version(version);
1790
+ register(program);
1791
+ register2(program);
1792
+ register3(program);
1793
+ register4(program);
1794
+ register5(program);
1795
+ register6(program);
1796
+ register7(program);
1797
+ register8(program);
1798
+ register9(program);
1799
+ register10(program);
1800
+ program.parse();