sillyspec 3.26.4 → 3.26.5
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/git-helper.js +21 -8
- package/src/index.js +2 -0
- package/src/quicklog.js +2 -0
- package/src/run/command.js +50 -7
- package/src/run/complete-handlers.js +95 -1
- package/src/run/complete.js +23 -5
- 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/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/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
|
}
|
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/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');
|
package/src/run/complete.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* completion path 收尾 + single-step path)+ skipStep/waitStep/continueStep(W6 Step7b-2 续搬)。
|
|
6
6
|
* 本 commit(7b-1)先搬 completeStep + 2 独占 helper(validateMetadata/validateFileLocations)。
|
|
7
7
|
*
|
|
8
|
-
* 安全锚:run.js 始终 barrel。completeStep 由 run.js
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* 安全锚:run.js 始终 barrel。completeStep 由 run/command.js 在 --done 路径调用,无测试专用导出
|
|
9
|
+
* (曾以 `_completeStepForTest` 别名 re-export 供 characterization 测试直驱动,2026-08-13 这些测试
|
|
10
|
+
* 全部迁到 CLI 子进程 `sillyspec run <stage> --done`,导出已移除——见 run.js barrel 注释)。
|
|
11
11
|
*
|
|
12
12
|
* 依赖(completeStep 仅用已抽叶子 + 随搬 helper,零 run.js 闭包,零动态 import):
|
|
13
13
|
* - shared.js: triggerSync/WAIT_MARKER_RE/getStageSteps;prompt.js: outputStep;gates.js: enforceDepsGate/runStageCompletionGates
|
|
@@ -24,6 +24,7 @@ import { enforceDepsGate, enforceReviewJsonGate, completeStageGates, readDesignS
|
|
|
24
24
|
import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact } from './complete-handlers.js'
|
|
25
25
|
import { formatExecuteSummary } from '../worktree-apply.js'
|
|
26
26
|
import { isEndToEndTaskText } from '../change-risk-profile.js'
|
|
27
|
+
import { deriveTitleFromLinkedChange } from '../quicklog.js'
|
|
27
28
|
|
|
28
29
|
// validateMetadata / readDesignScale / validateFileLocations 已迁至 ./gates.js(completeStageGates
|
|
29
30
|
// 共享收尾管线,消除 noAI 末步 / continueStep 完成分支绕过 gate 的 S1/S2/S3 不对称)。
|
|
@@ -60,7 +61,7 @@ export function resolveWaitingStepWithAnswer(steps, doneAnswer, nowStr) {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
63
|
-
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false } = options
|
|
64
|
+
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false } = options
|
|
64
65
|
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
65
66
|
const stageData = progress.stages[stageName]
|
|
66
67
|
|
|
@@ -248,7 +249,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
248
249
|
return { stageCompleted: false, currentIdx, nextPendingIdx: -1 }
|
|
249
250
|
}
|
|
250
251
|
// quick 收尾(W6 Step6b 抽至 complete-handlers.js handleQuickStageCompletion)
|
|
251
|
-
await handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, platformOpts, pm })
|
|
252
|
+
await handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, platformOpts, pm })
|
|
252
253
|
|
|
253
254
|
// ── reopen --done 回填(坑 brainstorm-reopen-step-state-desync)──
|
|
254
255
|
// nextPendingIdx === -1 且无 waiting,说明要进阶段完成分支。此时若存在 stale 步骤
|
|
@@ -299,6 +300,16 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
299
300
|
const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText, ctx: _completeCtx })
|
|
300
301
|
if (_stageGatesResult) return _stageGatesResult
|
|
301
302
|
|
|
303
|
+
// 完整流程 change title 刷新:proposal/design 落盘后(brainstorm/plan 完成),从 proposal 首个 #
|
|
304
|
+
// 标题刷新 changes.title(人类可读中文标题,用户可随时刷新)。quick-<hex> 无 proposal 目录 →
|
|
305
|
+
// deriveTitleFromLinkedChange 返回 '' 不刷新(quick 走 handleQuickStageCompletion 的 extractTitleFromResult)。
|
|
306
|
+
if (changeName && !/^quick-[0-9a-f]{8}$/.test(changeName)) {
|
|
307
|
+
try {
|
|
308
|
+
const refinedTitle = deriveTitleFromLinkedChange(specBase, changeName)
|
|
309
|
+
if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle })
|
|
310
|
+
} catch { /* title 刷新失败不阻断完成 */ }
|
|
311
|
+
}
|
|
312
|
+
|
|
302
313
|
// gate 全过:persist completed(task-01 移后)。此处到 _write 之间若崩,DB 仍 in-progress(内存已 completed 但未落盘),下次进 CLI 读 DB 即 in-progress,不产生"假 completed"。
|
|
303
314
|
pm._write(cwd, progress, changeName)
|
|
304
315
|
triggerSync(cwd, changeName, platformOpts)
|
|
@@ -764,6 +775,13 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
764
775
|
}
|
|
765
776
|
const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps: stageData.steps, currentIdx, outputText: null, ctx: _contCtx })
|
|
766
777
|
if (_stageGatesResult) return _stageGatesResult
|
|
778
|
+
// 完整流程 change title 刷新(同 completeStep 完成分支,wait 解除后阶段完成也刷新)。
|
|
779
|
+
if (changeName && !/^quick-[0-9a-f]{8}$/.test(changeName)) {
|
|
780
|
+
try {
|
|
781
|
+
const refinedTitle = deriveTitleFromLinkedChange(specBase, changeName)
|
|
782
|
+
if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle })
|
|
783
|
+
} catch { /* title 刷新失败不阻断完成 */ }
|
|
784
|
+
}
|
|
767
785
|
// gate 全过:persist completed(task-03 移后;此处无 triggerSync)。
|
|
768
786
|
pm._write(cwd, progress, changeName)
|
|
769
787
|
console.log(`\n✅ ${stageName} 阶段已完成(${stageData.steps.length}/${stageData.steps.length} 步)`)
|
package/src/run/gates.js
CHANGED
|
@@ -309,7 +309,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
309
309
|
: 'acceptance'
|
|
310
310
|
const searchDirs = [effectiveSpecBase, reviewChangeDir, cwd].filter(Boolean)
|
|
311
311
|
const reviewResult = validateStageReview({ stage: stageName, reviewType, runtimeRoot, reviewRunId, searchDirs })
|
|
312
|
-
printStageReviewResult(reviewResult, { stage: stageName })
|
|
312
|
+
printStageReviewResult(reviewResult, { stage: stageName, reviewRunId, runtimeRoot })
|
|
313
313
|
if (!reviewResult.ok) {
|
|
314
314
|
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
315
315
|
}
|