specwf 0.2.0 → 0.2.2

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/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { program } from "commander";
5
5
 
6
6
  // src/commands/specwf-init.ts
7
- import { join as join5 } from "path";
7
+ import { join as join8 } from "path";
8
8
 
9
9
  // src/core/config.ts
10
10
  import { join } from "path";
@@ -468,66 +468,9 @@ async function runBrownfieldInit(rootDir, specwfDir, info) {
468
468
  return domains;
469
469
  }
470
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
471
  // src/generators/omp-commands.ts
529
472
  import { readFileSync as readFileSync6 } from "fs";
530
- import { join as join6, dirname } from "path";
473
+ import { join as join5, dirname } from "path";
531
474
  import { fileURLToPath } from "url";
532
475
  var STEP_DEFS = [
533
476
  { step: "init", name: "specwf:init", description: "\u521D\u59CB\u5316 specwf \u9879\u76EE\u7ED3\u6784", usesAgent: true, agents: ["researcher"] },
@@ -548,9 +491,9 @@ var STEP_DEFS = [
548
491
  { step: "continue", name: "specwf:continue", description: "\u81EA\u52A8\u63A8\u8FDB \u2014 \u8BFB STATE \u786E\u5B9A\u4E0B\u4E00\u6B65", usesAgent: false, agents: [] }
549
492
  ];
550
493
  var __dirname = dirname(fileURLToPath(import.meta.url));
551
- var TEMPLATES_DIR = join6(__dirname, "templates", "commands");
494
+ var TEMPLATES_DIR = join5(__dirname, "templates", "commands");
552
495
  function loadTemplate(step) {
553
- return readFileSync6(join6(TEMPLATES_DIR, `${step}.md`), "utf-8");
496
+ return readFileSync6(join5(TEMPLATES_DIR, `${step}.md`), "utf-8");
554
497
  }
555
498
  function renderTemplate(template, vars) {
556
499
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
@@ -634,12 +577,12 @@ function generateAllCommands(config) {
634
577
 
635
578
  // src/generators/omp-agents.ts
636
579
  import { readFileSync as readFileSync7 } from "fs";
637
- import { join as join7, dirname as dirname2 } from "path";
580
+ import { join as join6, dirname as dirname2 } from "path";
638
581
  import { fileURLToPath as fileURLToPath2 } from "url";
639
582
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
640
- var TEMPLATES_DIR2 = join7(__dirname2, "templates", "agents");
583
+ var TEMPLATES_DIR2 = join6(__dirname2, "templates", "agents");
641
584
  function loadTemplate2(role) {
642
- return readFileSync7(join7(TEMPLATES_DIR2, `${role}.md`), "utf-8");
585
+ return readFileSync7(join6(TEMPLATES_DIR2, `${role}.md`), "utf-8");
643
586
  }
644
587
  function renderTemplate2(template, vars) {
645
588
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
@@ -750,12 +693,12 @@ function generateAllAgents(config) {
750
693
 
751
694
  // src/generators/skills.ts
752
695
  import { readFileSync as readFileSync8 } from "fs";
753
- import { join as join8, dirname as dirname3 } from "path";
696
+ import { join as join7, dirname as dirname3 } from "path";
754
697
  import { fileURLToPath as fileURLToPath3 } from "url";
755
698
  var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
756
- var TEMPLATES_DIR3 = join8(__dirname3, "templates", "skills");
699
+ var TEMPLATES_DIR3 = join7(__dirname3, "templates", "skills");
757
700
  function loadTemplate3(step) {
758
- return readFileSync8(join8(TEMPLATES_DIR3, `${step}.md`), "utf-8");
701
+ return readFileSync8(join7(TEMPLATES_DIR3, `${step}.md`), "utf-8");
759
702
  }
760
703
  function renderTemplate3(template, vars) {
761
704
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
@@ -828,7 +771,69 @@ function writeGeneratedFiles(files) {
828
771
  }
829
772
  }
830
773
 
774
+ // src/commands/specwf-init.ts
775
+ function register(program2) {
776
+ 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);
777
+ }
778
+ async function initHandler(options) {
779
+ const baseDir = options.dir.startsWith("/") ? options.dir : join8(process.cwd(), options.dir);
780
+ const specwfDir = join8(baseDir, "specwf");
781
+ if (isInitialized(specwfDir)) {
782
+ console.error("specwf \u5DF2\u521D\u59CB\u5316\u3002\u8FD0\u884C `specwf update` \u66F4\u65B0\u5E73\u53F0\u6587\u4EF6\u3002");
783
+ process.exit(1);
784
+ }
785
+ const wizard = await runInitWizard({ profile: options.profile, yes: options.yes });
786
+ const profile = wizard.profile;
787
+ const platform = wizard.platform;
788
+ const isBrownfield = options.brownfield || wizard.brownfield;
789
+ createSpecwfStructure(specwfDir);
790
+ console.log("\u2713 \u521B\u5EFA specwf/ \u76EE\u5F55\u7ED3\u6784");
791
+ saveConfig(specwfDir, {
792
+ version: 1,
793
+ platform,
794
+ profile,
795
+ context: wizard.context,
796
+ workflow: {},
797
+ review: {},
798
+ change: {},
799
+ git: { branching: "none", create_tag: true },
800
+ conventions: { inject: true },
801
+ models: {}
802
+ });
803
+ console.log("\u2713 \u521B\u5EFA project.yml (profile: " + profile + ")");
804
+ saveState(specwfDir, {
805
+ project: {
806
+ name: baseDir.split("/").pop() || "project",
807
+ status: "initialized",
808
+ current_milestone: null,
809
+ current_phase: null
810
+ },
811
+ active_context: {
812
+ type: "project",
813
+ ref: null,
814
+ step: "init"
815
+ },
816
+ changes: [],
817
+ adhoc: []
818
+ });
819
+ console.log("\u2713 \u521B\u5EFA state.md");
820
+ if (isBrownfield) {
821
+ const info = detectProjectInfo(process.cwd());
822
+ const domains = await runBrownfieldInit(process.cwd(), specwfDir, info);
823
+ console.log("\u2713 \u5B58\u91CF\u9879\u76EE codebase mapping \u5B8C\u6210 (" + domains.length + " \u4E2A spec \u57DF)");
824
+ }
825
+ console.log("specwf \u521D\u59CB\u5316\u5B8C\u6210\u3002");
826
+ try {
827
+ const files = generateAll({ version: 1, platform, profile, context: wizard.context, workflow: {}, review: {}, change: {}, git: { branching: "none", create_tag: true }, conventions: { inject: true }, models: {} });
828
+ writeGeneratedFiles(files);
829
+ console.log(`\u2713 \u5E73\u53F0\u6587\u4EF6\u5DF2\u751F\u6210 (${files.length} \u4E2A)`);
830
+ } catch {
831
+ console.log("\u26A0 \u5E73\u53F0\u6587\u4EF6\u751F\u6210\u5931\u8D25\uFF0C\u53EF\u7A0D\u540E\u8FD0\u884C `specwf update` \u91CD\u8BD5");
832
+ }
833
+ }
834
+
831
835
  // src/commands/specwf-update.ts
836
+ import { join as join9 } from "path";
832
837
  function register2(program2) {
833
838
  program2.command("update").description("\u66F4\u65B0\u5E73\u53F0\u6587\u4EF6\uFF08commands + agents\uFF09").option("--dir <path>", "specwf \u76EE\u5F55", "specwf").action(updateHandler);
834
839
  }
@@ -882,7 +887,128 @@ function findSpecwfDir() {
882
887
  }
883
888
 
884
889
  // src/commands/specwf-state.ts
890
+ import { join as join12 } from "path";
891
+
892
+ // src/core/state-validator.ts
893
+ import { existsSync as existsSync6, readFileSync as readFileSync9, readdirSync as readdirSync3 } from "fs";
885
894
  import { join as join11 } from "path";
895
+ var EXIT_CRITERIA = [
896
+ // milestone/active → 必须有 requirements.md
897
+ {
898
+ type: "milestone",
899
+ step: "active",
900
+ checks: [
901
+ { path: "requirements.md", description: "requirements.md \u4E0D\u5B58\u5728" }
902
+ ]
903
+ },
904
+ // project/requirements-defined → 必须有完整的 requirements.md
905
+ {
906
+ type: "project",
907
+ step: "requirements-defined",
908
+ checks: [
909
+ { path: "requirements.md", description: "requirements.md \u5185\u5BB9\u4E3A\u6A21\u677F\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5" }
910
+ ]
911
+ },
912
+ // project/researched → 必须有调研产出
913
+ {
914
+ type: "project",
915
+ step: "researched",
916
+ checks: [
917
+ { path: "research/summary.md", description: "research/summary.md \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u5B8C\u6210\u8C03\u7814" }
918
+ ]
919
+ },
920
+ // phase/discuss → 必须有 context.md
921
+ {
922
+ type: "phase",
923
+ step: "discuss",
924
+ checks: [
925
+ { path: "roadmap.md", description: "roadmap.md \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u62C6\u5206\u8DEF\u7EBF\u56FE" }
926
+ ]
927
+ },
928
+ // phase/research → 必须有 research.md
929
+ {
930
+ type: "phase",
931
+ step: "research",
932
+ checks: [
933
+ { path: "roadmap.md", description: "roadmap.md \u4E0D\u5B58\u5728" }
934
+ ]
935
+ },
936
+ // adhoc/proposal → proposal.md 不能是模板
937
+ {
938
+ type: "adhoc",
939
+ step: "proposal",
940
+ checks: [
941
+ { path: "changes/", description: "change \u7684 proposal.md \u4E3A\u6A21\u677F\u7A7A\u58F3\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5" }
942
+ ]
943
+ },
944
+ // change/planning → design.md + tasks.md 不能是模板
945
+ {
946
+ type: "change",
947
+ step: "planning",
948
+ checks: [
949
+ { path: "changes/", description: "design.md \u6216 tasks.md \u4E3A\u6A21\u677F\u7A7A\u58F3\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5" }
950
+ ]
951
+ }
952
+ ];
953
+ function isTemplateFile(filePath) {
954
+ try {
955
+ const content = readFileSync9(filePath, "utf-8");
956
+ const placeholders = content.match(/\{\{[a-zA-Z_-]+\}\}/g);
957
+ return (placeholders?.length ?? 0) > 3;
958
+ } catch {
959
+ return false;
960
+ }
961
+ }
962
+ function findChangeDir(specwfDir) {
963
+ const changesDir = join11(specwfDir, "changes");
964
+ if (!existsSync6(changesDir)) return [];
965
+ try {
966
+ return readdirSync3(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
967
+ } catch {
968
+ return [];
969
+ }
970
+ }
971
+ function checkExitCondition(specwfDir, check) {
972
+ const fullPath = join11(specwfDir, check.path);
973
+ if (check.path.endsWith("/") || check.description.includes("\u7684 ")) {
974
+ const changes = findChangeDir(specwfDir);
975
+ for (const change of changes) {
976
+ for (const doc of ["proposal.md", "design.md", "tasks.md"]) {
977
+ const docPath = join11(specwfDir, "changes", change, doc);
978
+ if (existsSync6(docPath) && isTemplateFile(docPath)) {
979
+ return `changes/${change}/${doc} \u4ECD\u4E3A\u6A21\u677F\u7A7A\u58F3\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5`;
980
+ }
981
+ }
982
+ }
983
+ return null;
984
+ }
985
+ if (!existsSync6(fullPath)) {
986
+ return check.description;
987
+ }
988
+ if (isTemplateFile(fullPath)) {
989
+ return check.description;
990
+ }
991
+ return null;
992
+ }
993
+ function validateStepAdvance(contextType, contextStep, cwd) {
994
+ const specwfDir = join11(cwd, "specwf");
995
+ const criteria = EXIT_CRITERIA.find(
996
+ (c) => c.type === contextType && c.step === contextStep
997
+ );
998
+ if (!criteria) {
999
+ return { valid: true, errors: [] };
1000
+ }
1001
+ const errors = [];
1002
+ for (const check of criteria.checks) {
1003
+ const error = checkExitCondition(specwfDir, check);
1004
+ if (error) {
1005
+ errors.push(error);
1006
+ }
1007
+ }
1008
+ return { valid: errors.length === 0, errors };
1009
+ }
1010
+
1011
+ // src/commands/specwf-state.ts
886
1012
  function register4(program2) {
887
1013
  const cmd = program2.command("state").description("\u67E5\u770B/\u4FEE\u6539\u5F53\u524D\u72B6\u6001");
888
1014
  cmd.command("show").description("\u67E5\u770B\u5F53\u524D\u72B6\u6001").action(showState);
@@ -892,7 +1018,7 @@ function register4(program2) {
892
1018
  cmd.action(showState);
893
1019
  }
894
1020
  function findSpecwfDir2() {
895
- return join11(process.cwd(), "specwf");
1021
+ return join12(process.cwd(), "specwf");
896
1022
  }
897
1023
  function showState() {
898
1024
  const specwfDir = findSpecwfDir2();
@@ -915,13 +1041,13 @@ function setMilestone(id) {
915
1041
  updateState(specwfDir, (state) => {
916
1042
  state.project.current_milestone = id;
917
1043
  state.project.current_phase = null;
918
- state.active_context.type = "phase";
1044
+ state.active_context.type = "milestone";
919
1045
  state.active_context.ref = `milestones/${id}`;
920
- state.active_context.step = "discuss";
921
- state.project.status = "phase-discuss";
1046
+ state.active_context.step = "active";
1047
+ state.project.status = "milestone-active";
922
1048
  });
923
- console.log(`\u2713 \u5207\u6362\u5230 milestone: ${id}\uFF08\u72B6\u6001: phase-discuss\uFF09`);
924
- console.log("\u2192 \u4E0B\u4E00\u6B65: /specwf:discuss");
1049
+ console.log(`\u2713 \u5207\u6362\u5230 milestone: ${id}\uFF08\u72B6\u6001: milestone-active\uFF09`);
1050
+ console.log("\u2192 \u4E0B\u4E00\u6B65: \u5B9A\u4E49\u91CC\u7A0B\u7891\u9700\u6C42: /specwf:grill");
925
1051
  }
926
1052
  function setPhase(id) {
927
1053
  const specwfDir = findSpecwfDir2();
@@ -937,18 +1063,51 @@ function setPhase(id) {
937
1063
  }
938
1064
  function setStep(step) {
939
1065
  const specwfDir = findSpecwfDir2();
940
- updateState(specwfDir, (state) => {
941
- state.active_context.step = step;
1066
+ const state = loadState(specwfDir);
1067
+ const ctx = state.active_context;
1068
+ let currentStatus;
1069
+ switch (ctx.type) {
1070
+ case "project":
1071
+ currentStatus = state.project.status;
1072
+ break;
1073
+ case "milestone":
1074
+ currentStatus = "milestone-active";
1075
+ break;
1076
+ case "phase":
1077
+ currentStatus = `phase-${ctx.step}`;
1078
+ break;
1079
+ case "change":
1080
+ currentStatus = `change-${ctx.step}`;
1081
+ break;
1082
+ case "adhoc":
1083
+ currentStatus = `adhoc-${ctx.step}`;
1084
+ break;
1085
+ default:
1086
+ currentStatus = state.project.status;
1087
+ }
1088
+ const result = validateStepAdvance(ctx.type, ctx.step, process.cwd());
1089
+ if (!result.valid) {
1090
+ console.log("\u2500".repeat(50));
1091
+ console.log("\u274C \u524D\u7F6E\u6761\u4EF6\u672A\u6EE1\u8DB3\uFF0C\u65E0\u6CD5\u63A8\u8FDB:");
1092
+ for (const err of result.errors) {
1093
+ console.log(` \u2022 ${err}`);
1094
+ }
1095
+ ;
1096
+ console.log("\u2500".repeat(50));
1097
+ return;
1098
+ }
1099
+ updateState(specwfDir, (state2) => {
1100
+ state2.active_context.step = step;
942
1101
  });
943
1102
  console.log(`\u2713 \u5F53\u524D\u6B65\u9AA4: ${step}`);
944
1103
  }
945
1104
 
946
1105
  // src/commands/specwf-context.ts
947
- import { join as join13 } from "path";
1106
+ import { join as join14 } from "path";
948
1107
 
949
1108
  // 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";
1109
+ import { join as join13 } from "path";
1110
+ import { readdirSync as readdirSync4, existsSync as existsSync7, statSync as statSync2 } from "fs";
952
1111
  var PROJECT_STEPS = ["init", "grill", "research", "roadmap"];
953
1112
  var PHASE_STEPS = ["discuss", "research-phase", "split"];
954
1113
  var CHANGE_STEPS = ["plan", "apply", "review", "verify", "archive"];
@@ -973,7 +1132,7 @@ function generateContext(specwfDir, step) {
973
1132
  requirements: []
974
1133
  };
975
1134
  result.conventions = getAllConventions(specwfDir);
976
- if (existsSync6(join12(specwfDir, "requirements.md"))) {
1135
+ if (existsSync7(join13(specwfDir, "requirements.md"))) {
977
1136
  result.requirements.push({ path: "requirements.md", description: "\u9700\u6C42\u89C4\u683C" });
978
1137
  }
979
1138
  if (isProjectStep(step)) {
@@ -987,7 +1146,7 @@ function generateContext(specwfDir, step) {
987
1146
  return result;
988
1147
  }
989
1148
  function getAllSpecs(specwfDir) {
990
- const specsDir = join12(specwfDir, "specs");
1149
+ const specsDir = join13(specwfDir, "specs");
991
1150
  return listSpecFiles(specsDir, "specs");
992
1151
  }
993
1152
  function getRelatedSpecs(specwfDir, state) {
@@ -1002,34 +1161,34 @@ function getRelatedSpecs(specwfDir, state) {
1002
1161
  return related.length > 0 ? related : allSpecs;
1003
1162
  }
1004
1163
  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" }));
1164
+ const convDir = join13(specwfDir, "conventions");
1165
+ if (!existsSync7(convDir)) return [];
1166
+ return readdirSync4(convDir).filter((f) => f.endsWith(".md")).map((f) => ({ path: `conventions/${f}`, description: "\u9879\u76EE\u7EA6\u5B9A" }));
1008
1167
  }
1009
1168
  function getChangeArtifacts(specwfDir, state) {
1010
1169
  const ref = state.active_context.ref;
1011
1170
  if (!ref) return [];
1012
- const changeDir = join12(specwfDir, ref);
1013
- if (!existsSync6(changeDir)) return [];
1171
+ const changeDir = join13(specwfDir, ref);
1172
+ if (!existsSync7(changeDir)) return [];
1014
1173
  const artifacts = [];
1015
1174
  for (const file of ["proposal.md", "design.md", "tasks.md", ".specwf.yaml"]) {
1016
- const fullPath = join12(changeDir, file);
1017
- if (existsSync6(fullPath)) {
1175
+ const fullPath = join13(changeDir, file);
1176
+ if (existsSync7(fullPath)) {
1018
1177
  artifacts.push({ path: `${ref}/${file}`, description: "change \u4EA7\u7269" });
1019
1178
  }
1020
1179
  }
1021
- const specsDir = join12(changeDir, "specs");
1022
- if (existsSync6(specsDir)) {
1180
+ const specsDir = join13(changeDir, "specs");
1181
+ if (existsSync7(specsDir)) {
1023
1182
  const deltaSpecs = listSpecFiles(specsDir, `${ref}/specs`);
1024
1183
  artifacts.push(...deltaSpecs);
1025
1184
  }
1026
1185
  return artifacts;
1027
1186
  }
1028
1187
  function listSpecFiles(dir, prefix) {
1029
- if (!existsSync6(dir)) return [];
1188
+ if (!existsSync7(dir)) return [];
1030
1189
  const results = [];
1031
- for (const entry of readdirSync3(dir)) {
1032
- const fullPath = join12(dir, entry);
1190
+ for (const entry of readdirSync4(dir)) {
1191
+ const fullPath = join13(dir, entry);
1033
1192
  const stat = statSync2(fullPath);
1034
1193
  if (stat.isDirectory()) {
1035
1194
  results.push(...listSpecFiles(fullPath, `${prefix}/${entry}`));
@@ -1084,7 +1243,7 @@ function register5(program2) {
1084
1243
  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
1244
  }
1086
1245
  function contextHandler(step, options) {
1087
- const specwfDir = join13(process.cwd(), "specwf");
1246
+ const specwfDir = join14(process.cwd(), "specwf");
1088
1247
  const result = generateContext(specwfDir, step);
1089
1248
  if (options.json) {
1090
1249
  console.log(JSON.stringify(result, null, 2));
@@ -1094,7 +1253,7 @@ function contextHandler(step, options) {
1094
1253
  }
1095
1254
 
1096
1255
  // src/commands/specwf-continue.ts
1097
- import { join as join14 } from "path";
1256
+ import { join as join15 } from "path";
1098
1257
 
1099
1258
  // src/types/state.ts
1100
1259
  var STATE_TRANSITIONS = [
@@ -1117,6 +1276,8 @@ var STATE_TRANSITIONS = [
1117
1276
  { from: "change-verifying", command: "replan", to: "change-planning", slashCommand: "/specwf:plan", subagent: true },
1118
1277
  { from: "change-verifying", command: "reapply", to: "change-applying", slashCommand: "/specwf:apply", subagent: true },
1119
1278
  { from: "change-reviewing", command: "fix", to: "change-applying", slashCommand: "/specwf:apply", subagent: true },
1279
+ // Milestone 层(新里程碑 = 项目流程 - init)
1280
+ { from: "milestone-active", command: "grill", to: "requirements-defined", slashCommand: "/specwf:grill" },
1120
1281
  // Ship
1121
1282
  { from: "change-archived", command: "ship-phase", to: "phase-shipped", slashCommand: "/specwf:ship" },
1122
1283
  { from: "phase-shipped", command: "next-phase", to: "phase-discuss", slashCommand: "/specwf:discuss" },
@@ -1144,9 +1305,10 @@ function determineChangeNextStep(specwfDir, changeName) {
1144
1305
  }
1145
1306
  const adhoc = state.adhoc.find((c) => c.name === changeName);
1146
1307
  if (adhoc) {
1308
+ const prefix = adhoc.status === "proposal" ? "adhoc" : "change";
1147
1309
  return determineFromChangeStatus(
1148
1310
  changeName,
1149
- `adhoc-${adhoc.status}`,
1311
+ `${prefix}-${adhoc.status}`,
1150
1312
  "adhoc"
1151
1313
  );
1152
1314
  }
@@ -1154,6 +1316,95 @@ function determineChangeNextStep(specwfDir, changeName) {
1154
1316
  error: `change \u4E0D\u5B58\u5728: ${changeName}\u3002\u53EF\u7528: ${listAvailableChanges(state)}`
1155
1317
  };
1156
1318
  }
1319
+ var STEP_INFO = {
1320
+ grill: {
1321
+ command: "grill",
1322
+ description: "\u901A\u8FC7\u9010\u6761\u63D0\u95EE\u6536\u96C6\u9700\u6C42\uFF0C\u4EA7\u51FA requirements.md",
1323
+ artifacts: ["specwf/requirements.md"],
1324
+ fileRef: ".omp/commands/specwf-grill.md"
1325
+ },
1326
+ research: {
1327
+ command: "research",
1328
+ description: "\u5E76\u884C\u8C03\u7814\u6280\u672F\u65B9\u5411\u548C\u67B6\u6784\u65B9\u6848",
1329
+ artifacts: ["specwf/research/stack.md", "specwf/research/architecture.md", "specwf/research/pitfalls.md", "specwf/research/summary.md"],
1330
+ fileRef: ".omp/commands/specwf-research.md"
1331
+ },
1332
+ "research-done": {
1333
+ command: "research-done",
1334
+ description: "\u6807\u8BB0\u8C03\u7814\u5B8C\u6210\uFF0C\u8FDB\u5165\u8DEF\u7EBF\u56FE\u62C6\u5206",
1335
+ artifacts: [],
1336
+ fileRef: ""
1337
+ },
1338
+ roadmap: {
1339
+ command: "roadmap",
1340
+ description: "\u5C06\u9879\u76EE\u62C6\u5206\u4E3A Milestone \xD7 Phase",
1341
+ artifacts: ["specwf/roadmap.md"],
1342
+ fileRef: ".omp/commands/specwf-roadmap.md"
1343
+ },
1344
+ discuss: {
1345
+ command: "discuss",
1346
+ description: "Phase \u8BA8\u8BBA\uFF0C\u6355\u83B7\u5B9E\u73B0\u51B3\u7B56",
1347
+ artifacts: ["milestones/<ms>/phases/<ph>/context.md"],
1348
+ fileRef: ".omp/commands/specwf-discuss.md"
1349
+ },
1350
+ "research-phase": {
1351
+ command: "research-phase",
1352
+ description: "\u5BF9\u5F53\u524D phase \u8FDB\u884C\u6280\u672F\u8C03\u7814",
1353
+ artifacts: ["milestones/<ms>/phases/<ph>/research.md"],
1354
+ fileRef: ".omp/commands/specwf-research-phase.md"
1355
+ },
1356
+ split: {
1357
+ command: "split",
1358
+ description: "\u5C06 phase \u62C6\u5206\u4E3A\u591A\u4E2A change\uFF0C\u786E\u5B9A\u4F9D\u8D56\u56FE",
1359
+ artifacts: ["specwf/roadmap.md\uFF08\u66F4\u65B0\uFF09"],
1360
+ fileRef: ".omp/commands/specwf-split.md"
1361
+ },
1362
+ plan: {
1363
+ command: "plan",
1364
+ description: "Change \u8BBE\u8BA1\uFF1A\u8BBE\u8BA1\u6280\u672F\u65B9\u6848\u3001\u62C6\u5206\u4EFB\u52A1\u3001\u9884\u5199 delta-specs",
1365
+ artifacts: ["design.md", "tasks.md", "specs/<domain>/spec.md"],
1366
+ fileRef: ".omp/commands/specwf-plan.md"
1367
+ },
1368
+ apply: {
1369
+ command: "apply",
1370
+ description: "\u6309 tasks.md \u5B9E\u73B0\u4EE3\u7801\uFF0Ctype:behavior \u8D70 RED\u2192GREEN\u2192REFACTOR",
1371
+ artifacts: ["\u4EE3\u7801\u53D8\u66F4", "\u6D4B\u8BD5"],
1372
+ fileRef: ".omp/commands/specwf-apply.md"
1373
+ },
1374
+ review: {
1375
+ command: "review",
1376
+ description: "\u4E09\u91CD\u5BA1\u67E5\uFF1A\u89C4\u683C\u5BA1\u67E5 + \u8D28\u91CF\u5BA1\u67E5 + \u76EE\u6807\u5BA1\u67E5",
1377
+ artifacts: ["REVIEW.md"],
1378
+ fileRef: ".omp/commands/specwf-review.md"
1379
+ },
1380
+ verify: {
1381
+ command: "verify",
1382
+ description: "\u8FD0\u884C\u6D4B\u8BD5\uFF0C\u8BCA\u65AD\u6839\u56E0\uFF0C\u8DEF\u7531\u56DE\u73AF",
1383
+ artifacts: ["VERIFICATION.md"],
1384
+ fileRef: ".omp/commands/specwf-verify.md"
1385
+ },
1386
+ archive: {
1387
+ command: "archive",
1388
+ description: "Delta-spec \u5408\u5E76 + \u4EE3\u7801\u8BA4\u77E5\u56DE\u704C + \u76EE\u5F55\u5F52\u6863",
1389
+ artifacts: ["archive/<change-id>/"],
1390
+ fileRef: ".omp/commands/specwf-archive.md"
1391
+ },
1392
+ "ship-phase": {
1393
+ command: "ship-phase",
1394
+ description: "\u521B\u5EFA PR + \u66F4\u65B0 state.md",
1395
+ artifacts: ["GitHub PR", "state.md \u66F4\u65B0"],
1396
+ fileRef: ".omp/commands/specwf-ship.md"
1397
+ },
1398
+ "ship-milestone": {
1399
+ command: "ship-milestone",
1400
+ description: "\u53D1\u5E03 release tag + \u66F4\u65B0\u7248\u672C\u53F7",
1401
+ artifacts: ["git tag", "RELEASE.md", "npm publish"],
1402
+ fileRef: ".omp/commands/specwf-ship.md"
1403
+ }
1404
+ };
1405
+ function getStepInfo(command) {
1406
+ return STEP_INFO[command];
1407
+ }
1157
1408
  function determineFromChangeStatus(name, statusKey, type) {
1158
1409
  const available = getNextSteps(statusKey);
1159
1410
  const availableSteps = available.map((t) => ({
@@ -1169,7 +1420,8 @@ function determineFromChangeStatus(name, statusKey, type) {
1169
1420
  slashCommand: first?.slashCommand || null,
1170
1421
  needsSubagent: first?.subagent ?? false,
1171
1422
  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
1423
+ hint: available.length === 0 ? "\u8BE5 change \u5DF2\u6CA1\u6709\u53EF\u7528\u4E0B\u4E00\u6B65\u3002\u521B\u5EFA\u65B0 change \u7EE7\u7EED\u3002" : null,
1424
+ nextStepInfo: first ? getStepInfo(first.command) : void 0
1173
1425
  };
1174
1426
  }
1175
1427
  function listAvailableChanges(state) {
@@ -1197,7 +1449,8 @@ function determineFromState(state) {
1197
1449
  slashCommand: first?.slashCommand || null,
1198
1450
  needsSubagent: first?.subagent ?? false,
1199
1451
  availableSteps,
1200
- hint
1452
+ hint,
1453
+ nextStepInfo: first ? getStepInfo(first.command) : void 0
1201
1454
  };
1202
1455
  }
1203
1456
  function resolveStatus(state) {
@@ -1263,14 +1516,27 @@ function formatContinueResult(result) {
1263
1516
  console.log(`\u5F53\u524D\u4F4D\u7F6E: ${result.context}`);
1264
1517
  console.log(`\u5F53\u524D\u6B65\u9AA4: ${result.currentStep}`);
1265
1518
  if (result.nextCommand) {
1519
+ const info = result.nextStepInfo;
1266
1520
  console.log("");
1267
- console.log(`\u2192 \u63A8\u8350\u4E0B\u4E00\u6B65: ${result.nextCommand}`);
1521
+ console.log(`\u2192 \u4E0B\u4E00\u6B65: ${result.nextCommand}`);
1268
1522
  if (result.slashCommand) {
1269
1523
  console.log(` Slash \u547D\u4EE4: ${result.slashCommand}`);
1270
1524
  }
1271
1525
  if (result.needsSubagent) {
1272
1526
  console.log(` \u9700\u8981\u5B50\u4EE3\u7406: \u662F`);
1273
1527
  }
1528
+ if (info) {
1529
+ console.log(` \u63CF\u8FF0: ${info.description}`);
1530
+ if (info.artifacts.length > 0) {
1531
+ console.log(` \u4EA7\u51FA\u7269:`);
1532
+ for (const a of info.artifacts) {
1533
+ console.log(` - ${a}`);
1534
+ }
1535
+ }
1536
+ if (info.fileRef) {
1537
+ console.log(` \u53C2\u8003: ${info.fileRef}`);
1538
+ }
1539
+ }
1274
1540
  } else {
1275
1541
  console.log("");
1276
1542
  console.log("\u2192 \u5F53\u524D\u65E0\u53EF\u7528\u4E0B\u4E00\u6B65");
@@ -1281,12 +1547,12 @@ function formatContinueResult(result) {
1281
1547
  console.log("\u2500".repeat(50));
1282
1548
  }
1283
1549
  function continueHandler() {
1284
- const specwfDir = join14(process.cwd(), "specwf");
1550
+ const specwfDir = join15(process.cwd(), "specwf");
1285
1551
  const result = determineNextStep(specwfDir);
1286
1552
  formatContinueResult(result);
1287
1553
  }
1288
1554
  function continueChangeHandler(name) {
1289
- const specwfDir = join14(process.cwd(), "specwf");
1555
+ const specwfDir = join15(process.cwd(), "specwf");
1290
1556
  const result = determineChangeNextStep(specwfDir, name);
1291
1557
  if ("error" in result) {
1292
1558
  console.log("\u2500".repeat(50));
@@ -1298,12 +1564,12 @@ function continueChangeHandler(name) {
1298
1564
  }
1299
1565
 
1300
1566
  // 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";
1567
+ import { join as join17 } from "path";
1568
+ import { existsSync as existsSync10, readdirSync as readdirSync6, mkdirSync as mkdirSync5, copyFileSync } from "fs";
1303
1569
 
1304
1570
  // src/core/delta-merge.ts
1305
1571
  import { createHash } from "crypto";
1306
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
1572
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
1307
1573
 
1308
1574
  // src/parser/heading-tree.ts
1309
1575
  function parseHeadings(markdown) {
@@ -1438,8 +1704,8 @@ function renderNodes(nodes, lines) {
1438
1704
  }
1439
1705
  }
1440
1706
  function mergeAndWrite(liveSpecPath, deltaSpecPath, baseFingerprint) {
1441
- const baseSpec = readFileSync10(liveSpecPath, "utf-8");
1442
- const deltaSpec = readFileSync10(deltaSpecPath, "utf-8");
1707
+ const baseSpec = readFileSync11(liveSpecPath, "utf-8");
1708
+ const deltaSpec = readFileSync11(deltaSpecPath, "utf-8");
1443
1709
  const result = mergeDeltaSpec(baseSpec, deltaSpec, baseFingerprint);
1444
1710
  if (result.type === "ok") {
1445
1711
  writeFileSync6(liveSpecPath, result.merged, "utf-8");
@@ -1449,8 +1715,8 @@ function mergeAndWrite(liveSpecPath, deltaSpecPath, baseFingerprint) {
1449
1715
 
1450
1716
  // src/core/code-extract.ts
1451
1717
  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";
1718
+ import { existsSync as existsSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync5 } from "fs";
1719
+ import { join as join16 } from "path";
1454
1720
  function extractFromGitDiff(repoDir, changeDir) {
1455
1721
  const diff = getGitDiff(repoDir);
1456
1722
  if (diff === null) {
@@ -1486,10 +1752,10 @@ function getGitDiff(repoDir) {
1486
1752
  }
1487
1753
  }
1488
1754
  function detectDomains(changeDir) {
1489
- const specsDir = join15(changeDir, "specs");
1490
- if (!existsSync8(specsDir)) return ["general"];
1755
+ const specsDir = join16(changeDir, "specs");
1756
+ if (!existsSync9(specsDir)) return ["general"];
1491
1757
  try {
1492
- return readdirSync4(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1758
+ return readdirSync5(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1493
1759
  } catch {
1494
1760
  return ["general"];
1495
1761
  }
@@ -1527,11 +1793,11 @@ function extractConstraints(diff, _domain) {
1527
1793
  return constraints;
1528
1794
  }
1529
1795
  function writeExtractionToSpec(specsDir, extraction) {
1530
- const domainDir = join15(specsDir, extraction.domain);
1531
- const specPath = join15(domainDir, "spec.md");
1796
+ const domainDir = join16(specsDir, extraction.domain);
1797
+ const specPath = join16(domainDir, "spec.md");
1532
1798
  let existing = "";
1533
- if (existsSync8(specPath)) {
1534
- existing = readFileSync11(specPath, "utf-8");
1799
+ if (existsSync9(specPath)) {
1800
+ existing = readFileSync12(specPath, "utf-8");
1535
1801
  }
1536
1802
  const section = generateAutoExtractedSection(extraction);
1537
1803
  const updated = existing.trim() ? `${existing.trim()}
@@ -1570,14 +1836,14 @@ function register7(program2) {
1570
1836
  program2.command("archive <change>").description("\u5F52\u6863 change\uFF08delta \u5408\u5E76 + \u4EE3\u7801\u56DE\u704C\uFF09").action(archiveHandler);
1571
1837
  }
1572
1838
  function archiveHandler(changePath) {
1573
- const specwfDir = join16(process.cwd(), "specwf");
1574
- const fullChangePath = join16(process.cwd(), changePath);
1575
- if (!existsSync9(fullChangePath)) {
1839
+ const specwfDir = join17(process.cwd(), "specwf");
1840
+ const fullChangePath = join17(process.cwd(), changePath);
1841
+ if (!existsSync10(fullChangePath)) {
1576
1842
  console.error(`\u9519\u8BEF: change \u76EE\u5F55\u4E0D\u5B58\u5728: ${changePath}`);
1577
1843
  process.exit(1);
1578
1844
  }
1579
- const specsDir = join16(fullChangePath, "specs");
1580
- if (existsSync9(specsDir)) {
1845
+ const specsDir = join17(fullChangePath, "specs");
1846
+ if (existsSync10(specsDir)) {
1581
1847
  mergeDeltaSpecs(specsDir, specwfDir);
1582
1848
  console.log("\u2713 delta-specs \u5408\u5E76\u5B8C\u6210");
1583
1849
  }
@@ -1585,7 +1851,7 @@ function archiveHandler(changePath) {
1585
1851
  const extractResult = extractFromGitDiff(repoDir, fullChangePath);
1586
1852
  if (extractResult.available && extractResult.extractions.length > 0) {
1587
1853
  for (const extraction of extractResult.extractions) {
1588
- writeExtractionToSpec(join16(specwfDir, "specs"), extraction);
1854
+ writeExtractionToSpec(join17(specwfDir, "specs"), extraction);
1589
1855
  }
1590
1856
  if (extractResult.extractions.length > 0) {
1591
1857
  console.log(`\u2713 \u4EE3\u7801\u8BA4\u77E5\u63D0\u53D6\u5B8C\u6210 (${extractResult.extractions.length} \u4E2A\u57DF)`);
@@ -1612,14 +1878,14 @@ function archiveHandler(changePath) {
1612
1878
  console.log("\u5F52\u6863\u5B8C\u6210\u3002");
1613
1879
  }
1614
1880
  function mergeDeltaSpecs(deltaDir, specwfDir) {
1615
- const entries = readdirSync5(deltaDir, { withFileTypes: true });
1881
+ const entries = readdirSync6(deltaDir, { withFileTypes: true });
1616
1882
  for (const entry of entries) {
1617
1883
  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 });
1884
+ const deltaSpecPath = join17(deltaDir, entry.name, "spec.md");
1885
+ const liveSpecPath = join17(specwfDir, "specs", entry.name, "spec.md");
1886
+ if (!existsSync10(deltaSpecPath)) continue;
1887
+ if (!existsSync10(liveSpecPath)) {
1888
+ mkdirSync5(join17(specwfDir, "specs", entry.name), { recursive: true });
1623
1889
  copyFileSync(deltaSpecPath, liveSpecPath);
1624
1890
  continue;
1625
1891
  }
@@ -1634,12 +1900,12 @@ function mergeDeltaSpecs(deltaDir, specwfDir) {
1634
1900
  }
1635
1901
 
1636
1902
  // src/commands/specwf-list.ts
1637
- import { join as join17 } from "path";
1903
+ import { join as join18 } from "path";
1638
1904
  function register8(program2) {
1639
1905
  program2.command("list").description("\u5217\u51FA milestones/phases/changes").option("--all", "\u5305\u542B\u5F52\u6863").action(listHandler);
1640
1906
  }
1641
1907
  function listHandler(options) {
1642
- const specwfDir = join17(process.cwd(), "specwf");
1908
+ const specwfDir = join18(process.cwd(), "specwf");
1643
1909
  let hasItems = false;
1644
1910
  const milestones = listMilestones(specwfDir);
1645
1911
  if (milestones.length > 0) {
@@ -1683,11 +1949,11 @@ function listHandler(options) {
1683
1949
  }
1684
1950
 
1685
1951
  // 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";
1952
+ import { join as join19, dirname as dirname4 } from "path";
1953
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
1688
1954
  import { fileURLToPath as fileURLToPath4 } from "url";
1689
1955
  var __dirname4 = dirname4(fileURLToPath4(import.meta.url));
1690
- var TEMPLATES_DIR4 = join18(__dirname4, "templates", "artifacts");
1956
+ var TEMPLATES_DIR4 = join19(__dirname4, "templates", "artifacts");
1691
1957
  var TEMPLATE_TYPES = [
1692
1958
  "proposal",
1693
1959
  "design",
@@ -1710,12 +1976,12 @@ function templateHandler(type, options) {
1710
1976
  console.error(`\u672A\u77E5\u6A21\u677F\u7C7B\u578B: ${type}\u3002\u53EF\u9009: ${TEMPLATE_TYPES.join(", ")}`);
1711
1977
  process.exit(1);
1712
1978
  }
1713
- const templatePath = join18(TEMPLATES_DIR4, type.endsWith(".yml") || type.endsWith(".md") ? type : `${type}.md`);
1714
- if (!existsSync10(templatePath)) {
1979
+ const templatePath = join19(TEMPLATES_DIR4, type.endsWith(".yml") || type.endsWith(".md") ? type : `${type}.md`);
1980
+ if (!existsSync11(templatePath)) {
1715
1981
  console.error(`\u6A21\u677F\u6587\u4EF6\u4E0D\u5B58\u5728: ${templatePath}`);
1716
1982
  process.exit(1);
1717
1983
  }
1718
- let content = readFileSync13(templatePath, "utf-8");
1984
+ let content = readFileSync14(templatePath, "utf-8");
1719
1985
  const name = options.name;
1720
1986
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1721
1987
  content = content.replace(/\{\{name\}\}/g, name);
@@ -1725,24 +1991,24 @@ function templateHandler(type, options) {
1725
1991
  let targetDir;
1726
1992
  let filename;
1727
1993
  if (options.dir) {
1728
- targetDir = options.dir.startsWith("/") ? options.dir : join18(process.cwd(), options.dir);
1994
+ targetDir = options.dir.startsWith("/") ? options.dir : join19(process.cwd(), options.dir);
1729
1995
  filename = type;
1730
1996
  } else if (type === "project.yml" || type === "state.md") {
1731
- targetDir = join18(process.cwd(), "specwf");
1997
+ targetDir = join19(process.cwd(), "specwf");
1732
1998
  filename = type;
1733
1999
  } else {
1734
- targetDir = join18(process.cwd(), "specwf", "changes", name);
2000
+ targetDir = join19(process.cwd(), "specwf", "changes", name);
1735
2001
  filename = type.endsWith(".yml") ? type : `${type}.md`;
1736
2002
  }
1737
2003
  mkdirSync6(targetDir, { recursive: true });
1738
- const fullPath = join18(targetDir, filename);
2004
+ const fullPath = join19(targetDir, filename);
1739
2005
  writeFileSync8(fullPath, content, "utf-8");
1740
2006
  console.log(`\u2713 \u521B\u5EFA ${fullPath}`);
1741
2007
  }
1742
2008
 
1743
2009
  // 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";
2010
+ import { join as join20, dirname as dirname5 } from "path";
2011
+ import { existsSync as existsSync12, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
1746
2012
  import { fileURLToPath as fileURLToPath5 } from "url";
1747
2013
  var __dirname5 = dirname5(fileURLToPath5(import.meta.url));
1748
2014
  function register10(program2) {
@@ -1753,23 +2019,23 @@ function register10(program2) {
1753
2019
  });
1754
2020
  }
1755
2021
  function newChange(name, options) {
1756
- const specwfDir = join19(process.cwd(), options.dir);
2022
+ const specwfDir = join20(process.cwd(), options.dir);
1757
2023
  const changeDir = createAdhocChangeDir(specwfDir, name);
1758
2024
  console.log(`\u2713 \u521B\u5EFA change \u76EE\u5F55: changes/${name}/`);
1759
- const templatesDir = join19(__dirname5, "templates", "artifacts");
2025
+ const templatesDir = join20(__dirname5, "templates", "artifacts");
1760
2026
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1761
2027
  for (const file of ["proposal.md", "design.md", "tasks.md"]) {
1762
- const tplPath = join19(templatesDir, file);
2028
+ const tplPath = join20(templatesDir, file);
1763
2029
  let content;
1764
- if (existsSync11(tplPath)) {
1765
- content = readFileSync14(tplPath, "utf-8");
2030
+ if (existsSync12(tplPath)) {
2031
+ content = readFileSync15(tplPath, "utf-8");
1766
2032
  content = content.replace(/\{\{name\}\}/g, name);
1767
2033
  content = content.replace(/\{\{date\}\}/g, date);
1768
2034
  } else {
1769
2035
  content = `# ${file.replace(".md", "")}: ${name}
1770
2036
  `;
1771
2037
  }
1772
- writeFileSync9(join19(changeDir, file), content, "utf-8");
2038
+ writeFileSync9(join20(changeDir, file), content, "utf-8");
1773
2039
  }
1774
2040
  console.log("\u2713 \u521B\u5EFA\u6A21\u677F\u6587\u4EF6: proposal.md, design.md, tasks.md");
1775
2041
  try {
@@ -1,10 +1,23 @@
1
1
  # 初始化项目
2
2
 
3
- 执行 `specwf init` 创建 specwf 目录结构。
3
+ 运行 `specwf init` 创建 specwf/ 目录结构 + platform.yml + state.md。
4
4
 
5
5
  ## 存量项目
6
6
 
7
- 如果已有代码,执行 `specwf init --brownfield`,CLI 自动完成 codebase mapping + spec bootstrap。
7
+ 已有代码的项目使用 brownfield 模式:
8
+
9
+ ```bash
10
+ specwf init --brownfield
11
+ ```
12
+
13
+ CLI 创建目录结构后,**agent 需使用 specwf 子代理并行执行以下任务**:
14
+
15
+ 1. **Codebase Mapping**(`specwf-researcher`)— 分析技术栈、模块结构、依赖关系
16
+ - 产出: `specwf/research/STACK.md` + `specwf/research/ARCHITECTURE.md`
17
+ 2. **Spec Bootstrap**(`specwf-researcher`)— 从核心模块提取行为契约
18
+ - 产出: `specwf/specs/<domain>/spec.md`(标记 BOOTSTRAPPED)
19
+
20
+ 使用 task 工具 fan-out 两个 specwf-researcher 并行执行。
8
21
 
9
22
  ## 上下文
10
23
 
@@ -13,8 +26,12 @@ specwf context init
13
26
  specwf state
14
27
  ```
15
28
 
29
+ 可参考 @specwf/conventions/coding.md。
30
+
16
31
  ## 下一步
17
32
 
33
+ 完成后:
34
+
18
35
  ```bash
19
36
  specwf continue
20
37
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specwf",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "规格驱动开发工作流 — spec-driven development workflow for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {