sillyspec 3.26.4 → 3.26.6
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/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +5 -3
- package/package.json +1 -1
- package/src/db.js +4 -2
- package/src/fs-atomic.js +2 -2
- package/src/git-helper.js +21 -8
- package/src/index.js +3 -1
- package/src/progress/stage-machine.js +10 -4
- package/src/quicklog.js +2 -0
- package/src/review-tier.js +26 -14
- package/src/run/command.js +50 -7
- package/src/run/complete-handlers.js +95 -1
- package/src/run/complete.js +81 -6
- package/src/run/gates.js +1 -1
- package/src/run/prompt.js +34 -20
- package/src/run/quick-audit.js +5 -6
- package/src/run/shared.js +45 -11
- package/src/run/stage.js +10 -0
- package/src/run.js +5 -5
- package/src/stage-contract-spec.js +11 -0
- package/src/stage-contract.js +11 -1
- package/src/stage-review.js +9 -1
- package/src/stages/archive.js +12 -26
- package/src/stages/brainstorm-auto.js +1 -0
- package/src/stages/brainstorm.js +1 -0
- package/src/stages/execute.js +45 -0
- package/src/stages/plan.js +17 -1
- package/src/stages/quick.js +2 -0
- package/src/stages/scan.js +2 -2
- package/src/stages/verify.js +1 -0
- package/src/worktree-apply.js +3 -3
- package/src/worktree.js +83 -8
- package/packages/dashboard/index.html +0 -15
- package/packages/dashboard/package-lock.json +0 -2384
- package/packages/dashboard/public/favicon.jpg +0 -0
- package/packages/dashboard/public/logo.jpg +0 -0
- package/packages/dashboard/public/prototype-dashboard.html +0 -836
- package/packages/dashboard/public/prototype-overview.html +0 -256
- package/packages/dashboard/src/App.vue +0 -558
- package/packages/dashboard/src/components/ActionBar.vue +0 -93
- package/packages/dashboard/src/components/CommandPalette.vue +0 -96
- package/packages/dashboard/src/components/DetailPanel.vue +0 -137
- package/packages/dashboard/src/components/DocPreview.vue +0 -257
- package/packages/dashboard/src/components/DocTree.vue +0 -114
- package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
- package/packages/dashboard/src/components/LogStream.vue +0 -65
- package/packages/dashboard/src/components/PipelineStage.vue +0 -95
- package/packages/dashboard/src/components/PipelineView.vue +0 -156
- package/packages/dashboard/src/components/ProjectCard.vue +0 -187
- package/packages/dashboard/src/components/ProjectList.vue +0 -210
- package/packages/dashboard/src/components/ProjectOverview.vue +0 -152
- package/packages/dashboard/src/components/StageBadge.vue +0 -67
- package/packages/dashboard/src/components/StepCard.vue +0 -94
- package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
- package/packages/dashboard/src/components/detail/DocsDetail.vue +0 -48
- package/packages/dashboard/src/components/detail/GitDetail.vue +0 -61
- package/packages/dashboard/src/components/detail/TechDetail.vue +0 -43
- package/packages/dashboard/src/composables/useDashboard.js +0 -213
- package/packages/dashboard/src/composables/useKeyboard.js +0 -119
- package/packages/dashboard/src/composables/useLayout.js +0 -131
- package/packages/dashboard/src/composables/useWebSocket.js +0 -129
- package/packages/dashboard/src/main.js +0 -8
- package/packages/dashboard/src/style.css +0 -132
- package/packages/dashboard/vite.config.js +0 -18
|
@@ -82,6 +82,7 @@ brainstorm 完成时按 design.md frontmatter 的 `scale` 分叉:
|
|
|
82
82
|
brainstorm 在 `tier=independent` 规模下除 design.md 等四件套外,还需产出一个 stage 级 `review.json`,CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
|
|
83
83
|
|
|
84
84
|
- 路径:`.sillyspec/.runtime/stage-reviews/brainstorm-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-brainstorm-<变更名>`。
|
|
85
|
+
- **run-id / marker 由 CLI 自动生成注入**(review step prompt 渲染时 echo 完整目录路径 + 写 marker;撞 gate 报缺 review.json 时 gate 也 echo 完整路径 + 写 marker)。直接用 CLI 给的路径写 review.json,**勿手算 run-id(必须 `review-` 前缀)、勿手写 marker**。卡住时用 `sillyspec register-stage-review --change <名> --stage brainstorm` 一步生成。
|
|
85
86
|
- 字段(`schemaVersion:1`,`reviewType=design` —— 区别于 execute 的 `acceptance`):
|
|
86
87
|
|
|
87
88
|
```json
|
|
@@ -88,6 +88,7 @@ execute 完成时,每个 task 必须有 `review.json` 且 verdict 通过,否
|
|
|
88
88
|
execute 还有**第二道**独立的 stage 级审查:除逐 task review.json 外,整个 execute 阶段完成还需一个 stage 级 `review.json`(在 "完成确认"/acceptance 步骤产出)。CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
|
|
89
89
|
|
|
90
90
|
- 路径:`.sillyspec/.runtime/stage-reviews/execute-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-execute-<变更名>`。
|
|
91
|
+
- **run-id / marker 由 CLI 自动生成注入**(review step prompt 渲染时 echo 完整目录路径 + 写 marker;撞 gate 报缺 review.json 时 gate 也 echo 完整路径 + 写 marker)。直接用 CLI 给的路径写 review.json,**勿手算 run-id(必须 `review-` 前缀)、勿手写 marker**。卡住时用 `sillyspec register-stage-review --change <名> --stage execute` 一步生成。
|
|
91
92
|
- 字段(`schemaVersion:1`,`reviewType=acceptance` —— 区别于 brainstorm/plan 的 `"design"`):
|
|
92
93
|
|
|
93
94
|
```json
|
|
@@ -51,6 +51,7 @@ plan 的步骤是动态的:`generate_plan`(生成分级计划)→ `review_
|
|
|
51
51
|
`review_plan` 步骤在 `tier=independent` 时产出一个 stage 级 `review.json`,CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
|
|
52
52
|
|
|
53
53
|
- 路径:`.sillyspec/.runtime/stage-reviews/plan-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-plan-<变更名>`。
|
|
54
|
+
- **run-id / marker 由 CLI 自动生成注入**(review step prompt 渲染时 echo 完整目录路径 + 写 marker;撞 gate 报缺 review.json 时 gate 也 echo 完整路径 + 写 marker)。直接用 CLI 给的路径写 review.json,**勿手算 run-id(必须 `review-` 前缀)、勿手写 marker**。卡住时用 `sillyspec register-stage-review --change <名> --stage plan` 一步生成。
|
|
54
55
|
- 字段(`schemaVersion:1`,`reviewType=plan` —— plan 阶段主审查文档是 `plan.md`;execute 阶段才是 `acceptance`):
|
|
55
56
|
|
|
56
57
|
```json
|
|
@@ -61,10 +61,11 @@ sillyspec run quick --done --change quick-<hash> --output "…" # 完成该会
|
|
|
61
61
|
|---|---|
|
|
62
62
|
| `--linked-changes none\|a,b` | **显式关联变更(取代 `--change`,推荐)**。none=不关联,a,b=关联列表 |
|
|
63
63
|
| `--files a.js,b.js` | 显式声明本次允许修改的文件(边界保护) |
|
|
64
|
-
| `--file-notes "p::注 \|\| p::注"` | quick `--done` 用:QUICKLOG「文件:」行落盘为多行带括注 bullet(省事后手改文件行)。格式 `path::括注`,`\|\|`
|
|
64
|
+
| `--file-notes "p::注 \|\| p::注"` | quick `--done` 用:QUICKLOG「文件:」行落盘为多行带括注 bullet(省事后手改文件行)。格式 `path::括注`,`\|\|` 分隔多条;**只随 step3 --done 同命令传**(CLI 短进程,step1/step2 传无效,不带到 step3) |
|
|
65
65
|
| `--allow-new` | 允许新增文件(默认禁止,防意外创建) |
|
|
66
|
+
| `--allow-delete` | 允许删除文件(默认 fail-closed,删除是破坏性操作;确认删除带此 flag 显式解锁) |
|
|
66
67
|
| `--force-baseline` | 允许覆盖 baseline 受保护文件 / 压制 `.sillyspec/` 危险判定(危险,慎用) |
|
|
67
|
-
| `--confirm` | ⚠️ 仅打印变更概览,**不解锁 blocked**(blocked 仍 exit 1)。真正解锁用 `--force-baseline`/`--allow-new` |
|
|
68
|
+
| `--confirm` | ⚠️ 仅打印变更概览,**不解锁 blocked**(blocked 仍 exit 1)。真正解锁用 `--force-baseline`/`--allow-new`/`--allow-delete` |
|
|
68
69
|
|
|
69
70
|
## 审计与并发变更(`--done` 边界审计)
|
|
70
71
|
|
|
@@ -73,7 +74,8 @@ sillyspec run quick --done --change quick-<hash> --output "…" # 完成该会
|
|
|
73
74
|
- **并发其他会话的 `.sillyspec/changes/<非关联变更>/` 不再被本 quick 拦截**。quick 自己没有 `changes/` 目录,该路径下非关联内容视为并发会话的工作,整体放行(确定性审计无法区分「并发工作」与「本 quick 偷建变更」,后者这类意图软判定留给 sillyhub)。
|
|
74
75
|
- **关联变更的文件仍走审计**:reverse-sync 改自己关联变更的 `design.md` 会被拦,需 `--force-baseline` 显式确认。
|
|
75
76
|
- **baseline 折叠目录前缀匹配**:step1 启动时整片 `changes/` 未跟踪会被 git 折叠成 `?? .sillyspec/changes/`(带尾斜杠 token);审计时若该目录下文件被并发会话跟踪而展开成文件级路径,按尾斜杠 token 前缀放行其下所有文件,不误判。
|
|
76
|
-
-
|
|
77
|
+
- **同文件并发 warn(advisory,不阻断)**:step1 启动时若你的某个 `allowedFile` 已在他者脏文件列表里(他者也改了这文件),--done 时 CLI 会比对当前内容 hash 与启动时录入的 hash——不一致(你也改了它)即判「同文件并发」并 warn:整文件 pathspec 提交会夹带他者 hunk。warn 给出分离指引(`git add -p <file>` 交互选你自己的 hunk,或 `git diff <file> > mine.patch` 编辑后 `git apply --cached mine.patch` 再 commit)。这**不阻断** --done(你可能有意整文件提交),看 warn 后自行决定是否分离。
|
|
78
|
+
- **`--confirm` ≠ 解锁**:它只打印概览。blocked 时真正解锁的是 `--force-baseline`(压制 `.sillyspec/` 危险判定 + baseline 覆盖)/`--allow-new`(放行新增)/`--allow-delete`(放行删除,删除是破坏性操作默认 fail-closed,须显式 opt-in),三者可在 step1 或 `--done` 传,都生效。
|
|
77
79
|
|
|
78
80
|
## 典型用法
|
|
79
81
|
|
package/package.json
CHANGED
package/src/db.js
CHANGED
|
@@ -93,8 +93,10 @@ export class DB {
|
|
|
93
93
|
* - 主库缺失/为空/损坏:尝试 .bak;成功则 warn 原因后 copy 回主库使用。
|
|
94
94
|
* - 都不可用:主库不存在→建全新空库;主库曾存在(空/损坏)→fail-loud 抛错(不静默建空库吞进度)。
|
|
95
95
|
* node:sqlite 打开 0 字节文件不抛错:主库须 statSync 显式判空(截断信号→走回退,防静默吞进度);
|
|
96
|
-
* .bak 作为恢复源只要存在且能打开即用(含 0 字节空库——node:sqlite
|
|
97
|
-
*
|
|
96
|
+
* .bak 作为恢复源只要存在且能打开即用(含 0 字节空库——node:sqlite 视其为合法空库)。
|
|
97
|
+
* 注:node:sqlite 提交即持久化,不再写前备份主 .bak(sql.js 时代 _save/_atomicWriteSync 已随
|
|
98
|
+
* 2026-08-11 迁移移除);此恢复分支是向后兼容兜底,仅对 sql.js 时代遗留的 .bak 生效,
|
|
99
|
+
* 全新 node:sqlite 项目不产生主 .bak)。
|
|
98
100
|
*/
|
|
99
101
|
_openWithFallback() {
|
|
100
102
|
const bakPath = `${this.dbPath}.bak`;
|
package/src/fs-atomic.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* 用于 .runtime/*.json、pointer、guard.json 等「会被其他进程 / hook 读取」的配置文件。
|
|
5
5
|
* 保证读者要么看到旧完整内容、要么看到新完整内容,不会读到半截 JSON。
|
|
6
6
|
*
|
|
7
|
-
* 注意:
|
|
8
|
-
*
|
|
7
|
+
* 注意:DB 持久化由 node:sqlite 引擎承担(提交即落盘 sillyspec.db + WAL 侧车),
|
|
8
|
+
* 不经此处的 writeAtomicSync;sillyspec.db 不走原子写改名。
|
|
9
9
|
*/
|
|
10
10
|
import { writeFileSync, renameSync, unlinkSync } from 'fs';
|
|
11
11
|
import { dirname, basename, join } from 'path';
|
package/src/git-helper.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* 三者统一带 `-c safe.directory=<cwd>` per-command(不污染全局 config)+ `-C cwd` 前缀;
|
|
11
11
|
* 默认 timeout 5000ms(opts.timeout 可覆盖),默认 trim(opts.trim:false 保留原样输出)。
|
|
12
12
|
* 数组形式天然规避 shell 拆词,路径含空格 / 元字符安全。
|
|
13
|
+
* safeGit 额外支持 opts.retryOnTimeout:ETIMEDOUT(机器忙瞬时抖动)时用 2× timeout 重试一次。
|
|
13
14
|
*/
|
|
14
15
|
import { execFileSync } from 'node:child_process'
|
|
15
16
|
|
|
@@ -17,22 +18,34 @@ import { execFileSync } from 'node:child_process'
|
|
|
17
18
|
* 安全执行 git:失败不抛,返回 { value, error } 结构。
|
|
18
19
|
* @param {string} cwd
|
|
19
20
|
* @param {string[]} args
|
|
20
|
-
* @param {{ trim?: boolean, timeout?: number }} [opts]
|
|
21
|
+
* @param {{ trim?: boolean, timeout?: number, retryOnTimeout?: boolean }} [opts]
|
|
21
22
|
* - trim:false 保留原样输出(git status --porcelain 首行前导空格
|
|
22
23
|
* 是状态码的一部分,trim 会削掉致 parsePorcelainPath 丢首字符,坑见 auditQuickCompletion 注释)
|
|
23
24
|
* - timeout 默认 5000ms,长操作(大 diff / worktree list)可传更大值
|
|
25
|
+
* - retryOnTimeout:true 时,ETIMEDOUT 用 2× timeout 重试一次(默认 false 向后兼容)。
|
|
26
|
+
* 机器忙时 git 子进程启动慢易瞬时超时;审计锚点 git 调用失败即 blocked 中断 quick,
|
|
27
|
+
* 重试一次能消化绝大多数瞬时抖动,免去用户手工重跑。权限 / 损坏等非超时错误不重试(救不回)。
|
|
24
28
|
* @returns {{ value: string|null, error: string|null }}
|
|
25
29
|
*/
|
|
26
30
|
export function safeGit(cwd, args, opts = {}) {
|
|
27
|
-
const { trim = true, timeout = 5000 } = opts
|
|
31
|
+
const { trim = true, timeout = 5000, retryOnTimeout = false } = opts
|
|
28
32
|
const fullArgs = ['-c', `safe.directory=${cwd}`, '-C', cwd, ...args]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// 单次尝试:返回 { value, errorObj }(errorObj 为原始 Error 或 null),不在内部格式化 message。
|
|
34
|
+
// 格式化(取首行)推迟到最终返回,避免重试分支重复格式化,并保留原始 code 供重试判定。
|
|
35
|
+
const attempt = (t) => {
|
|
36
|
+
try {
|
|
37
|
+
let value = execFileSync('git', fullArgs, { encoding: 'utf8', timeout: t })
|
|
38
|
+
if (trim) value = value.trim()
|
|
39
|
+
return { value, errorObj: null }
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return { value: null, errorObj: e }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let { value, errorObj } = attempt(timeout)
|
|
45
|
+
if (errorObj && retryOnTimeout && errorObj.code === 'ETIMEDOUT') {
|
|
46
|
+
({ value, errorObj } = attempt(timeout * 2))
|
|
35
47
|
}
|
|
48
|
+
return { value, error: errorObj ? errorObj.message.split('\n')[0] : null }
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
/**
|
package/src/index.js
CHANGED
|
@@ -1015,6 +1015,8 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
1015
1015
|
} else if (result.result === 'skipped') {
|
|
1016
1016
|
console.log(`⏭️ worktree 跳过清理: ${wtName} (mode: ${result.mode})`);
|
|
1017
1017
|
console.log(` 原因: in-place 模式没有隔离目录需要清理`);
|
|
1018
|
+
} else if (result.result === 'blocked') {
|
|
1019
|
+
console.error(`🚫 拒绝清理:有未落主仓交付变更,请先 sillyspec worktree apply ${wtName} 或 --force`);
|
|
1018
1020
|
} else {
|
|
1019
1021
|
console.log(`ℹ️ worktree 未找到: ${wtName}`);
|
|
1020
1022
|
}
|
|
@@ -1671,7 +1673,7 @@ SillySpec config — local.yaml 配置键速查
|
|
|
1671
1673
|
// 已知产物 → 用途说明。agent 据此判断每份产物能否作为证据/该读哪个。
|
|
1672
1674
|
const KNOWN = {
|
|
1673
1675
|
'sillyspec.db': '进度库 SQLite(stages/steps/changes 表)',
|
|
1674
|
-
'sillyspec.db.bak': '
|
|
1676
|
+
'sillyspec.db.bak': '进度库备份(sql.js 时代写前快照遗留,node:sqlite 不再写入,仅 _openWithFallback 向后兼容恢复用)',
|
|
1675
1677
|
'sillyspec.db.schema-version': '进度库 schema 版本标记',
|
|
1676
1678
|
'doctor-diagnosis.json': 'doctor --json 结构化诊断快照',
|
|
1677
1679
|
'doctor-dumps': 'doctor --dump-db 取证输出目录',
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// 调 pm.read/_write/_ensureDB/_getSpecDir/readGlobal/_runtimePath/listChanges/_appendAuditLog/
|
|
4
4
|
// _renderBatchProgress(persistence-core + 其他组 delegate)。组内方法互调保持 this.X(同 class)。
|
|
5
5
|
// completeStage 五层(resolve/validate/force/tx/history/print)整体搬迁,不拆流水线(保行为、最低风险)。
|
|
6
|
-
import { mkdirSync } from 'fs';
|
|
6
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
7
7
|
import { join, resolve, basename } from 'path';
|
|
8
8
|
import { writeAtomicSync } from '../fs-atomic.js';
|
|
9
9
|
import { runValidators } from '../stage-contract.js';
|
|
@@ -140,21 +140,27 @@ export class StageMachine {
|
|
|
140
140
|
console.log('');
|
|
141
141
|
console.log(' ═══════════════════════════════════════');
|
|
142
142
|
console.log(` 项目: ${(global?.project) || basename(cwd) || '(未命名)'}`);
|
|
143
|
-
|
|
143
|
+
// 当前操作目标与活跃列表区分(坑 status-change-pointer-ambiguous):多活跃时不带
|
|
144
|
+
// --change 的 run/progress 不隐式选任何一个(index.js --change 解析多活跃报错),明确
|
|
145
|
+
// 说出来,防止把残留空壳(default/quick-xxx/目录缺失记录)误当操作目标跑错 change。
|
|
146
|
+
console.log(` 当前操作目标: (多活跃——不带 --change 时不隐式选定,run 须显式 --change <name>)`);
|
|
147
|
+
console.log(` 活跃变更记录: ${changes.length} 个(下列为 DB 中存在的活跃记录,非操作目标)`);
|
|
144
148
|
console.log(' ═══════════════════════════════════════');
|
|
145
149
|
console.log('');
|
|
146
150
|
|
|
151
|
+
const changesRoot = join(this.pm._getSpecDir(cwd), 'changes');
|
|
147
152
|
for (const cn of changes) {
|
|
148
153
|
const data = this.pm.read(cwd, cn);
|
|
154
|
+
const dirMissing = !existsSync(join(changesRoot, cn));
|
|
149
155
|
if (!data) {
|
|
150
|
-
console.log(` 📂 ${cn} — (无法读取)`);
|
|
156
|
+
console.log(` 📂 ${cn} — (无法读取)${dirMissing ? ' ⚠️ 目录缺失(残留记录,可用 doctor 清理)' : ''}`);
|
|
151
157
|
continue;
|
|
152
158
|
}
|
|
153
159
|
const currentStage = data.currentStage || '(无)';
|
|
154
160
|
const stageLabel = STAGE_LABELS[data.currentStage] || currentStage;
|
|
155
161
|
const lastActive = data.lastActive ? this._timeAgo(data.lastActive) : '未知';
|
|
156
162
|
|
|
157
|
-
console.log(` 📂 ${cn}`);
|
|
163
|
+
console.log(` 📂 ${cn}${dirMissing ? ' ⚠️ 目录缺失(残留记录,可用 doctor 清理)' : ''}`);
|
|
158
164
|
console.log(` 当前阶段: ${stageLabel} 最近活跃: ${lastActive}`);
|
|
159
165
|
console.log('');
|
|
160
166
|
}
|
package/src/quicklog.js
CHANGED
|
@@ -243,6 +243,8 @@ async function rotateIfNeeded(userFile, gitUser) {
|
|
|
243
243
|
} else {
|
|
244
244
|
renameSync(userFile, archiveFile)
|
|
245
245
|
}
|
|
246
|
+
// echo 轮转:归档文件是 git 跟踪的新文件,提交时带上(勿漏,否则旧 ql 条目只在本地)
|
|
247
|
+
console.log(`🔄 QUICKLOG 已轮转(>500 行):${basename(userFile)} → ${basename(archiveFile)}(提交时带上归档文件,勿漏)`)
|
|
246
248
|
}
|
|
247
249
|
|
|
248
250
|
function tasksPath(specBase, change) {
|
package/src/review-tier.js
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* - tier=self → 当前 agent 自审,CLI 放行(不要求 review.json)
|
|
14
14
|
* - tier=independent → 强制独立审查子代理 + review.json,CLI 硬校验
|
|
15
15
|
*
|
|
16
|
-
* CLI 侧(run.js done gate)与 prompt
|
|
17
|
-
*
|
|
16
|
+
* CLI 侧(run.js done gate)与 prompt 注入侧用同一个函数,保证两边判定一致。
|
|
17
|
+
* 判定权归 agent 的 plan_level 自主判断(CLI 只做确定性映射,见 classifyReviewTier 注释);
|
|
18
|
+
* 无 plan_level 的阶段才退文件数启发式。
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import { parseFileChangeList } from './change-list.js'
|
|
@@ -28,14 +29,21 @@ export const SELF_REVIEW_FILE_THRESHOLD = 3
|
|
|
28
29
|
/**
|
|
29
30
|
* 审查分级
|
|
30
31
|
*
|
|
32
|
+
* 判定权归 agent 的 plan_level 判定(2026-08-14 tier-plan-level 改造):plan_level 是 agent
|
|
33
|
+
* 在 plan step1 按 prompt 分级规则做的自主判断,tier 直接映射它——CLI 不再用文件数启发式
|
|
34
|
+
* 推翻 agent 判定(此前 light + 4 文件会被文件数强制 independent,两套标准打架且不透明,
|
|
35
|
+
* 实证:agent 判 light 自审通过,完成时 CLI 按 7 文件强制 independent review.json)。
|
|
36
|
+
*
|
|
31
37
|
* 判定顺序(短路):
|
|
32
|
-
* 1. plan_level === 'none'
|
|
33
|
-
* 2.
|
|
34
|
-
* 3.
|
|
38
|
+
* 1. plan_level === 'none' → self(极小变更)
|
|
39
|
+
* 2. plan_level === 'light' → self(agent 自主判定轻量,自审)
|
|
40
|
+
* 3. plan_level === 'full' → independent(full 语义即大变更需独立审查,CLI 确定性强制)
|
|
41
|
+
* 4. 无 plan_level(brainstorm 等阶段 plan.md 未生成)→ 退文件数启发式:
|
|
42
|
+
* design.md 变更文件数 ≤ SELF_REVIEW_FILE_THRESHOLD → self,否则 independent
|
|
35
43
|
*
|
|
36
|
-
* fail-safe
|
|
37
|
-
*
|
|
38
|
-
* (宁严勿松)。
|
|
44
|
+
* fail-safe:无 plan_level 且 designPath 传了但解析不到文件清单(fileCount=0)按字面 0
|
|
45
|
+
* 处理(真无文件变更 = 极小);designPath 完全没传(fileCount=null,无法证明规模)→
|
|
46
|
+
* independent(宁严勿松)。
|
|
39
47
|
*
|
|
40
48
|
* @param {object} [opts]
|
|
41
49
|
* @param {string} [opts.planLevel] - plan_level 分类结果(none/light/full)。brainstorm/propose 在 plan 之前,可能拿不到。
|
|
@@ -44,7 +52,13 @@ export const SELF_REVIEW_FILE_THRESHOLD = 3
|
|
|
44
52
|
*/
|
|
45
53
|
export function classifyReviewTier({ planLevel, designPath } = {}) {
|
|
46
54
|
if (planLevel === 'none') {
|
|
47
|
-
return { tier: 'self', reason:
|
|
55
|
+
return { tier: 'self', reason: 'plan_level=none(极小变更,agent 自主判定自审)', fileCount: null }
|
|
56
|
+
}
|
|
57
|
+
if (planLevel === 'light') {
|
|
58
|
+
return { tier: 'self', reason: 'plan_level=light(agent 自主判定轻量变更,自审)', fileCount: null }
|
|
59
|
+
}
|
|
60
|
+
if (planLevel === 'full') {
|
|
61
|
+
return { tier: 'independent', reason: 'plan_level=full(agent 自主判定大变更,独立审查防确认偏差)', fileCount: null }
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
let fileCount = null
|
|
@@ -53,13 +67,11 @@ export function classifyReviewTier({ planLevel, designPath } = {}) {
|
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
if (fileCount !== null && fileCount <= SELF_REVIEW_FILE_THRESHOLD) {
|
|
56
|
-
return { tier: 'self', reason:
|
|
70
|
+
return { tier: 'self', reason: `无 plan_level,变更文件 ${fileCount} ≤ ${SELF_REVIEW_FILE_THRESHOLD}(启发式)`, fileCount }
|
|
57
71
|
}
|
|
58
72
|
|
|
59
73
|
const reason = fileCount === null
|
|
60
|
-
?
|
|
61
|
-
|
|
62
|
-
: '无法解析 design.md 变更文件清单,按正常规模处理')
|
|
63
|
-
: `变更文件 ${fileCount} > ${SELF_REVIEW_FILE_THRESHOLD}`
|
|
74
|
+
? '无法解析 design.md 变更文件清单,按正常规模处理(fail-safe)'
|
|
75
|
+
: `无 plan_level,变更文件 ${fileCount} > ${SELF_REVIEW_FILE_THRESHOLD}(启发式)`
|
|
64
76
|
return { tier: 'independent', reason, fileCount }
|
|
65
77
|
}
|
package/src/run/command.js
CHANGED
|
@@ -28,6 +28,7 @@ import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
|
28
28
|
import { outputStep } from './prompt.js'
|
|
29
29
|
import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
|
|
30
30
|
import { runStage } from './stage.js'
|
|
31
|
+
import { sanitizeDesc } from '../quicklog.js'
|
|
31
32
|
import { ProgressManager } from '../progress.js'
|
|
32
33
|
import { validateChangeExists } from '../stage-contract.js'
|
|
33
34
|
import { stageRegistry, auxiliaryStages } from '../stages/index.js'
|
|
@@ -44,6 +45,32 @@ const VALUE_FLAGS = new Set([
|
|
|
44
45
|
'--files', '--file-notes', '--from-step', '--mode', '--dir', '--confirm-mode',
|
|
45
46
|
])
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* 检测 `--files` 是否被空格分隔误用(坑 quick-files-space-separated-silently-drops)。
|
|
50
|
+
*
|
|
51
|
+
* --files 是单值 flag(VALUE_FLAGS:校验循环只跳一个 token、--k=v 只展开一个值),空格分隔的
|
|
52
|
+
* 多文件只有首个被当值,其余沦为位置参数被 `:482` 的 `startsWith('--')` 校验静默忽略 →
|
|
53
|
+
* guard.allowedFiles 只剩首个 → --done 边界审计误拦,边界保护形同虚设。
|
|
54
|
+
*
|
|
55
|
+
* 返回 --files 值及其后所有连续非 flag token(疑似被丢的多文件);length > 1 即误用,调用方
|
|
56
|
+
* fail-loud(process.exit 2,同 run --json :109 显式拒绝静默吞风格)。贪婪收集多值会破坏
|
|
57
|
+
* VALUE_FLAGS 单值框架(校验循环/等号展开/getFlagValue 都假设单值),故仅检测不改语义。
|
|
58
|
+
*
|
|
59
|
+
* 纯函数:无副作用、不读 process、不 mutate 入参,供 command.js 解析 + 单测复用。
|
|
60
|
+
* @param {string[]} flags runCommand 规范化后的 flags 数组(--k=v 已展开)
|
|
61
|
+
* @returns {string[]} --files 值 + 其后连续非 flag token;无 --files / 值是 flag 名 / 含逗号 → []
|
|
62
|
+
*/
|
|
63
|
+
export function detectSpaceSeparatedFiles(flags) {
|
|
64
|
+
const idx = flags.indexOf('--files')
|
|
65
|
+
if (idx === -1) return []
|
|
66
|
+
const val = flags[idx + 1]
|
|
67
|
+
// 值缺失 / 值本身是 flag 名(--files --done 漏值)/ 逗号分隔(正确用法)→ 不检测
|
|
68
|
+
if (!val || val.startsWith('--') || val.includes(',')) return []
|
|
69
|
+
const suspects = [val]
|
|
70
|
+
for (let j = idx + 2; j < flags.length && !flags[j].startsWith('--'); j++) suspects.push(flags[j])
|
|
71
|
+
return suspects
|
|
72
|
+
}
|
|
73
|
+
|
|
47
74
|
/**
|
|
48
75
|
* 从 progress 或变更目录推导变更名
|
|
49
76
|
*/
|
|
@@ -444,6 +471,16 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
444
471
|
}
|
|
445
472
|
|
|
446
473
|
// 解析 --files a.js,b.js(quick 专用:显式声明 allowedFiles)
|
|
474
|
+
// --files 是单值 flag,空格分隔的多文件只取首个致其余静默丢失(坑 quick-files-space-separated-silently-drops)。
|
|
475
|
+
// detectSpaceSeparatedFiles 检测误用 → fail-loud 报错指明逗号用法(同 run --json :109 风格)。
|
|
476
|
+
const spaceSuspects = detectSpaceSeparatedFiles(flags)
|
|
477
|
+
if (spaceSuspects.length > 1) {
|
|
478
|
+
console.error('❌ --files 检测到空格分隔的多文件:CLI 只识别首个,其余会被静默丢弃(边界保护失效)。')
|
|
479
|
+
console.error(` 疑似多文件:${spaceSuspects.join(' ')}`)
|
|
480
|
+
console.error(' --files 是单值参数,多文件请用逗号分隔(无空格):')
|
|
481
|
+
console.error(` --files ${spaceSuspects.join(',')}`)
|
|
482
|
+
process.exit(2) // 用法错 → exit 2
|
|
483
|
+
}
|
|
447
484
|
let quickFiles = []
|
|
448
485
|
const filesIdx = flags.indexOf('--files')
|
|
449
486
|
if (filesIdx !== -1 && flags[filesIdx + 1]) {
|
|
@@ -461,6 +498,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
461
498
|
setQuickFileNotes(quickFileNotes)
|
|
462
499
|
|
|
463
500
|
const isAllowNew = flags.includes('--allow-new')
|
|
501
|
+
const isAllowDelete = flags.includes('--allow-delete')
|
|
464
502
|
const isForceBaseline = flags.includes('--force-baseline')
|
|
465
503
|
const isForceRescan = flags.includes('--force-rescan')
|
|
466
504
|
|
|
@@ -471,7 +509,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
471
509
|
'--reason', '--options', '--answer', '--confirm-mode',
|
|
472
510
|
'--output', '--input', '--change', '--linked-changes',
|
|
473
511
|
'--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
|
|
474
|
-
'--files', '--file-notes', '--allow-new', '--force-baseline', '--force-rescan',
|
|
512
|
+
'--files', '--file-notes', '--allow-new', '--allow-delete', '--force-baseline', '--force-rescan',
|
|
475
513
|
'--json', '--dir', '--help',
|
|
476
514
|
'--reopen', '--from-step', '--mode',
|
|
477
515
|
'--deep', '--quick', '--standard', // scan profile 三档显式选择(scan-profile.js 从 argv 读;互斥见下方 PROFILE_FLAGS 检测)
|
|
@@ -594,7 +632,10 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
594
632
|
// 如果指定了变更名或有变更目录,自动初始化变更的 progress
|
|
595
633
|
const autoChange = changeName || resolveChangeNameAuto(cwd, specRoot)
|
|
596
634
|
if (autoChange) {
|
|
597
|
-
|
|
635
|
+
// 创建时即写 title(与 name/KEY 同时落 db):--input 需求描述 sanitizeDesc 优先,无则用 name 兜底。
|
|
636
|
+
// proposal 还没写、无权威标题来源,这是临时值;brainstorm/plan 完成 proposal/design 落盘后由
|
|
637
|
+
// complete.js 通用完成路径 deriveTitleFromLinkedChange 刷新为真实 # 标题(与 quick 启动 title 同源)。
|
|
638
|
+
progress = pm.initChange(cwd, autoChange, { title: inputText ? sanitizeDesc(inputText) : autoChange })
|
|
598
639
|
} else if (isAuxiliary) {
|
|
599
640
|
let autoName = changeName || resolveChangeNameAuto(cwd, specRoot) || 'default'
|
|
600
641
|
// archive 特例:归档后变更从活跃列表排除(listChanges WHERE status='active'),
|
|
@@ -616,7 +657,8 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
616
657
|
}
|
|
617
658
|
changeName = autoName
|
|
618
659
|
if (!progress) {
|
|
619
|
-
|
|
660
|
+
// auxiliary 创建也写 title(--input 优先 / name 兜底),同上完整流程语义。
|
|
661
|
+
progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
|
|
620
662
|
// initChange 可能因 project 表为空返回 null
|
|
621
663
|
if (!progress) {
|
|
622
664
|
progress = { currentStage: stageName, stages: {}, lastActive: new Date().toLocaleString('zh-CN', { hour12: false }), project: '' }
|
|
@@ -636,7 +678,8 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
636
678
|
console.log(`🔄 自动创建变更:${autoName}`)
|
|
637
679
|
console.log(` 提示:可以用 --change <名称> 指定自定义变更名`)
|
|
638
680
|
console.log(` 或事后重命名:sillyspec change-rename ${autoName} <新名称>`)
|
|
639
|
-
|
|
681
|
+
// brainstorm 自动创建变更也写 title(--input 需求描述优先 / 自动名兜底),proposal 落盘后刷新。
|
|
682
|
+
progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
|
|
640
683
|
changeName = autoName
|
|
641
684
|
} else {
|
|
642
685
|
console.error('❌ 未找到进度数据,请先运行 sillyspec init 或指定 --change <变更名>')
|
|
@@ -811,11 +854,11 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
811
854
|
// --done
|
|
812
855
|
if (isDone) {
|
|
813
856
|
const doneAnswer = getFlagValue('--answer')
|
|
814
|
-
return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, doneAnswer, isForceBaseline, isAllowNew })
|
|
857
|
+
return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, doneAnswer, isForceBaseline, isAllowNew, isAllowDelete })
|
|
815
858
|
}
|
|
816
859
|
|
|
817
860
|
// 默认:输出当前步骤
|
|
818
|
-
return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts, { quickFiles, isAllowNew, isForceBaseline, isForceRescan, linkedChanges, taskDescription: inputText })
|
|
861
|
+
return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts, { quickFiles, isAllowNew, isAllowDelete, isForceBaseline, isForceRescan, linkedChanges, taskDescription: inputText })
|
|
819
862
|
}
|
|
820
863
|
|
|
821
864
|
/**
|
|
@@ -915,7 +958,7 @@ async function resetStage(pm, progress, stageName, cwd, changeName, platformOpts
|
|
|
915
958
|
const wm = new WorktreeManager({ cwd })
|
|
916
959
|
const meta = wm.getMeta(changeName)
|
|
917
960
|
if (meta) {
|
|
918
|
-
const cleanResult = wm.cleanup(changeName)
|
|
961
|
+
const cleanResult = wm.cleanup(changeName, { force: true })
|
|
919
962
|
if (cleanResult.residual?.length > 0) {
|
|
920
963
|
console.warn(`⚠️ reset 清理 worktree 残留: ${cleanResult.residual.join('; ')}`)
|
|
921
964
|
console.warn(` 手动处理: sillyspec worktree cleanup ${changeName} --force`)
|
|
@@ -625,7 +625,7 @@ export async function handleWorkflowPostCheck({ stageName, steps, currentIdx, cw
|
|
|
625
625
|
* isForceBaseline/isAllowNew/platformOpts。辅助函数直接 import(safeGit/auditQuickCompletion ← shared,
|
|
626
626
|
* printQuickAuditReview ← quick-audit,4 个 quicklog fns ← quicklog,unlinkSync/rmSync ← fs 静态)。
|
|
627
627
|
*/
|
|
628
|
-
export async function handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, platformOpts, pm }) {
|
|
628
|
+
export async function handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, platformOpts, pm }) {
|
|
629
629
|
// quick 收尾:强校验 QUICKLOG 条目 + 翻状态 + 勾 tasks.md(CLI 接管)
|
|
630
630
|
if (stageName === 'quick') {
|
|
631
631
|
// §4.6 从 session guard.json 读 guard(不依赖 progress.quickGuard)。
|
|
@@ -666,6 +666,7 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
666
666
|
...guard,
|
|
667
667
|
forceBaseline: guard.forceBaseline || isForceBaseline,
|
|
668
668
|
allowNew: guard.allowNew || isAllowNew,
|
|
669
|
+
allowDelete: guard.allowDelete || isAllowDelete,
|
|
669
670
|
}
|
|
670
671
|
review = await auditQuickCompletion(cwd, mergedGuard, { isConfirm: confirm })
|
|
671
672
|
printQuickAuditReview(review)
|
|
@@ -833,6 +834,96 @@ export async function handleExecuteWaveArtifact({ stageName, steps, currentIdx,
|
|
|
833
834
|
return null
|
|
834
835
|
}
|
|
835
836
|
|
|
837
|
+
/**
|
|
838
|
+
* 聚合最新 execute run 各 task review.json 的 changedFiles(主仓 repo 过滤)。
|
|
839
|
+
*
|
|
840
|
+
* 复用 resolveLatestExecuteRunIdWithTasks(task-review.js:684,规避 marker 漂移)+ readReview。
|
|
841
|
+
* 仅主仓 repo(review.repo 缺省或 'main')的 changedFiles 参与主仓核验;跨仓 repo 的 task 文件
|
|
842
|
+
* 由跨仓仓独立落地,不在主仓 worktree/分支,混入会误报 missing(Grill M11)。
|
|
843
|
+
*
|
|
844
|
+
* @param {{ runtimeRoot: string, changeName: string }} opts
|
|
845
|
+
* @returns {Promise<string[]>} 主仓 changedFiles 聚合(去重保序);无 run / 无 tasks / 读取失败 → []
|
|
846
|
+
*/
|
|
847
|
+
export async function collectExecuteChangedFiles({ runtimeRoot, changeName }) {
|
|
848
|
+
if (!runtimeRoot || !changeName) return []
|
|
849
|
+
const { resolveLatestExecuteRunIdWithTasks, readReview, normalizeRepoKey } = await import('../task-review.js')
|
|
850
|
+
const runId = resolveLatestExecuteRunIdWithTasks({ runtimeRoot })
|
|
851
|
+
if (!runId) return []
|
|
852
|
+
const tasksDir = join(runtimeRoot, 'execute-runs', runId, 'tasks')
|
|
853
|
+
if (!existsSync(tasksDir)) return []
|
|
854
|
+
let taskIds
|
|
855
|
+
try {
|
|
856
|
+
taskIds = readdirSync(tasksDir)
|
|
857
|
+
} catch {
|
|
858
|
+
return []
|
|
859
|
+
}
|
|
860
|
+
const files = []
|
|
861
|
+
for (const taskId of taskIds) {
|
|
862
|
+
const r = readReview(join(tasksDir, taskId, 'review.json'))
|
|
863
|
+
if (!r.ok || !r.review) continue
|
|
864
|
+
// 跨仓 repo 过滤:仅主仓(repo 缺省视 'main')参与主仓核验,避免误报
|
|
865
|
+
if (normalizeRepoKey(r.review.repo) !== 'main') continue
|
|
866
|
+
if (Array.isArray(r.review.changedFiles)) {
|
|
867
|
+
for (const f of r.review.changedFiles) {
|
|
868
|
+
// 交付物过滤(与 worktree.js hasUnappliedChanges isDeliverable 同口径):.sillyspec/ 蓝图/
|
|
869
|
+
// runtime 产物与 meta.json 不参与落盘核验——它们随主仓/并行 session 维护,不在 worktree 分支,
|
|
870
|
+
// 混入会误报 missing
|
|
871
|
+
if (typeof f !== 'string' || f.trim() === '') continue
|
|
872
|
+
if (f.startsWith('.sillyspec/') || f === 'meta.json') continue
|
|
873
|
+
files.push(f)
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return [...new Set(files)]
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* execute 阶段级核验(防空跑谎报,D-002@v1 / FR-04/05/06):聚合最新 execute run 各 task review 声称的
|
|
882
|
+
* 交付文件,用 findMissingDeliverables 核验其存在于 worktree 分支 tree 或 worktree 工作区。
|
|
883
|
+
*
|
|
884
|
+
* - missing 文件 → console.warn 列清单,提示"apply 将无源可复制"(宽松非阻断,不 exit 不 throw)。
|
|
885
|
+
* - checked:false(worktree 目录 / 分支不存在)→ 保守提示"无法核验,请人工确认"。
|
|
886
|
+
* - 与 Task Review Gate 既有校验(零改动伪造 / 不相交伪造,task-review.js:590-623)互补不重复拦截:
|
|
887
|
+
* 本核验的真实增量窗口是「review 通过后文件被删且未 commit」+「无法核验时给人工确认提示」。
|
|
888
|
+
*
|
|
889
|
+
* 由 handleExecuteWorktreeCleanup 开头调用:核验发生在 worktree cleanup 之前,目录被清前先判定
|
|
890
|
+
* 交付文件是否落盘。整个钩子 try/catch 兜底——任何异常只 warn,绝不影响 execute 完成(FR-07)。
|
|
891
|
+
*
|
|
892
|
+
* ctx:stageName/changeName/cwd。WorktreeManager + findMissingDeliverables ← 动态 ../worktree.js;
|
|
893
|
+
* task-review 读取 ← 动态 ../task-review.js。runtimeRoot 用本地 specBase 解析(无 platformOpts 传入
|
|
894
|
+
* 时平台 runtimeRoot 不在本函数作用域,降级为本仓 .runtime,读不到 review 则跳过,零误报)。
|
|
895
|
+
*/
|
|
896
|
+
export async function handleExecuteDeliverableCheck({ stageName, changeName, cwd }) {
|
|
897
|
+
if (stageName !== 'execute' || !changeName) return null
|
|
898
|
+
try {
|
|
899
|
+
const { WorktreeManager, findMissingDeliverables } = await import('../worktree.js')
|
|
900
|
+
const wm = new WorktreeManager({ cwd })
|
|
901
|
+
const meta = wm.getMeta(changeName)
|
|
902
|
+
if (!meta) return null // 无 worktree meta(非 worktree execute / 已清),无需核验
|
|
903
|
+
const specBase = join(cwd, '.sillyspec')
|
|
904
|
+
const runtimeRoot = resolveRuntimeRoot({}, specBase)
|
|
905
|
+
const changedFiles = await collectExecuteChangedFiles({ runtimeRoot, changeName })
|
|
906
|
+
if (changedFiles.length === 0) return null
|
|
907
|
+
const { missing, checked } = findMissingDeliverables({
|
|
908
|
+
worktreePath: meta.worktreePath,
|
|
909
|
+
branch: meta.branch,
|
|
910
|
+
changedFiles,
|
|
911
|
+
})
|
|
912
|
+
if (!checked) {
|
|
913
|
+
console.warn('⚠️ execute 阶段级核验:无法核验(worktree 目录或分支不存在),请人工确认交付文件已落盘。')
|
|
914
|
+
return null
|
|
915
|
+
}
|
|
916
|
+
if (missing.length > 0) {
|
|
917
|
+
console.warn(`⚠️ execute 阶段级核验:以下 ${missing.length} 个声称实现的交付文件既不在分支也不在工作区,疑似空跑/从未落盘:`)
|
|
918
|
+
for (const f of missing) console.warn(` ${f}`)
|
|
919
|
+
console.warn(' 请检查子代理是否真实实现,或先 commit 到分支;apply 将无源可复制。')
|
|
920
|
+
}
|
|
921
|
+
} catch (e) {
|
|
922
|
+
console.warn(`⚠️ execute 阶段级核验跳过: ${e?.message || e}`)
|
|
923
|
+
}
|
|
924
|
+
return null
|
|
925
|
+
}
|
|
926
|
+
|
|
836
927
|
/**
|
|
837
928
|
* execute 阶段完成时条件性清理 worktree(W6 Step6c 从 completeStep 完成路径内联块抽出)。
|
|
838
929
|
* 不依赖 AI agent 的完成确认步骤:有未 apply 变更 → 保留 worktree;否则 cleanup(含 in-place 安全清理)。
|
|
@@ -841,6 +932,9 @@ export async function handleExecuteWaveArtifact({ stageName, steps, currentIdx,
|
|
|
841
932
|
* ctx:stageName/changeName/cwd。WorktreeManager ← 动态 ../worktree.js。
|
|
842
933
|
*/
|
|
843
934
|
export async function handleExecuteWorktreeCleanup({ stageName, changeName, cwd }) {
|
|
935
|
+
// 阶段级核验(D-002@v1,防空跑谎报):cleanup 之前判定 review 声称实现的交付文件是否落盘。
|
|
936
|
+
// 宽松非阻断:缺失 warn / 无法核验保守提示 / 异常只 warn,均不影响下方 cleanup 与 execute 完成。
|
|
937
|
+
await handleExecuteDeliverableCheck({ stageName, changeName, cwd })
|
|
844
938
|
if (stageName === 'execute' && changeName) {
|
|
845
939
|
try {
|
|
846
940
|
const { WorktreeManager } = await import('../worktree.js');
|