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 +412 -146
- package/dist/templates/commands/init.md +19 -2
- package/package.json +1 -1
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
|
|
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
|
|
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 =
|
|
494
|
+
var TEMPLATES_DIR = join5(__dirname, "templates", "commands");
|
|
552
495
|
function loadTemplate(step) {
|
|
553
|
-
return readFileSync6(
|
|
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
|
|
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 =
|
|
583
|
+
var TEMPLATES_DIR2 = join6(__dirname2, "templates", "agents");
|
|
641
584
|
function loadTemplate2(role) {
|
|
642
|
-
return readFileSync7(
|
|
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
|
|
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 =
|
|
699
|
+
var TEMPLATES_DIR3 = join7(__dirname3, "templates", "skills");
|
|
757
700
|
function loadTemplate3(step) {
|
|
758
|
-
return readFileSync8(
|
|
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
|
|
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 = "
|
|
1044
|
+
state.active_context.type = "milestone";
|
|
919
1045
|
state.active_context.ref = `milestones/${id}`;
|
|
920
|
-
state.active_context.step = "
|
|
921
|
-
state.project.status = "
|
|
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:
|
|
924
|
-
console.log("\u2192 \u4E0B\u4E00\u6B65: /specwf:
|
|
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
|
-
|
|
941
|
-
|
|
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
|
|
1106
|
+
import { join as join14 } from "path";
|
|
948
1107
|
|
|
949
1108
|
// src/core/spec-injector.ts
|
|
950
|
-
import { join as
|
|
951
|
-
import { readdirSync as
|
|
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 (
|
|
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 =
|
|
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 =
|
|
1006
|
-
if (!
|
|
1007
|
-
return
|
|
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 =
|
|
1013
|
-
if (!
|
|
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 =
|
|
1017
|
-
if (
|
|
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 =
|
|
1022
|
-
if (
|
|
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 (!
|
|
1188
|
+
if (!existsSync7(dir)) return [];
|
|
1030
1189
|
const results = [];
|
|
1031
|
-
for (const entry of
|
|
1032
|
-
const fullPath =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 \
|
|
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 =
|
|
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 =
|
|
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
|
|
1302
|
-
import { existsSync as
|
|
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
|
|
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 =
|
|
1442
|
-
const deltaSpec =
|
|
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
|
|
1453
|
-
import { join as
|
|
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 =
|
|
1490
|
-
if (!
|
|
1755
|
+
const specsDir = join16(changeDir, "specs");
|
|
1756
|
+
if (!existsSync9(specsDir)) return ["general"];
|
|
1491
1757
|
try {
|
|
1492
|
-
return
|
|
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 =
|
|
1531
|
-
const specPath =
|
|
1796
|
+
const domainDir = join16(specsDir, extraction.domain);
|
|
1797
|
+
const specPath = join16(domainDir, "spec.md");
|
|
1532
1798
|
let existing = "";
|
|
1533
|
-
if (
|
|
1534
|
-
existing =
|
|
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 =
|
|
1574
|
-
const fullChangePath =
|
|
1575
|
-
if (!
|
|
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 =
|
|
1580
|
-
if (
|
|
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(
|
|
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 =
|
|
1881
|
+
const entries = readdirSync6(deltaDir, { withFileTypes: true });
|
|
1616
1882
|
for (const entry of entries) {
|
|
1617
1883
|
if (!entry.isDirectory()) continue;
|
|
1618
|
-
const deltaSpecPath =
|
|
1619
|
-
const liveSpecPath =
|
|
1620
|
-
if (!
|
|
1621
|
-
if (!
|
|
1622
|
-
mkdirSync5(
|
|
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
|
|
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 =
|
|
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
|
|
1687
|
-
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, existsSync as
|
|
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 =
|
|
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 =
|
|
1714
|
-
if (!
|
|
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 =
|
|
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 :
|
|
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 =
|
|
1997
|
+
targetDir = join19(process.cwd(), "specwf");
|
|
1732
1998
|
filename = type;
|
|
1733
1999
|
} else {
|
|
1734
|
-
targetDir =
|
|
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 =
|
|
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
|
|
1745
|
-
import { existsSync as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2028
|
+
const tplPath = join20(templatesDir, file);
|
|
1763
2029
|
let content;
|
|
1764
|
-
if (
|
|
1765
|
-
content =
|
|
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(
|
|
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
|
-
|
|
3
|
+
运行 `specwf init` 创建 specwf/ 目录结构 + platform.yml + state.md。
|
|
4
4
|
|
|
5
5
|
## 存量项目
|
|
6
6
|
|
|
7
|
-
|
|
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
|
```
|