sillyspec 3.26.14 → 3.27.1
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-archive/SKILL.md +2 -0
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
- package/.claude/skills/sillyspec-commit/SKILL.md +18 -60
- package/.claude/skills/sillyspec-continue/SKILL.md +2 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
- package/.claude/skills/sillyspec-explore/SKILL.md +10 -3
- package/.claude/skills/sillyspec-plan/SKILL.md +10 -2
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -2
- package/.claude/skills/sillyspec-resume/SKILL.md +2 -0
- package/.claude/skills/sillyspec-verify/SKILL.md +4 -0
- package/.claude/skills/sillyspec-workspace/SKILL.md +7 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/packages/dashboard/server/index.js +22 -6
- package/src/change-risk-profile.js +21 -6
- package/src/commit-suggest.js +202 -0
- package/src/config-cat.js +88 -0
- package/src/db.js +14 -3
- package/src/docs-check.js +598 -534
- package/src/doctor-diagnostics.js +102 -1
- package/src/endpoint-extractor.js +21 -2
- package/src/fs-atomic.js +4 -2
- package/src/git-helper.js +73 -5
- package/src/hooks/claude-pre-tool-use.cjs +10 -5
- package/src/hooks/worktree-guard.js +42 -4
- package/src/index.js +557 -12
- package/src/init.js +1 -1
- package/src/local-register.js +98 -0
- package/src/module-impact.js +166 -0
- package/src/module-resolve.js +211 -0
- package/src/modules.js +82 -2
- package/src/progress/change-registry.js +43 -4
- package/src/progress/stage-machine.js +10 -5
- package/src/progress/step-store.js +12 -8
- package/src/progress.js +19 -3
- package/src/quicklog.js +106 -1
- package/src/run/command.js +95 -1
- package/src/run/complete-handlers.js +73 -5
- package/src/run/complete.js +47 -14
- package/src/run/gates.js +144 -17
- package/src/run/multi-repo-context.js +3 -1
- package/src/run/next.js +128 -0
- package/src/run/prompt.js +36 -4
- package/src/run/quick-audit.js +5 -2
- package/src/run/shared.js +48 -20
- package/src/run/stage.js +37 -3
- package/src/scan-postcheck.js +142 -2
- package/src/stage-contract-spec.js +1 -1
- package/src/stage-contract.js +14 -1
- package/src/stage-review.js +15 -6
- package/src/stages/archive.js +4 -2
- package/src/stages/brainstorm.js +1 -0
- package/src/stages/execute.js +275 -44
- package/src/stages/explore.js +11 -2
- package/src/stages/knowledge.js +3 -2
- package/src/stages/quick.js +18 -9
- package/src/stages/scan.js +1 -0
- package/src/stages/verify.js +16 -51
- package/src/sync.js +44 -3
- package/src/task-review.js +273 -11
- package/src/taskcard.js +52 -22
- package/src/verify-postcheck.js +223 -6
- package/src/verify-probes.js +264 -0
- package/src/workspace.js +125 -0
- package/src/worktree-apply.js +43 -11
- package/src/worktree-deps.js +59 -5
- package/src/worktree.js +128 -14
- package/templates/claude-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +1 -1
- package/templates/prompts/verify-probes.md +13 -47
package/src/stages/verify.js
CHANGED
|
@@ -172,11 +172,11 @@ export const definition = {
|
|
|
172
172
|
- Gradle:\`./gradlew :<模块>:test\`
|
|
173
173
|
- npm/pnpm:\`pnpm test --filter=<包名>\` 或 \`npm test -- --testPathPattern=<相关文件>\`
|
|
174
174
|
- Python:\`pytest <变更模块路径>/\`
|
|
175
|
-
3. 如果 local.yaml 有 lint 命令,运行 lint
|
|
175
|
+
3. 如果 local.yaml 有 lint 命令,运行 lint 检查并修复报出的问题(--done 时 CLI 会亲自实测 commands.lint 对账,实测失败会明示)
|
|
176
176
|
4. 搜索技术债务:grep TODO/FIXME/HACK/XXX(仅限变更文件)
|
|
177
177
|
|
|
178
178
|
### 注意
|
|
179
|
-
- **CLI 对账机制**:verify 阶段最终 --done 时,CLI 会亲自执行 local.yaml 的 commands.test(同步,耗时可能较长);实测失败会直接阻断 verify
|
|
179
|
+
- **CLI 对账机制**:verify 阶段最终 --done 时,CLI 会亲自执行 local.yaml 的 commands.test(同步,耗时可能较长);实测失败会直接阻断 verify 完成,谎报测试结果没有意义。commands.lint 同样会被 CLI 实测对账(advisory)
|
|
180
180
|
- 冒烟测试非必需:全量/模块实测结果以 CLI 对账为准,本步跑的结果仅供你提前发现问题并写入验证报告
|
|
181
181
|
|
|
182
182
|
### 输出
|
|
@@ -197,57 +197,22 @@ export const definition = {
|
|
|
197
197
|
你只需:在 verify-result.md 的「变更风险等级」section 如实记录变更性质;若变更涉及 daemon/backend 跨进程、session/lease/lifecycle 状态机、或部署启动路径,在「Runtime Evidence」section 提供真实集成证据(启动命令、daemon↔backend 调用与日志关键片段、终态断言)。
|
|
198
198
|
- **集成证据是自报告、CLI 不独立运行时核验**:「Runtime Evidence」由你如实填写,CLI 只校验其**字面存在**(是否含关键词),**不会替你启动 daemon、打真实请求或跑迁移**——它是否名副其实取决于你是否实跑过。务必实跑后据实填写,不得凭堆关键词通过门控。(测试套件对账另算:commands.test 由 CLI 真实执行,那块谎报无效。)
|
|
199
199
|
|
|
200
|
-
3.
|
|
201
|
-
4. 给出结论:PASS / PASS WITH NOTES / FAIL
|
|
200
|
+
3. **生成 verify-result.md 骨架(勿从零手写)**:先跑 \`sillyspec verify-probes --change <change-name> --init\`——一条命令生成九章节骨架(已存在不覆盖),其中**探针结果章节已机械预填**(探针 1 的 TODO/FIXME 命中清单、探针 3 的测试覆盖、探针 5 的 API 契约对账表、探针 6 的删除对账三态判定),文件落 \`.sillyspec/changes/<change-name>/verify-result.md\`。你只需把各 \`<!--TODO-->\` 占位替换为语义结论;半语义探针(2 关键词覆盖 / 4 决策追踪)与断言抽查、集成盲区标注由你补在对应 TODO 处
|
|
201
|
+
4. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)——**结论必须写明 PASS/FAIL 字样,留「待填」会被 gate 判不过**
|
|
202
202
|
5. **核对 module-impact.md**(若 changes/<change>/module-impact.md 存在):对照本次实际代码变更(git diff)与 module-impact.md 的模块影响矩阵,发现不一致(漏标受影响模块 / 影响类型错误 / 实际未触碰的模块被误标)则在 verify-result.md 标注。module-impact 由 plan 首版生成、execute 各 Wave 更新,verify 是最后一次核对机会(archive 仅终审不再生成)。这是 advisory 核对(不阻断 verify 完成),但 module-impact 与实际严重背离应记为风险。
|
|
203
203
|
|
|
204
|
-
### verify-result.md
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
## 探针结果
|
|
218
|
-
- 未实现标记扫描:...
|
|
219
|
-
- 关键词覆盖:...
|
|
220
|
-
- 测试覆盖(含断言有效性抽查):...
|
|
221
|
-
- 决策追踪覆盖:...
|
|
222
|
-
- API 契约对账:...
|
|
223
|
-
- 代码删除对账:声明修改/新增却被整文件删除 = 高风险;清单未列出的删除 = 未声明(CLI 会用 git diff --name-status HEAD advisory 复核,仅 warn 不阻断——是否 FAIL blocker 由你诚实判定)
|
|
224
|
-
|
|
225
|
-
## 决策追踪矩阵(如存在 decisions.md)
|
|
226
|
-
| 决策 ID | FR | Task | Evidence | 状态 |
|
|
227
|
-
|---|---|---|---|---|
|
|
228
|
-
| D-001@v1 | FR-01 | task-01 | test/file/path | PASS/PARTIAL/MISSING |
|
|
229
|
-
|
|
230
|
-
## 测试结果
|
|
231
|
-
(测试套件执行结果)
|
|
232
|
-
|
|
233
|
-
## 技术债务
|
|
234
|
-
(TODO/FIXME/HACK 统计)
|
|
235
|
-
|
|
236
|
-
## 变更风险等级
|
|
237
|
-
(自动检测的 change_risk_profile: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical;若 design.md frontmatter 有 risk_level 显式声明,以声明为准并在此注明「显式声明 = <等级>」+ 理由)
|
|
238
|
-
|
|
239
|
-
## Runtime Evidence(integration-critical / deployment-critical 必填;自报告,CLI 仅校验字面存在、不独立核验,须真实执行过)
|
|
240
|
-
按本次变更实际触碰的运行时组件填写(模板是通用形态——长驻进程 / 服务端点 / 生命周期状态机的证据结构;未涉及的行写「不涉及」并注明原因,勿堆砌无关关键词):
|
|
241
|
-
- 长驻进程/服务 启动命令:
|
|
242
|
-
- 服务地址(本变更触碰的端点):
|
|
243
|
-
- 触发核心路径的请求/命令(附关键响应):
|
|
244
|
-
- 进程日志关键片段(证明走了新路径、无降级/回退分支):
|
|
245
|
-
- 生命周期终态断言(本变更涉及的状态机:初始态 → 运行态 → 终态,逐个确认到达):
|
|
246
|
-
- 失败模式排除(针对本变更声明的风险,逐条说明为何未触发):
|
|
247
|
-
|
|
248
|
-
## 代码审查
|
|
249
|
-
(问题列表 + 总体评价)
|
|
250
|
-
\`\`\`
|
|
204
|
+
### verify-result.md 章节结构(骨架已含,占位替换即可)
|
|
205
|
+
|
|
206
|
+
结论(PASS / PASS WITH NOTES / FAIL)→ 任务完成度 → 设计一致性 → 探针结果(已预填)→ 测试结果 → 决策追踪矩阵(\`| 决策 ID | FR | Task | Evidence | 状态 |\`,存在 decisions.md 才留)→ 技术债务 → 变更风险等级 → Runtime Evidence → 代码审查。
|
|
207
|
+
|
|
208
|
+
**Runtime Evidence 行结构**(integration/deployment-critical 必填;按实际触碰的运行时组件写,未涉及的行写「不涉及」勿堆关键词):长驻进程启动命令 / 触碰的服务端点 / 触发核心路径的请求(附关键响应)/ 进程日志关键片段(证明走了新路径)/ 生命周期终态断言(初始态→运行态→终态)/ 失败模式排除(逐条说明为何未触发)。
|
|
209
|
+
|
|
210
|
+
### 🧹 服务进程登记与自动回收(真实启动验证必读,坑 verify-service-process-leak)
|
|
211
|
+
「真实启动一次」起的长驻服务(uvicorn/node server 等)**必须登记 PID,CLI 在 verify 收尾自动回收**——不登记的进程会挂死机器(实测 uvicorn 漏挂一天多):
|
|
212
|
+
1. 后台启动服务并取 PID(PowerShell:\`Start-Process -PassThru\` 取 \`.Id\`;bash:\`cmd & echo $!\`)
|
|
213
|
+
2. 每行一个 PID 追加到 \`\${SPEC_ROOT}/.runtime/verify-services.pids\`(\`Add-Content\` / \`echo >>\`)
|
|
214
|
+
3. verify \`--done\` 时 CLI 读该文件逐个 kill 并清空——你在 verify-result.md 的「生命周期终态断言」里写「PID 已登记,CLI 收尾回收」即可
|
|
215
|
+
登记是硬要求:没有 PID 登记的「真实启动」证据在 deployment-critical 门控下视为不完整(进程无法证明被回收)。
|
|
251
216
|
|
|
252
217
|
### 输出
|
|
253
218
|
verify-result.md 路径 + 验证报告摘要 + 下一步命令
|
package/src/sync.js
CHANGED
|
@@ -13,6 +13,7 @@ import { writeAtomicSync } from './fs-atomic.js';
|
|
|
13
13
|
import { join } from 'path';
|
|
14
14
|
import { resolvePlatformSpecDir } from './progress.js';
|
|
15
15
|
import { safeGit } from './git-helper.js';
|
|
16
|
+
import { openDatabase } from './db-engine.js';
|
|
16
17
|
import { PLATFORM_MANAGED_FILENAME } from './run/shared.js';
|
|
17
18
|
import { syncSpecTree } from './spec-sync.js';
|
|
18
19
|
|
|
@@ -267,6 +268,9 @@ async function fetchJsonWithStatus(url, options = {}) {
|
|
|
267
268
|
export class SyncManager {
|
|
268
269
|
constructor(cwd) {
|
|
269
270
|
this.cwd = cwd;
|
|
271
|
+
// 归档尾声静默化旗标(坑 post-archive-sync-noise):sync() 探测到已归档时置位,
|
|
272
|
+
// 链内 syncDocuments 的「变更不存在」降 debug(每次 sync 调用重置)
|
|
273
|
+
this._suppressDocsMissingWarn = false;
|
|
270
274
|
}
|
|
271
275
|
|
|
272
276
|
/**
|
|
@@ -411,12 +415,24 @@ export class SyncManager {
|
|
|
411
415
|
return { synced: 0, errors: ['未指定变更名称'] };
|
|
412
416
|
}
|
|
413
417
|
|
|
414
|
-
//
|
|
415
|
-
//
|
|
418
|
+
// 检查变更目录是否存在(归档后目录已移走但 DB 仍有最终状态需推平台,serializeForSync 从
|
|
419
|
+
// DB 读不依赖文件系统目录)。归档终态探测(坑 post-archive-sync-noise,2026-08-21 实证:
|
|
420
|
+
// 归档尾声连打「目录不存在/变更不存在」warn,观感像出错):确认已归档(changes/archive/
|
|
421
|
+
// 有实体 或 DB status='archived')时降为一行 info 措辞——正常时序不是告警。
|
|
416
422
|
const changeDir = join(this.cwd, CHANGES_DIR, changeName);
|
|
423
|
+
let archivedQuietly = false;
|
|
417
424
|
if (!existsSync(changeDir)) {
|
|
418
|
-
|
|
425
|
+
archivedQuietly = existsSync(join(this.cwd, CHANGES_DIR, 'archive', changeName))
|
|
426
|
+
|| this._isChangeArchivedInDb(changeName);
|
|
427
|
+
if (archivedQuietly) {
|
|
428
|
+
console.log(`[sync] 变更已归档(目录在 archive/),继续从 DB 推送最终状态: ${changeName}`);
|
|
429
|
+
} else {
|
|
430
|
+
console.warn(`[sync] 变更目录不存在(可能是已归档,继续从 DB 同步最终状态): ${changeName}`);
|
|
431
|
+
}
|
|
419
432
|
}
|
|
433
|
+
// 透传给本次 sync 链内的 syncDocuments(best-effort 四件套直推:归档后目录已移走,
|
|
434
|
+
// 其「变更不存在」warn 属正常时序,静默化)
|
|
435
|
+
this._suppressDocsMissingWarn = archivedQuietly;
|
|
420
436
|
|
|
421
437
|
const MAX_PUSH_ATTEMPTS = 2;
|
|
422
438
|
for (let attempt = 1; attempt <= MAX_PUSH_ATTEMPTS; attempt++) {
|
|
@@ -617,6 +633,12 @@ export class SyncManager {
|
|
|
617
633
|
// 返回值 → 文档在平台模式下静默永不同步。
|
|
618
634
|
const changeDir = join(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), 'changes', changeName);
|
|
619
635
|
if (!existsSync(changeDir)) {
|
|
636
|
+
// 归档尾声静默化(坑 post-archive-sync-noise):sync() 链内调用(_suppressDocsMissingWarn
|
|
637
|
+
// 置位,目录已移 archive/ 的正常时序)降为 debug;独立调用(手动 sync-docs)保留 warn
|
|
638
|
+
if (this._suppressDocsMissingWarn) {
|
|
639
|
+
debugLog(`[sync] 四件套目录不存在(变更已归档,跳过文档直推): ${changeName}`);
|
|
640
|
+
return { synced: 0, errors: [] };
|
|
641
|
+
}
|
|
620
642
|
console.warn(`[sync] 变更不存在: ${changeName}`);
|
|
621
643
|
return { synced: 0, errors: [`变更不存在: ${changeName}`] };
|
|
622
644
|
}
|
|
@@ -734,6 +756,25 @@ export class SyncManager {
|
|
|
734
756
|
return config.platform || null;
|
|
735
757
|
}
|
|
736
758
|
|
|
759
|
+
/**
|
|
760
|
+
* DB 侧归档态探测(坑 post-archive-sync-noise):changes 表 status='archived' 即真。
|
|
761
|
+
* 只读直查(node:sqlite,不经 ProgressManager——sync 是短进程,惰性 import 无收益且
|
|
762
|
+
* 本调用点在同步代码段);DB 不存在/无行/读失败 → false(保守退回普通 warn 措辞)。
|
|
763
|
+
* @param {string} changeName
|
|
764
|
+
* @returns {boolean}
|
|
765
|
+
*/
|
|
766
|
+
_isChangeArchivedInDb(changeName) {
|
|
767
|
+
try {
|
|
768
|
+
const dbPath = join(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), '.runtime', 'sillyspec.db');
|
|
769
|
+
if (!existsSync(dbPath)) return false;
|
|
770
|
+
const db = openDatabase(dbPath, { readOnly: true });
|
|
771
|
+
try {
|
|
772
|
+
const row = db.prepare("SELECT status FROM changes WHERE name = ?").get(changeName);
|
|
773
|
+
return row?.status === 'archived';
|
|
774
|
+
} finally { try { db.close() } catch {} }
|
|
775
|
+
} catch { return false }
|
|
776
|
+
}
|
|
777
|
+
|
|
737
778
|
/**
|
|
738
779
|
* 写冲突文件 .runtime/sync-conflict-<change>.json(task-12 / D-002 / D-008 / D-010 / FR-05)。
|
|
739
780
|
* push 409 与 pull 本地脏度双向冲突命中时调用,强制提示用户走 platform resolve 三选一。
|
package/src/task-review.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'fs'
|
|
12
12
|
import { join, resolve } from 'path'
|
|
13
|
-
import { git } from './git-helper.js'
|
|
13
|
+
import { git, unquoteGitPath } from './git-helper.js'
|
|
14
14
|
import { pathMatches } from './change-list.js'
|
|
15
15
|
import { parseAllowedPaths, parseRepo, parseBaseCommit, parseHeadCommit } from './stages/plan-postcheck.js'
|
|
16
16
|
import { resolveVerifyChangedFiles } from './verify-postcheck.js'
|
|
@@ -378,6 +378,30 @@ function isTaskLowRisk(changeDir, taskId) {
|
|
|
378
378
|
}
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
/**
|
|
382
|
+
* 纯验证 task 判定(坑 verification-task-zero-diff,2026-08-21 实证):task-10/11 类
|
|
383
|
+
* 「跑测试/部署验证/观测确认」型任务无代码 diff 是本质属性而非伪造——emptyDiff 判伪造
|
|
384
|
+
* 对它们必误杀,此前只能引用验证区间并手工披露归属绕过。task 卡 frontmatter 显式声明
|
|
385
|
+
* `task_type: verification`(或 `verification_only: true`)即走合法零 diff 通道:
|
|
386
|
+
* emptyDiff 不判伪造(转 warning),但 review 必须带非空 requiredEvidence(披露验证
|
|
387
|
+
* 区间/结论出处)——不能拿声明逃掉披露义务。
|
|
388
|
+
* @returns {boolean}
|
|
389
|
+
*/
|
|
390
|
+
function isTaskVerification(changeDir, taskId) {
|
|
391
|
+
if (!changeDir) return false
|
|
392
|
+
const taskFile = join(changeDir, 'tasks', `${taskId}.md`)
|
|
393
|
+
if (!existsSync(taskFile)) return false
|
|
394
|
+
try {
|
|
395
|
+
const content = readFileSync(taskFile, 'utf8')
|
|
396
|
+
const fm = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
397
|
+
if (!fm) return false
|
|
398
|
+
return /^task_type:\s*['"]?verification['"]?\s*$/im.test(fm[1])
|
|
399
|
+
|| /^verification_only:\s*true\s*$/im.test(fm[1])
|
|
400
|
+
} catch {
|
|
401
|
+
return false
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
381
405
|
export function validateTaskReviews(opts) {
|
|
382
406
|
const { planContent, runtimeRoot, executeRunId, allowCannotVerify = true, changeDir = null, gitDir = null, ctx = null } = opts
|
|
383
407
|
|
|
@@ -463,6 +487,15 @@ export function validateTaskReviews(opts) {
|
|
|
463
487
|
if (evidence.emptyDiff) {
|
|
464
488
|
if (isTaskLowRisk(changeDir, taskId)) {
|
|
465
489
|
warnings.push(`${taskId}: base..head 无代码变更(task 声明 low_risk: true,不阻断)`)
|
|
490
|
+
} else if (isTaskVerification(changeDir, taskId)) {
|
|
491
|
+
// 纯验证 task 零 diff 通道(坑 verification-task-zero-diff):声明 task_type: verification
|
|
492
|
+
// 的「跑测试/部署验证/观测」型任务无代码 diff 是本质属性——不判伪造,但披露义务不豁免:
|
|
493
|
+
// requiredEvidence 为空 → error(声明不能替代证据)
|
|
494
|
+
if (Array.isArray(review.requiredEvidence) && review.requiredEvidence.length > 0) {
|
|
495
|
+
warnings.push(`${taskId}: base..head 无代码变更 — task 声明 task_type: verification(纯验证任务),零 diff 合法;requiredEvidence 已披露验证区间/结论出处`)
|
|
496
|
+
} else {
|
|
497
|
+
errors.push(`${taskId}: 纯验证 task(task_type: verification)零 diff 合法,但 review 缺 requiredEvidence(须披露验证区间/命令/结论出处)——声明不能替代披露`)
|
|
498
|
+
}
|
|
466
499
|
} else {
|
|
467
500
|
errors.push(`${taskId}: base..head(${review.base.slice(0, 8)}..${review.head.slice(0, 8)})无任何代码变更 — 评审了一个零改动的任务,review 疑似伪造`)
|
|
468
501
|
continue
|
|
@@ -525,18 +558,21 @@ export function validateTaskReviews(opts) {
|
|
|
525
558
|
|
|
526
559
|
// QUAL-01 收口:原为本地 execFileSync('git')(无 safe.directory、与 git-helper 口径分裂),
|
|
527
560
|
// 现走统一入口;抛错/trim 语义不变,timeout 15000 保留(rev-parse/diff 大仓余量)
|
|
528
|
-
function runGit(gitDir, args) {
|
|
529
|
-
return git(gitDir, args, { timeout: 15000 })
|
|
561
|
+
function runGit(gitDir, args, opts = {}) {
|
|
562
|
+
return git(gitDir, args, { timeout: 15000, ...opts })
|
|
530
563
|
}
|
|
531
564
|
|
|
532
565
|
// porcelain 状态行 → 变更文件路径(对齐 shared.js:343 parsePorcelainPath:`XY path`、引号包裹、
|
|
533
566
|
// rename `R old -> new` 取箭头后)。未 commit 的 working-tree 改动对账用。
|
|
567
|
+
// ⚠️ 入参必须是【未 trim】的原始输出(git(..., {trim:false}))——porcelain 行首空格是状态码
|
|
568
|
+
// 一部分,git() 默认 trim 会把 ' M feature.js' 削成 'M feature.js',slice(3) 再咬掉路径首字符
|
|
569
|
+
// (实测 'eature.js')→ 未提交文件对账全错位(verifyReviewGitEvidence 相交比对误判「完全不相交」)。
|
|
534
570
|
function parsePorcelainFiles(statusOut) {
|
|
535
571
|
const files = []
|
|
536
572
|
for (const line of String(statusOut).split('\n')) {
|
|
537
573
|
if (line.trim() === '') continue
|
|
538
574
|
let p = line.slice(3).trim()
|
|
539
|
-
if (p.length >= 2 && p.startsWith('"') && p.endsWith('"')) p = p.slice(1, -1)
|
|
575
|
+
if (p.length >= 2 && p.startsWith('"') && p.endsWith('"')) p = unquoteGitPath(p.slice(1, -1))
|
|
540
576
|
const arrow = p.indexOf(' -> ')
|
|
541
577
|
if (arrow !== -1) p = p.slice(arrow + 4)
|
|
542
578
|
if (p) files.push(p.replace(/\\/g, '/'))
|
|
@@ -608,7 +644,11 @@ export function verifyReviewGitEvidence(review, gitDir, cache = null) {
|
|
|
608
644
|
runGit(gitDir, ['rev-parse', '--verify', '--quiet', `${hash}^{commit}`])
|
|
609
645
|
c.verified.add(vkey)
|
|
610
646
|
} catch {
|
|
611
|
-
|
|
647
|
+
// 可执行指引(坑 review-head-real-commit-late,2026-08-22 实证:head 必须真实 commit 的
|
|
648
|
+
// 要求撞门才知道——报错直接给操作序列:worktree 改动先 commit,取 HEAD 作 head)
|
|
649
|
+
errors.push(`${field} "${hash}" 不是仓库中的真实 commit — review.json 疑似伪造。` +
|
|
650
|
+
`head/base 必须是 git rev-parse 可解析的 commit hash:worktree 内先 git add -A && git commit -m "<task>",` +
|
|
651
|
+
`再取 git rev-parse HEAD 作 head(base 用 task 卡锡点或基线 commit);不要填分支名/伪 hash/working-tree 描述`)
|
|
612
652
|
}
|
|
613
653
|
}
|
|
614
654
|
if (errors.length > 0) {
|
|
@@ -639,7 +679,7 @@ export function verifyReviewGitEvidence(review, gitDir, cache = null) {
|
|
|
639
679
|
try {
|
|
640
680
|
let wtStatus = c.wtStatus.get(gitDir)
|
|
641
681
|
if (wtStatus === undefined) {
|
|
642
|
-
wtStatus = runGit(gitDir, ['status', '--porcelain'])
|
|
682
|
+
wtStatus = runGit(gitDir, ['status', '--porcelain'], { trim: false })
|
|
643
683
|
c.wtStatus.set(gitDir, wtStatus)
|
|
644
684
|
}
|
|
645
685
|
if (wtStatus && wtStatus.trim().length > 0) {
|
|
@@ -656,15 +696,25 @@ export function verifyReviewGitEvidence(review, gitDir, cache = null) {
|
|
|
656
696
|
|
|
657
697
|
// changedFiles 交叉比对:完全不相交 = review 描述的改动与实际 diff 无关(diffFiles 已含 working-tree)
|
|
658
698
|
if (!emptyDiff && Array.isArray(review.changedFiles) && review.changedFiles.length > 0) {
|
|
659
|
-
|
|
699
|
+
// 注记剥离(坑 changedfiles-annotation-suffix-mismatch,2026-08-22 实证:changedFiles 带
|
|
700
|
+
// 「src/a.js(新增)」类注记后缀被判完全不相交,agent 用正则修还贪婪吞了路径段两轮才对)。
|
|
701
|
+
// 结构化剥法:只剥「路径尾部」的注记形态——全角/半角括号注记、// 与 # 行内注释、空白后的
|
|
702
|
+
// 说明文字;注记本身不参与匹配(想写说明放 reviewerNotes/requiredEvidence)。
|
|
703
|
+
const normalize = (p) => String(p)
|
|
704
|
+
.replace(/^\.\//, '')
|
|
705
|
+
.replace(/[((][^(())]*[))]\s*$/, '') // 尾部(注记)/(note)——中段括号(路径段)不动
|
|
706
|
+
.replace(/\s+(\/\/|#).*$/, '') // 空格后的 // 或 # 注释
|
|
707
|
+
.trim()
|
|
660
708
|
const diffSet = new Set(diffFiles.map(normalize))
|
|
661
709
|
const hasOverlap = review.changedFiles.some(f => {
|
|
662
710
|
const nf = normalize(f)
|
|
711
|
+
if (!nf) return false
|
|
663
712
|
// 兼容 review 写相对/部分路径:前后缀匹配即可
|
|
664
713
|
return diffSet.has(nf) || diffFiles.some(d => d.endsWith(nf) || nf.endsWith(d))
|
|
665
714
|
})
|
|
666
715
|
if (!hasOverlap) {
|
|
667
|
-
errors.push(`changedFiles 与实际 git diff(${review.base.slice(0, 8)}..${review.head.slice(0, 8)},${diffFiles.length} 个文件)完全不相交 — review
|
|
716
|
+
errors.push(`changedFiles 与实际 git diff(${review.base.slice(0, 8)}..${review.head.slice(0, 8)},${diffFiles.length} 个文件)完全不相交 — review 内容与代码变更无关。` +
|
|
717
|
+
`检查:changedFiles 必须是纯文件路径(注记/说明写 reviewerNotes,不拼进路径数组);路径拼写与 diff 对照`)
|
|
668
718
|
}
|
|
669
719
|
}
|
|
670
720
|
|
|
@@ -983,7 +1033,30 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
983
1033
|
}
|
|
984
1034
|
|
|
985
1035
|
// base..head diff 文件集(worktree-aware;null=git 不可用,[]=无 commit diff)
|
|
986
|
-
|
|
1036
|
+
let diffFiles = resolveVerifyChangedFiles(cwd, changeName)
|
|
1037
|
+
// ⚠️ 并入 worktree 未提交改动(坑 draft-attribution-uncommitted-worktree,2026-08-21 实证):
|
|
1038
|
+
// 子代理默认不 commit(execute 复盘 a 同源事实),真实改动全在 worktree working-tree——只看
|
|
1039
|
+
// base..HEAD commit diff 时归属恒空,9/9 草稿全成「无归属」靠主代理手写升级。与
|
|
1040
|
+
// verifyReviewGitEvidence 的 working-tree 并入口径同源:status --porcelain 文件(排除
|
|
1041
|
+
// .sillyspec/ 运行时产物)并入归属集,按 allowed_paths 正常路径归属;review.head 仍为 HEAD
|
|
1042
|
+
// commit——evidence 校验对「commit diff 空 + working-tree 有改动」明示不判伪造,语义一致。
|
|
1043
|
+
try {
|
|
1044
|
+
const wm = new WorktreeManager({ cwd })
|
|
1045
|
+
const meta = wm.getMeta(changeName)
|
|
1046
|
+
if (meta) {
|
|
1047
|
+
const wtGitDir = (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath))
|
|
1048
|
+
? meta.worktreePath
|
|
1049
|
+
: cwd
|
|
1050
|
+
const wtStatus = runGit(wtGitDir, ['status', '--porcelain'], { trim: false })
|
|
1051
|
+
const wtFiles = parsePorcelainFiles(wtStatus)
|
|
1052
|
+
.map(p => String(p).replace(/\\/g, '/'))
|
|
1053
|
+
.filter(p => p !== '.sillyspec' && !p.startsWith('.sillyspec/'))
|
|
1054
|
+
if (wtFiles.length > 0) {
|
|
1055
|
+
diffFiles = [...new Set([...(Array.isArray(diffFiles) ? diffFiles : []), ...wtFiles])]
|
|
1056
|
+
console.log(`[sillyspec] 草稿归属并入 ${wtFiles.length} 个 worktree 未提交文件(子代理未 commit 的改动按 allowed_paths 归属)`)
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
} catch { /* working-tree 并入失败退回 commit diff 口径(fail-open,不阻断草稿) */ }
|
|
987
1060
|
// 单仓模式(无 ctx):主仓无 diff 即无任何 task 可生成 → 提前返回(原逻辑零回归)。
|
|
988
1061
|
// 有 ctx:主仓无 diff 不阻断——跨仓 task 的 diff 在跨仓仓根独立取(per-task),主仓 task 自然跳过(空 changedFiles)。
|
|
989
1062
|
if (!ctx && (!diffFiles || diffFiles.length === 0)) {
|
|
@@ -1109,8 +1182,9 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
1109
1182
|
specVerdict: 'cannot_verify',
|
|
1110
1183
|
qualityVerdict: 'cannot_verify',
|
|
1111
1184
|
requiredEvidence: ['auto-generated draft (no-attributed-diff): ' + taskId +
|
|
1112
|
-
' 的 allowed_paths 未命中本次 diff 任何文件(task 未实现 / 改动与他人同文件 /
|
|
1113
|
-
'需人工确认实际改动后升级 verdict,确属未实现则回 fail'
|
|
1185
|
+
' 的 allowed_paths 未命中本次 diff 任何文件(task 未实现 / 改动与他人同文件 / 路径不匹配 / 纯验证任务)——' +
|
|
1186
|
+
'需人工确认实际改动后升级 verdict,确属未实现则回 fail;若是纯验证任务(跑测试/部署验证/观测,无代码 diff 是本质属性),' +
|
|
1187
|
+
'task 卡 frontmatter 声明 task_type: verification + review 披露验证区间即走合法零 diff 通道'],
|
|
1114
1188
|
reviewerNotes: 'auto-generated draft from git diff ' + taskBase.slice(0, 8) + '..' + taskHead.slice(0, 8) +
|
|
1115
1189
|
';verdict=未评审(无归属 diff 兜底草稿,坑 task-review-draft-skip-leak)',
|
|
1116
1190
|
}
|
|
@@ -1150,6 +1224,194 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
1150
1224
|
return { generated, skipped, noAttribution: noAttributionCount, unattributed, executeRunId }
|
|
1151
1225
|
}
|
|
1152
1226
|
|
|
1227
|
+
/**
|
|
1228
|
+
* adoptTaskReviewMechanics —— 已存在 review.json 的 mechanics 字段一键重算代填(保 verdict)。
|
|
1229
|
+
*
|
|
1230
|
+
* 背景(2026-08-21 agent-手工产出审计项①):review.json 里 schemaVersion/task/base/head/
|
|
1231
|
+
* changedFiles(/repo) 全部可由 CLI 从 git + task 卡机械推导(generateTaskReviewDrafts 已证明),
|
|
1232
|
+
* agent/子代理手算 hash、手抄 diff 清单是纯错误面——而 gate(verifyReviewGitEvidence)校验的
|
|
1233
|
+
* 恰是这些字段。与草稿生成互补:草稿只补「缺失」,adopt 修「已存在但 mechanics 错/缺」——
|
|
1234
|
+
* 保留人工语义字段(specVerdict/qualityVerdict/requiredEvidence/reviewerNotes),只重写 mechanics。
|
|
1235
|
+
*
|
|
1236
|
+
* verdict 保护策略(不猜 agent 本意):
|
|
1237
|
+
* - 合法 verdict(VALID_VERDICTS)原样保留;
|
|
1238
|
+
* - 非法/缺失 verdict 降级 cannot_verify,reviewerNotes 追加标记,requiredEvidence 缺失补占位
|
|
1239
|
+
* (schema 要求 cannot_verify 必带非空 requiredEvidence);
|
|
1240
|
+
* - JSON 解析失败的 review 不救(无法可信保留任何字段),报出来留给 agent 修 JSON 后重跑。
|
|
1241
|
+
*
|
|
1242
|
+
* fail-open:与草稿生成同契约——缺数据返回统计与 reasons,不抛。
|
|
1243
|
+
*
|
|
1244
|
+
* @param {{ changeName: string, cwd: string, platformOpts?: object, ctx?: object|null }} opts
|
|
1245
|
+
* @returns {Promise<{ adopted: number, unchanged: number, skipped: number, missing: number,
|
|
1246
|
+
* downgraded: string[], reasons: string[], executeRunId?: string }>}
|
|
1247
|
+
*/
|
|
1248
|
+
export async function adoptTaskReviewMechanics({ changeName, cwd, platformOpts = {}, ctx = null }) {
|
|
1249
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
1250
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
1251
|
+
const tasksDir = join(specBase, 'changes', changeName, 'tasks')
|
|
1252
|
+
const reasons = []
|
|
1253
|
+
|
|
1254
|
+
if (!changeName) {
|
|
1255
|
+
return { adopted: 0, unchanged: 0, skipped: 0, missing: 0, downgraded: [], reasons: ['无 changeName'] }
|
|
1256
|
+
}
|
|
1257
|
+
if (!existsSync(tasksDir)) {
|
|
1258
|
+
return { adopted: 0, unchanged: 0, skipped: 0, missing: 0, downgraded: [], reasons: ['无 tasks/ 目录'] }
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// executeRunId:与草稿/gate 同源 marker。缺 marker 不新建 run——adopt 修的是当前 run 里
|
|
1262
|
+
// 已存在的 review,marker 丢失说明无「当前 run」语义,无对象可修(gate 对 marker 缺失放行,
|
|
1263
|
+
// 下游 Task Review Gate 兜底,这里同口径不造 run)。
|
|
1264
|
+
const runIdFile = join(runtimeRoot, 'current-execute-run-id-' + changeName)
|
|
1265
|
+
let executeRunId = ''
|
|
1266
|
+
try {
|
|
1267
|
+
if (existsSync(runIdFile)) {
|
|
1268
|
+
const c = readFileSync(runIdFile, 'utf8').trim()
|
|
1269
|
+
if (isValidExecuteRunId(c)) executeRunId = c
|
|
1270
|
+
}
|
|
1271
|
+
} catch {}
|
|
1272
|
+
if (!executeRunId) {
|
|
1273
|
+
return { adopted: 0, unchanged: 0, skipped: 0, missing: 0, downgraded: [], reasons: ['无 execute run marker——无当前 run,adopt 无对象'] }
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// base/head + diff:与 generateTaskReviewDrafts 同源口径(meta 锡点 + worktree rev-parse +
|
|
1277
|
+
// resolveVerifyChangedFiles),保证 adopt 产物与 gate 校验输入一致。
|
|
1278
|
+
const diffFiles = resolveVerifyChangedFiles(cwd, changeName)
|
|
1279
|
+
let base = null
|
|
1280
|
+
let head = null
|
|
1281
|
+
let reviewGitDir = cwd
|
|
1282
|
+
try {
|
|
1283
|
+
const wm = new WorktreeManager({ cwd })
|
|
1284
|
+
const meta = wm.getMeta(changeName)
|
|
1285
|
+
base = (meta && (meta.baselineCommit || meta.baseHash)) || null
|
|
1286
|
+
if (meta && meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
1287
|
+
reviewGitDir = meta.worktreePath
|
|
1288
|
+
}
|
|
1289
|
+
} catch {}
|
|
1290
|
+
if (base) {
|
|
1291
|
+
try {
|
|
1292
|
+
head = runGit(reviewGitDir, ['rev-parse', 'HEAD'])
|
|
1293
|
+
} catch {
|
|
1294
|
+
head = null
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f)).sort()
|
|
1299
|
+
let adopted = 0
|
|
1300
|
+
let unchanged = 0
|
|
1301
|
+
let skipped = 0
|
|
1302
|
+
let missing = 0
|
|
1303
|
+
const downgraded = []
|
|
1304
|
+
|
|
1305
|
+
for (const tf of taskFiles) {
|
|
1306
|
+
const taskId = tf.replace(/\.md$/, '')
|
|
1307
|
+
const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId, 'review.json')
|
|
1308
|
+
if (!existsSync(reviewPath)) {
|
|
1309
|
+
missing++
|
|
1310
|
+
continue
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const existing = readReview(reviewPath)
|
|
1314
|
+
if (existing.parseError) {
|
|
1315
|
+
skipped++
|
|
1316
|
+
reasons.push(`${taskId}: JSON 解析失败,adopt 不猜字段——先修 JSON 再重跑`)
|
|
1317
|
+
continue
|
|
1318
|
+
}
|
|
1319
|
+
const prev = existing.review || {}
|
|
1320
|
+
|
|
1321
|
+
const content = readFileSync(join(tasksDir, tf), 'utf8')
|
|
1322
|
+
const allowedPaths = parseAllowedPaths(content)
|
|
1323
|
+
const taskRepo = normalizeRepoKey(parseRepo(content))
|
|
1324
|
+
const crossEntry = (ctx && taskRepo !== DEFAULT_REPO_KEY) ? ctx.resolve(taskRepo) : null
|
|
1325
|
+
let taskBase = base
|
|
1326
|
+
let taskHead = head
|
|
1327
|
+
let taskChangedFiles
|
|
1328
|
+
let draftRepo
|
|
1329
|
+
|
|
1330
|
+
if (crossEntry) {
|
|
1331
|
+
const baseCommit = parseBaseCommit(content)
|
|
1332
|
+
const headCommit = parseHeadCommit(content)
|
|
1333
|
+
if (!baseCommit || !headCommit) {
|
|
1334
|
+
skipped++
|
|
1335
|
+
reasons.push(`${taskId}: 跨仓 task 卡缺 base/head 锡点,无法 adopt`)
|
|
1336
|
+
continue
|
|
1337
|
+
}
|
|
1338
|
+
taskBase = baseCommit
|
|
1339
|
+
taskHead = headCommit
|
|
1340
|
+
draftRepo = taskRepo
|
|
1341
|
+
let crossDiffFiles = []
|
|
1342
|
+
try {
|
|
1343
|
+
const out = runGit(crossEntry.gitDir, ['diff', '--name-only', `${baseCommit}..${headCommit}`])
|
|
1344
|
+
crossDiffFiles = out ? out.split('\n').filter(Boolean).map(p => p.replace(/\\/g, '/')) : []
|
|
1345
|
+
} catch {
|
|
1346
|
+
skipped++
|
|
1347
|
+
reasons.push(`${taskId}: 跨仓 git diff 失败(${crossEntry.gitDir})`)
|
|
1348
|
+
continue
|
|
1349
|
+
}
|
|
1350
|
+
taskChangedFiles = allowedPaths.length > 0
|
|
1351
|
+
? crossDiffFiles.filter(f => allowedPaths.some(p => pathMatches(f, p)))
|
|
1352
|
+
: crossDiffFiles
|
|
1353
|
+
} else {
|
|
1354
|
+
const mainDiff = Array.isArray(diffFiles) ? diffFiles : []
|
|
1355
|
+
taskChangedFiles = allowedPaths.length > 0
|
|
1356
|
+
? mainDiff.filter(f => allowedPaths.some(p => pathMatches(f, p)))
|
|
1357
|
+
: []
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
if (!crossEntry && (!taskBase || !taskHead)) {
|
|
1361
|
+
skipped++
|
|
1362
|
+
reasons.push(`${taskId}: 主仓缺 worktree meta base/head,无法 adopt`)
|
|
1363
|
+
continue
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const merged = { ...prev }
|
|
1367
|
+
merged.schemaVersion = REVIEW_SCHEMA_VERSION
|
|
1368
|
+
merged.task = taskId
|
|
1369
|
+
merged.base = taskBase
|
|
1370
|
+
merged.head = taskHead
|
|
1371
|
+
merged.changedFiles = taskChangedFiles
|
|
1372
|
+
if (draftRepo) merged.repo = draftRepo
|
|
1373
|
+
|
|
1374
|
+
let verdictTouched = false
|
|
1375
|
+
for (const v of ['specVerdict', 'qualityVerdict']) {
|
|
1376
|
+
if (!VALID_VERDICTS.includes(merged[v])) {
|
|
1377
|
+
merged[v] = 'cannot_verify'
|
|
1378
|
+
verdictTouched = true
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
if (verdictTouched) downgraded.push(taskId)
|
|
1382
|
+
let evidenceFilled = false
|
|
1383
|
+
if ((merged.specVerdict === 'cannot_verify' || merged.qualityVerdict === 'cannot_verify')
|
|
1384
|
+
&& (!Array.isArray(merged.requiredEvidence) || merged.requiredEvidence.length === 0)) {
|
|
1385
|
+
merged.requiredEvidence = [`adopt 代填:原 review 缺 requiredEvidence(verdict=${merged.specVerdict}/${merged.qualityVerdict}),待人工复核补证据`]
|
|
1386
|
+
evidenceFilled = true
|
|
1387
|
+
}
|
|
1388
|
+
if (verdictTouched) {
|
|
1389
|
+
merged.reviewerNotes = String(merged.reviewerNotes || '') + (merged.reviewerNotes ? '; ' : '')
|
|
1390
|
+
+ 'adopt: 原 verdict 非法/缺失已降级 cannot_verify(不猜本意)'
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const mechanicsChanged = prev.schemaVersion !== merged.schemaVersion
|
|
1394
|
+
|| prev.base !== merged.base
|
|
1395
|
+
|| prev.head !== merged.head
|
|
1396
|
+
|| JSON.stringify(prev.changedFiles ?? null) !== JSON.stringify(merged.changedFiles)
|
|
1397
|
+
|| prev.repo !== merged.repo
|
|
1398
|
+
|
|
1399
|
+
try {
|
|
1400
|
+
if (mechanicsChanged || verdictTouched || evidenceFilled) {
|
|
1401
|
+
writeFileSync(reviewPath, JSON.stringify(merged, null, 2) + '\n')
|
|
1402
|
+
adopted++
|
|
1403
|
+
} else {
|
|
1404
|
+
unchanged++
|
|
1405
|
+
}
|
|
1406
|
+
} catch (e) {
|
|
1407
|
+
skipped++
|
|
1408
|
+
reasons.push(`${taskId}: 写回失败 ${e.message}`)
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
return { adopted, unchanged, skipped, missing, downgraded, reasons, executeRunId }
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1153
1415
|
/**
|
|
1154
1416
|
* 获取当前(或最新)execute run id
|
|
1155
1417
|
* 从 runtime 目录下查找 execute-runs/ 子目录
|