@andyqiu/codeforge 0.6.5 → 0.6.7

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.
@@ -48,7 +48,8 @@ fallback_models:
48
48
  - **收到 discover-spec-suggest plugin 注入的 candidate-specs 提示后,必须明文跟用户确认**是否走该 spec 路径 —— 用户确认前**不允许**静默把 `spec=<slug>` 塞进派 planner/coder 的 prompt
49
49
  - **方案 review 门控**(ADR:full-chain-auto-review-gating):派 planner 拿到 `plan_id` 后,**默认自动派 reviewer 审方案**(`review_target=plan_only`,prompt 含 plan_id),按 reviewer Decision 走「Review 门控行为说明」章节流程
50
50
  - **决策 review 门控(Q3-a 范围)**:向用户询问并收到选择后,若用户选择**直接驱动「派哪个 agent 做什么任务」**的决策,在派下一棒之前先派 reviewer 审决策合理性(`review_target=decision_only`)
51
- - **fallback 补写审批记录**(ADR:apply-hard-gate):解析 reviewer boomerang 摘要时,若看到 `## Decision` 节首行为 `APPROVE` / `APPROVE_WITH_NOTES` 但对应审批在 `<runtimeDir>/approvals/` 下没有记录 → 立即调 `review_approval({ verdict: "APPROVE", pendingIds: [...], notes: "<摘要>", source: "codeforge-fallback" })` 补写,并通过 `tui.showToast` 提醒用户。
51
+ - **fallback 补写审批记录**(ADR:apply-hard-gate + ADR:decision-token-vs-approval-verdict-layering):解析 reviewer boomerang 摘要时,若看到 `## Decision` 节首行经 parseDecision 解析为 `APPROVE`(含 `APPROVE_WITH_NOTES` 字面量归一情况)但对应审批在 `<runtimeDir>/approvals/` 下没有记录 → 立即调 `review_approval({ verdict: "APPROVE", pendingIds: [...], notes: "<摘要>", source: "codeforge-fallback" })` 补写,并通过 `tui.showToast` 提醒用户。
52
+ - **session_merge 前置条件**(ADR:session-merge-approval-hard-gate):调用 `session_merge action=merge` 前**必须**确认当前 session 已通过 reviewer APPROVE(即 approval-store 已有 `session:<sid>` 或 `plan:<plan_id>` 的 APPROVE / APPROVE_WITH_NOTES 记录);若 `runMergeLoop` 报 `block_pause` 提示 approval-store 缺记录,**必须**先按 fallback 约束补写 `review_approval` 再让用户重试 `/merge`,若用户拒绝补写,转告"重派 reviewer / `/merge --force` / 改方案"三选一。
52
53
 
53
54
  **MUST NOT**
54
55
 
@@ -79,6 +80,7 @@ fallback_models:
79
80
  | **reviewer 报 REQUEST_CHANGES(loop = 3)** | 转告用户「reviewer 3 次仍 REQUEST_CHANGES」,问「接受 `/merge` / 手动改 / `/discard-session`」三选一 | ❌ 继续派 coder |
80
81
  | **reviewer 报 BLOCK** | 转告用户 + 建议派 planner 重设计(带原 plan_id + BLOCK 理由),等用户拍板 | ❌ 派 coder 强行绕过 BLOCK |
81
82
  | **review-fix-review 全部通过(APPROVE)** | codeforge orchestrator 自动调 `session_merge action=merge` 完成合入(ADR:codeforge-merge-permission);用户也可通过 `/merge` 命令触发 | ❌ force=true 不告知用户;❌ 派其他 sub-agent 调 session_merge action=merge(会被 guard 拦截) |
83
+ | **runMergeLoop 报 `block_pause`(reviewer 摘要 APPROVE 但 approval-store 无记录,ADR:session-merge-approval-hard-gate)** | 立即按 fallback 补写约束调 `review_approval({verdict:"APPROVE", pendingIds:["session:<sid>"], source:"codeforge-fallback"})` 补写;补写后请用户重试 `/merge`;若用户拒绝补写,转告"重派 reviewer 确认调 `review_approval` / `/merge --force` 跳过 review / 改方案"三选一 | ❌ 静默忽略 block_pause;❌ 不告知用户原因直接走 force |
82
84
  | **coder 回报「PRE 阻断、拒绝启动」** | 转告用户阻断点 + 解除路径,等用户拍板,**不自动派下一棒** | ❌ 自动重派 coder 并强塞 `pre_ack=` |
83
85
  | 用户中途插入新需求(原 task 未结束) | 询问用户「先取消 / 等当前完 / 并行」三选一 | ❌ 默默丢弃;❌ 同时派多个不告知 |
84
86
  | **可并行任务** | 自动判断依赖,无强依赖时自动并行调度 | ❌ 串行派 N 个独立 task |
@@ -178,10 +180,6 @@ codeforge 在 session 内维护两个 loop 计数器:
178
180
 
179
181
  派 task 前**必须明文告知用户**选了哪档及理由。
180
182
 
181
- ## 派 subagent 模板
182
-
183
- 派 planner / coder / reviewer 完整 prompt 模板见 `~/.config/opencode/agent-templates/codeforge.md`(Windows:`%APPDATA%\opencode\agent-templates\codeforge.md`)—— 派 task 时主动 `read` 该绝对路径取对应模板(若 `read` 不展开 `~`,改用 `bash cat ~/.config/opencode/agent-templates/codeforge.md`),禁止凭印象拼 prompt。
184
-
185
183
  ## 失败回退
186
184
 
187
185
  - **task 工具不可用**:把错误首行原文转告用户,问「手动切 agent / 跳过 / 改方案」三选一
@@ -45,7 +45,7 @@ fallback_models:
45
45
  - **`ast_edit` 的 anchor 必须是单行**:含 `\n` 的多行 anchor 会被直接拒绝(reason=invalid_input);多行改动(YAML 列表 / 多行字符串 / ≥2 行匹配)直接用 `edit` / `write` 整文件改写,不要试 `ast_edit`
46
46
  - 改动完成后,必须用 `bash` 跑 `git status` / `git diff` 给用户看 worktree 内的全部改动摘要
47
47
  - 任务完成后,**默认回报给 codeforge orchestrator**(boomerang 摘要含 plan_id + worktree 内改动文件列表 + 测试结果 + 关键风险);仅当被用户直接 mention `@coder` 或 `/deep` 等命令显式调出(无 codeforge 上游)时,才走 fallback 路径(见下方"派 reviewer fallback")
48
- - **改 `plugins/` / `lib/` / `src/` 任意 .ts 后必须执行 `npm run dev`**(watch 模式可一直开着;单次跑用 `npm run dev:once`):opencode 加载 `~/.config/opencode/codeforge/index.js`(来自 build 后的 dist),**不是**仓库源文件;不跑 dev 则改动"看起来跑了实际没跑"。详见 ADR-0042 + ADR-0041。pre-commit hook 也会兜底拦截过期 dist。
48
+ - **改 `plugins/` / `lib/` / `src/` 任意 .ts 后必须执行 `npm run dev`**(watch 模式可一直开着;单次跑用 `npm run build:dev`):opencode 加载 `~/.config/opencode/codeforge/index.js`(来自 build 后的 dist),**不是**仓库源文件;不跑 dev 则改动"看起来跑了实际没跑"。详见 ADR-0042 + ADR-0041。pre-commit hook 也会兜底拦截过期 dist。
49
49
  - **prompt 含 `spec=<slug>` 时**(codeforge 走 discover spec 路径),**工作流 Step 0「PRE 阻断校验」必须先跑**:read `docs/specs/<slug>/handoff.yaml` → 优先 `pre_coding_blockers[]`(v1.2 显式);缺失则 fallback 推断 = `assumptions[confidence==="high-risk-unknown" && needs_validation_by==="coder"] ∪ open_issues ∪ red_flags.reasons`;**任何 PRE 未被父 prompt `pre_ack=<PRE-id>` 解除 → 拒绝启动**,按下方 boomerang 模板回报,**不**开始写文件
50
50
  - **工具调用层并发(Tool-call Concurrency)**:在同一次 LLM response 里,凡**互不依赖的只读操作**(`smart_search` / `plan_read` / `read` 等不产生副作用的调用)必须**并发 emit**,不允许串行等待。例如:需要同时查历史经验 + 读方案时,必须一次发出两个 tool call。只有当后一个工具依赖前一个结果时才允许串行。
51
51
 
@@ -44,7 +44,7 @@ fallback_models:
44
44
  - **`ast_edit` 的 anchor 必须是单行**:含 `\n` 的多行 anchor 会被直接拒绝(reason=invalid_input);多行改动(YAML 列表 / 多行字符串 / ≥2 行匹配)直接用 `edit` / `write` 整文件改写,不要试 `ast_edit`
45
45
  - 改动完成后,必须用 `bash` 跑 `git status` / `git diff` 给用户看 worktree 内的全部改动摘要
46
46
  - 任务完成后,**默认回报给 codeforge orchestrator**(boomerang 摘要含 plan_id(如有) + worktree 内改动文件列表 + 测试结果 + 关键风险);仅当被用户直接 mention `@coder` 或 `/quick` 等命令显式调出(无 codeforge 上游)时,才走 fallback 路径(见下方"派 reviewer fallback")
47
- - **改 `plugins/` / `lib/` / `src/` 任意 .ts 后必须执行 `npm run dev`**(watch 模式可一直开着;单次跑用 `npm run dev:once`):opencode 加载 `~/.config/opencode/codeforge/index.js`(来自 build 后的 dist),**不是**仓库源文件;不跑 dev 则改动"看起来跑了实际没跑"。详见 ADR-0042 + ADR-0041。pre-commit hook 也会兜底拦截过期 dist。
47
+ - **改 `plugins/` / `lib/` / `src/` 任意 .ts 后必须执行 `npm run dev`**(watch 模式可一直开着;单次跑用 `npm run build:dev`):opencode 加载 `~/.config/opencode/codeforge/index.js`(来自 build 后的 dist),**不是**仓库源文件;不跑 dev 则改动"看起来跑了实际没跑"。详见 ADR-0042 + ADR-0041。pre-commit hook 也会兜底拦截过期 dist。
48
48
  - **prompt 含 `spec=<slug>` 时**(codeforge 走 discover spec 路径),**工作流 Step 0「PRE 阻断校验」必须先跑**:read `docs/specs/<slug>/handoff.yaml` → 优先 `pre_coding_blockers[]`(v1.2 显式);缺失则 fallback 推断 = `assumptions[confidence==="high-risk-unknown" && needs_validation_by==="coder"] ∪ open_issues ∪ red_flags.reasons`;**任何 PRE 未被父 prompt `pre_ack=<PRE-id>` 解除 → 拒绝启动**,按下方 boomerang 模板回报,**不**开始写文件
49
49
  - **工具调用层并发(Tool-call Concurrency)**:在同一次 LLM response 里,凡**互不依赖的只读操作**(`smart_search` / `plan_read` / `read` 等不产生副作用的调用)必须**并发 emit**,不允许串行等待。例如:需要同时查历史经验 + 读方案时,必须一次发出两个 tool call。只有当后一个工具依赖前一个结果时才允许串行。
50
50
 
package/agents/coder.md CHANGED
@@ -45,7 +45,7 @@ fallback_models:
45
45
  - **`ast_edit` 的 anchor 必须是单行**:含 `\n` 的多行 anchor 会被直接拒绝(reason=invalid_input);多行改动(YAML 列表 / 多行字符串 / ≥2 行匹配)直接用 `edit` / `write` 整文件改写,不要试 `ast_edit`
46
46
  - 改动完成后,必须用 `bash` 跑 `git status` / `git diff` 给用户看 worktree 内的全部改动摘要
47
47
  - 任务完成后,**默认回报给 codeforge orchestrator**(boomerang 摘要含 plan_id + worktree 内改动文件列表 + 测试结果 + 关键风险);仅当被用户直接 mention `@coder` 或 `/quick` 等命令显式调出(无 codeforge 上游)时,才走 fallback 路径(见下方"派 reviewer fallback")
48
- - **改 `plugins/` / `lib/` / `src/` 任意 .ts 后必须执行 `npm run dev`**(watch 模式可一直开着;单次跑用 `npm run dev:once`):opencode 加载 `~/.config/opencode/codeforge/index.js`(来自 build 后的 dist),**不是**仓库源文件;不跑 dev 则改动"看起来跑了实际没跑"。详见 ADR-0042 + ADR-0041。pre-commit hook 也会兜底拦截过期 dist。
48
+ - **改 `plugins/` / `lib/` / `src/` 任意 .ts 后必须执行 `npm run dev`**(watch 模式可一直开着;单次跑用 `npm run build:dev`):opencode 加载 `~/.config/opencode/codeforge/index.js`(来自 build 后的 dist),**不是**仓库源文件;不跑 dev 则改动"看起来跑了实际没跑"。详见 ADR-0042 + ADR-0041。pre-commit hook 也会兜底拦截过期 dist。
49
49
  - **prompt 含 `spec=<slug>` 时**(codeforge 走 discover spec 路径),**工作流 Step 0「PRE 阻断校验」必须先跑**:read `docs/specs/<slug>/handoff.yaml` → 优先 `pre_coding_blockers[]`(v1.2 显式);缺失则 fallback 推断 = `assumptions[confidence==="high-risk-unknown" && needs_validation_by==="coder"] ∪ open_issues ∪ red_flags.reasons`;**任何 PRE 未被父 prompt `pre_ack=<PRE-id>` 解除 → 拒绝启动**,按下方 boomerang 模板回报,**不**开始写文件
50
50
  - **工具调用层并发(Tool-call Concurrency)**:在同一次 LLM response 里,凡**互不依赖的只读操作**(`smart_search` / `plan_read` / `read` 等不产生副作用的调用)必须**并发 emit**,不允许串行等待。例如:需要同时查历史经验 + 读方案时,必须一次发出两个 tool call。只有当后一个工具依赖前一个结果时才允许串行。
51
51
 
@@ -147,7 +147,7 @@ reviewer 的 `read` 工具**仅允许**读以下路径(白名单):
147
147
  - 按 pending 文件清单加载对应 profile
148
148
  - 必须为每个文件(或方案章节)独立给出意见(不允许"整体看起来 OK"这种笼统结论)
149
149
  - 必须给出明确的最终决策:`APPROVE` / `REQUEST_CHANGES` / `BLOCK`
150
- - **`## Decision` 节内首行必须是 `APPROVE` / `REQUEST_CHANGES` / `BLOCK` 三选一**(可有 backtick 包裹,executor 容错;后续行写理由;ADR-0027 workflow 引擎按此节首行做分支跳转)
150
+ - **`## Decision` 节内首行必须是 `APPROVE` / `REQUEST_CHANGES` / `BLOCK` 三选一**(协议层,ADR:workflow-on-decision-branching)。⚠️ 严禁把审批层 verdict 字面量(`APPROVE_WITH_NOTES`)写进首行——容错层会归一为 APPROVE,但漂移到其他变体(`APPROVE_MINOR` 等)会被严格拒绝 → merge-loop 误判死循环。详见 ADR:decision-token-vs-approval-verdict-layering。
151
151
  - 如果 `REQUEST_CHANGES`,必须给出**具体到行**的修改建议(坐标 + 改成什么);审方案时给「方案哪段」的具体意见
152
152
  - 必须跑项目的测试 / lint 命令(除非 bash 被拒;模式 1/2 审方案/决策时不强制跑测试)
153
153
  - 完成审阅后,**默认回报给 codeforge orchestrator**(boomerang 摘要 = Decision + File-by-File 关键意见,不复制 diff 全文);仅当被用户直接 mention `@reviewer` 或工作流显式调出(无 codeforge 上游)时,才走 fallback 路径
@@ -169,6 +169,35 @@ reviewer 的 `read` 工具**仅允许**读以下路径(白名单):
169
169
  ```
170
170
  否则 codeforge / `/merge` workflow 后续会被 `plugins/tool-policy.ts` / merge-loop 硬拦截。`REQUEST_CHANGES` / `BLOCK` **不**调此工具。
171
171
 
172
+ - **两层 token 语义独立**(ADR:decision-token-vs-approval-verdict-layering):
173
+
174
+ | 层 | 出现位置 | 合法值 | 用途 |
175
+ |---|---|---|---|
176
+ | 协议层 | `## Decision` 节首行 | `APPROVE` / `REQUEST_CHANGES` / `BLOCK` | merge-loop 状态机、workflow on_decision 分支 |
177
+ | 审批层 | `review_approval({ verdict })` 工具参数 | `APPROVE` / `APPROVE_WITH_NOTES` | audit 记录"无保留通过"vs"通过但留意" |
178
+
179
+ **✅ 正确组合**(通过但留意):
180
+
181
+ ````
182
+ // 先调工具
183
+ review_approval({ verdict: "APPROVE_WITH_NOTES", pendingIds: [...], notes: "X 处可优化但不阻塞" })
184
+
185
+ // 再输出 markdown
186
+ ## Decision
187
+
188
+ APPROVE
189
+
190
+ 通过;详见 approval notes 中关于 X 的建议。
191
+ ````
192
+
193
+ **❌ 错误组合**(会触发 merge-loop 17 min 死循环 bug):
194
+
195
+ ````
196
+ ## Decision
197
+
198
+ APPROVE_WITH_NOTES ← ⚠️ 协议层不识别!容错层归一可救回,但 APPROVE_MINOR 等变体会失败
199
+ ````
200
+
172
201
  **MUST NOT**
173
202
 
174
203
  - ❌ 不允许编辑任何文件(permissions 已禁)
@@ -146,7 +146,7 @@ reviewer 的 `read` 工具**仅允许**读以下路径(白名单):
146
146
  - 按 pending 文件清单加载对应 profile
147
147
  - 必须为每个文件(或方案章节)独立给出意见(不允许"整体看起来 OK"这种笼统结论)
148
148
  - 必须给出明确的最终决策:`APPROVE` / `REQUEST_CHANGES` / `BLOCK`
149
- - **`## Decision` 节内首行必须是 `APPROVE` / `REQUEST_CHANGES` / `BLOCK` 三选一**(可有 backtick 包裹,executor 容错;后续行写理由;ADR-0027 workflow 引擎按此节首行做分支跳转)
149
+ - **`## Decision` 节内首行必须是 `APPROVE` / `REQUEST_CHANGES` / `BLOCK` 三选一**(协议层,ADR:workflow-on-decision-branching)。⚠️ 严禁把审批层 verdict 字面量(`APPROVE_WITH_NOTES`)写进首行——容错层会归一为 APPROVE,但漂移到其他变体(`APPROVE_MINOR` 等)会被严格拒绝 → merge-loop 误判死循环。详见 ADR:decision-token-vs-approval-verdict-layering。
150
150
  - 如果 `REQUEST_CHANGES`,必须给出**具体到行**的修改建议(坐标 + 改成什么);审方案时给「方案哪段」的具体意见
151
151
  - 必须跑项目的测试 / lint 命令(除非 bash 被拒;模式 1/2 审方案/决策时不强制跑测试)
152
152
  - 完成审阅后,**默认回报给 codeforge orchestrator**(boomerang 摘要 = Decision + File-by-File 关键意见,不复制 diff 全文);仅当被用户直接 mention `@reviewer` 或工作流显式调出(无 codeforge 上游)时,才走 fallback 路径
@@ -168,6 +168,35 @@ reviewer 的 `read` 工具**仅允许**读以下路径(白名单):
168
168
  ```
169
169
  否则 codeforge / `/merge` workflow 后续会被 `plugins/tool-policy.ts` / merge-loop 硬拦截。`REQUEST_CHANGES` / `BLOCK` **不**调此工具。
170
170
 
171
+ - **两层 token 语义独立**(ADR:decision-token-vs-approval-verdict-layering):
172
+
173
+ | 层 | 出现位置 | 合法值 | 用途 |
174
+ |---|---|---|---|
175
+ | 协议层 | `## Decision` 节首行 | `APPROVE` / `REQUEST_CHANGES` / `BLOCK` | merge-loop 状态机、workflow on_decision 分支 |
176
+ | 审批层 | `review_approval({ verdict })` 工具参数 | `APPROVE` / `APPROVE_WITH_NOTES` | audit 记录"无保留通过"vs"通过但留意" |
177
+
178
+ **✅ 正确组合**(通过但留意):
179
+
180
+ ````
181
+ // 先调工具
182
+ review_approval({ verdict: "APPROVE_WITH_NOTES", pendingIds: [...], notes: "X 处可优化但不阻塞" })
183
+
184
+ // 再输出 markdown
185
+ ## Decision
186
+
187
+ APPROVE
188
+
189
+ 通过;详见 approval notes 中关于 X 的建议。
190
+ ````
191
+
192
+ **❌ 错误组合**(会触发 merge-loop 17 min 死循环 bug):
193
+
194
+ ````
195
+ ## Decision
196
+
197
+ APPROVE_WITH_NOTES ← ⚠️ 协议层不识别!容错层归一可救回,但 APPROVE_MINOR 等变体会失败
198
+ ````
199
+
171
200
  **MUST NOT**
172
201
 
173
202
  - ❌ 不允许编辑任何文件(permissions 已禁)
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ // ADR:codeforge-doctor-command
3
+ /**
4
+ * codeforge-doctor — 主动诊断 CodeForge 安装健康状态
5
+ *
6
+ * 用法:
7
+ * node bin/codeforge-doctor.mjs # 默认检查 global (~/.config/opencode)
8
+ * node bin/codeforge-doctor.mjs --project # 检查 project (<cwd>/.opencode)
9
+ * codeforge doctor # 经 dispatcher
10
+ * codeforge doctor --project # 检查项目级
11
+ *
12
+ * 说明:
13
+ * 不依赖 dist/,直接 import install.mjs export(零依赖 ESM),
14
+ * npm 安装场景下也能运行。
15
+ *
16
+ * 检查项:
17
+ * D1 manifest 登记但 disk 缺失(来源2:外部腐蚀)
18
+ * D2 source 有但 disk 缺失(来源1/2:未安装或被删)
19
+ * D3 disk∩source 但 manifest 无(manifest 失配)
20
+ * 第三方文件(disk 有但不在 manifest 也不在 source)→ 静默跳过
21
+ *
22
+ * exit code:有 ✗ → 1,仅 ⚠ → 0,全绿 → 0(便于脚本化)
23
+ *
24
+ * scope 提示:显式标注当前检查的是哪个 target,对治跨 mode 混淆(来源1)。
25
+ */
26
+
27
+ import { existsSync } from "node:fs"
28
+ import * as path from "node:path"
29
+ import * as os from "node:os"
30
+ import * as url from "node:url"
31
+
32
+ const __filename = url.fileURLToPath(import.meta.url)
33
+ const __dirname = path.dirname(__filename)
34
+ const REPO_ROOT = path.resolve(__dirname, "..")
35
+
36
+ // ────────────────────────────────────────────────────────────────────
37
+ // import install.mjs export(compareManifestVsDisk / readMdManifest / MD_MANIFEST_REL)
38
+ // ────────────────────────────────────────────────────────────────────
39
+ const installMjs = path.join(REPO_ROOT, "install.mjs")
40
+ let compareManifestVsDisk, MD_MANIFEST_REL
41
+
42
+ try {
43
+ const mod = await import(url.pathToFileURL(installMjs).href)
44
+ compareManifestVsDisk = mod.compareManifestVsDisk
45
+ MD_MANIFEST_REL = mod.MD_MANIFEST_REL
46
+ } catch (e) {
47
+ console.error(`✗ 无法加载 install.mjs:${e?.message ?? e}`)
48
+ console.error(` 请确认 install.mjs 存在于:${installMjs}`)
49
+ process.exit(1)
50
+ }
51
+
52
+ // ────────────────────────────────────────────────────────────────────
53
+ // 颜色 / 日志
54
+ // ────────────────────────────────────────────────────────────────────
55
+ const TTY = process.stdout.isTTY
56
+ const C = TTY
57
+ ? { reset: "\x1b[0m", bold: "\x1b[1m", red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m", dim: "\x1b[2m" }
58
+ : { reset: "", bold: "", red: "", green: "", yellow: "", cyan: "", dim: "" }
59
+
60
+ // ────────────────────────────────────────────────────────────────────
61
+ // 路径解析(与 install.mjs resolvePaths 等价精简版)
62
+ // ────────────────────────────────────────────────────────────────────
63
+ function xdgConfigHome() {
64
+ if (process.env.XDG_CONFIG_HOME) return process.env.XDG_CONFIG_HOME
65
+ if (process.platform === "win32" && process.env.APPDATA) return process.env.APPDATA
66
+ return path.join(os.homedir(), ".config")
67
+ }
68
+
69
+ function resolveTarget(args) {
70
+ if (args.project) {
71
+ return { mode: "project", targetRoot: path.join(process.cwd(), ".opencode") }
72
+ }
73
+ return { mode: "global", targetRoot: path.join(xdgConfigHome(), "opencode") }
74
+ }
75
+
76
+ // ────────────────────────────────────────────────────────────────────
77
+ // 参数解析
78
+ // ────────────────────────────────────────────────────────────────────
79
+ function parseArgs(argv) {
80
+ const args = { project: false, help: false }
81
+ for (const a of argv) {
82
+ if (a === "--project" || a === "-p") args.project = true
83
+ if (a === "--help" || a === "-h") args.help = true
84
+ }
85
+ return args
86
+ }
87
+
88
+ // ────────────────────────────────────────────────────────────────────
89
+ // CLI
90
+ // ────────────────────────────────────────────────────────────────────
91
+ const rawArgs = process.argv.slice(2)
92
+ const args = parseArgs(rawArgs)
93
+
94
+ if (args.help) {
95
+ console.log(`${C.bold}codeforge doctor${C.reset} — 诊断 CodeForge 安装健康状态
96
+
97
+ 用法:
98
+ codeforge doctor # 检查 global (~/.config/opencode)
99
+ codeforge doctor --project # 检查 project (<cwd>/.opencode)
100
+
101
+ 检查项:
102
+ D1 manifest 登记但 disk 缺失 → ✗(建议跑 codeforge install)
103
+ D2 source 有但 disk 缺失 → ✗(建议跑 codeforge install)
104
+ D3 disk∩source 但 manifest 无 → ⚠(manifest 可能未更新)
105
+ 第三方 .md 文件(不在 manifest 也不在 source)→ 静默跳过
106
+
107
+ exit code:有 ✗ → 1,仅 ⚠ → 0,全绿 → 0`)
108
+ process.exit(0)
109
+ }
110
+
111
+ const { mode, targetRoot } = resolveTarget(args)
112
+ const shortenHome = (p) => {
113
+ const home = os.homedir()
114
+ return home && p.startsWith(home) ? "~" + p.slice(home.length) : p
115
+ }
116
+
117
+ console.log(`${C.bold}codeforge doctor${C.reset}`)
118
+ console.log(` scope : ${C.cyan}${mode}${C.reset}`)
119
+ console.log(` target : ${shortenHome(targetRoot)}`)
120
+ if (!existsSync(targetRoot)) {
121
+ console.log(` ${C.yellow}⚠${C.reset} target 目录不存在(未安装?)`)
122
+ console.log(` 建议:跑 ${C.bold}codeforge install${C.reset} 安装 CodeForge`)
123
+ process.exit(0)
124
+ }
125
+ console.log()
126
+
127
+ // 检查 manifest 是否存在(仅提示,不阻断后续检查)
128
+ const manifestPath = path.join(targetRoot, MD_MANIFEST_REL)
129
+ if (!existsSync(manifestPath)) {
130
+ console.log(` ${C.yellow}⚠${C.reset} manifest 文件不存在:${shortenHome(manifestPath)}`)
131
+ console.log(` 提示:可能是首次安装前、或 manifest 被删除。建议跑 ${C.bold}codeforge install${C.reset}`)
132
+ console.log()
133
+ }
134
+
135
+ // 执行三方比对
136
+ let result
137
+ try {
138
+ result = compareManifestVsDisk({ targetRoot, sourceRoot: REPO_ROOT })
139
+ } catch (e) {
140
+ console.error(`${C.red}✗${C.reset} compareManifestVsDisk 执行失败:${e?.message ?? e}`)
141
+ process.exit(1)
142
+ }
143
+
144
+ const { missing, unmanaged, stale } = result
145
+ let hasErrors = false
146
+
147
+ // D1:manifest 登记但 disk 缺失
148
+ if (missing.length > 0) {
149
+ hasErrors = true
150
+ console.log(`${C.red}✗${C.reset} ${C.bold}D1 manifest 登记但 disk 缺失${C.reset}(${missing.length} 个文件):`)
151
+ for (const { dir, file } of missing) {
152
+ console.log(` ${C.red}✗${C.reset} ${dir}/${file}`)
153
+ }
154
+ console.log(` 建议:跑 ${C.bold}codeforge install${C.reset} 重新安装缺失文件`)
155
+ console.log()
156
+ }
157
+
158
+ // D2:source 有但 disk 缺失
159
+ if (unmanaged.length > 0) {
160
+ hasErrors = true
161
+ console.log(`${C.red}✗${C.reset} ${C.bold}D2 source 有但 disk 缺失${C.reset}(${unmanaged.length} 个文件,可能未安装):`)
162
+ for (const { dir, file } of unmanaged) {
163
+ console.log(` ${C.red}✗${C.reset} ${dir}/${file}`)
164
+ }
165
+ console.log(` 建议:跑 ${C.bold}codeforge install${C.reset} 安装缺失文件`)
166
+ console.log()
167
+ }
168
+
169
+ // D3:disk∩source 但 manifest 无
170
+ if (stale.length > 0) {
171
+ console.log(`${C.yellow}⚠${C.reset} ${C.bold}D3 disk∩source 但 manifest 未登记${C.reset}(${stale.length} 个文件,manifest 可能未更新):`)
172
+ for (const { dir, file } of stale) {
173
+ console.log(` ${C.yellow}⚠${C.reset} ${dir}/${file}`)
174
+ }
175
+ console.log(` 建议:跑 ${C.bold}codeforge install${C.reset} 刷新 manifest`)
176
+ console.log()
177
+ }
178
+
179
+ // 全绿
180
+ if (!hasErrors && stale.length === 0) {
181
+ console.log(`${C.green}✓${C.reset} 所有文件检查通过,CodeForge 安装健康`)
182
+ if (mode === "global") {
183
+ console.log(` ${C.dim}提示:若你在项目目录里跑过 node install.mjs,也可跑 codeforge doctor --project 检查项目级${C.reset}`)
184
+ }
185
+ }
186
+
187
+ process.exit(hasErrors ? 1 : 0)
package/bin/codeforge.mjs CHANGED
@@ -390,6 +390,24 @@ async function cmdUpgrade(args) {
390
390
  return 0
391
391
  }
392
392
 
393
+ // ────────────────────────────────────────────────────────────────────
394
+ // 子命令:doctor —— 转发到 codeforge-doctor.mjs(ADR:codeforge-doctor-command)
395
+ // 不依赖 dist/,直接 import install.mjs export,npm 安装场景也能跑。
396
+ // ────────────────────────────────────────────────────────────────────
397
+ function cmdDoctor(args) {
398
+ const target = path.join(REPO_ROOT, "bin", "codeforge-doctor.mjs")
399
+ if (!existsSync(target)) {
400
+ err(`doctor 脚本不存在:${target}`)
401
+ err(`请重装 codeforge:npm install -g @andyqiu/codeforge`)
402
+ return 2
403
+ }
404
+ // 透传所有参数(--project / --help 等)
405
+ const passArgs = args._[0] === "doctor" ? args._.slice(1) : args._
406
+ const flagArgs = Object.entries(args.flags).map(([k, v]) => v === true ? `--${k}` : `--${k}=${v}`)
407
+ const r = spawnSync(process.execPath, [target, ...passArgs, ...flagArgs], { stdio: "inherit" })
408
+ return r.status ?? 1
409
+ }
410
+
393
411
  // ────────────────────────────────────────────────────────────────────
394
412
  // 子命令:runtime —— 转发到 codeforge-runtime-info.mjs
395
413
  // ────────────────────────────────────────────────────────────────────
@@ -496,6 +514,7 @@ function cmdHelp() {
496
514
  codeforge version
497
515
  codeforge rollback [--target=<path>] [--dry-run] # 恢复最近 backup(auto_install 失败救场)
498
516
  codeforge upgrade|update [--dry-run] # 升级到 npm latest 并重新 install --global
517
+ codeforge doctor [--project] # 诊断 CodeForge 安装健康状态(manifest/disk/source 三方比对)
499
518
  codeforge runtime where [<path>] # 打印当前项目的全局运行时目录
500
519
  codeforge adr-init [--force] [--dry-run] [--write-prepare] [--no-pre-push]
501
520
  # 把 ADR 校验体系(hooks + scripts + 模板)下发到当前 git 项目
@@ -559,6 +578,8 @@ async function main() {
559
578
  case "upgrade":
560
579
  case "update":
561
580
  return cmdUpgrade(args)
581
+ case "doctor":
582
+ return cmdDoctor(args)
562
583
  case "runtime":
563
584
  return cmdRuntime(args)
564
585
  case "adr-init":