specwf 0.2.0 → 0.2.1

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 (2) hide show
  1. package/dist/cli.js +340 -79
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -882,7 +882,128 @@ function findSpecwfDir() {
882
882
  }
883
883
 
884
884
  // src/commands/specwf-state.ts
885
+ import { join as join12 } from "path";
886
+
887
+ // src/core/state-validator.ts
888
+ import { existsSync as existsSync6, readFileSync as readFileSync9, readdirSync as readdirSync3 } from "fs";
885
889
  import { join as join11 } from "path";
890
+ var EXIT_CRITERIA = [
891
+ // milestone/active → 必须有 requirements.md
892
+ {
893
+ type: "milestone",
894
+ step: "active",
895
+ checks: [
896
+ { path: "requirements.md", description: "requirements.md \u4E0D\u5B58\u5728" }
897
+ ]
898
+ },
899
+ // project/requirements-defined → 必须有完整的 requirements.md
900
+ {
901
+ type: "project",
902
+ step: "requirements-defined",
903
+ checks: [
904
+ { path: "requirements.md", description: "requirements.md \u5185\u5BB9\u4E3A\u6A21\u677F\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5" }
905
+ ]
906
+ },
907
+ // project/researched → 必须有调研产出
908
+ {
909
+ type: "project",
910
+ step: "researched",
911
+ checks: [
912
+ { path: "research/summary.md", description: "research/summary.md \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u5B8C\u6210\u8C03\u7814" }
913
+ ]
914
+ },
915
+ // phase/discuss → 必须有 context.md
916
+ {
917
+ type: "phase",
918
+ step: "discuss",
919
+ checks: [
920
+ { path: "roadmap.md", description: "roadmap.md \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u62C6\u5206\u8DEF\u7EBF\u56FE" }
921
+ ]
922
+ },
923
+ // phase/research → 必须有 research.md
924
+ {
925
+ type: "phase",
926
+ step: "research",
927
+ checks: [
928
+ { path: "roadmap.md", description: "roadmap.md \u4E0D\u5B58\u5728" }
929
+ ]
930
+ },
931
+ // adhoc/proposal → proposal.md 不能是模板
932
+ {
933
+ type: "adhoc",
934
+ step: "proposal",
935
+ checks: [
936
+ { path: "changes/", description: "change \u7684 proposal.md \u4E3A\u6A21\u677F\u7A7A\u58F3\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5" }
937
+ ]
938
+ },
939
+ // change/planning → design.md + tasks.md 不能是模板
940
+ {
941
+ type: "change",
942
+ step: "planning",
943
+ checks: [
944
+ { path: "changes/", description: "design.md \u6216 tasks.md \u4E3A\u6A21\u677F\u7A7A\u58F3\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5" }
945
+ ]
946
+ }
947
+ ];
948
+ function isTemplateFile(filePath) {
949
+ try {
950
+ const content = readFileSync9(filePath, "utf-8");
951
+ const placeholders = content.match(/\{\{[a-zA-Z_-]+\}\}/g);
952
+ return (placeholders?.length ?? 0) > 3;
953
+ } catch {
954
+ return false;
955
+ }
956
+ }
957
+ function findChangeDir(specwfDir) {
958
+ const changesDir = join11(specwfDir, "changes");
959
+ if (!existsSync6(changesDir)) return [];
960
+ try {
961
+ return readdirSync3(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
962
+ } catch {
963
+ return [];
964
+ }
965
+ }
966
+ function checkExitCondition(specwfDir, check) {
967
+ const fullPath = join11(specwfDir, check.path);
968
+ if (check.path.endsWith("/") || check.description.includes("\u7684 ")) {
969
+ const changes = findChangeDir(specwfDir);
970
+ for (const change of changes) {
971
+ for (const doc of ["proposal.md", "design.md", "tasks.md"]) {
972
+ const docPath = join11(specwfDir, "changes", change, doc);
973
+ if (existsSync6(docPath) && isTemplateFile(docPath)) {
974
+ return `changes/${change}/${doc} \u4ECD\u4E3A\u6A21\u677F\u7A7A\u58F3\uFF0C\u8BF7\u586B\u5199\u540E\u91CD\u8BD5`;
975
+ }
976
+ }
977
+ }
978
+ return null;
979
+ }
980
+ if (!existsSync6(fullPath)) {
981
+ return check.description;
982
+ }
983
+ if (isTemplateFile(fullPath)) {
984
+ return check.description;
985
+ }
986
+ return null;
987
+ }
988
+ function validateStepAdvance(contextType, contextStep, cwd) {
989
+ const specwfDir = join11(cwd, "specwf");
990
+ const criteria = EXIT_CRITERIA.find(
991
+ (c) => c.type === contextType && c.step === contextStep
992
+ );
993
+ if (!criteria) {
994
+ return { valid: true, errors: [] };
995
+ }
996
+ const errors = [];
997
+ for (const check of criteria.checks) {
998
+ const error = checkExitCondition(specwfDir, check);
999
+ if (error) {
1000
+ errors.push(error);
1001
+ }
1002
+ }
1003
+ return { valid: errors.length === 0, errors };
1004
+ }
1005
+
1006
+ // src/commands/specwf-state.ts
886
1007
  function register4(program2) {
887
1008
  const cmd = program2.command("state").description("\u67E5\u770B/\u4FEE\u6539\u5F53\u524D\u72B6\u6001");
888
1009
  cmd.command("show").description("\u67E5\u770B\u5F53\u524D\u72B6\u6001").action(showState);
@@ -892,7 +1013,7 @@ function register4(program2) {
892
1013
  cmd.action(showState);
893
1014
  }
894
1015
  function findSpecwfDir2() {
895
- return join11(process.cwd(), "specwf");
1016
+ return join12(process.cwd(), "specwf");
896
1017
  }
897
1018
  function showState() {
898
1019
  const specwfDir = findSpecwfDir2();
@@ -915,13 +1036,13 @@ function setMilestone(id) {
915
1036
  updateState(specwfDir, (state) => {
916
1037
  state.project.current_milestone = id;
917
1038
  state.project.current_phase = null;
918
- state.active_context.type = "phase";
1039
+ state.active_context.type = "milestone";
919
1040
  state.active_context.ref = `milestones/${id}`;
920
- state.active_context.step = "discuss";
921
- state.project.status = "phase-discuss";
1041
+ state.active_context.step = "active";
1042
+ state.project.status = "milestone-active";
922
1043
  });
923
- console.log(`\u2713 \u5207\u6362\u5230 milestone: ${id}\uFF08\u72B6\u6001: phase-discuss\uFF09`);
924
- console.log("\u2192 \u4E0B\u4E00\u6B65: /specwf:discuss");
1044
+ console.log(`\u2713 \u5207\u6362\u5230 milestone: ${id}\uFF08\u72B6\u6001: milestone-active\uFF09`);
1045
+ console.log("\u2192 \u4E0B\u4E00\u6B65: \u5B9A\u4E49\u91CC\u7A0B\u7891\u9700\u6C42: /specwf:grill");
925
1046
  }
926
1047
  function setPhase(id) {
927
1048
  const specwfDir = findSpecwfDir2();
@@ -937,18 +1058,51 @@ function setPhase(id) {
937
1058
  }
938
1059
  function setStep(step) {
939
1060
  const specwfDir = findSpecwfDir2();
940
- updateState(specwfDir, (state) => {
941
- state.active_context.step = step;
1061
+ const state = loadState(specwfDir);
1062
+ const ctx = state.active_context;
1063
+ let currentStatus;
1064
+ switch (ctx.type) {
1065
+ case "project":
1066
+ currentStatus = state.project.status;
1067
+ break;
1068
+ case "milestone":
1069
+ currentStatus = "milestone-active";
1070
+ break;
1071
+ case "phase":
1072
+ currentStatus = `phase-${ctx.step}`;
1073
+ break;
1074
+ case "change":
1075
+ currentStatus = `change-${ctx.step}`;
1076
+ break;
1077
+ case "adhoc":
1078
+ currentStatus = `adhoc-${ctx.step}`;
1079
+ break;
1080
+ default:
1081
+ currentStatus = state.project.status;
1082
+ }
1083
+ const result = validateStepAdvance(ctx.type, ctx.step, process.cwd());
1084
+ if (!result.valid) {
1085
+ console.log("\u2500".repeat(50));
1086
+ console.log("\u274C \u524D\u7F6E\u6761\u4EF6\u672A\u6EE1\u8DB3\uFF0C\u65E0\u6CD5\u63A8\u8FDB:");
1087
+ for (const err of result.errors) {
1088
+ console.log(` \u2022 ${err}`);
1089
+ }
1090
+ ;
1091
+ console.log("\u2500".repeat(50));
1092
+ return;
1093
+ }
1094
+ updateState(specwfDir, (state2) => {
1095
+ state2.active_context.step = step;
942
1096
  });
943
1097
  console.log(`\u2713 \u5F53\u524D\u6B65\u9AA4: ${step}`);
944
1098
  }
945
1099
 
946
1100
  // src/commands/specwf-context.ts
947
- import { join as join13 } from "path";
1101
+ import { join as join14 } from "path";
948
1102
 
949
1103
  // 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";
1104
+ import { join as join13 } from "path";
1105
+ import { readdirSync as readdirSync4, existsSync as existsSync7, statSync as statSync2 } from "fs";
952
1106
  var PROJECT_STEPS = ["init", "grill", "research", "roadmap"];
953
1107
  var PHASE_STEPS = ["discuss", "research-phase", "split"];
954
1108
  var CHANGE_STEPS = ["plan", "apply", "review", "verify", "archive"];
@@ -973,7 +1127,7 @@ function generateContext(specwfDir, step) {
973
1127
  requirements: []
974
1128
  };
975
1129
  result.conventions = getAllConventions(specwfDir);
976
- if (existsSync6(join12(specwfDir, "requirements.md"))) {
1130
+ if (existsSync7(join13(specwfDir, "requirements.md"))) {
977
1131
  result.requirements.push({ path: "requirements.md", description: "\u9700\u6C42\u89C4\u683C" });
978
1132
  }
979
1133
  if (isProjectStep(step)) {
@@ -987,7 +1141,7 @@ function generateContext(specwfDir, step) {
987
1141
  return result;
988
1142
  }
989
1143
  function getAllSpecs(specwfDir) {
990
- const specsDir = join12(specwfDir, "specs");
1144
+ const specsDir = join13(specwfDir, "specs");
991
1145
  return listSpecFiles(specsDir, "specs");
992
1146
  }
993
1147
  function getRelatedSpecs(specwfDir, state) {
@@ -1002,34 +1156,34 @@ function getRelatedSpecs(specwfDir, state) {
1002
1156
  return related.length > 0 ? related : allSpecs;
1003
1157
  }
1004
1158
  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" }));
1159
+ const convDir = join13(specwfDir, "conventions");
1160
+ if (!existsSync7(convDir)) return [];
1161
+ return readdirSync4(convDir).filter((f) => f.endsWith(".md")).map((f) => ({ path: `conventions/${f}`, description: "\u9879\u76EE\u7EA6\u5B9A" }));
1008
1162
  }
1009
1163
  function getChangeArtifacts(specwfDir, state) {
1010
1164
  const ref = state.active_context.ref;
1011
1165
  if (!ref) return [];
1012
- const changeDir = join12(specwfDir, ref);
1013
- if (!existsSync6(changeDir)) return [];
1166
+ const changeDir = join13(specwfDir, ref);
1167
+ if (!existsSync7(changeDir)) return [];
1014
1168
  const artifacts = [];
1015
1169
  for (const file of ["proposal.md", "design.md", "tasks.md", ".specwf.yaml"]) {
1016
- const fullPath = join12(changeDir, file);
1017
- if (existsSync6(fullPath)) {
1170
+ const fullPath = join13(changeDir, file);
1171
+ if (existsSync7(fullPath)) {
1018
1172
  artifacts.push({ path: `${ref}/${file}`, description: "change \u4EA7\u7269" });
1019
1173
  }
1020
1174
  }
1021
- const specsDir = join12(changeDir, "specs");
1022
- if (existsSync6(specsDir)) {
1175
+ const specsDir = join13(changeDir, "specs");
1176
+ if (existsSync7(specsDir)) {
1023
1177
  const deltaSpecs = listSpecFiles(specsDir, `${ref}/specs`);
1024
1178
  artifacts.push(...deltaSpecs);
1025
1179
  }
1026
1180
  return artifacts;
1027
1181
  }
1028
1182
  function listSpecFiles(dir, prefix) {
1029
- if (!existsSync6(dir)) return [];
1183
+ if (!existsSync7(dir)) return [];
1030
1184
  const results = [];
1031
- for (const entry of readdirSync3(dir)) {
1032
- const fullPath = join12(dir, entry);
1185
+ for (const entry of readdirSync4(dir)) {
1186
+ const fullPath = join13(dir, entry);
1033
1187
  const stat = statSync2(fullPath);
1034
1188
  if (stat.isDirectory()) {
1035
1189
  results.push(...listSpecFiles(fullPath, `${prefix}/${entry}`));
@@ -1084,7 +1238,7 @@ function register5(program2) {
1084
1238
  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
1239
  }
1086
1240
  function contextHandler(step, options) {
1087
- const specwfDir = join13(process.cwd(), "specwf");
1241
+ const specwfDir = join14(process.cwd(), "specwf");
1088
1242
  const result = generateContext(specwfDir, step);
1089
1243
  if (options.json) {
1090
1244
  console.log(JSON.stringify(result, null, 2));
@@ -1094,7 +1248,7 @@ function contextHandler(step, options) {
1094
1248
  }
1095
1249
 
1096
1250
  // src/commands/specwf-continue.ts
1097
- import { join as join14 } from "path";
1251
+ import { join as join15 } from "path";
1098
1252
 
1099
1253
  // src/types/state.ts
1100
1254
  var STATE_TRANSITIONS = [
@@ -1117,6 +1271,8 @@ var STATE_TRANSITIONS = [
1117
1271
  { from: "change-verifying", command: "replan", to: "change-planning", slashCommand: "/specwf:plan", subagent: true },
1118
1272
  { from: "change-verifying", command: "reapply", to: "change-applying", slashCommand: "/specwf:apply", subagent: true },
1119
1273
  { from: "change-reviewing", command: "fix", to: "change-applying", slashCommand: "/specwf:apply", subagent: true },
1274
+ // Milestone 层(新里程碑 = 项目流程 - init)
1275
+ { from: "milestone-active", command: "grill", to: "requirements-defined", slashCommand: "/specwf:grill" },
1120
1276
  // Ship
1121
1277
  { from: "change-archived", command: "ship-phase", to: "phase-shipped", slashCommand: "/specwf:ship" },
1122
1278
  { from: "phase-shipped", command: "next-phase", to: "phase-discuss", slashCommand: "/specwf:discuss" },
@@ -1144,9 +1300,10 @@ function determineChangeNextStep(specwfDir, changeName) {
1144
1300
  }
1145
1301
  const adhoc = state.adhoc.find((c) => c.name === changeName);
1146
1302
  if (adhoc) {
1303
+ const prefix = adhoc.status === "proposal" ? "adhoc" : "change";
1147
1304
  return determineFromChangeStatus(
1148
1305
  changeName,
1149
- `adhoc-${adhoc.status}`,
1306
+ `${prefix}-${adhoc.status}`,
1150
1307
  "adhoc"
1151
1308
  );
1152
1309
  }
@@ -1154,6 +1311,95 @@ function determineChangeNextStep(specwfDir, changeName) {
1154
1311
  error: `change \u4E0D\u5B58\u5728: ${changeName}\u3002\u53EF\u7528: ${listAvailableChanges(state)}`
1155
1312
  };
1156
1313
  }
1314
+ var STEP_INFO = {
1315
+ grill: {
1316
+ command: "grill",
1317
+ description: "\u901A\u8FC7\u9010\u6761\u63D0\u95EE\u6536\u96C6\u9700\u6C42\uFF0C\u4EA7\u51FA requirements.md",
1318
+ artifacts: ["specwf/requirements.md"],
1319
+ fileRef: ".omp/commands/specwf-grill.md"
1320
+ },
1321
+ research: {
1322
+ command: "research",
1323
+ description: "\u5E76\u884C\u8C03\u7814\u6280\u672F\u65B9\u5411\u548C\u67B6\u6784\u65B9\u6848",
1324
+ artifacts: ["specwf/research/stack.md", "specwf/research/architecture.md", "specwf/research/pitfalls.md", "specwf/research/summary.md"],
1325
+ fileRef: ".omp/commands/specwf-research.md"
1326
+ },
1327
+ "research-done": {
1328
+ command: "research-done",
1329
+ description: "\u6807\u8BB0\u8C03\u7814\u5B8C\u6210\uFF0C\u8FDB\u5165\u8DEF\u7EBF\u56FE\u62C6\u5206",
1330
+ artifacts: [],
1331
+ fileRef: ""
1332
+ },
1333
+ roadmap: {
1334
+ command: "roadmap",
1335
+ description: "\u5C06\u9879\u76EE\u62C6\u5206\u4E3A Milestone \xD7 Phase",
1336
+ artifacts: ["specwf/roadmap.md"],
1337
+ fileRef: ".omp/commands/specwf-roadmap.md"
1338
+ },
1339
+ discuss: {
1340
+ command: "discuss",
1341
+ description: "Phase \u8BA8\u8BBA\uFF0C\u6355\u83B7\u5B9E\u73B0\u51B3\u7B56",
1342
+ artifacts: ["milestones/<ms>/phases/<ph>/context.md"],
1343
+ fileRef: ".omp/commands/specwf-discuss.md"
1344
+ },
1345
+ "research-phase": {
1346
+ command: "research-phase",
1347
+ description: "\u5BF9\u5F53\u524D phase \u8FDB\u884C\u6280\u672F\u8C03\u7814",
1348
+ artifacts: ["milestones/<ms>/phases/<ph>/research.md"],
1349
+ fileRef: ".omp/commands/specwf-research-phase.md"
1350
+ },
1351
+ split: {
1352
+ command: "split",
1353
+ description: "\u5C06 phase \u62C6\u5206\u4E3A\u591A\u4E2A change\uFF0C\u786E\u5B9A\u4F9D\u8D56\u56FE",
1354
+ artifacts: ["specwf/roadmap.md\uFF08\u66F4\u65B0\uFF09"],
1355
+ fileRef: ".omp/commands/specwf-split.md"
1356
+ },
1357
+ plan: {
1358
+ command: "plan",
1359
+ description: "Change \u8BBE\u8BA1\uFF1A\u8BBE\u8BA1\u6280\u672F\u65B9\u6848\u3001\u62C6\u5206\u4EFB\u52A1\u3001\u9884\u5199 delta-specs",
1360
+ artifacts: ["design.md", "tasks.md", "specs/<domain>/spec.md"],
1361
+ fileRef: ".omp/commands/specwf-plan.md"
1362
+ },
1363
+ apply: {
1364
+ command: "apply",
1365
+ description: "\u6309 tasks.md \u5B9E\u73B0\u4EE3\u7801\uFF0Ctype:behavior \u8D70 RED\u2192GREEN\u2192REFACTOR",
1366
+ artifacts: ["\u4EE3\u7801\u53D8\u66F4", "\u6D4B\u8BD5"],
1367
+ fileRef: ".omp/commands/specwf-apply.md"
1368
+ },
1369
+ review: {
1370
+ command: "review",
1371
+ description: "\u4E09\u91CD\u5BA1\u67E5\uFF1A\u89C4\u683C\u5BA1\u67E5 + \u8D28\u91CF\u5BA1\u67E5 + \u76EE\u6807\u5BA1\u67E5",
1372
+ artifacts: ["REVIEW.md"],
1373
+ fileRef: ".omp/commands/specwf-review.md"
1374
+ },
1375
+ verify: {
1376
+ command: "verify",
1377
+ description: "\u8FD0\u884C\u6D4B\u8BD5\uFF0C\u8BCA\u65AD\u6839\u56E0\uFF0C\u8DEF\u7531\u56DE\u73AF",
1378
+ artifacts: ["VERIFICATION.md"],
1379
+ fileRef: ".omp/commands/specwf-verify.md"
1380
+ },
1381
+ archive: {
1382
+ command: "archive",
1383
+ description: "Delta-spec \u5408\u5E76 + \u4EE3\u7801\u8BA4\u77E5\u56DE\u704C + \u76EE\u5F55\u5F52\u6863",
1384
+ artifacts: ["archive/<change-id>/"],
1385
+ fileRef: ".omp/commands/specwf-archive.md"
1386
+ },
1387
+ "ship-phase": {
1388
+ command: "ship-phase",
1389
+ description: "\u521B\u5EFA PR + \u66F4\u65B0 state.md",
1390
+ artifacts: ["GitHub PR", "state.md \u66F4\u65B0"],
1391
+ fileRef: ".omp/commands/specwf-ship.md"
1392
+ },
1393
+ "ship-milestone": {
1394
+ command: "ship-milestone",
1395
+ description: "\u53D1\u5E03 release tag + \u66F4\u65B0\u7248\u672C\u53F7",
1396
+ artifacts: ["git tag", "RELEASE.md", "npm publish"],
1397
+ fileRef: ".omp/commands/specwf-ship.md"
1398
+ }
1399
+ };
1400
+ function getStepInfo(command) {
1401
+ return STEP_INFO[command];
1402
+ }
1157
1403
  function determineFromChangeStatus(name, statusKey, type) {
1158
1404
  const available = getNextSteps(statusKey);
1159
1405
  const availableSteps = available.map((t) => ({
@@ -1169,7 +1415,8 @@ function determineFromChangeStatus(name, statusKey, type) {
1169
1415
  slashCommand: first?.slashCommand || null,
1170
1416
  needsSubagent: first?.subagent ?? false,
1171
1417
  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
1418
+ hint: available.length === 0 ? "\u8BE5 change \u5DF2\u6CA1\u6709\u53EF\u7528\u4E0B\u4E00\u6B65\u3002\u521B\u5EFA\u65B0 change \u7EE7\u7EED\u3002" : null,
1419
+ nextStepInfo: first ? getStepInfo(first.command) : void 0
1173
1420
  };
1174
1421
  }
1175
1422
  function listAvailableChanges(state) {
@@ -1197,7 +1444,8 @@ function determineFromState(state) {
1197
1444
  slashCommand: first?.slashCommand || null,
1198
1445
  needsSubagent: first?.subagent ?? false,
1199
1446
  availableSteps,
1200
- hint
1447
+ hint,
1448
+ nextStepInfo: first ? getStepInfo(first.command) : void 0
1201
1449
  };
1202
1450
  }
1203
1451
  function resolveStatus(state) {
@@ -1263,14 +1511,27 @@ function formatContinueResult(result) {
1263
1511
  console.log(`\u5F53\u524D\u4F4D\u7F6E: ${result.context}`);
1264
1512
  console.log(`\u5F53\u524D\u6B65\u9AA4: ${result.currentStep}`);
1265
1513
  if (result.nextCommand) {
1514
+ const info = result.nextStepInfo;
1266
1515
  console.log("");
1267
- console.log(`\u2192 \u63A8\u8350\u4E0B\u4E00\u6B65: ${result.nextCommand}`);
1516
+ console.log(`\u2192 \u4E0B\u4E00\u6B65: ${result.nextCommand}`);
1268
1517
  if (result.slashCommand) {
1269
1518
  console.log(` Slash \u547D\u4EE4: ${result.slashCommand}`);
1270
1519
  }
1271
1520
  if (result.needsSubagent) {
1272
1521
  console.log(` \u9700\u8981\u5B50\u4EE3\u7406: \u662F`);
1273
1522
  }
1523
+ if (info) {
1524
+ console.log(` \u63CF\u8FF0: ${info.description}`);
1525
+ if (info.artifacts.length > 0) {
1526
+ console.log(` \u4EA7\u51FA\u7269:`);
1527
+ for (const a of info.artifacts) {
1528
+ console.log(` - ${a}`);
1529
+ }
1530
+ }
1531
+ if (info.fileRef) {
1532
+ console.log(` \u53C2\u8003: ${info.fileRef}`);
1533
+ }
1534
+ }
1274
1535
  } else {
1275
1536
  console.log("");
1276
1537
  console.log("\u2192 \u5F53\u524D\u65E0\u53EF\u7528\u4E0B\u4E00\u6B65");
@@ -1281,12 +1542,12 @@ function formatContinueResult(result) {
1281
1542
  console.log("\u2500".repeat(50));
1282
1543
  }
1283
1544
  function continueHandler() {
1284
- const specwfDir = join14(process.cwd(), "specwf");
1545
+ const specwfDir = join15(process.cwd(), "specwf");
1285
1546
  const result = determineNextStep(specwfDir);
1286
1547
  formatContinueResult(result);
1287
1548
  }
1288
1549
  function continueChangeHandler(name) {
1289
- const specwfDir = join14(process.cwd(), "specwf");
1550
+ const specwfDir = join15(process.cwd(), "specwf");
1290
1551
  const result = determineChangeNextStep(specwfDir, name);
1291
1552
  if ("error" in result) {
1292
1553
  console.log("\u2500".repeat(50));
@@ -1298,12 +1559,12 @@ function continueChangeHandler(name) {
1298
1559
  }
1299
1560
 
1300
1561
  // 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";
1562
+ import { join as join17 } from "path";
1563
+ import { existsSync as existsSync10, readdirSync as readdirSync6, mkdirSync as mkdirSync5, copyFileSync } from "fs";
1303
1564
 
1304
1565
  // src/core/delta-merge.ts
1305
1566
  import { createHash } from "crypto";
1306
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
1567
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
1307
1568
 
1308
1569
  // src/parser/heading-tree.ts
1309
1570
  function parseHeadings(markdown) {
@@ -1438,8 +1699,8 @@ function renderNodes(nodes, lines) {
1438
1699
  }
1439
1700
  }
1440
1701
  function mergeAndWrite(liveSpecPath, deltaSpecPath, baseFingerprint) {
1441
- const baseSpec = readFileSync10(liveSpecPath, "utf-8");
1442
- const deltaSpec = readFileSync10(deltaSpecPath, "utf-8");
1702
+ const baseSpec = readFileSync11(liveSpecPath, "utf-8");
1703
+ const deltaSpec = readFileSync11(deltaSpecPath, "utf-8");
1443
1704
  const result = mergeDeltaSpec(baseSpec, deltaSpec, baseFingerprint);
1444
1705
  if (result.type === "ok") {
1445
1706
  writeFileSync6(liveSpecPath, result.merged, "utf-8");
@@ -1449,8 +1710,8 @@ function mergeAndWrite(liveSpecPath, deltaSpecPath, baseFingerprint) {
1449
1710
 
1450
1711
  // src/core/code-extract.ts
1451
1712
  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";
1713
+ import { existsSync as existsSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync5 } from "fs";
1714
+ import { join as join16 } from "path";
1454
1715
  function extractFromGitDiff(repoDir, changeDir) {
1455
1716
  const diff = getGitDiff(repoDir);
1456
1717
  if (diff === null) {
@@ -1486,10 +1747,10 @@ function getGitDiff(repoDir) {
1486
1747
  }
1487
1748
  }
1488
1749
  function detectDomains(changeDir) {
1489
- const specsDir = join15(changeDir, "specs");
1490
- if (!existsSync8(specsDir)) return ["general"];
1750
+ const specsDir = join16(changeDir, "specs");
1751
+ if (!existsSync9(specsDir)) return ["general"];
1491
1752
  try {
1492
- return readdirSync4(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1753
+ return readdirSync5(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1493
1754
  } catch {
1494
1755
  return ["general"];
1495
1756
  }
@@ -1527,11 +1788,11 @@ function extractConstraints(diff, _domain) {
1527
1788
  return constraints;
1528
1789
  }
1529
1790
  function writeExtractionToSpec(specsDir, extraction) {
1530
- const domainDir = join15(specsDir, extraction.domain);
1531
- const specPath = join15(domainDir, "spec.md");
1791
+ const domainDir = join16(specsDir, extraction.domain);
1792
+ const specPath = join16(domainDir, "spec.md");
1532
1793
  let existing = "";
1533
- if (existsSync8(specPath)) {
1534
- existing = readFileSync11(specPath, "utf-8");
1794
+ if (existsSync9(specPath)) {
1795
+ existing = readFileSync12(specPath, "utf-8");
1535
1796
  }
1536
1797
  const section = generateAutoExtractedSection(extraction);
1537
1798
  const updated = existing.trim() ? `${existing.trim()}
@@ -1570,14 +1831,14 @@ function register7(program2) {
1570
1831
  program2.command("archive <change>").description("\u5F52\u6863 change\uFF08delta \u5408\u5E76 + \u4EE3\u7801\u56DE\u704C\uFF09").action(archiveHandler);
1571
1832
  }
1572
1833
  function archiveHandler(changePath) {
1573
- const specwfDir = join16(process.cwd(), "specwf");
1574
- const fullChangePath = join16(process.cwd(), changePath);
1575
- if (!existsSync9(fullChangePath)) {
1834
+ const specwfDir = join17(process.cwd(), "specwf");
1835
+ const fullChangePath = join17(process.cwd(), changePath);
1836
+ if (!existsSync10(fullChangePath)) {
1576
1837
  console.error(`\u9519\u8BEF: change \u76EE\u5F55\u4E0D\u5B58\u5728: ${changePath}`);
1577
1838
  process.exit(1);
1578
1839
  }
1579
- const specsDir = join16(fullChangePath, "specs");
1580
- if (existsSync9(specsDir)) {
1840
+ const specsDir = join17(fullChangePath, "specs");
1841
+ if (existsSync10(specsDir)) {
1581
1842
  mergeDeltaSpecs(specsDir, specwfDir);
1582
1843
  console.log("\u2713 delta-specs \u5408\u5E76\u5B8C\u6210");
1583
1844
  }
@@ -1585,7 +1846,7 @@ function archiveHandler(changePath) {
1585
1846
  const extractResult = extractFromGitDiff(repoDir, fullChangePath);
1586
1847
  if (extractResult.available && extractResult.extractions.length > 0) {
1587
1848
  for (const extraction of extractResult.extractions) {
1588
- writeExtractionToSpec(join16(specwfDir, "specs"), extraction);
1849
+ writeExtractionToSpec(join17(specwfDir, "specs"), extraction);
1589
1850
  }
1590
1851
  if (extractResult.extractions.length > 0) {
1591
1852
  console.log(`\u2713 \u4EE3\u7801\u8BA4\u77E5\u63D0\u53D6\u5B8C\u6210 (${extractResult.extractions.length} \u4E2A\u57DF)`);
@@ -1612,14 +1873,14 @@ function archiveHandler(changePath) {
1612
1873
  console.log("\u5F52\u6863\u5B8C\u6210\u3002");
1613
1874
  }
1614
1875
  function mergeDeltaSpecs(deltaDir, specwfDir) {
1615
- const entries = readdirSync5(deltaDir, { withFileTypes: true });
1876
+ const entries = readdirSync6(deltaDir, { withFileTypes: true });
1616
1877
  for (const entry of entries) {
1617
1878
  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 });
1879
+ const deltaSpecPath = join17(deltaDir, entry.name, "spec.md");
1880
+ const liveSpecPath = join17(specwfDir, "specs", entry.name, "spec.md");
1881
+ if (!existsSync10(deltaSpecPath)) continue;
1882
+ if (!existsSync10(liveSpecPath)) {
1883
+ mkdirSync5(join17(specwfDir, "specs", entry.name), { recursive: true });
1623
1884
  copyFileSync(deltaSpecPath, liveSpecPath);
1624
1885
  continue;
1625
1886
  }
@@ -1634,12 +1895,12 @@ function mergeDeltaSpecs(deltaDir, specwfDir) {
1634
1895
  }
1635
1896
 
1636
1897
  // src/commands/specwf-list.ts
1637
- import { join as join17 } from "path";
1898
+ import { join as join18 } from "path";
1638
1899
  function register8(program2) {
1639
1900
  program2.command("list").description("\u5217\u51FA milestones/phases/changes").option("--all", "\u5305\u542B\u5F52\u6863").action(listHandler);
1640
1901
  }
1641
1902
  function listHandler(options) {
1642
- const specwfDir = join17(process.cwd(), "specwf");
1903
+ const specwfDir = join18(process.cwd(), "specwf");
1643
1904
  let hasItems = false;
1644
1905
  const milestones = listMilestones(specwfDir);
1645
1906
  if (milestones.length > 0) {
@@ -1683,11 +1944,11 @@ function listHandler(options) {
1683
1944
  }
1684
1945
 
1685
1946
  // 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";
1947
+ import { join as join19, dirname as dirname4 } from "path";
1948
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
1688
1949
  import { fileURLToPath as fileURLToPath4 } from "url";
1689
1950
  var __dirname4 = dirname4(fileURLToPath4(import.meta.url));
1690
- var TEMPLATES_DIR4 = join18(__dirname4, "templates", "artifacts");
1951
+ var TEMPLATES_DIR4 = join19(__dirname4, "templates", "artifacts");
1691
1952
  var TEMPLATE_TYPES = [
1692
1953
  "proposal",
1693
1954
  "design",
@@ -1710,12 +1971,12 @@ function templateHandler(type, options) {
1710
1971
  console.error(`\u672A\u77E5\u6A21\u677F\u7C7B\u578B: ${type}\u3002\u53EF\u9009: ${TEMPLATE_TYPES.join(", ")}`);
1711
1972
  process.exit(1);
1712
1973
  }
1713
- const templatePath = join18(TEMPLATES_DIR4, type.endsWith(".yml") || type.endsWith(".md") ? type : `${type}.md`);
1714
- if (!existsSync10(templatePath)) {
1974
+ const templatePath = join19(TEMPLATES_DIR4, type.endsWith(".yml") || type.endsWith(".md") ? type : `${type}.md`);
1975
+ if (!existsSync11(templatePath)) {
1715
1976
  console.error(`\u6A21\u677F\u6587\u4EF6\u4E0D\u5B58\u5728: ${templatePath}`);
1716
1977
  process.exit(1);
1717
1978
  }
1718
- let content = readFileSync13(templatePath, "utf-8");
1979
+ let content = readFileSync14(templatePath, "utf-8");
1719
1980
  const name = options.name;
1720
1981
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1721
1982
  content = content.replace(/\{\{name\}\}/g, name);
@@ -1725,24 +1986,24 @@ function templateHandler(type, options) {
1725
1986
  let targetDir;
1726
1987
  let filename;
1727
1988
  if (options.dir) {
1728
- targetDir = options.dir.startsWith("/") ? options.dir : join18(process.cwd(), options.dir);
1989
+ targetDir = options.dir.startsWith("/") ? options.dir : join19(process.cwd(), options.dir);
1729
1990
  filename = type;
1730
1991
  } else if (type === "project.yml" || type === "state.md") {
1731
- targetDir = join18(process.cwd(), "specwf");
1992
+ targetDir = join19(process.cwd(), "specwf");
1732
1993
  filename = type;
1733
1994
  } else {
1734
- targetDir = join18(process.cwd(), "specwf", "changes", name);
1995
+ targetDir = join19(process.cwd(), "specwf", "changes", name);
1735
1996
  filename = type.endsWith(".yml") ? type : `${type}.md`;
1736
1997
  }
1737
1998
  mkdirSync6(targetDir, { recursive: true });
1738
- const fullPath = join18(targetDir, filename);
1999
+ const fullPath = join19(targetDir, filename);
1739
2000
  writeFileSync8(fullPath, content, "utf-8");
1740
2001
  console.log(`\u2713 \u521B\u5EFA ${fullPath}`);
1741
2002
  }
1742
2003
 
1743
2004
  // 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";
2005
+ import { join as join20, dirname as dirname5 } from "path";
2006
+ import { existsSync as existsSync12, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
1746
2007
  import { fileURLToPath as fileURLToPath5 } from "url";
1747
2008
  var __dirname5 = dirname5(fileURLToPath5(import.meta.url));
1748
2009
  function register10(program2) {
@@ -1753,23 +2014,23 @@ function register10(program2) {
1753
2014
  });
1754
2015
  }
1755
2016
  function newChange(name, options) {
1756
- const specwfDir = join19(process.cwd(), options.dir);
2017
+ const specwfDir = join20(process.cwd(), options.dir);
1757
2018
  const changeDir = createAdhocChangeDir(specwfDir, name);
1758
2019
  console.log(`\u2713 \u521B\u5EFA change \u76EE\u5F55: changes/${name}/`);
1759
- const templatesDir = join19(__dirname5, "templates", "artifacts");
2020
+ const templatesDir = join20(__dirname5, "templates", "artifacts");
1760
2021
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1761
2022
  for (const file of ["proposal.md", "design.md", "tasks.md"]) {
1762
- const tplPath = join19(templatesDir, file);
2023
+ const tplPath = join20(templatesDir, file);
1763
2024
  let content;
1764
- if (existsSync11(tplPath)) {
1765
- content = readFileSync14(tplPath, "utf-8");
2025
+ if (existsSync12(tplPath)) {
2026
+ content = readFileSync15(tplPath, "utf-8");
1766
2027
  content = content.replace(/\{\{name\}\}/g, name);
1767
2028
  content = content.replace(/\{\{date\}\}/g, date);
1768
2029
  } else {
1769
2030
  content = `# ${file.replace(".md", "")}: ${name}
1770
2031
  `;
1771
2032
  }
1772
- writeFileSync9(join19(changeDir, file), content, "utf-8");
2033
+ writeFileSync9(join20(changeDir, file), content, "utf-8");
1773
2034
  }
1774
2035
  console.log("\u2713 \u521B\u5EFA\u6A21\u677F\u6587\u4EF6: proposal.md, design.md, tasks.md");
1775
2036
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specwf",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "规格驱动开发工作流 — spec-driven development workflow for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {