mancode 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -16
- package/README.zh-CN.md +33 -12
- package/dist/cli.js +2310 -908
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,9 +7,9 @@ import {
|
|
|
7
7
|
import { program } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/commands/init.ts
|
|
10
|
-
import { promises as
|
|
10
|
+
import { promises as fs4 } from "fs";
|
|
11
11
|
import os from "os";
|
|
12
|
-
import
|
|
12
|
+
import path15 from "path";
|
|
13
13
|
import process5 from "process";
|
|
14
14
|
|
|
15
15
|
// src/installers/claude-code.ts
|
|
@@ -108,7 +108,7 @@ var PLAN_COACH_AGENT = {
|
|
|
108
108
|
tools: ["Read", "Grep", "Glob"],
|
|
109
109
|
body: `\u4F60\u662F mancode \u6559\u7EC3\u7EC4\u7684 Plan Coach\uFF08\u8BA1\u5212\u6559\u7EC3\uFF09\u3002
|
|
110
110
|
|
|
111
|
-
\u4F60\u7684\u804C\u8D23\uFF1A\u5728\u7528\u6237\u786E\u8BA4\u524D\
|
|
111
|
+
\u4F60\u7684\u804C\u8D23\uFF1A\u5148\u68C0\u67E5\u9700\u6C42\u662F\u5426\u8DB3\u591F\u652F\u6491\u8BA1\u5212\uFF0C\u518D\u5728\u7528\u6237\u786E\u8BA4\u524D\u628A Scout Report\u3001\u7ED3\u6784\u5316 requirements \u548C\u56E2\u961F\u4E0A\u4E0B\u6587\u6574\u7406\u6210\u53EF\u6267\u884C plan\u3002
|
|
112
112
|
|
|
113
113
|
## \u786C\u7EA6\u675F
|
|
114
114
|
|
|
@@ -116,6 +116,16 @@ var PLAN_COACH_AGENT = {
|
|
|
116
116
|
- \u4E0D\u521B\u5EFA README\u3001\u6E90\u7801\u3001\u914D\u7F6E\u3001\u6D4B\u8BD5\u6216\u56E2\u961F memory\u3002
|
|
117
117
|
- \u4E0D\u628A proposal \u5199\u6210 decision\u3002\u56E2\u961F\u51B3\u7B56\u53EA\u80FD\u5728\u7528\u6237\u786E\u8BA4\u5E76\u5B8C\u6210\u5B9E\u73B0\u540E\u8FDB\u5165 \`.mancode/memory/decisions.md\`\u3002
|
|
118
118
|
- \u53EA\u5728\u6700\u7EC8\u54CD\u5E94\u91CC\u8FD4\u56DE\u8BA1\u5212\u6587\u672C\uFF1B\u8C03\u7528\u65B9\u4F1A\u8D1F\u8D23\u5199\u5165 \`.mancode/workflows/<taskId>/plan.md\`\u3002
|
|
119
|
+
- \u4E0D\u7528\u81EA\u5DF1\u7684\u5047\u8BBE\u586B\u8865\u4F1A\u6539\u53D8\u8303\u56F4\u3001\u67B6\u6784\u3001\u6210\u672C\u6216\u9A8C\u6536\u7684 blocking \u51B3\u7B56\u3002
|
|
120
|
+
|
|
121
|
+
## \u8F93\u5165\u5C31\u7EEA\u68C0\u67E5
|
|
122
|
+
|
|
123
|
+
\u5148\u8BFB\u53D6 \`requirements.json\`\uFF0C\u4EE5\u5176\u4E2D\u7684 confirmedScope\u3001excludedScope\u3001technicalDecisions\u3001defaults\u3001blockingUnknowns\u3001coverage \u548C acceptanceCriteria \u4E3A\u6743\u5A01\u8F93\u5165\uFF1B\`requirements.md\` \u53EA\u7528\u4E8E\u9605\u8BFB\u3002\u9010\u9879\u68C0\u67E5 platform\u3001core_scope\u3001technical_stack\u3001data_and_persistence\u3001performance\u3001compatibility\u3001security \u7684\u72B6\u6001\u548C\u7406\u7531\u662F\u5426\u4E0E\u4E8B\u5B9E\u4E00\u81F4\u3002\u68C0\u67E5\u9700\u6C42\u662F\u5426\u8986\u76D6\u4EFB\u52A1\u5B9E\u9645\u9002\u7528\u7684\u7528\u6237\u76EE\u6807\u3001\u6838\u5FC3\u6D41\u7A0B\u3001\u9996\u671F\u8303\u56F4\u3001\u6392\u9664\u9879\u3001\u6280\u672F\u4E0E\u8FD0\u884C\u7EA6\u675F\u3001\u6570\u636E/\u72B6\u6001/\u96C6\u6210\u3001\u5173\u952E\u6027\u80FD/\u517C\u5BB9\u6027/\u5B89\u5168\u8981\u6C42\u548C\u9A8C\u6536\u6807\u51C6\u3002blockingUnknowns \u975E\u7A7A\u3001coverage \u7528\u65E0\u6839\u636E\u7684 not_applicable \u63A9\u76D6\u51B3\u7B56\u3001\u6838\u5FC3\u884C\u4E3A\u7F3A\u5C11\u9A8C\u6536 ID\uFF0C\u6216\u6587\u6863\u4E0E\u7ED3\u6784\u5316\u8F93\u5165\u77DB\u76FE\u65F6\u5FC5\u987B\u8FD4\u56DE NEEDS_CLARIFICATION\u3002
|
|
124
|
+
|
|
125
|
+
\u53EA\u80FD\u8FD4\u56DE\u4E24\u79CD\u7ED3\u679C\uFF1A
|
|
126
|
+
|
|
127
|
+
1. \`NEEDS_CLARIFICATION\`\uFF1A\u5B58\u5728 blocking \u7F3A\u53E3\u65F6\uFF0C\u4E0D\u751F\u6210\u8BA1\u5212\u3002\u5217\u51FA\u7F3A\u5931\u51B3\u7B56\u3001\u5BF9\u8BA1\u5212\u7684\u5F71\u54CD\u30012\u20133 \u4E2A\u53EF\u884C\u9009\u9879\u3001\u660E\u786E\u63A8\u8350\u548C\u5EFA\u8BAE\u95EE\u9898\u3002
|
|
128
|
+
2. \`READY_FOR_PLAN\`\uFF1A\u6CA1\u6709 blocking \u7F3A\u53E3\u65F6\uFF0C\u8F93\u51FA\u6807\u8BB0\u540E\u518D\u751F\u6210\u5B8C\u6574\u8BA1\u5212\u3002defaultable \u7EC6\u8282\u5FC5\u987B\u4F5C\u4E3A\u663E\u5F0F\u9ED8\u8BA4\u503C\u5199\u5165\u8BA1\u5212\u3002
|
|
119
129
|
|
|
120
130
|
## \u8F93\u51FA\u683C\u5F0F
|
|
121
131
|
|
|
@@ -127,7 +137,7 @@ var PLAN_COACH_AGENT = {
|
|
|
127
137
|
- \u7B80\u5355 / \u4E2D\u7B49 / \u590D\u6742\uFF1B\u8BF4\u660E\u7406\u7531
|
|
128
138
|
## \u6A21\u5757\u7D22\u5F15
|
|
129
139
|
## \u6280\u672F\u4E0E\u4EA4\u4ED8\u7EA6\u675F\uFF08\u4EC5\u65B0\u9879\u76EE\u6216\u6280\u672F\u9009\u62E9\u672A\u5B9A\u65F6\uFF09
|
|
130
|
-
- \
|
|
140
|
+
- \u5DF2\u786E\u8BA4\u65B9\u6848\u3001\u9009\u62E9\u7406\u7531\u548C\u53D6\u820D\uFF1B\u5C1A\u672A\u786E\u8BA4\u4F1A\u6539\u53D8\u67B6\u6784\u7684\u6280\u672F\u9009\u62E9\u65F6\u8FD4\u56DE NEEDS_CLARIFICATION\uFF1B\u5DF2\u6709\u9879\u76EE\u5199\u201C\u6CBF\u7528\u68C0\u6D4B\u5230\u7684\u9879\u76EE\u7EA6\u5B9A\u201D
|
|
131
141
|
## \u6A21\u5757\uFF1A<\u540D\u79F0>
|
|
132
142
|
### \u6539\u52A8\u6587\u4EF6 / \u65B0\u5EFA\u6587\u4EF6
|
|
133
143
|
### \u590D\u7528\u8D44\u6E90\uFF08\u5F15\u7528 scout-report.md \u884C\u53F7\uFF09
|
|
@@ -135,13 +145,16 @@ var PLAN_COACH_AGENT = {
|
|
|
135
145
|
### \u4E0D\u505A\u4EC0\u4E48
|
|
136
146
|
### \u5B9E\u65BD\u6B65\u9AA4
|
|
137
147
|
### \u98CE\u9669\u70B9
|
|
148
|
+
### \u56DE\u9000\u65B9\u5F0F
|
|
138
149
|
### \u5B8C\u6210\u5B9A\u4E49
|
|
150
|
+
## \u975E\u963B\u585E\u9ED8\u8BA4\u503C
|
|
139
151
|
## \u9A8C\u8BC1\u8BA1\u5212
|
|
152
|
+
- \u628A\u6BCF\u4E2A required \u9A8C\u6536 ID \u6620\u5C04\u5230 automated / manual / hybrid \u9A8C\u8BC1\uFF0C\u4E0D\u5F97\u7528\u9875\u9762\u52A0\u8F7D\u6216\u4EE3\u7801\u9605\u8BFB\u4EE3\u66FF\u6838\u5FC3\u4EA4\u4E92
|
|
140
153
|
- build / lint / typecheck / test / smoke test
|
|
141
154
|
## \u9884\u4F30
|
|
142
155
|
\`\`\`
|
|
143
156
|
|
|
144
|
-
\u5982\u679C\u4EFB\u52A1\u4E0D\u5E94\u7EE7\u7EED\u5B9E\u65BD\uFF0C\
|
|
157
|
+
\u5982\u679C\u4EFB\u52A1\u4E0D\u5E94\u7EE7\u7EED\u5B9E\u65BD\uFF0C\u8FD4\u56DE NEEDS_CLARIFICATION\uFF0C\u4E0D\u8981\u7528\u4E00\u4EFD\u770B\u4F3C\u5B8C\u6574\u7684\u8BA1\u5212\u63A9\u76D6\u7F3A\u5931\u9700\u6C42\u3002`
|
|
145
158
|
};
|
|
146
159
|
|
|
147
160
|
// src/templates/agents/head-coach.ts
|
|
@@ -584,6 +597,21 @@ if (state.teamModeAutoDetected === true && mode === 'solo') {
|
|
|
584
597
|
);
|
|
585
598
|
}
|
|
586
599
|
|
|
600
|
+
if (
|
|
601
|
+
mode === 'solo' &&
|
|
602
|
+
state.activeSoloPlan &&
|
|
603
|
+
typeof state.activeSoloPlan.taskId === 'string'
|
|
604
|
+
) {
|
|
605
|
+
output.push(
|
|
606
|
+
'',
|
|
607
|
+
'### 已确认的 solo 实施计划',
|
|
608
|
+
'Task: ' + sanitize(state.activeSoloPlan.taskId),
|
|
609
|
+
'Plan version: ' + sanitize(state.activeSoloPlan.planVersion || 1),
|
|
610
|
+
'- 收到继续实施指令时,先读对应 workflow 的 requirements.md 和 plan.md。',
|
|
611
|
+
'- 按已确认范围轻量开发,不重新进入 /man,不运行独立 reviewer。',
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
587
615
|
console.log(output.join('\n'));
|
|
588
616
|
|
|
589
617
|
function readJson(filePath) {
|
|
@@ -646,8 +674,31 @@ if (mode === 'solo') {
|
|
|
646
674
|
const rawInput = readFileSync(0, 'utf8');
|
|
647
675
|
const userPrompt = readPrompt(rawInput);
|
|
648
676
|
const planningPattern = /先(?:别|不要|看|看看|调研|分析|评估)|给.*方案|给.*计划|怎么.*做|如何.*做|怎么.*实现|如何.*实现|应该怎么|怎么.*拆|拆分|只给.*计划|不要.*改代码|别.*改代码|不要.*动代码|别.*动代码|评估.*风险|风险.*评估|设计.*方案|架构|迁移|集成|\b(?:plan|planning|research|investigate|approach|proposal|architecture|risk|migration|integration)\b|how (?:should|would|to)|do not (?:edit|modify|change)|don.t (?:edit|modify|change)|no code changes|without changing code/iu;
|
|
677
|
+
const approvedPlanExecutionPattern = /按.*计划|继续.*计划|执行.*计划|实现.*计划|(?:implement|execute|continue|resume).*(?:approved )?plan/iu;
|
|
678
|
+
const hasActiveSoloPlan =
|
|
679
|
+
mode === 'solo' &&
|
|
680
|
+
state.activeSoloPlan &&
|
|
681
|
+
typeof state.activeSoloPlan.taskId === 'string';
|
|
682
|
+
|
|
683
|
+
if (hasActiveSoloPlan) {
|
|
684
|
+
output.push(
|
|
685
|
+
'## mancode 已确认计划',
|
|
686
|
+
'',
|
|
687
|
+
'当前 solo 计划:.mancode/workflows/' +
|
|
688
|
+
sanitize(state.activeSoloPlan.taskId) +
|
|
689
|
+
'/plan.md(v' +
|
|
690
|
+
sanitize(state.activeSoloPlan.planVersion || 1) +
|
|
691
|
+
')。',
|
|
692
|
+
'实施前同时读取 requirements.md;只执行确认范围,完成后运行 workflow handoff <taskId> --complete。',
|
|
693
|
+
'',
|
|
694
|
+
);
|
|
695
|
+
}
|
|
649
696
|
|
|
650
|
-
if (
|
|
697
|
+
if (
|
|
698
|
+
mode === 'solo' &&
|
|
699
|
+
planningPattern.test(userPrompt) &&
|
|
700
|
+
!(hasActiveSoloPlan && approvedPlanExecutionPattern.test(userPrompt))
|
|
701
|
+
) {
|
|
651
702
|
output.push(
|
|
652
703
|
'## mancode 自动路由',
|
|
653
704
|
'',
|
|
@@ -771,6 +822,14 @@ var SOLO_SKILL = `# mancode \xB7 solo mode
|
|
|
771
822
|
3. \u641C\u7D22\u76F8\u4F3C\u5B9E\u73B0\uFF08"\u8FD9\u4E2A\u9879\u76EE\u91CC\u6709\u6CA1\u6709\uFF1F"\uFF09
|
|
772
823
|
4. \u68C0\u67E5\u53EF\u590D\u7528\u8D44\u6E90
|
|
773
824
|
|
|
825
|
+
### \u6267\u884C\u5DF2\u786E\u8BA4\u7684 /man \u8BA1\u5212
|
|
826
|
+
\u5F53 \`.mancode/state.json\` \u7684 \`activeSoloPlan\` \u975E\u7A7A\u4E14\u7528\u6237\u8981\u6C42\u7EE7\u7EED\u8BE5\u8BA1\u5212\u65F6\uFF1A
|
|
827
|
+
1. \u8BFB\u53D6\u5BF9\u5E94 workflow \u7684 \`requirements.md\` \u548C \`plan.md\`\uFF0C\u786E\u8BA4 taskId \u4E0E planVersion\u3002
|
|
828
|
+
2. \u5F00\u5DE5\u524D\u56DE\u6267\u76EE\u6807\u3001\u6280\u672F\u65B9\u6848\u3001\u5305\u542B\u8303\u56F4\u3001\u6392\u9664\u8303\u56F4\u3001\u9A8C\u8BC1\u65B9\u5F0F\u548C\u6B8B\u4F59\u5047\u8BBE\u3002
|
|
829
|
+
3. \u53EA\u6309\u8BA1\u5212\u8F7B\u91CF\u5B9E\u65BD\uFF1B\u4E0D\u91CD\u65B0\u89C4\u5212\u3001\u4E0D\u8C03\u7528 Film Reviewer\u3001\u4E0D\u521B\u5EFA\u65B0\u7684 /man workflow\u3002
|
|
830
|
+
4. \u53D1\u73B0\u4F1A\u6539\u53D8\u67B6\u6784\u3001\u8303\u56F4\u6216\u9A8C\u6536\u7684\u7F3A\u53E3\u65F6\u6682\u505C\u5E76\u8BE2\u95EE\uFF0C\u4E0D\u81EA\u884C\u6269\u5927\u8BA1\u5212\u3002
|
|
831
|
+
5. \u5B8C\u6210\u6700\u7A84\u6709\u6548\u9A8C\u8BC1\u548C\u4E00\u6B21\u53D7\u9650 diff \u81EA\u68C0\u540E\uFF0C\u8FD0\u884C \`mancode workflow handoff <taskId> --complete\`\uFF1B\u7531 CLI \u539F\u5B50\u6E05\u7406 \`activeSoloPlan\`\u3001\u628A workflow \u6807\u8BB0 completed \u5E76\u79FB\u51FA Active Plans\uFF0C\u4FDD\u7559\u8BA1\u5212\u6587\u4EF6\u3002
|
|
832
|
+
|
|
774
833
|
### UI \u4EFB\u52A1\uFF08\u6761\u4EF6\u6267\u884C\uFF09
|
|
775
834
|
\u4EC5\u5F53 project-profile \u786E\u8BA4\u6709 UI \u8D44\u4EA7\u4E14\u4EFB\u52A1\u786E\u5B9E\u6D89\u53CA\u754C\u9762\u65F6\uFF1A
|
|
776
835
|
1. UserPromptSubmit hook \u4F1A\u6CE8\u5165\u9879\u76EE\u5BA1\u7F8E token\uFF08\u5982\u6709\uFF09
|
|
@@ -813,16 +872,18 @@ var CORE_CODING_PRINCIPLES = `## \u94C1\u5F8B\uFF08\u6C38\u4E0D\u8FDD\u53CD\uFF0
|
|
|
813
872
|
// src/templates/skills/man.ts
|
|
814
873
|
var MAN_SKILL = {
|
|
815
874
|
name: "man",
|
|
816
|
-
description: "
|
|
875
|
+
description: "Planning and progressive engineering workflow: research, align requirements, recommend options, produce a confirmed plan, then choose plan-only, lightweight solo implementation, or the full governed workflow. Use for planning requests and complex or high-risk changes.",
|
|
817
876
|
body: `# mancode \xB7 /man (Progressive Governance)
|
|
818
877
|
|
|
819
|
-
\u7528\u6237\u7528 \`/man <task>\` \u89E6\u53D1\u4F60\u3002\u7ACB\u5373\u8BFB \`.mancode/state.json\`\u3002\u82E5\u5DF2\u6709\u4E0D\u540C\u7684 active workflow\uFF0C\u4E0D\u5F97\u76F4\u63A5\u6E05\u7A7A\u6307\u9488\uFF1B\u5148\u8BA9\u7528\u6237\u9009\u62E9\u6062\u590D\u539F\u6D41\u7A0B\u6216\u7528 \`/mansolo\` \u653E\u5F03\u3002task \u4E3A\u7A7A\u65F6\u8BE2\u95EE\u4EFB\u52A1\uFF0C\u7B49\u5F85\u671F\u95F4\u4ECD\u662F \`man\` \u6A21\u5F0F\u3002
|
|
878
|
+
\u7528\u6237\u7528 \`/man <task>\` \u89E6\u53D1\u4F60\u3002\u7ACB\u5373\u8BFB \`.mancode/state.json\`\u3002\u82E5\u5DF2\u6709\u4E0D\u540C\u7684 active workflow\uFF0C\u4E0D\u5F97\u76F4\u63A5\u6E05\u7A7A\u6307\u9488\uFF1B\u5148\u8BA9\u7528\u6237\u9009\u62E9\u6062\u590D\u539F\u6D41\u7A0B\u6216\u7528 \`/mansolo\` \u653E\u5F03\u3002\u82E5 \`activeSoloPlan\` \u975E\u7A7A\uFF0C\u5148\u8BA9\u7528\u6237\u9009\u62E9\u7EE7\u7EED\u8BE5\u8BA1\u5212\uFF1B\u5DF2\u5B8C\u6210\u65F6\u8FD0\u884C \`mancode workflow handoff <\u65E7taskId> --complete\`\uFF0C\u653E\u5F03\u65F6\u53D6\u5F97\u786E\u8BA4\u540E\u901A\u8FC7 CLI \u6807\u8BB0\u65E7 workflow abandoned \u5E76\u6E05\u7406\u6307\u9488\uFF0C\u7136\u540E\u624D\u80FD\u521B\u5EFA\u65B0\u8BA1\u5212\u3002handoff CLI \u4F1A\u62D2\u7EDD\u9759\u9ED8\u8986\u76D6\u3002task \u4E3A\u7A7A\u65F6\u8BE2\u95EE\u4EFB\u52A1\uFF0C\u7B49\u5F85\u671F\u95F4\u4ECD\u662F \`man\` \u6A21\u5F0F\u3002
|
|
879
|
+
|
|
880
|
+
\u7528 \`mancode workflow create man "<task>" --json\` \u521B\u5EFA workflow \u5E76\u8BFB\u53D6\u8FD4\u56DE\u7684 taskId\uFF1B\u4E0D\u5F97\u76F4\u63A5\u521B\u5EFA\u6216\u6539\u5199 metadata.json\u3002\u968F\u540E\u53EA\u7528 workflow CLI \u66F4\u65B0\u9700\u6C42\u3001\u9A8C\u8BC1\u3001step/status/planVersion/skippedSteps\u3002\u901A\u7528 \`--skipped\` \u53EA\u7528\u4E8E Step 1\u20132 \u7684 \`clarification\`\uFF1B\u7528\u6237\u660E\u786E\u8DF3\u8FC7\u6574\u4E2A review \u65F6\u5FC5\u987B\u5728 Step 6 \u4F7F\u7528\u4E13\u7528 \`workflow review ... skip --reason\`\uFF0C\u4E0D\u80FD\u5199\u6570\u5B57\u3001\`film-1\` \u6216 \`film-2\`\u3002state \u7684 \`currentMode\`\u3001\`currentTask\` \u548C \`currentWorkflowMode\` \u6307\u5411\u672C task\u3002
|
|
820
881
|
|
|
821
|
-
|
|
882
|
+
## \u8BA1\u5212\u804C\u8D23\u4E0E\u6280\u672F\u9009\u62E9\u5173\u5361
|
|
822
883
|
|
|
823
|
-
|
|
884
|
+
\u628A \`/man\` \u4F5C\u4E3A\u6B63\u5F0F\u8BA1\u5212\u5165\u53E3\uFF1A\u5148\u5BF9\u9F50\u9700\u6C42\u5E76\u4EA7\u51FA\u53EF\u786E\u8BA4\u7684\u8BA1\u5212\uFF0C\u4E0D\u56E0\u8BA1\u5212\u5B8C\u6210\u800C\u81EA\u52A8\u8FDB\u5165\u5B8C\u6574\u5B9E\u65BD\u3002\u8BA1\u5212\u5173\u5361\u5FC5\u987B\u8BA9\u7528\u6237\u9009\u62E9\u53EA\u4FDD\u7559\u8BA1\u5212\u3001\u4EA4\u7ED9\u9ED8\u8BA4 \`solo\` \u8F7B\u91CF\u5B9E\u65BD\u3001\u7EE7\u7EED\u5B8C\u6574 \`/man\` \u6216\u4FEE\u6539\u8BA1\u5212\u3002
|
|
824
885
|
|
|
825
|
-
\u5148\u8BFB \`.mancode/project-profile.json\`\u3002\u5982\u679C\u7528\u6237\u8981\u65B0\u5EFA\u9879\u76EE\u3001profile \u4E3A unknown\uFF0C\u4E14\u7528\u6237\u6CA1\u6709\u6307\u5B9A\u6280\u672F\u6808\uFF1A\
|
|
886
|
+
\u5148\u8BFB \`.mancode/project-profile.json\`\u3002\u5982\u679C\u7528\u6237\u8981\u65B0\u5EFA\u9879\u76EE\u3001profile \u4E3A unknown\uFF0C\u4E14\u7528\u6237\u6CA1\u6709\u6307\u5B9A\u6280\u672F\u6808\uFF1A\u6536\u96C6\u9002\u7528\u7684\u76EE\u6807\u5E73\u53F0\u3001\u89C4\u6A21\u3001\u79BB\u7EBF/\u90E8\u7F72\u3001\u56E2\u961F\u719F\u6089\u5EA6\u3001\u9884\u7B97\u4E0E\u96C6\u6210\u7EA6\u675F\uFF1B\u5217\u51FA 2\u20133 \u4E2A\u53EF\u884C\u65B9\u6848\u7684\u4F18\u7F3A\u70B9\u3001\u660E\u786E\u63A8\u8350\u548C\u63A8\u8350\u7406\u7531\uFF0C\u53D6\u5F97\u7528\u6237\u786E\u8BA4\u3002\u4E0D\u8981\u628A\u8BED\u8A00\u3001\u6846\u67B6\u3001UI \u5E93\u3001\u6570\u636E\u5B58\u50A8\u6216\u6D4F\u89C8\u5668\u81EA\u52A8\u5316\u5F53\u4F5C\u9759\u9ED8\u9ED8\u8BA4\u3002\u5DF2\u6709\u9879\u76EE\u4EE5\u68C0\u6D4B\u5230\u7684\u4E8B\u5B9E\u548C\u4ED3\u5E93\u7EA6\u5B9A\u4E3A\u51C6\u3002
|
|
826
887
|
|
|
827
888
|
### Step 1: Scout \u8C03\u7814
|
|
828
889
|
|
|
@@ -830,18 +891,34 @@ var MAN_SKILL = {
|
|
|
830
891
|
|
|
831
892
|
### Step 2: \u9700\u6C42\u6F84\u6E05
|
|
832
893
|
|
|
833
|
-
\
|
|
894
|
+
\u57FA\u4E8E Scout \u62A5\u544A\u505A\u9700\u6C42\u5C31\u7EEA\u5224\u65AD\uFF0C\u4E0D\u7528\u56FA\u5B9A\u8F6E\u6570\u6216\u95EE\u9898\u6570\u91CF\u4EE3\u66FF\u7406\u89E3\u8D28\u91CF\u3002\u628A\u6240\u6709\u4F1A\u6539\u53D8\u8303\u56F4\u3001\u67B6\u6784\u3001\u6210\u672C\u6216\u9A8C\u6536\u4E14\u65E0\u6CD5\u4ECE\u9879\u76EE\u4E8B\u5B9E\u4E2D\u67E5\u6E05\u7684\u7591\u95EE\u95EE\u51FA\u6765\uFF1B\u5141\u8BB8\u6309\u9700\u8981\u5206\u591A\u6279\u6F84\u6E05\uFF0C\u4E0D\u9650\u5236\u6BCF\u6279\u6570\u91CF\u3002\u6BCF\u6B21\u56DE\u7B54\u540E\u91CD\u65B0\u5224\u65AD\uFF0C\u53EA\u8FFD\u95EE\u4ECD\u963B\u585E\u8BA1\u5212\u7684\u672A\u77E5\u9879\uFF0C\u4E0D\u91CD\u590D\u5DF2\u786E\u8BA4\u5185\u5BB9\u3002
|
|
895
|
+
|
|
896
|
+
\u628A\u672A\u77E5\u9879\u5206\u4E3A\uFF1A
|
|
897
|
+
- **blocking**\uFF1A\u6280\u672F\u6808\u3001\u6838\u5FC3\u8303\u56F4\u3001\u6301\u4E45\u5316\u3001\u4E3B\u8981\u4EA4\u4E92\u3001\u5173\u952E\u6027\u80FD/\u517C\u5BB9\u6027/\u5B89\u5168\u76EE\u6807\u7B49\u4F1A\u6539\u53D8\u65B9\u6848\u7684\u51B3\u7B56\uFF0C\u5FC5\u987B\u786E\u8BA4\u3002
|
|
898
|
+
- **recommendable**\uFF1A\u6709\u5408\u9002\u65B9\u6848\u65F6\u76F4\u63A5\u7ED9 2\u20133 \u4E2A\u65B9\u6848\u3001\u4F18\u7F3A\u70B9\u548C\u660E\u786E\u63A8\u8350\uFF0C\u8BA9\u7528\u6237\u63A5\u53D7\u6216\u8C03\u6574\uFF1B\u4E0D\u8981\u53EA\u628A\u9009\u62E9\u9898\u629B\u7ED9\u7528\u6237\u3002
|
|
899
|
+
- **defaultable**\uFF1A\u547D\u540D\u3001\u5FAE\u5C0F\u6837\u5F0F\u7B49\u4F4E\u5F71\u54CD\u7EC6\u8282\uFF0C\u53EF\u91C7\u7528\u9ED8\u8BA4\u503C\uFF0C\u4F46\u5199\u660E\u9ED8\u8BA4\u503C\u548C\u7406\u7531\u3002
|
|
900
|
+
|
|
901
|
+
\u6309\u4EFB\u52A1\u5B9E\u9645\u9002\u7528\u8303\u56F4\u68C0\u67E5\uFF1A\u7528\u6237\u76EE\u6807\u4E0E\u5E73\u53F0\u3001\u6838\u5FC3\u6D41\u7A0B\u3001\u9996\u671F\u8303\u56F4\u3001\u6392\u9664\u9879\u3001\u6280\u672F\u4E0E\u8FD0\u884C\u7EA6\u675F\u3001\u6570\u636E/\u72B6\u6001/\u96C6\u6210\u3001\u6027\u80FD/\u517C\u5BB9\u6027/\u5B89\u5168\u3001\u53EF\u9A8C\u8BC1\u5B8C\u6210\u6807\u51C6\u3002\u8FDE\u7EED\u6F84\u6E05\u6CA1\u6709\u51CF\u5C11 blocking \u9879\u65F6\uFF0C\u4E0D\u6563\u95EE\uFF1B\u8BA9\u7528\u6237\u9009\u62E9\u7F29\u5C0F\u9996\u671F\u8303\u56F4\u3001\u63A5\u53D7\u5217\u660E\u7684\u63A8\u8350\u9ED8\u8BA4\u503C\u6216\u6682\u505C\u3002
|
|
902
|
+
|
|
903
|
+
\u628A\u7ED3\u8BBA\u5199\u5165\u7ED3\u6784\u5316\u9700\u6C42\u8F93\u5165\uFF0C\u81F3\u5C11\u5305\u542B \`goal\`\u3001\u975E\u7A7A \`confirmedScope\`\u3001\`excludedScope\`\u3001\`technicalDecisions\`\u3001\`defaults\`\u3001\`blockingUnknowns\`\u3001\`coverage\` \u548C \`acceptanceCriteria\`\u3002coverage \u5FC5\u987B\u9010\u9879\u8BF4\u660E platform\u3001core_scope\u3001technical_stack\u3001data_and_persistence\u3001performance\u3001compatibility\u3001security \u662F \`confirmed\`\u3001\`defaulted\` \u6216\u5E26\u7406\u7531\u7684 \`not_applicable\`\uFF0C\u4E0D\u80FD\u7528\u7A7A\u6570\u7EC4\u5047\u88C5\u5DF2\u7ECF\u8003\u8651\u3002\u6BCF\u4E2A\u6838\u5FC3\u7528\u6237\u884C\u4E3A\u5FC5\u987B\u6709\u7A33\u5B9A\u9A8C\u6536 ID\uFF08\`AC-1\` \u7B49\uFF09\u3001\u63CF\u8FF0\u3001\`required\` \u548C\u9A8C\u8BC1\u65B9\u5F0F \`automated\` / \`manual\` / \`hybrid\`\uFF1B\u81F3\u5C11\u4E00\u4E2A\u9A8C\u6536\u9879\u5FC5\u9700\u3002\u8FD0\u884C \`mancode workflow requirements <taskId> finalize --file <requirements-input.json>\`\uFF0C\u7531 CLI \u6821\u9A8C\u5E76\u751F\u6210\u6743\u5A01 \`requirements.json\` \u4E0E \`requirements.md\`\uFF0C\u4E0D\u8981\u624B\u5DE5\u5236\u9020\u4E24\u4EFD\u53EF\u80FD\u51B2\u7A81\u7684\u7ED3\u8BBA\u3002\u65E0\u987B\u63D0\u95EE\u65F6\u53EF\u628A \`clarification\` \u8BB0\u5165\u7D2F\u8BA1 skippedSteps\uFF0C\u4F46\u4E0D\u80FD\u8DF3\u8FC7\u9700\u6C42\u6458\u8981\u4E0E\u5C31\u7EEA\u5224\u65AD\u3002\u4ECD\u6709 blocking \u9879\u65F6\u505C\u5728 Step 2\uFF1B\u53EA\u6709 CLI \u8FD4\u56DE ready \u624D\u8FD0\u884C \`mancode workflow update <taskId> --step 3\`\u3002
|
|
834
904
|
|
|
835
905
|
### Step 3: Plan Coach \u51FA\u8BA1\u5212
|
|
836
906
|
|
|
837
|
-
\u8C03\u7528\u53EA\u8BFB \`plan-coach\`\uFF0C\u8F93\u5165 task\u3001scout-report \u548C requirements
|
|
907
|
+
\u8C03\u7528\u53EA\u8BFB \`plan-coach\`\uFF0C\u8F93\u5165 task\u3001scout-report\u3001\`requirements.json\` \u548C\u6E32\u67D3\u540E\u7684 \`requirements.md\`\u3002Plan Coach \u5148\u8FD4\u56DE \`READY_FOR_PLAN\` \u6216 \`NEEDS_CLARIFICATION\`\u3002\u540E\u8005\u53EA\u5217\u7F3A\u5931\u51B3\u7B56\u3001\u5F71\u54CD\u3001\u63A8\u8350\u548C\u95EE\u9898\uFF1B\u4E3B skill \u5C06 workflow \u9000\u56DE Step 2 \u5E76\u91CD\u65B0 finalize requirements\uFF0C\u4E0D\u5F97\u5F3A\u884C\u8865\u5168\u8BA1\u5212\u3002\u524D\u8005\u8FD4\u56DE\u8BA1\u5212\u6587\u672C\uFF0C\u7531\u4E3B skill \u5199\u5165 \`plan.md\`\u3002
|
|
908
|
+
|
|
909
|
+
\u8BA1\u5212\u5FC5\u987B\u542B\u9700\u6C42\u6458\u8981\u3001\u4EFB\u52A1\u5206\u7EA7\u3001\u6280\u672F\u9009\u62E9\u53CA\u7406\u7531\u3001\u6A21\u5757\u7D22\u5F15\u3001\u590D\u7528\u8D44\u6E90\u4E0E scout \u884C\u53F7\u3001\u6838\u5FC3\u884C\u4E3A\u3001\u6700\u5C0F\u7B56\u7565\u3001\u4E0D\u505A\u4EC0\u4E48\u3001\u6B65\u9AA4\u3001\u98CE\u9669/\u56DE\u9000\u3001\u5B8C\u6210\u5B9A\u4E49\u3001\u771F\u5B9E\u9A8C\u8BC1\u4E0E smoke test\u3001\u9884\u4F30\u548C\u975E\u963B\u585E\u9ED8\u8BA4\u503C\u3002\u9996\u6B21\u8BA1\u5212\u5199\u5165\u6210\u529F\u540E\u8FD0\u884C \`mancode workflow update <taskId> --step 4\`\uFF1B\u91CD\u5199\u8BA1\u5212\u65F6\u4FDD\u6301 Step 4\uFF0C\u8FD0\u884C \`--plan-version <\u5F53\u524D\u7248\u672C+1>\`\uFF0C\u4E0D\u5F97\u76F4\u63A5\u7F16\u8F91 planVersion\u3002
|
|
838
910
|
|
|
839
911
|
### Step 4: \u8BA1\u5212\u5173\u5361
|
|
840
912
|
|
|
841
913
|
\u7528 AskUserQuestion \u8BA9\u7528\u6237\u9009\u62E9\uFF1A
|
|
842
|
-
1. **\
|
|
843
|
-
2. **\u7EE7\u7EED\
|
|
844
|
-
3. **\
|
|
914
|
+
1. **\u4EA4\u7ED9 solo \u8F7B\u91CF\u6267\u884C**\uFF1A\u5E38\u89C4\u4F4E\u98CE\u9669\u4EFB\u52A1\u63A8\u8350\u3002\u8FD0\u884C \`mancode workflow handoff <taskId> --to solo\`\uFF1B\u4FDD\u7559\u5DF2\u786E\u8BA4\u8BA1\u5212\u548C Active Plans\uFF0C\u7531 solo \u6309\u8BA1\u5212\u505A\u6700\u7A84\u9A8C\u8BC1\u548C\u4E00\u6B21\u53D7\u9650\u81EA\u68C0\uFF0C\u4E0D\u8FDB\u5165 Step 5\u20139\u3002
|
|
915
|
+
2. **\u7EE7\u7EED\u5B8C\u6574 /man**\uFF1A\u9AD8\u98CE\u9669\u6216\u9700\u8981\u72EC\u7ACB\u5BA1\u67E5\u65F6\u63A8\u8350\u3002\u5148\u8FD0\u884C \`mancode workflow decide <taskId> --plan-decision governed_execution\`\uFF0C\u8F93\u51FA\u5F00\u5DE5\u56DE\u6267\uFF0C\u518D\u66F4\u65B0\u81F3 Step 5\u3002
|
|
916
|
+
3. **\u53EA\u8981\u8BA1\u5212\uFF08\u4EC5\u4FDD\u7559\uFF09**\uFF1A\u8FD0\u884C \`mancode workflow decide <taskId> --plan-decision plan_only\`\uFF0C\u7531 CLI \u539F\u5B50\u4FDD\u5B58 planned \u72B6\u6001\u3001\u5207\u56DE solo \u5E76\u6E05\u7A7A\u53D7\u7BA1 workflow \u6307\u9488\uFF0C\u7ED3\u675F\u3002
|
|
917
|
+
4. **\u4FEE\u6539\u8BA1\u5212**\uFF1A\u6536\u96C6\u610F\u89C1\uFF1B\u82E5\u6539\u53D8\u9700\u6C42\u5219\u56DE Step 2\uFF0C\u5426\u5219\u91CD\u8DD1 Plan Coach\u3002\u91CD\u5199\u5B8C\u6210\u540E\u9012\u589E planVersion\uFF0C\u4FDD\u6301\u8BA1\u5212\u51B3\u7B56\u4E3A\u7A7A\u3002
|
|
918
|
+
|
|
919
|
+
\u6839\u636E\u98CE\u9669\u660E\u786E\u63A8\u8350\u7406\u7531\uFF0C\u4E0D\u628A\u5B8C\u6574 \`/man\` \u6C38\u8FDC\u6807\u4E3A\u63A8\u8350\u3002\u9274\u6743\u3001\u652F\u4ED8\u3001\u654F\u611F\u6570\u636E\u3001\u8FC1\u79FB/\u5220\u9664\u3001\u516C\u5F00 API\u3001\u672A\u53EF\u4FE1\u8F93\u5165\u3001\u5E76\u53D1\u3001\u8DE8\u670D\u52A1\u6216\u57FA\u7840\u8BBE\u65BD\u9ED8\u8BA4\u63A8\u8350\u5B8C\u6574 \`/man\`\uFF1B\u666E\u901A\u539F\u578B\u3001\u5185\u90E8\u5DE5\u5177\u548C\u4F4E\u98CE\u9669\u529F\u80FD\u9ED8\u8BA4\u63A8\u8350 solo\u3002
|
|
920
|
+
|
|
921
|
+
\u786E\u8BA4\u6267\u884C\u540E\u3001\u4FEE\u6539\u4E1A\u52A1\u6587\u4EF6\u524D\u8F93\u51FA\u5F00\u5DE5\u56DE\u6267\uFF1A\u8BA1\u5212\u7248\u672C\u3001\u6267\u884C\u65B9\u5F0F\u3001\u76EE\u6807\u4E0E\u4EA4\u4ED8\u7269\u3001\u6280\u672F\u65B9\u6848\u3001\u5305\u542B\u8303\u56F4\u3001\u6392\u9664\u8303\u56F4\u3001\u9A8C\u8BC1\u65B9\u5F0F\u548C\u6B8B\u4F59\u5047\u8BBE\u3002\u4ECD\u6709 blocking \u672A\u77E5\u9879\u65F6\u4E0D\u5F97\u58F0\u79F0\u5F00\u59CB\u5B9E\u65BD\u3002
|
|
845
922
|
|
|
846
923
|
### Step 5: \u5B9E\u65BD
|
|
847
924
|
|
|
@@ -849,9 +926,11 @@ var MAN_SKILL = {
|
|
|
849
926
|
|
|
850
927
|
### Step 6: \u81EA\u6D4B\u3001\u8BCA\u65AD\u4E0E\u56DE\u5F52
|
|
851
928
|
|
|
852
|
-
\u8FD0\u884C\
|
|
929
|
+
\u5148\u8FD0\u884C \`mancode workflow verify <taskId> init\`\uFF0C\u518D\u6309 \`requirements.json\` \u7684\u6BCF\u4E2A\u9A8C\u6536 ID \u8BB0\u5F55\u771F\u5B9E\u7ED3\u679C\u3002\u81EA\u52A8 passed/failed \u5FC5\u987B\u4F7F\u7528 \`mancode workflow verify <taskId> record --acceptance AC-N --method automated --result passed|failed --evidence "<\u6458\u8981>" --command "<\u5B9E\u9645\u547D\u4EE4>" --exit-code <\u9000\u51FA\u7801> [--evidence-file <\u62A5\u544A>]\`\uFF1BCLI \u6821\u9A8C passed \u7684\u9000\u51FA\u7801\u4E3A 0\u3001failed \u4E3A\u975E 0\u3002\u9700\u8981\u771F\u5B9E\u6D4F\u89C8\u5668\u3001\u8BBE\u5907\u6216\u4EBA\u7684\u5224\u65AD\u65F6\uFF0C\u4F7F\u7528 \`mancode workflow verify <taskId> require-manual --acceptance AC-N --evidence "<\u81EA\u52A8\u5316\u4E0D\u80FD\u8986\u76D6\u7684\u539F\u56E0>"\`\uFF1BCLI \u4F1A\u963B\u585E\u4E3B\u4EFB\u52A1\u3002\u660E\u786E\u544A\u8BC9\u7528\u6237\u5177\u4F53\u5B9E\u6D4B\u6B65\u9AA4\u5E76\u505C\u4E0B\uFF0C\u53EA\u6709\u6536\u5230\u7528\u6237\u660E\u786E\u786E\u8BA4\u540E\u624D\u4F7F\u7528 \`mancode workflow verify <taskId> confirm-manual --acceptance AC-N --evidence "<\u7528\u6237\u786E\u8BA4\u539F\u6587>"\`\u3002\u6B64\u8BC1\u636E\u7528\u4E8E\u5BA1\u8BA1\uFF0C\u4E0D\u4EE3\u8868 CLI \u80FD\u8BA4\u8BC1\u64CD\u4F5C\u8005\u8EAB\u4EFD\u3002\u4E0D\u5F97\u7528\u9875\u9762\u52A0\u8F7D\u3001\u63A7\u5236\u63D0\u793A\u3001\u622A\u56FE\u3001\u4EE3\u7801\u9605\u8BFB\u6216 reviewer \u4EE3\u66FF\u6838\u5FC3\u4EA4\u4E92\u9A8C\u6536\u3002
|
|
853
930
|
|
|
854
|
-
\
|
|
931
|
+
\u8FD0\u884C\u5B9E\u9645 build/lint/typecheck/test \u548C smoke test\u3002\u76F8\u540C\u4EE3\u7801\u3001\u73AF\u5883\u3001\u547D\u4EE4\u4E0B\u76F8\u540C\u9519\u8BEF\u7B7E\u540D\u5931\u8D25\u4E24\u6B21\uFF0C\u505C\u6B62\u76F2\u8BD5\u5E76\u8BCA\u65AD\u6839\u56E0\u3002\u9700\u8981\u590D\u6742\u590D\u73B0\u6216\u56DE\u5F52\u65F6\uFF0C\u7528 \`mancode workflow create manba "<\u95EE\u9898>" --parent-task <taskId> --json\` \u521B\u5EFA\u5B50 workflow\uFF1B\u7236\u4EFB\u52A1\u4FDD\u6301 Step 6\u3002\u5B50\u4EFB\u52A1 fixed/verified/no_repro \u540E\u6062\u590D\u672C\u4EFB\u52A1\uFF1B\`manual_test_required\` \u4ECD\u5FC5\u987B\u8D70\u4E0A\u8FF0\u4EBA\u5DE5\u786E\u8BA4\u3002\u6240\u6709 required \u9A8C\u6536\u53CA hybrid \u7684\u4E24\u4E2A\u90E8\u5206\u90FD passed \u540E\uFF0CCLI \u624D\u5141\u8BB8\u8FDB\u5165 Step 7 \u6216\u542F\u52A8 review\uFF1B\u8BA1\u5212\u7248\u672C\u6216\u7ED3\u6784\u5316\u9700\u6C42\u53D8\u5316\u4F1A\u4F7F\u65E7\u9A8C\u8BC1\u5931\u6548\u3002
|
|
932
|
+
|
|
933
|
+
\u9A8C\u8BC1\u540E\u57FA\u4E8E**\u5B9E\u9645 diff**\u5199 \`review-scope.md\`\uFF1Abase\u3001\u6539\u52A8\u6587\u4EF6\u3001\u9700\u6C42\u3001\u5DF2\u8DD1\u9A8C\u8BC1\u3001\u786C\u98CE\u9669\u548C\u5BA1\u67E5\u6DF1\u5EA6\u3002\u9274\u6743\u3001\u652F\u4ED8\u3001\u654F\u611F\u6570\u636E\u3001\u8FC1\u79FB/\u5220\u9664\u3001\u516C\u5F00 API\u3001\u672A\u53EF\u4FE1\u8F93\u5165\u3001\u5E76\u53D1\u3001\u8DE8\u670D\u52A1\u6216\u57FA\u7840\u8BBE\u65BD\u547D\u4E2D\u4EFB\u4E00\u9879\u65F6\u7528\u5B8C\u6574\u5BA1\u67E5 \`full\`\uFF1B\u5426\u5219\u7528\u5B9A\u5411\u5BA1\u67E5 \`targeted\`\u3002\u8FD0\u884C \`mancode workflow review <taskId> init --review-depth targeted --review-domain quality\` \u6216 \`--review-depth full\`\u3002\u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u8DF3\u8FC7\u5BA1\u67E5\u65F6\u624D\u8FD0\u884C \`mancode workflow review <taskId> skip --reason "<\u7528\u6237\u7406\u7531>"\`\uFF1BCLI \u4F1A\u8BB0\u5F55\u539F\u56E0\u5E76\u7D2F\u8BA1 \`review\`\uFF0C\u4E0D\u5F97\u901A\u8FC7\u901A\u7528 skipped \u53C2\u6570\u7ED5\u8FC7\u3002
|
|
855
934
|
|
|
856
935
|
### Step 7: Film #1 \u4EE3\u7801\u8D28\u91CF\u5BA1\u67E5\u4E0E\u4FEE\u590D
|
|
857
936
|
|
|
@@ -864,7 +943,7 @@ var MAN_SKILL = {
|
|
|
864
943
|
### Step 9: \u589E\u5F3A\u6536\u5C3E
|
|
865
944
|
|
|
866
945
|
1. \u5B58\u5728 open blocker \u65F6\uFF0CHead Coach \u4E00\u6B21\u6027\u4FEE\u590D\u5168\u90E8 blocker\uFF0C\u5E76\u7528 \`mancode workflow review <taskId> remediate --resolved Q1,D1\` \u8BB0\u5F55\u552F\u4E00\u4E00\u8F6E\u4FEE\u590D\uFF1B\u6CA1\u6709 blocker \u65F6\u4E0D\u8FD0\u884C remediate\u3002\u4E0D\u8981\u4E3A \u{1F7E1}/\u{1F7E2} \u6269\u5927\u6539\u52A8\u3002
|
|
867
|
-
2. \u91CD\u8DD1\
|
|
946
|
+
2. remediation \u4F1A\u4F7F\u65E7\u9A8C\u8BC1\u6574\u6279\u5931\u6548\u3002\u5728 Step 9 \u91CD\u8DD1\u5168\u90E8 required \u9A8C\u6536\uFF0C\u5E76\u901A\u8FC7 verify record/require-manual/confirm-manual \u91CD\u65B0\u767B\u8BB0\u8BC1\u636E\uFF1B\u672A\u91CD\u65B0\u5168\u90E8\u901A\u8FC7\u4E0D\u80FD completed\u3002\u4E0D\u91CD\u65B0\u8FD0\u884C\u5DF2\u5B8C\u6210\u7684 reviewer\u3002\u4FEE\u590D\u82E5\u5F15\u5165\u65B0\u7684\u9AD8\u98CE\u9669\u9762\u5219\u6807\u8BB0 blocked\uFF0C\u4E0D\u80FD\u5F00\u542F\u65E0\u754C review \u5FAA\u73AF\u3002
|
|
868
947
|
3. \u5199 \`summary.md\`\uFF1A\u6539\u52A8\u3001\u65B0\u5EFA\u3001\u590D\u7528\u3001\u9A8C\u8BC1\u3001\u5BA1\u67E5\u6DF1\u5EA6\u3001findings \u5904\u7F6E\u3001\u8DF3\u8FC7\u6B65\u9AA4\u548C\u6B8B\u4F59\u98CE\u9669\u3002
|
|
869
948
|
4. CLI \u786E\u8BA4\u6240\u9700\u5BA1\u67E5\u9886\u57DF\u5B8C\u6210\u4E14 blocker \u6E05\u96F6\u540E\u624D\u5199 \`completed\`\uFF1B\u5426\u5219\u7528 \`--status blocked --blocking-reason "<\u539F\u56E0>"\`\u3002
|
|
870
949
|
5. \u5173\u952E\u51B3\u7B56 appendTeamDecision \u5230 \`decisions.md\`\uFF0C\u66F4\u65B0 Active Plans\u3002
|
|
@@ -1190,13 +1269,13 @@ var MANTEAM_SKILL = {
|
|
|
1190
1269
|
\u6309 \`/man\` \u7684 9 \u6B65\u6D41\u7A0B\u6267\u884C\uFF08\u542B\u6F84\u6E05\u3001\u8BA1\u5212\u5173\u5361\u548C\u589E\u5F3A\u6536\u5C3E\uFF09\uFF0C\u4F46\u6BCF\u4E00\u6B65\u589E\u52A0\u56E2\u961F\u7EA6\u675F\uFF1A
|
|
1191
1270
|
|
|
1192
1271
|
1. Scout Report\uFF1A\u5FC5\u987B\u5217\u51FA\u5171\u4EAB\u6587\u4EF6\u3001\u8FD1\u671F\u76F8\u5173\u63D0\u4EA4\u3001\u6F5C\u5728\u51B2\u7A81\u6587\u4EF6\u3002
|
|
1193
|
-
2. \u6F84\u6E05\uFF1A\
|
|
1194
|
-
3. Game Plan\
|
|
1195
|
-
4. \u8BA1\u5212\u5173\u5361\uFF1A\u7528\u6237\u53EF\u53EA\u4FDD\u7559\u8BA1\u5212\u3001\u7EE7\u7EED\u6267\u884C\u6216\u91CD\u5199\u8BA1\u5212\uFF1BActive Plans \u6309 taskId \u66F4\u65B0\u3002
|
|
1272
|
+
2. \u6F84\u6E05\uFF1A\u6CBF\u7528 /man \u7684\u9700\u6C42\u5C31\u7EEA\u95E8\u69DB\uFF0C\u4E0D\u8BBE\u56FA\u5B9A\u8F6E\u6570\u6216\u6BCF\u6279\u95EE\u9898\u6570\u91CF\uFF1B\u95EE\u51FA\u6240\u6709\u4F1A\u6539\u53D8\u51B3\u7B56\u4E14\u65E0\u6CD5\u4ECE\u9879\u76EE\u4E8B\u5B9E\u67E5\u6E05\u7684\u7591\u95EE\uFF0C\u4E0D\u91CD\u590D\u5DF2\u786E\u8BA4\u5185\u5BB9\u3002\u6709\u5408\u9002\u65B9\u6848\u65F6\u76F4\u63A5\u7ED9\u51FA\u9009\u9879\u3001\u4F18\u7F3A\u70B9\u548C\u660E\u786E\u63A8\u8350\u3002\u901A\u8FC7 \`workflow requirements ... finalize\` \u56FA\u5316\u4E03\u4E2A coverage \u7EF4\u5EA6\u3001\u7ED3\u6784\u5316\u9700\u6C42\u548C\u5E26\u9A8C\u8BC1\u65B9\u5F0F\u7684\u9A8C\u6536 ID\uFF0C\u53EA\u6709 CLI \u5224\u5B9A ready \u624D\u8FDB\u5165\u8BA1\u5212\u3002
|
|
1273
|
+
3. Game Plan\uFF1APlan Coach \u5148\u505A\u8F93\u5165\u5C31\u7EEA\u68C0\u67E5\uFF1B\u8BA1\u5212\u91CC\u5FC5\u987B\u5305\u542B\u53D8\u66F4\u8FB9\u754C\u3001\u6280\u672F\u9009\u62E9\u7406\u7531\u3001\u517C\u5BB9\u6027\u98CE\u9669\u3001\u56DE\u6EDA\u65B9\u5F0F\uFF0C\u7981\u6B62\u63D0\u524D\u4FEE\u6539\u4E1A\u52A1\u6587\u4EF6\u6216\u56E2\u961F memory\u3002
|
|
1274
|
+
4. \u8BA1\u5212\u5173\u5361\uFF1A\u7528\u6237\u53EF\u53EA\u4FDD\u7559\u8BA1\u5212\u3001\u7EE7\u7EED\u5B8C\u6574\u56E2\u961F\u6267\u884C\u3001\u660E\u786E\u4EA4\u7ED9 solo \u8F7B\u91CF\u6267\u884C\u6216\u91CD\u5199\u8BA1\u5212\uFF1BActive Plans \u6309 taskId \u66F4\u65B0\u3002\u56E2\u961F\u5171\u4EAB\u6587\u4EF6\u6216\u4EA4\u63A5\u98CE\u9669\u5B58\u5728\u65F6\u63A8\u8350\u5B8C\u6574\u56E2\u961F\u6267\u884C\u3002
|
|
1196
1275
|
5. \u5B9E\u65BD\uFF1A\u6539\u52A8\u524D\u518D\u6B21\u68C0\u67E5 \`git status --short\`\uFF0C\u907F\u514D\u8E29\u7528\u6237\u6216\u961F\u53CB\u6539\u52A8\u3002
|
|
1197
|
-
6. Self-test\uFF1A\u4F18\u5148\u8DD1\u9879\u76EE\u5DF2\u6709\u9A8C\u8BC1\u547D\u4EE4\uFF1B\u5931\u8D25\u4E24\u6B21\u505C\u4E0B\u8BCA\u65AD\u6839\u56E0\u3002\u9700\u8981\
|
|
1276
|
+
6. Self-test\uFF1A\u5728 Step 6 \u521D\u59CB\u5316 verification ledger\uFF0C\u628A\u6BCF\u4E2A required \u9A8C\u6536 ID \u7684\u81EA\u52A8\u3001\u4EBA\u5DE5\u6216 hybrid \u7ED3\u679C\u548C\u8BC1\u636E\u901A\u8FC7 CLI \u8BB0\u5F55\uFF1B\u81EA\u52A8\u7ED3\u679C\u5305\u542B\u547D\u4EE4\u4E0E\u9000\u51FA\u7801\uFF0C\u672A\u5168\u90E8\u901A\u8FC7\u4E0D\u5F97\u8FDB\u5165 review\u3002\u4F18\u5148\u8DD1\u9879\u76EE\u5DF2\u6709\u9A8C\u8BC1\u547D\u4EE4\uFF1B\u5931\u8D25\u4E24\u6B21\u505C\u4E0B\u8BCA\u65AD\u6839\u56E0\u3002\u9700\u8981\u4EBA\u5DE5\u9A8C\u8BC1\u65F6\u6807\u8BB0 require-manual \u5E76\u7B49\u5F85\u7528\u6237\u660E\u786E\u786E\u8BA4\uFF0C\u4E0D\u5F97\u81EA\u52A8\u6062\u590D\u3002remediation \u540E\u5728 Step 9 \u91CD\u65B0\u767B\u8BB0\u5168\u90E8\u9A8C\u6536\u3002
|
|
1198
1277
|
7. Review scope + Film #1\uFF1A\u57FA\u4E8E\u5B9E\u9645 diff \u5199 \`review-scope.md\`\uFF0C\u7528 \`workflow review ... init\` \u9009\u62E9 targeted \u6216 full\uFF1B\u91CD\u70B9\u5BA1\u67E5\u884C\u4E3A\u3001\u53EF\u7EF4\u62A4\u6027\u3001\u56E2\u961F\u98CE\u683C\u4E0E\u6D4B\u8BD5\uFF0Cfinding \u5FC5\u987B\u6709\u8BC1\u636E\u548C\u7A33\u5B9A ID\u3002
|
|
1199
|
-
8. Film #2\uFF1A\u4EC5 full \u6267\u884C\uFF0C\u5148\u8BFB Film #1 \u62A5\u544A\u5E76\u53BB\u91CD\uFF0C\u53EA\u5BA1\u67E5\u8FB9\u754C\u3001\u5B89\u5168\u3001\u6027\u80FD\u3001\u5E76\u53D1\u548C\u517C\u5BB9\u6027\
|
|
1278
|
+
8. Film #2\uFF1A\u4EC5 full \u6267\u884C\uFF0C\u5148\u8BFB Film #1 \u62A5\u544A\u5E76\u53BB\u91CD\uFF0C\u53EA\u5BA1\u67E5\u8FB9\u754C\u3001\u5B89\u5168\u3001\u6027\u80FD\u3001\u5E76\u53D1\u548C\u517C\u5BB9\u6027\u3002targeted \u7684\u7B2C\u4E8C\u5BA1\u662F\u4E0D\u9002\u7528\uFF0C\u4E0D\u80FD\u8BB0\u4E3A skipped\uFF1B\u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u624D\u53EF\u8DF3\u8FC7\u5168\u90E8 review\uFF0C\u5E76\u5199\u5165 \`review\` \u548C\u6B8B\u4F59\u98CE\u9669\u3002
|
|
1200
1279
|
9. Post-game\uFF1A\u6C47\u603B blocker\uFF0C\u53EA\u505A\u4E00\u8F6E\u4FEE\u590D\u5E76\u7528 \`workflow review ... remediate\` \u8BB0\u5F55\uFF1B\u590D\u9A8C\u540E\u5199 summary \u4E0E hand-off\u3002\u9A8C\u8BC1\u5931\u8D25\u3001\u5BA1\u67E5\u4E0D\u5B8C\u6574\u6216\u4ECD\u6709 blocker \u65F6\u6807\u8BB0 blocked\uFF0C\u4E0D\u5F97\u6807 completed\u3002
|
|
1201
1280
|
|
|
1202
1281
|
\u5982\u679C\u7528\u6237\u5728\u786E\u8BA4\u9636\u6BB5\u9009\u62E9"\u9000\u51FA"\u6216\u653E\u5F03\u56E2\u961F workflow\uFF1A\u7528 \`mancode workflow update <taskId> --status abandoned\` \u66F4\u65B0\u5E76\u6E05\u7406 Active Plans\uFF1B\u82E5\u6709\u6D3B\u8DC3 manba \u5B50\u4EFB\u52A1\uFF0C\u5148\u53D6\u5F97\u786E\u8BA4\u5E76\u9010\u4E2A abandoned\u3002\u6210\u529F\u540E\u518D\u7528 Edit \u66F4\u65B0 state \u56DE solo\u3002\u4E0D\u8981\u76F4\u63A5\u6539 metadata\uFF0C\u4E5F\u4E0D\u8981\u628A abandoned workflow \u7559\u5728 active state\u3002
|
|
@@ -2164,8 +2243,8 @@ var MODE_META = {
|
|
|
2164
2243
|
].join("\n")
|
|
2165
2244
|
},
|
|
2166
2245
|
man: {
|
|
2167
|
-
description: "
|
|
2168
|
-
intro: "# mancode man \u2014
|
|
2246
|
+
description: "Planning and progressive workflow that aligns requirements, recommends options, produces a confirmed plan, and then offers plan-only, solo handoff, or full governed execution.",
|
|
2247
|
+
intro: "# mancode man \u2014 Planning and Progressive Delivery",
|
|
2169
2248
|
workflow: [
|
|
2170
2249
|
"## Progressive nine-step workflow (simulate roles in one conversation)",
|
|
2171
2250
|
"",
|
|
@@ -2177,16 +2256,16 @@ var MODE_META = {
|
|
|
2177
2256
|
"never advance state merely because a report was requested or an agent was called.",
|
|
2178
2257
|
"",
|
|
2179
2258
|
"Step 1 \u2014 Scout: inspect the profile, existing implementation, dependencies, risks, and unknowns; write `scout-report.md`.",
|
|
2180
|
-
"Step 2 \u2014 Clarify
|
|
2181
|
-
"Step 3 \u2014 Plan
|
|
2182
|
-
"Step 4 \u2014 Plan gate: the user
|
|
2259
|
+
"Step 2 \u2014 Clarify by readiness, not a fixed round or question count. Ask every decision-changing question that cannot be resolved from project facts; use as many batches as needed without repeating confirmed answers. Classify unknowns as blocking, recommendable, or defaultable. When a good approach exists, present 2\u20133 options and make a clear recommendation. Create structured input with non-empty scope, exclusions, technical decisions, defaults, blocking unknowns, stable acceptance IDs, and explicit confirmed/defaulted/not-applicable coverage for platform, core scope, stack, data, performance, compatibility, and security. Run `mancode workflow requirements <taskId> finalize --file <input.json>`; only advance to Step 3 when the CLI returns ready.",
|
|
2260
|
+
"Step 3 \u2014 Plan Coach first returns READY_FOR_PLAN or NEEDS_CLARIFICATION. Never hide blocking gaps inside assumptions. When ready, write `plan.md` with confirmed scope, technical choices and reasons, reuse, exclusions, behavior, risks/rollback, completion criteria, defaults, and real validation. First plan is v1; revisions use `--plan-version <current+1>`.",
|
|
2261
|
+
"Step 4 \u2014 Plan gate: recommend based on risk and let the user choose (1) hand the confirmed plan to lightweight solo via `mancode workflow handoff <taskId> --to solo`, (2) continue full governed execution via `mancode workflow decide <taskId> --plan-decision governed_execution` before Step 5, (3) keep the plan only via `mancode workflow decide <taskId> --plan-decision plan_only`, or (4) revise. Do not implement before this choice. Before either execution path, echo the plan version, delivery, stack, inclusions, exclusions, validation, and residual assumptions.",
|
|
2183
2262
|
"Step 5 \u2014 Implement the confirmed minimum change.",
|
|
2184
|
-
"Step 6 \u2014 Run detected build/lint/typecheck/test and smoke checks
|
|
2263
|
+
"Step 6 \u2014 Run `workflow verify <taskId> init`, execute detected build/lint/typecheck/test and smoke checks, and record every required acceptance ID with reproducible evidence; automated passed/failed records include the command and exit code. Use require-manual when a foreground browser, device, or human judgment is necessary; stop for explicit user confirmation before confirm-manual. The CLI blocks Step 7 and review until all checks pass. Then write `review-scope.md` and initialize targeted or full review. An explicit user review skip uses `workflow review <taskId> skip --reason <reason>` at Step 6, never generic skipped metadata.",
|
|
2185
2264
|
"Step 7 \u2014 Run one quality review limited to the changed diff and direct impact. Findings require changed-line evidence and user impact, with at most three new findings. Record stable blocker IDs through `workflow review ... complete`; do not fix yet.",
|
|
2186
|
-
"Step 8 \u2014 Only full review runs the security/boundary reviewer. It must read Film #1, mark the same root cause duplicate, and stay within security, permissions, recovery, resources, and boundaries. A targeted review
|
|
2187
|
-
"Step 9 \u2014 If open blockers exist, fix them in one remediation round and record resolved IDs through `workflow review ... remediate`;
|
|
2265
|
+
"Step 8 \u2014 Only full review runs the security/boundary reviewer. It must read Film #1, mark the same root cause duplicate, and stay within security, permissions, recovery, resources, and boundaries. A targeted review treats the second domain as not applicable; it is never recorded as a skipped step.",
|
|
2266
|
+
"Step 9 \u2014 If open blockers exist, fix them in one remediation round and record resolved IDs through `workflow review ... remediate`; remediation invalidates all earlier acceptance evidence, so re-run and re-record every required check at Step 9 without re-running completed reviewers. Write `summary.md` and set completed only when verification and required review domains are complete and blockers are zero.",
|
|
2188
2267
|
"",
|
|
2189
|
-
"
|
|
2268
|
+
"A solo handoff keeps the workflow planned and stores activeSoloPlan in state; solo reads requirements.md and plan.md, implements the confirmed scope with narrow validation and one bounded self-check, and does not run the /man reviewers. After successful solo verification, run `mancode workflow handoff <taskId> --complete` to atomically clear the pointer, complete the workflow at Step 4, and remove it from Active Plans. After plan-only/completed/abandoned, return state to solo. On blocked, preserve enough state to resume. A CLI rejection is a hard stop, never a reason to bypass metadata validation."
|
|
2190
2269
|
].join("\n")
|
|
2191
2270
|
},
|
|
2192
2271
|
manteam: {
|
|
@@ -2288,6 +2367,9 @@ function renderProjectContext(state, tokens, profile, options) {
|
|
|
2288
2367
|
`- UI library: ${uiLibrary}`,
|
|
2289
2368
|
`- Project profile: ${currentProfile?.projectKind || "unknown"}; validation: ${currentProfile?.availableValidation?.join(", ") || "inspect project"}`,
|
|
2290
2369
|
"- At the start of each session, read `.mancode/state.json` to check the current mode and project context.",
|
|
2370
|
+
...state?.activeSoloPlan?.taskId ? [
|
|
2371
|
+
`- Active solo plan: ${state.activeSoloPlan.taskId} (plan v${state.activeSoloPlan.planVersion ?? 1}); read its requirements.md and plan.md before implementation.`
|
|
2372
|
+
] : [],
|
|
2291
2373
|
"- Read `.mancode/project-profile.json` before choosing tools or validation. Only for a UI task in a profile with detected UI assets, read `.mancode/aesthetics/style-tokens.json`."
|
|
2292
2374
|
].join("\n");
|
|
2293
2375
|
}
|
|
@@ -2631,9 +2713,9 @@ function renderManpsRule() {
|
|
|
2631
2713
|
return renderModeSkill("manps", "/");
|
|
2632
2714
|
}
|
|
2633
2715
|
|
|
2634
|
-
// src/installers/
|
|
2635
|
-
import {
|
|
2636
|
-
import
|
|
2716
|
+
// src/installers/platform-status.ts
|
|
2717
|
+
import { promises as fs } from "fs";
|
|
2718
|
+
import path8 from "path";
|
|
2637
2719
|
|
|
2638
2720
|
// src/installers/managed-block.ts
|
|
2639
2721
|
var DEFAULT_MANCODE_START_MARKER = "<!-- mancode:start -->";
|
|
@@ -2725,104 +2807,14 @@ function findMarkerLine(content, marker) {
|
|
|
2725
2807
|
return null;
|
|
2726
2808
|
}
|
|
2727
2809
|
|
|
2728
|
-
// src/installers/codex.ts
|
|
2729
|
-
async function installCodex(projectRoot, options) {
|
|
2730
|
-
await installMancodeCore(projectRoot);
|
|
2731
|
-
const agentsPath = path7.join(projectRoot, "AGENTS.md");
|
|
2732
|
-
const existing = await readTextIfExists(agentsPath);
|
|
2733
|
-
const sharedContent = await generateSharedContent(projectRoot, {
|
|
2734
|
-
platform: "codex",
|
|
2735
|
-
displayName: "Codex (ChatGPT desktop/CLI)",
|
|
2736
|
-
capabilities: {
|
|
2737
|
-
slashCommands: "partial",
|
|
2738
|
-
subagents: false,
|
|
2739
|
-
hooks: false,
|
|
2740
|
-
skills: "agents-skills"
|
|
2741
|
-
},
|
|
2742
|
-
minimal: options.minimal,
|
|
2743
|
-
techStack: options.techStack,
|
|
2744
|
-
uiLibrary: options.uiLibrary,
|
|
2745
|
-
projectProfile: options.projectProfile
|
|
2746
|
-
});
|
|
2747
|
-
const block = [
|
|
2748
|
-
DEFAULT_MANCODE_START_MARKER,
|
|
2749
|
-
"<!-- Managed by mancode. Do not edit this block manually. -->",
|
|
2750
|
-
"",
|
|
2751
|
-
"# mancode Configuration",
|
|
2752
|
-
"",
|
|
2753
|
-
sharedContent.trim(),
|
|
2754
|
-
DEFAULT_MANCODE_END_MARKER
|
|
2755
|
-
].join("\n");
|
|
2756
|
-
await writeFile6(agentsPath, replaceManagedBlock(existing, block), "utf-8");
|
|
2757
|
-
await installCodexSkills(projectRoot, options.minimal ?? false);
|
|
2758
|
-
}
|
|
2759
|
-
|
|
2760
|
-
// src/installers/copilot.ts
|
|
2761
|
-
import { mkdir as mkdir6, writeFile as writeFile7 } from "fs/promises";
|
|
2762
|
-
import path8 from "path";
|
|
2763
|
-
async function installCopilot(projectRoot, options) {
|
|
2764
|
-
await installMancodeCore(projectRoot);
|
|
2765
|
-
const githubDir = path8.join(projectRoot, ".github");
|
|
2766
|
-
await mkdir6(githubDir, { recursive: true });
|
|
2767
|
-
const instructionsPath = path8.join(githubDir, "copilot-instructions.md");
|
|
2768
|
-
const existing = await readTextIfExists(instructionsPath);
|
|
2769
|
-
const sharedContent = await generateSharedContent(projectRoot, {
|
|
2770
|
-
platform: "copilot",
|
|
2771
|
-
displayName: "GitHub Copilot",
|
|
2772
|
-
capabilities: {
|
|
2773
|
-
slashCommands: "none",
|
|
2774
|
-
subagents: false,
|
|
2775
|
-
hooks: false,
|
|
2776
|
-
skills: "instructions"
|
|
2777
|
-
},
|
|
2778
|
-
minimal: true,
|
|
2779
|
-
techStack: options.techStack,
|
|
2780
|
-
uiLibrary: options.uiLibrary,
|
|
2781
|
-
projectProfile: options.projectProfile
|
|
2782
|
-
});
|
|
2783
|
-
const sections = [
|
|
2784
|
-
DEFAULT_MANCODE_START_MARKER,
|
|
2785
|
-
"<!-- Managed by mancode. Do not edit this block manually. -->",
|
|
2786
|
-
"",
|
|
2787
|
-
"# mancode for GitHub Copilot",
|
|
2788
|
-
"",
|
|
2789
|
-
sharedContent.trim()
|
|
2790
|
-
];
|
|
2791
|
-
if (!options.minimal) {
|
|
2792
|
-
sections.push("", renderCopilotPromptConventions());
|
|
2793
|
-
}
|
|
2794
|
-
sections.push(DEFAULT_MANCODE_END_MARKER);
|
|
2795
|
-
await writeFile7(
|
|
2796
|
-
instructionsPath,
|
|
2797
|
-
replaceManagedBlock(existing, sections.join("\n")),
|
|
2798
|
-
"utf-8"
|
|
2799
|
-
);
|
|
2800
|
-
await installCopilotPrompts(projectRoot, options.minimal ?? false);
|
|
2801
|
-
}
|
|
2802
|
-
function renderCopilotPromptConventions() {
|
|
2803
|
-
return [
|
|
2804
|
-
"## mancode Prompt Conventions",
|
|
2805
|
-
"",
|
|
2806
|
-
"GitHub Copilot does not provide native mancode slash commands, hooks, or isolated subagents. Treat these names as user prompt conventions:",
|
|
2807
|
-
"",
|
|
2808
|
-
"- man: use progressive research, planning, implementation, verification, and bounded risk-based review.",
|
|
2809
|
-
"- manba: diagnose bugs and validate real user flows or regressions.",
|
|
2810
|
-
"- manteam: read team memory and write handoff-friendly summaries.",
|
|
2811
|
-
"- manps: prefer `mancode manps [area]` before cleanup.",
|
|
2812
|
-
"- mansolo: exit any active mode and return to default solo behavior.",
|
|
2813
|
-
"",
|
|
2814
|
-
"These correspond to prompt files in `.github/prompts/`. Select the matching prompt to activate a mode."
|
|
2815
|
-
].join("\n");
|
|
2816
|
-
}
|
|
2817
|
-
|
|
2818
2810
|
// src/installers/zcode.ts
|
|
2819
|
-
import { writeFile as
|
|
2820
|
-
import
|
|
2811
|
+
import { writeFile as writeFile6 } from "fs/promises";
|
|
2812
|
+
import path7 from "path";
|
|
2821
2813
|
var ZCODE_MANCODE_START_MARKER = "<!-- mancode:zcode:start -->";
|
|
2822
2814
|
var ZCODE_MANCODE_END_MARKER = "<!-- mancode:zcode:end -->";
|
|
2823
2815
|
async function installZcode(projectRoot, options) {
|
|
2824
2816
|
await installMancodeCore(projectRoot);
|
|
2825
|
-
const agentsPath =
|
|
2817
|
+
const agentsPath = path7.join(projectRoot, "AGENTS.md");
|
|
2826
2818
|
const existing = await readTextIfExists(agentsPath);
|
|
2827
2819
|
const sharedContent = await generateSharedContent(projectRoot, {
|
|
2828
2820
|
platform: "zcode",
|
|
@@ -2847,7 +2839,7 @@ async function installZcode(projectRoot, options) {
|
|
|
2847
2839
|
sharedContent.trim(),
|
|
2848
2840
|
ZCODE_MANCODE_END_MARKER
|
|
2849
2841
|
].join("\n");
|
|
2850
|
-
await
|
|
2842
|
+
await writeFile6(
|
|
2851
2843
|
agentsPath,
|
|
2852
2844
|
replaceManagedBlock(
|
|
2853
2845
|
existing,
|
|
@@ -2860,120 +2852,491 @@ async function installZcode(projectRoot, options) {
|
|
|
2860
2852
|
await installZcodeSkills(projectRoot, options.minimal ?? false);
|
|
2861
2853
|
}
|
|
2862
2854
|
|
|
2863
|
-
// src/installers/
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
skills: "native"
|
|
2873
|
-
},
|
|
2874
|
-
install: installClaudeCode
|
|
2875
|
-
},
|
|
2876
|
-
cursor: {
|
|
2877
|
-
name: "cursor",
|
|
2878
|
-
displayName: "Cursor",
|
|
2879
|
-
capabilities: {
|
|
2880
|
-
slashCommands: "native",
|
|
2881
|
-
subagents: false,
|
|
2882
|
-
hooks: false,
|
|
2883
|
-
skills: "rules"
|
|
2884
|
-
},
|
|
2885
|
-
install: installCursor
|
|
2886
|
-
},
|
|
2887
|
-
codex: {
|
|
2888
|
-
name: "codex",
|
|
2889
|
-
displayName: "Codex (ChatGPT desktop/CLI)",
|
|
2890
|
-
capabilities: {
|
|
2891
|
-
slashCommands: "partial",
|
|
2892
|
-
subagents: false,
|
|
2893
|
-
hooks: false,
|
|
2894
|
-
skills: "agents-skills"
|
|
2895
|
-
},
|
|
2896
|
-
install: installCodex
|
|
2897
|
-
},
|
|
2898
|
-
copilot: {
|
|
2899
|
-
name: "copilot",
|
|
2900
|
-
displayName: "GitHub Copilot",
|
|
2901
|
-
capabilities: {
|
|
2902
|
-
slashCommands: "none",
|
|
2903
|
-
subagents: false,
|
|
2904
|
-
hooks: false,
|
|
2905
|
-
skills: "instructions"
|
|
2906
|
-
},
|
|
2907
|
-
install: installCopilot
|
|
2908
|
-
},
|
|
2909
|
-
zcode: {
|
|
2910
|
-
name: "zcode",
|
|
2911
|
-
displayName: "ZCode",
|
|
2912
|
-
capabilities: {
|
|
2913
|
-
slashCommands: "partial",
|
|
2914
|
-
subagents: false,
|
|
2915
|
-
hooks: false,
|
|
2916
|
-
skills: "agents-skills"
|
|
2917
|
-
},
|
|
2918
|
-
install: installZcode
|
|
2919
|
-
}
|
|
2920
|
-
};
|
|
2921
|
-
function getPlatformInstaller(platform) {
|
|
2922
|
-
if (!isPlatformName(platform)) return null;
|
|
2923
|
-
return PLATFORM_INSTALLERS[platform];
|
|
2924
|
-
}
|
|
2925
|
-
function getPlatformInstallers() {
|
|
2926
|
-
return Object.values(PLATFORM_INSTALLERS);
|
|
2927
|
-
}
|
|
2928
|
-
function formatPlatformName(platform) {
|
|
2929
|
-
return getPlatformInstaller(platform)?.displayName ?? platform;
|
|
2855
|
+
// src/installers/platform-status.ts
|
|
2856
|
+
async function checkPlatformStatus(rootDir, platform, installed) {
|
|
2857
|
+
const readiness = await checkPlatformReadiness(rootDir, platform);
|
|
2858
|
+
return {
|
|
2859
|
+
installed,
|
|
2860
|
+
ready: installed && readiness.ready,
|
|
2861
|
+
target: readiness.target,
|
|
2862
|
+
detail: describeStatus(installed, readiness)
|
|
2863
|
+
};
|
|
2930
2864
|
}
|
|
2931
|
-
function
|
|
2932
|
-
return
|
|
2865
|
+
async function isPlatformPresent(rootDir, platform) {
|
|
2866
|
+
return (await checkPlatformReadiness(rootDir, platform)).present;
|
|
2933
2867
|
}
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
hasRemote: false
|
|
2947
|
-
};
|
|
2948
|
-
var REMOTE_PATTERN = /github\.com|gitlab\.com|bitbucket\.org/;
|
|
2949
|
-
var GIT_ENV = {
|
|
2950
|
-
...process2.env,
|
|
2951
|
-
GIT_PAGER: "cat",
|
|
2952
|
-
PAGER: "cat",
|
|
2953
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
2954
|
-
};
|
|
2955
|
-
async function detectTeamStatus(projectRoot) {
|
|
2956
|
-
try {
|
|
2957
|
-
await access2(path10.join(projectRoot, ".git"));
|
|
2958
|
-
const inside = await runGit(projectRoot, [
|
|
2959
|
-
"rev-parse",
|
|
2960
|
-
"--is-inside-work-tree"
|
|
2961
|
-
]);
|
|
2962
|
-
if (inside.trim() !== "true") return NO_TEAM;
|
|
2963
|
-
const [allEmails, recentEmails, remotes] = await Promise.all([
|
|
2964
|
-
runGit(projectRoot, ["log", "--all", "--format=%ae"]),
|
|
2965
|
-
runGit(projectRoot, ["log", "--since=30 days ago", "--format=%ae"]),
|
|
2966
|
-
runGit(projectRoot, ["remote", "-v"])
|
|
2868
|
+
async function checkPlatformReadiness(rootDir, platform) {
|
|
2869
|
+
if (platform === "claude-code") {
|
|
2870
|
+
const [hasSoloSkill, registered, hasHookFiles] = await Promise.all([
|
|
2871
|
+
fileMatches(
|
|
2872
|
+
path8.join(rootDir, ".claude", "skills", "solo", "SKILL.md"),
|
|
2873
|
+
(content) => isGeneratedClaudeSkill(content, "solo")
|
|
2874
|
+
),
|
|
2875
|
+
claudeHooksRegistered(rootDir),
|
|
2876
|
+
pathsExist([
|
|
2877
|
+
path8.join(rootDir, ".mancode", "hooks", "session-start.mjs"),
|
|
2878
|
+
path8.join(rootDir, ".mancode", "hooks", "user-prompt-submit.mjs")
|
|
2879
|
+
])
|
|
2967
2880
|
]);
|
|
2968
|
-
const
|
|
2969
|
-
const
|
|
2970
|
-
const
|
|
2881
|
+
const present = hasSoloSkill && registered && hasHookFiles;
|
|
2882
|
+
const minimal = await isPlatformMinimal(rootDir, "claude-code");
|
|
2883
|
+
const fullContentReady = minimal ? true : await claudeFullContentReady(rootDir);
|
|
2971
2884
|
return {
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
};
|
|
2885
|
+
present,
|
|
2886
|
+
ready: present && fullContentReady,
|
|
2887
|
+
target: ".claude/",
|
|
2888
|
+
readyDetail: present && fullContentReady ? minimal ? "solo skill and hooks ready" : "skills, agents, and hooks ready" : "skills, agents, hook files, or hook registration are incomplete"
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
if (platform === "cursor") {
|
|
2892
|
+
const hasCoreRules = await allManagedSkills(
|
|
2893
|
+
MANCODE_CURSOR_CORE_RULE_FILES.map(
|
|
2894
|
+
(file) => path8.join(rootDir, ".cursor", "rules", file)
|
|
2895
|
+
),
|
|
2896
|
+
[CURSOR_RULE_MANAGED_MARKER]
|
|
2897
|
+
);
|
|
2898
|
+
if (await isPlatformMinimal(rootDir, "cursor")) {
|
|
2899
|
+
return {
|
|
2900
|
+
present: hasCoreRules,
|
|
2901
|
+
ready: hasCoreRules,
|
|
2902
|
+
target: ".cursor/rules/",
|
|
2903
|
+
readyDetail: "mancode core rules present"
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
const [hasRules, hasCommands] = await Promise.all([
|
|
2907
|
+
allManagedSkills(
|
|
2908
|
+
MANCODE_CURSOR_RULE_FILES.map(
|
|
2909
|
+
(file) => path8.join(rootDir, ".cursor", "rules", file)
|
|
2910
|
+
),
|
|
2911
|
+
[CURSOR_RULE_MANAGED_MARKER]
|
|
2912
|
+
),
|
|
2913
|
+
allManagedSkills(
|
|
2914
|
+
MODE_NAMES.map(
|
|
2915
|
+
(mode) => path8.join(rootDir, ".cursor", "commands", `${mode}.md`)
|
|
2916
|
+
),
|
|
2917
|
+
[MODE_FILE_MANAGED_MARKER]
|
|
2918
|
+
)
|
|
2919
|
+
]);
|
|
2920
|
+
return {
|
|
2921
|
+
present: hasCoreRules,
|
|
2922
|
+
ready: hasRules && hasCommands,
|
|
2923
|
+
target: ".cursor/rules/ + .cursor/commands/",
|
|
2924
|
+
readyDetail: hasRules && hasCommands ? "mancode rules and commands present" : "mode rules or commands are missing, incomplete, or user-authored"
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
if (platform === "codex") {
|
|
2928
|
+
const hasBlock2 = await fileHasManagedBlock(path8.join(rootDir, "AGENTS.md"));
|
|
2929
|
+
if (!hasBlock2) {
|
|
2930
|
+
return {
|
|
2931
|
+
present: false,
|
|
2932
|
+
ready: false,
|
|
2933
|
+
target: "AGENTS.md",
|
|
2934
|
+
readyDetail: "managed block missing"
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
if (await isPlatformMinimal(rootDir, "codex")) {
|
|
2938
|
+
return {
|
|
2939
|
+
present: true,
|
|
2940
|
+
ready: true,
|
|
2941
|
+
target: "AGENTS.md",
|
|
2942
|
+
readyDetail: "managed block present"
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
const skillsDir = path8.join(rootDir, ".agents", "skills");
|
|
2946
|
+
const hasSkills = await allManagedSkills(
|
|
2947
|
+
MODE_NAMES.map((mode) => path8.join(skillsDir, mode, "SKILL.md")),
|
|
2948
|
+
MANCODE_AGENT_SKILL_MARKERS
|
|
2949
|
+
);
|
|
2950
|
+
return {
|
|
2951
|
+
present: true,
|
|
2952
|
+
ready: hasSkills,
|
|
2953
|
+
target: "AGENTS.md + .agents/skills/",
|
|
2954
|
+
readyDetail: hasSkills ? "managed block and skills present" : "mode skills are missing, incomplete, or user-authored"
|
|
2955
|
+
};
|
|
2956
|
+
}
|
|
2957
|
+
if (platform === "zcode") {
|
|
2958
|
+
const hasBlock2 = await fileHasManagedBlock(
|
|
2959
|
+
path8.join(rootDir, "AGENTS.md"),
|
|
2960
|
+
ZCODE_MANCODE_START_MARKER,
|
|
2961
|
+
ZCODE_MANCODE_END_MARKER
|
|
2962
|
+
);
|
|
2963
|
+
if (!hasBlock2) {
|
|
2964
|
+
return {
|
|
2965
|
+
present: false,
|
|
2966
|
+
ready: false,
|
|
2967
|
+
target: "AGENTS.md",
|
|
2968
|
+
readyDetail: "managed block missing"
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
if (await isPlatformMinimal(rootDir, "zcode")) {
|
|
2972
|
+
return {
|
|
2973
|
+
present: true,
|
|
2974
|
+
ready: true,
|
|
2975
|
+
target: "AGENTS.md",
|
|
2976
|
+
readyDetail: "managed block present"
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
const skillsDir = path8.join(rootDir, ".agents", "skills");
|
|
2980
|
+
const hasSkills = await allManagedSkills(
|
|
2981
|
+
MODE_NAMES.map((mode) => path8.join(skillsDir, mode, "SKILL.md")),
|
|
2982
|
+
MANCODE_AGENT_SKILL_MARKERS
|
|
2983
|
+
);
|
|
2984
|
+
return {
|
|
2985
|
+
present: true,
|
|
2986
|
+
ready: hasSkills,
|
|
2987
|
+
target: "AGENTS.md + .agents/skills/",
|
|
2988
|
+
readyDetail: hasSkills ? "managed block and skills present" : "mode skills are missing, incomplete, or user-authored"
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
const hasBlock = await fileHasManagedBlock(
|
|
2992
|
+
path8.join(rootDir, ".github", "copilot-instructions.md")
|
|
2993
|
+
);
|
|
2994
|
+
if (!hasBlock) {
|
|
2995
|
+
return {
|
|
2996
|
+
present: false,
|
|
2997
|
+
ready: false,
|
|
2998
|
+
target: ".github/copilot-instructions.md",
|
|
2999
|
+
readyDetail: "managed block missing"
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
if (!await isPlatformMinimal(rootDir, "copilot")) {
|
|
3003
|
+
const promptsDir = path8.join(rootDir, ".github", "prompts");
|
|
3004
|
+
const hasPrompts = await allManagedSkills(
|
|
3005
|
+
MODE_NAMES.map((mode) => path8.join(promptsDir, `${mode}.prompt.md`)),
|
|
3006
|
+
[MODE_FILE_MANAGED_MARKER]
|
|
3007
|
+
);
|
|
3008
|
+
return {
|
|
3009
|
+
present: true,
|
|
3010
|
+
ready: hasPrompts,
|
|
3011
|
+
target: ".github/copilot-instructions.md + .github/prompts/",
|
|
3012
|
+
readyDetail: hasPrompts ? "managed block and prompts present" : "mode prompts are missing, incomplete, or user-authored"
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
return {
|
|
3016
|
+
present: true,
|
|
3017
|
+
ready: true,
|
|
3018
|
+
target: ".github/copilot-instructions.md",
|
|
3019
|
+
readyDetail: "managed block present"
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
function describeStatus(installed, readiness) {
|
|
3023
|
+
if (installed && readiness.present) return readiness.readyDetail;
|
|
3024
|
+
if (!installed && readiness.present) {
|
|
3025
|
+
return "target files present but platform is not recorded in config";
|
|
3026
|
+
}
|
|
3027
|
+
return "missing generated files or registration";
|
|
3028
|
+
}
|
|
3029
|
+
async function allManagedSkills(paths, markers) {
|
|
3030
|
+
const results = await Promise.all(
|
|
3031
|
+
paths.map((filePath) => fileHasAnyMarker(filePath, markers))
|
|
3032
|
+
);
|
|
3033
|
+
return results.every(Boolean);
|
|
3034
|
+
}
|
|
3035
|
+
async function claudeFullContentReady(rootDir) {
|
|
3036
|
+
const skillChecks = MVP2_SKILLS.map(
|
|
3037
|
+
(skill) => fileMatches(
|
|
3038
|
+
path8.join(rootDir, ".claude", "skills", skill.name, "SKILL.md"),
|
|
3039
|
+
(content) => isGeneratedClaudeSkill(content, skill.name)
|
|
3040
|
+
)
|
|
3041
|
+
);
|
|
3042
|
+
const agentChecks = ALL_AGENTS.map(
|
|
3043
|
+
(agent) => fileMatches(
|
|
3044
|
+
path8.join(rootDir, ".claude", "agents", `${agent.name}.md`),
|
|
3045
|
+
(content) => isGeneratedClaudeAgent(content, agent.name)
|
|
3046
|
+
)
|
|
3047
|
+
);
|
|
3048
|
+
const results = await Promise.all([...skillChecks, ...agentChecks]);
|
|
3049
|
+
return results.every(Boolean);
|
|
3050
|
+
}
|
|
3051
|
+
async function fileMatches(filePath, predicate) {
|
|
3052
|
+
try {
|
|
3053
|
+
return predicate(await fs.readFile(filePath, "utf-8"));
|
|
3054
|
+
} catch {
|
|
3055
|
+
return false;
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
async function pathsExist(paths) {
|
|
3059
|
+
return (await Promise.all(paths.map(pathExists2))).every(Boolean);
|
|
3060
|
+
}
|
|
3061
|
+
async function fileHasAnyMarker(filePath, needles) {
|
|
3062
|
+
try {
|
|
3063
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
3064
|
+
return needles.some((needle) => content.includes(needle));
|
|
3065
|
+
} catch {
|
|
3066
|
+
return false;
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
async function isPlatformMinimal(rootDir, platform) {
|
|
3070
|
+
try {
|
|
3071
|
+
const raw = await fs.readFile(
|
|
3072
|
+
path8.join(rootDir, ".mancode", "config.json"),
|
|
3073
|
+
"utf-8"
|
|
3074
|
+
);
|
|
3075
|
+
const config = JSON.parse(raw);
|
|
3076
|
+
if (!isRecord3(config.platformOptions)) return false;
|
|
3077
|
+
const options = config.platformOptions[platform];
|
|
3078
|
+
return isRecord3(options) && options.minimal === true;
|
|
3079
|
+
} catch {
|
|
3080
|
+
return false;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
async function fileHasManagedBlock(filePath, startMarker = DEFAULT_MANCODE_START_MARKER, endMarker = DEFAULT_MANCODE_END_MARKER) {
|
|
3084
|
+
try {
|
|
3085
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
3086
|
+
return hasManagedBlock(content, startMarker, endMarker);
|
|
3087
|
+
} catch {
|
|
3088
|
+
return false;
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
async function claudeHooksRegistered(rootDir) {
|
|
3092
|
+
try {
|
|
3093
|
+
const raw = await fs.readFile(
|
|
3094
|
+
path8.join(rootDir, ".claude", "settings.json"),
|
|
3095
|
+
"utf-8"
|
|
3096
|
+
);
|
|
3097
|
+
const settings = JSON.parse(raw);
|
|
3098
|
+
return hooksRegistered(settings);
|
|
3099
|
+
} catch {
|
|
3100
|
+
return false;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
function hooksRegistered(settings) {
|
|
3104
|
+
if (!isRecord3(settings)) return false;
|
|
3105
|
+
const hooks = settings.hooks;
|
|
3106
|
+
if (!isRecord3(hooks)) return false;
|
|
3107
|
+
return hasHookCommand(hooks.SessionStart, ".mancode/hooks/session-start.mjs") && hasHookCommand(
|
|
3108
|
+
hooks.UserPromptSubmit,
|
|
3109
|
+
".mancode/hooks/user-prompt-submit.mjs"
|
|
3110
|
+
);
|
|
3111
|
+
}
|
|
3112
|
+
function hasHookCommand(value, needle) {
|
|
3113
|
+
if (!Array.isArray(value)) return false;
|
|
3114
|
+
return value.some((group) => {
|
|
3115
|
+
if (!isRecord3(group) || !Array.isArray(group.hooks)) return false;
|
|
3116
|
+
return group.hooks.some((hook) => {
|
|
3117
|
+
if (!isRecord3(hook) || typeof hook.command !== "string") return false;
|
|
3118
|
+
return hook.command.includes(needle);
|
|
3119
|
+
});
|
|
3120
|
+
});
|
|
3121
|
+
}
|
|
3122
|
+
async function pathExists2(p) {
|
|
3123
|
+
try {
|
|
3124
|
+
await fs.access(p);
|
|
3125
|
+
return true;
|
|
3126
|
+
} catch {
|
|
3127
|
+
return false;
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
function isRecord3(value) {
|
|
3131
|
+
return typeof value === "object" && value !== null;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
// src/installers/codex.ts
|
|
3135
|
+
import { writeFile as writeFile7 } from "fs/promises";
|
|
3136
|
+
import path9 from "path";
|
|
3137
|
+
async function installCodex(projectRoot, options) {
|
|
3138
|
+
await installMancodeCore(projectRoot);
|
|
3139
|
+
const agentsPath = path9.join(projectRoot, "AGENTS.md");
|
|
3140
|
+
const existing = await readTextIfExists(agentsPath);
|
|
3141
|
+
const sharedContent = await generateSharedContent(projectRoot, {
|
|
3142
|
+
platform: "codex",
|
|
3143
|
+
displayName: "Codex (ChatGPT desktop/CLI)",
|
|
3144
|
+
capabilities: {
|
|
3145
|
+
slashCommands: "partial",
|
|
3146
|
+
subagents: false,
|
|
3147
|
+
hooks: false,
|
|
3148
|
+
skills: "agents-skills"
|
|
3149
|
+
},
|
|
3150
|
+
minimal: options.minimal,
|
|
3151
|
+
techStack: options.techStack,
|
|
3152
|
+
uiLibrary: options.uiLibrary,
|
|
3153
|
+
projectProfile: options.projectProfile
|
|
3154
|
+
});
|
|
3155
|
+
const block = [
|
|
3156
|
+
DEFAULT_MANCODE_START_MARKER,
|
|
3157
|
+
"<!-- Managed by mancode. Do not edit this block manually. -->",
|
|
3158
|
+
"",
|
|
3159
|
+
"# mancode Configuration",
|
|
3160
|
+
"",
|
|
3161
|
+
sharedContent.trim(),
|
|
3162
|
+
DEFAULT_MANCODE_END_MARKER
|
|
3163
|
+
].join("\n");
|
|
3164
|
+
await writeFile7(agentsPath, replaceManagedBlock(existing, block), "utf-8");
|
|
3165
|
+
await installCodexSkills(projectRoot, options.minimal ?? false);
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
// src/installers/copilot.ts
|
|
3169
|
+
import { mkdir as mkdir6, writeFile as writeFile8 } from "fs/promises";
|
|
3170
|
+
import path10 from "path";
|
|
3171
|
+
async function installCopilot(projectRoot, options) {
|
|
3172
|
+
await installMancodeCore(projectRoot);
|
|
3173
|
+
const githubDir = path10.join(projectRoot, ".github");
|
|
3174
|
+
await mkdir6(githubDir, { recursive: true });
|
|
3175
|
+
const instructionsPath = path10.join(githubDir, "copilot-instructions.md");
|
|
3176
|
+
const existing = await readTextIfExists(instructionsPath);
|
|
3177
|
+
const sharedContent = await generateSharedContent(projectRoot, {
|
|
3178
|
+
platform: "copilot",
|
|
3179
|
+
displayName: "GitHub Copilot",
|
|
3180
|
+
capabilities: {
|
|
3181
|
+
slashCommands: "none",
|
|
3182
|
+
subagents: false,
|
|
3183
|
+
hooks: false,
|
|
3184
|
+
skills: "instructions"
|
|
3185
|
+
},
|
|
3186
|
+
minimal: true,
|
|
3187
|
+
techStack: options.techStack,
|
|
3188
|
+
uiLibrary: options.uiLibrary,
|
|
3189
|
+
projectProfile: options.projectProfile
|
|
3190
|
+
});
|
|
3191
|
+
const sections = [
|
|
3192
|
+
DEFAULT_MANCODE_START_MARKER,
|
|
3193
|
+
"<!-- Managed by mancode. Do not edit this block manually. -->",
|
|
3194
|
+
"",
|
|
3195
|
+
"# mancode for GitHub Copilot",
|
|
3196
|
+
"",
|
|
3197
|
+
sharedContent.trim()
|
|
3198
|
+
];
|
|
3199
|
+
if (!options.minimal) {
|
|
3200
|
+
sections.push("", renderCopilotPromptConventions());
|
|
3201
|
+
}
|
|
3202
|
+
sections.push(DEFAULT_MANCODE_END_MARKER);
|
|
3203
|
+
await writeFile8(
|
|
3204
|
+
instructionsPath,
|
|
3205
|
+
replaceManagedBlock(existing, sections.join("\n")),
|
|
3206
|
+
"utf-8"
|
|
3207
|
+
);
|
|
3208
|
+
await installCopilotPrompts(projectRoot, options.minimal ?? false);
|
|
3209
|
+
}
|
|
3210
|
+
function renderCopilotPromptConventions() {
|
|
3211
|
+
return [
|
|
3212
|
+
"## mancode Prompt Conventions",
|
|
3213
|
+
"",
|
|
3214
|
+
"GitHub Copilot does not provide native mancode slash commands, hooks, or isolated subagents. Treat these names as user prompt conventions:",
|
|
3215
|
+
"",
|
|
3216
|
+
"- man: use progressive research, planning, implementation, verification, and bounded risk-based review.",
|
|
3217
|
+
"- manba: diagnose bugs and validate real user flows or regressions.",
|
|
3218
|
+
"- manteam: read team memory and write handoff-friendly summaries.",
|
|
3219
|
+
"- manps: prefer `mancode manps [area]` before cleanup.",
|
|
3220
|
+
"- mansolo: exit any active mode and return to default solo behavior.",
|
|
3221
|
+
"",
|
|
3222
|
+
"These correspond to prompt files in `.github/prompts/`. Select the matching prompt to activate a mode."
|
|
3223
|
+
].join("\n");
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
// src/installers/registry.ts
|
|
3227
|
+
var PLATFORM_INSTALLERS = {
|
|
3228
|
+
"claude-code": {
|
|
3229
|
+
name: "claude-code",
|
|
3230
|
+
displayName: "Claude Code",
|
|
3231
|
+
capabilities: {
|
|
3232
|
+
slashCommands: "native",
|
|
3233
|
+
subagents: true,
|
|
3234
|
+
hooks: true,
|
|
3235
|
+
skills: "native"
|
|
3236
|
+
},
|
|
3237
|
+
install: installClaudeCode
|
|
3238
|
+
},
|
|
3239
|
+
cursor: {
|
|
3240
|
+
name: "cursor",
|
|
3241
|
+
displayName: "Cursor",
|
|
3242
|
+
capabilities: {
|
|
3243
|
+
slashCommands: "native",
|
|
3244
|
+
subagents: false,
|
|
3245
|
+
hooks: false,
|
|
3246
|
+
skills: "rules"
|
|
3247
|
+
},
|
|
3248
|
+
install: installCursor
|
|
3249
|
+
},
|
|
3250
|
+
codex: {
|
|
3251
|
+
name: "codex",
|
|
3252
|
+
displayName: "Codex (ChatGPT desktop/CLI)",
|
|
3253
|
+
capabilities: {
|
|
3254
|
+
slashCommands: "partial",
|
|
3255
|
+
subagents: false,
|
|
3256
|
+
hooks: false,
|
|
3257
|
+
skills: "agents-skills"
|
|
3258
|
+
},
|
|
3259
|
+
install: installCodex
|
|
3260
|
+
},
|
|
3261
|
+
copilot: {
|
|
3262
|
+
name: "copilot",
|
|
3263
|
+
displayName: "GitHub Copilot",
|
|
3264
|
+
capabilities: {
|
|
3265
|
+
slashCommands: "none",
|
|
3266
|
+
subagents: false,
|
|
3267
|
+
hooks: false,
|
|
3268
|
+
skills: "instructions"
|
|
3269
|
+
},
|
|
3270
|
+
install: installCopilot
|
|
3271
|
+
},
|
|
3272
|
+
zcode: {
|
|
3273
|
+
name: "zcode",
|
|
3274
|
+
displayName: "ZCode",
|
|
3275
|
+
capabilities: {
|
|
3276
|
+
slashCommands: "partial",
|
|
3277
|
+
subagents: false,
|
|
3278
|
+
hooks: false,
|
|
3279
|
+
skills: "agents-skills"
|
|
3280
|
+
},
|
|
3281
|
+
install: installZcode
|
|
3282
|
+
}
|
|
3283
|
+
};
|
|
3284
|
+
function getPlatformInstaller(platform) {
|
|
3285
|
+
if (!isPlatformName(platform)) return null;
|
|
3286
|
+
return PLATFORM_INSTALLERS[platform];
|
|
3287
|
+
}
|
|
3288
|
+
function getPlatformInstallers() {
|
|
3289
|
+
return Object.values(PLATFORM_INSTALLERS);
|
|
3290
|
+
}
|
|
3291
|
+
function formatPlatformName(platform) {
|
|
3292
|
+
return getPlatformInstaller(platform)?.displayName ?? platform;
|
|
3293
|
+
}
|
|
3294
|
+
function isPlatformName(platform) {
|
|
3295
|
+
return Object.prototype.hasOwnProperty.call(PLATFORM_INSTALLERS, platform);
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
// src/system/detect-team.ts
|
|
3299
|
+
import { execFile } from "child_process";
|
|
3300
|
+
import { access as access2 } from "fs/promises";
|
|
3301
|
+
import path11 from "path";
|
|
3302
|
+
import process2 from "process";
|
|
3303
|
+
import { promisify } from "util";
|
|
3304
|
+
var execFileAsync = promisify(execFile);
|
|
3305
|
+
var NO_TEAM = {
|
|
3306
|
+
isTeam: false,
|
|
3307
|
+
contributors: 1,
|
|
3308
|
+
recentActive: 1,
|
|
3309
|
+
hasRemote: false
|
|
3310
|
+
};
|
|
3311
|
+
var REMOTE_PATTERN = /github\.com|gitlab\.com|bitbucket\.org/;
|
|
3312
|
+
var GIT_ENV = {
|
|
3313
|
+
...process2.env,
|
|
3314
|
+
GIT_PAGER: "cat",
|
|
3315
|
+
PAGER: "cat",
|
|
3316
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
3317
|
+
};
|
|
3318
|
+
async function detectTeamStatus(projectRoot) {
|
|
3319
|
+
try {
|
|
3320
|
+
await access2(path11.join(projectRoot, ".git"));
|
|
3321
|
+
const inside = await runGit(projectRoot, [
|
|
3322
|
+
"rev-parse",
|
|
3323
|
+
"--is-inside-work-tree"
|
|
3324
|
+
]);
|
|
3325
|
+
if (inside.trim() !== "true") return NO_TEAM;
|
|
3326
|
+
const [allEmails, recentEmails, remotes] = await Promise.all([
|
|
3327
|
+
runGit(projectRoot, ["log", "--all", "--format=%ae"]),
|
|
3328
|
+
runGit(projectRoot, ["log", "--since=30 days ago", "--format=%ae"]),
|
|
3329
|
+
runGit(projectRoot, ["remote", "-v"])
|
|
3330
|
+
]);
|
|
3331
|
+
const contributors = countUniqueLines(allEmails);
|
|
3332
|
+
const recentActive = countUniqueLines(recentEmails);
|
|
3333
|
+
const hasRemote = REMOTE_PATTERN.test(remotes);
|
|
3334
|
+
return {
|
|
3335
|
+
isTeam: contributors > 1 && hasRemote && recentActive > 1,
|
|
3336
|
+
contributors: contributors || 1,
|
|
3337
|
+
recentActive: recentActive || 1,
|
|
3338
|
+
hasRemote
|
|
3339
|
+
};
|
|
2977
3340
|
} catch {
|
|
2978
3341
|
return NO_TEAM;
|
|
2979
3342
|
}
|
|
@@ -3012,8 +3375,8 @@ async function detectSystemDeps(env = process3.env) {
|
|
|
3012
3375
|
|
|
3013
3376
|
// src/system/init-onboarding.ts
|
|
3014
3377
|
import { execFileSync } from "child_process";
|
|
3015
|
-
import { promises as
|
|
3016
|
-
import
|
|
3378
|
+
import { promises as fs2 } from "fs";
|
|
3379
|
+
import path12 from "path";
|
|
3017
3380
|
import process4 from "process";
|
|
3018
3381
|
import { stdin, stdout } from "process";
|
|
3019
3382
|
import { createInterface } from "readline/promises";
|
|
@@ -3118,7 +3481,7 @@ async function detectPlatformHints(rootDir, environment = process4.env) {
|
|
|
3118
3481
|
hints.add("copilot");
|
|
3119
3482
|
const exists = async (relative) => {
|
|
3120
3483
|
try {
|
|
3121
|
-
await
|
|
3484
|
+
await fs2.access(path12.join(rootDir, relative));
|
|
3122
3485
|
return true;
|
|
3123
3486
|
} catch {
|
|
3124
3487
|
return false;
|
|
@@ -3194,7 +3557,7 @@ function createTerminalPrompter() {
|
|
|
3194
3557
|
|
|
3195
3558
|
// src/system/project-profile.ts
|
|
3196
3559
|
import { readFile as readFile7, readdir as readdir3, stat as stat2 } from "fs/promises";
|
|
3197
|
-
import
|
|
3560
|
+
import path13 from "path";
|
|
3198
3561
|
var PROJECT_MANIFESTS = [
|
|
3199
3562
|
"package.json",
|
|
3200
3563
|
"pyproject.toml",
|
|
@@ -3227,12 +3590,12 @@ async function detectProjectProfile(projectRoot) {
|
|
|
3227
3590
|
"data",
|
|
3228
3591
|
"notebooks"
|
|
3229
3592
|
]);
|
|
3230
|
-
const packageJson = await readJson2(
|
|
3593
|
+
const packageJson = await readJson2(path13.join(projectRoot, "package.json"));
|
|
3231
3594
|
const [pubspec, pythonManifests] = await Promise.all([
|
|
3232
|
-
readText(
|
|
3595
|
+
readText(path13.join(projectRoot, "pubspec.yaml")),
|
|
3233
3596
|
Promise.all([
|
|
3234
|
-
readText(
|
|
3235
|
-
readText(
|
|
3597
|
+
readText(path13.join(projectRoot, "pyproject.toml")),
|
|
3598
|
+
readText(path13.join(projectRoot, "requirements.txt"))
|
|
3236
3599
|
]).then((parts) => parts.filter(Boolean).join("\n"))
|
|
3237
3600
|
]);
|
|
3238
3601
|
const deps = Object.keys({
|
|
@@ -3241,7 +3604,7 @@ async function detectProjectProfile(projectRoot) {
|
|
|
3241
3604
|
...packageJson?.peerDependencies
|
|
3242
3605
|
});
|
|
3243
3606
|
const flutter = isFlutterProject(pubspec);
|
|
3244
|
-
const shadcn = entries.includes("components.json") || deps.some((dep) => dep.startsWith("@radix-ui/")) && await isDirectory(
|
|
3607
|
+
const shadcn = entries.includes("components.json") || deps.some((dep) => dep.startsWith("@radix-ui/")) && await isDirectory(path13.join(projectRoot, "src", "components", "ui"));
|
|
3245
3608
|
const languages = inferLanguages(manifests, entries);
|
|
3246
3609
|
const frameworks = inferFrameworks(
|
|
3247
3610
|
deps,
|
|
@@ -3284,7 +3647,7 @@ async function existingDirs(root, candidates) {
|
|
|
3284
3647
|
const found = [];
|
|
3285
3648
|
for (const candidate of candidates) {
|
|
3286
3649
|
try {
|
|
3287
|
-
if ((await stat2(
|
|
3650
|
+
if ((await stat2(path13.join(root, candidate))).isDirectory())
|
|
3288
3651
|
found.push(candidate);
|
|
3289
3652
|
} catch {
|
|
3290
3653
|
}
|
|
@@ -3458,8 +3821,8 @@ function isFlutterProject(pubspec) {
|
|
|
3458
3821
|
}
|
|
3459
3822
|
|
|
3460
3823
|
// src/system/scan-aesthetics.ts
|
|
3461
|
-
import { promises as
|
|
3462
|
-
import
|
|
3824
|
+
import { promises as fs3 } from "fs";
|
|
3825
|
+
import path14 from "path";
|
|
3463
3826
|
var MAX_COMPONENT_SCAN_DEPTH = 12;
|
|
3464
3827
|
var MAX_COMPONENT_FILES = 2e3;
|
|
3465
3828
|
async function scanAesthetics(projectRoot, uiLibrary = null) {
|
|
@@ -3472,7 +3835,7 @@ async function scanAesthetics(projectRoot, uiLibrary = null) {
|
|
|
3472
3835
|
const cssScan = await scanCssVariables(projectRoot);
|
|
3473
3836
|
if (configResult) {
|
|
3474
3837
|
sourceFiles.push(configResult.relPath);
|
|
3475
|
-
const content = await
|
|
3838
|
+
const content = await fs3.readFile(configResult.absPath, "utf-8");
|
|
3476
3839
|
const inspectableContent = stripJsComments(content);
|
|
3477
3840
|
const themeBlock = findKeyBlock(inspectableContent, "theme");
|
|
3478
3841
|
if (themeBlock) {
|
|
@@ -3495,7 +3858,7 @@ async function scanAesthetics(projectRoot, uiLibrary = null) {
|
|
|
3495
3858
|
} else if (await hasTailwindDep(projectRoot) || uiLibrary) {
|
|
3496
3859
|
matchLevel = "low";
|
|
3497
3860
|
}
|
|
3498
|
-
if (uiLibrary && await
|
|
3861
|
+
if (uiLibrary && await pathExists3(path14.join(projectRoot, "package.json"))) {
|
|
3499
3862
|
sourceFiles.push("package.json");
|
|
3500
3863
|
}
|
|
3501
3864
|
sourceFiles.push(...cssScan.sourceFiles);
|
|
@@ -3520,8 +3883,8 @@ async function findTailwindConfig(projectRoot) {
|
|
|
3520
3883
|
"tailwind.config.mjs"
|
|
3521
3884
|
];
|
|
3522
3885
|
for (const name of candidates) {
|
|
3523
|
-
const absPath =
|
|
3524
|
-
if (await
|
|
3886
|
+
const absPath = path14.join(projectRoot, name);
|
|
3887
|
+
if (await pathExists3(absPath)) {
|
|
3525
3888
|
return { absPath, relPath: name };
|
|
3526
3889
|
}
|
|
3527
3890
|
}
|
|
@@ -3747,8 +4110,8 @@ async function scanComponents(projectRoot) {
|
|
|
3747
4110
|
const names = /* @__PURE__ */ new Set();
|
|
3748
4111
|
let visitedFiles = 0;
|
|
3749
4112
|
for (const relRoot of roots) {
|
|
3750
|
-
const absRoot =
|
|
3751
|
-
if (!await
|
|
4113
|
+
const absRoot = path14.join(projectRoot, relRoot);
|
|
4114
|
+
if (!await pathExists3(absRoot)) continue;
|
|
3752
4115
|
visitedFiles = await collectComponentNames(absRoot, names, 0, visitedFiles);
|
|
3753
4116
|
if (visitedFiles >= MAX_COMPONENT_FILES) break;
|
|
3754
4117
|
}
|
|
@@ -3761,16 +4124,16 @@ async function collectComponentNames(dir, names, depth, visitedFiles) {
|
|
|
3761
4124
|
let fileCount = visitedFiles;
|
|
3762
4125
|
let entries;
|
|
3763
4126
|
try {
|
|
3764
|
-
entries = await
|
|
4127
|
+
entries = await fs3.readdir(dir);
|
|
3765
4128
|
} catch {
|
|
3766
4129
|
return fileCount;
|
|
3767
4130
|
}
|
|
3768
4131
|
for (const entry of entries) {
|
|
3769
4132
|
if (fileCount >= MAX_COMPONENT_FILES) return fileCount;
|
|
3770
|
-
const abs =
|
|
4133
|
+
const abs = path14.join(dir, entry);
|
|
3771
4134
|
let info;
|
|
3772
4135
|
try {
|
|
3773
|
-
info = await
|
|
4136
|
+
info = await fs3.lstat(abs);
|
|
3774
4137
|
} catch {
|
|
3775
4138
|
continue;
|
|
3776
4139
|
}
|
|
@@ -3788,7 +4151,7 @@ async function collectComponentNames(dir, names, depth, visitedFiles) {
|
|
|
3788
4151
|
""
|
|
3789
4152
|
);
|
|
3790
4153
|
if (base === "index") {
|
|
3791
|
-
base =
|
|
4154
|
+
base = path14.basename(dir);
|
|
3792
4155
|
}
|
|
3793
4156
|
if (base.startsWith(".")) continue;
|
|
3794
4157
|
const componentName = toPascalCase(base);
|
|
@@ -3810,9 +4173,9 @@ async function scanCssVariables(projectRoot) {
|
|
|
3810
4173
|
const variables = {};
|
|
3811
4174
|
const sourceFiles = [];
|
|
3812
4175
|
for (const relPath of candidates) {
|
|
3813
|
-
const absPath =
|
|
3814
|
-
if (!await
|
|
3815
|
-
const content = await
|
|
4176
|
+
const absPath = path14.join(projectRoot, relPath);
|
|
4177
|
+
if (!await pathExists3(absPath)) continue;
|
|
4178
|
+
const content = await fs3.readFile(absPath, "utf-8");
|
|
3816
4179
|
const found = extractCssVariables(content);
|
|
3817
4180
|
if (Object.keys(found).length === 0) continue;
|
|
3818
4181
|
Object.assign(variables, found);
|
|
@@ -3855,8 +4218,8 @@ function toPascalCase(value) {
|
|
|
3855
4218
|
}
|
|
3856
4219
|
async function hasTailwindDep(projectRoot) {
|
|
3857
4220
|
try {
|
|
3858
|
-
const raw = await
|
|
3859
|
-
|
|
4221
|
+
const raw = await fs3.readFile(
|
|
4222
|
+
path14.join(projectRoot, "package.json"),
|
|
3860
4223
|
"utf-8"
|
|
3861
4224
|
);
|
|
3862
4225
|
const pkg = JSON.parse(raw);
|
|
@@ -3870,9 +4233,9 @@ async function hasTailwindDep(projectRoot) {
|
|
|
3870
4233
|
function escapeRegex(s) {
|
|
3871
4234
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3872
4235
|
}
|
|
3873
|
-
async function
|
|
4236
|
+
async function pathExists3(p) {
|
|
3874
4237
|
try {
|
|
3875
|
-
await
|
|
4238
|
+
await fs3.access(p);
|
|
3876
4239
|
return true;
|
|
3877
4240
|
} catch {
|
|
3878
4241
|
return false;
|
|
@@ -3887,9 +4250,9 @@ var EXIT_USER_CANCEL = 3;
|
|
|
3887
4250
|
var EXIT_INIT_FAILED = 5;
|
|
3888
4251
|
var DEFAULT_INIT_PLATFORM = "claude-code";
|
|
3889
4252
|
async function init(rootDir = process5.cwd(), options = {}) {
|
|
3890
|
-
const mancodeDir =
|
|
3891
|
-
const stateFile =
|
|
3892
|
-
const wasInitialized = await
|
|
4253
|
+
const mancodeDir = path15.join(rootDir, ".mancode");
|
|
4254
|
+
const stateFile = path15.join(mancodeDir, "state.json");
|
|
4255
|
+
const wasInitialized = await pathExists4(stateFile);
|
|
3893
4256
|
let mutationSnapshots = [];
|
|
3894
4257
|
let directorySnapshots = [];
|
|
3895
4258
|
const locale = detectInitLocale(options.lang);
|
|
@@ -3925,7 +4288,7 @@ async function init(rootDir = process5.cwd(), options = {}) {
|
|
|
3925
4288
|
)
|
|
3926
4289
|
);
|
|
3927
4290
|
}
|
|
3928
|
-
if (!await
|
|
4291
|
+
if (!await pathExists4(rootDir)) {
|
|
3929
4292
|
console.error(
|
|
3930
4293
|
localize(
|
|
3931
4294
|
locale,
|
|
@@ -3935,26 +4298,28 @@ async function init(rootDir = process5.cwd(), options = {}) {
|
|
|
3935
4298
|
);
|
|
3936
4299
|
return EXIT_NOT_A_PROJECT_DIR;
|
|
3937
4300
|
}
|
|
3938
|
-
const isGitRepo = await
|
|
4301
|
+
const isGitRepo = await pathExists4(path15.join(rootDir, ".git"));
|
|
3939
4302
|
const hasManifest = await hasProjectManifest(rootDir);
|
|
3940
4303
|
let isGenericProject = false;
|
|
3941
4304
|
if (!isGitRepo && !hasManifest) {
|
|
3942
4305
|
const genericSafety = await canInitializeGenericProject(rootDir);
|
|
3943
|
-
if (!genericSafety.ok) {
|
|
4306
|
+
if (!genericSafety.ok && !(wasInitialized && genericSafety.reason === "nonempty")) {
|
|
3944
4307
|
printNotProjectDirectory(rootDir, locale, genericSafety.reason);
|
|
3945
4308
|
return EXIT_NOT_A_PROJECT_DIR;
|
|
3946
4309
|
}
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
if (
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
4310
|
+
if (!wasInitialized) {
|
|
4311
|
+
const prompter = options.prompter ?? (options.interactive ? createTerminalPrompter() : null);
|
|
4312
|
+
const confirmed = options.empty || options.yes ? true : prompter ? await prompter.confirmGenericProject({ rootDir, locale }) : false;
|
|
4313
|
+
if (!confirmed) {
|
|
4314
|
+
if (options.interactive) {
|
|
4315
|
+
console.log(
|
|
4316
|
+
locale === "zh-CN" ? "\u5DF2\u53D6\u6D88\u521D\u59CB\u5316\u3002" : "Initialization cancelled."
|
|
4317
|
+
);
|
|
4318
|
+
return EXIT_USER_CANCEL;
|
|
4319
|
+
}
|
|
4320
|
+
printNotProjectDirectory(rootDir, locale, "empty");
|
|
4321
|
+
return EXIT_NOT_A_PROJECT_DIR;
|
|
3955
4322
|
}
|
|
3956
|
-
printNotProjectDirectory(rootDir, locale, "empty");
|
|
3957
|
-
return EXIT_NOT_A_PROJECT_DIR;
|
|
3958
4323
|
}
|
|
3959
4324
|
isGenericProject = true;
|
|
3960
4325
|
}
|
|
@@ -4009,9 +4374,12 @@ async function init(rootDir = process5.cwd(), options = {}) {
|
|
|
4009
4374
|
);
|
|
4010
4375
|
}
|
|
4011
4376
|
const existingPlatform = wasInitialized ? await readExistingInitPlatform(stateFile) : null;
|
|
4377
|
+
const existingConfig = wasInitialized ? await readExistingInitPreferences(mancodeDir) : { platforms: [], minimalByPlatform: {} };
|
|
4378
|
+
const recoverablePlatforms = wasInitialized && existingConfig.platforms.length === 0 ? await detectManagedInitPlatforms(rootDir) : [];
|
|
4379
|
+
const forceReinstallPlatforms = options.force && options.platform === void 0 ? existingConfig.platforms.length > 0 ? existingConfig.platforms : recoverablePlatforms : [];
|
|
4012
4380
|
const platformHints = await detectPlatformHints(rootDir);
|
|
4013
4381
|
const selectedPlatforms = await selectInitPlatforms({
|
|
4014
|
-
option: options.platform,
|
|
4382
|
+
option: options.platform ?? (forceReinstallPlatforms.length > 0 ? forceReinstallPlatforms.join(",") : void 0),
|
|
4015
4383
|
existingPlatform,
|
|
4016
4384
|
hints: platformHints,
|
|
4017
4385
|
interactive: options.interactive,
|
|
@@ -4048,13 +4416,17 @@ async function init(rootDir = process5.cwd(), options = {}) {
|
|
|
4048
4416
|
const managedFiles = getInitManagedFilePaths(rootDir, selectedPlatforms);
|
|
4049
4417
|
mutationSnapshots = await snapshotFiles(managedFiles);
|
|
4050
4418
|
directorySnapshots = await snapshotDirectories(managedFiles, [
|
|
4051
|
-
|
|
4052
|
-
|
|
4419
|
+
path15.join(mancodeDir, "workflows"),
|
|
4420
|
+
path15.join(mancodeDir, "preseason-reports")
|
|
4053
4421
|
]);
|
|
4054
4422
|
const team = await detectTeamStatus(rootDir);
|
|
4055
|
-
const
|
|
4056
|
-
|
|
4057
|
-
|
|
4423
|
+
const platformMinimal = Object.fromEntries(
|
|
4424
|
+
selectedPlatforms.map((platform) => [
|
|
4425
|
+
platform,
|
|
4426
|
+
existingConfig.minimalByPlatform[platform] ?? false
|
|
4427
|
+
])
|
|
4428
|
+
);
|
|
4429
|
+
const existingTeamMode = existingConfig.forceTeamMode === true ? "on" : existingConfig.teamMode ?? "auto";
|
|
4058
4430
|
const teamModeEnabled = options.team ?? (existingTeamMode === "on" ? true : existingTeamMode === "off" ? false : team.isTeam);
|
|
4059
4431
|
if (options.team === true) {
|
|
4060
4432
|
console.log(
|
|
@@ -4092,6 +4464,7 @@ async function init(rootDir = process5.cwd(), options = {}) {
|
|
|
4092
4464
|
currentTask: null,
|
|
4093
4465
|
currentWorkflowMode: null,
|
|
4094
4466
|
skippedSteps: [],
|
|
4467
|
+
activeSoloPlan: null,
|
|
4095
4468
|
teamModeAutoDetected: teamModeEnabled,
|
|
4096
4469
|
contributors: team.contributors,
|
|
4097
4470
|
projectMode: isGenericProject ? "generic" : "detected"
|
|
@@ -4100,12 +4473,12 @@ async function init(rootDir = process5.cwd(), options = {}) {
|
|
|
4100
4473
|
await validateClaudeCodeSettings(rootDir);
|
|
4101
4474
|
}
|
|
4102
4475
|
await installMancodeCore(rootDir);
|
|
4103
|
-
await
|
|
4476
|
+
await fs4.mkdir(mancodeDir, { recursive: true });
|
|
4104
4477
|
const stateContent = `${JSON.stringify(state, null, 2)}
|
|
4105
4478
|
`;
|
|
4106
|
-
await
|
|
4107
|
-
await
|
|
4108
|
-
|
|
4479
|
+
await fs4.writeFile(stateFile, stateContent, "utf-8");
|
|
4480
|
+
await fs4.writeFile(
|
|
4481
|
+
path15.join(mancodeDir, "project-profile.json"),
|
|
4109
4482
|
`${JSON.stringify(profile, null, 2)}
|
|
4110
4483
|
`,
|
|
4111
4484
|
"utf-8"
|
|
@@ -4121,7 +4494,7 @@ async function init(rootDir = process5.cwd(), options = {}) {
|
|
|
4121
4494
|
...Object.fromEntries(
|
|
4122
4495
|
selectedPlatforms.map((platform) => [
|
|
4123
4496
|
platform,
|
|
4124
|
-
{ minimal:
|
|
4497
|
+
{ minimal: platformMinimal[platform] ?? false }
|
|
4125
4498
|
])
|
|
4126
4499
|
)
|
|
4127
4500
|
}
|
|
@@ -4142,12 +4515,12 @@ async function init(rootDir = process5.cwd(), options = {}) {
|
|
|
4142
4515
|
)
|
|
4143
4516
|
);
|
|
4144
4517
|
const tokens = await scanAesthetics(rootDir, uiLibrary);
|
|
4145
|
-
const tokensPath =
|
|
4518
|
+
const tokensPath = path15.join(
|
|
4146
4519
|
mancodeDir,
|
|
4147
4520
|
"aesthetics",
|
|
4148
4521
|
"style-tokens.json"
|
|
4149
4522
|
);
|
|
4150
|
-
await
|
|
4523
|
+
await fs4.writeFile(
|
|
4151
4524
|
tokensPath,
|
|
4152
4525
|
`${JSON.stringify(tokens, null, 2)}
|
|
4153
4526
|
`,
|
|
@@ -4208,7 +4581,7 @@ async function init(rootDir = process5.cwd(), options = {}) {
|
|
|
4208
4581
|
techStack: profileStack,
|
|
4209
4582
|
uiLibrary,
|
|
4210
4583
|
projectProfile: profile,
|
|
4211
|
-
minimal:
|
|
4584
|
+
minimal: platformMinimal[installer.name] ?? false,
|
|
4212
4585
|
force: options.force
|
|
4213
4586
|
});
|
|
4214
4587
|
}
|
|
@@ -4293,16 +4666,16 @@ async function init(rootDir = process5.cwd(), options = {}) {
|
|
|
4293
4666
|
}
|
|
4294
4667
|
}
|
|
4295
4668
|
async function updateConfigOptions(mancodeDir, patch, preserveInstalledPlatforms) {
|
|
4296
|
-
const configPath =
|
|
4669
|
+
const configPath = path15.join(mancodeDir, "config.json");
|
|
4297
4670
|
let config = {};
|
|
4298
4671
|
try {
|
|
4299
|
-
config = JSON.parse(await
|
|
4672
|
+
config = JSON.parse(await fs4.readFile(configPath, "utf-8"));
|
|
4300
4673
|
} catch {
|
|
4301
4674
|
}
|
|
4302
4675
|
const existingPlatforms = Array.isArray(config.platforms) ? config.platforms.filter(
|
|
4303
4676
|
(platform) => typeof platform === "string"
|
|
4304
4677
|
) : [];
|
|
4305
|
-
const existingPlatformOptions =
|
|
4678
|
+
const existingPlatformOptions = isRecord4(config.platformOptions) ? config.platformOptions : {};
|
|
4306
4679
|
const definedPatch = Object.fromEntries(
|
|
4307
4680
|
Object.entries(patch).filter(([, value]) => value !== void 0)
|
|
4308
4681
|
);
|
|
@@ -4316,570 +4689,306 @@ async function updateConfigOptions(mancodeDir, patch, preserveInstalledPlatforms
|
|
|
4316
4689
|
...patch.platformOptions
|
|
4317
4690
|
}
|
|
4318
4691
|
} : definedPatch;
|
|
4319
|
-
await
|
|
4692
|
+
await fs4.writeFile(
|
|
4320
4693
|
configPath,
|
|
4321
4694
|
`${JSON.stringify({ ...config, ...mergedPatch }, null, 2)}
|
|
4322
4695
|
`,
|
|
4323
4696
|
"utf-8"
|
|
4324
4697
|
);
|
|
4325
4698
|
}
|
|
4326
|
-
async function readExistingInitPreferences(mancodeDir
|
|
4699
|
+
async function readExistingInitPreferences(mancodeDir) {
|
|
4327
4700
|
try {
|
|
4328
4701
|
const config = JSON.parse(
|
|
4329
|
-
await
|
|
4702
|
+
await fs4.readFile(path15.join(mancodeDir, "config.json"), "utf-8")
|
|
4330
4703
|
);
|
|
4331
|
-
const preferences = {
|
|
4704
|
+
const preferences = {
|
|
4705
|
+
platforms: Array.isArray(config.platforms) ? config.platforms.filter(
|
|
4706
|
+
(platform) => typeof platform === "string" && getPlatformInstaller(platform) !== null
|
|
4707
|
+
) : [],
|
|
4708
|
+
minimalByPlatform: {}
|
|
4709
|
+
};
|
|
4332
4710
|
if (typeof config.forceTeamMode === "boolean") {
|
|
4333
4711
|
preferences.forceTeamMode = config.forceTeamMode;
|
|
4334
4712
|
}
|
|
4335
4713
|
if (config.teamMode === "auto" || config.teamMode === "on" || config.teamMode === "off") {
|
|
4336
4714
|
preferences.teamMode = config.teamMode;
|
|
4337
4715
|
}
|
|
4338
|
-
if (
|
|
4339
|
-
const
|
|
4340
|
-
|
|
4341
|
-
|
|
4716
|
+
if (isRecord4(config.platformOptions)) {
|
|
4717
|
+
for (const platform of preferences.platforms) {
|
|
4718
|
+
const platformOptions = config.platformOptions[platform];
|
|
4719
|
+
if (isRecord4(platformOptions) && platformOptions.minimal === true) {
|
|
4720
|
+
preferences.minimalByPlatform[platform] = true;
|
|
4721
|
+
}
|
|
4342
4722
|
}
|
|
4343
4723
|
}
|
|
4344
4724
|
return preferences;
|
|
4345
4725
|
} catch {
|
|
4346
|
-
return {};
|
|
4726
|
+
return { platforms: [], minimalByPlatform: {} };
|
|
4347
4727
|
}
|
|
4348
4728
|
}
|
|
4349
4729
|
async function readExistingInitPlatform(stateFile) {
|
|
4350
4730
|
try {
|
|
4351
|
-
const state = JSON.parse(await
|
|
4731
|
+
const state = JSON.parse(await fs4.readFile(stateFile, "utf-8"));
|
|
4352
4732
|
return typeof state.platform === "string" && getPlatformInstaller(state.platform) ? state.platform : null;
|
|
4353
4733
|
} catch {
|
|
4354
4734
|
return null;
|
|
4355
4735
|
}
|
|
4356
|
-
}
|
|
4357
|
-
async function
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
}
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
detected: input.hints
|
|
4370
|
-
});
|
|
4371
|
-
}
|
|
4372
|
-
if (input.interactive === false) {
|
|
4373
|
-
return input.hints.length === 1 ? input.hints : null;
|
|
4374
|
-
}
|
|
4375
|
-
return [DEFAULT_INIT_PLATFORM];
|
|
4376
|
-
}
|
|
4377
|
-
async function hasProjectManifest(rootDir) {
|
|
4378
|
-
for (const name of PROJECT_MANIFESTS) {
|
|
4379
|
-
if (await pathExists3(path14.join(rootDir, name))) return true;
|
|
4380
|
-
}
|
|
4381
|
-
return false;
|
|
4382
|
-
}
|
|
4383
|
-
async function canInitializeGenericProject(rootDir) {
|
|
4384
|
-
const resolved = path14.resolve(rootDir);
|
|
4385
|
-
if (resolved === path14.parse(resolved).root || resolved === path14.resolve(os.homedir())) {
|
|
4386
|
-
return { ok: false, reason: "unsafe" };
|
|
4387
|
-
}
|
|
4388
|
-
try {
|
|
4389
|
-
const entries = await fs3.readdir(resolved);
|
|
4390
|
-
const meaningfulEntries = entries.filter(
|
|
4391
|
-
(entry) => ![".DS_Store", "Thumbs.db", ".gitkeep"].includes(entry)
|
|
4392
|
-
);
|
|
4393
|
-
return meaningfulEntries.length === 0 ? { ok: true } : { ok: false, reason: "nonempty" };
|
|
4394
|
-
} catch {
|
|
4395
|
-
return { ok: false, reason: "unsafe" };
|
|
4396
|
-
}
|
|
4397
|
-
}
|
|
4398
|
-
function printNotProjectDirectory(rootDir, locale, reason) {
|
|
4399
|
-
if (locale === "zh-CN") {
|
|
4400
|
-
console.error(`\u2717 \u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u53EF\u521D\u59CB\u5316\u7684\u9879\u76EE\u76EE\u5F55\uFF1A${rootDir}`);
|
|
4401
|
-
if (reason === "unsafe") {
|
|
4402
|
-
console.error(" \u4E3A\u907F\u514D\u8BEF\u5199\uFF0C\u4E0D\u80FD\u5728 Home \u76EE\u5F55\u6216\u78C1\u76D8\u6839\u76EE\u5F55\u521D\u59CB\u5316\u3002");
|
|
4403
|
-
} else if (reason === "nonempty") {
|
|
4404
|
-
console.error(
|
|
4405
|
-
" \u672A\u8BC6\u522B\u5230\u9879\u76EE\u6587\u4EF6\uFF0C\u4E14\u76EE\u5F55\u4E2D\u5DF2\u6709\u6587\u4EF6\u3002\u8BF7\u5148\u8FDB\u5165\u9879\u76EE\u76EE\u5F55\u3002"
|
|
4406
|
-
);
|
|
4407
|
-
} else {
|
|
4408
|
-
console.error(
|
|
4409
|
-
" \u672A\u53D1\u73B0 .git \u6216\u9879\u76EE\u6587\u4EF6\u3002\u4EA4\u4E92\u7EC8\u7AEF\u4E2D\u53EF\u786E\u8BA4\u901A\u7528\u9879\u76EE\uFF0C\u6216\u4F7F\u7528 --empty\u3002"
|
|
4410
|
-
);
|
|
4411
|
-
}
|
|
4412
|
-
return;
|
|
4413
|
-
}
|
|
4414
|
-
console.error(`\u2717 Not a project directory: ${rootDir}`);
|
|
4415
|
-
if (reason === "unsafe") {
|
|
4416
|
-
console.error(
|
|
4417
|
-
" Refusing to initialize a home directory or filesystem root."
|
|
4418
|
-
);
|
|
4419
|
-
} else if (reason === "nonempty") {
|
|
4420
|
-
console.error(
|
|
4421
|
-
" No project files were detected and the directory is not empty."
|
|
4422
|
-
);
|
|
4423
|
-
} else {
|
|
4424
|
-
console.error(
|
|
4425
|
-
" No .git or recognized project manifest found. Use an interactive terminal or --empty for a new project."
|
|
4426
|
-
);
|
|
4427
|
-
}
|
|
4428
|
-
}
|
|
4429
|
-
function getInitManagedFilePaths(rootDir, platforms) {
|
|
4430
|
-
const files = [
|
|
4431
|
-
".mancode/state.json",
|
|
4432
|
-
".mancode/config.json",
|
|
4433
|
-
".mancode/project-profile.json",
|
|
4434
|
-
".mancode/aesthetics/style-tokens.json",
|
|
4435
|
-
".mancode/hooks/session-start.mjs",
|
|
4436
|
-
".mancode/hooks/user-prompt-submit.mjs",
|
|
4437
|
-
".mancode/logs/hooks.log",
|
|
4438
|
-
".mancode/memory/prd.md",
|
|
4439
|
-
".mancode/memory/spec.md",
|
|
4440
|
-
".mancode/memory/decisions.md"
|
|
4441
|
-
];
|
|
4442
|
-
if (platforms.includes("claude-code")) {
|
|
4443
|
-
files.push(
|
|
4444
|
-
".mancode/hooks/session-start.sh",
|
|
4445
|
-
".mancode/hooks/user-prompt-submit.sh",
|
|
4446
|
-
".claude/settings.json",
|
|
4447
|
-
".claude/skills/man8/SKILL.md",
|
|
4448
|
-
".claude/skills/mamba/SKILL.md",
|
|
4449
|
-
".claude/skills/solo/SKILL.md",
|
|
4450
|
-
".claude/skills/mancode-mamba.md",
|
|
4451
|
-
".claude/skills/mancode-solo.md",
|
|
4452
|
-
".claude/skills/mancode-man8.md"
|
|
4453
|
-
);
|
|
4454
|
-
for (const mode of MODE_NAMES) {
|
|
4455
|
-
files.push(
|
|
4456
|
-
`.claude/skills/${mode}/SKILL.md`,
|
|
4457
|
-
`.claude/skills/mancode-${mode}.md`
|
|
4458
|
-
);
|
|
4459
|
-
}
|
|
4460
|
-
for (const agent of ALL_AGENTS) {
|
|
4461
|
-
files.push(`.claude/agents/${agent.name}.md`);
|
|
4462
|
-
}
|
|
4463
|
-
}
|
|
4464
|
-
if (platforms.includes("cursor")) {
|
|
4465
|
-
files.push(
|
|
4466
|
-
".cursor/rules/mancode-mamba.mdc",
|
|
4467
|
-
".cursor/rules/mancode-man8.mdc",
|
|
4468
|
-
".cursor/commands/mamba.md",
|
|
4469
|
-
".cursor/commands/man8.md"
|
|
4470
|
-
);
|
|
4471
|
-
for (const fileName of MANCODE_CURSOR_RULE_FILES) {
|
|
4472
|
-
files.push(`.cursor/rules/${fileName}`);
|
|
4473
|
-
}
|
|
4474
|
-
for (const mode of MODE_NAMES) {
|
|
4475
|
-
files.push(`.cursor/commands/${mode}.md`);
|
|
4476
|
-
}
|
|
4477
|
-
}
|
|
4478
|
-
if (platforms.includes("codex") || platforms.includes("zcode")) {
|
|
4479
|
-
files.push(
|
|
4480
|
-
"AGENTS.md",
|
|
4481
|
-
".agents/skills/mamba/SKILL.md",
|
|
4482
|
-
".agents/skills/man8/SKILL.md"
|
|
4483
|
-
);
|
|
4484
|
-
for (const mode of MODE_NAMES) {
|
|
4485
|
-
files.push(`.agents/skills/${mode}/SKILL.md`);
|
|
4486
|
-
}
|
|
4487
|
-
}
|
|
4488
|
-
if (platforms.includes("codex")) {
|
|
4489
|
-
files.push(".codex/skills/mamba/SKILL.md", ".codex/skills/man8/SKILL.md");
|
|
4490
|
-
for (const mode of MODE_NAMES) {
|
|
4491
|
-
files.push(`.codex/skills/${mode}/SKILL.md`);
|
|
4492
|
-
}
|
|
4493
|
-
}
|
|
4494
|
-
if (platforms.includes("zcode")) {
|
|
4495
|
-
files.push(".zcode/skills/mamba/SKILL.md", ".zcode/skills/man8/SKILL.md");
|
|
4496
|
-
for (const mode of MODE_NAMES) {
|
|
4497
|
-
files.push(`.zcode/skills/${mode}/SKILL.md`);
|
|
4498
|
-
}
|
|
4499
|
-
}
|
|
4500
|
-
if (platforms.includes("copilot")) {
|
|
4501
|
-
files.push(
|
|
4502
|
-
".github/copilot-instructions.md",
|
|
4503
|
-
".github/prompts/mamba.prompt.md",
|
|
4504
|
-
".github/prompts/man8.prompt.md"
|
|
4505
|
-
);
|
|
4506
|
-
for (const mode of MODE_NAMES) {
|
|
4507
|
-
files.push(`.github/prompts/${mode}.prompt.md`);
|
|
4508
|
-
}
|
|
4509
|
-
}
|
|
4510
|
-
return [...new Set(files.map((file) => path14.join(rootDir, file)))];
|
|
4511
|
-
}
|
|
4512
|
-
async function snapshotFiles(filePaths) {
|
|
4513
|
-
return Promise.all(
|
|
4514
|
-
filePaths.map(async (filePath) => {
|
|
4515
|
-
try {
|
|
4516
|
-
return { filePath, content: await fs3.readFile(filePath, "utf-8") };
|
|
4517
|
-
} catch (error) {
|
|
4518
|
-
if (isNodeError5(error) && error.code === "ENOENT") {
|
|
4519
|
-
return { filePath, content: null };
|
|
4520
|
-
}
|
|
4521
|
-
throw error;
|
|
4522
|
-
}
|
|
4523
|
-
})
|
|
4524
|
-
);
|
|
4525
|
-
}
|
|
4526
|
-
async function snapshotDirectories(filePaths, additionalDirectories = []) {
|
|
4527
|
-
const directories = new Set(additionalDirectories);
|
|
4528
|
-
for (const filePath of filePaths) {
|
|
4529
|
-
let current = path14.dirname(filePath);
|
|
4530
|
-
while (current !== path14.dirname(current)) {
|
|
4531
|
-
directories.add(current);
|
|
4532
|
-
current = path14.dirname(current);
|
|
4533
|
-
}
|
|
4534
|
-
}
|
|
4535
|
-
return Promise.all(
|
|
4536
|
-
[...directories].map(async (dirPath) => ({
|
|
4537
|
-
dirPath,
|
|
4538
|
-
existed: await pathExists3(dirPath)
|
|
4539
|
-
}))
|
|
4540
|
-
);
|
|
4541
|
-
}
|
|
4542
|
-
async function restoreFiles(snapshots) {
|
|
4543
|
-
for (const snapshot of snapshots) {
|
|
4544
|
-
if (snapshot.content === null) {
|
|
4545
|
-
await fs3.rm(snapshot.filePath, { force: true });
|
|
4546
|
-
continue;
|
|
4547
|
-
}
|
|
4548
|
-
await fs3.mkdir(path14.dirname(snapshot.filePath), { recursive: true });
|
|
4549
|
-
await fs3.writeFile(snapshot.filePath, snapshot.content, "utf-8");
|
|
4550
|
-
}
|
|
4551
|
-
}
|
|
4552
|
-
async function removeNewEmptyDirectories(snapshots) {
|
|
4553
|
-
const candidates = snapshots.filter((snapshot) => !snapshot.existed).sort((a, b) => b.dirPath.length - a.dirPath.length);
|
|
4554
|
-
for (const { dirPath } of candidates) {
|
|
4555
|
-
try {
|
|
4556
|
-
await fs3.rmdir(dirPath);
|
|
4557
|
-
} catch {
|
|
4558
|
-
}
|
|
4559
|
-
}
|
|
4560
|
-
}
|
|
4561
|
-
function isRecord3(value) {
|
|
4562
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4563
|
-
}
|
|
4564
|
-
function isNodeError5(error) {
|
|
4565
|
-
return error instanceof Error && "code" in error;
|
|
4566
|
-
}
|
|
4567
|
-
function printPlatformCreatedFiles(platform, locale) {
|
|
4568
|
-
if (platform === "claude-code") {
|
|
4569
|
-
console.log(
|
|
4570
|
-
localize(
|
|
4571
|
-
locale,
|
|
4572
|
-
" .claude/settings.json # hook \u6CE8\u518C",
|
|
4573
|
-
" .claude/settings.json # hook registration"
|
|
4574
|
-
)
|
|
4575
|
-
);
|
|
4576
|
-
console.log(
|
|
4577
|
-
localize(
|
|
4578
|
-
locale,
|
|
4579
|
-
" .claude/skills/ # solo + MVP-2 slash skills",
|
|
4580
|
-
" .claude/skills/ # solo + MVP-2 slash skills"
|
|
4581
|
-
)
|
|
4582
|
-
);
|
|
4583
|
-
return;
|
|
4736
|
+
}
|
|
4737
|
+
async function detectManagedInitPlatforms(rootDir) {
|
|
4738
|
+
const detected = await Promise.all(
|
|
4739
|
+
getPlatformInstallers().map(async (platform) => ({
|
|
4740
|
+
name: platform.name,
|
|
4741
|
+
present: await isPlatformPresent(rootDir, platform.name)
|
|
4742
|
+
}))
|
|
4743
|
+
);
|
|
4744
|
+
return detected.filter((platform) => platform.present).map(({ name }) => name);
|
|
4745
|
+
}
|
|
4746
|
+
async function selectInitPlatforms(input) {
|
|
4747
|
+
if (input.option !== void 0) {
|
|
4748
|
+
return parsePlatformSelection(input.option);
|
|
4584
4749
|
}
|
|
4585
|
-
if (
|
|
4586
|
-
|
|
4587
|
-
return;
|
|
4750
|
+
if (input.existingPlatform) return [input.existingPlatform];
|
|
4751
|
+
if (input.yes && input.interactive !== void 0) {
|
|
4752
|
+
return input.hints.length === 1 ? input.hints : null;
|
|
4588
4753
|
}
|
|
4589
|
-
if (
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4754
|
+
if (input.interactive) {
|
|
4755
|
+
const prompter = input.prompter ?? createTerminalPrompter();
|
|
4756
|
+
return prompter.selectPlatforms({
|
|
4757
|
+
locale: input.locale,
|
|
4758
|
+
detected: input.hints
|
|
4759
|
+
});
|
|
4593
4760
|
}
|
|
4594
|
-
if (
|
|
4595
|
-
|
|
4596
|
-
console.log(" .agents/skills/ # ZCode mode skills");
|
|
4597
|
-
return;
|
|
4761
|
+
if (input.interactive === false) {
|
|
4762
|
+
return input.hints.length === 1 ? input.hints : null;
|
|
4598
4763
|
}
|
|
4599
|
-
|
|
4764
|
+
return [DEFAULT_INIT_PLATFORM];
|
|
4600
4765
|
}
|
|
4601
|
-
function
|
|
4602
|
-
|
|
4766
|
+
async function hasProjectManifest(rootDir) {
|
|
4767
|
+
for (const name of PROJECT_MANIFESTS) {
|
|
4768
|
+
if (await pathExists4(path15.join(rootDir, name))) return true;
|
|
4769
|
+
}
|
|
4770
|
+
return false;
|
|
4603
4771
|
}
|
|
4604
|
-
async function
|
|
4772
|
+
async function canInitializeGenericProject(rootDir) {
|
|
4773
|
+
const resolved = path15.resolve(rootDir);
|
|
4774
|
+
if (resolved === path15.parse(resolved).root || resolved === path15.resolve(os.homedir())) {
|
|
4775
|
+
return { ok: false, reason: "unsafe" };
|
|
4776
|
+
}
|
|
4605
4777
|
try {
|
|
4606
|
-
await
|
|
4607
|
-
|
|
4778
|
+
const entries = await fs4.readdir(resolved);
|
|
4779
|
+
const meaningfulEntries = entries.filter(
|
|
4780
|
+
(entry) => ![".DS_Store", "Thumbs.db", ".gitkeep"].includes(entry)
|
|
4781
|
+
);
|
|
4782
|
+
return meaningfulEntries.length === 0 ? { ok: true } : { ok: false, reason: "nonempty" };
|
|
4608
4783
|
} catch {
|
|
4609
|
-
return false;
|
|
4784
|
+
return { ok: false, reason: "unsafe" };
|
|
4610
4785
|
}
|
|
4611
4786
|
}
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
detail: describeStatus(installed, readiness)
|
|
4628
|
-
};
|
|
4629
|
-
}
|
|
4630
|
-
async function checkPlatformReadiness(rootDir, platform) {
|
|
4631
|
-
if (platform === "claude-code") {
|
|
4632
|
-
const [hasSoloSkill, registered, hasHookFiles] = await Promise.all([
|
|
4633
|
-
fileMatches(
|
|
4634
|
-
path15.join(rootDir, ".claude", "skills", "solo", "SKILL.md"),
|
|
4635
|
-
(content) => isGeneratedClaudeSkill(content, "solo")
|
|
4636
|
-
),
|
|
4637
|
-
claudeHooksRegistered(rootDir),
|
|
4638
|
-
pathsExist([
|
|
4639
|
-
path15.join(rootDir, ".mancode", "hooks", "session-start.mjs"),
|
|
4640
|
-
path15.join(rootDir, ".mancode", "hooks", "user-prompt-submit.mjs")
|
|
4641
|
-
])
|
|
4642
|
-
]);
|
|
4643
|
-
const present = hasSoloSkill && registered && hasHookFiles;
|
|
4644
|
-
const minimal = await isPlatformMinimal(rootDir, "claude-code");
|
|
4645
|
-
const fullContentReady = minimal ? true : await claudeFullContentReady(rootDir);
|
|
4646
|
-
return {
|
|
4647
|
-
present,
|
|
4648
|
-
ready: present && fullContentReady,
|
|
4649
|
-
target: ".claude/",
|
|
4650
|
-
readyDetail: present && fullContentReady ? minimal ? "solo skill and hooks ready" : "skills, agents, and hooks ready" : "skills, agents, hook files, or hook registration are incomplete"
|
|
4651
|
-
};
|
|
4787
|
+
function printNotProjectDirectory(rootDir, locale, reason) {
|
|
4788
|
+
if (locale === "zh-CN") {
|
|
4789
|
+
console.error(`\u2717 \u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u53EF\u521D\u59CB\u5316\u7684\u9879\u76EE\u76EE\u5F55\uFF1A${rootDir}`);
|
|
4790
|
+
if (reason === "unsafe") {
|
|
4791
|
+
console.error(" \u4E3A\u907F\u514D\u8BEF\u5199\uFF0C\u4E0D\u80FD\u5728 Home \u76EE\u5F55\u6216\u78C1\u76D8\u6839\u76EE\u5F55\u521D\u59CB\u5316\u3002");
|
|
4792
|
+
} else if (reason === "nonempty") {
|
|
4793
|
+
console.error(
|
|
4794
|
+
" \u672A\u8BC6\u522B\u5230\u9879\u76EE\u6587\u4EF6\uFF0C\u4E14\u76EE\u5F55\u4E2D\u5DF2\u6709\u6587\u4EF6\u3002\u8BF7\u5148\u8FDB\u5165\u9879\u76EE\u76EE\u5F55\u3002"
|
|
4795
|
+
);
|
|
4796
|
+
} else {
|
|
4797
|
+
console.error(
|
|
4798
|
+
" \u672A\u53D1\u73B0 .git \u6216\u9879\u76EE\u6587\u4EF6\u3002\u4EA4\u4E92\u7EC8\u7AEF\u4E2D\u53EF\u786E\u8BA4\u901A\u7528\u9879\u76EE\uFF0C\u6216\u4F7F\u7528 --empty\u3002"
|
|
4799
|
+
);
|
|
4800
|
+
}
|
|
4801
|
+
return;
|
|
4652
4802
|
}
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4803
|
+
console.error(`\u2717 Not a project directory: ${rootDir}`);
|
|
4804
|
+
if (reason === "unsafe") {
|
|
4805
|
+
console.error(
|
|
4806
|
+
" Refusing to initialize a home directory or filesystem root."
|
|
4807
|
+
);
|
|
4808
|
+
} else if (reason === "nonempty") {
|
|
4809
|
+
console.error(
|
|
4810
|
+
" No project files were detected and the directory is not empty."
|
|
4811
|
+
);
|
|
4812
|
+
} else {
|
|
4813
|
+
console.error(
|
|
4814
|
+
" No .git or recognized project manifest found. Use an interactive terminal or --empty for a new project."
|
|
4659
4815
|
);
|
|
4660
|
-
if (await isPlatformMinimal(rootDir, "cursor")) {
|
|
4661
|
-
return {
|
|
4662
|
-
present: hasCoreRules,
|
|
4663
|
-
ready: hasCoreRules,
|
|
4664
|
-
target: ".cursor/rules/",
|
|
4665
|
-
readyDetail: "mancode core rules present"
|
|
4666
|
-
};
|
|
4667
|
-
}
|
|
4668
|
-
const [hasRules, hasCommands] = await Promise.all([
|
|
4669
|
-
allManagedSkills(
|
|
4670
|
-
MANCODE_CURSOR_RULE_FILES.map(
|
|
4671
|
-
(file) => path15.join(rootDir, ".cursor", "rules", file)
|
|
4672
|
-
),
|
|
4673
|
-
[CURSOR_RULE_MANAGED_MARKER]
|
|
4674
|
-
),
|
|
4675
|
-
allManagedSkills(
|
|
4676
|
-
MODE_NAMES.map(
|
|
4677
|
-
(mode) => path15.join(rootDir, ".cursor", "commands", `${mode}.md`)
|
|
4678
|
-
),
|
|
4679
|
-
[MODE_FILE_MANAGED_MARKER]
|
|
4680
|
-
)
|
|
4681
|
-
]);
|
|
4682
|
-
return {
|
|
4683
|
-
present: hasCoreRules,
|
|
4684
|
-
ready: hasRules && hasCommands,
|
|
4685
|
-
target: ".cursor/rules/ + .cursor/commands/",
|
|
4686
|
-
readyDetail: hasRules && hasCommands ? "mancode rules and commands present" : "mode rules or commands are missing, incomplete, or user-authored"
|
|
4687
|
-
};
|
|
4688
4816
|
}
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4817
|
+
}
|
|
4818
|
+
function getInitManagedFilePaths(rootDir, platforms) {
|
|
4819
|
+
const files = [
|
|
4820
|
+
".mancode/state.json",
|
|
4821
|
+
".mancode/config.json",
|
|
4822
|
+
".mancode/project-profile.json",
|
|
4823
|
+
".mancode/aesthetics/style-tokens.json",
|
|
4824
|
+
".mancode/hooks/session-start.mjs",
|
|
4825
|
+
".mancode/hooks/user-prompt-submit.mjs",
|
|
4826
|
+
".mancode/logs/hooks.log",
|
|
4827
|
+
".mancode/memory/prd.md",
|
|
4828
|
+
".mancode/memory/spec.md",
|
|
4829
|
+
".mancode/memory/decisions.md"
|
|
4830
|
+
];
|
|
4831
|
+
if (platforms.includes("claude-code")) {
|
|
4832
|
+
files.push(
|
|
4833
|
+
".mancode/hooks/session-start.sh",
|
|
4834
|
+
".mancode/hooks/user-prompt-submit.sh",
|
|
4835
|
+
".claude/settings.json",
|
|
4836
|
+
".claude/skills/man8/SKILL.md",
|
|
4837
|
+
".claude/skills/mamba/SKILL.md",
|
|
4838
|
+
".claude/skills/solo/SKILL.md",
|
|
4839
|
+
".claude/skills/mancode-mamba.md",
|
|
4840
|
+
".claude/skills/mancode-solo.md",
|
|
4841
|
+
".claude/skills/mancode-man8.md"
|
|
4842
|
+
);
|
|
4843
|
+
for (const mode of MODE_NAMES) {
|
|
4844
|
+
files.push(
|
|
4845
|
+
`.claude/skills/${mode}/SKILL.md`,
|
|
4846
|
+
`.claude/skills/mancode-${mode}.md`
|
|
4847
|
+
);
|
|
4698
4848
|
}
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
present: true,
|
|
4702
|
-
ready: true,
|
|
4703
|
-
target: "AGENTS.md",
|
|
4704
|
-
readyDetail: "managed block present"
|
|
4705
|
-
};
|
|
4849
|
+
for (const agent of ALL_AGENTS) {
|
|
4850
|
+
files.push(`.claude/agents/${agent.name}.md`);
|
|
4706
4851
|
}
|
|
4707
|
-
const skillsDir = path15.join(rootDir, ".agents", "skills");
|
|
4708
|
-
const hasSkills = await allManagedSkills(
|
|
4709
|
-
MODE_NAMES.map((mode) => path15.join(skillsDir, mode, "SKILL.md")),
|
|
4710
|
-
MANCODE_AGENT_SKILL_MARKERS
|
|
4711
|
-
);
|
|
4712
|
-
return {
|
|
4713
|
-
present: true,
|
|
4714
|
-
ready: hasSkills,
|
|
4715
|
-
target: "AGENTS.md + .agents/skills/",
|
|
4716
|
-
readyDetail: hasSkills ? "managed block and skills present" : "mode skills are missing, incomplete, or user-authored"
|
|
4717
|
-
};
|
|
4718
4852
|
}
|
|
4719
|
-
if (
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4853
|
+
if (platforms.includes("cursor")) {
|
|
4854
|
+
files.push(
|
|
4855
|
+
".cursor/rules/mancode-mamba.mdc",
|
|
4856
|
+
".cursor/rules/mancode-man8.mdc",
|
|
4857
|
+
".cursor/commands/mamba.md",
|
|
4858
|
+
".cursor/commands/man8.md"
|
|
4724
4859
|
);
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
present: false,
|
|
4728
|
-
ready: false,
|
|
4729
|
-
target: "AGENTS.md",
|
|
4730
|
-
readyDetail: "managed block missing"
|
|
4731
|
-
};
|
|
4860
|
+
for (const fileName of MANCODE_CURSOR_RULE_FILES) {
|
|
4861
|
+
files.push(`.cursor/rules/${fileName}`);
|
|
4732
4862
|
}
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
present: true,
|
|
4736
|
-
ready: true,
|
|
4737
|
-
target: "AGENTS.md",
|
|
4738
|
-
readyDetail: "managed block present"
|
|
4739
|
-
};
|
|
4863
|
+
for (const mode of MODE_NAMES) {
|
|
4864
|
+
files.push(`.cursor/commands/${mode}.md`);
|
|
4740
4865
|
}
|
|
4741
|
-
const skillsDir = path15.join(rootDir, ".agents", "skills");
|
|
4742
|
-
const hasSkills = await allManagedSkills(
|
|
4743
|
-
MODE_NAMES.map((mode) => path15.join(skillsDir, mode, "SKILL.md")),
|
|
4744
|
-
MANCODE_AGENT_SKILL_MARKERS
|
|
4745
|
-
);
|
|
4746
|
-
return {
|
|
4747
|
-
present: true,
|
|
4748
|
-
ready: hasSkills,
|
|
4749
|
-
target: "AGENTS.md + .agents/skills/",
|
|
4750
|
-
readyDetail: hasSkills ? "managed block and skills present" : "mode skills are missing, incomplete, or user-authored"
|
|
4751
|
-
};
|
|
4752
|
-
}
|
|
4753
|
-
const hasBlock = await fileHasManagedBlock(
|
|
4754
|
-
path15.join(rootDir, ".github", "copilot-instructions.md")
|
|
4755
|
-
);
|
|
4756
|
-
if (!hasBlock) {
|
|
4757
|
-
return {
|
|
4758
|
-
present: false,
|
|
4759
|
-
ready: false,
|
|
4760
|
-
target: ".github/copilot-instructions.md",
|
|
4761
|
-
readyDetail: "managed block missing"
|
|
4762
|
-
};
|
|
4763
4866
|
}
|
|
4764
|
-
if (
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4867
|
+
if (platforms.includes("codex") || platforms.includes("zcode")) {
|
|
4868
|
+
files.push(
|
|
4869
|
+
"AGENTS.md",
|
|
4870
|
+
".agents/skills/mamba/SKILL.md",
|
|
4871
|
+
".agents/skills/man8/SKILL.md"
|
|
4769
4872
|
);
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
target: ".github/copilot-instructions.md + .github/prompts/",
|
|
4774
|
-
readyDetail: hasPrompts ? "managed block and prompts present" : "mode prompts are missing, incomplete, or user-authored"
|
|
4775
|
-
};
|
|
4873
|
+
for (const mode of MODE_NAMES) {
|
|
4874
|
+
files.push(`.agents/skills/${mode}/SKILL.md`);
|
|
4875
|
+
}
|
|
4776
4876
|
}
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
};
|
|
4783
|
-
}
|
|
4784
|
-
function describeStatus(installed, readiness) {
|
|
4785
|
-
if (installed && readiness.present) return readiness.readyDetail;
|
|
4786
|
-
if (!installed && readiness.present) {
|
|
4787
|
-
return "target files present but platform is not recorded in config";
|
|
4877
|
+
if (platforms.includes("codex")) {
|
|
4878
|
+
files.push(".codex/skills/mamba/SKILL.md", ".codex/skills/man8/SKILL.md");
|
|
4879
|
+
for (const mode of MODE_NAMES) {
|
|
4880
|
+
files.push(`.codex/skills/${mode}/SKILL.md`);
|
|
4881
|
+
}
|
|
4788
4882
|
}
|
|
4789
|
-
|
|
4883
|
+
if (platforms.includes("zcode")) {
|
|
4884
|
+
files.push(".zcode/skills/mamba/SKILL.md", ".zcode/skills/man8/SKILL.md");
|
|
4885
|
+
for (const mode of MODE_NAMES) {
|
|
4886
|
+
files.push(`.zcode/skills/${mode}/SKILL.md`);
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
if (platforms.includes("copilot")) {
|
|
4890
|
+
files.push(
|
|
4891
|
+
".github/copilot-instructions.md",
|
|
4892
|
+
".github/prompts/mamba.prompt.md",
|
|
4893
|
+
".github/prompts/man8.prompt.md"
|
|
4894
|
+
);
|
|
4895
|
+
for (const mode of MODE_NAMES) {
|
|
4896
|
+
files.push(`.github/prompts/${mode}.prompt.md`);
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
return [...new Set(files.map((file) => path15.join(rootDir, file)))];
|
|
4790
4900
|
}
|
|
4791
|
-
async function
|
|
4792
|
-
|
|
4793
|
-
|
|
4901
|
+
async function snapshotFiles(filePaths) {
|
|
4902
|
+
return Promise.all(
|
|
4903
|
+
filePaths.map(async (filePath) => {
|
|
4904
|
+
try {
|
|
4905
|
+
return { filePath, content: await fs4.readFile(filePath, "utf-8") };
|
|
4906
|
+
} catch (error) {
|
|
4907
|
+
if (isNodeError5(error) && error.code === "ENOENT") {
|
|
4908
|
+
return { filePath, content: null };
|
|
4909
|
+
}
|
|
4910
|
+
throw error;
|
|
4911
|
+
}
|
|
4912
|
+
})
|
|
4794
4913
|
);
|
|
4795
|
-
return results.every(Boolean);
|
|
4796
4914
|
}
|
|
4797
|
-
async function
|
|
4798
|
-
const
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4915
|
+
async function snapshotDirectories(filePaths, additionalDirectories = []) {
|
|
4916
|
+
const directories = new Set(additionalDirectories);
|
|
4917
|
+
for (const filePath of filePaths) {
|
|
4918
|
+
let current = path15.dirname(filePath);
|
|
4919
|
+
while (current !== path15.dirname(current)) {
|
|
4920
|
+
directories.add(current);
|
|
4921
|
+
current = path15.dirname(current);
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
return Promise.all(
|
|
4925
|
+
[...directories].map(async (dirPath) => ({
|
|
4926
|
+
dirPath,
|
|
4927
|
+
existed: await pathExists4(dirPath)
|
|
4928
|
+
}))
|
|
4809
4929
|
);
|
|
4810
|
-
const results = await Promise.all([...skillChecks, ...agentChecks]);
|
|
4811
|
-
return results.every(Boolean);
|
|
4812
4930
|
}
|
|
4813
|
-
async function
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4931
|
+
async function restoreFiles(snapshots) {
|
|
4932
|
+
for (const snapshot of snapshots) {
|
|
4933
|
+
if (snapshot.content === null) {
|
|
4934
|
+
await fs4.rm(snapshot.filePath, { force: true });
|
|
4935
|
+
continue;
|
|
4936
|
+
}
|
|
4937
|
+
await fs4.mkdir(path15.dirname(snapshot.filePath), { recursive: true });
|
|
4938
|
+
await fs4.writeFile(snapshot.filePath, snapshot.content, "utf-8");
|
|
4818
4939
|
}
|
|
4819
4940
|
}
|
|
4820
|
-
async function
|
|
4821
|
-
|
|
4822
|
-
}
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
} catch {
|
|
4828
|
-
return false;
|
|
4941
|
+
async function removeNewEmptyDirectories(snapshots) {
|
|
4942
|
+
const candidates = snapshots.filter((snapshot) => !snapshot.existed).sort((a, b) => b.dirPath.length - a.dirPath.length);
|
|
4943
|
+
for (const { dirPath } of candidates) {
|
|
4944
|
+
try {
|
|
4945
|
+
await fs4.rmdir(dirPath);
|
|
4946
|
+
} catch {
|
|
4947
|
+
}
|
|
4829
4948
|
}
|
|
4830
4949
|
}
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
const raw = await fs4.readFile(
|
|
4834
|
-
path15.join(rootDir, ".mancode", "config.json"),
|
|
4835
|
-
"utf-8"
|
|
4836
|
-
);
|
|
4837
|
-
const config = JSON.parse(raw);
|
|
4838
|
-
if (!isRecord4(config.platformOptions)) return false;
|
|
4839
|
-
const options = config.platformOptions[platform];
|
|
4840
|
-
return isRecord4(options) && options.minimal === true;
|
|
4841
|
-
} catch {
|
|
4842
|
-
return false;
|
|
4843
|
-
}
|
|
4950
|
+
function isRecord4(value) {
|
|
4951
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4844
4952
|
}
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
const content = await fs4.readFile(filePath, "utf-8");
|
|
4848
|
-
return hasManagedBlock(content, startMarker, endMarker);
|
|
4849
|
-
} catch {
|
|
4850
|
-
return false;
|
|
4851
|
-
}
|
|
4953
|
+
function isNodeError5(error) {
|
|
4954
|
+
return error instanceof Error && "code" in error;
|
|
4852
4955
|
}
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4956
|
+
function printPlatformCreatedFiles(platform, locale) {
|
|
4957
|
+
if (platform === "claude-code") {
|
|
4958
|
+
console.log(
|
|
4959
|
+
localize(
|
|
4960
|
+
locale,
|
|
4961
|
+
" .claude/settings.json # hook \u6CE8\u518C",
|
|
4962
|
+
" .claude/settings.json # hook registration"
|
|
4963
|
+
)
|
|
4858
4964
|
);
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4965
|
+
console.log(
|
|
4966
|
+
localize(
|
|
4967
|
+
locale,
|
|
4968
|
+
" .claude/skills/ # solo + MVP-2 slash skills",
|
|
4969
|
+
" .claude/skills/ # solo + MVP-2 slash skills"
|
|
4970
|
+
)
|
|
4971
|
+
);
|
|
4972
|
+
return;
|
|
4863
4973
|
}
|
|
4974
|
+
if (platform === "cursor") {
|
|
4975
|
+
console.log(" .cursor/rules/ # Cursor project rules");
|
|
4976
|
+
return;
|
|
4977
|
+
}
|
|
4978
|
+
if (platform === "codex") {
|
|
4979
|
+
console.log(" AGENTS.md # Codex managed block");
|
|
4980
|
+
console.log(" .agents/skills/ # Codex mode skills");
|
|
4981
|
+
return;
|
|
4982
|
+
}
|
|
4983
|
+
if (platform === "zcode") {
|
|
4984
|
+
console.log(" AGENTS.md # ZCode managed block");
|
|
4985
|
+
console.log(" .agents/skills/ # ZCode mode skills");
|
|
4986
|
+
return;
|
|
4987
|
+
}
|
|
4988
|
+
console.log(" .github/copilot-instructions.md # Copilot instructions");
|
|
4864
4989
|
}
|
|
4865
|
-
function
|
|
4866
|
-
|
|
4867
|
-
const hooks = settings.hooks;
|
|
4868
|
-
if (!isRecord4(hooks)) return false;
|
|
4869
|
-
return hasHookCommand(hooks.SessionStart, ".mancode/hooks/session-start.mjs") && hasHookCommand(
|
|
4870
|
-
hooks.UserPromptSubmit,
|
|
4871
|
-
".mancode/hooks/user-prompt-submit.mjs"
|
|
4872
|
-
);
|
|
4873
|
-
}
|
|
4874
|
-
function hasHookCommand(value, needle) {
|
|
4875
|
-
if (!Array.isArray(value)) return false;
|
|
4876
|
-
return value.some((group) => {
|
|
4877
|
-
if (!isRecord4(group) || !Array.isArray(group.hooks)) return false;
|
|
4878
|
-
return group.hooks.some((hook) => {
|
|
4879
|
-
if (!isRecord4(hook) || typeof hook.command !== "string") return false;
|
|
4880
|
-
return hook.command.includes(needle);
|
|
4881
|
-
});
|
|
4882
|
-
});
|
|
4990
|
+
function localize(locale, chinese, english) {
|
|
4991
|
+
return locale === "zh-CN" ? chinese : english;
|
|
4883
4992
|
}
|
|
4884
4993
|
async function pathExists4(p) {
|
|
4885
4994
|
try {
|
|
@@ -4889,11 +4998,11 @@ async function pathExists4(p) {
|
|
|
4889
4998
|
return false;
|
|
4890
4999
|
}
|
|
4891
5000
|
}
|
|
4892
|
-
function isRecord4(value) {
|
|
4893
|
-
return typeof value === "object" && value !== null;
|
|
4894
|
-
}
|
|
4895
5001
|
|
|
4896
5002
|
// src/commands/install.ts
|
|
5003
|
+
import { promises as fs5 } from "fs";
|
|
5004
|
+
import path16 from "path";
|
|
5005
|
+
import process6 from "process";
|
|
4897
5006
|
var EXIT_OK2 = 0;
|
|
4898
5007
|
var EXIT_NOT_INITIALIZED = 1;
|
|
4899
5008
|
var EXIT_UNSUPPORTED_PLATFORM = 2;
|
|
@@ -6558,23 +6667,225 @@ async function pathExists9(p) {
|
|
|
6558
6667
|
// src/commands/status.ts
|
|
6559
6668
|
import { spawn } from "child_process";
|
|
6560
6669
|
import { promises as fs9 } from "fs";
|
|
6561
|
-
import
|
|
6670
|
+
import path26 from "path";
|
|
6562
6671
|
import process10 from "process";
|
|
6563
6672
|
|
|
6564
|
-
// src/system/workflow.ts
|
|
6565
|
-
import {
|
|
6566
|
-
mkdir as mkdir9,
|
|
6567
|
-
readFile as
|
|
6568
|
-
readdir as readdir5,
|
|
6569
|
-
rm as rm4,
|
|
6570
|
-
stat as stat4,
|
|
6571
|
-
writeFile as
|
|
6572
|
-
} from "fs/promises";
|
|
6573
|
-
import
|
|
6673
|
+
// src/system/workflow.ts
|
|
6674
|
+
import {
|
|
6675
|
+
mkdir as mkdir9,
|
|
6676
|
+
readFile as readFile12,
|
|
6677
|
+
readdir as readdir5,
|
|
6678
|
+
rm as rm4,
|
|
6679
|
+
stat as stat4,
|
|
6680
|
+
writeFile as writeFile13
|
|
6681
|
+
} from "fs/promises";
|
|
6682
|
+
import path25 from "path";
|
|
6683
|
+
|
|
6684
|
+
// src/system/requirements-ledger.ts
|
|
6685
|
+
import { createHash as createHash2 } from "crypto";
|
|
6686
|
+
import { readFile as readFile9, writeFile as writeFile10 } from "fs/promises";
|
|
6687
|
+
import path22 from "path";
|
|
6688
|
+
var REQUIREMENTS_FILE = "requirements.json";
|
|
6689
|
+
var REQUIREMENTS_MARKDOWN_FILE = "requirements.md";
|
|
6690
|
+
var ACCEPTANCE_ID_PATTERN = /^AC-[A-Z0-9][A-Z0-9-]{0,27}$/;
|
|
6691
|
+
var REQUIREMENT_DIMENSIONS = [
|
|
6692
|
+
"platform",
|
|
6693
|
+
"core_scope",
|
|
6694
|
+
"technical_stack",
|
|
6695
|
+
"data_and_persistence",
|
|
6696
|
+
"performance",
|
|
6697
|
+
"compatibility",
|
|
6698
|
+
"security"
|
|
6699
|
+
];
|
|
6700
|
+
function parseRequirementsLedger(raw) {
|
|
6701
|
+
let value;
|
|
6702
|
+
try {
|
|
6703
|
+
value = JSON.parse(raw);
|
|
6704
|
+
} catch {
|
|
6705
|
+
throw new Error("requirements input must be valid JSON");
|
|
6706
|
+
}
|
|
6707
|
+
if (!isRecord8(value) || value.version !== 1) {
|
|
6708
|
+
throw new Error("requirements version must be 1");
|
|
6709
|
+
}
|
|
6710
|
+
const stringFields = [
|
|
6711
|
+
"confirmedScope",
|
|
6712
|
+
"excludedScope",
|
|
6713
|
+
"technicalDecisions",
|
|
6714
|
+
"defaults",
|
|
6715
|
+
"blockingUnknowns"
|
|
6716
|
+
];
|
|
6717
|
+
if (typeof value.goal !== "string" || !value.goal.trim()) {
|
|
6718
|
+
throw new Error("requirements goal is required");
|
|
6719
|
+
}
|
|
6720
|
+
for (const field of stringFields) {
|
|
6721
|
+
if (!isNonEmptyStringArray(value[field])) {
|
|
6722
|
+
if (!Array.isArray(value[field]) || value[field].length > 0) {
|
|
6723
|
+
throw new Error(`requirements ${field} must contain non-empty strings`);
|
|
6724
|
+
}
|
|
6725
|
+
}
|
|
6726
|
+
}
|
|
6727
|
+
if (!Array.isArray(value.coverage)) {
|
|
6728
|
+
throw new Error("requirements coverage is required");
|
|
6729
|
+
}
|
|
6730
|
+
const coverageDimensions = /* @__PURE__ */ new Set();
|
|
6731
|
+
const coverage = value.coverage.map((item) => {
|
|
6732
|
+
if (!isRecord8(item) || !isRequirementDimension(item.dimension) || !isCoverageStatus(item.status) || typeof item.rationale !== "string" || !item.rationale.trim()) {
|
|
6733
|
+
throw new Error("invalid requirements coverage item");
|
|
6734
|
+
}
|
|
6735
|
+
if (coverageDimensions.has(item.dimension)) {
|
|
6736
|
+
throw new Error(`duplicate requirements coverage: ${item.dimension}`);
|
|
6737
|
+
}
|
|
6738
|
+
coverageDimensions.add(item.dimension);
|
|
6739
|
+
return {
|
|
6740
|
+
dimension: item.dimension,
|
|
6741
|
+
status: item.status,
|
|
6742
|
+
rationale: item.rationale.trim()
|
|
6743
|
+
};
|
|
6744
|
+
});
|
|
6745
|
+
const missingDimensions = REQUIREMENT_DIMENSIONS.filter(
|
|
6746
|
+
(dimension) => !coverageDimensions.has(dimension)
|
|
6747
|
+
);
|
|
6748
|
+
if (missingDimensions.length > 0) {
|
|
6749
|
+
throw new Error(
|
|
6750
|
+
`requirements coverage is missing: ${missingDimensions.join(", ")}`
|
|
6751
|
+
);
|
|
6752
|
+
}
|
|
6753
|
+
if (value.confirmedScope.length === 0) {
|
|
6754
|
+
throw new Error("requirements confirmedScope must not be empty");
|
|
6755
|
+
}
|
|
6756
|
+
const stackCoverage = coverage.find(
|
|
6757
|
+
(item) => item.dimension === "technical_stack"
|
|
6758
|
+
);
|
|
6759
|
+
if (stackCoverage?.status !== "not_applicable" && value.technicalDecisions.length === 0) {
|
|
6760
|
+
throw new Error(
|
|
6761
|
+
"requirements technicalDecisions are required when technical_stack applies"
|
|
6762
|
+
);
|
|
6763
|
+
}
|
|
6764
|
+
if (!Array.isArray(value.acceptanceCriteria) || value.acceptanceCriteria.length === 0) {
|
|
6765
|
+
throw new Error("requirements need at least one acceptance criterion");
|
|
6766
|
+
}
|
|
6767
|
+
const ids = /* @__PURE__ */ new Set();
|
|
6768
|
+
const acceptanceCriteria = value.acceptanceCriteria.map((item) => {
|
|
6769
|
+
if (!isRecord8(item) || typeof item.id !== "string" || !ACCEPTANCE_ID_PATTERN.test(item.id) || typeof item.description !== "string" || !item.description.trim() || typeof item.required !== "boolean" || !isVerificationMethod(item.method)) {
|
|
6770
|
+
throw new Error("invalid acceptance criterion");
|
|
6771
|
+
}
|
|
6772
|
+
if (ids.has(item.id)) {
|
|
6773
|
+
throw new Error(`duplicate acceptance criterion: ${item.id}`);
|
|
6774
|
+
}
|
|
6775
|
+
ids.add(item.id);
|
|
6776
|
+
return {
|
|
6777
|
+
id: item.id,
|
|
6778
|
+
description: item.description.trim(),
|
|
6779
|
+
required: item.required,
|
|
6780
|
+
method: item.method
|
|
6781
|
+
};
|
|
6782
|
+
});
|
|
6783
|
+
if (!acceptanceCriteria.some((item) => item.required)) {
|
|
6784
|
+
throw new Error(
|
|
6785
|
+
"requirements need at least one required acceptance criterion"
|
|
6786
|
+
);
|
|
6787
|
+
}
|
|
6788
|
+
return {
|
|
6789
|
+
version: 1,
|
|
6790
|
+
goal: value.goal.trim(),
|
|
6791
|
+
confirmedScope: normalizeStrings(value.confirmedScope),
|
|
6792
|
+
excludedScope: normalizeStrings(value.excludedScope),
|
|
6793
|
+
technicalDecisions: normalizeStrings(value.technicalDecisions),
|
|
6794
|
+
defaults: normalizeStrings(value.defaults),
|
|
6795
|
+
blockingUnknowns: normalizeStrings(value.blockingUnknowns),
|
|
6796
|
+
coverage,
|
|
6797
|
+
acceptanceCriteria
|
|
6798
|
+
};
|
|
6799
|
+
}
|
|
6800
|
+
function requirementsAreReady(ledger) {
|
|
6801
|
+
return ledger.blockingUnknowns.length === 0;
|
|
6802
|
+
}
|
|
6803
|
+
function requirementsDigest(ledger) {
|
|
6804
|
+
return createHash2("sha256").update(JSON.stringify(ledger)).digest("hex");
|
|
6805
|
+
}
|
|
6806
|
+
async function readRequirementsLedger(projectRoot, taskId) {
|
|
6807
|
+
try {
|
|
6808
|
+
const raw = await readFile9(requirementsPath(projectRoot, taskId), "utf-8");
|
|
6809
|
+
return parseRequirementsLedger(raw);
|
|
6810
|
+
} catch {
|
|
6811
|
+
return null;
|
|
6812
|
+
}
|
|
6813
|
+
}
|
|
6814
|
+
async function writeRequirementsArtifacts(projectRoot, taskId, ledger) {
|
|
6815
|
+
const dir = workflowDir(projectRoot, taskId);
|
|
6816
|
+
await writeFile10(
|
|
6817
|
+
path22.join(dir, REQUIREMENTS_FILE),
|
|
6818
|
+
`${JSON.stringify(ledger, null, 2)}
|
|
6819
|
+
`,
|
|
6820
|
+
"utf-8"
|
|
6821
|
+
);
|
|
6822
|
+
await writeFile10(
|
|
6823
|
+
path22.join(dir, REQUIREMENTS_MARKDOWN_FILE),
|
|
6824
|
+
renderRequirementsMarkdown(ledger),
|
|
6825
|
+
"utf-8"
|
|
6826
|
+
);
|
|
6827
|
+
}
|
|
6828
|
+
function renderRequirementsMarkdown(ledger) {
|
|
6829
|
+
const section = (title, items) => `## ${title}
|
|
6830
|
+
|
|
6831
|
+
${items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- \u65E0"}
|
|
6832
|
+
`;
|
|
6833
|
+
const criteria = ledger.acceptanceCriteria.map(
|
|
6834
|
+
(item) => `- **${item.id}** [${item.required ? "\u5FC5\u9700" : "\u53EF\u9009"} / ${item.method}] ${item.description}`
|
|
6835
|
+
).join("\n");
|
|
6836
|
+
const coverage = ledger.coverage.map((item) => `- **${item.dimension}** [${item.status}] ${item.rationale}`).join("\n");
|
|
6837
|
+
return `# Requirements
|
|
6838
|
+
|
|
6839
|
+
## \u7528\u6237\u76EE\u6807
|
|
6840
|
+
|
|
6841
|
+
${ledger.goal}
|
|
6842
|
+
|
|
6843
|
+
${section("\u5DF2\u786E\u8BA4\u8303\u56F4", ledger.confirmedScope)}
|
|
6844
|
+
${section("\u660E\u786E\u6392\u9664\u9879", ledger.excludedScope)}
|
|
6845
|
+
${section("\u6280\u672F\u51B3\u7B56", ledger.technicalDecisions)}
|
|
6846
|
+
${section("\u9ED8\u8BA4\u503C\u4E0E\u7406\u7531", ledger.defaults)}
|
|
6847
|
+
## \u9700\u6C42\u8986\u76D6
|
|
6848
|
+
|
|
6849
|
+
${coverage}
|
|
6850
|
+
|
|
6851
|
+
## \u9A8C\u6536\u6807\u51C6
|
|
6852
|
+
|
|
6853
|
+
${criteria}
|
|
6854
|
+
|
|
6855
|
+
${section("\u963B\u585E\u6027\u672A\u51B3\u95EE\u9898", ledger.blockingUnknowns)}
|
|
6856
|
+
## Readiness
|
|
6857
|
+
|
|
6858
|
+
${requirementsAreReady(ledger) ? "READY" : "NEEDS_CLARIFICATION"}
|
|
6859
|
+
`;
|
|
6860
|
+
}
|
|
6861
|
+
function requirementsPath(projectRoot, taskId) {
|
|
6862
|
+
return path22.join(workflowDir(projectRoot, taskId), REQUIREMENTS_FILE);
|
|
6863
|
+
}
|
|
6864
|
+
function workflowDir(projectRoot, taskId) {
|
|
6865
|
+
return path22.join(projectRoot, ".mancode", "workflows", taskId);
|
|
6866
|
+
}
|
|
6867
|
+
function normalizeStrings(value) {
|
|
6868
|
+
return value.map((item) => item.trim());
|
|
6869
|
+
}
|
|
6870
|
+
function isNonEmptyStringArray(value) {
|
|
6871
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string" && item.trim().length > 0);
|
|
6872
|
+
}
|
|
6873
|
+
function isVerificationMethod(value) {
|
|
6874
|
+
return value === "automated" || value === "manual" || value === "hybrid";
|
|
6875
|
+
}
|
|
6876
|
+
function isRequirementDimension(value) {
|
|
6877
|
+
return REQUIREMENT_DIMENSIONS.some((dimension) => dimension === value);
|
|
6878
|
+
}
|
|
6879
|
+
function isCoverageStatus(value) {
|
|
6880
|
+
return value === "confirmed" || value === "defaulted" || value === "not_applicable";
|
|
6881
|
+
}
|
|
6882
|
+
function isRecord8(value) {
|
|
6883
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6884
|
+
}
|
|
6574
6885
|
|
|
6575
6886
|
// src/system/review-ledger.ts
|
|
6576
|
-
import { mkdir as mkdir8, readFile as
|
|
6577
|
-
import
|
|
6887
|
+
import { mkdir as mkdir8, readFile as readFile10, stat as stat3, writeFile as writeFile11 } from "fs/promises";
|
|
6888
|
+
import path23 from "path";
|
|
6578
6889
|
var REVIEW_FILE = "review-ledger.json";
|
|
6579
6890
|
var TASK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
|
|
6580
6891
|
var BLOCKER_ID_PATTERN = /^[A-Z][A-Z0-9-]{0,31}$/;
|
|
@@ -6603,6 +6914,27 @@ async function initializeReview(projectRoot, taskId, depth, targetDomain) {
|
|
|
6603
6914
|
await writeReviewLedger(projectRoot, taskId, ledger);
|
|
6604
6915
|
return ledger;
|
|
6605
6916
|
}
|
|
6917
|
+
async function initializeSkippedReview(projectRoot, taskId, reason) {
|
|
6918
|
+
assertValidTaskId(taskId);
|
|
6919
|
+
if (!reason.trim()) throw new Error("review skip reason is required");
|
|
6920
|
+
const existing = await readReviewLedger(projectRoot, taskId);
|
|
6921
|
+
if (existing) throw new Error(`review already initialized: ${taskId}`);
|
|
6922
|
+
const ledger = {
|
|
6923
|
+
version: "1.0",
|
|
6924
|
+
depth: "targeted",
|
|
6925
|
+
requiredDomains: [],
|
|
6926
|
+
completedDomains: [],
|
|
6927
|
+
reports: {},
|
|
6928
|
+
blockers: [],
|
|
6929
|
+
remediationRounds: 0,
|
|
6930
|
+
skipped: {
|
|
6931
|
+
reason: reason.trim(),
|
|
6932
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6933
|
+
}
|
|
6934
|
+
};
|
|
6935
|
+
await writeReviewLedger(projectRoot, taskId, ledger);
|
|
6936
|
+
return ledger;
|
|
6937
|
+
}
|
|
6606
6938
|
async function completeReviewDomain(projectRoot, taskId, domain, report, blockerIds) {
|
|
6607
6939
|
const ledger = await requireReviewLedger(projectRoot, taskId);
|
|
6608
6940
|
if (!ledger.requiredDomains.includes(domain)) {
|
|
@@ -6675,6 +7007,7 @@ async function remediateReviewBlockers(projectRoot, taskId, resolvedIds) {
|
|
|
6675
7007
|
async function reviewCanComplete(projectRoot, taskId) {
|
|
6676
7008
|
const ledger = await readReviewLedger(projectRoot, taskId);
|
|
6677
7009
|
if (!ledger) return false;
|
|
7010
|
+
if (ledger.skipped) return true;
|
|
6678
7011
|
return ledger.requiredDomains.every(
|
|
6679
7012
|
(domain) => ledger.completedDomains.includes(domain)
|
|
6680
7013
|
) && ledger.blockers.every((blocker) => blocker.status === "resolved");
|
|
@@ -6682,7 +7015,7 @@ async function reviewCanComplete(projectRoot, taskId) {
|
|
|
6682
7015
|
async function readReviewLedger(projectRoot, taskId) {
|
|
6683
7016
|
if (!TASK_ID_PATTERN.test(taskId)) return null;
|
|
6684
7017
|
try {
|
|
6685
|
-
const raw = await
|
|
7018
|
+
const raw = await readFile10(reviewPath(projectRoot, taskId), "utf-8");
|
|
6686
7019
|
const value = JSON.parse(raw);
|
|
6687
7020
|
return isReviewLedger(value) ? value : null;
|
|
6688
7021
|
} catch {
|
|
@@ -6690,8 +7023,8 @@ async function readReviewLedger(projectRoot, taskId) {
|
|
|
6690
7023
|
}
|
|
6691
7024
|
}
|
|
6692
7025
|
function isReviewLedger(value) {
|
|
6693
|
-
if (!
|
|
6694
|
-
if (value.version !== "1.0" || !isReviewDepth(value.depth) || !isDomainArray(value.requiredDomains) || !isDomainArray(value.completedDomains) || !
|
|
7026
|
+
if (!isRecord9(value)) return false;
|
|
7027
|
+
if (value.version !== "1.0" || !isReviewDepth(value.depth) || !isDomainArray(value.requiredDomains) || !isDomainArray(value.completedDomains) || !isRecord9(value.reports) || !Array.isArray(value.blockers) || value.remediationRounds !== 0 && value.remediationRounds !== 1) {
|
|
6695
7028
|
return false;
|
|
6696
7029
|
}
|
|
6697
7030
|
const requiredDomains = value.requiredDomains;
|
|
@@ -6699,6 +7032,9 @@ function isReviewLedger(value) {
|
|
|
6699
7032
|
const reports = value.reports;
|
|
6700
7033
|
const blockers = value.blockers;
|
|
6701
7034
|
const expectedDomains = value.depth === "full" ? ["quality", "security"] : requiredDomains;
|
|
7035
|
+
if (value.skipped !== void 0) {
|
|
7036
|
+
return isRecord9(value.skipped) && typeof value.skipped.reason === "string" && value.skipped.reason.trim().length > 0 && typeof value.skipped.recordedAt === "string" && requiredDomains.length === 0 && completedDomains.length === 0 && Object.keys(reports).length === 0 && blockers.length === 0 && value.remediationRounds === 0;
|
|
7037
|
+
}
|
|
6702
7038
|
if (value.depth === "targeted" && requiredDomains.length !== 1 || requiredDomains.length !== expectedDomains.length || !expectedDomains.every((domain) => requiredDomains.includes(domain)) || new Set(completedDomains).size !== completedDomains.length || completedDomains.some((domain) => !requiredDomains.includes(domain)) || completedDomains.some((domain) => {
|
|
6703
7039
|
const report = reports[domain];
|
|
6704
7040
|
return typeof report !== "string" || !isSafeReportPath(report);
|
|
@@ -6707,13 +7043,13 @@ function isReviewLedger(value) {
|
|
|
6707
7043
|
}
|
|
6708
7044
|
const blockerIds = /* @__PURE__ */ new Set();
|
|
6709
7045
|
return blockers.every(
|
|
6710
|
-
(blocker) =>
|
|
7046
|
+
(blocker) => isRecord9(blocker) && typeof blocker.id === "string" && BLOCKER_ID_PATTERN.test(blocker.id) && !blockerIds.has(blocker.id) && blockerIds.add(blocker.id) && isReviewDomain(blocker.domain) && completedDomains.includes(blocker.domain) && (blocker.status === "open" || blocker.status === "resolved")
|
|
6711
7047
|
);
|
|
6712
7048
|
}
|
|
6713
7049
|
function isDomainArray(value) {
|
|
6714
7050
|
return Array.isArray(value) && value.every(isReviewDomain);
|
|
6715
7051
|
}
|
|
6716
|
-
function
|
|
7052
|
+
function isRecord9(value) {
|
|
6717
7053
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6718
7054
|
}
|
|
6719
7055
|
async function requireReviewLedger(projectRoot, taskId) {
|
|
@@ -6723,17 +7059,21 @@ async function requireReviewLedger(projectRoot, taskId) {
|
|
|
6723
7059
|
return ledger;
|
|
6724
7060
|
}
|
|
6725
7061
|
async function writeReviewLedger(projectRoot, taskId, ledger) {
|
|
6726
|
-
const dir =
|
|
7062
|
+
const dir = path23.join(projectRoot, ".mancode", "workflows", taskId);
|
|
6727
7063
|
await mkdir8(dir, { recursive: true });
|
|
6728
|
-
await
|
|
6729
|
-
|
|
7064
|
+
await writeFile11(
|
|
7065
|
+
path23.join(dir, REVIEW_FILE),
|
|
6730
7066
|
`${JSON.stringify(ledger, null, 2)}
|
|
6731
7067
|
`,
|
|
6732
7068
|
"utf-8"
|
|
6733
7069
|
);
|
|
6734
7070
|
}
|
|
7071
|
+
function reviewLedgerPath(projectRoot, taskId) {
|
|
7072
|
+
assertValidTaskId(taskId);
|
|
7073
|
+
return reviewPath(projectRoot, taskId);
|
|
7074
|
+
}
|
|
6735
7075
|
function reviewPath(projectRoot, taskId) {
|
|
6736
|
-
return
|
|
7076
|
+
return path23.join(projectRoot, ".mancode", "workflows", taskId, REVIEW_FILE);
|
|
6737
7077
|
}
|
|
6738
7078
|
function assertValidTaskId(taskId) {
|
|
6739
7079
|
if (!TASK_ID_PATTERN.test(taskId)) {
|
|
@@ -6746,7 +7086,7 @@ function assertSafeReportPath(report) {
|
|
|
6746
7086
|
}
|
|
6747
7087
|
}
|
|
6748
7088
|
async function assertReportExists(projectRoot, taskId, report) {
|
|
6749
|
-
const reportPath =
|
|
7089
|
+
const reportPath = path23.join(
|
|
6750
7090
|
projectRoot,
|
|
6751
7091
|
".mancode",
|
|
6752
7092
|
"workflows",
|
|
@@ -6760,11 +7100,221 @@ async function assertReportExists(projectRoot, taskId, report) {
|
|
|
6760
7100
|
throw new Error(`review report not found: ${report}`);
|
|
6761
7101
|
}
|
|
6762
7102
|
function isSafeReportPath(report) {
|
|
6763
|
-
const normalized =
|
|
7103
|
+
const normalized = path23.posix.normalize(report.replaceAll("\\", "/"));
|
|
7104
|
+
return Boolean(
|
|
7105
|
+
report.trim() && !path23.isAbsolute(report) && normalized !== ".." && !normalized.startsWith("../") && normalized.endsWith(".md")
|
|
7106
|
+
);
|
|
7107
|
+
}
|
|
7108
|
+
|
|
7109
|
+
// src/system/verification-ledger.ts
|
|
7110
|
+
import { readFile as readFile11, writeFile as writeFile12 } from "fs/promises";
|
|
7111
|
+
import path24 from "path";
|
|
7112
|
+
var VERIFICATION_FILE = "verification-ledger.json";
|
|
7113
|
+
function initializeVerificationLedger(requirements, planVersion, remediationRound = 0) {
|
|
7114
|
+
const checks = requirements.acceptanceCriteria.map((criterion) => ({
|
|
7115
|
+
acceptanceId: criterion.id,
|
|
7116
|
+
required: criterion.required,
|
|
7117
|
+
...criterion.method === "automated" || criterion.method === "hybrid" ? { automated: { status: "pending" } } : {},
|
|
7118
|
+
...criterion.method === "manual" || criterion.method === "hybrid" ? { manual: { status: "pending" } } : {}
|
|
7119
|
+
}));
|
|
7120
|
+
return {
|
|
7121
|
+
version: 1,
|
|
7122
|
+
planVersion,
|
|
7123
|
+
requirementsDigest: requirementsDigest(requirements),
|
|
7124
|
+
remediationRound,
|
|
7125
|
+
status: deriveVerificationStatus(checks),
|
|
7126
|
+
checks
|
|
7127
|
+
};
|
|
7128
|
+
}
|
|
7129
|
+
function recordVerification(ledger, acceptanceId, component, result, evidence, automatedDetails) {
|
|
7130
|
+
if (!evidence.trim()) throw new Error("verification evidence is required");
|
|
7131
|
+
if (component === "automated" && result === "manual_required") {
|
|
7132
|
+
throw new Error("automated verification cannot be manual_required");
|
|
7133
|
+
}
|
|
7134
|
+
if (component === "automated" && (result === "passed" || result === "failed")) {
|
|
7135
|
+
if (!automatedDetails?.command.trim() || !Number.isInteger(automatedDetails.exitCode)) {
|
|
7136
|
+
throw new Error(
|
|
7137
|
+
"automated passed/failed verification requires command and exit code"
|
|
7138
|
+
);
|
|
7139
|
+
}
|
|
7140
|
+
if (result === "passed" && automatedDetails.exitCode !== 0) {
|
|
7141
|
+
throw new Error("passed verification requires exit code 0");
|
|
7142
|
+
}
|
|
7143
|
+
if (result === "failed" && automatedDetails.exitCode === 0) {
|
|
7144
|
+
throw new Error("failed verification requires a non-zero exit code");
|
|
7145
|
+
}
|
|
7146
|
+
}
|
|
7147
|
+
if (component === "manual" && result !== "manual_required") {
|
|
7148
|
+
throw new Error(
|
|
7149
|
+
"manual verification must use require-manual or confirm-manual"
|
|
7150
|
+
);
|
|
7151
|
+
}
|
|
7152
|
+
const index = ledger.checks.findIndex(
|
|
7153
|
+
(check2) => check2.acceptanceId === acceptanceId
|
|
7154
|
+
);
|
|
7155
|
+
if (index < 0)
|
|
7156
|
+
throw new Error(`unknown acceptance criterion: ${acceptanceId}`);
|
|
7157
|
+
const check = ledger.checks[index];
|
|
7158
|
+
if (!check) throw new Error(`unknown acceptance criterion: ${acceptanceId}`);
|
|
7159
|
+
if (!check[component]) {
|
|
7160
|
+
throw new Error(
|
|
7161
|
+
`${acceptanceId} does not require ${component} verification`
|
|
7162
|
+
);
|
|
7163
|
+
}
|
|
7164
|
+
const checks = [...ledger.checks];
|
|
7165
|
+
checks[index] = {
|
|
7166
|
+
...check,
|
|
7167
|
+
[component]: {
|
|
7168
|
+
status: result,
|
|
7169
|
+
evidence: evidence.trim(),
|
|
7170
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7171
|
+
...component === "automated" && automatedDetails ? {
|
|
7172
|
+
command: automatedDetails.command.trim(),
|
|
7173
|
+
exitCode: automatedDetails.exitCode,
|
|
7174
|
+
...automatedDetails.evidenceFile ? { evidenceFile: automatedDetails.evidenceFile } : {}
|
|
7175
|
+
} : {}
|
|
7176
|
+
}
|
|
7177
|
+
};
|
|
7178
|
+
return { ...ledger, status: deriveVerificationStatus(checks), checks };
|
|
7179
|
+
}
|
|
7180
|
+
function resetVerificationForRemediation(ledger, remediationRound) {
|
|
7181
|
+
if (!Number.isInteger(remediationRound) || remediationRound < ledger.remediationRound) {
|
|
7182
|
+
throw new Error("invalid verification remediation round");
|
|
7183
|
+
}
|
|
7184
|
+
if (remediationRound === ledger.remediationRound) return ledger;
|
|
7185
|
+
const checks = ledger.checks.map((check) => ({
|
|
7186
|
+
acceptanceId: check.acceptanceId,
|
|
7187
|
+
required: check.required,
|
|
7188
|
+
...check.automated ? { automated: { status: "pending" } } : {},
|
|
7189
|
+
...check.manual ? { manual: { status: "pending" } } : {}
|
|
7190
|
+
}));
|
|
7191
|
+
return {
|
|
7192
|
+
...ledger,
|
|
7193
|
+
remediationRound,
|
|
7194
|
+
status: deriveVerificationStatus(checks),
|
|
7195
|
+
checks
|
|
7196
|
+
};
|
|
7197
|
+
}
|
|
7198
|
+
function confirmManualVerification(ledger, acceptanceId, evidence) {
|
|
7199
|
+
if (!evidence.trim())
|
|
7200
|
+
throw new Error("manual confirmation evidence is required");
|
|
7201
|
+
const index = ledger.checks.findIndex(
|
|
7202
|
+
(check2) => check2.acceptanceId === acceptanceId
|
|
7203
|
+
);
|
|
7204
|
+
if (index < 0)
|
|
7205
|
+
throw new Error(`unknown acceptance criterion: ${acceptanceId}`);
|
|
7206
|
+
const check = ledger.checks[index];
|
|
7207
|
+
if (!check) throw new Error(`unknown acceptance criterion: ${acceptanceId}`);
|
|
7208
|
+
if (!check.manual) {
|
|
7209
|
+
throw new Error(`${acceptanceId} does not require manual verification`);
|
|
7210
|
+
}
|
|
7211
|
+
if (check.manual.status !== "manual_required") {
|
|
7212
|
+
throw new Error(`${acceptanceId} is not awaiting manual confirmation`);
|
|
7213
|
+
}
|
|
7214
|
+
const checks = [...ledger.checks];
|
|
7215
|
+
checks[index] = {
|
|
7216
|
+
...check,
|
|
7217
|
+
manual: {
|
|
7218
|
+
status: "passed",
|
|
7219
|
+
evidence: evidence.trim(),
|
|
7220
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7221
|
+
}
|
|
7222
|
+
};
|
|
7223
|
+
return { ...ledger, status: deriveVerificationStatus(checks), checks };
|
|
7224
|
+
}
|
|
7225
|
+
function deriveVerificationStatus(checks) {
|
|
7226
|
+
const statuses = checks.filter((check) => check.required).flatMap((check) => [check.automated?.status, check.manual?.status]).filter((status2) => Boolean(status2));
|
|
7227
|
+
if (statuses.includes("blocked")) return "blocked";
|
|
7228
|
+
if (statuses.includes("manual_required")) return "manual_required";
|
|
7229
|
+
if (statuses.includes("failed")) return "failed";
|
|
7230
|
+
if (statuses.every((status2) => status2 === "passed")) return "passed";
|
|
7231
|
+
return "pending";
|
|
7232
|
+
}
|
|
7233
|
+
async function readVerificationLedger(projectRoot, taskId) {
|
|
7234
|
+
try {
|
|
7235
|
+
const raw = await readFile11(verificationPath(projectRoot, taskId), "utf-8");
|
|
7236
|
+
const value = JSON.parse(raw);
|
|
7237
|
+
return isVerificationLedger(value) ? value : null;
|
|
7238
|
+
} catch {
|
|
7239
|
+
return null;
|
|
7240
|
+
}
|
|
7241
|
+
}
|
|
7242
|
+
async function writeVerificationLedger(projectRoot, taskId, ledger) {
|
|
7243
|
+
await writeFile12(
|
|
7244
|
+
verificationPath(projectRoot, taskId),
|
|
7245
|
+
`${JSON.stringify(ledger, null, 2)}
|
|
7246
|
+
`,
|
|
7247
|
+
"utf-8"
|
|
7248
|
+
);
|
|
7249
|
+
}
|
|
7250
|
+
async function verificationCanAdvance(projectRoot, taskId, planVersion) {
|
|
7251
|
+
const [ledger, requirements, review] = await Promise.all([
|
|
7252
|
+
readVerificationLedger(projectRoot, taskId),
|
|
7253
|
+
readRequirementsLedger(projectRoot, taskId),
|
|
7254
|
+
readReviewLedger(projectRoot, taskId)
|
|
7255
|
+
]);
|
|
6764
7256
|
return Boolean(
|
|
6765
|
-
|
|
7257
|
+
ledger && requirements && ledger.status === "passed" && ledger.planVersion === planVersion && ledger.remediationRound === (review?.remediationRounds ?? 0) && ledger.requirementsDigest === requirementsDigest(requirements) && ledgerMatchesRequirements(ledger, requirements)
|
|
7258
|
+
);
|
|
7259
|
+
}
|
|
7260
|
+
function verificationLedgerPath(projectRoot, taskId) {
|
|
7261
|
+
return verificationPath(projectRoot, taskId);
|
|
7262
|
+
}
|
|
7263
|
+
function verificationPath(projectRoot, taskId) {
|
|
7264
|
+
return path24.join(
|
|
7265
|
+
projectRoot,
|
|
7266
|
+
".mancode",
|
|
7267
|
+
"workflows",
|
|
7268
|
+
taskId,
|
|
7269
|
+
VERIFICATION_FILE
|
|
6766
7270
|
);
|
|
6767
7271
|
}
|
|
7272
|
+
function isVerificationLedger(value) {
|
|
7273
|
+
if (typeof value !== "object" || value === null || !("version" in value) || value.version !== 1 || !("planVersion" in value) || !Number.isInteger(value.planVersion) || !("requirementsDigest" in value) || typeof value.requirementsDigest !== "string" || !("remediationRound" in value) || typeof value.remediationRound !== "number" || !Number.isInteger(value.remediationRound) || value.remediationRound < 0 || !("status" in value) || !isComponentStatus(value.status) || !("checks" in value) || !Array.isArray(value.checks)) {
|
|
7274
|
+
return false;
|
|
7275
|
+
}
|
|
7276
|
+
const ids = /* @__PURE__ */ new Set();
|
|
7277
|
+
const validChecks = value.checks.every((check) => {
|
|
7278
|
+
if (typeof check !== "object" || check === null || !("acceptanceId" in check) || typeof check.acceptanceId !== "string" || !("required" in check) || typeof check.required !== "boolean") {
|
|
7279
|
+
return false;
|
|
7280
|
+
}
|
|
7281
|
+
if (ids.has(check.acceptanceId)) return false;
|
|
7282
|
+
ids.add(check.acceptanceId);
|
|
7283
|
+
return ["automated", "manual"].every((component) => {
|
|
7284
|
+
const evidence = check[component];
|
|
7285
|
+
if (evidence === void 0) return true;
|
|
7286
|
+
const valid = typeof evidence === "object" && evidence !== null && "status" in evidence && isComponentStatus(evidence.status);
|
|
7287
|
+
if (!valid) return false;
|
|
7288
|
+
if (evidence.status === "pending") {
|
|
7289
|
+
return !("evidence" in evidence) && !("updatedAt" in evidence);
|
|
7290
|
+
}
|
|
7291
|
+
const baseEvidenceIsValid = "evidence" in evidence && typeof evidence.evidence === "string" && evidence.evidence.trim().length > 0 && "updatedAt" in evidence && typeof evidence.updatedAt === "string";
|
|
7292
|
+
if (!baseEvidenceIsValid) return false;
|
|
7293
|
+
if (component === "automated" && (evidence.status === "passed" || evidence.status === "failed")) {
|
|
7294
|
+
return "command" in evidence && typeof evidence.command === "string" && evidence.command.trim().length > 0 && "exitCode" in evidence && Number.isInteger(evidence.exitCode) && (evidence.status === "passed" ? evidence.exitCode === 0 : evidence.exitCode !== 0);
|
|
7295
|
+
}
|
|
7296
|
+
return true;
|
|
7297
|
+
});
|
|
7298
|
+
});
|
|
7299
|
+
return validChecks && value.status === deriveVerificationStatus(value.checks);
|
|
7300
|
+
}
|
|
7301
|
+
function ledgerMatchesRequirements(ledger, requirements) {
|
|
7302
|
+
if (ledger.checks.length !== requirements.acceptanceCriteria.length) {
|
|
7303
|
+
return false;
|
|
7304
|
+
}
|
|
7305
|
+
return requirements.acceptanceCriteria.every((criterion) => {
|
|
7306
|
+
const check = ledger.checks.find(
|
|
7307
|
+
(candidate) => candidate.acceptanceId === criterion.id
|
|
7308
|
+
);
|
|
7309
|
+
if (!check || check.required !== criterion.required) return false;
|
|
7310
|
+
const needsAutomated = criterion.method === "automated" || criterion.method === "hybrid";
|
|
7311
|
+
const needsManual = criterion.method === "manual" || criterion.method === "hybrid";
|
|
7312
|
+
return Boolean(check.automated) === needsAutomated && Boolean(check.manual) === needsManual;
|
|
7313
|
+
});
|
|
7314
|
+
}
|
|
7315
|
+
function isComponentStatus(value) {
|
|
7316
|
+
return value === "pending" || value === "passed" || value === "failed" || value === "manual_required" || value === "blocked";
|
|
7317
|
+
}
|
|
6768
7318
|
|
|
6769
7319
|
// src/system/workflow.ts
|
|
6770
7320
|
var METADATA_FILE = "metadata.json";
|
|
@@ -6786,7 +7336,7 @@ async function createWorkflow(projectRoot, task, mode, options = {}) {
|
|
|
6786
7336
|
await validateParentTask(projectRoot, mode, options.parentTaskId, true);
|
|
6787
7337
|
await mkdir9(workflowsRoot(projectRoot), { recursive: true });
|
|
6788
7338
|
const taskId = await allocateTaskId(projectRoot, generateTaskId(task));
|
|
6789
|
-
const dir =
|
|
7339
|
+
const dir = workflowDir2(projectRoot, taskId);
|
|
6790
7340
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6791
7341
|
const meta = {
|
|
6792
7342
|
taskId,
|
|
@@ -6798,7 +7348,12 @@ async function createWorkflow(projectRoot, task, mode, options = {}) {
|
|
|
6798
7348
|
updatedAt: now,
|
|
6799
7349
|
status: "in_progress",
|
|
6800
7350
|
...options.parentTaskId ? { parentTaskId: options.parentTaskId } : {},
|
|
6801
|
-
...mode === "man" || mode === "manteam" ? {
|
|
7351
|
+
...mode === "man" || mode === "manteam" ? {
|
|
7352
|
+
planVersion: 1,
|
|
7353
|
+
reviewPolicyVersion: options.planningPolicyVersion === 2 ? 2 : 1,
|
|
7354
|
+
...options.planningPolicyVersion === 2 ? { verificationPolicyVersion: 1 } : {},
|
|
7355
|
+
...options.planningPolicyVersion ? { planningPolicyVersion: options.planningPolicyVersion } : {}
|
|
7356
|
+
} : {}
|
|
6802
7357
|
};
|
|
6803
7358
|
await writeMetadata(dir, meta);
|
|
6804
7359
|
return meta;
|
|
@@ -6807,7 +7362,7 @@ async function allocateTaskId(projectRoot, baseTaskId) {
|
|
|
6807
7362
|
for (let attempt = 0; attempt < 1e3; attempt++) {
|
|
6808
7363
|
const taskId = attempt === 0 ? baseTaskId : `${baseTaskId}-${attempt + 1}`;
|
|
6809
7364
|
try {
|
|
6810
|
-
await mkdir9(
|
|
7365
|
+
await mkdir9(workflowDir2(projectRoot, taskId));
|
|
6811
7366
|
return taskId;
|
|
6812
7367
|
} catch (err) {
|
|
6813
7368
|
if (isNodeError9(err) && err.code === "EEXIST") {
|
|
@@ -6822,13 +7377,13 @@ async function readWorkflow(projectRoot, taskId) {
|
|
|
6822
7377
|
if (!isValidWorkflowTaskId(taskId)) return null;
|
|
6823
7378
|
const file = metadataPath(projectRoot, taskId);
|
|
6824
7379
|
try {
|
|
6825
|
-
const raw = await
|
|
7380
|
+
const raw = await readFile12(file, "utf-8");
|
|
6826
7381
|
return parseWorkflowMeta(raw, taskId);
|
|
6827
7382
|
} catch {
|
|
6828
7383
|
return null;
|
|
6829
7384
|
}
|
|
6830
7385
|
}
|
|
6831
|
-
async function updateWorkflow(projectRoot, taskId, patch) {
|
|
7386
|
+
async function updateWorkflow(projectRoot, taskId, patch, options = {}) {
|
|
6832
7387
|
assertValidTaskId2(taskId);
|
|
6833
7388
|
const existing = await readWorkflow(projectRoot, taskId);
|
|
6834
7389
|
if (!existing) {
|
|
@@ -6848,8 +7403,8 @@ async function updateWorkflow(projectRoot, taskId, patch) {
|
|
|
6848
7403
|
if (updated.status !== "blocked" && !explicitlyPatchedBlockingReason) {
|
|
6849
7404
|
updated.blockingReason = void 0;
|
|
6850
7405
|
}
|
|
6851
|
-
await validateWorkflowMeta(projectRoot, updated, existing);
|
|
6852
|
-
await writeMetadata(
|
|
7406
|
+
await validateWorkflowMeta(projectRoot, updated, existing, options);
|
|
7407
|
+
await writeMetadata(workflowDir2(projectRoot, taskId), updated);
|
|
6853
7408
|
if (updated.parentTaskId) {
|
|
6854
7409
|
await propagateChildStatus(projectRoot, updated);
|
|
6855
7410
|
}
|
|
@@ -6872,7 +7427,7 @@ async function listWorkflows(projectRoot) {
|
|
|
6872
7427
|
}
|
|
6873
7428
|
async function deleteWorkflow(projectRoot, taskId) {
|
|
6874
7429
|
if (!isValidWorkflowTaskId(taskId)) return false;
|
|
6875
|
-
const dir =
|
|
7430
|
+
const dir = workflowDir2(projectRoot, taskId);
|
|
6876
7431
|
try {
|
|
6877
7432
|
await stat4(dir);
|
|
6878
7433
|
} catch {
|
|
@@ -6886,19 +7441,19 @@ async function deleteWorkflow(projectRoot, taskId) {
|
|
|
6886
7441
|
return true;
|
|
6887
7442
|
}
|
|
6888
7443
|
function workflowsRoot(projectRoot) {
|
|
6889
|
-
return
|
|
7444
|
+
return path25.join(projectRoot, ".mancode", "workflows");
|
|
6890
7445
|
}
|
|
6891
|
-
function
|
|
7446
|
+
function workflowDir2(projectRoot, taskId) {
|
|
6892
7447
|
assertValidTaskId2(taskId);
|
|
6893
|
-
return
|
|
7448
|
+
return path25.join(workflowsRoot(projectRoot), taskId);
|
|
6894
7449
|
}
|
|
6895
7450
|
function metadataPath(projectRoot, taskId) {
|
|
6896
|
-
return
|
|
7451
|
+
return path25.join(workflowDir2(projectRoot, taskId), METADATA_FILE);
|
|
6897
7452
|
}
|
|
6898
7453
|
async function writeMetadata(dir, meta) {
|
|
6899
7454
|
const content = `${JSON.stringify(meta, null, 2)}
|
|
6900
7455
|
`;
|
|
6901
|
-
await
|
|
7456
|
+
await writeFile13(path25.join(dir, METADATA_FILE), content, "utf-8");
|
|
6902
7457
|
}
|
|
6903
7458
|
function parseWorkflowMeta(raw, taskId) {
|
|
6904
7459
|
try {
|
|
@@ -6923,6 +7478,12 @@ function isWorkflowStatus(value) {
|
|
|
6923
7478
|
function isWorkflowOutcome(value) {
|
|
6924
7479
|
return value === "fixed" || value === "verified" || value === "no_repro" || value === "manual_test_required";
|
|
6925
7480
|
}
|
|
7481
|
+
function isRequirementsStatus(value) {
|
|
7482
|
+
return value === "ready" || value === "needs_clarification";
|
|
7483
|
+
}
|
|
7484
|
+
function isPlanDecision(value) {
|
|
7485
|
+
return value === "plan_only" || value === "solo_handoff" || value === "governed_execution";
|
|
7486
|
+
}
|
|
6926
7487
|
async function validateParentTask(projectRoot, mode, parentTaskId, requireStepSix = false) {
|
|
6927
7488
|
if (!parentTaskId) return;
|
|
6928
7489
|
if (mode !== "mamba") {
|
|
@@ -6938,7 +7499,7 @@ async function validateParentTask(projectRoot, mode, parentTaskId, requireStepSi
|
|
|
6938
7499
|
);
|
|
6939
7500
|
}
|
|
6940
7501
|
}
|
|
6941
|
-
async function validateWorkflowMeta(projectRoot, updated, existing) {
|
|
7502
|
+
async function validateWorkflowMeta(projectRoot, updated, existing, options) {
|
|
6942
7503
|
if (updated.mode !== existing.mode) {
|
|
6943
7504
|
throw new Error("workflow mode cannot be changed");
|
|
6944
7505
|
}
|
|
@@ -6951,6 +7512,17 @@ async function validateWorkflowMeta(projectRoot, updated, existing) {
|
|
|
6951
7512
|
if (updated.reviewPolicyVersion !== existing.reviewPolicyVersion) {
|
|
6952
7513
|
throw new Error("workflow review policy version cannot be changed");
|
|
6953
7514
|
}
|
|
7515
|
+
if (updated.planningPolicyVersion !== existing.planningPolicyVersion) {
|
|
7516
|
+
throw new Error("workflow planning policy version cannot be changed");
|
|
7517
|
+
}
|
|
7518
|
+
if (updated.requirementsDigest !== existing.requirementsDigest && existing.currentStep > 2) {
|
|
7519
|
+
throw new Error(
|
|
7520
|
+
"workflow requirements cannot change after planning starts"
|
|
7521
|
+
);
|
|
7522
|
+
}
|
|
7523
|
+
if (updated.verificationPolicyVersion !== existing.verificationPolicyVersion) {
|
|
7524
|
+
throw new Error("workflow verification policy version cannot be changed");
|
|
7525
|
+
}
|
|
6954
7526
|
if (!Number.isInteger(updated.currentStep) || updated.currentStep < 1 || updated.currentStep > maxWorkflowStep(updated.mode)) {
|
|
6955
7527
|
throw new Error(`invalid workflow step: ${updated.currentStep}`);
|
|
6956
7528
|
}
|
|
@@ -6960,6 +7532,13 @@ async function validateWorkflowMeta(projectRoot, updated, existing) {
|
|
|
6960
7532
|
if (!Array.isArray(updated.skippedSteps) || updated.skippedSteps.some((step) => typeof step !== "string")) {
|
|
6961
7533
|
throw new Error("workflow skipped steps must be strings");
|
|
6962
7534
|
}
|
|
7535
|
+
if (updated.reviewPolicyVersion === 2 && updated.skippedSteps.some(
|
|
7536
|
+
(step) => step !== "clarification" && step !== "review"
|
|
7537
|
+
)) {
|
|
7538
|
+
throw new Error(
|
|
7539
|
+
"workflow policy v2 only allows skipping clarification or review"
|
|
7540
|
+
);
|
|
7541
|
+
}
|
|
6963
7542
|
if (!canTransition(existing.status, updated.status)) {
|
|
6964
7543
|
throw new Error(
|
|
6965
7544
|
`invalid workflow status transition: ${existing.status} -> ${updated.status}`
|
|
@@ -6971,7 +7550,7 @@ async function validateWorkflowMeta(projectRoot, updated, existing) {
|
|
|
6971
7550
|
if (updated.status === "planned" && updated.currentStep !== 4) {
|
|
6972
7551
|
throw new Error("planned workflows must be at step 4");
|
|
6973
7552
|
}
|
|
6974
|
-
if (updated.status === "completed" && updated.currentStep !== maxWorkflowStep(updated.mode)) {
|
|
7553
|
+
if (updated.status === "completed" && updated.currentStep !== maxWorkflowStep(updated.mode) && !(updated.currentStep === 4 && updated.planDecision === "solo_handoff")) {
|
|
6975
7554
|
throw new Error(
|
|
6976
7555
|
`completed ${updated.mode === "mamba" ? "manba" : updated.mode} workflows must be at step ${maxWorkflowStep(updated.mode)}`
|
|
6977
7556
|
);
|
|
@@ -7001,12 +7580,106 @@ async function validateWorkflowMeta(projectRoot, updated, existing) {
|
|
|
7001
7580
|
if (updated.mode === "mamba" && updated.planVersion !== void 0) {
|
|
7002
7581
|
throw new Error("manba workflows cannot have a plan version");
|
|
7003
7582
|
}
|
|
7004
|
-
if (updated.reviewPolicyVersion !== void 0 && updated.reviewPolicyVersion !== 1) {
|
|
7583
|
+
if (updated.reviewPolicyVersion !== void 0 && updated.reviewPolicyVersion !== 1 && updated.reviewPolicyVersion !== 2) {
|
|
7005
7584
|
throw new Error("invalid workflow review policy version");
|
|
7006
7585
|
}
|
|
7007
7586
|
if (updated.mode === "mamba" && updated.reviewPolicyVersion !== void 0) {
|
|
7008
7587
|
throw new Error("manba workflows cannot have a review policy version");
|
|
7009
7588
|
}
|
|
7589
|
+
if (updated.planningPolicyVersion !== void 0 && updated.planningPolicyVersion !== 1 && updated.planningPolicyVersion !== 2) {
|
|
7590
|
+
throw new Error("invalid workflow planning policy version");
|
|
7591
|
+
}
|
|
7592
|
+
if (updated.mode === "mamba" && updated.planningPolicyVersion !== void 0) {
|
|
7593
|
+
throw new Error("manba workflows cannot have a planning policy version");
|
|
7594
|
+
}
|
|
7595
|
+
if (updated.verificationPolicyVersion !== void 0 && updated.verificationPolicyVersion !== 1) {
|
|
7596
|
+
throw new Error("invalid workflow verification policy version");
|
|
7597
|
+
}
|
|
7598
|
+
if (updated.mode === "mamba" && updated.verificationPolicyVersion !== void 0) {
|
|
7599
|
+
throw new Error(
|
|
7600
|
+
"manba workflows cannot have a verification policy version"
|
|
7601
|
+
);
|
|
7602
|
+
}
|
|
7603
|
+
if (updated.verificationStatus !== void 0 && !isVerificationStatus(updated.verificationStatus)) {
|
|
7604
|
+
throw new Error("invalid workflow verification status");
|
|
7605
|
+
}
|
|
7606
|
+
if (updated.mode === "mamba" && updated.verificationStatus !== void 0) {
|
|
7607
|
+
throw new Error("manba workflows cannot have a verification status");
|
|
7608
|
+
}
|
|
7609
|
+
if (updated.requirementsStatus !== void 0 && !isRequirementsStatus(updated.requirementsStatus)) {
|
|
7610
|
+
throw new Error("invalid workflow requirements status");
|
|
7611
|
+
}
|
|
7612
|
+
if (updated.mode === "mamba" && updated.requirementsStatus !== void 0) {
|
|
7613
|
+
throw new Error("manba workflows cannot have a requirements status");
|
|
7614
|
+
}
|
|
7615
|
+
if (updated.requirementsDigest !== void 0 && typeof updated.requirementsDigest !== "string") {
|
|
7616
|
+
throw new Error("workflow requirements digest must be a string");
|
|
7617
|
+
}
|
|
7618
|
+
if (updated.mode === "mamba" && updated.requirementsDigest !== void 0) {
|
|
7619
|
+
throw new Error("manba workflows cannot have a requirements digest");
|
|
7620
|
+
}
|
|
7621
|
+
if (updated.planDecision !== void 0 && !isPlanDecision(updated.planDecision)) {
|
|
7622
|
+
throw new Error("invalid workflow plan decision");
|
|
7623
|
+
}
|
|
7624
|
+
if (updated.mode === "mamba" && updated.planDecision !== void 0) {
|
|
7625
|
+
throw new Error("manba workflows cannot have a plan decision");
|
|
7626
|
+
}
|
|
7627
|
+
if (updated.planDecision !== existing.planDecision && existing.planDecision !== void 0) {
|
|
7628
|
+
throw new Error("workflow plan decision cannot be changed");
|
|
7629
|
+
}
|
|
7630
|
+
if (updated.planDecision !== existing.planDecision && existing.currentStep !== 4) {
|
|
7631
|
+
throw new Error("workflow plan decision can only be set at step 4");
|
|
7632
|
+
}
|
|
7633
|
+
if (updated.planningPolicyVersion === 1 || updated.planningPolicyVersion === 2) {
|
|
7634
|
+
if (updated.currentStep >= 3) {
|
|
7635
|
+
if (updated.requirementsStatus !== "ready") {
|
|
7636
|
+
throw new Error("requirements must be ready before planning");
|
|
7637
|
+
}
|
|
7638
|
+
if (!await workflowArtifactExists(
|
|
7639
|
+
projectRoot,
|
|
7640
|
+
updated.taskId,
|
|
7641
|
+
"requirements.md"
|
|
7642
|
+
)) {
|
|
7643
|
+
throw new Error("requirements.md is required before planning");
|
|
7644
|
+
}
|
|
7645
|
+
if (updated.planningPolicyVersion === 2) {
|
|
7646
|
+
const requirements = await readRequirementsLedger(
|
|
7647
|
+
projectRoot,
|
|
7648
|
+
updated.taskId
|
|
7649
|
+
);
|
|
7650
|
+
if (!requirements || !requirementsAreReady(requirements) || updated.requirementsDigest !== requirementsDigest(requirements)) {
|
|
7651
|
+
throw new Error(
|
|
7652
|
+
"finalized requirements.json with no blocking unknowns is required before planning"
|
|
7653
|
+
);
|
|
7654
|
+
}
|
|
7655
|
+
}
|
|
7656
|
+
}
|
|
7657
|
+
if (updated.currentStep >= 4 && !await workflowArtifactExists(projectRoot, updated.taskId, "plan.md")) {
|
|
7658
|
+
throw new Error("plan.md is required before the plan gate");
|
|
7659
|
+
}
|
|
7660
|
+
if (updated.status === "planned" && updated.planDecision !== "plan_only" && updated.planDecision !== "solo_handoff") {
|
|
7661
|
+
throw new Error(
|
|
7662
|
+
"planned workflows require a plan-only or solo-handoff decision"
|
|
7663
|
+
);
|
|
7664
|
+
}
|
|
7665
|
+
if (updated.currentStep >= 5 && updated.status === "in_progress" && updated.planDecision !== "governed_execution") {
|
|
7666
|
+
throw new Error("governed execution must be confirmed before step 5");
|
|
7667
|
+
}
|
|
7668
|
+
}
|
|
7669
|
+
if (updated.verificationPolicyVersion === 1 && updated.currentStep >= 7 && updated.planDecision !== "solo_handoff" && !options.allowIncompleteVerification && !await verificationCanAdvance(
|
|
7670
|
+
projectRoot,
|
|
7671
|
+
updated.taskId,
|
|
7672
|
+
updated.planVersion ?? 1
|
|
7673
|
+
)) {
|
|
7674
|
+
throw new Error(
|
|
7675
|
+
"workflow verification is incomplete, stale, or requires manual confirmation"
|
|
7676
|
+
);
|
|
7677
|
+
}
|
|
7678
|
+
if (existing.status === "blocked" && existing.blockingReason?.startsWith("[verification]") && updated.status === "in_progress" && (updated.verificationStatus === "manual_required" || updated.verificationStatus === "blocked")) {
|
|
7679
|
+
throw new Error(
|
|
7680
|
+
"verification-blocked workflows must resume through verify"
|
|
7681
|
+
);
|
|
7682
|
+
}
|
|
7010
7683
|
if (updated.planVersion !== void 0 && (!Number.isInteger(updated.planVersion) || updated.planVersion < 1)) {
|
|
7011
7684
|
throw new Error("workflow plan version must be a positive integer");
|
|
7012
7685
|
}
|
|
@@ -7020,16 +7693,22 @@ async function validateWorkflowMeta(projectRoot, updated, existing) {
|
|
|
7020
7693
|
if ((isTerminalWorkflowStatus(updated.status) || updated.status === "planned") && (await listActiveMambaChildren(projectRoot, updated.taskId)).length > 0) {
|
|
7021
7694
|
throw new Error("cannot finish workflow with an active manba child");
|
|
7022
7695
|
}
|
|
7023
|
-
if (updated.status === "completed" && updated.reviewPolicyVersion === 1 && !reviewWasExplicitlySkipped(updated) && !await reviewCanComplete(projectRoot, updated.taskId)) {
|
|
7696
|
+
if (updated.status === "completed" && updated.reviewPolicyVersion === 1 && updated.planDecision !== "solo_handoff" && !reviewWasExplicitlySkipped(updated) && !await reviewCanComplete(projectRoot, updated.taskId)) {
|
|
7697
|
+
throw new Error("workflow review is incomplete or still has open blockers");
|
|
7698
|
+
}
|
|
7699
|
+
if (updated.status === "completed" && updated.reviewPolicyVersion === 2 && updated.planDecision !== "solo_handoff" && !await reviewCanComplete(projectRoot, updated.taskId)) {
|
|
7024
7700
|
throw new Error("workflow review is incomplete or still has open blockers");
|
|
7025
7701
|
}
|
|
7702
|
+
if (updated.status === "completed" && updated.verificationPolicyVersion === 1 && updated.planDecision !== "solo_handoff" && !await workflowArtifactExists(projectRoot, updated.taskId, "summary.md")) {
|
|
7703
|
+
throw new Error("summary.md is required before workflow completion");
|
|
7704
|
+
}
|
|
7026
7705
|
}
|
|
7027
7706
|
function canTransition(from, to) {
|
|
7028
7707
|
if (from === to) return true;
|
|
7029
7708
|
if (from === "in_progress") return true;
|
|
7030
|
-
if (from === "planned"
|
|
7031
|
-
return to === "in_progress" || to === "abandoned";
|
|
7032
|
-
|
|
7709
|
+
if (from === "planned")
|
|
7710
|
+
return to === "in_progress" || to === "completed" || to === "abandoned";
|
|
7711
|
+
if (from === "blocked") return to === "in_progress" || to === "abandoned";
|
|
7033
7712
|
return false;
|
|
7034
7713
|
}
|
|
7035
7714
|
function isTerminalWorkflowStatus(status2) {
|
|
@@ -7050,7 +7729,19 @@ function isWorkflowShape(obj) {
|
|
|
7050
7729
|
return false;
|
|
7051
7730
|
if (obj.planVersion !== void 0 && (!Number.isInteger(obj.planVersion) || obj.planVersion < 1))
|
|
7052
7731
|
return false;
|
|
7053
|
-
if (obj.reviewPolicyVersion !== void 0 && obj.reviewPolicyVersion !== 1)
|
|
7732
|
+
if (obj.reviewPolicyVersion !== void 0 && obj.reviewPolicyVersion !== 1 && obj.reviewPolicyVersion !== 2)
|
|
7733
|
+
return false;
|
|
7734
|
+
if (obj.planningPolicyVersion !== void 0 && obj.planningPolicyVersion !== 1 && obj.planningPolicyVersion !== 2)
|
|
7735
|
+
return false;
|
|
7736
|
+
if (obj.verificationPolicyVersion !== void 0 && obj.verificationPolicyVersion !== 1)
|
|
7737
|
+
return false;
|
|
7738
|
+
if (obj.requirementsStatus !== void 0 && !isRequirementsStatus(obj.requirementsStatus))
|
|
7739
|
+
return false;
|
|
7740
|
+
if (obj.requirementsDigest !== void 0 && typeof obj.requirementsDigest !== "string")
|
|
7741
|
+
return false;
|
|
7742
|
+
if (obj.planDecision !== void 0 && !isPlanDecision(obj.planDecision))
|
|
7743
|
+
return false;
|
|
7744
|
+
if (obj.verificationStatus !== void 0 && !isVerificationStatus(obj.verificationStatus))
|
|
7054
7745
|
return false;
|
|
7055
7746
|
if (status2 === "blocked") {
|
|
7056
7747
|
if (!obj.blockingReason?.trim()) return false;
|
|
@@ -7058,7 +7749,7 @@ function isWorkflowShape(obj) {
|
|
|
7058
7749
|
return false;
|
|
7059
7750
|
}
|
|
7060
7751
|
if (mode === "mamba") {
|
|
7061
|
-
if (status2 === "planned" || obj.planVersion !== void 0 || obj.reviewPolicyVersion !== void 0)
|
|
7752
|
+
if (status2 === "planned" || obj.planVersion !== void 0 || obj.reviewPolicyVersion !== void 0 || obj.planningPolicyVersion !== void 0 || obj.verificationPolicyVersion !== void 0 || obj.requirementsStatus !== void 0 || obj.requirementsDigest !== void 0 || obj.planDecision !== void 0 || obj.verificationStatus !== void 0)
|
|
7062
7753
|
return false;
|
|
7063
7754
|
if (obj.parentTaskId !== void 0 && !isValidWorkflowTaskId(obj.parentTaskId))
|
|
7064
7755
|
return false;
|
|
@@ -7069,9 +7760,20 @@ function isWorkflowShape(obj) {
|
|
|
7069
7760
|
}
|
|
7070
7761
|
return true;
|
|
7071
7762
|
}
|
|
7763
|
+
async function workflowArtifactExists(projectRoot, taskId, filename) {
|
|
7764
|
+
try {
|
|
7765
|
+
await stat4(path25.join(workflowDir2(projectRoot, taskId), filename));
|
|
7766
|
+
return true;
|
|
7767
|
+
} catch {
|
|
7768
|
+
return false;
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
7072
7771
|
function reviewWasExplicitlySkipped(meta) {
|
|
7073
7772
|
return meta.skippedSteps.includes("review") || meta.skippedSteps.includes("film-1") && meta.skippedSteps.includes("film-2");
|
|
7074
7773
|
}
|
|
7774
|
+
function isVerificationStatus(value) {
|
|
7775
|
+
return value === "pending" || value === "passed" || value === "failed" || value === "manual_required" || value === "blocked";
|
|
7776
|
+
}
|
|
7075
7777
|
async function listActiveMambaChildren(projectRoot, parentTaskId) {
|
|
7076
7778
|
return (await listMambaChildren(projectRoot, parentTaskId)).filter(
|
|
7077
7779
|
(workflow2) => workflow2.status === "in_progress" || workflow2.status === "blocked"
|
|
@@ -7090,7 +7792,7 @@ async function propagateChildStatus(projectRoot, child) {
|
|
|
7090
7792
|
const parent = await readWorkflow(projectRoot, parentTaskId);
|
|
7091
7793
|
if (!parent || parent.status !== "in_progress") return;
|
|
7092
7794
|
const reason = child.status === "blocked" ? `manba child ${child.taskId} is blocked: ${child.blockingReason ?? "unknown reason"}` : `manba child ${child.taskId} requires manual testing`;
|
|
7093
|
-
await writeMetadata(
|
|
7795
|
+
await writeMetadata(workflowDir2(projectRoot, parent.taskId), {
|
|
7094
7796
|
...parent,
|
|
7095
7797
|
status: "blocked",
|
|
7096
7798
|
blockingReason: reason,
|
|
@@ -7119,7 +7821,7 @@ var EXIT_OK7 = 0;
|
|
|
7119
7821
|
var EXIT_NOT_INITIALIZED5 = 1;
|
|
7120
7822
|
var EXIT_CORRUPT_STATE2 = 2;
|
|
7121
7823
|
async function status(rootDir = process10.cwd(), options = {}) {
|
|
7122
|
-
const stateFile =
|
|
7824
|
+
const stateFile = path26.join(rootDir, ".mancode", "state.json");
|
|
7123
7825
|
if (!await pathExists10(stateFile)) {
|
|
7124
7826
|
console.error("\u2717 mancode not initialized.");
|
|
7125
7827
|
console.error(" Run `mancode init` to get started.");
|
|
@@ -7170,6 +7872,7 @@ async function status(rootDir = process10.cwd(), options = {}) {
|
|
|
7170
7872
|
hookInjection,
|
|
7171
7873
|
team: effectiveTeam,
|
|
7172
7874
|
currentWorkflow,
|
|
7875
|
+
activeSoloPlan: state.activeSoloPlan ?? null,
|
|
7173
7876
|
platformStatus: {},
|
|
7174
7877
|
projectRefreshRecommended
|
|
7175
7878
|
};
|
|
@@ -7183,10 +7886,10 @@ async function status(rootDir = process10.cwd(), options = {}) {
|
|
|
7183
7886
|
}
|
|
7184
7887
|
async function shouldRefreshProject(rootDir, state) {
|
|
7185
7888
|
if (state.projectMode !== "generic") return false;
|
|
7186
|
-
const hasGit = await pathExists10(
|
|
7889
|
+
const hasGit = await pathExists10(path26.join(rootDir, ".git"));
|
|
7187
7890
|
if (hasGit) return true;
|
|
7188
7891
|
for (const manifest of PROJECT_MANIFESTS) {
|
|
7189
|
-
if (await pathExists10(
|
|
7892
|
+
if (await pathExists10(path26.join(rootDir, manifest))) return true;
|
|
7190
7893
|
}
|
|
7191
7894
|
return false;
|
|
7192
7895
|
}
|
|
@@ -7230,19 +7933,19 @@ async function getCurrentWorkflow(rootDir, taskId) {
|
|
|
7230
7933
|
}
|
|
7231
7934
|
async function getProjectName(rootDir) {
|
|
7232
7935
|
try {
|
|
7233
|
-
const raw = await fs9.readFile(
|
|
7936
|
+
const raw = await fs9.readFile(path26.join(rootDir, "package.json"), "utf-8");
|
|
7234
7937
|
const pkg = JSON.parse(raw);
|
|
7235
7938
|
if (pkg.name && typeof pkg.name === "string") {
|
|
7236
7939
|
return pkg.name;
|
|
7237
7940
|
}
|
|
7238
7941
|
} catch {
|
|
7239
7942
|
}
|
|
7240
|
-
return
|
|
7943
|
+
return path26.basename(rootDir);
|
|
7241
7944
|
}
|
|
7242
7945
|
async function readConfig2(rootDir) {
|
|
7243
7946
|
try {
|
|
7244
7947
|
const raw = await fs9.readFile(
|
|
7245
|
-
|
|
7948
|
+
path26.join(rootDir, ".mancode", "config.json"),
|
|
7246
7949
|
"utf-8"
|
|
7247
7950
|
);
|
|
7248
7951
|
return JSON.parse(raw);
|
|
@@ -7270,9 +7973,9 @@ function getEffectiveTeamStatus(state, config, detected) {
|
|
|
7270
7973
|
}
|
|
7271
7974
|
async function checkHooks(rootDir) {
|
|
7272
7975
|
const [sessionStart, userPromptSubmit, registered] = await Promise.all([
|
|
7273
|
-
pathExists10(
|
|
7976
|
+
pathExists10(path26.join(rootDir, ".mancode", "hooks", "session-start.mjs")),
|
|
7274
7977
|
pathExists10(
|
|
7275
|
-
|
|
7978
|
+
path26.join(rootDir, ".mancode", "hooks", "user-prompt-submit.mjs")
|
|
7276
7979
|
),
|
|
7277
7980
|
isRegistered(rootDir)
|
|
7278
7981
|
]);
|
|
@@ -7281,7 +7984,7 @@ async function checkHooks(rootDir) {
|
|
|
7281
7984
|
async function isRegistered(rootDir) {
|
|
7282
7985
|
try {
|
|
7283
7986
|
const raw = await fs9.readFile(
|
|
7284
|
-
|
|
7987
|
+
path26.join(rootDir, ".claude", "settings.json"),
|
|
7285
7988
|
"utf-8"
|
|
7286
7989
|
);
|
|
7287
7990
|
const settings = JSON.parse(raw);
|
|
@@ -7291,9 +7994,9 @@ async function isRegistered(rootDir) {
|
|
|
7291
7994
|
}
|
|
7292
7995
|
}
|
|
7293
7996
|
function hooksRegistered2(settings) {
|
|
7294
|
-
if (!
|
|
7997
|
+
if (!isRecord10(settings)) return false;
|
|
7295
7998
|
const hooks = settings.hooks;
|
|
7296
|
-
if (!
|
|
7999
|
+
if (!isRecord10(hooks)) return false;
|
|
7297
8000
|
return hasHookCommand2(hooks.SessionStart, ".mancode/hooks/session-start.mjs") && hasHookCommand2(
|
|
7298
8001
|
hooks.UserPromptSubmit,
|
|
7299
8002
|
".mancode/hooks/user-prompt-submit.mjs"
|
|
@@ -7302,15 +8005,15 @@ function hooksRegistered2(settings) {
|
|
|
7302
8005
|
function hasHookCommand2(value, needle) {
|
|
7303
8006
|
if (!Array.isArray(value)) return false;
|
|
7304
8007
|
return value.some((group) => {
|
|
7305
|
-
if (!
|
|
8008
|
+
if (!isRecord10(group) || !Array.isArray(group.hooks)) return false;
|
|
7306
8009
|
return group.hooks.some((hook) => {
|
|
7307
|
-
if (!
|
|
8010
|
+
if (!isRecord10(hook) || typeof hook.command !== "string") return false;
|
|
7308
8011
|
return hook.command.includes(needle);
|
|
7309
8012
|
});
|
|
7310
8013
|
});
|
|
7311
8014
|
}
|
|
7312
8015
|
async function estimateHookInjection(rootDir) {
|
|
7313
|
-
const hookPath =
|
|
8016
|
+
const hookPath = path26.join(
|
|
7314
8017
|
rootDir,
|
|
7315
8018
|
".mancode",
|
|
7316
8019
|
"hooks",
|
|
@@ -7402,6 +8105,11 @@ function printText(r) {
|
|
|
7402
8105
|
);
|
|
7403
8106
|
}
|
|
7404
8107
|
}
|
|
8108
|
+
if (r.activeSoloPlan) {
|
|
8109
|
+
console.log(
|
|
8110
|
+
`Solo plan: ${r.activeSoloPlan.taskId} (plan v${r.activeSoloPlan.planVersion})`
|
|
8111
|
+
);
|
|
8112
|
+
}
|
|
7405
8113
|
console.log("");
|
|
7406
8114
|
console.log("Installed platforms:");
|
|
7407
8115
|
for (const p of r.platforms) {
|
|
@@ -7442,7 +8150,7 @@ function formatTeamStatus(team) {
|
|
|
7442
8150
|
function workflowStepMax(mode) {
|
|
7443
8151
|
return maxWorkflowStep(mode);
|
|
7444
8152
|
}
|
|
7445
|
-
function
|
|
8153
|
+
function isRecord10(value) {
|
|
7446
8154
|
return typeof value === "object" && value !== null;
|
|
7447
8155
|
}
|
|
7448
8156
|
async function pathExists10(p) {
|
|
@@ -7455,14 +8163,14 @@ async function pathExists10(p) {
|
|
|
7455
8163
|
}
|
|
7456
8164
|
|
|
7457
8165
|
// src/commands/uninstall.ts
|
|
7458
|
-
import { access as access5, readFile as
|
|
7459
|
-
import
|
|
8166
|
+
import { access as access5, readFile as readFile13, rm as rm5, writeFile as writeFile14 } from "fs/promises";
|
|
8167
|
+
import path27 from "path";
|
|
7460
8168
|
import process11 from "process";
|
|
7461
8169
|
var EXIT_OK8 = 0;
|
|
7462
8170
|
var EXIT_NOT_INITIALIZED6 = 1;
|
|
7463
8171
|
var EXIT_UNSUPPORTED_PLATFORM2 = 2;
|
|
7464
8172
|
async function uninstall(rootDir = process11.cwd(), platform, options = {}) {
|
|
7465
|
-
const stateFile =
|
|
8173
|
+
const stateFile = path27.join(rootDir, ".mancode", "state.json");
|
|
7466
8174
|
if (!await pathExists11(stateFile)) {
|
|
7467
8175
|
console.error("\u2717 mancode not initialized.");
|
|
7468
8176
|
console.error(" Run `mancode init` first.");
|
|
@@ -7514,7 +8222,7 @@ async function uninstallAll(rootDir) {
|
|
|
7514
8222
|
await uninstallPlatform(rootDir, p);
|
|
7515
8223
|
}
|
|
7516
8224
|
console.log("\u2713 Removing .mancode/ directory...");
|
|
7517
|
-
await rm5(
|
|
8225
|
+
await rm5(path27.join(rootDir, ".mancode"), {
|
|
7518
8226
|
recursive: true,
|
|
7519
8227
|
force: true
|
|
7520
8228
|
});
|
|
@@ -7524,10 +8232,10 @@ async function uninstallClaudeCode(rootDir) {
|
|
|
7524
8232
|
await cleanClaudeSettings(rootDir);
|
|
7525
8233
|
}
|
|
7526
8234
|
async function cleanClaudeSettings(rootDir) {
|
|
7527
|
-
const settingsPath =
|
|
8235
|
+
const settingsPath = path27.join(rootDir, ".claude", "settings.json");
|
|
7528
8236
|
let content;
|
|
7529
8237
|
try {
|
|
7530
|
-
content = await
|
|
8238
|
+
content = await readFile13(settingsPath, "utf-8");
|
|
7531
8239
|
} catch {
|
|
7532
8240
|
return;
|
|
7533
8241
|
}
|
|
@@ -7538,14 +8246,14 @@ async function cleanClaudeSettings(rootDir) {
|
|
|
7538
8246
|
return;
|
|
7539
8247
|
}
|
|
7540
8248
|
const cleanedHooks = {};
|
|
7541
|
-
if (
|
|
8249
|
+
if (isRecord11(settings.hooks)) {
|
|
7542
8250
|
for (const [event, value] of Object.entries(settings.hooks)) {
|
|
7543
8251
|
const cleaned = cleanClaudeHookValue(value);
|
|
7544
8252
|
if (cleaned !== void 0) cleanedHooks[event] = cleaned;
|
|
7545
8253
|
}
|
|
7546
8254
|
settings.hooks = Object.keys(cleanedHooks).length > 0 ? cleanedHooks : void 0;
|
|
7547
8255
|
}
|
|
7548
|
-
if (
|
|
8256
|
+
if (isRecord11(settings.skills)) {
|
|
7549
8257
|
const retainedSkills = Object.fromEntries(
|
|
7550
8258
|
Object.entries(settings.skills).filter(
|
|
7551
8259
|
([name, value]) => LEGACY_CLAUDE_SKILL_SETTINGS[name] !== value
|
|
@@ -7553,7 +8261,7 @@ async function cleanClaudeSettings(rootDir) {
|
|
|
7553
8261
|
);
|
|
7554
8262
|
settings.skills = Object.keys(retainedSkills).length > 0 ? retainedSkills : void 0;
|
|
7555
8263
|
}
|
|
7556
|
-
await
|
|
8264
|
+
await writeFile14(
|
|
7557
8265
|
settingsPath,
|
|
7558
8266
|
`${JSON.stringify(settings, null, 2)}
|
|
7559
8267
|
`,
|
|
@@ -7565,7 +8273,7 @@ function cleanClaudeHookValue(value) {
|
|
|
7565
8273
|
const entries2 = value.flatMap(cleanClaudeHookEntry);
|
|
7566
8274
|
return entries2.length > 0 ? entries2 : void 0;
|
|
7567
8275
|
}
|
|
7568
|
-
if (!
|
|
8276
|
+
if (!isRecord11(value)) return value;
|
|
7569
8277
|
if (Array.isArray(value.hooks) || typeof value.command === "string") {
|
|
7570
8278
|
const entries2 = cleanClaudeHookEntry(value);
|
|
7571
8279
|
return entries2.length > 0 ? entries2 : void 0;
|
|
@@ -7579,7 +8287,7 @@ function cleanClaudeHookValue(value) {
|
|
|
7579
8287
|
}
|
|
7580
8288
|
function cleanClaudeHookEntry(entry) {
|
|
7581
8289
|
if (Array.isArray(entry)) return entry.flatMap(cleanClaudeHookEntry);
|
|
7582
|
-
if (!
|
|
8290
|
+
if (!isRecord11(entry)) return [entry];
|
|
7583
8291
|
if (Array.isArray(entry.hooks)) {
|
|
7584
8292
|
const hooks = entry.hooks.filter((hook) => !isMancodeHookEntry(hook));
|
|
7585
8293
|
return hooks.length > 0 ? [{ ...entry, hooks }] : [];
|
|
@@ -7587,11 +8295,11 @@ function cleanClaudeHookEntry(entry) {
|
|
|
7587
8295
|
return isMancodeHookEntry(entry) ? [] : [entry];
|
|
7588
8296
|
}
|
|
7589
8297
|
function isMancodeHookEntry(value) {
|
|
7590
|
-
return
|
|
8298
|
+
return isRecord11(value) && typeof value.command === "string" && isGeneratedMancodeHookCommand(value.command);
|
|
7591
8299
|
}
|
|
7592
8300
|
function containsLegacyMancodeHookPath2(value) {
|
|
7593
8301
|
if (Array.isArray(value)) return value.some(containsLegacyMancodeHookPath2);
|
|
7594
|
-
if (!
|
|
8302
|
+
if (!isRecord11(value)) return false;
|
|
7595
8303
|
if (typeof value.command === "string" && value.command.includes(".mancode/hooks/")) {
|
|
7596
8304
|
return true;
|
|
7597
8305
|
}
|
|
@@ -7602,12 +8310,12 @@ async function uninstallCursor(rootDir) {
|
|
|
7602
8310
|
await removeCursorCommands(rootDir);
|
|
7603
8311
|
}
|
|
7604
8312
|
async function uninstallCodex(rootDir) {
|
|
7605
|
-
const agentsPath =
|
|
8313
|
+
const agentsPath = path27.join(rootDir, "AGENTS.md");
|
|
7606
8314
|
try {
|
|
7607
|
-
const content = await
|
|
8315
|
+
const content = await readFile13(agentsPath, "utf-8");
|
|
7608
8316
|
const cleaned = removeManagedBlock(content);
|
|
7609
8317
|
if (cleaned.trim()) {
|
|
7610
|
-
await
|
|
8318
|
+
await writeFile14(agentsPath, `${cleaned}
|
|
7611
8319
|
`, "utf-8");
|
|
7612
8320
|
} else {
|
|
7613
8321
|
await rm5(agentsPath, { force: true });
|
|
@@ -7617,16 +8325,16 @@ async function uninstallCodex(rootDir) {
|
|
|
7617
8325
|
await removeCodexSkills(rootDir);
|
|
7618
8326
|
}
|
|
7619
8327
|
async function uninstallCopilot(rootDir) {
|
|
7620
|
-
const instructionsPath =
|
|
8328
|
+
const instructionsPath = path27.join(
|
|
7621
8329
|
rootDir,
|
|
7622
8330
|
".github",
|
|
7623
8331
|
"copilot-instructions.md"
|
|
7624
8332
|
);
|
|
7625
8333
|
try {
|
|
7626
|
-
const content = await
|
|
8334
|
+
const content = await readFile13(instructionsPath, "utf-8");
|
|
7627
8335
|
const cleaned = removeManagedBlock(content);
|
|
7628
8336
|
if (cleaned.trim()) {
|
|
7629
|
-
await
|
|
8337
|
+
await writeFile14(instructionsPath, `${cleaned}
|
|
7630
8338
|
`, "utf-8");
|
|
7631
8339
|
} else {
|
|
7632
8340
|
await rm5(instructionsPath, { force: true });
|
|
@@ -7636,16 +8344,16 @@ async function uninstallCopilot(rootDir) {
|
|
|
7636
8344
|
await removeCopilotPrompts(rootDir);
|
|
7637
8345
|
}
|
|
7638
8346
|
async function uninstallZcode(rootDir) {
|
|
7639
|
-
const agentsPath =
|
|
8347
|
+
const agentsPath = path27.join(rootDir, "AGENTS.md");
|
|
7640
8348
|
try {
|
|
7641
|
-
const content = await
|
|
8349
|
+
const content = await readFile13(agentsPath, "utf-8");
|
|
7642
8350
|
const cleaned = removeManagedBlock(
|
|
7643
8351
|
content,
|
|
7644
8352
|
ZCODE_MANCODE_START_MARKER,
|
|
7645
8353
|
ZCODE_MANCODE_END_MARKER
|
|
7646
8354
|
);
|
|
7647
8355
|
if (cleaned.trim()) {
|
|
7648
|
-
await
|
|
8356
|
+
await writeFile14(agentsPath, `${cleaned}
|
|
7649
8357
|
`, "utf-8");
|
|
7650
8358
|
} else {
|
|
7651
8359
|
await rm5(agentsPath, { force: true });
|
|
@@ -7655,13 +8363,13 @@ async function uninstallZcode(rootDir) {
|
|
|
7655
8363
|
await removeZcodeSkills(rootDir);
|
|
7656
8364
|
}
|
|
7657
8365
|
async function removeFromConfig(rootDir, platform) {
|
|
7658
|
-
const configPath =
|
|
8366
|
+
const configPath = path27.join(rootDir, ".mancode", "config.json");
|
|
7659
8367
|
try {
|
|
7660
|
-
const raw = await
|
|
8368
|
+
const raw = await readFile13(configPath, "utf-8");
|
|
7661
8369
|
const config = JSON.parse(raw);
|
|
7662
8370
|
if (Array.isArray(config.platforms)) {
|
|
7663
8371
|
config.platforms = config.platforms.filter((p) => p !== platform);
|
|
7664
|
-
if (
|
|
8372
|
+
if (isRecord11(config.platformOptions)) {
|
|
7665
8373
|
const platformOptions = Object.fromEntries(
|
|
7666
8374
|
Object.entries(config.platformOptions).filter(
|
|
7667
8375
|
([name]) => name !== platform
|
|
@@ -7669,7 +8377,7 @@ async function removeFromConfig(rootDir, platform) {
|
|
|
7669
8377
|
);
|
|
7670
8378
|
config.platformOptions = Object.keys(platformOptions).length > 0 ? platformOptions : void 0;
|
|
7671
8379
|
}
|
|
7672
|
-
await
|
|
8380
|
+
await writeFile14(
|
|
7673
8381
|
configPath,
|
|
7674
8382
|
`${JSON.stringify(config, null, 2)}
|
|
7675
8383
|
`,
|
|
@@ -7679,7 +8387,7 @@ async function removeFromConfig(rootDir, platform) {
|
|
|
7679
8387
|
} catch {
|
|
7680
8388
|
}
|
|
7681
8389
|
}
|
|
7682
|
-
function
|
|
8390
|
+
function isRecord11(value) {
|
|
7683
8391
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7684
8392
|
}
|
|
7685
8393
|
async function pathExists11(p) {
|
|
@@ -7702,13 +8410,13 @@ function version() {
|
|
|
7702
8410
|
}
|
|
7703
8411
|
|
|
7704
8412
|
// src/commands/workflow.ts
|
|
7705
|
-
import { access as access6 } from "fs/promises";
|
|
7706
|
-
import
|
|
8413
|
+
import { access as access6, readFile as readFile14, rm as rm6, writeFile as writeFile15 } from "fs/promises";
|
|
8414
|
+
import path28 from "path";
|
|
7707
8415
|
var EXIT_OK9 = 0;
|
|
7708
8416
|
var EXIT_NOT_INITIALIZED7 = 1;
|
|
7709
8417
|
var EXIT_INVALID_ARG2 = 2;
|
|
7710
8418
|
async function workflow(rootDir, subcommand, args = [], options = {}) {
|
|
7711
|
-
if (!await pathExists12(
|
|
8419
|
+
if (!await pathExists12(path28.join(rootDir, ".mancode", "state.json"))) {
|
|
7712
8420
|
if (options.json) {
|
|
7713
8421
|
console.log(JSON.stringify({ error: "not initialized" }, null, 2));
|
|
7714
8422
|
} else {
|
|
@@ -7726,8 +8434,16 @@ async function workflow(rootDir, subcommand, args = [], options = {}) {
|
|
|
7726
8434
|
return workflowList(rootDir, options);
|
|
7727
8435
|
case "show":
|
|
7728
8436
|
return workflowShow(rootDir, args[0], options);
|
|
8437
|
+
case "handoff":
|
|
8438
|
+
return workflowHandoff(rootDir, args[0], options);
|
|
8439
|
+
case "decide":
|
|
8440
|
+
return workflowDecide(rootDir, args[0], options);
|
|
7729
8441
|
case "review":
|
|
7730
8442
|
return workflowReview(rootDir, args, options);
|
|
8443
|
+
case "requirements":
|
|
8444
|
+
return workflowRequirements(rootDir, args, options);
|
|
8445
|
+
case "verify":
|
|
8446
|
+
return workflowVerify(rootDir, args, options);
|
|
7731
8447
|
case "clean":
|
|
7732
8448
|
return workflowClean(rootDir, options);
|
|
7733
8449
|
default:
|
|
@@ -7740,13 +8456,279 @@ async function workflow(rootDir, subcommand, args = [], options = {}) {
|
|
|
7740
8456
|
)
|
|
7741
8457
|
);
|
|
7742
8458
|
} else {
|
|
7743
|
-
console.error(`\u2717 Invalid workflow subcommand: ${subcommand}`);
|
|
7744
|
-
console.error(
|
|
7745
|
-
" Use: create <man|manba|manteam> <task> |
|
|
8459
|
+
console.error(`\u2717 Invalid workflow subcommand: ${subcommand}`);
|
|
8460
|
+
console.error(
|
|
8461
|
+
" Use: create <man|manba|manteam> <task> | requirements <taskId> finalize --file <path> | verify <taskId> <init|record|require-manual|confirm-manual|show> | decide <taskId> --plan-decision <plan_only|governed_execution> | handoff <taskId> --to solo | review <taskId> <init|complete|remediate|skip|show> | list | show <taskId> | clean"
|
|
8462
|
+
);
|
|
8463
|
+
}
|
|
8464
|
+
return EXIT_INVALID_ARG2;
|
|
8465
|
+
}
|
|
8466
|
+
}
|
|
8467
|
+
async function workflowRequirements(rootDir, args, options) {
|
|
8468
|
+
const taskId = args[0];
|
|
8469
|
+
const action = args[1];
|
|
8470
|
+
if (!taskId || !isValidWorkflowTaskId(taskId)) {
|
|
8471
|
+
return invalidArg(options, `invalid taskId: ${taskId ?? ""}`);
|
|
8472
|
+
}
|
|
8473
|
+
if (action !== "finalize" || !options.file) {
|
|
8474
|
+
return invalidArg(options, "requirements requires finalize --file <path>");
|
|
8475
|
+
}
|
|
8476
|
+
const meta = await readWorkflow(rootDir, taskId);
|
|
8477
|
+
if (!meta || meta.mode !== "man" && meta.mode !== "manteam" || meta.status !== "in_progress" || meta.currentStep > 2) {
|
|
8478
|
+
return invalidArg(
|
|
8479
|
+
options,
|
|
8480
|
+
"requirements can only be finalized for an in-progress man or manteam workflow at step 1 or 2"
|
|
8481
|
+
);
|
|
8482
|
+
}
|
|
8483
|
+
const workflowPath = path28.join(rootDir, ".mancode", "workflows", taskId);
|
|
8484
|
+
const jsonPath = path28.join(workflowPath, "requirements.json");
|
|
8485
|
+
const markdownPath = path28.join(workflowPath, "requirements.md");
|
|
8486
|
+
const metadataPath2 = path28.join(workflowPath, "metadata.json");
|
|
8487
|
+
let originalJson;
|
|
8488
|
+
let originalMarkdown;
|
|
8489
|
+
let originalMetadata;
|
|
8490
|
+
try {
|
|
8491
|
+
const inputPath = path28.isAbsolute(options.file) ? options.file : path28.resolve(rootDir, options.file);
|
|
8492
|
+
const input = await readFile14(inputPath, "utf-8");
|
|
8493
|
+
const requirements = parseRequirementsLedger(input);
|
|
8494
|
+
[originalJson, originalMarkdown, originalMetadata] = await Promise.all([
|
|
8495
|
+
readOptionalText2(jsonPath),
|
|
8496
|
+
readOptionalText2(markdownPath),
|
|
8497
|
+
readFile14(metadataPath2, "utf-8")
|
|
8498
|
+
]);
|
|
8499
|
+
await writeRequirementsArtifacts(rootDir, taskId, requirements);
|
|
8500
|
+
await updateWorkflow(rootDir, taskId, {
|
|
8501
|
+
requirementsStatus: requirementsAreReady(requirements) ? "ready" : "needs_clarification",
|
|
8502
|
+
requirementsDigest: requirementsDigest(requirements)
|
|
8503
|
+
});
|
|
8504
|
+
if (options.json) {
|
|
8505
|
+
console.log(
|
|
8506
|
+
JSON.stringify(
|
|
8507
|
+
{
|
|
8508
|
+
requirements,
|
|
8509
|
+
requirementsStatus: requirementsAreReady(requirements) ? "ready" : "needs_clarification"
|
|
8510
|
+
},
|
|
8511
|
+
null,
|
|
8512
|
+
2
|
|
8513
|
+
)
|
|
8514
|
+
);
|
|
8515
|
+
} else {
|
|
8516
|
+
console.log(
|
|
8517
|
+
`Finalized requirements: ${taskId} (${requirementsAreReady(requirements) ? "ready" : "needs_clarification"})`
|
|
8518
|
+
);
|
|
8519
|
+
}
|
|
8520
|
+
return EXIT_OK9;
|
|
8521
|
+
} catch (error) {
|
|
8522
|
+
let rollbackIncomplete = false;
|
|
8523
|
+
if (originalJson !== void 0 && originalMarkdown !== void 0 && originalMetadata !== void 0) {
|
|
8524
|
+
const rollback = await Promise.allSettled([
|
|
8525
|
+
restoreOptionalText(jsonPath, originalJson),
|
|
8526
|
+
restoreOptionalText(markdownPath, originalMarkdown),
|
|
8527
|
+
writeFile15(metadataPath2, originalMetadata, "utf-8")
|
|
8528
|
+
]);
|
|
8529
|
+
rollbackIncomplete = rollback.some(
|
|
8530
|
+
(result) => result.status === "rejected"
|
|
8531
|
+
);
|
|
8532
|
+
}
|
|
8533
|
+
const message = error instanceof Error ? error.message : "unable to finalize requirements";
|
|
8534
|
+
return invalidArg(
|
|
8535
|
+
options,
|
|
8536
|
+
rollbackIncomplete ? `${message}; rollback was incomplete` : message
|
|
8537
|
+
);
|
|
8538
|
+
}
|
|
8539
|
+
}
|
|
8540
|
+
async function workflowVerify(rootDir, args, options) {
|
|
8541
|
+
const taskId = args[0];
|
|
8542
|
+
const action = args[1];
|
|
8543
|
+
if (!taskId || !isValidWorkflowTaskId(taskId)) {
|
|
8544
|
+
return invalidArg(options, `invalid taskId: ${taskId ?? ""}`);
|
|
8545
|
+
}
|
|
8546
|
+
const meta = await readWorkflow(rootDir, taskId);
|
|
8547
|
+
if (!meta || meta.mode !== "man" && meta.mode !== "manteam") {
|
|
8548
|
+
return invalidArg(options, `verification is not valid for: ${taskId}`);
|
|
8549
|
+
}
|
|
8550
|
+
if (action === "show") {
|
|
8551
|
+
const ledger = await readVerificationLedger(rootDir, taskId);
|
|
8552
|
+
if (!ledger) {
|
|
8553
|
+
return invalidArg(options, `verification not initialized: ${taskId}`);
|
|
8554
|
+
}
|
|
8555
|
+
outputVerificationLedger(ledger, options);
|
|
8556
|
+
return EXIT_OK9;
|
|
8557
|
+
}
|
|
8558
|
+
if (meta.verificationPolicyVersion !== 1 || (action === "init" ? meta.currentStep !== 6 : meta.currentStep !== 6 && meta.currentStep !== 9) || meta.planDecision !== "governed_execution" || meta.status !== "in_progress" && meta.status !== "blocked") {
|
|
8559
|
+
return invalidArg(
|
|
8560
|
+
options,
|
|
8561
|
+
"verification requires a governed policy-v2 workflow at step 6, or step 9 when refreshing evidence"
|
|
8562
|
+
);
|
|
8563
|
+
}
|
|
8564
|
+
try {
|
|
8565
|
+
let ledger;
|
|
8566
|
+
if (action === "init") {
|
|
8567
|
+
if (await readVerificationLedger(rootDir, taskId)) {
|
|
8568
|
+
return invalidArg(
|
|
8569
|
+
options,
|
|
8570
|
+
`verification already initialized: ${taskId}`
|
|
8571
|
+
);
|
|
8572
|
+
}
|
|
8573
|
+
const requirements = await readRequirementsLedger(rootDir, taskId);
|
|
8574
|
+
if (!requirements || !requirementsAreReady(requirements)) {
|
|
8575
|
+
return invalidArg(options, "ready requirements.json is required");
|
|
8576
|
+
}
|
|
8577
|
+
ledger = initializeVerificationLedger(
|
|
8578
|
+
requirements,
|
|
8579
|
+
meta.planVersion ?? 1,
|
|
8580
|
+
0
|
|
8581
|
+
);
|
|
8582
|
+
} else {
|
|
8583
|
+
let existing = await readVerificationLedger(rootDir, taskId);
|
|
8584
|
+
if (!existing) {
|
|
8585
|
+
return invalidArg(options, `verification not initialized: ${taskId}`);
|
|
8586
|
+
}
|
|
8587
|
+
const review = await readReviewLedger(rootDir, taskId);
|
|
8588
|
+
existing = resetVerificationForRemediation(
|
|
8589
|
+
existing,
|
|
8590
|
+
review?.remediationRounds ?? 0
|
|
8591
|
+
);
|
|
8592
|
+
if (!options.acceptance || !options.evidence) {
|
|
8593
|
+
return invalidArg(
|
|
8594
|
+
options,
|
|
8595
|
+
`${action ?? "verification"} requires --acceptance and --evidence`
|
|
8596
|
+
);
|
|
8597
|
+
}
|
|
8598
|
+
if (action === "record") {
|
|
8599
|
+
if (options.method !== "automated" || options.result !== "passed" && options.result !== "failed" && options.result !== "blocked") {
|
|
8600
|
+
return invalidArg(
|
|
8601
|
+
options,
|
|
8602
|
+
"record requires --method automated and --result passed|failed|blocked"
|
|
8603
|
+
);
|
|
8604
|
+
}
|
|
8605
|
+
let automatedDetails;
|
|
8606
|
+
if (options.result === "passed" || options.result === "failed") {
|
|
8607
|
+
const exitCode = parseExactInteger(options.exitCode);
|
|
8608
|
+
if (!options.command?.trim() || exitCode === null) {
|
|
8609
|
+
return invalidArg(
|
|
8610
|
+
options,
|
|
8611
|
+
"automated passed/failed verification requires --command and --exit-code"
|
|
8612
|
+
);
|
|
8613
|
+
}
|
|
8614
|
+
if (options.evidenceFile) {
|
|
8615
|
+
const evidencePath = path28.isAbsolute(options.evidenceFile) ? options.evidenceFile : path28.resolve(rootDir, options.evidenceFile);
|
|
8616
|
+
if (!await pathExists12(evidencePath)) {
|
|
8617
|
+
return invalidArg(
|
|
8618
|
+
options,
|
|
8619
|
+
`verification evidence file not found: ${options.evidenceFile}`
|
|
8620
|
+
);
|
|
8621
|
+
}
|
|
8622
|
+
}
|
|
8623
|
+
automatedDetails = {
|
|
8624
|
+
command: options.command,
|
|
8625
|
+
exitCode,
|
|
8626
|
+
...options.evidenceFile ? { evidenceFile: options.evidenceFile } : {}
|
|
8627
|
+
};
|
|
8628
|
+
}
|
|
8629
|
+
ledger = recordVerification(
|
|
8630
|
+
existing,
|
|
8631
|
+
options.acceptance,
|
|
8632
|
+
"automated",
|
|
8633
|
+
options.result,
|
|
8634
|
+
options.evidence,
|
|
8635
|
+
automatedDetails
|
|
8636
|
+
);
|
|
8637
|
+
} else if (action === "require-manual") {
|
|
8638
|
+
ledger = recordVerification(
|
|
8639
|
+
existing,
|
|
8640
|
+
options.acceptance,
|
|
8641
|
+
"manual",
|
|
8642
|
+
"manual_required",
|
|
8643
|
+
options.evidence
|
|
8644
|
+
);
|
|
8645
|
+
} else if (action === "confirm-manual") {
|
|
8646
|
+
ledger = confirmManualVerification(
|
|
8647
|
+
existing,
|
|
8648
|
+
options.acceptance,
|
|
8649
|
+
options.evidence
|
|
8650
|
+
);
|
|
8651
|
+
} else {
|
|
8652
|
+
return invalidArg(
|
|
8653
|
+
options,
|
|
8654
|
+
`invalid verification action: ${action ?? ""}`
|
|
7746
8655
|
);
|
|
7747
8656
|
}
|
|
7748
|
-
|
|
8657
|
+
}
|
|
8658
|
+
const transitionError = await commitVerificationTransition(
|
|
8659
|
+
rootDir,
|
|
8660
|
+
meta,
|
|
8661
|
+
ledger
|
|
8662
|
+
);
|
|
8663
|
+
if (transitionError) return invalidArg(options, transitionError);
|
|
8664
|
+
outputVerificationLedger(ledger, options);
|
|
8665
|
+
return EXIT_OK9;
|
|
8666
|
+
} catch (error) {
|
|
8667
|
+
return invalidArg(
|
|
8668
|
+
options,
|
|
8669
|
+
error instanceof Error ? error.message : "unable to update verification"
|
|
8670
|
+
);
|
|
8671
|
+
}
|
|
8672
|
+
}
|
|
8673
|
+
async function commitVerificationTransition(rootDir, meta, ledger) {
|
|
8674
|
+
const ledgerPath = verificationLedgerPath(rootDir, meta.taskId);
|
|
8675
|
+
const metadataPath2 = path28.join(
|
|
8676
|
+
rootDir,
|
|
8677
|
+
".mancode",
|
|
8678
|
+
"workflows",
|
|
8679
|
+
meta.taskId,
|
|
8680
|
+
"metadata.json"
|
|
8681
|
+
);
|
|
8682
|
+
const specPath = path28.join(rootDir, ".mancode", "memory", "spec.md");
|
|
8683
|
+
const [originalLedger, originalMetadata, originalSpec] = await Promise.all([
|
|
8684
|
+
readOptionalText2(ledgerPath),
|
|
8685
|
+
readFile14(metadataPath2, "utf-8"),
|
|
8686
|
+
readOptionalText2(specPath)
|
|
8687
|
+
]);
|
|
8688
|
+
const wasVerificationBlocked = meta.status === "blocked" && meta.blockingReason?.startsWith("[verification]");
|
|
8689
|
+
const shouldBlock = ledger.status === "manual_required" || ledger.status === "blocked";
|
|
8690
|
+
const workflowPatch = {
|
|
8691
|
+
verificationStatus: ledger.status,
|
|
8692
|
+
...shouldBlock ? {
|
|
8693
|
+
status: "blocked",
|
|
8694
|
+
blockingReason: `[verification] ${ledger.status === "manual_required" ? "manual confirmation required" : "verification blocked"}`
|
|
8695
|
+
} : wasVerificationBlocked ? { status: "in_progress", blockingReason: void 0 } : {}
|
|
8696
|
+
};
|
|
8697
|
+
try {
|
|
8698
|
+
await writeVerificationLedger(rootDir, meta.taskId, ledger);
|
|
8699
|
+
await updateWorkflow(rootDir, meta.taskId, workflowPatch, {
|
|
8700
|
+
allowIncompleteVerification: true
|
|
8701
|
+
});
|
|
8702
|
+
const updated = await readWorkflow(rootDir, meta.taskId);
|
|
8703
|
+
if (updated) {
|
|
8704
|
+
await upsertActivePlan(rootDir, {
|
|
8705
|
+
taskId: updated.taskId,
|
|
8706
|
+
status: updated.status,
|
|
8707
|
+
planVersion: updated.planVersion ?? 1
|
|
8708
|
+
});
|
|
8709
|
+
}
|
|
8710
|
+
return null;
|
|
8711
|
+
} catch (error) {
|
|
8712
|
+
const rollback = await Promise.allSettled([
|
|
8713
|
+
restoreOptionalText(ledgerPath, originalLedger),
|
|
8714
|
+
writeFile15(metadataPath2, originalMetadata, "utf-8"),
|
|
8715
|
+
restoreOptionalText(specPath, originalSpec)
|
|
8716
|
+
]);
|
|
8717
|
+
const message = error instanceof Error ? error.message : "verification transition failed";
|
|
8718
|
+
return rollback.some((result) => result.status === "rejected") ? `${message}; rollback was incomplete` : message;
|
|
8719
|
+
}
|
|
8720
|
+
}
|
|
8721
|
+
function outputVerificationLedger(ledger, options) {
|
|
8722
|
+
if (options.json) {
|
|
8723
|
+
console.log(JSON.stringify(ledger, null, 2));
|
|
8724
|
+
return;
|
|
7749
8725
|
}
|
|
8726
|
+
const passed = ledger.checks.filter(
|
|
8727
|
+
(check) => [check.automated, check.manual].filter(Boolean).every((component) => component?.status === "passed")
|
|
8728
|
+
).length;
|
|
8729
|
+
console.log(
|
|
8730
|
+
`Verification: ${ledger.status}; criteria ${passed}/${ledger.checks.length}; plan v${ledger.planVersion}`
|
|
8731
|
+
);
|
|
7750
8732
|
}
|
|
7751
8733
|
async function workflowReview(rootDir, args, options) {
|
|
7752
8734
|
const taskId = args[0];
|
|
@@ -7772,6 +8754,36 @@ async function workflowReview(rootDir, args, options) {
|
|
|
7772
8754
|
"review requires an in_progress workflow at step 6 or later"
|
|
7773
8755
|
);
|
|
7774
8756
|
}
|
|
8757
|
+
if ((action === "init" || action === "skip") && meta.verificationPolicyVersion === 1 && !await verificationCanAdvance(rootDir, taskId, meta.planVersion ?? 1)) {
|
|
8758
|
+
return invalidArg(
|
|
8759
|
+
options,
|
|
8760
|
+
"workflow verification must pass before review can start"
|
|
8761
|
+
);
|
|
8762
|
+
}
|
|
8763
|
+
if (action === "skip") {
|
|
8764
|
+
if (meta.reviewPolicyVersion !== 2 || meta.currentStep !== 6 || meta.planDecision !== "governed_execution" || !options.reason?.trim()) {
|
|
8765
|
+
return invalidArg(
|
|
8766
|
+
options,
|
|
8767
|
+
"review skip requires a governed policy-v2 workflow at step 6 and --reason"
|
|
8768
|
+
);
|
|
8769
|
+
}
|
|
8770
|
+
try {
|
|
8771
|
+
const error = await commitReviewSkip(
|
|
8772
|
+
rootDir,
|
|
8773
|
+
meta,
|
|
8774
|
+
options.reason.trim()
|
|
8775
|
+
);
|
|
8776
|
+
if (error) return invalidArg(options, error);
|
|
8777
|
+
const ledger = await readReviewLedger(rootDir, taskId);
|
|
8778
|
+
outputReviewLedger(ledger, options);
|
|
8779
|
+
return EXIT_OK9;
|
|
8780
|
+
} catch (error) {
|
|
8781
|
+
return invalidArg(
|
|
8782
|
+
options,
|
|
8783
|
+
error instanceof Error ? error.message : "unable to skip review"
|
|
8784
|
+
);
|
|
8785
|
+
}
|
|
8786
|
+
}
|
|
7775
8787
|
try {
|
|
7776
8788
|
let ledger;
|
|
7777
8789
|
if (action === "init") {
|
|
@@ -7817,7 +8829,7 @@ async function workflowReview(rootDir, args, options) {
|
|
|
7817
8829
|
return invalidArg(
|
|
7818
8830
|
options,
|
|
7819
8831
|
`invalid review action: ${action ?? ""}`,
|
|
7820
|
-
"Use: mancode workflow review <taskId> <init|complete|remediate|show>"
|
|
8832
|
+
"Use: mancode workflow review <taskId> <init|complete|remediate|skip|show>"
|
|
7821
8833
|
);
|
|
7822
8834
|
}
|
|
7823
8835
|
outputReviewLedger(ledger, options);
|
|
@@ -7829,12 +8841,44 @@ async function workflowReview(rootDir, args, options) {
|
|
|
7829
8841
|
);
|
|
7830
8842
|
}
|
|
7831
8843
|
}
|
|
8844
|
+
async function commitReviewSkip(rootDir, meta, reason) {
|
|
8845
|
+
const ledgerPath = reviewLedgerPath(rootDir, meta.taskId);
|
|
8846
|
+
const metadataPath2 = path28.join(
|
|
8847
|
+
rootDir,
|
|
8848
|
+
".mancode",
|
|
8849
|
+
"workflows",
|
|
8850
|
+
meta.taskId,
|
|
8851
|
+
"metadata.json"
|
|
8852
|
+
);
|
|
8853
|
+
const [originalLedger, originalMetadata] = await Promise.all([
|
|
8854
|
+
readOptionalText2(ledgerPath),
|
|
8855
|
+
readFile14(metadataPath2, "utf-8")
|
|
8856
|
+
]);
|
|
8857
|
+
try {
|
|
8858
|
+
await initializeSkippedReview(rootDir, meta.taskId, reason);
|
|
8859
|
+
await updateWorkflow(rootDir, meta.taskId, {
|
|
8860
|
+
skippedSteps: [.../* @__PURE__ */ new Set([...meta.skippedSteps, "review"])]
|
|
8861
|
+
});
|
|
8862
|
+
return null;
|
|
8863
|
+
} catch (error) {
|
|
8864
|
+
const rollback = await Promise.allSettled([
|
|
8865
|
+
restoreOptionalText(ledgerPath, originalLedger),
|
|
8866
|
+
writeFile15(metadataPath2, originalMetadata, "utf-8")
|
|
8867
|
+
]);
|
|
8868
|
+
const message = error instanceof Error ? error.message : "review skip failed";
|
|
8869
|
+
return rollback.some((result) => result.status === "rejected") ? `${message}; rollback was incomplete` : message;
|
|
8870
|
+
}
|
|
8871
|
+
}
|
|
7832
8872
|
function outputReviewLedger(ledger, options) {
|
|
7833
8873
|
if (options.json) {
|
|
7834
8874
|
console.log(JSON.stringify(ledger, null, 2));
|
|
7835
8875
|
return;
|
|
7836
8876
|
}
|
|
7837
8877
|
if (!ledger) return;
|
|
8878
|
+
if (ledger.skipped) {
|
|
8879
|
+
console.log(`Review: skipped; reason ${ledger.skipped.reason}`);
|
|
8880
|
+
return;
|
|
8881
|
+
}
|
|
7838
8882
|
const openBlockers = ledger.blockers.filter(
|
|
7839
8883
|
(blocker) => blocker.status === "open"
|
|
7840
8884
|
);
|
|
@@ -7865,7 +8909,8 @@ async function workflowCreate(rootDir, args, options) {
|
|
|
7865
8909
|
}
|
|
7866
8910
|
try {
|
|
7867
8911
|
const meta = await createWorkflow(rootDir, task, mode, {
|
|
7868
|
-
parentTaskId: options.parentTask
|
|
8912
|
+
parentTaskId: options.parentTask,
|
|
8913
|
+
planningPolicyVersion: mode === "man" || mode === "manteam" ? 2 : void 0
|
|
7869
8914
|
});
|
|
7870
8915
|
if (options.json) {
|
|
7871
8916
|
console.log(JSON.stringify(meta, null, 2));
|
|
@@ -7885,7 +8930,7 @@ async function workflowUpdate(rootDir, taskId, options) {
|
|
|
7885
8930
|
return invalidArg(
|
|
7886
8931
|
options,
|
|
7887
8932
|
"missing taskId",
|
|
7888
|
-
"Use: mancode workflow update <taskId> [--step N] [--status in_progress|planned|completed|blocked|abandoned] [--blocking-reason <reason>] [--outcome <outcome>] [--plan-version N] [--skipped
|
|
8933
|
+
"Use: mancode workflow update <taskId> [--step N] [--status in_progress|planned|completed|blocked|abandoned] [--requirements-status ready|needs_clarification] [--blocking-reason <reason>] [--outcome <outcome>] [--plan-version N] [--skipped clarification]"
|
|
7889
8934
|
);
|
|
7890
8935
|
}
|
|
7891
8936
|
if (!isValidWorkflowTaskId(taskId)) {
|
|
@@ -7946,8 +8991,45 @@ async function workflowUpdate(rootDir, taskId, options) {
|
|
|
7946
8991
|
}
|
|
7947
8992
|
patch.planVersion = planVersion;
|
|
7948
8993
|
}
|
|
8994
|
+
if (options.requirementsStatus !== void 0) {
|
|
8995
|
+
if (!isRequirementsStatus(options.requirementsStatus) || existing.mode !== "man" && existing.mode !== "manteam") {
|
|
8996
|
+
return invalidArg(
|
|
8997
|
+
options,
|
|
8998
|
+
`invalid --requirements-status: ${options.requirementsStatus}`
|
|
8999
|
+
);
|
|
9000
|
+
}
|
|
9001
|
+
patch.requirementsStatus = options.requirementsStatus;
|
|
9002
|
+
}
|
|
9003
|
+
if (options.planDecision !== void 0) {
|
|
9004
|
+
if (options.planDecision !== "governed_execution" || existing.mode !== "man" && existing.mode !== "manteam") {
|
|
9005
|
+
return invalidArg(
|
|
9006
|
+
options,
|
|
9007
|
+
`invalid --plan-decision: ${options.planDecision}`
|
|
9008
|
+
);
|
|
9009
|
+
}
|
|
9010
|
+
patch.planDecision = options.planDecision;
|
|
9011
|
+
}
|
|
7949
9012
|
if (options.skipped !== void 0) {
|
|
7950
|
-
|
|
9013
|
+
const requestedSkipped = options.skipped.split(",").map((item) => item.trim()).filter(Boolean);
|
|
9014
|
+
if (existing.reviewPolicyVersion === 2) {
|
|
9015
|
+
if (requestedSkipped.includes("review")) {
|
|
9016
|
+
return invalidArg(
|
|
9017
|
+
options,
|
|
9018
|
+
"policy-v2 review skips require workflow review <taskId> skip --reason <reason>"
|
|
9019
|
+
);
|
|
9020
|
+
}
|
|
9021
|
+
if (existing.currentStep > 2 || requestedSkipped.some((item) => item !== "clarification")) {
|
|
9022
|
+
return invalidArg(
|
|
9023
|
+
options,
|
|
9024
|
+
"policy-v2 clarification can only be skipped at step 1 or 2"
|
|
9025
|
+
);
|
|
9026
|
+
}
|
|
9027
|
+
patch.skippedSteps = [
|
|
9028
|
+
.../* @__PURE__ */ new Set([...existing.skippedSteps, ...requestedSkipped])
|
|
9029
|
+
];
|
|
9030
|
+
} else {
|
|
9031
|
+
patch.skippedSteps = requestedSkipped;
|
|
9032
|
+
}
|
|
7951
9033
|
}
|
|
7952
9034
|
if (Object.keys(patch).length === 0) {
|
|
7953
9035
|
return invalidArg(options, "missing update fields");
|
|
@@ -7978,6 +9060,307 @@ async function workflowUpdate(rootDir, taskId, options) {
|
|
|
7978
9060
|
}
|
|
7979
9061
|
return EXIT_OK9;
|
|
7980
9062
|
}
|
|
9063
|
+
async function workflowHandoff(rootDir, taskId, options) {
|
|
9064
|
+
if (!taskId || !isValidWorkflowTaskId(taskId)) {
|
|
9065
|
+
return invalidArg(options, `invalid taskId: ${taskId ?? ""}`);
|
|
9066
|
+
}
|
|
9067
|
+
if (options.complete) {
|
|
9068
|
+
if (options.to !== void 0) {
|
|
9069
|
+
return invalidArg(options, "--complete cannot be combined with --to");
|
|
9070
|
+
}
|
|
9071
|
+
return workflowCompleteHandoff(rootDir, taskId, options);
|
|
9072
|
+
}
|
|
9073
|
+
if (options.to !== "solo") {
|
|
9074
|
+
return invalidArg(options, "handoff requires --to solo");
|
|
9075
|
+
}
|
|
9076
|
+
const meta = await readWorkflow(rootDir, taskId);
|
|
9077
|
+
if (!meta || meta.mode !== "man" && meta.mode !== "manteam") {
|
|
9078
|
+
return invalidArg(options, `workflow cannot be handed off: ${taskId}`);
|
|
9079
|
+
}
|
|
9080
|
+
if (meta.status !== "in_progress" || meta.currentStep !== 4 || meta.requirementsStatus !== "ready" || meta.planDecision !== void 0) {
|
|
9081
|
+
return invalidArg(
|
|
9082
|
+
options,
|
|
9083
|
+
"solo handoff requires an undecided in-progress workflow at step 4 with ready requirements"
|
|
9084
|
+
);
|
|
9085
|
+
}
|
|
9086
|
+
const workflowPath = path28.join(rootDir, ".mancode", "workflows", taskId);
|
|
9087
|
+
if (!await pathExists12(path28.join(workflowPath, "requirements.md")) || !await pathExists12(path28.join(workflowPath, "plan.md"))) {
|
|
9088
|
+
return invalidArg(
|
|
9089
|
+
options,
|
|
9090
|
+
"solo handoff requires requirements.md and plan.md"
|
|
9091
|
+
);
|
|
9092
|
+
}
|
|
9093
|
+
const statePath = path28.join(rootDir, ".mancode", "state.json");
|
|
9094
|
+
let originalState;
|
|
9095
|
+
let state;
|
|
9096
|
+
try {
|
|
9097
|
+
originalState = await readFile14(statePath, "utf-8");
|
|
9098
|
+
state = JSON.parse(originalState);
|
|
9099
|
+
} catch {
|
|
9100
|
+
return invalidArg(
|
|
9101
|
+
options,
|
|
9102
|
+
"unable to read .mancode/state.json for handoff"
|
|
9103
|
+
);
|
|
9104
|
+
}
|
|
9105
|
+
if (state.currentTask !== taskId) {
|
|
9106
|
+
return invalidArg(
|
|
9107
|
+
options,
|
|
9108
|
+
"current workflow state does not match the handoff task"
|
|
9109
|
+
);
|
|
9110
|
+
}
|
|
9111
|
+
const activeSoloPlan = readActiveSoloPlan(state.activeSoloPlan);
|
|
9112
|
+
if (activeSoloPlan && activeSoloPlan.taskId !== taskId) {
|
|
9113
|
+
return invalidArg(
|
|
9114
|
+
options,
|
|
9115
|
+
`another solo plan is active: ${activeSoloPlan.taskId}; complete or abandon it before handoff`
|
|
9116
|
+
);
|
|
9117
|
+
}
|
|
9118
|
+
const nextState = {
|
|
9119
|
+
...state,
|
|
9120
|
+
currentMode: "solo",
|
|
9121
|
+
lastMode: typeof state.currentMode === "string" ? state.currentMode : meta.mode,
|
|
9122
|
+
currentTask: null,
|
|
9123
|
+
currentWorkflowMode: null,
|
|
9124
|
+
skippedSteps: [],
|
|
9125
|
+
activeSoloPlan: {
|
|
9126
|
+
taskId,
|
|
9127
|
+
planVersion: meta.planVersion ?? 1
|
|
9128
|
+
}
|
|
9129
|
+
};
|
|
9130
|
+
const transitionError = await commitPlanningTransition({
|
|
9131
|
+
rootDir,
|
|
9132
|
+
taskId,
|
|
9133
|
+
statePath,
|
|
9134
|
+
originalState,
|
|
9135
|
+
nextState,
|
|
9136
|
+
workflowPatch: {
|
|
9137
|
+
status: "planned",
|
|
9138
|
+
planDecision: "solo_handoff"
|
|
9139
|
+
},
|
|
9140
|
+
activePlanStatus: "planned",
|
|
9141
|
+
planVersion: meta.planVersion ?? 1
|
|
9142
|
+
});
|
|
9143
|
+
if (transitionError) {
|
|
9144
|
+
return invalidArg(options, transitionError);
|
|
9145
|
+
}
|
|
9146
|
+
const result = await readWorkflow(rootDir, taskId);
|
|
9147
|
+
if (options.json) {
|
|
9148
|
+
console.log(
|
|
9149
|
+
JSON.stringify(
|
|
9150
|
+
{ workflow: result, activeSoloPlan: nextState.activeSoloPlan },
|
|
9151
|
+
null,
|
|
9152
|
+
2
|
|
9153
|
+
)
|
|
9154
|
+
);
|
|
9155
|
+
} else {
|
|
9156
|
+
console.log(`Handed off plan to solo: ${taskId}`);
|
|
9157
|
+
}
|
|
9158
|
+
return EXIT_OK9;
|
|
9159
|
+
}
|
|
9160
|
+
async function workflowCompleteHandoff(rootDir, taskId, options) {
|
|
9161
|
+
const meta = await readWorkflow(rootDir, taskId);
|
|
9162
|
+
if (!meta || meta.mode !== "man" && meta.mode !== "manteam" || meta.status !== "planned" || meta.currentStep !== 4 || meta.planDecision !== "solo_handoff") {
|
|
9163
|
+
return invalidArg(
|
|
9164
|
+
options,
|
|
9165
|
+
`workflow is not an active solo handoff: ${taskId}`
|
|
9166
|
+
);
|
|
9167
|
+
}
|
|
9168
|
+
const statePath = path28.join(rootDir, ".mancode", "state.json");
|
|
9169
|
+
let originalState;
|
|
9170
|
+
let state;
|
|
9171
|
+
try {
|
|
9172
|
+
originalState = await readFile14(statePath, "utf-8");
|
|
9173
|
+
state = JSON.parse(originalState);
|
|
9174
|
+
} catch {
|
|
9175
|
+
return invalidArg(
|
|
9176
|
+
options,
|
|
9177
|
+
"unable to read .mancode/state.json for handoff completion"
|
|
9178
|
+
);
|
|
9179
|
+
}
|
|
9180
|
+
const activeSoloPlan = readActiveSoloPlan(state.activeSoloPlan);
|
|
9181
|
+
if (activeSoloPlan?.taskId !== taskId) {
|
|
9182
|
+
return invalidArg(
|
|
9183
|
+
options,
|
|
9184
|
+
"active solo plan does not match the handoff task"
|
|
9185
|
+
);
|
|
9186
|
+
}
|
|
9187
|
+
const nextState = {
|
|
9188
|
+
...state,
|
|
9189
|
+
activeSoloPlan: null
|
|
9190
|
+
};
|
|
9191
|
+
const transitionError = await commitPlanningTransition({
|
|
9192
|
+
rootDir,
|
|
9193
|
+
taskId,
|
|
9194
|
+
statePath,
|
|
9195
|
+
originalState,
|
|
9196
|
+
nextState,
|
|
9197
|
+
workflowPatch: { status: "completed" },
|
|
9198
|
+
activePlanStatus: "completed",
|
|
9199
|
+
planVersion: meta.planVersion ?? 1
|
|
9200
|
+
});
|
|
9201
|
+
if (transitionError) return invalidArg(options, transitionError);
|
|
9202
|
+
const result = await readWorkflow(rootDir, taskId);
|
|
9203
|
+
if (options.json) {
|
|
9204
|
+
console.log(
|
|
9205
|
+
JSON.stringify({ workflow: result, activeSoloPlan: null }, null, 2)
|
|
9206
|
+
);
|
|
9207
|
+
} else {
|
|
9208
|
+
console.log(`Completed solo handoff: ${taskId}`);
|
|
9209
|
+
}
|
|
9210
|
+
return EXIT_OK9;
|
|
9211
|
+
}
|
|
9212
|
+
async function workflowDecide(rootDir, taskId, options) {
|
|
9213
|
+
if (!taskId || !isValidWorkflowTaskId(taskId)) {
|
|
9214
|
+
return invalidArg(options, `invalid taskId: ${taskId ?? ""}`);
|
|
9215
|
+
}
|
|
9216
|
+
if (!isPlanDecision(options.planDecision) || options.planDecision === "solo_handoff") {
|
|
9217
|
+
return invalidArg(
|
|
9218
|
+
options,
|
|
9219
|
+
"decide requires --plan-decision plan_only or governed_execution"
|
|
9220
|
+
);
|
|
9221
|
+
}
|
|
9222
|
+
if (options.planDecision === "governed_execution") {
|
|
9223
|
+
return workflowUpdate(rootDir, taskId, {
|
|
9224
|
+
...options,
|
|
9225
|
+
planDecision: "governed_execution"
|
|
9226
|
+
});
|
|
9227
|
+
}
|
|
9228
|
+
const meta = await readWorkflow(rootDir, taskId);
|
|
9229
|
+
if (!meta || meta.mode !== "man" && meta.mode !== "manteam" || meta.status !== "in_progress" || meta.currentStep !== 4 || meta.requirementsStatus !== "ready" || meta.planDecision !== void 0) {
|
|
9230
|
+
return invalidArg(
|
|
9231
|
+
options,
|
|
9232
|
+
`workflow is not ready for a plan-only decision: ${taskId}`
|
|
9233
|
+
);
|
|
9234
|
+
}
|
|
9235
|
+
const workflowPath = path28.join(rootDir, ".mancode", "workflows", taskId);
|
|
9236
|
+
if (!await pathExists12(path28.join(workflowPath, "requirements.md")) || !await pathExists12(path28.join(workflowPath, "plan.md"))) {
|
|
9237
|
+
return invalidArg(
|
|
9238
|
+
options,
|
|
9239
|
+
"plan-only requires requirements.md and plan.md"
|
|
9240
|
+
);
|
|
9241
|
+
}
|
|
9242
|
+
const statePath = path28.join(rootDir, ".mancode", "state.json");
|
|
9243
|
+
let originalState;
|
|
9244
|
+
let state;
|
|
9245
|
+
try {
|
|
9246
|
+
originalState = await readFile14(statePath, "utf-8");
|
|
9247
|
+
state = JSON.parse(originalState);
|
|
9248
|
+
} catch {
|
|
9249
|
+
return invalidArg(
|
|
9250
|
+
options,
|
|
9251
|
+
"unable to read .mancode/state.json for decision"
|
|
9252
|
+
);
|
|
9253
|
+
}
|
|
9254
|
+
if (state.currentTask !== taskId) {
|
|
9255
|
+
return invalidArg(
|
|
9256
|
+
options,
|
|
9257
|
+
"current workflow state does not match the decision task"
|
|
9258
|
+
);
|
|
9259
|
+
}
|
|
9260
|
+
const activeSoloPlan = readActiveSoloPlan(state.activeSoloPlan);
|
|
9261
|
+
if (activeSoloPlan) {
|
|
9262
|
+
return invalidArg(
|
|
9263
|
+
options,
|
|
9264
|
+
`another solo plan is active: ${activeSoloPlan.taskId}; resolve it before plan-only`
|
|
9265
|
+
);
|
|
9266
|
+
}
|
|
9267
|
+
const nextState = {
|
|
9268
|
+
...state,
|
|
9269
|
+
currentMode: "solo",
|
|
9270
|
+
lastMode: typeof state.currentMode === "string" ? state.currentMode : meta.mode,
|
|
9271
|
+
currentTask: null,
|
|
9272
|
+
currentWorkflowMode: null,
|
|
9273
|
+
skippedSteps: [],
|
|
9274
|
+
activeSoloPlan: null
|
|
9275
|
+
};
|
|
9276
|
+
const transitionError = await commitPlanningTransition({
|
|
9277
|
+
rootDir,
|
|
9278
|
+
taskId,
|
|
9279
|
+
statePath,
|
|
9280
|
+
originalState,
|
|
9281
|
+
nextState,
|
|
9282
|
+
workflowPatch: { status: "planned", planDecision: "plan_only" },
|
|
9283
|
+
activePlanStatus: "planned",
|
|
9284
|
+
planVersion: meta.planVersion ?? 1
|
|
9285
|
+
});
|
|
9286
|
+
if (transitionError) return invalidArg(options, transitionError);
|
|
9287
|
+
const result = await readWorkflow(rootDir, taskId);
|
|
9288
|
+
if (options.json) {
|
|
9289
|
+
console.log(
|
|
9290
|
+
JSON.stringify({ workflow: result, state: nextState }, null, 2)
|
|
9291
|
+
);
|
|
9292
|
+
} else {
|
|
9293
|
+
console.log(`Saved plan without execution: ${taskId}`);
|
|
9294
|
+
}
|
|
9295
|
+
return EXIT_OK9;
|
|
9296
|
+
}
|
|
9297
|
+
async function commitPlanningTransition(args) {
|
|
9298
|
+
const metadataPath2 = path28.join(
|
|
9299
|
+
args.rootDir,
|
|
9300
|
+
".mancode",
|
|
9301
|
+
"workflows",
|
|
9302
|
+
args.taskId,
|
|
9303
|
+
"metadata.json"
|
|
9304
|
+
);
|
|
9305
|
+
const specPath = path28.join(args.rootDir, ".mancode", "memory", "spec.md");
|
|
9306
|
+
let originalMetadata;
|
|
9307
|
+
let originalSpec;
|
|
9308
|
+
try {
|
|
9309
|
+
originalMetadata = await readFile14(metadataPath2, "utf-8");
|
|
9310
|
+
originalSpec = await readOptionalText2(specPath);
|
|
9311
|
+
} catch (error) {
|
|
9312
|
+
return error instanceof Error ? `unable to prepare planning transition: ${error.message}` : "unable to prepare planning transition";
|
|
9313
|
+
}
|
|
9314
|
+
try {
|
|
9315
|
+
await updateWorkflow(args.rootDir, args.taskId, args.workflowPatch);
|
|
9316
|
+
await writeFile15(
|
|
9317
|
+
args.statePath,
|
|
9318
|
+
`${JSON.stringify(args.nextState, null, 2)}
|
|
9319
|
+
`,
|
|
9320
|
+
"utf-8"
|
|
9321
|
+
);
|
|
9322
|
+
await upsertActivePlan(args.rootDir, {
|
|
9323
|
+
taskId: args.taskId,
|
|
9324
|
+
status: args.activePlanStatus,
|
|
9325
|
+
planVersion: args.planVersion
|
|
9326
|
+
});
|
|
9327
|
+
return null;
|
|
9328
|
+
} catch (error) {
|
|
9329
|
+
const rollback = await Promise.allSettled([
|
|
9330
|
+
writeFile15(metadataPath2, originalMetadata, "utf-8"),
|
|
9331
|
+
writeFile15(args.statePath, args.originalState, "utf-8"),
|
|
9332
|
+
originalSpec === null ? rm6(specPath, { force: true }) : writeFile15(specPath, originalSpec, "utf-8")
|
|
9333
|
+
]);
|
|
9334
|
+
const message = error instanceof Error ? error.message : "planning transition failed";
|
|
9335
|
+
return rollback.some((result) => result.status === "rejected") ? `${message}; rollback was incomplete` : message;
|
|
9336
|
+
}
|
|
9337
|
+
}
|
|
9338
|
+
async function readOptionalText2(filePath) {
|
|
9339
|
+
try {
|
|
9340
|
+
return await readFile14(filePath, "utf-8");
|
|
9341
|
+
} catch (error) {
|
|
9342
|
+
if (isNodeError10(error) && error.code === "ENOENT") return null;
|
|
9343
|
+
throw error;
|
|
9344
|
+
}
|
|
9345
|
+
}
|
|
9346
|
+
async function restoreOptionalText(filePath, content) {
|
|
9347
|
+
if (content === null) {
|
|
9348
|
+
await rm6(filePath, { force: true });
|
|
9349
|
+
return;
|
|
9350
|
+
}
|
|
9351
|
+
await writeFile15(filePath, content, "utf-8");
|
|
9352
|
+
}
|
|
9353
|
+
function readActiveSoloPlan(value) {
|
|
9354
|
+
if (!value || typeof value !== "object") return null;
|
|
9355
|
+
const plan = value;
|
|
9356
|
+
if (typeof plan.taskId !== "string" || !Number.isInteger(plan.planVersion)) {
|
|
9357
|
+
return null;
|
|
9358
|
+
}
|
|
9359
|
+
return { taskId: plan.taskId, planVersion: plan.planVersion };
|
|
9360
|
+
}
|
|
9361
|
+
function isNodeError10(error) {
|
|
9362
|
+
return error instanceof Error && "code" in error;
|
|
9363
|
+
}
|
|
7981
9364
|
async function workflowList(rootDir, options) {
|
|
7982
9365
|
const workflows = await listWorkflows(rootDir);
|
|
7983
9366
|
const views = attachActiveChildren(workflows);
|
|
@@ -8049,6 +9432,11 @@ async function workflowShow(rootDir, taskId, options) {
|
|
|
8049
9432
|
if (meta.parentTaskId) console.log(`Parent: ${meta.parentTaskId}`);
|
|
8050
9433
|
if (meta.planVersion !== void 0)
|
|
8051
9434
|
console.log(`Plan version: ${meta.planVersion}`);
|
|
9435
|
+
if (meta.requirementsStatus)
|
|
9436
|
+
console.log(`Requirements: ${meta.requirementsStatus}`);
|
|
9437
|
+
if (meta.verificationStatus)
|
|
9438
|
+
console.log(`Verification: ${meta.verificationStatus}`);
|
|
9439
|
+
if (meta.planDecision) console.log(`Plan choice: ${meta.planDecision}`);
|
|
8052
9440
|
if (meta.outcome) console.log(`Outcome: ${meta.outcome}`);
|
|
8053
9441
|
if (meta.blockingReason) console.log(`Blocked: ${meta.blockingReason}`);
|
|
8054
9442
|
if (children !== void 0) {
|
|
@@ -8167,6 +9555,11 @@ function parseExactPositiveInteger(value) {
|
|
|
8167
9555
|
const parsed = Number(value);
|
|
8168
9556
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
8169
9557
|
}
|
|
9558
|
+
function parseExactInteger(value) {
|
|
9559
|
+
if (!value || !/^-?\d+$/.test(value)) return null;
|
|
9560
|
+
const parsed = Number(value);
|
|
9561
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
9562
|
+
}
|
|
8170
9563
|
function parseOlderThan(value) {
|
|
8171
9564
|
if (!value) return void 0;
|
|
8172
9565
|
const match = /^(\d+)([dhm])$/.exec(value);
|
|
@@ -8241,13 +9634,22 @@ program.command("uninstall [platform]").description("Remove platform adapter or
|
|
|
8241
9634
|
program.command("workflow <subcommand> [args...]").description("Manage mancode workflows").option("--dry-run", "Preview clean without deleting").option("--older-than <duration>", "Clean workflows older than (e.g. 30d)").option("--step <n>", "Update workflow current step").option("--status <status>", "Update workflow status").option(
|
|
8242
9635
|
"--parent-task <taskId>",
|
|
8243
9636
|
"Parent /man or /manteam workflow for manba"
|
|
8244
|
-
).option("--blocking-reason <reason>", "Explain why a workflow is blocked").option("--outcome <outcome>", "Set manba outcome").option("--plan-version <n>", "Set the next man/manteam plan revision").option(
|
|
9637
|
+
).option("--blocking-reason <reason>", "Explain why a workflow is blocked").option("--outcome <outcome>", "Set manba outcome").option("--plan-version <n>", "Set the next man/manteam plan revision").option(
|
|
9638
|
+
"--requirements-status <status>",
|
|
9639
|
+
"Planning readiness: ready or needs_clarification"
|
|
9640
|
+
).option(
|
|
9641
|
+
"--plan-decision <decision>",
|
|
9642
|
+
"Plan gate choice: plan_only or governed_execution"
|
|
9643
|
+
).option("--to <mode>", "Workflow handoff target (solo)").option("--complete", "Complete an active solo handoff").option(
|
|
9644
|
+
"--skipped <steps>",
|
|
9645
|
+
"Policy v2: clarification only; use workflow review skip for review"
|
|
9646
|
+
).option("--review-depth <depth>", "Review depth: targeted or full").option("--review-domain <domain>", "Review domain: quality or security").option(
|
|
8245
9647
|
"--report <path>",
|
|
8246
9648
|
"Relative Markdown report path for a review domain"
|
|
8247
9649
|
).option("--blockers <ids>", "Comma-separated blocker ids found by a review").option(
|
|
8248
9650
|
"--resolved <ids>",
|
|
8249
9651
|
"Comma-separated blocker ids resolved in remediation"
|
|
8250
|
-
).option("--json", "Output as JSON (for scripts)").action(async (subcommand, args, options) => {
|
|
9652
|
+
).option("--file <path>", "Structured requirements JSON input file").option("--acceptance <id>", "Acceptance criterion id (for example AC-1)").option("--method <method>", "Verification method: automated or manual").option("--result <result>", "Verification result").option("--evidence <text>", "Verification evidence or user confirmation").option("--command <command>", "Command used for automated verification").option("--exit-code <code>", "Exit code from automated verification").option("--evidence-file <path>", "Existing verification report or artifact").option("--reason <reason>", "Reason for an explicit review skip").option("--json", "Output as JSON (for scripts)").action(async (subcommand, args, options) => {
|
|
8251
9653
|
const code = await workflow(process.cwd(), subcommand, args ?? [], options);
|
|
8252
9654
|
process.exitCode = code;
|
|
8253
9655
|
});
|