@aipper/aiws 0.0.24 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +89 -33
  2. package/package.json +3 -3
  3. package/src/cli.js +40 -2
  4. package/src/codex-skills.js +4 -4
  5. package/src/commands/change-advice.js +191 -0
  6. package/src/commands/change-evidence-command.js +242 -0
  7. package/src/commands/change-evidence-entry.js +40 -0
  8. package/src/commands/change-evidence.js +338 -0
  9. package/src/commands/change-finish.js +484 -0
  10. package/src/commands/change-git-status.js +53 -0
  11. package/src/commands/change-lifecycle-entry.js +60 -0
  12. package/src/commands/change-lifecycle.js +234 -0
  13. package/src/commands/change-metrics-command.js +110 -0
  14. package/src/commands/change-next-command.js +132 -0
  15. package/src/commands/change-review-gates.js +315 -0
  16. package/src/commands/change-scope-gate.js +34 -0
  17. package/src/commands/change-start.js +138 -0
  18. package/src/commands/change-state-command.js +40 -0
  19. package/src/commands/change-status-command.js +126 -0
  20. package/src/commands/change-status-context.js +202 -0
  21. package/src/commands/change-validate-entry.js +45 -0
  22. package/src/commands/change-validate.js +127 -0
  23. package/src/commands/change.js +540 -1692
  24. package/src/commands/dashboard.js +38 -12
  25. package/src/commands/init.js +1 -1
  26. package/src/commands/opencode-status.js +61 -0
  27. package/src/commands/update.js +5 -1
  28. package/src/dashboard/app.js +205 -2
  29. package/src/dashboard/index.html +5 -2
  30. package/src/dashboard/style.css +86 -0
  31. package/src/governance.js +157 -0
  32. package/src/json-schema-lite.js +164 -0
  33. package/src/opencode-env.js +76 -0
  34. package/src/spec.js +131 -0
  35. package/src/template.js +42 -2
package/README.md CHANGED
@@ -1,34 +1,68 @@
1
1
  # @aipper/aiws
2
2
 
3
- AI Workspace CLI:把 Claude Code / OpenCode / Codex / iFlow 对齐到同一套“真值文件 + 可审计工作流”,降低规则漂移。
3
+ `@aipper/aiws` 是面向 AI coding workflow CLI。
4
4
 
5
- 核心能力:
6
- - 初始化/更新模板:`aiws init` / `aiws update`
7
- - 强门禁校验:`aiws validate`(可选证据落盘 `--stamp`)
8
- - 回滚:`aiws rollback`(从 `.aiws/backups/` 恢复)
9
- - 变更工件工作流(脱离 dotfiles):`aiws change ...`
10
- - Git hooks:`aiws hooks install/status`
11
- - Codex:repo skills(推荐)+ 可选全局 skills 安装
5
+ 它负责把真值文件、变更工件、门禁和跨工具入口落到仓库里,让 Claude Code、OpenCode、Codex 可以共享同一套项目约束,而不是每个工具各写一份 prompt。
12
6
 
13
- 真值来源(SSOT):`@aipper/aiws-spec`(模板与契约)。
7
+ ## 当前能力
8
+
9
+ - `aiws init` / `aiws update`:初始化或升级 workspace 模板
10
+ - `aiws validate`:校验模板漂移与门禁,可选 `--stamp`
11
+ - `aiws rollback`:从 `.aiws/backups/` 回滚
12
+ - `aiws change ...`:变更工件工作流
13
+ - `aiws dashboard serve`:本地 change/dashboard API
14
+ - `aiws hooks install/status`
15
+ - `aiws opencode status`
16
+ - `aiws codex install-skills/status-skills/uninstall-skills`
17
+
18
+ 支持工具:
19
+ - Claude Code
20
+ - OpenCode
21
+ - Codex
22
+
23
+ 说明:
24
+ - `ws-dev-lite` 是当前推荐的小改动入口
14
25
 
15
26
  ## 安装
16
27
 
17
- 二选一:
18
- - 全局安装(推荐,适合长期使用):`npm i -g @aipper/aiws`
19
- - 临时运行(不污染全局版本):`npx @aipper/aiws <command>`
28
+ ```bash
29
+ npm i -g @aipper/aiws
30
+ #
31
+ npx @aipper/aiws --help
32
+ ```
20
33
 
21
34
  ## 快速开始
22
35
 
23
- 在任意 git 仓库根目录:
24
36
  ```bash
25
37
  aiws init .
26
38
  aiws hooks install .
27
39
 
28
- # 建议:用 change 工作流生成可审计工件并切分支
29
40
  aiws change start demo-change --no-design --hooks
41
+ aiws change validate demo-change --strict
42
+ aiws validate . --stamp
43
+ ```
44
+
45
+ 初始化后会生成:
46
+ - 真值文件:`AI_PROJECT.md`、`REQUIREMENTS.md`、`AI_WORKSPACE.md`
47
+ - 工件目录:`changes/`
48
+ - AI 入口:`.agents/skills/`、`.claude/skills/`、`.claude/commands/`、`.opencode/skills/`、`.opencode/commands/`
49
+ - hooks:`.githooks/`
30
50
 
31
- # 提交前门禁(并落盘证据)
51
+ ## 推荐用法
52
+
53
+ AI 工具内:
54
+ - 不确定该从哪里开始:`$using-aiws`
55
+ - simple/local 单点修复:`$ws-dev-lite`
56
+ - 常规实现:`$ws-dev`
57
+ - 中大型任务:`$ws-plan` → `$ws-plan-verify` → `$ws-dev`
58
+ - 提交前:`$ws-review` / `$ws-commit`
59
+ - 交付前:`$ws-deliver` / `$ws-finish`
60
+
61
+ shell 内:
62
+
63
+ ```bash
64
+ aiws change start <change-id> --no-design
65
+ aiws change validate <change-id> --strict
32
66
  aiws validate . --stamp
33
67
  ```
34
68
 
@@ -40,29 +74,51 @@ aiws update [path]
40
74
  aiws validate [path] [--stamp]
41
75
  aiws rollback [path] <timestamp|latest>
42
76
 
43
- aiws change <list|new|start|status|next|sync|validate|archive|templates>
44
- aiws hooks <install|status>
45
- aiws codex <install-skills|status-skills|uninstall-skills|install-prompts|status|uninstall-prompts>
77
+ aiws change new <change-id> [--no-design]
78
+ aiws change start <change-id> [--switch|--no-switch|--worktree]
79
+ aiws change status [change-id]
80
+ aiws change next [change-id]
81
+ aiws change sync [change-id]
82
+ aiws change validate [change-id] [--strict] [--check-evidence] [--check-scope]
83
+ aiws change evidence [change-id]
84
+ aiws change archive [change-id]
85
+
86
+ aiws dashboard serve [--host 127.0.0.1] [--port 3456]
87
+ aiws hooks install [path]
88
+ aiws hooks status [path]
89
+ aiws opencode status [path]
90
+ aiws codex install-skills
91
+ aiws codex status-skills
92
+ aiws codex uninstall-skills
46
93
  ```
47
94
 
48
- ## Codex(repo skills 优先)
95
+ ## Dashboard
49
96
 
50
- - `aiws init .` 会生成 `.agents/skills/`(随仓库共享),在 Codex 中可显式调用(示例):`$ws-preflight` / `$ws-plan` / `$p-tasks-plan` / `$ws-handoff` / `$ws-dev` / `$ws-review` / `$ws-commit`
51
- - 交付收尾(submodules+superproject 分步提交 + 安全合并):`$ws-deliver`
52
- - 收尾(安全合并回目标分支):`$ws-finish`(底层调用 `aiws change finish`,默认 fast-forward)
53
- - 可选:安装全局 skills 到 `~/.codex/skills/`(或 `$CODEX_HOME/skills`):
54
- - `aiws codex install-skills`
55
- - `aiws codex status-skills`
56
- - `aiws codex uninstall-skills`
57
- - legacy prompts(deprecated,仅兼容):`aiws codex install-prompts`
97
+ `aiws dashboard serve` 会显示:
98
+ - change phase
99
+ - strict blockers
100
+ - review gates
101
+ - scope gate
102
+ - collaboration 统计
103
+ - next advice
58
104
 
59
- ## 证据落盘与协作
105
+ 本地 API:
106
+ - `/api/changes`
107
+ - `/api/workflow-stages`
108
+ - `/api/change/<id>/validate?strict=1`
60
109
 
61
- - `aiws validate --stamp` 会写入:`.agentdocs/tmp/aiws-validate/*.json`(默认被 `.gitignore` 忽略)
62
- - `$ws-review` 会落盘:`.agentdocs/tmp/review/codex-review.md`(默认被 `.gitignore` 忽略)
63
- - 建议把“可协作/可审计”的内容放进 `changes/<change-id>/`(proposal/tasks/design)并提交;`.agentdocs/tmp/` 作为本地缓存更稳
110
+ ## 跨工具入口
111
+
112
+ `aiws init .` 会把入口投影到:
113
+ - Codex:`.agents/skills/*`
114
+ - Claude:`.claude/skills/*` + `.claude/commands/*`
115
+ - OpenCode:`.opencode/skills/*` + `.opencode/commands/*` + `.opencode/command/*`
116
+
117
+ 目标是统一 workflow 入口,不保证工具自动发现或自动加载。
64
118
 
65
119
  ## 运行要求
66
120
 
67
- - Node.js:>= 20
68
- - `aiws validate`:需要 `python3`(用于执行 `tools/ws_change_check.py` 与 `tools/requirements_contract.py`)
121
+ - Node.js >= 20
122
+ - `aiws validate` 需要 `python3`
123
+
124
+ 更多仓库级说明见根 README:[`../../README.md`](../../README.md)
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@aipper/aiws",
3
- "version": "0.0.24",
4
- "description": "AI Workspace CLI (init/update/validate) for Claude Code / OpenCode / Codex / iFlow.",
3
+ "version": "0.0.26",
4
+ "description": "AI Workspace CLI (init/update/validate) for Claude Code / OpenCode / Codex.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "aiws": "./bin/aiws.js"
8
8
  },
9
9
  "dependencies": {
10
- "@aipper/aiws-spec": "0.0.24"
10
+ "@aipper/aiws-spec": "0.0.26"
11
11
  },
12
12
  "files": [
13
13
  "bin",
package/src/cli.js CHANGED
@@ -14,6 +14,7 @@ import { codexStatusSkillsCommand } from "./commands/codex-status-skills.js";
14
14
  import { codexUninstallSkillsCommand } from "./commands/codex-uninstall-skills.js";
15
15
  import { hooksInstallCommand } from "./commands/hooks-install.js";
16
16
  import { hooksStatusCommand } from "./commands/hooks-status.js";
17
+ import { opencodeStatusCommand } from "./commands/opencode-status.js";
17
18
  import { dashboardServeCommand } from "./commands/dashboard.js";
18
19
  import {
19
20
  changeArchiveCommand,
@@ -46,6 +47,10 @@ export async function cliMain(argv) {
46
47
  printHooksHelp();
47
48
  return 0;
48
49
  }
50
+ if (args[0] === "opencode" && (args.includes("-h") || args.includes("--help"))) {
51
+ printOpenCodeHelp();
52
+ return 0;
53
+ }
49
54
  if (args[0] === "dashboard" && (args.includes("-h") || args.includes("--help"))) {
50
55
  printDashboardHelp();
51
56
  return 0;
@@ -242,6 +247,23 @@ export async function cliMain(argv) {
242
247
  throw new UserError(`Unknown dashboard subcommand: ${sub}`, { details: "Use `aiws dashboard --help` to see available subcommands." });
243
248
  }
244
249
  }
250
+ case "opencode": {
251
+ const sub = args.shift();
252
+ if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
253
+ printOpenCodeHelp();
254
+ return 0;
255
+ }
256
+ switch (sub) {
257
+ case "status": {
258
+ const { positionals } = parseArgs(args, {});
259
+ const targetPath = positionals[0] ?? ".";
260
+ await opencodeStatusCommand({ targetPath });
261
+ return 0;
262
+ }
263
+ default:
264
+ throw new UserError(`Unknown opencode subcommand: ${sub}`, { details: "Use `aiws opencode --help` to see available subcommands." });
265
+ }
266
+ }
245
267
  case "change": {
246
268
  const sub = args.shift();
247
269
  if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
@@ -433,6 +455,7 @@ Usage:
433
455
  aiws change <subcommand>
434
456
  aiws metrics <subcommand>
435
457
  aiws dashboard <subcommand>
458
+ aiws opencode <subcommand>
436
459
  aiws codex <subcommand>
437
460
  aiws hooks <subcommand>
438
461
 
@@ -458,7 +481,8 @@ Usage:
458
481
  Notes:
459
482
  - Prefer repo skills: .agents/skills (no install needed)
460
483
  - Prefer global skills over legacy prompts
461
- - Default skills dir: ~/.codex/skills (or $CODEX_HOME/skills)
484
+ - Default skills dir: ~/.agents/skills
485
+ - Compatibility/test override: $CODEX_HOME/.agents/skills or --dir <path>
462
486
  - install-prompts is legacy (prompts are deprecated)
463
487
  - Default target dir: ~/.codex/prompts
464
488
  - If CODEX_HOME is set: $CODEX_HOME/prompts
@@ -478,6 +502,18 @@ Notes:
478
502
  `);
479
503
  }
480
504
 
505
+ function printOpenCodeHelp() {
506
+ console.log(`aiws opencode
507
+
508
+ Usage:
509
+ aiws opencode status [path]
510
+
511
+ Notes:
512
+ - Detects project-local .opencode/ and oh-my-opencode (oMo) config.
513
+ - Helps verify whether AIWS OpenCode skills should prefer oMo agents or fallback to standard-opencode.
514
+ `);
515
+ }
516
+
481
517
  function printDashboardHelp() {
482
518
  console.log(`aiws dashboard
483
519
 
@@ -486,6 +522,8 @@ Usage:
486
522
 
487
523
  Notes:
488
524
  - Local-only dashboard, no external dependencies.
525
+ - Dashboard also exposes workflow governance data sourced from @aipper/aiws-spec:
526
+ /api/changes, /api/workflow-stages, /api/change/<id>/validate?strict=1
489
527
  - Recommended: run change quality gate first (AI tools: $ws-plan-verify):
490
528
  aiws change validate <change-id> --strict
491
529
  `);
@@ -515,7 +553,7 @@ Notes:
515
553
  you can omit <change-id> for status/next/validate/sync/archive/finish.
516
554
  - Execution quality gate first: aiws change validate [change-id] --strict
517
555
  (equivalent to running $ws-plan-verify in AI tools).
518
- - status/next now prioritize quality-gate guidance before coding ($ws-dev).
556
+ - status/next now prioritize quality-gate guidance before coding ($ws-dev-lite / $ws-dev).
519
557
  - start refuses to switch/create worktree with a dirty working tree (unless --allow-dirty).
520
558
  - finish --push is submodule-aware: when .gitmodules exists it pushes submodules first, then the target branch, then safely cleans the separate change worktree if possible.
521
559
  - If .gitmodules exists and you didn't specify --switch/--no-switch/--worktree,
@@ -7,16 +7,16 @@ import { UserError } from "./errors.js";
7
7
  /**
8
8
  * Resolve Codex global skills directory:
9
9
  * - If `--dir` provided, use it directly.
10
- * - Else if `CODEX_HOME` is set, use `$CODEX_HOME/skills`.
11
- * - Else use `~/.codex/skills`.
10
+ * - Else if `CODEX_HOME` is set, use `$CODEX_HOME/.agents/skills` as a compatibility/test override root.
11
+ * - Else use `~/.agents/skills`.
12
12
  *
13
13
  * @param {string | undefined} dirOption
14
14
  */
15
15
  export function resolveCodexSkillsDir(dirOption) {
16
16
  if (dirOption) return path.resolve(dirOption);
17
17
  const envHome = String(process.env.CODEX_HOME || "").trim();
18
- if (envHome) return path.join(path.resolve(envHome), "skills");
19
- return path.join(os.homedir(), ".codex", "skills");
18
+ if (envHome) return path.join(path.resolve(envHome), ".agents", "skills");
19
+ return path.join(os.homedir(), ".agents", "skills");
20
20
  }
21
21
 
22
22
  function timestamp() {
@@ -0,0 +1,191 @@
1
+ import path from "node:path";
2
+
3
+ /**
4
+ * @param {string} gitRoot
5
+ * @param {string} changeId
6
+ * @param {{
7
+ * listGitWorktrees: (gitRoot: string) => Promise<Array<{ worktree: string, branch: string }>>
8
+ * }} deps
9
+ */
10
+ export async function resolveChangeWorktreeHint(gitRoot, changeId, deps) {
11
+ try {
12
+ const worktrees = await deps.listGitWorktrees(gitRoot);
13
+ const changeRef = `refs/heads/change/${changeId}`;
14
+ const changeWorktree = worktrees.find((worktree) => String(worktree.branch || "") === changeRef)?.worktree || "";
15
+ return changeWorktree && path.resolve(changeWorktree) !== path.resolve(gitRoot) ? changeWorktree : "";
16
+ } catch {
17
+ return "";
18
+ }
19
+ }
20
+
21
+ /**
22
+ * @param {string} gitRoot
23
+ * @param {string} changeId
24
+ * @param {{
25
+ * computeChangeStatus: (gitRoot: string, changeId: string) => Promise<any>,
26
+ * currentBranch: (gitRoot: string) => Promise<string>,
27
+ * inferChangeIdFromBranch: (branch: string) => string,
28
+ * listGitWorktrees: (gitRoot: string) => Promise<Array<{ worktree: string, branch: string }>>,
29
+ * changeDirAbs: (gitRoot: string, changeId: string) => string,
30
+ * governanceGuidanceLines: (status: any) => Promise<string[]>,
31
+ * scopeGateFailureAction: (changeId: string) => string,
32
+ * scopeGatePendingRecommendation: (changeId: string) => string,
33
+ * scopeGatePassedRecommendation: () => string
34
+ * }} deps
35
+ */
36
+ export async function computeChangeNextAdvice(gitRoot, changeId, deps) {
37
+ const status = await deps.computeChangeStatus(gitRoot, changeId);
38
+ const branch = await deps.currentBranch(gitRoot);
39
+ const inferred = deps.inferChangeIdFromBranch(branch);
40
+ const advice = {
41
+ changeId,
42
+ branch: branch || "",
43
+ inferredChangeId: inferred || "",
44
+ phase: status.phase,
45
+ governance: status.governance || null,
46
+ scopeGate: status.scopeGate || null,
47
+ blockersStrict: status.blockersStrict,
48
+ evidence: status.evidence?.counts || null,
49
+ collaboration: status.collaboration?.counts || null,
50
+ actions: [],
51
+ prohibitions: [],
52
+ recommended: [],
53
+ };
54
+
55
+ if (!branch) {
56
+ advice.actions.push("当前为 detached HEAD:建议切到 `change/<change-id>` 分支后再继续(或显式传入 `<change-id>`)");
57
+ } else if (inferred && inferred !== changeId) {
58
+ advice.actions.push(`当前分支为 \`${branch}\`(推断 change=${inferred}),但你在查看 change=${changeId}:建议先切换到 \`change/${changeId}\` 再执行开发/交付`);
59
+ } else if (!inferred) {
60
+ advice.actions.push(`当前分支为 \`${branch}\`(非 change 分支):建议 \`aiws change start ${changeId} --hooks --switch\` 或 \`git switch change/${changeId}\``);
61
+ }
62
+
63
+ const changeWorktree = await resolveChangeWorktreeHint(gitRoot, changeId, deps);
64
+ if (changeWorktree) {
65
+ advice.actions.push(`change/${changeId} 分支在另一个 worktree:\`cd ${changeWorktree}\`(建议在该目录执行开发/交付相关命令)`);
66
+ advice.prohibitions.push("不要在当前 worktree 继续写代码(很可能不在 change 分支上)");
67
+ }
68
+
69
+ if (status.metaState !== "ok") advice.actions.push(`补齐元信息并建立真值基线:\`aiws change sync ${changeId}\``);
70
+ if (status.driftFiles.length > 0) {
71
+ advice.actions.push("真值/合同已变化:先对齐 `AI_PROJECT.md` / `AI_WORKSPACE.md` / `REQUIREMENTS.md` 与 proposal/tasks");
72
+ advice.actions.push(`确认后同步基线:\`aiws change sync ${changeId}\``);
73
+ }
74
+
75
+ const proposalPath = path.join(deps.changeDirAbs(gitRoot, changeId), "proposal.md");
76
+ const tasksPath = path.join(deps.changeDirAbs(gitRoot, changeId), "tasks.md");
77
+ if (status.blockersStrict.some((item) => item.includes("proposal.md") && item.includes("WS:TODO"))) {
78
+ advice.actions.push(`完善 proposal:\`$EDITOR ${proposalPath}\``);
79
+ }
80
+ if (status.blockersStrict.some((item) => item.includes("tasks.md") && item.includes("WS:TODO"))) {
81
+ advice.actions.push(`完善 tasks:\`$EDITOR ${tasksPath}\``);
82
+ }
83
+ if (status.blockersStrict.some((item) => item.includes("proposal.md missing attribution"))) {
84
+ advice.actions.push("补齐归因:在 proposal.md 填写非空 `Req_ID` 或 `Problem_ID`(严格校验需要)");
85
+ }
86
+ if (status.blockersStrict.some((item) => item.includes("missing Contract_Row"))) {
87
+ advice.actions.push("补齐绑定:在 proposal.md 填写非空 `Contract_Row`(严格校验需要)");
88
+ }
89
+ if (status.blockersStrict.some((item) => item.includes("Plan_File"))) {
90
+ advice.actions.push("补齐绑定:修正 proposal.md 的 `Plan_File`(严格校验需要;必须存在且仅 1 个)");
91
+ }
92
+ if (status.blockersStrict.some((item) => item.includes("missing Evidence_Path"))) {
93
+ advice.actions.push("补齐绑定:在 proposal.md 填写非空 `Evidence_Path`(严格校验需要;建议优先写 `changes/<id>/evidence/...`)");
94
+ }
95
+ if (status.scopeGate?.status === "failed") {
96
+ advice.actions.push(deps.scopeGateFailureAction(changeId));
97
+ }
98
+ if (status.tasks.unchecked > 0) advice.actions.push(`完成未勾选任务(${status.tasks.unchecked} 项)`);
99
+ if (status.blockersStrict.length === 0 && status.tasks.unchecked > 0) {
100
+ advice.actions.push("质量门通过后继续开发:simple/local 单点修复优先 `$ws-dev-lite`;否则 `$ws-dev`");
101
+ }
102
+ if ((status.collaboration?.counts?.analysis || 0) > 0 && (status.collaboration?.counts?.review || 0) === 0) {
103
+ advice.actions.push("已有委托分析但还没有 review 汇总结论:建议在 `changes/<id>/review/` 落盘结论后再交付");
104
+ }
105
+ if ((status.collaboration?.counts?.patches || 0) > 0) {
106
+ advice.actions.push("存在委托 patch 草案:提交/交付前先人工审查 `changes/<id>/patches/`,不要直接无审查应用");
107
+ }
108
+
109
+ if (status.blockersStrict.length > 0 && advice.actions.length === 0) {
110
+ advice.actions.push("先修复 Blockers (strict) 后复跑质量门,再进入编码:simple/local 单点修复优先 `$ws-dev-lite`;否则 `$ws-dev`");
111
+ }
112
+
113
+ if (status.blockersStrict.length === 0 && status.tasks.unchecked === 0) {
114
+ for (const line of await deps.governanceGuidanceLines(status)) advice.recommended.push(line);
115
+ if (advice.evidence && advice.evidence.persistent === 0) {
116
+ advice.recommended.push("交付前建议生成持久证据并回填:`aiws change evidence <change-id>`");
117
+ }
118
+ if (status.scopeGate?.status === "not_run") {
119
+ advice.recommended.push(deps.scopeGatePendingRecommendation(changeId));
120
+ } else if (status.scopeGate?.status === "passed") {
121
+ advice.recommended.push(deps.scopeGatePassedRecommendation());
122
+ }
123
+ if ((status.governance?.recommendedStage || "") === "ws-finish" || (status.governance?.currentStage || "") === "ws-commit") {
124
+ advice.recommended.push("交付前门禁(推荐落盘):`aiws validate . --stamp`");
125
+ }
126
+ if ((status.collaboration?.counts?.review || 0) > 1) advice.recommended.push("存在多份 review:交付前建议在 handoff 或 review 汇总里收敛最终结论");
127
+ } else if (advice.actions.length === 0) {
128
+ advice.recommended.push("继续开发:simple/local 单点修复优先 `$ws-dev-lite`;否则 `$ws-dev`(小步实现 + 可复现验证)");
129
+ if ((status.collaboration?.counts?.analysis || 0) > 0) advice.recommended.push("已有委托分析:进入实现前先收敛 `changes/<id>/analysis/` 结论");
130
+ if ((status.collaboration?.counts?.patches || 0) > 0) advice.recommended.push("已有 patch 草案:应用前先人工审查 `changes/<id>/patches/`");
131
+ }
132
+
133
+ return advice;
134
+ }
135
+
136
+ /**
137
+ * @param {{
138
+ * changeId: string,
139
+ * gitRoot: string,
140
+ * now: string,
141
+ * status: any,
142
+ * advice: any,
143
+ * planVerifyLine: string
144
+ * }} input
145
+ */
146
+ export function renderChangeStateMarkdown(input) {
147
+ /** @type {string[]} */
148
+ const lines = [];
149
+ lines.push(`# AIWS STATE: ${input.changeId}`);
150
+ lines.push("");
151
+ lines.push(`Generated: ${input.now}`);
152
+ lines.push("");
153
+ lines.push("## Context");
154
+ lines.push(`- Worktree: \`${input.gitRoot}\``);
155
+ lines.push(`- Branch: \`${input.advice.branch || "(detached)"}\``);
156
+ lines.push("");
157
+ lines.push("## Phase");
158
+ lines.push(`- Phase: \`${input.status.phase}\``);
159
+ lines.push(`- Tasks: ${input.status.tasks.done}/${input.status.tasks.total} (unchecked=${input.status.tasks.unchecked})`);
160
+ lines.push("");
161
+ lines.push("## Bindings");
162
+ if (input.status.reqId) lines.push(`- Req_ID: \`${input.status.reqId}\``);
163
+ if (input.status.probId) lines.push(`- Problem_ID: \`${input.status.probId}\``);
164
+ if (input.status.bindings?.contractRow) lines.push(`- Contract_Row: \`${input.status.bindings.contractRow}\``);
165
+ if (input.status.bindings?.planFile) lines.push(`- Plan_File: \`${input.status.bindings.planFile}\``);
166
+ lines.push("");
167
+ lines.push("## Evidence");
168
+ if (input.status.evidence?.counts) {
169
+ const counts = input.status.evidence.counts;
170
+ lines.push(
171
+ `- Declared: \`${counts.total}\` (exists=\`${counts.exists}\`, missing=\`${counts.missing}\`, persistent=\`${counts.persistent}\`, tmp=\`${counts.tmp}\`)`,
172
+ );
173
+ } else {
174
+ lines.push("- (unknown)");
175
+ }
176
+ lines.push("");
177
+ lines.push("## Blockers (strict)");
178
+ if (input.status.blockersStrict.length === 0) lines.push("- (none)");
179
+ else for (const blocker of input.status.blockersStrict) lines.push(`- ${blocker}`);
180
+ lines.push("");
181
+ lines.push("## Next");
182
+ lines.push(`- ${input.planVerifyLine}`);
183
+ for (const action of input.advice.actions) lines.push(`- ${action}`);
184
+ for (const recommendation of input.advice.recommended) lines.push(`- ${recommendation}`);
185
+ if (input.advice.prohibitions.length > 0) {
186
+ lines.push("");
187
+ lines.push("## Do Not");
188
+ for (const prohibition of input.advice.prohibitions) lines.push(`- ${prohibition}`);
189
+ }
190
+ return lines.join("\n") + "\n";
191
+ }