sillyspec 3.29.4 → 3.31.0
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-auto/SKILL.md +1 -1
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +7 -105
- package/AGENTS.md +38 -20
- package/SKILL.md +9 -3
- package/assets/command-cards/flow.md +32 -0
- package/assets/command-cards/run-archive.md +26 -0
- package/assets/command-cards/run-brainstorm.md +33 -0
- package/assets/command-cards/run-execute.md +29 -0
- package/assets/command-cards/run-plan.md +27 -0
- package/assets/command-cards/run-quick.md +17 -0
- package/assets/command-cards/run-verify.md +29 -0
- package/assets/command-cards/status.md +27 -0
- package/package.json +2 -1
- package/packages/dashboard/server/executor.js +2 -1
- package/packages/dashboard/server/parser.js +7 -7
- package/r7-test-env/README-R7-TEST.md +93 -0
- package/r7-test-env/demo-project/lib/calc.js +6 -0
- package/r7-test-env/demo-project/package.json +1 -0
- package/r7-test-env/demo-project/test.js +10 -0
- package/r7-test-env/prompt-R7-SMOKE.md +41 -0
- package/r7-test-env/sillyspec-r7.cmd +3 -0
- package/round4/account.mjs +98 -0
- package/round4/b1-analyze-task08.mjs +40 -0
- package/round4/b1-b1-target-validation.md +32 -0
- package/round4/b3a-prompt-audit-result.md +66 -0
- package/round4/b3a-prompt-audit.mjs +89 -0
- package/round4/brief-autocompact.md +34 -0
- package/round4/brief-session-replay.md +50 -0
- package/round4/deep-read-synthesis.md +38 -0
- package/round4/extract-full.mjs +38 -0
- package/round4/extract-timeline.mjs +43 -0
- package/round4/find-sessions.mjs +13 -0
- package/round4/full-r4-l.md +4707 -0
- package/round4/full-r4-o-l.md +4300 -0
- package/round4/full-r4-os.md +1681 -0
- package/round4/full-r4-sf.md +4255 -0
- package/round4/full-r4-sq.md +1734 -0
- package/round4/gate-audit-relay-receipt.md +30 -0
- package/round4/gate-value-audit.md +52 -0
- package/round4/optimization-plan.md +165 -0
- package/round4/prereg.md +67 -0
- package/round4/prompt-R4-L.md +63 -0
- package/round4/prompt-R4-O-L.md +59 -0
- package/round4/prompt-R4-O-S.md +56 -0
- package/round4/prompt-R4-S-F.md +60 -0
- package/round4/prompt-R4-S-Q.md +66 -0
- package/round4/r4-final-report.html +347 -0
- package/round4/r4-os-timeline.txt +297 -0
- package/round4/r4-sf-timeline.txt +738 -0
- package/round4/r4-sq-timeline.txt +322 -0
- package/round4/r4l-timeline.txt +793 -0
- package/round4/r4ol-timeline.txt +729 -0
- package/round4/r4sf-timeline.txt +852 -0
- package/round4/r5-scenario-matrix.md +46 -0
- package/round4/result-S.md +171 -0
- package/round4/result-final.md +46 -0
- package/round4/result-quality.md +63 -0
- package/src/change-list.js +9 -0
- package/src/classify-change.js +6 -0
- package/src/command-cards.js +74 -0
- package/src/commit-guard.js +2 -0
- package/src/config-schema.js +64 -12
- package/src/contract-matrix.js +26 -5
- package/src/db.js +35 -4
- package/src/decision-distill.js +9 -3
- package/src/diagnostic-codes.js +2 -1
- package/src/docs-check.js +1 -1
- package/src/doctor-diagnostics.js +44 -2
- package/src/flow-draft.js +748 -0
- package/src/flow-parity.js +236 -0
- package/src/flow-review.js +184 -0
- package/src/flow.js +1492 -0
- package/src/foreign-declared.js +17 -1
- package/src/fr-index.js +396 -22
- package/src/fs-atomic.js +5 -1
- package/src/git-helper.js +2 -2
- package/src/greenfield-bootstrap.js +98 -0
- package/src/handoff.js +120 -7
- package/src/hunk-attribution.js +191 -0
- package/src/index.js +602 -21
- package/src/init.js +25 -8
- package/src/knowledge-classify.js +47 -1
- package/src/knowledge-digest.js +162 -0
- package/src/knowledge-quicklog.js +246 -0
- package/src/machine-draft.js +128 -0
- package/src/machine-interface.js +194 -7
- package/src/mcp-server.js +1 -0
- package/src/module-impact.js +51 -22
- package/src/module-resolve.js +3 -1
- package/src/preview-progress.js +160 -0
- package/src/progress/change-registry.js +11 -3
- package/src/progress/shared.js +3 -2
- package/src/progress.js +56 -13
- package/src/quicklog.js +44 -6
- package/src/redomain.js +147 -0
- package/src/review-material-pack.js +204 -1
- package/src/run/archive-distill.js +4 -2
- package/src/run/bg-sync.js +67 -0
- package/src/run/command.js +127 -10
- package/src/run/complete-handlers.js +363 -73
- package/src/run/complete.js +356 -74
- package/src/run/gate-snapshot-ledger.js +213 -0
- package/src/run/gate-snapshot.js +260 -38
- package/src/run/gates.js +482 -268
- package/src/run/green-cache.js +123 -0
- package/src/run/junction-rm.js +70 -0
- package/src/run/next.js +14 -0
- package/src/run/prompt.js +185 -55
- package/src/run/quick-audit.js +213 -14
- package/src/run/shared.js +123 -4
- package/src/run/stage.js +182 -216
- package/src/run/test-ledger.js +264 -0
- package/src/run/verify-quality-scan.js +84 -7
- package/src/scope-audit.js +6 -1
- package/src/sentinel-assertions.js +114 -0
- package/src/setup.js +3 -3
- package/src/spec-sync.js +41 -1
- package/src/stage-contract.js +26 -8
- package/src/stage-review-checklist.js +2 -0
- package/src/stage-review.js +7 -2
- package/src/stage-templates.js +157 -0
- package/src/stages/archive.js +2 -0
- package/src/stages/brainstorm.js +24 -106
- package/src/stages/execute.js +222 -85
- package/src/stages/knowledge.js +74 -1
- package/src/stages/plan-postcheck.js +385 -20
- package/src/stages/plan.js +23 -1
- package/src/stages/quick.js +10 -2
- package/src/stages/verify.js +4 -2
- package/src/sync.js +81 -23
- package/src/task-done.js +181 -0
- package/src/task-review.js +8 -55
- package/src/taskcard-frontmatter.js +57 -0
- package/src/test-bindings.js +549 -0
- package/src/timeline.js +218 -0
- package/src/ui-visual.js +175 -0
- package/src/verify-draft.js +214 -0
- package/src/verify-postcheck.js +919 -159
- package/src/verify-probes.js +103 -25
- package/src/watcher.js +1031 -0
- package/src/worktree-apply.js +44 -4
- package/src/worktree-deps.js +22 -8
- package/src/worktree-policy.js +42 -0
- package/src/worktree.js +102 -15
- package/templates/agents-instruction.md +37 -18
package/src/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, unlinkSync } from 'fs';
|
|
10
10
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
11
11
|
import { basename, dirname, extname, join, resolve, isAbsolute, sep, relative } from 'path';
|
|
12
|
-
import { safeGit, git } from './git-helper.js';
|
|
12
|
+
import { safeGit, git, gitQuiet } from './git-helper.js';
|
|
13
13
|
import { getVersion } from './version.js';
|
|
14
14
|
|
|
15
15
|
// E22(性能#2):progress.js(拖 db.js→node:sqlite 链 ~48ms)与 run/shared.js(拖 stages 全家
|
|
@@ -53,11 +53,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
53
53
|
--json 输出 JSON(程序化读取)
|
|
54
54
|
|
|
55
55
|
阶段特有参数:
|
|
56
|
-
quick:
|
|
57
|
-
--files a.js,b.js 显式声明允许修改的文件
|
|
58
|
-
--allow-new 允许新增文件(默认禁止)
|
|
59
|
-
--force-baseline 允许覆盖 baseline 受保护文件
|
|
60
|
-
--confirm 完成时确认接受变更审计
|
|
56
|
+
quick: (通道已退役 v3.31.0——仅收尾升级前在途会话:--done/--cancel/--change <会话ID>;新工作走 flow start)
|
|
61
57
|
scan: --quick | --standard | --deep 显式选择 profile(优先于规模自动判定;三档互斥)
|
|
62
58
|
--force-rescan 覆盖已有 scan 文档保护
|
|
63
59
|
--diff [--base <commit>] [--full] [--report] scan 文档 vs 源码漂移清单(纯只读)
|
|
@@ -95,6 +91,10 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
95
91
|
sillyspec config [schema] [--json] 打印 local.yaml 全部已知键 + 生效状态 + 读取点(堵外部 agent 配置发现缺口)
|
|
96
92
|
sillyspec config cat [--json] 读取 local.yaml 实际内容(自动定位真实路径,worktree 内解析到主仓)
|
|
97
93
|
sillyspec runtime list [--json] 枚举 .sillyspec/.runtime/ 运行时产物(只读,看手上有哪些证据/状态文件)
|
|
94
|
+
sillyspec watcher alerts --change <名> [--all] [--follow] [--runtime-root <路径>]
|
|
95
|
+
哨兵告警查询(watcher 事件流只读可视化;--all 全事件、--follow 2s 轮询持续打印)
|
|
96
|
+
sillyspec watcher timeline --change <名> [--runtime-root <路径>]
|
|
97
|
+
变更合成时间线(事件流 × tasks.md × git 提交锚三源真实留痕渲染;活跃/归档变更均可)
|
|
98
98
|
sillyspec dispatch <probe | hint> SillyHub 派发能力探测 + 策略生成(agent 调用桥,仅渲染不执行 tool)
|
|
99
99
|
sillyspec agent-log [--detect] [--json] 本地 agent 会话日志查询/现场探测(run 命令自动上报平台 POST /api/agent-logs 并本地留底)
|
|
100
100
|
|
|
@@ -114,7 +114,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
114
114
|
sillyspec delta --change <name> [--json] 生成变更 delta.md(Before/Delta/After 三段式聚合;幂等覆盖重跑即刷新)
|
|
115
115
|
sillyspec next 项目状态探测:输出当前状态 + 下一步命令 + 依据(吸收 continue/resume 手工探测表)
|
|
116
116
|
sillyspec commit [--json] 智能提交建议:收集 QUICKLOG/已勾 task/阶段产出语义,生成建议 message(只建议不执行)
|
|
117
|
-
sillyspec verify-probes --change <name> [--init] verify 机械探针(TODO 标记/测试覆盖/API 对账/删除对账);--init 生成 verify-result.md
|
|
117
|
+
sillyspec verify-probes --change <name> [--init [--force]] verify 机械探针(TODO 标记/测试覆盖/API 对账/删除对账);--init 生成 verify-result.md 骨架(--force 覆盖重生成,手填内容会重置)
|
|
118
118
|
sillyspec module-impact --change <name> 生成 module-impact.md 骨架(文件×模块归属按 module-map 预填 + 未匹配清单)
|
|
119
119
|
sillyspec scope-audit --change <name> [--json] 变更范围对账:计划×实际三态全表 + 行数(✓ 计划内/⚠️ 计划外/⚠️ 计划未动;quick 会话传 quick-<id> 出归属表;跨仓条目按 local.yaml repos 分仓真实对账——--json 出 repos[] 仓库维度汇总;已归档变更可查——快照记录态;advisory 只读不设门禁)
|
|
120
120
|
sillyspec module-docs-sync --change <name> [--note ...] diff 归属模块 → sidecar 追加变更索引行 + 卡 updated_at 戳(幂等)
|
|
@@ -145,7 +145,8 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
145
145
|
sillyspec knowledge <search --query "..." --limit N
|
|
146
146
|
| inspect --id "..."
|
|
147
147
|
| validate | refresh
|
|
148
|
-
| propose --title "..." --category <name
|
|
148
|
+
| propose --title "..." --category <name>
|
|
149
|
+
| inbox [--json] (知识收件箱:uncategorized 待审清单+基线态)>
|
|
149
150
|
sillyspec platform <connect <url> [--token <t>]
|
|
150
151
|
| disconnect
|
|
151
152
|
| sync [--change <name>] | sync-docs [--change <name>]
|
|
@@ -167,11 +168,10 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
167
168
|
示例:
|
|
168
169
|
sillyspec init
|
|
169
170
|
sillyspec run brainstorm --change 2026-07-03-add-login
|
|
170
|
-
sillyspec
|
|
171
|
+
sillyspec flow start --change my-fix --input "<动机与背景+独立一行『成功标准:』+每行一条『- 可验证标准』>" # 轻量变更(默认快道,2 次调用)
|
|
171
172
|
sillyspec run verify --done --output "验证通过,测试全绿"
|
|
172
173
|
sillyspec run archive --done --confirm --output "归档完成"
|
|
173
174
|
sillyspec run plan --reopen --from-step 2 # 修订 plan,从第 2 步重做
|
|
174
|
-
sillyspec run quick --non-interactive --done --output "CI 内的快修" # 脚本/CI
|
|
175
175
|
sillyspec progress show
|
|
176
176
|
sillyspec progress show --all # 多变更汇总时无信号变更全量单行列出(默认折叠超 8 个)
|
|
177
177
|
sillyspec progress show --json # 全局总览 envelope(面板/脚本消费)
|
|
@@ -196,6 +196,36 @@ async function withJsonOutput(json, fn) {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
// quick 通道直接退役(2026-09-25-quick-channel-retire):CLI 入口预门——拦「新会话启动形态」的
|
|
200
|
+
// quick 调用。必须在 process entry 层拒绝:run/command.js 对新会话的 id 生成 / owner.json 落盘 /
|
|
201
|
+
// 「📌 已建立」公告都发生在 runStage 之前,晚于此处拒绝会留幻影会话残留(违背零副作用承诺)。
|
|
202
|
+
// 放行面(在途收尾与校验路径,全部先于 command.js 会话建立、零副作用):
|
|
203
|
+
// ① --done/--cancel/--status/--skip/--reset/--reopen/--help——收尾/只读/帮助
|
|
204
|
+
// ② --change quick-<8hex>——在途续跑(指向不存在会话的由 stage.js 第二道网拒绝)
|
|
205
|
+
// ③ 含「新启合法集」之外旗标的形态——未知旗标由 command.js 未知参数校验 exit 2(该校验
|
|
206
|
+
// 2026-09-19 已上移至会话建立之前),放行不产生副作用;漏放行的合法收尾形态同理被
|
|
207
|
+
// stage.js 兜底网拒绝。进程内直调 runCommand 的入口不经此门,亦由 stage.js 兜底。
|
|
208
|
+
const QUICK_FRESH_START_FLAGS = new Set([
|
|
209
|
+
'--input', '--linked-changes', '--files', '--file-notes',
|
|
210
|
+
'--allow-new', '--allow-delete', '--force-baseline',
|
|
211
|
+
'--non-interactive', '--interactive', '--session',
|
|
212
|
+
'--json', '--dir', '--spec-dir', '--spec-root', '--runtime-root', '--workspace-id',
|
|
213
|
+
'--skip-approval', '--no-docs', '--confirm-mode',
|
|
214
|
+
])
|
|
215
|
+
function refuseRetiredQuickFreshStart(stageArgs) {
|
|
216
|
+
const flags = stageArgs.slice(1).filter((a) => String(a).startsWith('-'))
|
|
217
|
+
const passThrough = flags.some((f) => !QUICK_FRESH_START_FLAGS.has(f))
|
|
218
|
+
|| (() => {
|
|
219
|
+
const i = stageArgs.indexOf('--change')
|
|
220
|
+
return i !== -1 && /^quick-[0-9a-f]{8}$/.test(String(stageArgs[i + 1] || ''))
|
|
221
|
+
})()
|
|
222
|
+
if (passThrough) return false
|
|
223
|
+
console.error('❌ quick 通道已退役,不再接受新会话(v3.31.0 起)。')
|
|
224
|
+
console.error(' 新工作请走轻量变更:sillyspec flow start --change <名> --input "<动机与背景;随后独立一行『成功标准:』;再每行一条『- <可验证标准>』>"')
|
|
225
|
+
console.error(' 升级前进行中的 quick 会话仍可收尾:sillyspec run quick --change <会话ID> 续跑 / --done 收口 / --cancel 取消。')
|
|
226
|
+
process.exit(1)
|
|
227
|
+
}
|
|
228
|
+
|
|
199
229
|
async function main() {
|
|
200
230
|
const args = process.argv.slice(2);
|
|
201
231
|
|
|
@@ -211,7 +241,7 @@ async function main() {
|
|
|
211
241
|
|
|
212
242
|
// E22:重路径统一加载(轻路径 --version/help 已早退,未付此税)。
|
|
213
243
|
const { ProgressManager, resolvePlatformSpecDir, resolvePlatformOpts } = await import('./progress.js');
|
|
214
|
-
const { didYouMean, assertSafeChangeName, assertDatedChangeName, resolveSpecDir, detectWorktreeSpecDrift, detectCwdInsideWorktree } = await import('./run/shared.js');
|
|
244
|
+
const { didYouMean, assertSafeChangeName, assertDatedChangeName, resolveSpecDir, detectWorktreeSpecDrift, detectCwdInsideWorktree, ancestorSpecDirs } = await import('./run/shared.js');
|
|
215
245
|
|
|
216
246
|
// 解析全局选项
|
|
217
247
|
let json = false;
|
|
@@ -222,6 +252,7 @@ async function main() {
|
|
|
222
252
|
// --tool 多值收集(逗号分隔 + 重复 flag);空数组 = 未提供,cmdInit 侧自动检测
|
|
223
253
|
const toolValues = [];
|
|
224
254
|
let interactive = false;
|
|
255
|
+
let forceCards = false;
|
|
225
256
|
// init 专属:--no-skills 跳过 skills 复制段(platform init 勿污染项目内工具目录)
|
|
226
257
|
let noSkills = false;
|
|
227
258
|
// 平台模式 flag(init 落平台指针用;非 init 命令时忽略——runCommand 自行解析 filteredArgs)
|
|
@@ -247,6 +278,9 @@ async function main() {
|
|
|
247
278
|
if (t) toolValues.push(t);
|
|
248
279
|
}
|
|
249
280
|
i++;
|
|
281
|
+
} else if (args[i] === '--force-cards') {
|
|
282
|
+
// 流程命令卡强制覆盖(2026-09-21-flow-command-cards):跳过手改保护分支
|
|
283
|
+
forceCards = true;
|
|
250
284
|
} else if (args[i] === '--no-skills') {
|
|
251
285
|
// init 专属:跳过 skills 复制段(吞进变量,不透传 filteredArgs)
|
|
252
286
|
noSkills = true;
|
|
@@ -339,13 +373,19 @@ async function main() {
|
|
|
339
373
|
console.error(` 主仓根:${wtInfo.mainRepoRoot}`);
|
|
340
374
|
console.error(' SillySpec 进度库只存在于主仓,worktree 内跑会产生分裂进度(坑 worktree-cwd-silent-split)。');
|
|
341
375
|
console.error(' 修复:cd 到主仓根后重跑,或用 --dir <主仓根>,或用 --allow-worktree-cwd 强制放行。');
|
|
376
|
+
// 一键放行整行(R4-S-Q 实证:agent 试 3 次才拼对组合——worktree 守卫要 --allow-worktree-cwd,
|
|
377
|
+
// 而 worktree 嵌套在主仓 .sillyspec/.runtime/ 下又触发 monorepo 多实例拦,需再补 --spec-dir
|
|
378
|
+
// 指向 worktree 自己的 .sillyspec,两 flag 缺一不可。给整行可贴命令,不再让调用方试错拼装。)
|
|
379
|
+
const wtSpecDir = (ancestorSpecDirs(guardDir)[0]) || join(guardDir, '.sillyspec');
|
|
380
|
+
console.error(` 一键放行(本 worktree 作独立实例跑,整行照贴):`);
|
|
381
|
+
console.error(` sillyspec ${args.join(' ')} --allow-worktree-cwd --spec-dir "${wtSpecDir}"`);
|
|
342
382
|
process.exit(2);
|
|
343
383
|
}
|
|
344
384
|
}
|
|
345
385
|
|
|
346
386
|
switch (command) {
|
|
347
387
|
case 'init':
|
|
348
|
-
await (await import('./init.js')).cmdInit(dir, { tool, tools: toolValues.length > 0 ? toolValues : null, interactive, specDir, noSkills, platformOpts: (platformWorkspaceId || platformRuntimeRoot) ? { workspaceId: platformWorkspaceId, runtimeRoot: platformRuntimeRoot } : null });
|
|
388
|
+
await (await import('./init.js')).cmdInit(dir, { tool, tools: toolValues.length > 0 ? toolValues : null, interactive, specDir, noSkills, forceCards, platformOpts: (platformWorkspaceId || platformRuntimeRoot) ? { workspaceId: platformWorkspaceId, runtimeRoot: platformRuntimeRoot } : null });
|
|
349
389
|
break;
|
|
350
390
|
case 'setup':
|
|
351
391
|
const setupList = filteredArgs.includes('--list') || filteredArgs.includes('-l');
|
|
@@ -380,6 +420,22 @@ async function main() {
|
|
|
380
420
|
break;
|
|
381
421
|
}
|
|
382
422
|
pm.show(progDir, progChangeName, { all: filteredArgs.includes('--all') });
|
|
423
|
+
// 预览出口(2026-09-23-watcher-preview-progress task-03 / FR-04):--preview 在默认
|
|
424
|
+
// 输出后追加 watcher 预览段(带「预览」徽标与证据引用);缺省零行为变化(保险丝外显式通道)。
|
|
425
|
+
if (filteredArgs.includes('--preview') && progChangeName) {
|
|
426
|
+
const { readPreviewProgress } = await import('./progress.js');
|
|
427
|
+
const pv = readPreviewProgress(resolvePlatformSpecDir(dir, specDir) || join(progDir, '.sillyspec'), progChangeName);
|
|
428
|
+
console.log('');
|
|
429
|
+
if (!pv.exists || pv.stages.length === 0) {
|
|
430
|
+
console.log('👀 预览:无 watcher 预览行(变更未注册/库缺失/尚无投影)');
|
|
431
|
+
} else {
|
|
432
|
+
console.log(`👀 预览(watcher 推断,authority='watcher'——非流程真相,CLI --done 才是):${pv.stages.length} 行`);
|
|
433
|
+
for (const r of pv.stages) {
|
|
434
|
+
const files = r.evidence && Array.isArray(r.evidence.files) ? r.evidence.files.join('/') : '';
|
|
435
|
+
console.log(` [预览] ${r.stage} → ${r.status}${files ? `(依据:${files}${r.evidence && r.evidence.changed ? ' 本拍有变化' : ''})` : ''}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
383
439
|
break;
|
|
384
440
|
}
|
|
385
441
|
case 'check': {
|
|
@@ -514,6 +570,9 @@ async function main() {
|
|
|
514
570
|
// 只传 cwd 致 --spec-dir 对 gate verify 无效。不传 --spec-dir 时 specBase 走默认
|
|
515
571
|
// resolveSpecDir(cwd),向后兼容。
|
|
516
572
|
const gateOpts = { cwd: dir };
|
|
573
|
+
// P1 --full 只读预检档(batch3 task-03):覆盖 --done 独有检查面(reconcile/stage review)——
|
|
574
|
+
// 预检过=必过(同因),省一轮 gate 失败重跑的整 agent 回合
|
|
575
|
+
if (args.includes('--full')) gateOpts.full = true;
|
|
517
576
|
// A4:平台指针 / 显式 --spec-dir / 本地 fallback 三合一解析 specBase——平台模式(指针指向外部
|
|
518
577
|
// specDir)下 gate 也能核验(原只传 cwd 致 runGate 的 resolveSpecDir 指向本地孤儿库,恒
|
|
519
578
|
// 「无法核验」exit 2)。resolvePlatformSpecDir 指针失效时抛 PointerUnreachableError,由顶层 catch 优雅 fail-closed。
|
|
@@ -554,6 +613,13 @@ async function main() {
|
|
|
554
613
|
// 顶层 errors/warnings 是各 check 的聚合,已在上面按 check 分组显示;不再重复打印——
|
|
555
614
|
// 非 json 是 agent 默认消费路径,重复文本纯耗 context(W1-J)。
|
|
556
615
|
}
|
|
616
|
+
// quick-D(2026-09-20 报告问题 D):gate 信封落稳定路径——失败明细只活在 stdout 时,宿主
|
|
617
|
+
// 转后台截断即丢(实证 agent 靠推断绕 3 轮)。fail-soft;runGate 只读契约不动(落盘归 CLI 层)。
|
|
618
|
+
try {
|
|
619
|
+
const { writeGateResultArtifact } = await import('./machine-interface.js');
|
|
620
|
+
const gateArtifact = writeGateResultArtifact({ specBase: gateSpecBase || gateOpts.specBase || dir, stage: gateStage, changeName: gateChange, envelope });
|
|
621
|
+
if (gateArtifact && !envelope.ok) console.error(`\n📄 gate 失败明细已落盘(稳定路径,stdout 被截断时直读): ${gateArtifact}`);
|
|
622
|
+
} catch { /* fail-soft:落盘失败不影响 gate 结论 */ }
|
|
557
623
|
process.exitCode = exitCode;
|
|
558
624
|
break;
|
|
559
625
|
}
|
|
@@ -764,9 +830,12 @@ async function main() {
|
|
|
764
830
|
if (!taskSub || ['help', '--help', '-h'].includes(taskSub)) {
|
|
765
831
|
console.log(`用法:
|
|
766
832
|
sillyspec task start --change <名> --task task-NN [--note "一句话在做什么"]
|
|
833
|
+
sillyspec task done --change <名> --task task-NN --verdict <pass|fail|cannot_verify> [--notes "评审备注"]
|
|
834
|
+
[--evidence "..."] [--commit -m "提交信息"] [-- <精确路径...>] [--force]
|
|
767
835
|
sillyspec task finish --change <名> --task task-NN
|
|
768
836
|
sillyspec task list --change <名>
|
|
769
|
-
task
|
|
837
|
+
task done 四合一(r5l 方案1):review write(落 review.json+自动勾选)→ finish(清标记)→ 可选 wt-commit,
|
|
838
|
+
子步幂等可断点重入;task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标红 >2h 中断遗留`);
|
|
770
839
|
break;
|
|
771
840
|
}
|
|
772
841
|
const tChangeIdx = filteredArgs.indexOf('--change');
|
|
@@ -788,6 +857,76 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
|
|
|
788
857
|
task: tTask, change: tChange, note: tNote || '', startedAt: new Date().toISOString(),
|
|
789
858
|
}, null, 2) + '\n');
|
|
790
859
|
console.log(`📌 ${tTask} 已标记进行中${tNote ? '(' + tNote + ')' : ''}——完工时 sillyspec task finish --change ${tChange} --task ${tTask}`);
|
|
860
|
+
// ── 受影响测试族前移注入(2026-09-21 R5 接线 ql-20260921-007,②)──
|
|
861
|
+
// deps(auto) 同源计算(verify-postcheck discoverModuleDependentTests)从 quick 收尾门
|
|
862
|
+
// 前移到任务开工:30 秒定向定位替代 100 秒全量轰炸+混合归因(W1 实证全量当首验多跑
|
|
863
|
+
// 2 轮+triage 三轮)。按 task 卡 allowed_paths 声明面计算;best-effort:卡片缺失/
|
|
864
|
+
// 解析失败/零命中零输出零阻断。
|
|
865
|
+
try {
|
|
866
|
+
const tCardPath = join(tSpecBase, 'changes', tChange, 'tasks', `${tTask}.md`);
|
|
867
|
+
if (existsSync(tCardPath)) {
|
|
868
|
+
const { parseAllowedPaths } = await import('./stages/plan-postcheck.js');
|
|
869
|
+
const { discoverModuleDependentTests } = await import('./verify-postcheck.js');
|
|
870
|
+
const tAllowed = parseAllowedPaths(readFileSync(tCardPath, 'utf8'));
|
|
871
|
+
if (tAllowed.length > 0) {
|
|
872
|
+
const tDeps = discoverModuleDependentTests({ cwd: dir, changedFiles: tAllowed, coveredCommands: [] });
|
|
873
|
+
if (tDeps.length > 0) {
|
|
874
|
+
console.log(`🎯 受影响测试族(deps(auto) 同源 ${tDeps.length} 个)——写完先跑定向再全量(30s 定位 < 100s 全量+归因):`);
|
|
875
|
+
console.log(` node --test ${tDeps.slice(0, 10).join(' ')}${tDeps.length > 10 ? ` …等共 ${tDeps.length} 个` : ''}`);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
} catch { /* 注入 best-effort */ }
|
|
880
|
+
} else if (taskSub === 'done') {
|
|
881
|
+
// ── task done 四合一(r5l-forensic-verdict 方案 1 / 评审护栏#3):13 任务 × 4 连收尾
|
|
882
|
+
// 往返(review write+勾选 / finish / wt-commit)合并为单进程串行 + 合并输出——每次省
|
|
883
|
+
// 一次 ~300K 全量上下文重发。原子性:子步幂等跳过 / 中断精确报告已完成子步 / 重入断点
|
|
884
|
+
// 续;ownership 与拒覆盖语义继承 writeTaskReview / runWtCommit 既有判定(编排零旁路)。──
|
|
885
|
+
const tdVal = (flag) => {
|
|
886
|
+
const i = filteredArgs.indexOf(flag);
|
|
887
|
+
return i >= 0 && filteredArgs[i + 1] && !String(filteredArgs[i + 1]).startsWith("--") ? filteredArgs[i + 1] : null;
|
|
888
|
+
};
|
|
889
|
+
const tdVerdict = tdVal('--verdict');
|
|
890
|
+
const tdNotes = tdVal('--notes');
|
|
891
|
+
const tdEvidence = tdVal('--evidence');
|
|
892
|
+
const tdCommit = filteredArgs.includes('--commit');
|
|
893
|
+
const tdMessage = tdVal('-m') || tdVal('--message');
|
|
894
|
+
const tdBase = tdVal('--base');
|
|
895
|
+
const tdHead = tdVal('--head');
|
|
896
|
+
const tdCfIdx = filteredArgs.indexOf('--changed-files');
|
|
897
|
+
// 存在即显式覆盖(含空串=纯验证任务零 diff 声明)——与 review write 同语义
|
|
898
|
+
const tdFilesOverride = tdCfIdx >= 0
|
|
899
|
+
? (filteredArgs[tdCfIdx + 1] || '').split(',').map((x) => x.trim()).filter(Boolean)
|
|
900
|
+
: null;
|
|
901
|
+
const tdPsIdx = filteredArgs.indexOf('--pathspec-from-file');
|
|
902
|
+
const tdPsFile = tdPsIdx >= 0 && filteredArgs[tdPsIdx + 1] ? filteredArgs[tdPsIdx + 1] : null;
|
|
903
|
+
const tdDd = filteredArgs.indexOf('--');
|
|
904
|
+
const tdPaths = tdDd === -1 ? [] : filteredArgs.slice(tdDd + 1);
|
|
905
|
+
if (!tdVerdict || (tdCommit && !tdMessage)) {
|
|
906
|
+
console.error('用法: sillyspec task done --change <名> --task task-NN --verdict <pass|fail|cannot_verify> [--notes "评审备注"] [--evidence "cannot_verify 证据"]\n [--commit -m "提交信息"] [--pathspec-from-file <f>] [-- <精确路径...>] [--force]\n 四合一收尾:review write(落 review.json+自动勾选)→ task finish(清进行中标记)→ 可选 wt-commit;\n 子步幂等可断点重入;verdict 改判已存在 review 需 --force(与 review write 拒覆盖同款)');
|
|
907
|
+
process.exit(2);
|
|
908
|
+
}
|
|
909
|
+
if (tdCommit && tdPaths.length === 0 && !tdPsFile) {
|
|
910
|
+
console.error('❌ --commit 需要提交路径:`--` 后逐个给出本 task 的精确路径(或 --pathspec-from-file <f>)——wt-commit 刻意不支持空 pathspec(同 Wave 兄弟 WIP 防卷入)');
|
|
911
|
+
process.exit(2);
|
|
912
|
+
}
|
|
913
|
+
const tdPlatformOpts = {};
|
|
914
|
+
const tdResolved = resolvePlatformOpts(dir, specDir);
|
|
915
|
+
if (tdResolved) {
|
|
916
|
+
tdPlatformOpts.specRoot = tdResolved.specRoot;
|
|
917
|
+
if (tdResolved.runtimeRoot) tdPlatformOpts.runtimeRoot = tdResolved.runtimeRoot;
|
|
918
|
+
}
|
|
919
|
+
const { runTaskDone } = await import('./task-done.js');
|
|
920
|
+
const tdResult = await runTaskDone({
|
|
921
|
+
changeName: tChange, cwd: dir, taskId: tTask, verdict: tdVerdict,
|
|
922
|
+
notes: tdNotes || '', evidence: tdEvidence,
|
|
923
|
+
commitMessage: tdCommit ? tdMessage : null,
|
|
924
|
+
pathspecs: tdPaths, pathspecFile: tdPsFile,
|
|
925
|
+
markerDir: tDir, platformOpts: tdPlatformOpts,
|
|
926
|
+
baseOverride: tdBase, headOverride: tdHead, changedFilesOverride: tdFilesOverride,
|
|
927
|
+
force: filteredArgs.includes('--force'),
|
|
928
|
+
});
|
|
929
|
+
if (!tdResult.ok) process.exit(1);
|
|
791
930
|
} else if (taskSub === 'finish') {
|
|
792
931
|
try { unlinkSync(join(tDir, `${tTask}.json`)); console.log(`✅ ${tTask} 进行中标记已清除(完工)`); }
|
|
793
932
|
catch { console.log(`ℹ️ ${tTask} 无进行中标记(已完工或从未 start)`); }
|
|
@@ -808,7 +947,7 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
|
|
|
808
947
|
}
|
|
809
948
|
if (stale > 0) console.log(` 共 ${stale} 个疑似中断遗留——主代理接管时逐一审查(半成品无主的痛点出口)`);
|
|
810
949
|
} else {
|
|
811
|
-
console.error(`❌ 未知子命令「${taskSub}」(start | finish | list)`); process.exit(2);
|
|
950
|
+
console.error(`❌ 未知子命令「${taskSub}」(start | done | finish | list)`); process.exit(2);
|
|
812
951
|
}
|
|
813
952
|
break;
|
|
814
953
|
}
|
|
@@ -1007,7 +1146,7 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
|
|
|
1007
1146
|
const { spawnSync } = await import('node:child_process');
|
|
1008
1147
|
const { fileURLToPath } = await import('node:url');
|
|
1009
1148
|
// ESM 无 __dirname(ql-20260911-029:此处曾直接引用,--apply 常态路径必 ReferenceError)
|
|
1010
|
-
const r = spawnSync(process.execPath, [fileURLToPath(new URL('../bin/sillyspec.js', import.meta.url)), '--dir', dir, ...childArgs], { stdio: 'inherit' });
|
|
1149
|
+
const r = spawnSync(process.execPath, [fileURLToPath(new URL('../bin/sillyspec.js', import.meta.url)), '--dir', dir, ...childArgs], { stdio: 'inherit', windowsHide: true });
|
|
1011
1150
|
process.exit(r.status === null ? 1 : r.status);
|
|
1012
1151
|
}
|
|
1013
1152
|
if (json) {
|
|
@@ -1114,12 +1253,21 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
|
|
|
1114
1253
|
// 六探针中的纯机械四个(未实现标记 grep / 测试文件递归查找 / API 契约对账 / 删除对账)
|
|
1115
1254
|
// 一条命令跑完并渲染成可直接粘贴的 markdown;半语义探针(2/4 + 3.4/3.5)显式留 TODO。
|
|
1116
1255
|
// --init 顺带生成 verify-result.md 七章节骨架(探针结果预填;结论留「待填」——gate 找
|
|
1117
|
-
// 不到 PASS/FAIL
|
|
1256
|
+
// 不到 PASS/FAIL 即判不过,骨架不能直接过门)。已存在不覆盖;--force 覆盖重生成全骨架
|
|
1257
|
+
// (quick-B,2026-09-20 报告问题 B:骨架 :2571 指引曾承诺「重跑 --init 刷新本段」但已存在
|
|
1258
|
+
// 场景实为 no-op——刷新通道缺失;design-init --force 同款先例)。
|
|
1118
1259
|
const vpChangeIdx = args.indexOf('--change');
|
|
1119
1260
|
const vpChange = vpChangeIdx >= 0 && args[vpChangeIdx + 1] && !String(args[vpChangeIdx + 1]).startsWith("--") ? args[vpChangeIdx + 1] : null;
|
|
1120
1261
|
const vpInit = args.includes('--init');
|
|
1262
|
+
const vpForce = args.includes('--force');
|
|
1263
|
+
const vpDraft = args.includes('--draft');
|
|
1264
|
+
const vpAmendDraft = args.includes('--amend-draft');
|
|
1265
|
+
if (vpDraft && !vpInit) {
|
|
1266
|
+
console.error('❌ --draft 需与 --init 同用(draft 是骨架生成的机器起草模式)——sillyspec verify-probes --change <名> --init --draft');
|
|
1267
|
+
process.exit(2);
|
|
1268
|
+
}
|
|
1121
1269
|
if (!vpChange) {
|
|
1122
|
-
console.error('用法: sillyspec verify-probes --change <name> [--init] [--json] [--spec-dir <path>]\n 跑机械探针(TODO 标记/测试覆盖/API
|
|
1270
|
+
console.error('用法: sillyspec verify-probes --change <name> [--init [--force]] [--draft] [--amend-draft] [--json] [--spec-dir <path>]\n 跑机械探针(TODO 标记/测试覆盖/API 契约/删除对账)输出 markdown;--init 生成 verify-result.md 骨架(探针预填,已存在不覆盖;--force 覆盖重生成全骨架——手填内容会重置,先备份);\n --init --draft 填槽模式(r5l 方案3):任务完成度/风险等级/测试结果/决策追踪四节机器预填完整句子+指纹防篡改,agent 只填三处 <!--AGENT:--> 槽(结论枚举/移交项/审查叙述);\n --amend-draft 留痕重锚:确要修改机器预填段时重算指纹并记 sidecar 审计(--done 篡改门禁的显式例外通道)');
|
|
1123
1271
|
process.exit(2);
|
|
1124
1272
|
}
|
|
1125
1273
|
assertSafeChangeName(vpChange, '--change 变更名');
|
|
@@ -1146,6 +1294,24 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
|
|
|
1146
1294
|
} catch { return null }
|
|
1147
1295
|
}
|
|
1148
1296
|
const vpResult = runVerifyProbes({ cwd: dir, changeName: vpChange, specDir: vpSpecBase });
|
|
1297
|
+
// ── --amend-draft(r5l 方案3/护栏#2:机器段修改的唯一留痕通道)── 对 md 中现存
|
|
1298
|
+
// MACHINE-DRAFT 标记段按当前内容重锚哈希 + sidecar amendment 审计,不跑探针不写骨架。
|
|
1299
|
+
if (vpAmendDraft) {
|
|
1300
|
+
const { resolveRuntimeRoot } = await import('./run/shared.js');
|
|
1301
|
+
const { amendDraftMarkers } = await import('./verify-draft.js');
|
|
1302
|
+
const vpAmRoot = resolveRuntimeRoot({}, vpSpecBase);
|
|
1303
|
+
const vpAmKeys = amendDraftMarkers({
|
|
1304
|
+
mdPath: join(vpSpecBase, 'changes', vpChange, 'verify-result.md'),
|
|
1305
|
+
changeName: vpChange, runtimeRoot: vpAmRoot,
|
|
1306
|
+
});
|
|
1307
|
+
if (vpAmKeys.length > 0) {
|
|
1308
|
+
try { await mirrorInitArtifact(join(vpSpecBase, 'changes', vpChange, 'verify-result.md'), 'verify-result.md', readFileSync(join(vpSpecBase, 'changes', vpChange, 'verify-result.md'), 'utf8')) } catch { /* 镜像失败不阻断 */ }
|
|
1309
|
+
console.log(`✏️ amend-draft 已重锚 ${vpAmKeys.length} 个机器段:${vpAmKeys.join('、')}(sidecar 记 amendment 审计——verify --done 篡改门禁按新指纹放行)`);
|
|
1310
|
+
} else {
|
|
1311
|
+
console.log('ℹ️ 无 MACHINE-DRAFT 标记段可重锚(报告未用 --draft 生成,或标记已全部删除——后者会被 --done 拒收,先恢复标记或重跑 --init --force --draft)');
|
|
1312
|
+
}
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1149
1315
|
if (json) {
|
|
1150
1316
|
console.log(JSON.stringify({ command: 'verify-probes', change: vpChange, ...vpResult }, null, 2));
|
|
1151
1317
|
break;
|
|
@@ -1168,8 +1334,15 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
|
|
|
1168
1334
|
writeFileSync(vpReportPath, vpInjected);
|
|
1169
1335
|
await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpInjected);
|
|
1170
1336
|
console.log(`\n📄 存量旧格式 verify-result.md 已补注入探针预填段: ${vpReportPath}(正文其余未动;补注后按新预填段如实核对结论)${vpPlatformNote}`);
|
|
1337
|
+
} else if (vpForce) {
|
|
1338
|
+
// quick-B:--force 覆盖重生成全骨架(含接口矩阵等 CLI 预填段)——手填内容会重置,
|
|
1339
|
+
// 醒目警告 + 备份指引(预填段过期/骨架仍是「无接口面」注记时的刷新通道)
|
|
1340
|
+
const vpSkeleton = generateVerifyResultSkeleton(vpResult);
|
|
1341
|
+
writeFileSync(vpReportPath, vpSkeleton);
|
|
1342
|
+
await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpSkeleton);
|
|
1343
|
+
console.log(`\n📄 --force 已重生成 verify-result.md 骨架: ${vpReportPath}(探针与预填段已刷新;⚠️ 手填的结论/移交项等已被重置——需保留的内容从 git 或备份找回后回填)${vpPlatformNote}`);
|
|
1171
1344
|
} else {
|
|
1172
|
-
console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}${vpPlatformNote}
|
|
1345
|
+
console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}${vpPlatformNote}(预填段过期需刷新时用 --init --force)`);
|
|
1173
1346
|
}
|
|
1174
1347
|
} else {
|
|
1175
1348
|
const vpSkeleton = generateVerifyResultSkeleton(vpResult);
|
|
@@ -1177,6 +1350,68 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
|
|
|
1177
1350
|
await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpSkeleton);
|
|
1178
1351
|
console.log(`\n📄 已生成 verify-result.md 骨架: ${vpReportPath}(探针已预填;结论必须写明 PASS/FAIL,留待填会被 gate 判不过)${vpPlatformNote}`);
|
|
1179
1352
|
}
|
|
1353
|
+
// ── --draft 填槽模式(r5l 方案3/护栏#2,v2 件二)── 四节机器预填完整句子 + 指纹防篡改。
|
|
1354
|
+
// 位置刻意在 backfill/ensure/inject 三 enricher 之前:决策追踪矩阵由本段先落(含标记),
|
|
1355
|
+
// injectDecisionChainDraft 随后见表格自然 no-op(幂等两不误)。材料与 verify 门对账同源:
|
|
1356
|
+
// 测试结果优先级 P2 账本 > 质量扫描记录(gates.js 同序);风险判级 evaluateConclusionDraft
|
|
1357
|
+
// 同款口径(detectChangeRisk 词表已退役 D-008)。幂等:段内非 TODO 占位(已手写/已
|
|
1358
|
+
// draft 化)零改动;平台模式 sidecar 随 vpSpecBase 锚定(门禁侧 not-applicable 即降级放行,
|
|
1359
|
+
// 不产生假红)。
|
|
1360
|
+
if (vpDraft && existsSync(vpReportPath)) {
|
|
1361
|
+
try {
|
|
1362
|
+
const { resolveRuntimeRoot } = await import('./run/shared.js');
|
|
1363
|
+
const { summarizeTaskCompletion } = await import('./task-review.js');
|
|
1364
|
+
const { buildDecisionChainMatrix } = await import('./verify-probes.js');
|
|
1365
|
+
const { parseFileChangeListDetailed } = await import('./change-list.js');
|
|
1366
|
+
const { loadBlastDeclarationsAllProjects } = await import('./blast-surface.js');
|
|
1367
|
+
const { resolveChangeRisk, extractExplicitRiskLevel } = await import('./change-risk-profile.js');
|
|
1368
|
+
const { consultTestLedger } = await import('./run/test-ledger.js');
|
|
1369
|
+
const { loadReusableQualityScan } = await import('./run/verify-quality-scan.js');
|
|
1370
|
+
const { buildDraftSections, applyDraftMode } = await import('./verify-draft.js');
|
|
1371
|
+
const vpChangeDir = join(vpSpecBase, 'changes', vpChange);
|
|
1372
|
+
const vpRuntimeRoot = resolveRuntimeRoot({}, vpSpecBase);
|
|
1373
|
+
// ① 任务完成度(summarizeTaskCompletion 同源:review verdict 真源 + 降级说明句)
|
|
1374
|
+
const vpSummarize = summarizeTaskCompletion({ changeDir: vpChangeDir, runtimeRoot: vpRuntimeRoot, changeName: vpChange });
|
|
1375
|
+
// ② 风险等级(evaluateConclusionDraft 同款输入面;无清单 → null 走降级句)
|
|
1376
|
+
let vpRisk = null;
|
|
1377
|
+
try {
|
|
1378
|
+
const vpDesignPath = join(vpChangeDir, 'design.md');
|
|
1379
|
+
const vpDesignContent = existsSync(vpDesignPath) ? readFileSync(vpDesignPath, 'utf8') : '';
|
|
1380
|
+
const vpDeclared = parseFileChangeListDetailed(vpDesignPath, { keepSillyspecDocs: true }).map((e) => e.path);
|
|
1381
|
+
if (vpDeclared.length > 0) {
|
|
1382
|
+
vpRisk = resolveChangeRisk({
|
|
1383
|
+
files: vpDeclared,
|
|
1384
|
+
blastDeclarations: loadBlastDeclarationsAllProjects({ specBase: vpSpecBase }),
|
|
1385
|
+
explicitRiskLevel: vpDesignContent ? extractExplicitRiskLevel(vpDesignContent) : null,
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
} catch { /* 风险材料不可得 → 降级句(完整句子,不空段) */ }
|
|
1389
|
+
// ③ 测试结果(与 verify 门对账同源优先级:P2 账本 > 扫描记录 > 无记录提示句)
|
|
1390
|
+
let vpTest = null;
|
|
1391
|
+
try {
|
|
1392
|
+
const vpConsult = consultTestLedger({ runtimeRoot: vpRuntimeRoot, changeName: vpChange, projectRoot: dir, testRoot: join(dir, 'test'), specBase: vpSpecBase, cwd: dir });
|
|
1393
|
+
if (vpConsult.reuse) vpTest = { source: 'ledger', command: vpConsult.runPlan?.[0]?.runner || 'npm test', status: 'passed', ranAt: vpConsult.result.ranAt, durationMs: vpConsult.result.durationMs, total: vpConsult.result.total ?? null };
|
|
1394
|
+
} catch { /* 账本咨询异常 → 下一源 */ }
|
|
1395
|
+
if (!vpTest) {
|
|
1396
|
+
try {
|
|
1397
|
+
const vpScan = loadReusableQualityScan({ specBase: vpSpecBase, cwd: dir, changeName: vpChange });
|
|
1398
|
+
if (vpScan && vpScan.testResult) vpTest = { source: 'scan', command: vpScan.testResult.command, status: vpScan.testResult.status, ranAt: vpScan.ranAt, durationMs: vpScan.testResult.durationMs, total: vpScan.testResult.total ?? null };
|
|
1399
|
+
} catch { /* 扫描记录不可得 → 提示句 */ }
|
|
1400
|
+
}
|
|
1401
|
+
// ④ 决策追踪(buildDecisionChainMatrix 机械半边——Evidence/状态两列留人工)
|
|
1402
|
+
const vpChain = buildDecisionChainMatrix(vpChangeDir);
|
|
1403
|
+
const vpDraftRes = applyDraftMode({
|
|
1404
|
+
mdPath: vpReportPath, changeName: vpChange, runtimeRoot: vpRuntimeRoot,
|
|
1405
|
+
sections: buildDraftSections({ summarize: vpSummarize, risk: vpRisk, testResult: vpTest, decisionChain: vpChain }),
|
|
1406
|
+
});
|
|
1407
|
+
if (vpDraftRes) {
|
|
1408
|
+
try { await mirrorInitArtifact(vpReportPath, 'verify-result.md', readFileSync(vpReportPath, 'utf8')) } catch { /* 镜像失败不阻断 */ }
|
|
1409
|
+
console.log(`\n📝 填槽模式已应用:${vpDraftRes.applied.length} 节机器预填(${vpDraftRes.applied.join('、')},MACHINE-DRAFT 指纹防篡改——整段改写会被 verify --done 拒收);你只填三处 <!--AGENT:--> 槽(结论枚举/移交项/审查叙述)${vpPlatformNote}`);
|
|
1410
|
+
}
|
|
1411
|
+
} catch (e) {
|
|
1412
|
+
console.warn(`⚠️ --draft 机器起草降级(fail-soft,骨架保持原态): ${e && e.message ? e.message : e}`);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1180
1415
|
const vpFacts = writeVerifyFacts(join(vpSpecBase, 'changes', vpChange), vpResult, vpChange, {
|
|
1181
1416
|
platformNote: formatPlatformPathNote(vpPlatformBase, vpChange, 'verify-facts.json'),
|
|
1182
1417
|
}); // P3b:facts 底稿随 --init 刷新(骨架已存在也刷新;v2 起分段合并保留固化段)
|
|
@@ -1243,6 +1478,15 @@ task 进行中状态标记:开工/完工落 .runtime/task-progress/,list 标
|
|
|
1243
1478
|
}
|
|
1244
1479
|
} catch { /* 草稿预填失败不阻断 --init */ }
|
|
1245
1480
|
}
|
|
1481
|
+
// ── 产物落盘即推平台(2026-09-24 fr-test-readside 断流实证修复④):--init/--force
|
|
1482
|
+
// 写探针报告/骨架/facts 后不触发同步,verify 期步骤与文件迟到平台直到 --done——
|
|
1483
|
+
// 补 triggerSync 与 stage 渲染同拍(bg 异步,不阻塞命令返回)。──
|
|
1484
|
+
if (vpChange && (vpInit || vpForce)) {
|
|
1485
|
+
try {
|
|
1486
|
+
const { triggerSync } = await import('./run/shared.js')
|
|
1487
|
+
triggerSync(dir, vpChange, { specRoot: vpSpecBase })
|
|
1488
|
+
} catch { /* 同步触发失败不阻断探针命令 */ }
|
|
1489
|
+
}
|
|
1246
1490
|
break;
|
|
1247
1491
|
}
|
|
1248
1492
|
case 'review-dispatch': {
|
|
@@ -2481,6 +2725,9 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
2481
2725
|
break
|
|
2482
2726
|
}
|
|
2483
2727
|
const { runCommand } = await import('./run.js')
|
|
2728
|
+
// quick 通道退役预门(2026-09-25-quick-channel-retire):`run quick` 新会话形态在进
|
|
2729
|
+
// runCommand(及其前置副作用:sid 生成/owner.json/已建立公告)之前拒绝
|
|
2730
|
+
if (filteredArgs[1] === 'quick') refuseRetiredQuickFreshStart(filteredArgs.slice(1))
|
|
2484
2731
|
// 平台模式(--spec-dir 已指定)时,--dir 是明确的 source_root,不应被 resolveEffectiveDir 纠正
|
|
2485
2732
|
const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
|
|
2486
2733
|
// 下行 pull:对齐顶层 stage 别名块(task-10 / D-009 / FR-04)。修复前 case 'run' 漏接,
|
|
@@ -2757,8 +3004,11 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
2757
3004
|
case 'verify':
|
|
2758
3005
|
case 'auto':
|
|
2759
3006
|
case 'archive': {
|
|
2760
|
-
const { runCommand } = await import('./run.js')
|
|
2761
3007
|
const stageArgs = [command, ...filteredArgs.slice(1)]
|
|
3008
|
+
// quick 通道退役预门(2026-09-25-quick-channel-retire):顶层 quick 别名的新会话形态
|
|
3009
|
+
// 在进 runCommand(及其前置副作用)之前拒绝
|
|
3010
|
+
if (command === 'quick') refuseRetiredQuickFreshStart(stageArgs)
|
|
3011
|
+
const { runCommand } = await import('./run.js')
|
|
2762
3012
|
const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
|
|
2763
3013
|
// 下行 pull:stage 命令(run/--done,含 archive)启动时拉一次(task-10 / D-009 / FR-04)
|
|
2764
3014
|
// 不在每步 pull,仅低频边界点;未连接平台静默跳过
|
|
@@ -2769,7 +3019,182 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
|
|
|
2769
3019
|
}
|
|
2770
3020
|
case 'knowledge': {
|
|
2771
3021
|
const { cmdKnowledge } = await import('./stages/knowledge.js')
|
|
2772
|
-
await cmdKnowledge(filteredArgs.slice(1), specDir ? dir : resolveEffectiveDir(dir), { specDir })
|
|
3022
|
+
await cmdKnowledge(filteredArgs.slice(1), specDir ? dir : resolveEffectiveDir(dir), { specDir, json })
|
|
3023
|
+
break
|
|
3024
|
+
}
|
|
3025
|
+
// ── sillyspec tests(2026-09-24-fr-test-bindings task-02,fr-test-binding §3.2/§5):
|
|
3026
|
+
// 绑定面视图 + 唯一合法修理工。锚可解析+路径存在硬校验——修理工不得制造悬空。──
|
|
3027
|
+
case 'tests': {
|
|
3028
|
+
const {
|
|
3029
|
+
queryByAnchor, queryByChange, upsertFrBindings, upsertQlBindings,
|
|
3030
|
+
unbindFrRows, unbindQlRows, anchorResolvable, normalizeRow, testAnchorFile,
|
|
3031
|
+
readFrBindings, upsertFrBindingsRaw, resolveTestFileRel,
|
|
3032
|
+
} = await import('./test-bindings.js')
|
|
3033
|
+
const effDir = specDir ? dir : resolveEffectiveDir(dir)
|
|
3034
|
+
const specBase = specDir || join(effDir, '.sillyspec')
|
|
3035
|
+
const knowledgeRoot = join(specBase, 'knowledge')
|
|
3036
|
+
// ── repair-paths(2026-09-26-dynamic-test-inference):存量绑定路径错形修复 ──
|
|
3037
|
+
// 三形态混杂(根相对 ✓ / 丢子项目前缀的 cwd 相对 / 裸文件名——平台仓实证)统一归一为
|
|
3038
|
+
// 仓根相对。缺省干跑预览,--write 落盘(upsertFrBindingsRaw 权威重写,路径级修复不触
|
|
3039
|
+
// 行语义)。逐条经硬校验(resolveTestFileRel 只认磁盘在场文件),修复工不得制造悬空。
|
|
3040
|
+
if (filteredArgs.slice(1).includes('repair-paths')) {
|
|
3041
|
+
const write = filteredArgs.includes('--write')
|
|
3042
|
+
const frDir = join(knowledgeRoot, 'fr')
|
|
3043
|
+
if (!existsSync(frDir)) { console.error(`❌ knowledge/fr 不存在(${frDir})——本仓无 FR 绑定面`); process.exitCode = 1; break }
|
|
3044
|
+
let plannedRows = 0, plannedPaths = 0, unresolvable = []
|
|
3045
|
+
const filePlan = []
|
|
3046
|
+
for (const f of readdirSync(frDir).filter(x => x.endsWith('.md')).sort()) {
|
|
3047
|
+
const p = join(frDir, f)
|
|
3048
|
+
const lines = readFileSync(p, 'utf8').split(/\r?\n/)
|
|
3049
|
+
const frIds = [...new Set(lines.filter(l => /^## (FR-[\w-]+-\d+)\s/.test(l)).map(l => l.match(/^## (FR-[\w-]+-\d+)/)[1]))]
|
|
3050
|
+
for (const frId of frIds) {
|
|
3051
|
+
const rows = readFrBindings({ knowledgeRoot, frId })
|
|
3052
|
+
let rowChanged = 0
|
|
3053
|
+
const fixedRows = rows.map(r => {
|
|
3054
|
+
const fixedTests = (r.tests || []).map(t => {
|
|
3055
|
+
const filePart = testAnchorFile(String(t))
|
|
3056
|
+
const anchorPart = String(t).slice(filePart.length)
|
|
3057
|
+
if (existsSync(resolve(effDir, filePart.replace(/\\/g, '/')))) return String(t) // 已是仓根相对
|
|
3058
|
+
const rel = resolveTestFileRel(filePart, { projectRoot: effDir })
|
|
3059
|
+
if (rel) { plannedPaths++; return rel + anchorPart }
|
|
3060
|
+
unresolvable.push(`${frId}:${t}`)
|
|
3061
|
+
return String(t)
|
|
3062
|
+
})
|
|
3063
|
+
if (JSON.stringify(fixedTests) !== JSON.stringify(r.tests || [])) { rowChanged++; return { ...r, tests: fixedTests } }
|
|
3064
|
+
return r
|
|
3065
|
+
})
|
|
3066
|
+
if (rowChanged > 0) { plannedRows += rowChanged; filePlan.push({ file: f, frId, rows: fixedRows }) }
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
if (plannedPaths === 0) {
|
|
3070
|
+
console.log(`✅ 绑定路径无需修复(fr/*.md 全部条目 tests 均可自仓根解析${unresolvable.length ? `;${unresolvable.length} 个无法定位需人工核(--write 也只保留原值):${unresolvable.slice(0, 5).join('、')}${unresolvable.length > 5 ? ' 等' : ''}` : ''})`)
|
|
3071
|
+
break
|
|
3072
|
+
}
|
|
3073
|
+
console.log(`🔧 绑定路径修复${write ? '' : '预览(--write 落盘)'}:${plannedRows} 行 / ${plannedPaths} 个路径归一为仓根相对,涉及 ${filePlan.length} 个条目`)
|
|
3074
|
+
for (const fp of filePlan.slice(0, 10)) console.log(` ${fp.file} :: ${fp.frId}`)
|
|
3075
|
+
if (filePlan.length > 10) console.log(` … 共 ${filePlan.length} 个条目`)
|
|
3076
|
+
if (unresolvable.length > 0) console.warn(`⚠️ ${unresolvable.length} 个路径无法自仓根定位(保留原值,人工核):${unresolvable.slice(0, 5).join('、')}${unresolvable.length > 5 ? ' 等' : ''}`)
|
|
3077
|
+
if (!write) break
|
|
3078
|
+
for (const fp of filePlan) upsertFrBindingsRaw({ knowledgeRoot, frId: fp.frId, rows: fp.rows })
|
|
3079
|
+
console.log(`✅ 已写入 ${filePlan.length} 个条目的归一路径`)
|
|
3080
|
+
break
|
|
3081
|
+
}
|
|
3082
|
+
const flag = (name) => { const i = filteredArgs.indexOf(name); return i !== -1 && i + 1 < filteredArgs.length ? filteredArgs[i + 1] : null }
|
|
3083
|
+
const has = (name) => filteredArgs.includes(name)
|
|
3084
|
+
const anchor = flag('--anchor')
|
|
3085
|
+
const change = flag('--change')
|
|
3086
|
+
const rowId = flag('--row-id')
|
|
3087
|
+
const testsArg = flag('--tests')
|
|
3088
|
+
const reason = flag('--reason') || 'spec'
|
|
3089
|
+
const fail = (msg) => { console.error(`❌ ${msg}`); process.exitCode = 1 }
|
|
3090
|
+
// repair-paths/--redomain 自带用法门,不走 anchor/change 前置守卫
|
|
3091
|
+
if (!anchor && !change && !has('--repair-paths') && !filteredArgs.slice(1).includes('repair-paths') && !has('--redomain')) { fail('用法: sillyspec tests --anchor <FR-…|ql-…> | --change <名> [--bind|--unbind] [--tests <p1,p2>] [--row-id <id>] [--reason spec|capability|regression] | repair-paths | --redomain --from <域> --to <域>'); break }
|
|
3092
|
+
// 变更期局部锚(FR-NN 未铸全局)只读拒绝修改——修理工仅面向提升后行
|
|
3093
|
+
const isLocalAnchor = anchor && /^FR-\d+$/.test(anchor)
|
|
3094
|
+
if ((has('--bind') || has('--unbind')) && isLocalAnchor) { fail(`变更期局部锚「${anchor}」只读——修理工仅面向提升后的全局锚(FR-<域>-NNN / ql-…)`); break }
|
|
3095
|
+
if (has('--bind') || has('--unbind')) {
|
|
3096
|
+
if (!anchor) { fail('--bind/--unbind 须配 --anchor'); break }
|
|
3097
|
+
if (!anchorResolvable({ specBase, knowledgeRoot, anchor })) { fail(`锚不可解析「${anchor}」(FR=活库条目 / ql=QUICKLOG 在册)——修理工不得制造悬空锚`); break }
|
|
3098
|
+
}
|
|
3099
|
+
if (has('--bind')) {
|
|
3100
|
+
const tests = (testsArg || '').split(',').map(s => s.trim()).filter(Boolean)
|
|
3101
|
+
if (tests.length === 0) { fail('--bind 须配 --tests <p1,p2>(仓根相对路径)'); break }
|
|
3102
|
+
const missing = tests.filter(p => !existsSync(resolve(effDir, p)))
|
|
3103
|
+
if (missing.length > 0) { fail(`tests 路径不存在:${missing.join('、')}(相对 ${effDir})——修理工不得制造悬空路径`); break }
|
|
3104
|
+
const head = (() => { try { return gitQuiet(effDir, ['rev-parse', 'HEAD']) || null } catch { return null } })()
|
|
3105
|
+
const row = { anchor, row_id: rowId || `manual:${Date.now().toString(36)}:${tests[0]}`, tests, reason, state: 'active', discovery: 'agent', confirmed_by: 'agent', confirmed_at: head, source_change: change || 'manual' }
|
|
3106
|
+
try {
|
|
3107
|
+
normalizeRow(row)
|
|
3108
|
+
if (/^FR-/.test(anchor)) upsertFrBindings({ knowledgeRoot, frId: anchor, rows: [row], projectRoot: effDir })
|
|
3109
|
+
else upsertQlBindings({ specBase, qlId: anchor, rows: [row] })
|
|
3110
|
+
console.log(`✅ 绑定写入:${anchor} ← ${tests.join(', ')}(confirmed_by=agent${head ? ` @${head.slice(0, 8)}` : ''})`)
|
|
3111
|
+
} catch (e) { fail(`绑定写入被拒:${e && e.message ? e.message : e}`) }
|
|
3112
|
+
break
|
|
3113
|
+
}
|
|
3114
|
+
if (has('--unbind')) {
|
|
3115
|
+
const ids = rowId ? [rowId] : (testsArg || '').split(',').map(s => s.trim()).filter(Boolean)
|
|
3116
|
+
if (ids.length === 0) { fail('--unbind 须配 --row-id <id> 或 --tests <p1,…>'); break }
|
|
3117
|
+
if (/^FR-/.test(anchor)) {
|
|
3118
|
+
const cur = queryByAnchor({ specBase, knowledgeRoot, anchor })
|
|
3119
|
+
// tests 条目可携带用例锚(2026-09-26-binding-anchor-fidelity)——给路径也能命中带锚行
|
|
3120
|
+
const del = cur.filter(r => ids.includes(r.row_id) || (testsArg && ids.length === 1 && r.tests.some(t => ids.includes(t) || ids.includes(testAnchorFile(t))))).map(r => r.row_id)
|
|
3121
|
+
if (del.length === 0) { fail(`未命中可解绑行(anchor=${anchor},给=${ids.join(',')})`); break }
|
|
3122
|
+
unbindFrRows({ knowledgeRoot, frId: anchor, rowIds: del })
|
|
3123
|
+
console.log(`✅ 解绑 ${del.length} 行(${anchor})`)
|
|
3124
|
+
} else {
|
|
3125
|
+
const cur = queryByAnchor({ specBase, knowledgeRoot, anchor })
|
|
3126
|
+
const del = cur.filter(r => ids.includes(r.row_id)).map(r => r.row_id)
|
|
3127
|
+
if (del.length === 0) { fail(`未命中可解绑行(anchor=${anchor})`); break }
|
|
3128
|
+
unbindQlRows({ specBase, qlId: anchor, rowIds: del })
|
|
3129
|
+
console.log(`✅ 解绑 ${del.length} 行(${anchor})`)
|
|
3130
|
+
}
|
|
3131
|
+
break
|
|
3132
|
+
}
|
|
3133
|
+
// ── confirm(2026-09-27-confirm-on-use 三层治理①层):消费时确认翻牌 ──
|
|
3134
|
+
// candidate 机器行(confirmed_by=null)批量翻 active(confirmed_by=agent)——抽查式
|
|
3135
|
+
// 确认的写回通道。机械防橡皮图章:--evidence 必须是可自仓根解析的真实测试路径
|
|
3136
|
+
// (resolveTestFileRel 单源),解析失败拒绝 exit 1。
|
|
3137
|
+
if (has('--confirm')) {
|
|
3138
|
+
if (!anchor || !/^FR-/.test(anchor)) { fail('用法: sillyspec tests --confirm --anchor FR-<域>-NNN --evidence <真实测试路径>'); break }
|
|
3139
|
+
const evidenceRaw = flag('--evidence')
|
|
3140
|
+
if (!evidenceRaw) { fail('--confirm 须配 --evidence <真实测试路径>(防橡皮图章:证据必须是盘上文件)'); break }
|
|
3141
|
+
const { resolveTestFileRel: _rtf2 } = await import('./test-bindings.js')
|
|
3142
|
+
const evRel = _rtf2(testAnchorFile(evidenceRaw), { projectRoot: effDir }) || (existsSync(resolve(effDir, evidenceRaw.replace(/\\/g, '/'))) ? evidenceRaw.replace(/\\/g, '/') : null)
|
|
3143
|
+
// 评审 P2-1 清偿:证据须为测试形态文件(*.test.* / *.spec.* / test_*.py / *_test.*)——
|
|
3144
|
+
// resolveTestFileRel 直取分支无形态检查(package.json/win.ini 实证可绕),此处收口
|
|
3145
|
+
const evBase = evRel ? evRel.split('/').pop() : ''
|
|
3146
|
+
const EV_TEST_SHAPE_RE = new RegExp('\\.(test|spec)\\.[cm]?[jt]sx?$')
|
|
3147
|
+
const evShaped = !!evRel && (EV_TEST_SHAPE_RE.test(evBase) || /^test_[\w.]+\.py$/.test(evBase) || /_test\.(go|py)$/.test(evBase))
|
|
3148
|
+
if (!evShaped) { fail(`证据必须是盘上真实测试文件(*.test.* / test_*.py 等形态):${evidenceRaw}——不收口头相符,不收非测试文件`); break }
|
|
3149
|
+
const rows = readFrBindings({ knowledgeRoot, frId: anchor })
|
|
3150
|
+
if (rows.length === 0) { fail(`${anchor} 无绑定行——确认对象不存在(先 --bind 或经归档提升)`); break }
|
|
3151
|
+
const head2 = (() => { try { return gitQuiet(effDir, ['rev-parse', 'HEAD']) || null } catch { return null } })()
|
|
3152
|
+
const confirmed = rows.map((r) => r.confirmed_by === 'agent' ? r : { ...r, state: 'active', confirmed_by: 'agent', confirmed_at: head2 })
|
|
3153
|
+
const flipped = confirmed.filter((r, i) => rows[i].confirmed_by !== 'agent').length
|
|
3154
|
+
if (flipped === 0) { console.log(`ℹ️ ${anchor} 全部绑定行已 active(幂等,无需确认)`); break }
|
|
3155
|
+
upsertFrBindingsRaw({ knowledgeRoot, frId: anchor, rows: confirmed, projectRoot: effDir })
|
|
3156
|
+
console.log(`✅ 确认翻牌:${anchor} ${flipped}/${rows.length} 行 candidate→active(confirmed_by=agent${head2 ? ` @${head2.slice(0, 8)}` : ''};证据=${evRel})`)
|
|
3157
|
+
break
|
|
3158
|
+
}
|
|
3159
|
+
// ── redomain(2026-09-27-redomain,三层治理 v2 ③):伪域条目机器迁移 ──
|
|
3160
|
+
// --from <域> --to <域> [--anchor <FR-id> 可重复];干跑缺省列条目,--write 落盘。
|
|
3161
|
+
// 身份铁律:迁域不换号(绑定/supersede 链靠 ID 寻址);平台伪域卡「一键迁移」的 CLI 端。
|
|
3162
|
+
if (has('--redomain')) {
|
|
3163
|
+
const fromD = flag('--from')
|
|
3164
|
+
const toD = flag('--to')
|
|
3165
|
+
if (!fromD || !toD) { fail('用法: sillyspec tests --redomain --from <源域> --to <目标域> [--anchor <FR-id>] [--write]'); break }
|
|
3166
|
+
// 多 --anchor 收集
|
|
3167
|
+
const anchorList = []
|
|
3168
|
+
for (let i = 0; i < filteredArgs.length - 1; i++) if (filteredArgs[i] === '--anchor') anchorList.push(filteredArgs[i + 1])
|
|
3169
|
+
try {
|
|
3170
|
+
const { planRedomain, redomainFrEntries } = await import('./redomain.js')
|
|
3171
|
+
if (!has('--write')) {
|
|
3172
|
+
const plan = planRedomain({ knowledgeRoot, from: fromD, to: toD, anchors: anchorList.length ? anchorList : null })
|
|
3173
|
+
console.log(`🔧 域迁移预览(--write 落盘):fr/${fromD}.md → fr/${toD}.md,${plan.entries.length} 条`)
|
|
3174
|
+
for (const e of plan.entries.slice(0, 10)) console.log(` ${e.id} ${e.title}`)
|
|
3175
|
+
if (plan.entries.length > 10) console.log(` … 共 ${plan.entries.length} 条`)
|
|
3176
|
+
console.log(` 目标域文件${plan.targetExists ? '在场' : '将新建'};源域${plan.sourceWillDelete ? '将删空壳' : '保留余条'};ID 不变(身份保持)`)
|
|
3177
|
+
break
|
|
3178
|
+
}
|
|
3179
|
+
const r = redomainFrEntries({ knowledgeRoot, from: fromD, to: toD, anchors: anchorList.length ? anchorList : null })
|
|
3180
|
+
console.log(`✅ 域迁移完成:${r.moved.length} 条 fr/${fromD}.md → fr/${toD}.md(ID 不变)`)
|
|
3181
|
+
for (const m of r.moved.slice(0, 10)) console.log(` ${m.id} ${m.title}`)
|
|
3182
|
+
if (r.sourceDeleted) console.log(` 源域空壳已删:fr/${fromD}.md`)
|
|
3183
|
+
if (r.targetCreated) console.log(` 目标域文件新建:fr/${toD}.md`)
|
|
3184
|
+
if (r.indexLineAdded) console.log(` INDEX 路由行已补:${toD}`)
|
|
3185
|
+
} catch (e) { fail(`域迁移被拒:${(e && e.message) || e}`) }
|
|
3186
|
+
break
|
|
3187
|
+
}
|
|
3188
|
+
// 视图
|
|
3189
|
+
const rows = anchor
|
|
3190
|
+
? queryByAnchor({ specBase, knowledgeRoot, anchor })
|
|
3191
|
+
: queryByChange({ specBase, knowledgeRoot, sourceChange: change })
|
|
3192
|
+
if (rows.length === 0) { console.log(`(无绑定行:${anchor || change})`); break }
|
|
3193
|
+
console.log(`${anchor || change} — ${rows.length} 行`)
|
|
3194
|
+
for (const r of rows) {
|
|
3195
|
+
console.log(` ${r.state === 'active' ? '🟢' : '⚪'} ${r.tests.join(', ')}|reason=${r.reason}|discovery=${r.discovery}|confirmed_by=${r.confirmed_by || '-'}${r.confirmed_at ? '@' + String(r.confirmed_at).slice(0, 8) : ''}|status=${r.status}`)
|
|
3196
|
+
console.log(` row=${r.row_id}|source=${r.source_change || '-'}`)
|
|
3197
|
+
}
|
|
2773
3198
|
break
|
|
2774
3199
|
}
|
|
2775
3200
|
case 'dashboard': {
|
|
@@ -4126,6 +4551,12 @@ checkbox 行;depends_on 自动反填行内注解 "(depends_on: task-01,02)";
|
|
|
4126
4551
|
process.exitCode = r.ok ? 0 : 1;
|
|
4127
4552
|
break;
|
|
4128
4553
|
}
|
|
4554
|
+
case 'flow': {
|
|
4555
|
+
// R7 切片二:2-调用轻量协议命令族(start/done/amend-draft)——协议必需交互=2
|
|
4556
|
+
const flowMod = await import('./flow.js');
|
|
4557
|
+
await flowMod.cmdFlow(filteredArgs.slice(1), dir, specDir);
|
|
4558
|
+
break;
|
|
4559
|
+
}
|
|
4129
4560
|
case 'workflow': {
|
|
4130
4561
|
const wfSub = filteredArgs[1];
|
|
4131
4562
|
if (!wfSub || wfSub === 'help' || wfSub === '--help') {
|
|
@@ -4636,8 +5067,158 @@ SillySpec pull — 拉取服务器 spec 快照到本地(X2 / FR-07)
|
|
|
4636
5067
|
}
|
|
4637
5068
|
break;
|
|
4638
5069
|
}
|
|
5070
|
+
case 'watcher': {
|
|
5071
|
+
// 哨兵告警可视化出口(2026-09-23 quick / 1a91587c 后续件):watcher 落的
|
|
5072
|
+
// watcher-events-<change>.jsonl 是事件唯一真相源,此前只能人肉 grep。纯只读查询 +
|
|
5073
|
+
// 可选 --follow 轮询;不改 applySentinelRules、不做平台推送、不加新事件类型。
|
|
5074
|
+
// 子命令面:alerts;预留名(勿占):events(原始流格式化)/ tail(时间窗过滤)。
|
|
5075
|
+
const watcherSub = filteredArgs[1];
|
|
5076
|
+
if (watcherSub !== 'alerts' && watcherSub !== 'timeline') {
|
|
5077
|
+
const sug = didYouMean(watcherSub || '', ['alerts', 'timeline']);
|
|
5078
|
+
console.error(`❌ 未知子命令: watcher ${watcherSub || '(空)'}`);
|
|
5079
|
+
if (sug) console.error(` 你是想输入「watcher ${sug}」吗?`);
|
|
5080
|
+
console.error('用法: sillyspec watcher alerts --change <名> [--all] [--follow] [--runtime-root <路径>]');
|
|
5081
|
+
console.error(' sillyspec watcher timeline --change <名> [--runtime-root <路径>]');
|
|
5082
|
+
process.exit(2);
|
|
5083
|
+
}
|
|
5084
|
+
// ── timeline 子命令(2026-09-26-watcher-timeline):变更合成时间线只读出口 ──
|
|
5085
|
+
// 三源 join(事件流 × tasks.md × git 提交锚)在 src/timeline.js 纯函数层;此处只做
|
|
5086
|
+
// I/O 接线(runtimeRoot 解析与 alerts 同源;change 目录活跃>归档双路径探测)。
|
|
5087
|
+
if (watcherSub === 'timeline') {
|
|
5088
|
+
const tRest = filteredArgs.slice(2);
|
|
5089
|
+
let tChange = null;
|
|
5090
|
+
let tRuntimeRootFlag = null;
|
|
5091
|
+
for (let i = 0; i < tRest.length; i++) {
|
|
5092
|
+
if (tRest[i] === '--change' && tRest[i + 1]) tChange = tRest[++i];
|
|
5093
|
+
else if (tRest[i] === '--runtime-root' && tRest[i + 1]) tRuntimeRootFlag = resolve(tRest[++i]);
|
|
5094
|
+
}
|
|
5095
|
+
if (!tChange) {
|
|
5096
|
+
console.error('❌ watcher timeline 需要 --change <变更名>');
|
|
5097
|
+
console.error('用法: sillyspec watcher timeline --change <名> [--runtime-root <路径>]');
|
|
5098
|
+
process.exit(2);
|
|
5099
|
+
}
|
|
5100
|
+
assertSafeChangeName(tChange, '变更名');
|
|
5101
|
+
const { resolveRuntimeRoot } = await import('./run/shared.js');
|
|
5102
|
+
const { readWatcherEvents } = await import('./watcher.js');
|
|
5103
|
+
const { loadChangeTasks, readBirthTs, resolveCommitAnchors, parseTaskLines, renderTimeline } = await import('./timeline.js');
|
|
5104
|
+
const tSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
|
|
5105
|
+
const tPlatformOpts = resolvePlatformOpts(dir, specDir);
|
|
5106
|
+
const tRuntimeRoot = resolveRuntimeRoot({ runtimeRoot: tRuntimeRootFlag || (tPlatformOpts && tPlatformOpts.runtimeRoot) || null }, tSpecBase);
|
|
5107
|
+
let tRes = readWatcherEvents({ runtimeRoot: tRuntimeRoot, change: tChange });
|
|
5108
|
+
let tSourceNote = '';
|
|
5109
|
+
if (!tRes.exists) {
|
|
5110
|
+
// 归档包烤制副本回退(2026-09-28-archive-timeline-bake):本机 .runtime 无事件流(跨机
|
|
5111
|
+
// clone / 机本位清理)时读归档包内 watcher-events.jsonl 副本——时间线不随 .runtime 存活消失;
|
|
5112
|
+
// 副本也缺失时保持既有 exit 2 提示语义。
|
|
5113
|
+
const tBakedPath = join(tSpecBase, 'changes', 'archive', tChange, 'watcher-events.jsonl');
|
|
5114
|
+
const tBaked = readWatcherEvents({ path: tBakedPath });
|
|
5115
|
+
if (tBaked.exists) {
|
|
5116
|
+
tRes = tBaked;
|
|
5117
|
+
tSourceNote = `(事件源:归档包烤制快照 ${tBakedPath}——本机 .runtime 无事件流)`;
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
if (!tRes.exists) {
|
|
5121
|
+
console.error(`📭 ${tChange} 无事件流(未跑过 watcher),无从合成时间线`);
|
|
5122
|
+
console.error(` 事件流路径:${join(tRuntimeRoot, `watcher-events-${tChange}.jsonl`)};活跃变更跑任意 flow/run 命令会拉起 watcher`);
|
|
5123
|
+
process.exit(2);
|
|
5124
|
+
}
|
|
5125
|
+
const { changeDir, tasksMd } = loadChangeTasks(tSpecBase, tChange);
|
|
5126
|
+
const tasks = tasksMd ? parseTaskLines(tasksMd) : null;
|
|
5127
|
+
let tier = null;
|
|
5128
|
+
if (changeDir) {
|
|
5129
|
+
try {
|
|
5130
|
+
const m = /^tier:\s*(\S+)/m.exec(readFileSync(join(changeDir, 'flow-state.yaml'), 'utf8'));
|
|
5131
|
+
if (m) tier = m[1];
|
|
5132
|
+
} catch { /* 缺 flow-state(头脑风暴预段等)按未知 */ }
|
|
5133
|
+
}
|
|
5134
|
+
const { gitQuiet } = await import('./git-helper.js');
|
|
5135
|
+
const anchors = resolveCommitAnchors(tRes.events, (hash) => {
|
|
5136
|
+
const out = gitQuiet(dir, ['log', '-1', '--format=%h%x1f%aI%x1f%B', hash]);
|
|
5137
|
+
if (typeof out !== 'string' || !out.trim()) return null;
|
|
5138
|
+
const sep = out.indexOf('\x1f');
|
|
5139
|
+
const rest = out.slice(sep + 1);
|
|
5140
|
+
const sep2 = rest.indexOf('\x1f');
|
|
5141
|
+
return { hash: out.slice(0, sep), dateISO: rest.slice(0, sep2), message: rest.slice(sep2 + 1) };
|
|
5142
|
+
});
|
|
5143
|
+
console.log(renderTimeline({ change: tChange, events: tRes.events, tasks, anchors, birthTs: readBirthTs(changeDir), tier }));
|
|
5144
|
+
if (tSourceNote) console.log(tSourceNote);
|
|
5145
|
+
break;
|
|
5146
|
+
}
|
|
5147
|
+
const wRest = filteredArgs.slice(2);
|
|
5148
|
+
let wChange = null;
|
|
5149
|
+
let wRuntimeRootFlag = null;
|
|
5150
|
+
let wAll = false;
|
|
5151
|
+
let wFollow = false;
|
|
5152
|
+
for (let i = 0; i < wRest.length; i++) {
|
|
5153
|
+
if (wRest[i] === '--change' && wRest[i + 1]) wChange = wRest[++i];
|
|
5154
|
+
else if (wRest[i] === '--runtime-root' && wRest[i + 1]) wRuntimeRootFlag = resolve(wRest[++i]);
|
|
5155
|
+
else if (wRest[i] === '--all') wAll = true;
|
|
5156
|
+
else if (wRest[i] === '--follow') wFollow = true;
|
|
5157
|
+
}
|
|
5158
|
+
if (!wChange) {
|
|
5159
|
+
console.error('❌ watcher alerts 需要 --change <变更名>(事件流按变更名分文件)');
|
|
5160
|
+
console.error('用法: sillyspec watcher alerts --change <名> [--all] [--follow] [--runtime-root <路径>]');
|
|
5161
|
+
process.exit(2);
|
|
5162
|
+
}
|
|
5163
|
+
assertSafeChangeName(wChange, '变更名');
|
|
5164
|
+
const { resolveRuntimeRoot } = await import('./run/shared.js');
|
|
5165
|
+
const { readWatcherEvents, watcherEventsPath } = await import('./watcher.js');
|
|
5166
|
+
// runtimeRoot 解析与 watcher.js spawnWatcher 同源:--runtime-root flag > 平台指针
|
|
5167
|
+
// runtimeRoot > specBase/.runtime(resolveRuntimeRoot 单点收敛)。
|
|
5168
|
+
const wSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
|
|
5169
|
+
const wPlatformOpts = resolvePlatformOpts(dir, specDir);
|
|
5170
|
+
const wRuntimeRoot = resolveRuntimeRoot({ runtimeRoot: wRuntimeRootFlag || (wPlatformOpts && wPlatformOpts.runtimeRoot) || null }, wSpecBase);
|
|
5171
|
+
|
|
5172
|
+
const ALERTS_POLL_MS = 2000;
|
|
5173
|
+
const fmtLocal = (ts) => {
|
|
5174
|
+
const d = new Date(Number(ts));
|
|
5175
|
+
if (Number.isNaN(d.getTime())) return String(ts);
|
|
5176
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
5177
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
5178
|
+
};
|
|
5179
|
+
// detail 截 80:超长取前 79 列 + 省略号恰好 80 列
|
|
5180
|
+
const clip80 = (s) => {
|
|
5181
|
+
const t = String(s ?? '');
|
|
5182
|
+
return t.length > 80 ? t.slice(0, 79) + '…' : t;
|
|
5183
|
+
};
|
|
5184
|
+
const fmtRow = (e) => `${fmtLocal(e.ts)} | ${String(e.rule ?? e.kind ?? '').padEnd(12)} | ${clip80(e.detail)}`;
|
|
5185
|
+
const fmtStats = (warnN, evN, badN) => `📊 ${warnN} 条告警 / ${evN} 条事件${badN > 0 ? `(跳过 ${badN} 坏行)` : ''}`;
|
|
5186
|
+
|
|
5187
|
+
let wRes = readWatcherEvents({ runtimeRoot: wRuntimeRoot, change: wChange });
|
|
5188
|
+
if (!wRes.exists) {
|
|
5189
|
+
console.log(`📭 ${wChange} 无事件流(未跑过 watcher): ${watcherEventsPath(wRuntimeRoot, wChange)}`);
|
|
5190
|
+
break;
|
|
5191
|
+
}
|
|
5192
|
+
console.log(`🕒 watcher alerts — ${wChange}(${wRuntimeRoot})`);
|
|
5193
|
+
console.log(`${'本地时间'.padEnd(19)} | ${wAll ? 'rule/kind' : 'rule'.padEnd(12)} | detail`);
|
|
5194
|
+
const shown = wAll ? wRes.events : wRes.warnings;
|
|
5195
|
+
for (const e of shown) console.log(fmtRow(e));
|
|
5196
|
+
console.log(fmtStats(wRes.warnings.length, wRes.events.length, wRes.badLines));
|
|
5197
|
+
if (!wFollow) break;
|
|
5198
|
+
|
|
5199
|
+
// --follow:2s 轮询增量打印(jsonl 单写者 append-only,按 events 序号单调锚定已打印
|
|
5200
|
+
// 水位;坏行不产事件不干扰锚——残行下一轮解析成功后自然补打)。Ctrl-C 打印末次统计退出。
|
|
5201
|
+
console.log('\n⏳ 持续监听中(2s 轮询,Ctrl-C 退出)');
|
|
5202
|
+
const printNew = (from) => {
|
|
5203
|
+
const rows = (wAll ? wRes.events : wRes.warnings).slice(from);
|
|
5204
|
+
for (const e of rows) console.log(fmtRow(e));
|
|
5205
|
+
};
|
|
5206
|
+
let printed = shown.length;
|
|
5207
|
+
process.on('SIGINT', () => {
|
|
5208
|
+
console.log(`\n${fmtStats(wRes.warnings.length, wRes.events.length, wRes.badLines)}(已停止监听)`);
|
|
5209
|
+
process.exit(0);
|
|
5210
|
+
});
|
|
5211
|
+
// eslint-disable-next-line no-constant-condition
|
|
5212
|
+
while (true) {
|
|
5213
|
+
await new Promise((r) => setTimeout(r, ALERTS_POLL_MS));
|
|
5214
|
+
wRes = readWatcherEvents({ runtimeRoot: wRuntimeRoot, change: wChange });
|
|
5215
|
+
if (!wRes.exists) continue; // 文件中途被删:继续等(watcher 重启会重建)
|
|
5216
|
+
printNew(printed);
|
|
5217
|
+
printed = wAll ? wRes.events.length : wRes.warnings.length;
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
4639
5220
|
default: {
|
|
4640
|
-
const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'agent-log', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'change-delete', 'knowledge', 'platform', 'pull', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime'];
|
|
5221
|
+
const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'agent-log', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'change-delete', 'knowledge', 'platform', 'pull', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime', 'watcher'];
|
|
4641
5222
|
const suggestion = didYouMean(command, topCommands);
|
|
4642
5223
|
console.error(`❌ 未知命令: ${command}`);
|
|
4643
5224
|
if (command === '--status') {
|