@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.
- package/README.md +89 -33
- package/package.json +3 -3
- package/src/cli.js +40 -2
- package/src/codex-skills.js +4 -4
- package/src/commands/change-advice.js +191 -0
- package/src/commands/change-evidence-command.js +242 -0
- package/src/commands/change-evidence-entry.js +40 -0
- package/src/commands/change-evidence.js +338 -0
- package/src/commands/change-finish.js +484 -0
- package/src/commands/change-git-status.js +53 -0
- package/src/commands/change-lifecycle-entry.js +60 -0
- package/src/commands/change-lifecycle.js +234 -0
- package/src/commands/change-metrics-command.js +110 -0
- package/src/commands/change-next-command.js +132 -0
- package/src/commands/change-review-gates.js +315 -0
- package/src/commands/change-scope-gate.js +34 -0
- package/src/commands/change-start.js +138 -0
- package/src/commands/change-state-command.js +40 -0
- package/src/commands/change-status-command.js +126 -0
- package/src/commands/change-status-context.js +202 -0
- package/src/commands/change-validate-entry.js +45 -0
- package/src/commands/change-validate.js +127 -0
- package/src/commands/change.js +540 -1692
- package/src/commands/dashboard.js +38 -12
- package/src/commands/init.js +1 -1
- package/src/commands/opencode-status.js +61 -0
- package/src/commands/update.js +5 -1
- package/src/dashboard/app.js +205 -2
- package/src/dashboard/index.html +5 -2
- package/src/dashboard/style.css +86 -0
- package/src/governance.js +157 -0
- package/src/json-schema-lite.js +164 -0
- package/src/opencode-env.js +76 -0
- package/src/spec.js +131 -0
- package/src/template.js +42 -2
package/README.md
CHANGED
|
@@ -1,34 +1,68 @@
|
|
|
1
1
|
# @aipper/aiws
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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 <
|
|
44
|
-
aiws
|
|
45
|
-
aiws
|
|
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
|
-
##
|
|
95
|
+
## Dashboard
|
|
49
96
|
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
68
|
-
- `aiws validate
|
|
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.
|
|
4
|
-
"description": "AI Workspace CLI (init/update/validate) for Claude Code / OpenCode / Codex
|
|
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.
|
|
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: ~/.
|
|
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,
|
package/src/codex-skills.js
CHANGED
|
@@ -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 `~/.
|
|
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(), ".
|
|
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
|
+
}
|