@aipper/aiws-spec 0.0.20 → 0.0.21
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/docs/cli-interface.md +32 -1
- package/package.json +1 -1
- package/templates/workspace/.agents/skills/ws-commit/SKILL.md +10 -1
- package/templates/workspace/.agents/skills/ws-deliver/SKILL.md +34 -4
- package/templates/workspace/.agents/skills/ws-dev/SKILL.md +1 -0
- package/templates/workspace/.agents/skills/ws-finish/SKILL.md +41 -1
- package/templates/workspace/.claude/commands/ws-commit.md +2 -1
- package/templates/workspace/.claude/commands/ws-deliver.md +7 -1
- package/templates/workspace/.claude/commands/ws-dev.md +4 -2
- package/templates/workspace/.claude/commands/ws-finish.md +10 -1
- package/templates/workspace/.codex/prompts/ws-dev.md +5 -2
- package/templates/workspace/.iflow/commands/ws-commit.toml +2 -1
- package/templates/workspace/.iflow/commands/ws-deliver.toml +7 -1
- package/templates/workspace/.iflow/commands/ws-finish.toml +10 -1
- package/templates/workspace/.opencode/command/ws-commit.md +2 -1
- package/templates/workspace/.opencode/command/ws-deliver.md +7 -1
- package/templates/workspace/.opencode/command/ws-dev.md +4 -2
- package/templates/workspace/.opencode/command/ws-finish.md +10 -1
- package/templates/workspace/AI_PROJECT.md +2 -0
- package/templates/workspace/changes/templates/proposal.md +35 -5
- package/templates/workspace/tools/ws_change_check.py +198 -0
package/docs/cli-interface.md
CHANGED
|
@@ -274,9 +274,31 @@
|
|
|
274
274
|
- 调用 `python3 tools/ws_change_check.py` 校验单个 change 工件目录。
|
|
275
275
|
- `--strict`:强门禁(例如 WS:TODO、占位符、缺归因、truth drift 等都失败)。
|
|
276
276
|
- `--allow-truth-drift`:仅用于紧急场景跳过 truth drift gating(仍会输出 warnings;不建议常用)。
|
|
277
|
+
- `--check-evidence`:证据存在性门禁(可选):校验 `Evidence_Path` 声明的路径必须是工作区相对路径且文件存在;并要求至少包含 1 条持久证据路径(`changes/<id>/evidence/...` 或 `changes/<id>/review/...`)。
|
|
278
|
+
- `--check-scope`:scope 门禁(可选,best-effort):要求 proposal/plan 中包含 “In Scope” 与 “Out of Scope” 标记(用于减少 scope creep)。
|
|
277
279
|
|
|
278
280
|
接口(仅定义):
|
|
279
|
-
- `aiws change validate [<change-id>] [--strict] [--allow-truth-drift]`
|
|
281
|
+
- `aiws change validate [<change-id>] [--strict] [--allow-truth-drift] [--check-evidence] [--check-scope]`
|
|
282
|
+
|
|
283
|
+
说明:
|
|
284
|
+
- `--check-evidence/--check-scope` 默认不启用,避免对历史 change 造成行为破坏;交付阶段建议启用 `--check-evidence`。
|
|
285
|
+
|
|
286
|
+
### `aiws change evidence`
|
|
287
|
+
|
|
288
|
+
语义:
|
|
289
|
+
- 为交付阶段生成“可入库的持久证据”,并自动回填 `Evidence_Path`:
|
|
290
|
+
- 写入 `changes/<change-id>/evidence/`(例如:严格门禁结果快照、状态快照、validate stamp 复制等)
|
|
291
|
+
- 将生成的证据路径追加回填到:
|
|
292
|
+
- `changes/<change-id>/proposal.md` 的 `Evidence_Path`
|
|
293
|
+
- `Plan_File` 指向的计划文件中的 `Evidence_Path`(若存在)
|
|
294
|
+
- 若存在 `.agentdocs/tmp/review/codex-review.md` 且 `changes/<id>/review/codex-review.md` 不存在:复制为持久 review 证据。
|
|
295
|
+
|
|
296
|
+
接口(仅定义):
|
|
297
|
+
- `aiws change evidence [<change-id>] [--no-validate] [--allow-fail]`
|
|
298
|
+
|
|
299
|
+
选项:
|
|
300
|
+
- `--no-validate`:不执行严格门禁校验;仅生成/回填证据(适用于早期阶段先固化证据结构)。
|
|
301
|
+
- `--allow-fail`:即使严格门禁失败也继续生成证据并回填(默认会失败退出,避免“带病交付”)。
|
|
280
302
|
|
|
281
303
|
### `aiws change archive`
|
|
282
304
|
|
|
@@ -301,6 +323,7 @@
|
|
|
301
323
|
- `aiws change list`
|
|
302
324
|
- `aiws change status [<change-id>]`
|
|
303
325
|
- `aiws change next [<change-id>]`
|
|
326
|
+
- `aiws change state [<change-id>] [--write]`(状态快照;`--write` 写入 `changes/<id>/STATE.md`)
|
|
304
327
|
|
|
305
328
|
### `aiws change templates which/init`
|
|
306
329
|
|
|
@@ -310,3 +333,11 @@
|
|
|
310
333
|
接口(仅定义):
|
|
311
334
|
- `aiws change templates which`
|
|
312
335
|
- `aiws change templates init`
|
|
336
|
+
|
|
337
|
+
## `aiws metrics`
|
|
338
|
+
|
|
339
|
+
语义:
|
|
340
|
+
- 读取 `changes/*/metrics.json` 与 `changes/archive/*/metrics.json`,输出 best-effort 流程指标汇总(不修改任何文件)。
|
|
341
|
+
|
|
342
|
+
接口(仅定义):
|
|
343
|
+
- `aiws metrics summary [--since YYYY-MM-DD]`
|
package/package.json
CHANGED
|
@@ -55,7 +55,16 @@ while read -r _ sub_path; do
|
|
|
55
55
|
done < <(git config --file .gitmodules --get-regexp '^submodule\..*\.path$' 2>/dev/null || true)
|
|
56
56
|
```
|
|
57
57
|
判定规则(强制):
|
|
58
|
-
- 任一 submodule `git status --porcelain` 非空:停止 superproject commit,先在对应 submodule 完成 commit
|
|
58
|
+
- 任一 submodule `git status --porcelain` 非空:停止 superproject commit,先在对应 submodule 完成 commit,再回到 superproject 更新并提交 gitlink。
|
|
59
|
+
- 若该 submodule 当前为 detached HEAD:先按 `.gitmodules` 的目标分支挂到 `aiws/pin/<target_branch>`;不要直接切 `change/<change-id>` / `main` / `master` 来“解 detached”。
|
|
60
|
+
处理指引(detached submodule):
|
|
61
|
+
```bash
|
|
62
|
+
sub_name="<submodule-name>"
|
|
63
|
+
base_branch="$(git branch --show-current)"
|
|
64
|
+
cfg_branch="$(git config --file .gitmodules --get "submodule.${sub_name}.branch" 2>/dev/null || true)"
|
|
65
|
+
if [[ "${cfg_branch:-}" == "." ]]; then cfg_branch="$base_branch"; fi
|
|
66
|
+
git -C "${sub_path}" checkout -B "aiws/pin/${cfg_branch}" HEAD
|
|
67
|
+
```
|
|
59
68
|
7) 检查当前 staging 内容(必须输出给用户确认):
|
|
60
69
|
```bash
|
|
61
70
|
git status --porcelain
|
|
@@ -64,11 +64,22 @@ git -C "$sub_path" status --porcelain
|
|
|
64
64
|
```
|
|
65
65
|
2) 若 submodule 处于 detached HEAD(`branch --show-current` 为空):
|
|
66
66
|
- 说明:这通常是因为 superproject 的 gitlink checkout(例如 `git submodule update`)导致 detached。
|
|
67
|
-
-
|
|
67
|
+
- 不要直接切 `change/<change-id>` / `main` / `master` 来“解 detached”。
|
|
68
|
+
- 若你要在该 submodule 里提交:先按 `.gitmodules` 的目标分支挂到 pin 分支 `aiws/pin/<target-branch>`,再在其上提交:
|
|
68
69
|
```bash
|
|
69
|
-
|
|
70
|
+
sub_name="<submodule-name>"
|
|
71
|
+
base_branch="$(git branch --show-current)"
|
|
72
|
+
cfg_branch="$(git config --file .gitmodules --get "submodule.${sub_name}.branch" 2>/dev/null || true)"
|
|
73
|
+
if [[ "${cfg_branch:-}" == "." ]]; then cfg_branch="$base_branch"; fi
|
|
74
|
+
if [[ -z "${cfg_branch:-}" ]]; then
|
|
75
|
+
echo "error: missing .gitmodules submodule.${sub_name}.branch (path=${sub_path})"
|
|
76
|
+
exit 2
|
|
77
|
+
fi
|
|
78
|
+
git -C "$sub_path" fetch origin --prune
|
|
79
|
+
git -C "$sub_path" checkout -B "aiws/pin/${cfg_branch}" HEAD
|
|
80
|
+
git -C "$sub_path" branch --set-upstream-to "origin/${cfg_branch}" "aiws/pin/${cfg_branch}" >/dev/null 2>&1 || true
|
|
70
81
|
```
|
|
71
|
-
-
|
|
82
|
+
- 若 `origin/<target-branch>` 不存在,或用户明确不想使用 pin 分支:停止,解释风险(提交可能不可追溯/难以推送)。
|
|
72
83
|
3) 选择性 staging(默认用 `-p` 更安全):
|
|
73
84
|
```bash
|
|
74
85
|
git -C "$sub_path" add -p
|
|
@@ -115,8 +126,26 @@ else
|
|
|
115
126
|
fi
|
|
116
127
|
```
|
|
117
128
|
|
|
129
|
+
## D2) 生成持久证据并回填 Evidence_Path(强烈建议)
|
|
130
|
+
> 说明:`.agentdocs/tmp/...` 默认 gitignored;交付前建议把关键结果落到 `changes/<change-id>/evidence/...` 并回填 `proposal.md`/`plan` 的 `Evidence_Path`,避免后续评审/二次会话读不到证据。
|
|
131
|
+
```bash
|
|
132
|
+
change_id="<change-id>"
|
|
133
|
+
if [[ -x "./node_modules/.bin/aiws" ]]; then
|
|
134
|
+
./node_modules/.bin/aiws change evidence "${change_id}"
|
|
135
|
+
elif command -v aiws >/dev/null 2>&1; then
|
|
136
|
+
aiws change evidence "${change_id}"
|
|
137
|
+
else
|
|
138
|
+
npx @aipper/aiws change evidence "${change_id}"
|
|
139
|
+
fi
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## D3) 生成状态快照(可选,建议)
|
|
143
|
+
```bash
|
|
144
|
+
aiws change state "${change_id}" --write
|
|
145
|
+
```
|
|
146
|
+
|
|
118
147
|
## E) 安全合并回目标分支(fast-forward)
|
|
119
|
-
优先使用 `$ws-finish`(底层调用 `aiws change finish
|
|
148
|
+
优先使用 `$ws-finish`(底层调用 `aiws change finish`,并在 push 成功后清理对应 change worktree)。
|
|
120
149
|
|
|
121
150
|
若需要显式指定目标分支:
|
|
122
151
|
```bash
|
|
@@ -132,4 +161,5 @@ aiws change archive <change-id>
|
|
|
132
161
|
- `Submodules:` 每个 submodule 的分支/提交摘要(repo → commit sha → message)
|
|
133
162
|
- `Superproject:` 提交摘要
|
|
134
163
|
- `Merge:` `aiws change finish` 的输出(into/from)
|
|
164
|
+
- `Worktree cleanup:` 若存在独立 change worktree,输出清理结果(removed/skipped + reason)
|
|
135
165
|
- `Evidence:` `.agentdocs/tmp/aiws-validate/*.json`(若使用 --stamp)
|
|
@@ -14,6 +14,7 @@ description: 开发(按需求实现并验证;适用于任何需要修改代
|
|
|
14
14
|
2) 建立变更归因(推荐):
|
|
15
15
|
- 推荐一键:`aiws change start <change-id> --hooks`(切分支 + 初始化变更工件 + 启用 hooks)
|
|
16
16
|
- superproject + submodule(推荐):`aiws change start <change-id> --hooks --worktree --submodules`(创建独立 worktree;当前目录分支保持不变)
|
|
17
|
+
- 若后续需要在 detached submodule 内提交:先挂到 `aiws/pin/<target-branch>`;不要直接切 `change/<change-id>` / `main` / `master`
|
|
17
18
|
- 若你明确要在 superproject 直接切分支:`aiws change start <change-id> --hooks --switch`(仅在存在 `.gitmodules` 时有意义;会尝试让 submodules 工作区跟随 superproject 指针)
|
|
18
19
|
- 或手工:`git switch -c change/<change-id>`,并创建 `changes/<change-id>/proposal.md` 与 `changes/<change-id>/tasks.md`(参考 `changes/README.md`)
|
|
19
20
|
3) 如涉及需求调整:先做需求评审(可用 `$ws-req-review`)→ 用户确认后再做需求落盘(可用 `$ws-req-change`)(避免需求漂移)。
|
|
@@ -46,6 +46,21 @@ else
|
|
|
46
46
|
npx @aipper/aiws validate . --stamp
|
|
47
47
|
fi
|
|
48
48
|
```
|
|
49
|
+
1.1) (强烈建议)收敛持久证据并回填 `Evidence_Path`:
|
|
50
|
+
```bash
|
|
51
|
+
change_id="<change-id>"
|
|
52
|
+
if [[ -x "./node_modules/.bin/aiws" ]]; then
|
|
53
|
+
./node_modules/.bin/aiws change evidence "${change_id}"
|
|
54
|
+
elif command -v aiws >/dev/null 2>&1; then
|
|
55
|
+
aiws change evidence "${change_id}"
|
|
56
|
+
else
|
|
57
|
+
npx @aipper/aiws change evidence "${change_id}"
|
|
58
|
+
fi
|
|
59
|
+
```
|
|
60
|
+
1.2) (可选)生成状态快照(建议):
|
|
61
|
+
```bash
|
|
62
|
+
aiws change state "${change_id}" --write
|
|
63
|
+
```
|
|
49
64
|
2) 安全合并(默认 fast-forward;并会在需要时切到目标分支):
|
|
50
65
|
```bash
|
|
51
66
|
# 若当前就在 change/<change-id> 分支上,可省略 <change-id>
|
|
@@ -71,6 +86,7 @@ git config --file .gitmodules --get-regexp '^submodule\..*\.path$' 2>/dev/null |
|
|
|
71
86
|
# 说明:`git submodule update` 会把 submodule checkout 到固定 gitlink commit,导致 detached HEAD。
|
|
72
87
|
# 为减少“游离状态”的协作摩擦,本步骤采用“pin 分支”策略:
|
|
73
88
|
# - 仅在 `.gitmodules` 明确配置了 `submodule.<name>.branch` 时执行(避免 origin 多分支导致误判)
|
|
89
|
+
# - 不要直接切 `change/<change-id>` / `main` / `master` 等业务分支来“解 detached”
|
|
74
90
|
# - 不改动 submodule 现有分支指针(例如不强行移动 main/master)
|
|
75
91
|
# - 创建/更新本地 pin 分支:`aiws/pin/<target_branch>` 指向 gitlink commit,并将其 upstream 设为 `origin/<target_branch>`
|
|
76
92
|
sub_sha="$(git rev-parse "HEAD:<sub_path>")"
|
|
@@ -112,7 +128,31 @@ git branch --show-current
|
|
|
112
128
|
git status -sb
|
|
113
129
|
git push
|
|
114
130
|
```
|
|
115
|
-
6)
|
|
131
|
+
6) push 成功后,清理 `change/<change-id>` 对应 worktree(若存在且不是当前 worktree):
|
|
132
|
+
```bash
|
|
133
|
+
change_id="<change-id>"
|
|
134
|
+
change_ref="refs/heads/change/${change_id}"
|
|
135
|
+
main_wt="$(git rev-parse --show-toplevel)"
|
|
136
|
+
change_wt="$(git worktree list --porcelain | awk -v ref="$change_ref" '
|
|
137
|
+
$1=="worktree" { wt=substr($0,10) }
|
|
138
|
+
$1=="branch" && $2==ref { print wt; exit }
|
|
139
|
+
')"
|
|
140
|
+
|
|
141
|
+
if [[ -n "${change_wt:-}" && "$change_wt" != "$main_wt" ]]; then
|
|
142
|
+
if [[ -n "$(git -C "$change_wt" status --porcelain 2>/dev/null)" ]]; then
|
|
143
|
+
echo "[warn] worktree not clean, skip remove: $change_wt"
|
|
144
|
+
echo "hint: clean it first, then run: git worktree remove \"$change_wt\""
|
|
145
|
+
else
|
|
146
|
+
git worktree remove "$change_wt"
|
|
147
|
+
git worktree prune
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
150
|
+
```
|
|
151
|
+
规则:
|
|
152
|
+
- 清理前先把 `change_wt` 输出给用户确认,避免误删。
|
|
153
|
+
- 仅使用 `git worktree remove`(不带 `--force`)。
|
|
154
|
+
|
|
155
|
+
7) (可选)归档变更工件(完成交付后推荐):
|
|
116
156
|
```bash
|
|
117
157
|
change_id="<change-id>"
|
|
118
158
|
if [[ -x "./node_modules/.bin/aiws" ]]; then
|
|
@@ -43,7 +43,8 @@ while read -r _ sub_path; do
|
|
|
43
43
|
done < <(git config --file .gitmodules --get-regexp '^submodule\\..*\\.path$' 2>/dev/null || true)
|
|
44
44
|
```
|
|
45
45
|
判定规则(强制):
|
|
46
|
-
- 任一 submodule `git status --porcelain` 非空:停止提交 superproject;先在对应 submodule 完成 commit
|
|
46
|
+
- 任一 submodule `git status --porcelain` 非空:停止提交 superproject;先在对应 submodule 完成 commit,再回到 superproject 更新并提交 gitlink。
|
|
47
|
+
- 若该 submodule 当前为 detached HEAD:先按 `.gitmodules` 的目标分支挂到 `aiws/pin/<target-branch>`;不要直接切 `change/<change-id>` / `main` / `master`。
|
|
47
48
|
6) 检查 staging(必须输出给用户确认):
|
|
48
49
|
```bash
|
|
49
50
|
git status --porcelain
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
- `git submodule status --recursive`
|
|
21
21
|
3) 逐个提交 submodules(按上一步顺序):
|
|
22
22
|
- `git -C "<sub_path>" status --porcelain`
|
|
23
|
+
- 若当前为 detached HEAD:不要直接切 `change/<change-id>` / `main` / `master`;先按 `.gitmodules` 的目标分支挂到 `aiws/pin/<target-branch>`
|
|
23
24
|
- `git -C "<sub_path>" add -p`
|
|
24
25
|
- `git -C "<sub_path>" diff --staged --stat`
|
|
25
26
|
- 生成并让用户确认该 submodule 的 commit message(每个 repo 单独确认)
|
|
@@ -32,13 +33,18 @@
|
|
|
32
33
|
- `git commit -m "<message>"`
|
|
33
34
|
5) (推荐)门禁 + 证据:
|
|
34
35
|
- `aiws validate . --stamp`(未安装全局 aiws 时可用 `npx @aipper/aiws validate . --stamp`)
|
|
36
|
+
5.1) (强烈建议)生成持久证据并回填 `Evidence_Path`:
|
|
37
|
+
- `aiws change evidence <change-id>`(未安装全局 aiws 时可用 `npx @aipper/aiws change evidence <change-id>`)
|
|
38
|
+
5.2) (可选)生成状态快照(建议):
|
|
39
|
+
- `aiws change state <change-id> --write`
|
|
35
40
|
6) 安全合并回目标分支:
|
|
36
|
-
- 优先运行 `/ws-finish`(底层调用 `aiws change finish`,默认 `--ff-only
|
|
41
|
+
- 优先运行 `/ws-finish`(底层调用 `aiws change finish`,默认 `--ff-only`;push 成功后会清理对应 change worktree)
|
|
37
42
|
|
|
38
43
|
输出要求:
|
|
39
44
|
- `子模块(Submodules):` 每个 submodule 的 commit 摘要(repo/path → sha → message)
|
|
40
45
|
- `主仓库(Superproject):` commit 摘要
|
|
41
46
|
- `合并信息(Merge):` `/ws-finish` 输出(into/from)
|
|
47
|
+
- `工作区清理(Worktree cleanup):` 若存在独立 change worktree,输出清理结果(removed/skipped + reason)
|
|
42
48
|
- `证据(Evidence):` `.agentdocs/tmp/aiws-validate/*.json`(若使用 --stamp)
|
|
43
49
|
<!-- AIWS_MANAGED_END:claude:ws-deliver -->
|
|
44
50
|
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
建议流程:
|
|
9
9
|
1) 先运行 `/ws-preflight`(读真值文件并输出约束摘要)。
|
|
10
10
|
2) 建立变更归因(推荐):
|
|
11
|
-
-
|
|
12
|
-
-
|
|
11
|
+
- 推荐一键:`aiws change start <change-id> --hooks`
|
|
12
|
+
- superproject + submodule(推荐):`aiws change start <change-id> --hooks --worktree --submodules`
|
|
13
|
+
- 若后续需要在 detached submodule 内提交:先挂到 `aiws/pin/<target-branch>`;不要直接切 `change/<change-id>` / `main` / `master`
|
|
14
|
+
- 或手工:`git switch -c change/<change-id>`,并创建 `changes/<change-id>/proposal.md` 与 `changes/<change-id>/tasks.md`(参考 `changes/README.md`)
|
|
13
15
|
3) 如涉及需求调整:先 `/ws-req-review` → 用户确认后再 `/ws-req-change`(避免需求漂移)。
|
|
14
16
|
4) 实施最小改动:任何改动都要能归因到 `REQUIREMENTS.md`(验收)或 `issues/problem-issues.csv`(问题)。
|
|
15
17
|
5) 运行 `AI_WORKSPACE.md` 里声明的验证命令;未运行不声称已运行。
|
|
@@ -32,6 +32,8 @@ fi
|
|
|
32
32
|
```
|
|
33
33
|
1) 先运行 `/ws-preflight`(确保真值文件齐全)。
|
|
34
34
|
2) (推荐)门禁校验并落盘证据:`aiws validate . --stamp`(未安装全局 aiws 时可用 `npx @aipper/aiws validate . --stamp`)。
|
|
35
|
+
2.1) (强烈建议)收敛持久证据并回填 `Evidence_Path`:`aiws change evidence <change-id>`(未安装全局 aiws 时可用 `npx @aipper/aiws change evidence <change-id>`)。
|
|
36
|
+
2.2) (可选)生成状态快照(建议):`aiws change state <change-id> --write`。
|
|
35
37
|
3) 安全合并并切回目标分支:
|
|
36
38
|
- 若当前就在 `change/<change-id>` 分支上,可直接执行:`aiws change finish`
|
|
37
39
|
- 否则执行:`aiws change finish <change-id>`
|
|
@@ -41,6 +43,7 @@ fi
|
|
|
41
43
|
- 对每个 `<sub_path>`:
|
|
42
44
|
- 读取 superproject 当前 gitlink:`git rev-parse "HEAD:<sub_path>"`
|
|
43
45
|
- 目标分支:必须在 `.gitmodules` 配置 `submodule.<name>.branch`(若为 `.` 则用当前主仓库分支;避免 origin 多分支时误判)
|
|
46
|
+
- 不要直接切 `change/<change-id>` / `main` / `master` 来“解 detached”
|
|
44
47
|
- 用 pin 分支挂回(不改动现有 main/master 指针):`git -C "<sub_path>" checkout -B "aiws/pin/<target-branch>" <gitlink-sha>`
|
|
45
48
|
- 仅当 `<gitlink-sha>` 属于 `origin/<target-branch>` 历史时才允许 push;否则停止并人工处理分叉
|
|
46
49
|
- push(只允许 fast-forward):`git -C "<sub_path>" push origin "<gitlink-sha>:refs/heads/<target-branch>"`
|
|
@@ -49,7 +52,13 @@ fi
|
|
|
49
52
|
- `git branch --show-current`
|
|
50
53
|
- `git status -sb`
|
|
51
54
|
- `git push`
|
|
52
|
-
8)
|
|
55
|
+
8) push 成功后,清理 `change/<change-id>` 对应 worktree(若存在且不是当前 worktree):
|
|
56
|
+
- `change_ref="refs/heads/change/<change-id>"`
|
|
57
|
+
- `main_wt="$(git rev-parse --show-toplevel)"`
|
|
58
|
+
- `change_wt="$(git worktree list --porcelain | awk -v ref="$change_ref" '$1=="worktree"{wt=substr($0,10)} $1=="branch"&&$2==ref{print wt; exit}')"`
|
|
59
|
+
- 若 `change_wt` 非空且不等于 `main_wt`:先输出并让用户确认,再执行 `git worktree remove "$change_wt"`(不带 `--force`),最后 `git worktree prune`
|
|
60
|
+
- 若 `git -C "$change_wt" status --porcelain` 非空:停止并提示先清理该 worktree
|
|
61
|
+
9) (可选)交付完成后归档变更工件:`aiws change archive <change-id>`。
|
|
53
62
|
|
|
54
63
|
安全:
|
|
55
64
|
- push 前先输出状态并请用户确认远端/分支。
|
|
@@ -13,12 +13,15 @@ argument-hint: ""
|
|
|
13
13
|
建议流程:
|
|
14
14
|
1) 先运行 `/ws-preflight`(读真值文件并输出约束摘要)。
|
|
15
15
|
2) 建立变更归因(推荐):
|
|
16
|
-
-
|
|
17
|
-
-
|
|
16
|
+
- 推荐一键:`aiws change start <change-id> --hooks`
|
|
17
|
+
- superproject + submodule(推荐):`aiws change start <change-id> --hooks --worktree --submodules`
|
|
18
|
+
- 若后续需要在 detached submodule 内提交:先挂到 `aiws/pin/<target-branch>`;不要直接切 `change/<change-id>` / `main` / `master`
|
|
19
|
+
- 或手工:`git switch -c change/<change-id>`,并创建 `changes/<change-id>/proposal.md` 与 `changes/<change-id>/tasks.md`(参考 `changes/README.md`)
|
|
18
20
|
3) 如涉及需求调整:先 `/ws-req-review` → 用户确认后再 `/ws-req-change`(避免需求漂移)。
|
|
19
21
|
4) 实施最小改动:任何改动都要能归因到 `REQUIREMENTS.md`(验收)或 `issues/problem-issues.csv`(问题)。
|
|
20
22
|
5) 运行 `AI_WORKSPACE.md` 里声明的验证命令;未运行不声称已运行。
|
|
21
23
|
6) 提交前强制:`aiws validate .`(commit/push hooks 也会阻断)。
|
|
24
|
+
7) 交付收尾(推荐,减少手动 merge 出错):运行 `/ws-finish`(底层调用 `aiws change finish`,默认 fast-forward 安全合并回目标分支)。
|
|
22
25
|
|
|
23
26
|
输出要求:
|
|
24
27
|
- `变更文件(Changed):` 文件清单
|
|
@@ -30,7 +30,8 @@ prompt = """
|
|
|
30
30
|
- 对每个 `<sub_path>` 输出:
|
|
31
31
|
- `git -C "<sub_path>" rev-parse --abbrev-ref HEAD`
|
|
32
32
|
- `git -C "<sub_path>" status --porcelain`
|
|
33
|
-
- 判定规则:任一 submodule `git status --porcelain` 非空 → **停止提交 superproject**,先在该 submodule 完成 commit
|
|
33
|
+
- 判定规则:任一 submodule `git status --porcelain` 非空 → **停止提交 superproject**,先在该 submodule 完成 commit,再回到 superproject 更新并提交 gitlink。
|
|
34
|
+
- 若该 submodule 当前为 detached HEAD:先按 `.gitmodules` 的目标分支挂到 `aiws/pin/<target-branch>`;不要直接切 `change/<change-id>` / `main` / `master`。
|
|
34
35
|
7) 检查 staging(必须输出给用户确认):
|
|
35
36
|
- `git status --porcelain`
|
|
36
37
|
- `git diff --staged --submodule=short`
|
|
@@ -25,6 +25,7 @@ prompt = """
|
|
|
25
25
|
- `git submodule status --recursive`
|
|
26
26
|
3) 逐个提交 submodules(按上一步顺序):
|
|
27
27
|
- `git -C "<sub_path>" status --porcelain`
|
|
28
|
+
- 若当前为 detached HEAD:不要直接切 `change/<change-id>` / `main` / `master`;先按 `.gitmodules` 的目标分支挂到 `aiws/pin/<target-branch>`
|
|
28
29
|
- `git -C "<sub_path>" add -p`
|
|
29
30
|
- `git -C "<sub_path>" diff --staged --stat`
|
|
30
31
|
- 生成并让用户确认该 submodule 的 commit message(每个 repo 单独确认)
|
|
@@ -37,12 +38,17 @@ prompt = """
|
|
|
37
38
|
- `git commit -m "<message>"`
|
|
38
39
|
5) (推荐)门禁 + 证据:
|
|
39
40
|
- `aiws validate . --stamp`(未安装全局 aiws 时可用 `npx @aipper/aiws validate . --stamp`)
|
|
41
|
+
5.1) (强烈建议)生成持久证据并回填 `Evidence_Path`:
|
|
42
|
+
- `aiws change evidence <change-id>`(未安装全局 aiws 时可用 `npx @aipper/aiws change evidence <change-id>`)
|
|
43
|
+
5.2) (可选)生成状态快照(建议):
|
|
44
|
+
- `aiws change state <change-id> --write`
|
|
40
45
|
6) 安全合并回目标分支:
|
|
41
|
-
- 优先运行 `ws:finish`(底层调用 `aiws change finish`,默认 `--ff-only
|
|
46
|
+
- 优先运行 `ws:finish`(底层调用 `aiws change finish`,默认 `--ff-only`;push 成功后会清理对应 change worktree)
|
|
42
47
|
|
|
43
48
|
输出要求:
|
|
44
49
|
- `Submodules:` 每个 submodule 的 commit 摘要(repo/path → sha → message)
|
|
45
50
|
- `Superproject:` commit 摘要
|
|
46
51
|
- `Merge:` `ws:finish`/`aiws change finish` 输出(into/from)
|
|
52
|
+
- `Worktree cleanup:` 若存在独立 change worktree,输出清理结果(removed/skipped + reason)
|
|
47
53
|
- `Evidence:` `.agentdocs/tmp/aiws-validate/*.json`(若使用 --stamp)
|
|
48
54
|
"""
|
|
@@ -20,6 +20,8 @@ prompt = """
|
|
|
20
20
|
4) 若工作区不干净:停止,并要求先 commit 或 stash(不要尝试自动处理)。
|
|
21
21
|
4.1) 若存在 `.gitmodules`:必须为每个 submodule 配置 `submodule.<name>.branch`(否则先运行 `ws:submodule-setup` 并提交 `.gitmodules`)。
|
|
22
22
|
5) (推荐)门禁校验并落盘证据:`aiws validate . --stamp`(未安装全局 aiws 时可用 `npx @aipper/aiws validate . --stamp`)。
|
|
23
|
+
5.1) (强烈建议)收敛持久证据并回填 `Evidence_Path`:`aiws change evidence <change-id>`(未安装全局 aiws 时可用 `npx @aipper/aiws change evidence <change-id>`)。
|
|
24
|
+
5.2) (可选)生成状态快照(建议):`aiws change state <change-id> --write`。
|
|
23
25
|
6) 安全合并(默认 fast-forward):
|
|
24
26
|
- 在 `change/<change-id>` 分支上:`aiws change finish`(会尝试读 `changes/<change-id>/.ws-change.json` 的 `base_branch` 并切回目标分支)
|
|
25
27
|
- 否则:`aiws change finish <change-id>`
|
|
@@ -29,6 +31,7 @@ prompt = """
|
|
|
29
31
|
- 对每个 `<sub_path>`:
|
|
30
32
|
- 读取 superproject 当前 gitlink:`git rev-parse "HEAD:<sub_path>"`
|
|
31
33
|
- 目标分支:必须在 `.gitmodules` 配置 `submodule.<name>.branch`(若为 `.` 则用当前主仓库分支;避免 origin 多分支时误判)
|
|
34
|
+
- 不要直接切 `change/<change-id>` / `main` / `master` 来“解 detached”
|
|
32
35
|
- 用 pin 分支挂回(不改动现有 main/master 指针):`git -C "<sub_path>" checkout -B "aiws/pin/<target-branch>" <gitlink-sha>`
|
|
33
36
|
- 仅当 `<gitlink-sha>` 属于 `origin/<target-branch>` 历史时才允许 push;否则停止并人工处理分叉
|
|
34
37
|
- push(只允许 fast-forward):`git -C "<sub_path>" push origin "<gitlink-sha>:refs/heads/<target-branch>"`
|
|
@@ -37,7 +40,13 @@ prompt = """
|
|
|
37
40
|
- `git branch --show-current`
|
|
38
41
|
- `git status -sb`
|
|
39
42
|
- `git push`
|
|
40
|
-
11)
|
|
43
|
+
11) push 成功后,清理 `change/<change-id>` 对应 worktree(若存在且不是当前 worktree):
|
|
44
|
+
- `change_ref="refs/heads/change/<change-id>"`
|
|
45
|
+
- `main_wt="$(git rev-parse --show-toplevel)"`
|
|
46
|
+
- `change_wt="$(git worktree list --porcelain | awk -v ref="$change_ref" '$1=="worktree"{wt=substr($0,10)} $1=="branch"&&$2==ref{print wt; exit}')"`
|
|
47
|
+
- 若 `change_wt` 非空且不等于 `main_wt`:先输出并让用户确认,再执行 `git worktree remove "$change_wt"`(不带 `--force`),最后 `git worktree prune`
|
|
48
|
+
- 若 `git -C "$change_wt" status --porcelain` 非空:停止并提示先清理该 worktree
|
|
49
|
+
12) (可选)交付完成后归档变更工件:`aiws change archive <change-id>`。
|
|
41
50
|
|
|
42
51
|
边界:
|
|
43
52
|
- push 前先输出状态并请用户确认远端/分支。
|
|
@@ -46,7 +46,8 @@ while read -r _ sub_path; do
|
|
|
46
46
|
done < <(git config --file .gitmodules --get-regexp '^submodule\\..*\\.path$' 2>/dev/null || true)
|
|
47
47
|
```
|
|
48
48
|
判定规则(强制):
|
|
49
|
-
- 任一 submodule `git status --porcelain` 非空:停止提交 superproject;先在对应 submodule 完成 commit
|
|
49
|
+
- 任一 submodule `git status --porcelain` 非空:停止提交 superproject;先在对应 submodule 完成 commit,再回到 superproject 更新并提交 gitlink。
|
|
50
|
+
- 若该 submodule 当前为 detached HEAD:先按 `.gitmodules` 的目标分支挂到 `aiws/pin/<target-branch>`;不要直接切 `change/<change-id>` / `main` / `master`。
|
|
50
51
|
6) 检查 staging(必须输出给用户确认):
|
|
51
52
|
```bash
|
|
52
53
|
git status --porcelain
|
|
@@ -23,6 +23,7 @@ description: 交付:submodules+superproject 分步提交并安全合并回 bas
|
|
|
23
23
|
- `git submodule status --recursive`
|
|
24
24
|
3) 逐个提交 submodules(按上一步顺序):
|
|
25
25
|
- `git -C "<sub_path>" status --porcelain`
|
|
26
|
+
- 若当前为 detached HEAD:不要直接切 `change/<change-id>` / `main` / `master`;先按 `.gitmodules` 的目标分支挂到 `aiws/pin/<target-branch>`
|
|
26
27
|
- `git -C "<sub_path>" add -p`
|
|
27
28
|
- `git -C "<sub_path>" diff --staged --stat`
|
|
28
29
|
- 生成并让用户确认该 submodule 的 commit message(每个 repo 单独确认)
|
|
@@ -35,13 +36,18 @@ description: 交付:submodules+superproject 分步提交并安全合并回 bas
|
|
|
35
36
|
- `git commit -m "<message>"`
|
|
36
37
|
5) (推荐)门禁 + 证据:
|
|
37
38
|
- `aiws validate . --stamp`(未安装全局 aiws 时可用 `npx @aipper/aiws validate . --stamp`)
|
|
39
|
+
5.1) (强烈建议)生成持久证据并回填 `Evidence_Path`:
|
|
40
|
+
- `aiws change evidence <change-id>`(未安装全局 aiws 时可用 `npx @aipper/aiws change evidence <change-id>`)
|
|
41
|
+
5.2) (可选)生成状态快照(建议):
|
|
42
|
+
- `aiws change state <change-id> --write`
|
|
38
43
|
6) 安全合并回目标分支:
|
|
39
|
-
- 优先运行 `/ws-finish`(底层调用 `aiws change finish`,默认 `--ff-only
|
|
44
|
+
- 优先运行 `/ws-finish`(底层调用 `aiws change finish`,默认 `--ff-only`;push 成功后会清理对应 change worktree)
|
|
40
45
|
|
|
41
46
|
输出要求:
|
|
42
47
|
- `Submodules:` 每个 submodule 的 commit 摘要(repo/path → sha → message)
|
|
43
48
|
- `Superproject:` commit 摘要
|
|
44
49
|
- `Merge:` `/ws-finish` 输出(into/from)
|
|
50
|
+
- `Worktree cleanup:` 若存在独立 change worktree,输出清理结果(removed/skipped + reason)
|
|
45
51
|
- `Evidence:` `.agentdocs/tmp/aiws-validate/*.json`(若使用 --stamp)
|
|
46
52
|
<!-- AIWS_MANAGED_END:opencode:ws-deliver -->
|
|
47
53
|
|
|
@@ -11,8 +11,10 @@ description: 开发:在 AIWS 约束下完成小步交付
|
|
|
11
11
|
建议流程:
|
|
12
12
|
1) 先运行 `/ws-preflight`(读真值文件并输出约束摘要)。
|
|
13
13
|
2) 建立变更归因(推荐):
|
|
14
|
-
-
|
|
15
|
-
-
|
|
14
|
+
- 推荐一键:`aiws change start <change-id> --hooks`
|
|
15
|
+
- superproject + submodule(推荐):`aiws change start <change-id> --hooks --worktree --submodules`
|
|
16
|
+
- 若后续需要在 detached submodule 内提交:先挂到 `aiws/pin/<target-branch>`;不要直接切 `change/<change-id>` / `main` / `master`
|
|
17
|
+
- 或手工:`git switch -c change/<change-id>`,并创建 `changes/<change-id>/proposal.md` 与 `changes/<change-id>/tasks.md`(参考 `changes/README.md`)
|
|
16
18
|
3) 如涉及需求调整:先 `/ws-req-review` → 用户确认后再 `/ws-req-change`(避免需求漂移)。
|
|
17
19
|
4) 实施最小改动:任何改动都要能归因到 `REQUIREMENTS.md`(验收)或 `issues/problem-issues.csv`(问题)。
|
|
18
20
|
5) 运行 `AI_WORKSPACE.md` 里声明的验证命令;未运行不声称已运行。
|
|
@@ -35,6 +35,8 @@ fi
|
|
|
35
35
|
```
|
|
36
36
|
1) 先运行 `/ws-preflight`(确保真值文件齐全)。
|
|
37
37
|
2) (推荐)门禁校验并落盘证据:`aiws validate . --stamp`(未安装全局 aiws 时可用 `npx @aipper/aiws validate . --stamp`)。
|
|
38
|
+
2.1) (强烈建议)收敛持久证据并回填 `Evidence_Path`:`aiws change evidence <change-id>`(未安装全局 aiws 时可用 `npx @aipper/aiws change evidence <change-id>`)。
|
|
39
|
+
2.2) (可选)生成状态快照(建议):`aiws change state <change-id> --write`。
|
|
38
40
|
3) 安全合并并切回目标分支:
|
|
39
41
|
- 若当前就在 `change/<change-id>` 分支上,可直接执行:`aiws change finish`
|
|
40
42
|
- 否则执行:`aiws change finish <change-id>`
|
|
@@ -44,6 +46,7 @@ fi
|
|
|
44
46
|
- 对每个 `<sub_path>`:
|
|
45
47
|
- 读取 superproject 当前 gitlink:`git rev-parse "HEAD:<sub_path>"`
|
|
46
48
|
- 目标分支:必须在 `.gitmodules` 配置 `submodule.<name>.branch`(若为 `.` 则用当前主仓库分支;避免 origin 多分支时误判)
|
|
49
|
+
- 不要直接切 `change/<change-id>` / `main` / `master` 来“解 detached”
|
|
47
50
|
- 用 pin 分支挂回(不改动现有 main/master 指针):`git -C "<sub_path>" checkout -B "aiws/pin/<target-branch>" <gitlink-sha>`
|
|
48
51
|
- 仅当 `<gitlink-sha>` 属于 `origin/<target-branch>` 历史时才允许 push;否则停止并人工处理分叉
|
|
49
52
|
- push(只允许 fast-forward):`git -C "<sub_path>" push origin "<gitlink-sha>:refs/heads/<target-branch>"`
|
|
@@ -52,7 +55,13 @@ fi
|
|
|
52
55
|
- `git branch --show-current`
|
|
53
56
|
- `git status -sb`
|
|
54
57
|
- `git push`
|
|
55
|
-
8)
|
|
58
|
+
8) push 成功后,清理 `change/<change-id>` 对应 worktree(若存在且不是当前 worktree):
|
|
59
|
+
- `change_ref="refs/heads/change/<change-id>"`
|
|
60
|
+
- `main_wt="$(git rev-parse --show-toplevel)"`
|
|
61
|
+
- `change_wt="$(git worktree list --porcelain | awk -v ref="$change_ref" '$1=="worktree"{wt=substr($0,10)} $1=="branch"&&$2==ref{print wt; exit}')"`
|
|
62
|
+
- 若 `change_wt` 非空且不等于 `main_wt`:先输出并让用户确认,再执行 `git worktree remove "$change_wt"`(不带 `--force`),最后 `git worktree prune`
|
|
63
|
+
- 若 `git -C "$change_wt" status --porcelain` 非空:停止并提示先清理该 worktree
|
|
64
|
+
9) (可选)交付完成后归档变更工件:`aiws change archive <change-id>`。
|
|
56
65
|
|
|
57
66
|
安全:
|
|
58
67
|
- push 前先输出状态并请用户确认远端/分支。
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
推荐(防规则/范围漂移):
|
|
39
39
|
- 创建工件:补齐 `changes/<change-id>/proposal.md`、`tasks.md`(可选 `design.md`)
|
|
40
40
|
- 声明 active change(团队共享):切到分支 `change/<change-id>`(也支持 `changes/`、`ws/`、`ws-change/`)
|
|
41
|
+
- 若仓库存在 `.gitmodules`:优先使用 `aiws change start <change-id> --worktree`(或至少 `--no-switch`);不要在当前 superproject worktree 里直接手工切分支。
|
|
42
|
+
- 若 submodule 因 gitlink checkout 处于 detached HEAD:只允许挂到 `aiws/pin/<target-branch>`;不要直接切 `change/<change-id>` / `main` / `master` 等业务分支来“解 detached”。
|
|
41
43
|
- 严格校验:`aiws validate .`(包含:漂移检测 + `ws_change_check` + `requirements_contract`)
|
|
42
44
|
- 启用 hooks(本地生效):`git config core.hooksPath .githooks`(提交/推送时自动跑 `aiws validate .`)
|
|
43
45
|
- CI 建议追加:`aiws validate .`
|
|
@@ -19,7 +19,17 @@
|
|
|
19
19
|
- 问题修复:`Problem_ID` = <!-- WS:TODO (问题修复可填;例如 PROB-001) -->
|
|
20
20
|
- `Contract_Row` = <!-- WS:TODO 绑定执行合同中的行 ID,可多项(逗号分隔);例如 Req_ID=TOOLING-001B -->
|
|
21
21
|
- `Plan_File` = <!-- WS:TODO 例如 plan/2026-02-08_15-30-00-xxx.md -->
|
|
22
|
-
- `Evidence_Path` = <!-- WS:TODO
|
|
22
|
+
- `Evidence_Path` = <!-- WS:TODO 证据路径,可多项(逗号分隔);优先写持久证据 changes/<change-id>/evidence/...;也可附带 changes/<change-id>/review/... 或 .agentdocs/tmp/... -->
|
|
23
|
+
|
|
24
|
+
## 依赖关系(可选)
|
|
25
|
+
|
|
26
|
+
- `Depends_On` = <!-- WS:TODO 本 change 依赖的前置 change ID(逗号分隔);例如 feature-auth -->
|
|
27
|
+
- `Blocks` = <!-- WS:TODO 本 change 完成后才能开始的后续 change ID(逗号分隔);例如 feature-dashboard -->
|
|
28
|
+
|
|
29
|
+
> 规则:
|
|
30
|
+
> - `Depends_On` 的 change 应已完成(archived);未完成时 `aiws change start` 会输出警告。
|
|
31
|
+
> - `Blocks` 用于记录依赖关系,便于后续 change 读取交接文档。
|
|
32
|
+
> - 依赖字段为可选,不强制填写。
|
|
23
33
|
|
|
24
34
|
> 规则:
|
|
25
35
|
> - `Req_ID` 与 `Problem_ID` 至少填写一项。
|
|
@@ -37,10 +47,28 @@
|
|
|
37
47
|
|
|
38
48
|
## 影响范围(Scope)
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
### In Scope(本次改动范围)
|
|
51
|
+
|
|
52
|
+
> 列出允许修改的文件/目录(支持 glob 模式);用于 `aiws change validate --check-scope` 检查越界改动。
|
|
53
|
+
|
|
54
|
+
- <!-- WS:TODO 例如:`packages/cli/src/commands/change.ts` - 修改 change 命令逻辑 -->
|
|
55
|
+
- <!-- WS:TODO 例如:`packages/cli/tests/**/*.test.ts` - 相关测试文件 -->
|
|
56
|
+
|
|
57
|
+
### Out of Scope(明确不改动)
|
|
58
|
+
|
|
59
|
+
> 列出明确不在本次改动范围内的模块/目录;防止"顺手重构"。
|
|
60
|
+
|
|
61
|
+
- <!-- WS:TODO 例如:`packages/spec/` - 不修改规范定义 -->
|
|
62
|
+
- <!-- WS:TODO 例如:配置文件 - 不修改 .aiws/config.yaml -->
|
|
63
|
+
|
|
64
|
+
### 外部影响
|
|
65
|
+
|
|
42
66
|
- 可能影响的外部接口/使用方:
|
|
43
|
-
- <!-- WS:TODO -->
|
|
67
|
+
- <!-- WS:TODO 例如:CLI 命令行接口、API 端点、配置格式等 -->
|
|
68
|
+
|
|
69
|
+
> 规则:
|
|
70
|
+
> - 实际改动超出 In Scope 时,需在 delivery 时解释原因并更新本章节。
|
|
71
|
+
> - 可使用 `aiws change validate --check-scope` 检查越界文件。
|
|
44
72
|
|
|
45
73
|
## 风险与回滚
|
|
46
74
|
|
|
@@ -64,4 +92,6 @@
|
|
|
64
92
|
- `requirements/CHANGELOG.md`:<!-- WS:TODO 需要/不需要 -->
|
|
65
93
|
- `requirements/requirements-issues.csv`:<!-- WS:TODO 需要/不需要 -->
|
|
66
94
|
- `issues/problem-issues.csv`:<!-- WS:TODO 需要/不需要 -->
|
|
67
|
-
-
|
|
95
|
+
- 证据落盘(推荐双层):
|
|
96
|
+
- 持久(建议入库):`changes/<change-id>/evidence/...`(例如 validate 总结、review 总结、关键日志摘要)
|
|
97
|
+
- 临时(可忽略入库):`.agentdocs/tmp/...`(例如 `aiws validate . --stamp` 的 JSON)
|
|
@@ -325,6 +325,8 @@ def validate_change(
|
|
|
325
325
|
change_id: str,
|
|
326
326
|
strict: bool,
|
|
327
327
|
allow_truth_drift: bool,
|
|
328
|
+
check_evidence: bool,
|
|
329
|
+
check_scope: bool,
|
|
328
330
|
) -> int:
|
|
329
331
|
change_dir = root / "changes" / change_id
|
|
330
332
|
required_files = ["proposal.md", "tasks.md"]
|
|
@@ -389,6 +391,185 @@ def validate_change(
|
|
|
389
391
|
if "WS:TODO" in text:
|
|
390
392
|
(errors if strict else warnings).append(f"WS:TODO markers remain in {rel}")
|
|
391
393
|
|
|
394
|
+
def evidence_is_persistent(p: str) -> bool:
|
|
395
|
+
p2 = p.replace("\\", "/")
|
|
396
|
+
return p2.startswith(f"changes/{change_id}/evidence/") or p2.startswith(f"changes/{change_id}/review/")
|
|
397
|
+
|
|
398
|
+
def parse_scope_patterns_from_plan(plan_text: str) -> List[str]:
|
|
399
|
+
# Extract bullet list items under "## Scope" (until next heading).
|
|
400
|
+
lines = (plan_text or "").splitlines()
|
|
401
|
+
patterns: List[str] = []
|
|
402
|
+
in_scope = False
|
|
403
|
+
for raw in lines:
|
|
404
|
+
line = raw.rstrip("\n")
|
|
405
|
+
if line.startswith("## "):
|
|
406
|
+
in_scope = line.strip() in ("## Scope", "## 影响范围(Scope)", "## 影响范围 (Scope)")
|
|
407
|
+
continue
|
|
408
|
+
if not in_scope:
|
|
409
|
+
continue
|
|
410
|
+
if line.startswith("## "):
|
|
411
|
+
break
|
|
412
|
+
s = line.strip()
|
|
413
|
+
if not s:
|
|
414
|
+
continue
|
|
415
|
+
if s.startswith("- "):
|
|
416
|
+
v = s[2:].strip()
|
|
417
|
+
if v.startswith("`") and v.endswith("`") and len(v) >= 2:
|
|
418
|
+
v = v[1:-1].strip()
|
|
419
|
+
if v.upper() == "TBD":
|
|
420
|
+
continue
|
|
421
|
+
# Normalize common "path (note)" into "path"
|
|
422
|
+
v = v.split("(", 1)[0].split("(", 1)[0].strip()
|
|
423
|
+
if v:
|
|
424
|
+
patterns.append(v)
|
|
425
|
+
return patterns
|
|
426
|
+
|
|
427
|
+
def normalize_scope_pattern(pat: str) -> str:
|
|
428
|
+
p = (pat or "").strip().replace("\\", "/")
|
|
429
|
+
if p.startswith("./"):
|
|
430
|
+
p = p[2:]
|
|
431
|
+
# Treat trailing slash as directory prefix match.
|
|
432
|
+
if p.endswith("/") and not p.endswith("/**"):
|
|
433
|
+
p = p + "**"
|
|
434
|
+
return p
|
|
435
|
+
|
|
436
|
+
def match_scope_pattern(relpath: str, pat: str) -> bool:
|
|
437
|
+
import fnmatch
|
|
438
|
+
|
|
439
|
+
rp = (relpath or "").replace("\\", "/")
|
|
440
|
+
p = normalize_scope_pattern(pat)
|
|
441
|
+
if not p:
|
|
442
|
+
return False
|
|
443
|
+
# If the pattern contains glob syntax, fnmatch it.
|
|
444
|
+
if any(ch in p for ch in ["*", "?", "[", "]"]):
|
|
445
|
+
return fnmatch.fnmatch(rp, p)
|
|
446
|
+
# Otherwise treat as prefix (file or directory).
|
|
447
|
+
if rp == p:
|
|
448
|
+
return True
|
|
449
|
+
return rp.startswith(p.rstrip("/") + "/")
|
|
450
|
+
|
|
451
|
+
def check_declared_paths(rel: str, decl: str) -> None:
|
|
452
|
+
paths = split_declared_values(decl)
|
|
453
|
+
if not paths:
|
|
454
|
+
return
|
|
455
|
+
has_persistent = any(evidence_is_persistent(p) for p in paths)
|
|
456
|
+
if not has_persistent:
|
|
457
|
+
(errors if strict else warnings).append(
|
|
458
|
+
f"{rel} Evidence_Path should include at least one persistent path under changes/<id>/evidence or changes/<id>/review"
|
|
459
|
+
)
|
|
460
|
+
for raw in paths:
|
|
461
|
+
p = raw.strip()
|
|
462
|
+
if not p:
|
|
463
|
+
continue
|
|
464
|
+
if p.startswith("./"):
|
|
465
|
+
p = p[2:]
|
|
466
|
+
if p.startswith("~") or os.path.isabs(p):
|
|
467
|
+
errors.append(f"{rel} Evidence_Path must be workspace-relative, got absolute path: {raw}")
|
|
468
|
+
continue
|
|
469
|
+
if ".." in Path(p).parts:
|
|
470
|
+
errors.append(f"{rel} Evidence_Path must not contain '..': {raw}")
|
|
471
|
+
continue
|
|
472
|
+
abs_p = (root / p).resolve()
|
|
473
|
+
try:
|
|
474
|
+
abs_p.relative_to(root)
|
|
475
|
+
except Exception:
|
|
476
|
+
errors.append(f"{rel} Evidence_Path points outside workspace: {raw}")
|
|
477
|
+
continue
|
|
478
|
+
if not abs_p.exists():
|
|
479
|
+
errors.append(f"{rel} Evidence_Path missing file: {raw}")
|
|
480
|
+
|
|
481
|
+
def scope_check_from_plan(plan_rel: str, plan_text: str) -> None:
|
|
482
|
+
if not check_scope:
|
|
483
|
+
return
|
|
484
|
+
patterns = [normalize_scope_pattern(p) for p in parse_scope_patterns_from_plan(plan_text)]
|
|
485
|
+
patterns = [p for p in patterns if p]
|
|
486
|
+
if not patterns:
|
|
487
|
+
(errors if strict else warnings).append(f"{plan_rel} scope check enabled but no patterns found under '## Scope'")
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
meta_path = change_dir / ".ws-change.json"
|
|
491
|
+
base_branch = ""
|
|
492
|
+
if meta_path.exists():
|
|
493
|
+
try:
|
|
494
|
+
meta = json.loads(read_text(meta_path))
|
|
495
|
+
base_branch = str((meta or {}).get("base_branch") or "").strip()
|
|
496
|
+
except Exception:
|
|
497
|
+
base_branch = ""
|
|
498
|
+
if not base_branch:
|
|
499
|
+
(errors if strict else warnings).append(
|
|
500
|
+
"scope check requires base_branch recorded in changes/<id>/.ws-change.json (run `aiws change start <id>` to record it)"
|
|
501
|
+
)
|
|
502
|
+
return
|
|
503
|
+
|
|
504
|
+
# Compute changed files vs merge-base with base_branch (include staged + unstaged).
|
|
505
|
+
try:
|
|
506
|
+
mb = subprocess.run(
|
|
507
|
+
["git", "-C", str(root), "merge-base", base_branch, "HEAD"],
|
|
508
|
+
check=False,
|
|
509
|
+
stdout=subprocess.PIPE,
|
|
510
|
+
stderr=subprocess.PIPE,
|
|
511
|
+
text=True,
|
|
512
|
+
)
|
|
513
|
+
except Exception as e:
|
|
514
|
+
(errors if strict else warnings).append(f"scope check failed to run git merge-base: {e}")
|
|
515
|
+
return
|
|
516
|
+
if mb.returncode != 0:
|
|
517
|
+
(errors if strict else warnings).append(f"scope check failed to compute merge-base vs {base_branch}: {mb.stderr.strip() or mb.stdout.strip()}")
|
|
518
|
+
return
|
|
519
|
+
base = (mb.stdout or "").strip()
|
|
520
|
+
if not base:
|
|
521
|
+
(errors if strict else warnings).append(f"scope check failed to compute merge-base vs {base_branch}: empty output")
|
|
522
|
+
return
|
|
523
|
+
|
|
524
|
+
def diff_names(args: List[str]) -> List[str]:
|
|
525
|
+
try:
|
|
526
|
+
res = subprocess.run(
|
|
527
|
+
["git", "-C", str(root), "diff", "--name-only", "--diff-filter=ACMR", *args],
|
|
528
|
+
check=False,
|
|
529
|
+
stdout=subprocess.PIPE,
|
|
530
|
+
stderr=subprocess.PIPE,
|
|
531
|
+
text=True,
|
|
532
|
+
)
|
|
533
|
+
except Exception:
|
|
534
|
+
return []
|
|
535
|
+
if res.returncode != 0:
|
|
536
|
+
return []
|
|
537
|
+
return [ln.strip() for ln in (res.stdout or "").splitlines() if ln.strip()]
|
|
538
|
+
|
|
539
|
+
# Include untracked files as "changed" too (new files are common in dev/deliver stages).
|
|
540
|
+
untracked: List[str] = []
|
|
541
|
+
try:
|
|
542
|
+
ls = subprocess.run(
|
|
543
|
+
["git", "-C", str(root), "ls-files", "--others", "--exclude-standard"],
|
|
544
|
+
check=False,
|
|
545
|
+
stdout=subprocess.PIPE,
|
|
546
|
+
stderr=subprocess.PIPE,
|
|
547
|
+
text=True,
|
|
548
|
+
)
|
|
549
|
+
if ls.returncode == 0:
|
|
550
|
+
untracked = [ln.strip() for ln in (ls.stdout or "").splitlines() if ln.strip()]
|
|
551
|
+
except Exception:
|
|
552
|
+
untracked = []
|
|
553
|
+
|
|
554
|
+
changed = sorted(set(diff_names([base]) + diff_names(["--cached", base]) + untracked))
|
|
555
|
+
if not changed:
|
|
556
|
+
return
|
|
557
|
+
|
|
558
|
+
# Always allow core workflow artifacts.
|
|
559
|
+
always_allow = [
|
|
560
|
+
f"changes/{change_id}/**",
|
|
561
|
+
"plan/**",
|
|
562
|
+
"REQUIREMENTS.md",
|
|
563
|
+
"requirements/requirements-issues.csv",
|
|
564
|
+
"issues/problem-issues.csv",
|
|
565
|
+
]
|
|
566
|
+
all_patterns = patterns + always_allow
|
|
567
|
+
|
|
568
|
+
out_of_scope = [p for p in changed if not any(match_scope_pattern(p, pat) for pat in all_patterns)]
|
|
569
|
+
if out_of_scope:
|
|
570
|
+
msg = "out-of-scope files detected (update plan Scope or explain): " + ", ".join(out_of_scope[:20]) + (" ..." if len(out_of_scope) > 20 else "")
|
|
571
|
+
(errors if strict else warnings).append(f"{plan_rel} {msg}")
|
|
572
|
+
|
|
392
573
|
req_id = ""
|
|
393
574
|
prob_id = ""
|
|
394
575
|
change_id_decl = ""
|
|
@@ -425,6 +606,8 @@ def validate_change(
|
|
|
425
606
|
errors.append("proposal.md must include non-empty Plan_File")
|
|
426
607
|
if strict and not evidence_path_decl:
|
|
427
608
|
errors.append("proposal.md must include non-empty Evidence_Path")
|
|
609
|
+
if check_evidence and evidence_path_decl:
|
|
610
|
+
check_declared_paths("proposal.md", evidence_path_decl)
|
|
428
611
|
|
|
429
612
|
if req_id and req_csv.exists():
|
|
430
613
|
ok = False
|
|
@@ -532,6 +715,9 @@ def validate_change(
|
|
|
532
715
|
errors.append(f"{plan_rel} must include non-empty Plan_File")
|
|
533
716
|
if strict and not plan_evidence_path:
|
|
534
717
|
errors.append(f"{plan_rel} must include non-empty Evidence_Path")
|
|
718
|
+
if check_evidence and plan_evidence_path:
|
|
719
|
+
check_declared_paths(plan_rel, plan_evidence_path)
|
|
720
|
+
scope_check_from_plan(plan_rel, plan_text)
|
|
535
721
|
|
|
536
722
|
if plan_file_from_plan:
|
|
537
723
|
plan_file_refs = split_declared_values(plan_file_from_plan)
|
|
@@ -602,6 +788,16 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
602
788
|
action="store_true",
|
|
603
789
|
help="Do not fail strict validation on truth drift (use only for emergencies).",
|
|
604
790
|
)
|
|
791
|
+
parser.add_argument(
|
|
792
|
+
"--check-evidence",
|
|
793
|
+
action="store_true",
|
|
794
|
+
help="Validate Evidence_Path points to workspace-relative existing files; require at least one persistent evidence path.",
|
|
795
|
+
)
|
|
796
|
+
parser.add_argument(
|
|
797
|
+
"--check-scope",
|
|
798
|
+
action="store_true",
|
|
799
|
+
help="Scope gate: compare git diff vs base_branch with plan '## Scope' patterns and warn/error on out-of-scope files.",
|
|
800
|
+
)
|
|
605
801
|
parser.add_argument(
|
|
606
802
|
"--allow-branches",
|
|
607
803
|
default="main,master",
|
|
@@ -663,6 +859,8 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
663
859
|
change_id=change_id,
|
|
664
860
|
strict=args.strict,
|
|
665
861
|
allow_truth_drift=args.allow_truth_drift,
|
|
862
|
+
check_evidence=args.check_evidence,
|
|
863
|
+
check_scope=args.check_scope,
|
|
666
864
|
)
|
|
667
865
|
|
|
668
866
|
|