sillyspec 3.32.10 → 3.33.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-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-explore/SKILL.md +107 -107
- package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.github/workflows/ci.yml +66 -0
- package/.husky/pre-push +7 -6
- package/AGENTS.md +1 -1
- package/assets/command-cards/flow.md +1 -1
- package/eval/eval.config.json +3 -3
- package/eval/lib/config.mjs +6 -3
- package/eval/lib/util.mjs +16 -3
- package/eval/lib/workdir.mjs +4 -1
- package/eval/results/eval.db +0 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-00-05-625Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/prompt.txt +8 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/prompt.txt +8 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stdout.txt +1 -0
- package/eval/results/workdirs/001-slugify__armA/package.json +6 -0
- package/eval/results/workdirs/001-slugify__armA/src/slugify.mjs +26 -0
- package/eval/results/workdirs/001-slugify__armB/package.json +6 -0
- package/eval/results/workdirs/001-slugify__armB/src/slugify.mjs +26 -0
- package/eval/results/workdirs/002-each-day__armA/package.json +6 -0
- package/eval/results/workdirs/002-each-day__armA/src/dates.mjs +30 -0
- package/eval/results/workdirs/002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/002-each-day__armB/package.json +6 -0
- package/eval/results/workdirs/002-each-day__armB/src/dates.mjs +4 -0
- package/eval/results/workdirs/003-word-report__armA/bin/report.mjs +36 -0
- package/eval/results/workdirs/003-word-report__armA/data/notes.txt +5 -0
- package/eval/results/workdirs/003-word-report__armA/package.json +6 -0
- package/eval/results/workdirs/003-word-report__armB/bin/report.mjs +36 -0
- package/eval/results/workdirs/003-word-report__armB/data/notes.txt +5 -0
- package/eval/results/workdirs/003-word-report__armB/package.json +6 -0
- package/eval/results/workdirs/004-phase-stats__armA/data/events.log +16 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/flow.md +44 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-archive.md +27 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-brainstorm.md +34 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-execute.md +30 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-plan.md +28 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-verify.md +30 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/status.md +28 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-auto/SKILL.md +54 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-continue/SKILL.md +17 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-execute/SKILL.md +222 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-explore/SKILL.md +107 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-flow/SKILL.md +62 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-knowledge/SKILL.md +40 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-propose/SKILL.md +16 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-resume/SKILL.md +21 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-state/SKILL.md +25 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-workspace/SKILL.md +33 -0
- package/eval/results/workdirs/004-phase-stats__armB/AGENTS.md +45 -0
- package/eval/results/workdirs/004-phase-stats__armB/CLAUDE.md +3 -0
- package/eval/results/workdirs/004-phase-stats__armB/data/events.log +16 -0
- package/eval/results/workdirs/run-001-slugify__armA/package.json +6 -0
- package/eval/results/workdirs/run-001-slugify__armA/src/slugify.mjs +26 -0
- package/eval/results/workdirs/run-001-slugify__armB/package.json +6 -0
- package/eval/results/workdirs/run-001-slugify__armB/src/slugify.mjs +26 -0
- package/eval/results/workdirs/run-002-each-day__armA/package.json +6 -0
- package/eval/results/workdirs/run-002-each-day__armA/src/dates.mjs +30 -0
- package/eval/results/workdirs/run-002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/run-002-each-day__armB/package.json +6 -0
- package/eval/results/workdirs/run-002-each-day__armB/src/dates.mjs +4 -0
- package/eval/results/workdirs/run-003-word-report__armA/bin/report.mjs +36 -0
- package/eval/results/workdirs/run-003-word-report__armA/data/notes.txt +5 -0
- package/eval/results/workdirs/run-003-word-report__armA/package.json +6 -0
- package/eval/results/workdirs/run-003-word-report__armB/bin/report.mjs +36 -0
- package/eval/results/workdirs/run-003-word-report__armB/data/notes.txt +5 -0
- package/eval/results/workdirs/run-003-word-report__armB/package.json +6 -0
- package/eval/results/workdirs/run-004-phase-stats__armA/bin/phase-stats.mjs +37 -0
- package/eval/results/workdirs/run-004-phase-stats__armA/data/events.log +16 -0
- package/eval/results/workdirs/run-004-phase-stats__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/run-004-phase-stats__armB/data/events.log +16 -0
- package/eval/results/workdirs/run-005-orders-export__armA/bin/export-orders.mjs +57 -0
- package/eval/results/workdirs/run-005-orders-export__armA/data/.gitattributes +1 -0
- package/eval/results/workdirs/run-005-orders-export__armA/data/orders.csv +9 -0
- package/eval/results/workdirs/run-005-orders-export__armA/out/summary.json +1 -0
- package/eval/results/workdirs/run-005-orders-export__armB/bin/export-orders.mjs +57 -0
- package/eval/results/workdirs/run-005-orders-export__armB/data/.gitattributes +1 -0
- package/eval/results/workdirs/run-005-orders-export__armB/data/orders.csv +9 -0
- package/eval/results/workdirs/run-005-orders-export__armB/out/summary.json +1 -0
- package/eval/tasks/004-phase-stats/files/data/events.log +16 -0
- package/eval/tasks/004-phase-stats/solution/bin/phase-stats.mjs +37 -0
- package/eval/tasks/004-phase-stats/task.json +6 -0
- package/eval/tasks/004-phase-stats/verify.mjs +38 -0
- package/eval/tasks/005-orders-export/files/data/.gitattributes +1 -0
- package/eval/tasks/005-orders-export/files/data/orders.csv +9 -0
- package/eval/tasks/005-orders-export/solution/bin/export-orders.mjs +57 -0
- package/eval/tasks/005-orders-export/task.json +6 -0
- package/eval/tasks/005-orders-export/verify.mjs +28 -0
- package/package.json +1 -1
- package/quicklog/tmp-fix-review.mjs +21 -0
- package/src/archive-delta.js +4 -3
- package/src/ceremony-tier.js +2 -1
- package/src/change-delete.js +6 -5
- package/src/contract-matrix.js +2 -1
- package/src/cross-placement.js +5 -1
- package/src/db.js +8 -2
- package/src/decision-distill.js +29 -3
- package/src/decision-redomain.js +148 -0
- package/src/deleted-ledger.js +2 -1
- package/src/design-facts.js +3 -3
- package/src/docs-debt.js +3 -3
- package/src/doctor-diagnostics.js +8 -3
- package/src/flow-draft.js +2 -1
- package/src/flow-parity.js +10 -2
- package/src/flow.js +112 -63
- package/src/forensics.js +236 -0
- package/src/fr-index.js +2 -1
- package/src/friction-ledger.js +2 -1
- package/src/friction-tally.js +8 -4
- package/src/handoff.js +2 -1
- package/src/hooks/worktree-guard.js +5 -5
- package/src/index.js +75 -8
- package/src/knowledge-graph.js +3 -4
- package/src/knowledge-hits.js +2 -1
- package/src/knowledge-vector.js +2 -1
- package/src/module-impact.js +2 -5
- package/src/modules.js +31 -0
- package/src/progress/consistency-doctor.js +5 -2
- package/src/progress/stage-machine.js +6 -2
- package/src/progress.js +19 -8
- package/src/quicklog.js +194 -9
- package/src/review-dispatch.js +4 -3
- package/src/review-tier.js +2 -1
- package/src/route-hindsight.js +4 -3
- package/src/run/bg-sync.js +10 -3
- package/src/run/command.js +13 -11
- package/src/run/complete-handlers.js +40 -37
- package/src/run/complete.js +12 -8
- package/src/run/gate-snapshot-ledger.js +2 -1
- package/src/run/gate-snapshot.js +57 -11
- package/src/run/gates.js +38 -21
- package/src/run/lint-ownership.js +37 -0
- package/src/run/prompt.js +46 -30
- package/src/run/scan-profile.js +2 -1
- package/src/run/shared.js +109 -5
- package/src/run/stage.js +5 -4
- package/src/run/test-ledger.js +2 -1
- package/src/run/verify-quality-scan.js +21 -6
- package/src/runtime-paths.js +189 -0
- package/src/scan-refresh.js +4 -3
- package/src/scope-audit.js +7 -6
- package/src/sentinel-assertions.js +37 -0
- package/src/spec-sync.js +118 -23
- package/src/stage-contract.js +24 -2
- package/src/stage-review.js +5 -3
- package/src/stages/brainstorm.js +72 -21
- package/src/stages/plan-postcheck.js +13 -2
- package/src/stages/plan.js +19 -0
- package/src/sync-noise.js +3 -2
- package/src/sync.js +71 -15
- package/src/task-done.js +2 -1
- package/src/task-review.js +7 -5
- package/src/task-tick.js +23 -1
- package/src/test-bindings.js +32 -3
- package/src/ui-visual.js +119 -4
- package/src/validate-artifacts.js +2 -1
- package/src/verify-draft.js +2 -1
- package/src/verify-postcheck.js +128 -27
- package/src/verify-probes.js +157 -52
- package/src/watcher.js +15 -9
- package/src/worktree-apply.js +28 -3
- package/src/write-audit.js +2 -1
- package/templates/agents-instruction.md +1 -1
package/src/stages/plan.js
CHANGED
|
@@ -219,6 +219,9 @@ execution_mode: main # dispatch=子代理派发(判据见「执行模式声
|
|
|
219
219
|
## 验收
|
|
220
220
|
- 具体可验证的验收条目
|
|
221
221
|
|
|
222
|
+
## Review Focus 盲区清单(隐含面)
|
|
223
|
+
- (≤5 条——上限不是配额,写不出就停;按最可能坑到人排序。每条 = 规格隐含但没有任何任务测试覆盖的输入/条件 + 合理人预期行为 + 承接落点 task-XX;写不出显式写「已检查,无」——不留空、不编造;泛泛风险("并发需注意"类)不算行)
|
|
224
|
+
|
|
222
225
|
## 覆盖矩阵(如存在 decisions.md)
|
|
223
226
|
| ID | 覆盖任务 | 验收证据 |
|
|
224
227
|
|---|---|---|
|
|
@@ -236,6 +239,7 @@ light 计划的约束:
|
|
|
236
239
|
- 如果存在 P0/P1 unresolved blocker,不生成 plan.md
|
|
237
240
|
- 任务清单(tasks.md)控制在 10 条以内
|
|
238
241
|
- 任务名使用 tasks.md checkbox 格式(\`- [ ] task-XX:\`),plan.md 不放任务行
|
|
242
|
+
- **比例自检**(2026-10-15-plan-review-focus):计划产物与 spec/design 相称——整段可粘贴代码体=转录不是计划;用签名、测试断言与规格精确值承载决策,为实现者保留判断空间
|
|
239
243
|
|
|
240
244
|
---
|
|
241
245
|
|
|
@@ -285,6 +289,15 @@ task-01 → task-03(最长路径,决定最短交付周期)
|
|
|
285
289
|
|
|
286
290
|
> 验收结论不落在 plan.md(此段是验收清单,plan.md 不做执行态文件)——逐项核验结果由 verify 阶段写入 verify-result.md(PASS/FAIL 结论与证据);task 级验收对照 TaskCard frontmatter 的 acceptance 字段。
|
|
287
291
|
|
|
292
|
+
## Review Focus 盲区清单(隐含面)
|
|
293
|
+
规格/设计隐含、但没有任何任务测试覆盖的输入/失效模式(≤5 条——上限不是配额,写不出第 4 条就停;按最可能坑到人排序;每行 = 具体输入或条件 + 可判定预期行为 + 承接落点;泛泛风险分析不算行):
|
|
294
|
+
|
|
295
|
+
| # | 输入/条件 | 合理人预期行为 | 承接落点 |
|
|
296
|
+
|---|---|---|---|
|
|
297
|
+
| 1 | ... | ... | task-XX(acceptance/测试) |
|
|
298
|
+
|
|
299
|
+
(写不出 → 写「已检查,无」一行——不留空、不编造。)
|
|
300
|
+
|
|
288
301
|
## 覆盖矩阵(如存在 decisions.md)
|
|
289
302
|
| ID | 覆盖任务 | 验收证据 |
|
|
290
303
|
|---|---|---|
|
|
@@ -294,6 +307,7 @@ task-01 → task-03(最长路径,决定最短交付周期)
|
|
|
294
307
|
full 计划的约束:
|
|
295
308
|
- **禁止**估时(任务总表不含估时列)
|
|
296
309
|
- **禁止**泛泛风险分析("需要充分测试"类废话转为具体验收条目)
|
|
310
|
+
- **比例自检**(2026-10-15-plan-review-focus):计划产物(plan.md + TaskCard implementation)与 spec/design 相称——整段可粘贴代码体=转录不是计划;用签名、测试断言与规格精确值承载决策,为实现者保留判断空间
|
|
297
311
|
- Mermaid 依赖关系图**仅当依赖关系非平凡时生成**(线性依赖或全并行时不生成)
|
|
298
312
|
- **Wave 段 ID 引用行必须保留**(\`- task-XX\` 纯 ID、无 checkbox 无任务名——机器解析依赖;任务名唯一真相在 tasks.md,plan.md 重抄即双写漂移)
|
|
299
313
|
- plan.md 包含 Wave 分组 + 任务总表 + 关键路径 + 全局硬约束(design 有跨 task 硬约束时逐字抄录)+ 全局验收标准,**不放实现细节**
|
|
@@ -390,6 +404,11 @@ prompt 要点:
|
|
|
390
404
|
6. 写通道降级:写操作持续被平台拒绝(session not in running turn 类)→ 重试 ≤3 次即停,完整结论(含 review.json 全文)作为最终文本回传主代理代落盘,reviewerNotes 首行留痕「代落盘:子代理写通道故障」;禁止长时间空转重试。
|
|
391
405
|
7. **审查经济学三律(2026-09-20 对撞实验驱动,同 brainstorm Grill 步)**:①请求预算硬钳——子代理请求上限 **10 次**(材料包基准面 + plan.md + ≤3 次定向补证),到达即出 verdict 禁「再确认一下」续读,派发 prompt 明示此预算;②FAIL 后增量复审——修复阻断项后 **resume 同一子代理**只发修复说明+diff 验证「阻断项解决否」,不重开全文审(64s vs 322s = 5 倍差实证),review.json checklist 追加增量复核行;原 agent 不可 resume 才新开(prompt 附首轮阻断项仍只验阻断项);③**并行化**——派发用 run_in_background: true,主代理立即继续 Step 4(生成 TaskCard),回收在即将 --done 本步骤前(pass → 完成 / fail → 增量复审 / 在跑 → 等待),Stage Review Gate 照常硬校验;④**审查只读纪律**——子代理工具面只读(Read/Grep/只读 Bash),禁止 Edit/Write 被审产物(design/plan/四件套/源码),唯一可写 review.json,发现问题写 checklist/blockers 由主代理修(2026-09-20 实证 Grill 越权改 design 15 处;③并行化下同文件双写竞态的前置防线,详见 brainstorm Grill 步全文)。
|
|
392
406
|
|
|
407
|
+
### 补充复查(2026-10-15-plan-review-focus)
|
|
408
|
+
1. **Review Focus 承接复查**:plan.md「Review Focus 盲区清单(隐含面)」每行是否落到拥有该代码的 task 的 acceptance/测试;空节必须是显式「已检查,无」而非跳过或编造低价值行凑数(≤5 条是上限不是配额)。
|
|
409
|
+
2. **比例自检复核**:plan.md 与 TaskCard implementation 与 spec/design 相称——整段可粘贴代码体=转录不是计划;签名+测试断言+规格精确值承载决策足够。
|
|
410
|
+
3. **共享面预扫留表**:每对共享 target_files / provides-expects_from 契约链 / 依赖边的任务逐对核「产出 vs 消费」一致、每张卡自洽(测试所指=代码所指、创建文件=后文触碰文件),核验结果以**表格**写进审查输出(共享对两任务一行、单卡自洽一行)——"scan is clean" 而无表格行 = 没扫过;plan-postcheck 硬校验的是字段对齐,此处核语义矛盾。
|
|
411
|
+
|
|
393
412
|
### module-impact.md 首版(scale≠small 时:CLI 自动生成,审查步勿手写)
|
|
394
413
|
module-impact.md 首版**由 CLI 在本阶段 --done 时自动生成**——文件×模块归属按 _module-map.yaml 前缀匹配机械预填,章节含「## 模块影响矩阵」「## 未匹配文件」「## 更新结果」表骨架(每受影响模块一行 pending),影响类型列留 <!--TODO--> 由 execute/verify 按实际 diff 回填。已存在不覆盖。手写整份首版是历史返工根源(章节标题变体会被 archive contains_sections 硬拦),**本步不要手写**。
|
|
395
414
|
仅当 --done 报 module-impact 缺失时(如无 module-map 的绿地项目生成降级失败),按 gate 报错提示手写兜底:两章节标题逐字用「## 模块影响矩阵」「## 未匹配文件」(未命中的文件归「未匹配文件」章节),并含「## 更新结果」表骨架(行:\`modules/<id>.md\`、\`_module-map.yaml\`,状态列 pending/skipped,末列是状态列,勿追加其他列)。
|
package/src/sync-noise.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* 调用方传 noMute 绕过。
|
|
25
25
|
*/
|
|
26
26
|
import { readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs'
|
|
27
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
27
28
|
import { join } from 'path'
|
|
28
29
|
|
|
29
30
|
const NOISE_MARKER = 'sync-noise-mute.json'
|
|
@@ -254,7 +255,7 @@ let _openedSelfHealByThisProcess = new Set()
|
|
|
254
255
|
const _seenSelfHealInProcess = new Set()
|
|
255
256
|
|
|
256
257
|
function _readSelfHealWindows() {
|
|
257
|
-
const p = _runtimeDir ?
|
|
258
|
+
const p = _runtimeDir ? runtimeStatePath(_runtimeDir, SELF_HEAL_MARKER) : null
|
|
258
259
|
if (!p) return null
|
|
259
260
|
try {
|
|
260
261
|
const s = JSON.parse(readFileSync(p, 'utf8'))
|
|
@@ -279,7 +280,7 @@ function _selfHealLineVisible(changeName, kind, noMute) {
|
|
|
279
280
|
next[key] = now + MUTE_WINDOW_MS
|
|
280
281
|
try {
|
|
281
282
|
mkdirSync(_runtimeDir, { recursive: true })
|
|
282
|
-
writeFileSync(
|
|
283
|
+
writeFileSync(runtimeStatePath(_runtimeDir, SELF_HEAL_MARKER), JSON.stringify(next) + '\n', 'utf8')
|
|
283
284
|
} catch { /* marker 写失败 = 闸门失效退化为直通,仅损失降噪 */ }
|
|
284
285
|
_openedSelfHealByThisProcess.add(key)
|
|
285
286
|
return true
|
package/src/sync.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync, chmodSync, rmSync, statSync } from 'fs';
|
|
12
12
|
import { createHash } from 'crypto';
|
|
13
13
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
14
|
+
import { runtimeStatePath } from './runtime-paths.js';
|
|
14
15
|
import { join, dirname, basename, isAbsolute, relative, resolve as pathResolve } from 'path';
|
|
15
16
|
import { homedir } from 'os';
|
|
16
17
|
import { resolvePlatformSpecDir } from './progress.js';
|
|
@@ -718,7 +719,19 @@ export class SyncManager {
|
|
|
718
719
|
console.warn(`⚠️ [sync] 变更 ${changeName} 的未决冲突已过期(base_ts ≥ 平台 ts=${stalePlatTs}),自动清除并恢复推送`);
|
|
719
720
|
} else {
|
|
720
721
|
console.warn(`⚠️ [sync] 变更 ${changeName} 存在未决平台冲突,跳过自动推送。处理:sillyspec platform resolve ${changeName} --keep-local | --take-platform | --abort`);
|
|
721
|
-
|
|
722
|
+
// spec 树与 progress 六表是独立通道(独立端点/独立冲突文件):progress 冲突抑制
|
|
723
|
+
// 只应封锁六表上行,不应连带封锁 spec 树——否则删除 op 的唯一上行通道被冲突文件
|
|
724
|
+
// 长期封锁(2026-10-16 下游实证:409 窗口内本地删除的任务卡在服务器留存,后续整树
|
|
725
|
+
// 快照拉取把它们写回本地=复活,FR-01 上游成因)。补跑 best-effort:失败不改本分支
|
|
726
|
+
// 既有的 conflict/suppressed 返回语义(spec 树自身有失败可见性与下轮重试)。
|
|
727
|
+
let specTreeErrors = [];
|
|
728
|
+
try {
|
|
729
|
+
await syncSpecTree(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), this._getPlatform(), changeName, { signal: opts.signal });
|
|
730
|
+
} catch (err) {
|
|
731
|
+
specTreeErrors = [`spec 树补跑失败(不影响抑制语义): ${err.message}`];
|
|
732
|
+
debugLog(`[sync] 冲突抑制期间 spec 树同步异常: ${changeName}: ${err.message}`);
|
|
733
|
+
}
|
|
734
|
+
return { synced: 0, errors: [`conflict pending: ${changeName}`, ...specTreeErrors], conflict: true, suppressed: true, specTreeAttempted: true };
|
|
722
735
|
}
|
|
723
736
|
}
|
|
724
737
|
}
|
|
@@ -904,8 +917,14 @@ export class SyncManager {
|
|
|
904
917
|
// 树根与进度锚点同源(safePlatformSpecDir,平台模式 → specRoot):此前硬编码
|
|
905
918
|
// cwd/.sillyspec,平台模式下该目录只有 local.yaml(被 walk 排除)→ 本地树恒空
|
|
906
919
|
// → 服务器清单全量 delete(BUG-01,computeSpecOps 护栏为第二道防线)。
|
|
920
|
+
// treeOk 透传(2026-10-17-archive-half-state-location-stuck-active):树通道失败
|
|
921
|
+
// 不影响进度上行返回值(既有语义不变),但结果对象必须如实携带——终态强校验
|
|
922
|
+
// (triggerSyncTerminalStrict)按 treeOk 判「平台 location 是否收敛」,吞进
|
|
923
|
+
// debugLog 会让归档半态(status=archived + location=active)静默存活。
|
|
924
|
+
let treeOk = false;
|
|
907
925
|
try {
|
|
908
|
-
await syncSpecTree(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), this._getPlatform(), changeName, { signal: opts.signal });
|
|
926
|
+
const treeResult = await syncSpecTree(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), this._getPlatform(), changeName, { signal: opts.signal });
|
|
927
|
+
treeOk = treeResult && treeResult.ok === true;
|
|
909
928
|
} catch (err) {
|
|
910
929
|
debugLog(`[sync] spec 树增量同步失败(不影响进度): ${changeName}: ${err.message}`);
|
|
911
930
|
}
|
|
@@ -922,7 +941,7 @@ export class SyncManager {
|
|
|
922
941
|
}
|
|
923
942
|
}
|
|
924
943
|
|
|
925
|
-
return { synced: 1, errors: [] };
|
|
944
|
+
return { synced: 1, errors: [], treeOk };
|
|
926
945
|
}
|
|
927
946
|
|
|
928
947
|
if (res.status !== 409) {
|
|
@@ -1293,7 +1312,7 @@ export class SyncManager {
|
|
|
1293
1312
|
.digest('hex');
|
|
1294
1313
|
} catch { /* 指纹失败按有变化处理 */ }
|
|
1295
1314
|
if (!manual && docsFingerprint) {
|
|
1296
|
-
const markerPath = join(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), '.runtime', `sync-docs-lastpush-${changeName}.json`);
|
|
1315
|
+
const markerPath = runtimeStatePath(join(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), '.runtime'), `sync-docs-lastpush-${changeName}.json`);
|
|
1297
1316
|
try {
|
|
1298
1317
|
const prev = JSON.parse(readFileSync(markerPath, 'utf8'));
|
|
1299
1318
|
if (prev && prev.hash === docsFingerprint) {
|
|
@@ -1323,7 +1342,8 @@ export class SyncManager {
|
|
|
1323
1342
|
try {
|
|
1324
1343
|
const runtimeDir = join(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), '.runtime');
|
|
1325
1344
|
mkdirSync(runtimeDir, { recursive: true });
|
|
1326
|
-
|
|
1345
|
+
try { mkdirSync(join(runtimeDir, 'state'), { recursive: true }); } catch { /* marker fail-open */ }
|
|
1346
|
+
writeFileSync(runtimeStatePath(runtimeDir, `sync-docs-lastpush-${changeName}.json`), JSON.stringify({ hash: docsFingerprint, at: Date.now() }) + '\n', 'utf8');
|
|
1327
1347
|
} catch { /* marker 失败只损失去重(下轮多推一次),不影响同步 */ }
|
|
1328
1348
|
}
|
|
1329
1349
|
console.log(`[sync] 已同步 ${syncedCount} 个文档: ${changeName}`);
|
|
@@ -1509,7 +1529,7 @@ export class SyncManager {
|
|
|
1509
1529
|
if (!specDir) return null;
|
|
1510
1530
|
const runtimeDir = join(specDir, '.runtime');
|
|
1511
1531
|
if (!existsSync(runtimeDir)) mkdirSync(runtimeDir, { recursive: true });
|
|
1512
|
-
const conflictPath =
|
|
1532
|
+
const conflictPath = runtimeStatePath(runtimeDir, `sync-conflict-${changeName}.json`);
|
|
1513
1533
|
const payload = {
|
|
1514
1534
|
change: changeName,
|
|
1515
1535
|
base_ts: info.base_ts ?? null,
|
|
@@ -1530,7 +1550,7 @@ export class SyncManager {
|
|
|
1530
1550
|
readConflictFile(changeName) {
|
|
1531
1551
|
const specDir = safePlatformSpecDir(this.cwd);
|
|
1532
1552
|
if (!specDir) return null;
|
|
1533
|
-
const conflictPath = join(specDir, '.runtime', `sync-conflict-${changeName}.json`);
|
|
1553
|
+
const conflictPath = runtimeStatePath(join(specDir, '.runtime'), `sync-conflict-${changeName}.json`);
|
|
1534
1554
|
if (!existsSync(conflictPath)) return null;
|
|
1535
1555
|
try {
|
|
1536
1556
|
return JSON.parse(readFileSync(conflictPath, 'utf8'));
|
|
@@ -1545,7 +1565,7 @@ export class SyncManager {
|
|
|
1545
1565
|
readSpecConflictFile(changeName) {
|
|
1546
1566
|
const specDir = safePlatformSpecDir(this.cwd);
|
|
1547
1567
|
if (!specDir) return null;
|
|
1548
|
-
const p = join(specDir, '.runtime', `spec-sync-conflict-${changeName}.json`);
|
|
1568
|
+
const p = runtimeStatePath(join(specDir, '.runtime'), `spec-sync-conflict-${changeName}.json`);
|
|
1549
1569
|
if (!existsSync(p)) return null;
|
|
1550
1570
|
try {
|
|
1551
1571
|
return JSON.parse(readFileSync(p, 'utf8'));
|
|
@@ -1558,7 +1578,7 @@ export class SyncManager {
|
|
|
1558
1578
|
clearSpecConflictFile(changeName) {
|
|
1559
1579
|
const specDir = safePlatformSpecDir(this.cwd);
|
|
1560
1580
|
if (!specDir) return false;
|
|
1561
|
-
const p = join(specDir, '.runtime', `spec-sync-conflict-${changeName}.json`);
|
|
1581
|
+
const p = runtimeStatePath(join(specDir, '.runtime'), `spec-sync-conflict-${changeName}.json`);
|
|
1562
1582
|
if (!existsSync(p)) return false;
|
|
1563
1583
|
try { unlinkSync(p); return true; }
|
|
1564
1584
|
catch { return false; }
|
|
@@ -1571,7 +1591,7 @@ export class SyncManager {
|
|
|
1571
1591
|
clearConflictFile(changeName) {
|
|
1572
1592
|
const specDir = safePlatformSpecDir(this.cwd);
|
|
1573
1593
|
if (!specDir) return false;
|
|
1574
|
-
const conflictPath = join(specDir, '.runtime', `sync-conflict-${changeName}.json`);
|
|
1594
|
+
const conflictPath = runtimeStatePath(join(specDir, '.runtime'), `sync-conflict-${changeName}.json`);
|
|
1575
1595
|
if (!existsSync(conflictPath)) return false;
|
|
1576
1596
|
try { unlinkSync(conflictPath); return true; }
|
|
1577
1597
|
catch { return false; }
|
|
@@ -1881,6 +1901,30 @@ export class SyncManager {
|
|
|
1881
1901
|
// ①rm 前先把内容原子写到树外 `<specDir 根名>.local.yaml.bak`(树内会被 rm 一并删掉,
|
|
1882
1902
|
// 故放父目录);②恢复主路径不变,失败时从 .bak 再兜底复制一次;③成功路径末尾
|
|
1883
1903
|
// 清理 .bak(明文 token 不留第二份;顺带清历史失败残留)。
|
|
1904
|
+
// 防复活抑制(坑 spec-tree-file-resurrect,2026-10-16 下游实证):progress 冲突/熔断
|
|
1905
|
+
// 窗口内本地删除的文件(删除 op 未上行成功,服务器清单仍含它)会在整树快照回放时被
|
|
1906
|
+
// 服务器旧内容写回=复活(幽灵任务卡 + 验证误报「文件未清理」)。抑制判据无状态现算:
|
|
1907
|
+
// 基线快照(.runtime/spec-sync-base.json = 上次成功同步时刻的本地文件全集,语义见
|
|
1908
|
+
// spec-sync.js BASE_SNAPSHOT)有、当前盘面无 = 本地删除,回放时跳过这些条目;删除 op
|
|
1909
|
+
// 后续上行成功 → 服务器清单不再含该路径 → bundle 不再携带 → 抑制面自然衰减。
|
|
1910
|
+
// 基线在下方 rm 时会被连带删除(bundle 恒不含 .runtime),故必须在 rm 前读取。
|
|
1911
|
+
// restoreDeleted: true = 用户显式意志优先,跳过抑制(恢复纯快照回放语义)。
|
|
1912
|
+
const suppressResurrect = new Set();
|
|
1913
|
+
const resurrectSkipped = []; // 被防复活抑制挡下的条目(suppressResurrect 命中面;try 外可见供返回值携带)
|
|
1914
|
+
if (!opts.restoreDeleted) {
|
|
1915
|
+
try {
|
|
1916
|
+
const baseRaw = JSON.parse(readFileSync(runtimeStatePath(join(specDir, '.runtime'), 'spec-sync-base.json'), 'utf8'));
|
|
1917
|
+
const baseHashes = baseRaw && typeof baseRaw.hashes === 'object' ? baseRaw.hashes : null;
|
|
1918
|
+
if (baseHashes) {
|
|
1919
|
+
for (const p of Object.keys(baseHashes)) {
|
|
1920
|
+
const rel = String(p).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
1921
|
+
if (!rel || rel === 'local.yaml' || rel.startsWith('.runtime/')) continue;
|
|
1922
|
+
if (!existsSync(join(specDir, rel))) suppressResurrect.add(rel);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
} catch { /* 基线缺失/损坏 = 无抑制面(全新空环境全量回放,既有行为不变) */ }
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1884
1928
|
const localYamlPath = join(specDir, 'local.yaml');
|
|
1885
1929
|
const localYamlBakPath = join(dirname(specDir), `${basename(specDir)}.local.yaml.bak`);
|
|
1886
1930
|
let localYamlSaved = null; // null = 本无此文件,无需恢复
|
|
@@ -1916,6 +1960,11 @@ export class SyncManager {
|
|
|
1916
1960
|
const crlfEntries = [];
|
|
1917
1961
|
for (const e of entries) {
|
|
1918
1962
|
_assertSafeTarName(e.name.replace(/\/+$/, ''), specDir);
|
|
1963
|
+
const relName = e.name.replace(/^\.\//, '').replace(/\\/g, '/');
|
|
1964
|
+
if (!e.isDir && suppressResurrect.has(relName)) {
|
|
1965
|
+
resurrectSkipped.push(relName); // 基线有、盘面无=本地删除:不从快照复活
|
|
1966
|
+
continue;
|
|
1967
|
+
}
|
|
1919
1968
|
const fullPath = join(specDir, e.name);
|
|
1920
1969
|
if (e.isDir) {
|
|
1921
1970
|
mkdirSync(fullPath, { recursive: true });
|
|
@@ -1926,6 +1975,10 @@ export class SyncManager {
|
|
|
1926
1975
|
if (e.data && e.data.length < 4 * 1024 * 1024) crlfEntries.push([e.name, e.data]); // CRLF 画像采样(超 4MB 大文件跳过采样,写入不受影响)
|
|
1927
1976
|
}
|
|
1928
1977
|
}
|
|
1978
|
+
// 防复活抑制可见(宁可吵不可静默复活):被挡路径点名(截断防刷屏),出路指 --restore-deleted
|
|
1979
|
+
if (resurrectSkipped.length > 0) {
|
|
1980
|
+
console.warn(`⚠️ [sync] 防复活抑制:${resurrectSkipped.length} 个本地已删除文件未从快照恢复(基线有、盘面无=本地删除尚未上行成功;确要从快照恢复用 --restore-deleted): ${resurrectSkipped.slice(0, 5).join(', ')}${resurrectSkipped.length > 5 ? ' 等' : ''}`);
|
|
1981
|
+
}
|
|
1929
1982
|
// 批量写入留痕(2026-09-17 反馈⑥②法证收口):整树解包=逐字节回放,他机 CRLF 内容
|
|
1930
1983
|
// 原样落盘——审计行记 时间/触发面/文件数/CRLF 命中数(fail-open)。
|
|
1931
1984
|
try {
|
|
@@ -1937,6 +1990,7 @@ export class SyncManager {
|
|
|
1937
1990
|
crlfPaths: crlfPaths.slice(0, WRITE_AUDIT_FILE_CAP),
|
|
1938
1991
|
crlfCount: crlfPaths.length,
|
|
1939
1992
|
force: opts.force === true,
|
|
1993
|
+
resurrectSkippedCount: resurrectSkipped.length,
|
|
1940
1994
|
});
|
|
1941
1995
|
} catch { /* 审计 fail-open */ }
|
|
1942
1996
|
if (localYamlSaved !== null) {
|
|
@@ -1962,7 +2016,7 @@ export class SyncManager {
|
|
|
1962
2016
|
return { ok: false, pulled: false, specDir, reason: `解压 spec bundle 失败: ${err.message}` };
|
|
1963
2017
|
}
|
|
1964
2018
|
console.log(`[sync] 已拉取 spec 快照并解压到 ${specDir}${specVersion ? `(平台 spec_version: ${specVersion},打包时刻快照非实时)` : ''}`);
|
|
1965
|
-
return { ok: true, pulled: true, specDir, specVersion };
|
|
2019
|
+
return { ok: true, pulled: true, specDir, specVersion, ...(resurrectSkipped.length > 0 ? { resurrectSkipped } : {}) };
|
|
1966
2020
|
}
|
|
1967
2021
|
|
|
1968
2022
|
/**
|
|
@@ -2063,7 +2117,7 @@ export class SyncManager {
|
|
|
2063
2117
|
// 基线快照刷新(best-effort):被覆盖路径回写当前盘面 hash(= 服务器内容 hash)、被删
|
|
2064
2118
|
// 路径移除条目——下次同步这些路径按「本地未改动」豁免回推(take-platform 语义闭环)。
|
|
2065
2119
|
try {
|
|
2066
|
-
const baseFile = join(specDir, '.runtime', 'spec-sync-base.json');
|
|
2120
|
+
const baseFile = runtimeStatePath(join(specDir, '.runtime'), 'spec-sync-base.json');
|
|
2067
2121
|
let base = null;
|
|
2068
2122
|
try { base = JSON.parse(readFileSync(baseFile, 'utf8')); } catch { base = null; }
|
|
2069
2123
|
const hashes = base && typeof base.hashes === 'object' ? base.hashes : {};
|
|
@@ -2290,9 +2344,11 @@ export class SyncManager {
|
|
|
2290
2344
|
if (!specDir) return [];
|
|
2291
2345
|
const runtimeDir = join(specDir, '.runtime');
|
|
2292
2346
|
if (!existsSync(runtimeDir)) return [];
|
|
2347
|
+
// 2026-10-18 布局分层:冲突文件落 state/;根目录扫描保留旧布局兜底
|
|
2293
2348
|
let files = [];
|
|
2294
|
-
|
|
2295
|
-
|
|
2349
|
+
for (const dir of [join(runtimeDir, 'state'), runtimeDir]) {
|
|
2350
|
+
try { files.push(...readdirSync(dir)); } catch { /* 可选层缺失跳过 */ }
|
|
2351
|
+
}
|
|
2296
2352
|
const conflicts = [];
|
|
2297
2353
|
// HUB-08:同时扫描进度冲突(sync-conflict-*)与 spec 树冲突(spec-sync-conflict-*),
|
|
2298
2354
|
// type 字段供 status 展示与 resolve 分流
|
|
@@ -2302,7 +2358,7 @@ export class SyncManager {
|
|
|
2302
2358
|
else if (f.startsWith('spec-sync-conflict-') && f.endsWith('.json')) prefix = 'spec-sync-conflict-';
|
|
2303
2359
|
else continue;
|
|
2304
2360
|
const type = prefix === 'sync-conflict-' ? 'progress' : 'spec-tree';
|
|
2305
|
-
const filePath = join(runtimeDir, f);
|
|
2361
|
+
const filePath = existsSync(runtimeStatePath(runtimeDir, f)) ? runtimeStatePath(runtimeDir, f) : join(runtimeDir, f);
|
|
2306
2362
|
try {
|
|
2307
2363
|
const cf = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
2308
2364
|
conflicts.push({
|
package/src/task-done.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* `task start` 的受影响测试族注入不在本命令面(开工前置提示,保留不动)。
|
|
20
20
|
*/
|
|
21
21
|
import { existsSync, readFileSync, unlinkSync } from 'node:fs'
|
|
22
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
22
23
|
import { join } from 'node:path'
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -50,7 +51,7 @@ export async function runTaskDone({ changeName, cwd, taskId, verdict, notes = ''
|
|
|
50
51
|
let reviewSkipped = false
|
|
51
52
|
try {
|
|
52
53
|
let runId = ''
|
|
53
|
-
const markerFile =
|
|
54
|
+
const markerFile = runtimeStatePath(runtimeRoot, 'current-execute-run-id-' + changeName)
|
|
54
55
|
try {
|
|
55
56
|
if (existsSync(markerFile)) {
|
|
56
57
|
const c = readFileSync(markerFile, 'utf8').trim()
|
package/src/task-review.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'fs'
|
|
12
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
12
13
|
import { join, resolve, basename, dirname } from 'path'
|
|
13
14
|
import { execFileSync } from 'child_process'
|
|
14
15
|
import { git, unquoteGitPath } from './git-helper.js'
|
|
@@ -959,7 +960,7 @@ export function resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName = n
|
|
|
959
960
|
}
|
|
960
961
|
|
|
961
962
|
export function resolveLatestExecuteRunId({ runtimeRoot, changeName }) {
|
|
962
|
-
const marker =
|
|
963
|
+
const marker = runtimeStatePath(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
963
964
|
try {
|
|
964
965
|
if (existsSync(marker)) {
|
|
965
966
|
const c = readFileSync(marker, 'utf8').trim()
|
|
@@ -1077,7 +1078,7 @@ export function resolveExecuteRunForChange({ runtimeRoot, changeName, taskIds =
|
|
|
1077
1078
|
// 1. marker
|
|
1078
1079
|
let markerRunId = null
|
|
1079
1080
|
try {
|
|
1080
|
-
const marker =
|
|
1081
|
+
const marker = runtimeStatePath(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
1081
1082
|
if (existsSync(marker)) {
|
|
1082
1083
|
const c = readFileSync(marker, 'utf8').trim()
|
|
1083
1084
|
if (c && !isValidExecuteRunId(c)) console.warn(`[sillyspec] execute run marker 内容非法(期望 exec-YYYY-MM-DD-HHMMSS,实得 ${JSON.stringify(c.slice(0, 60))}),忽略并回退归属扫描`)
|
|
@@ -1341,7 +1342,7 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
1341
1342
|
}
|
|
1342
1343
|
|
|
1343
1344
|
// exec-id:与 Task Review Gate(gates.js:269)/ autoCheckPlanFromReviews 同源
|
|
1344
|
-
const runIdFile =
|
|
1345
|
+
const runIdFile = runtimeStatePath(runtimeRoot, 'current-execute-run-id-' + changeName)
|
|
1345
1346
|
let executeRunId = ''
|
|
1346
1347
|
try {
|
|
1347
1348
|
if (existsSync(runIdFile)) {
|
|
@@ -1637,7 +1638,7 @@ export async function adoptTaskReviewMechanics({ changeName, cwd, platformOpts =
|
|
|
1637
1638
|
// executeRunId:与草稿/gate 同源 marker。缺 marker 不新建 run——adopt 修的是当前 run 里
|
|
1638
1639
|
// 已存在的 review,marker 丢失说明无「当前 run」语义,无对象可修(gate 对 marker 缺失放行,
|
|
1639
1640
|
// 下游 Task Review Gate 兜底,这里同口径不造 run)。
|
|
1640
|
-
const runIdFile =
|
|
1641
|
+
const runIdFile = runtimeStatePath(runtimeRoot, 'current-execute-run-id-' + changeName)
|
|
1641
1642
|
let executeRunId = ''
|
|
1642
1643
|
try {
|
|
1643
1644
|
if (existsSync(runIdFile)) {
|
|
@@ -1969,7 +1970,7 @@ export async function writeTaskReview({
|
|
|
1969
1970
|
if (!existsSync(taskCardPath)) {
|
|
1970
1971
|
return { ok: false, errors: [`task 卡不存在:${taskCardPath}(先由 plan 阶段生成或手建)`], warnings }
|
|
1971
1972
|
}
|
|
1972
|
-
const markerFile =
|
|
1973
|
+
const markerFile = runtimeStatePath(runtimeRoot, 'current-execute-run-id-' + changeName)
|
|
1973
1974
|
let executeRunId = ''
|
|
1974
1975
|
try {
|
|
1975
1976
|
if (existsSync(markerFile)) {
|
|
@@ -1986,6 +1987,7 @@ export async function writeTaskReview({
|
|
|
1986
1987
|
// 排他认领(坑 exec-run-id-same-second-collision)+ 幂等 mkdir,认领碰撞换后缀
|
|
1987
1988
|
executeRunId = claimExecuteRunId(runtimeRoot, executeRunId)
|
|
1988
1989
|
mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
|
|
1990
|
+
try { mkdirSync(dirname(markerFile), { recursive: true }); } catch { /* fail-open */ }
|
|
1989
1991
|
writeFileSync(markerFile, executeRunId + '\n')
|
|
1990
1992
|
stampExecuteRunChange(runtimeRoot, executeRunId, changeName)
|
|
1991
1993
|
warnings.push(`无 execute run marker——已生成新 runId ${executeRunId} 并建目录(正常应先跑过 execute Wave 步)`)
|
package/src/task-tick.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* - 归档件只读:变更已归档时拒收(exit 2)。
|
|
20
20
|
*/
|
|
21
21
|
import { existsSync, readFileSync, appendFileSync } from 'node:fs'
|
|
22
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
22
23
|
import { join } from 'node:path'
|
|
23
24
|
|
|
24
25
|
/** 任务行(宽容缩进/星号有序标记,与哨兵判集行锚同口径 + 缩进容差)。 */
|
|
@@ -137,7 +138,7 @@ export async function runTaskTick({ changeName, cwd, taskId, specBase, evidence
|
|
|
137
138
|
try {
|
|
138
139
|
const { resolveRuntimeRoot } = await import('./run/shared.js')
|
|
139
140
|
const runtimeRoot = resolveRuntimeRoot({}, specBase)
|
|
140
|
-
const eventsPath =
|
|
141
|
+
const eventsPath = runtimeStatePath(runtimeRoot, `watcher-events-${changeName}.jsonl`)
|
|
141
142
|
const from = Math.max(0, r.checked - 1)
|
|
142
143
|
appendFileSync(eventsPath, JSON.stringify({
|
|
143
144
|
ts: Date.now(), kind: 'task-done', stage: 'tasks',
|
|
@@ -168,6 +169,27 @@ export async function runTaskTick({ changeName, cwd, taskId, specBase, evidence
|
|
|
168
169
|
? `|下一任务:${r.next.id} ${r.next.desc.slice(0, 40)}${r.next.desc.length > 40 ? '…' : ''}`
|
|
169
170
|
: `|🎉 ${r.total}/${r.total} 全勾——可收口:sillyspec flow done --change ${changeName}`
|
|
170
171
|
console.log(`${r.kind === 'ticked' ? '✅ 已勾' : 'ℹ️ 已是勾选态(幂等跳过写盘)'}(${r.checked}/${r.total})${nextTip}`)
|
|
172
|
+
if (r.kind === 'ticked') {
|
|
173
|
+
// 勾选随交付入库教学(2026-10-17-tick-timing-source,坑 tick-timing-squash-blindspot 根因 1/3
|
|
174
|
+
// 断链处):翻格后给现成提交命令——pathspec 含 tasks.md 与侧车(勾选修改与任务交付同一提交
|
|
175
|
+
// 入库,消息占位带 task-NN token),把纪律变成可抄的命令;并当场提醒「此前勾选仍未进
|
|
176
|
+
// git」(已勾 ≥2 格且 tasks.md 零提交)——反馈不再等到 flow done 终点。best-effort:
|
|
177
|
+
// git 不可用(非 git 目录/失败返回 null)只给命令不给提醒,失败不影响翻格与 exit code。
|
|
178
|
+
try {
|
|
179
|
+
const { relative } = await import('node:path')
|
|
180
|
+
const rel = (p) => relative(cwd, p).split('\\').join('/')
|
|
181
|
+
const sidecar = join(changesRoot, changeName, 'task-ticks.jsonl')
|
|
182
|
+
const paths = `${rel(tasksPath)}${existsSync(sidecar) ? ` ${rel(sidecar)}` : ''}`
|
|
183
|
+
console.log(`📦 勾选随交付入库(add 与 commit 同一清单——勾选修改随任务交付同一提交):`)
|
|
184
|
+
console.log(` git add -- <交付文件…> ${paths}`)
|
|
185
|
+
console.log(` git commit -m "<本任务交付概括> (${normalizeTaskId(taskId) || taskId})" -- <交付文件…> ${paths}`)
|
|
186
|
+
const { gitQuiet } = await import('./git-helper.js')
|
|
187
|
+
const touched = gitQuiet(cwd, ['log', '-n', '1', '--format=%h', '--', tasksPath])
|
|
188
|
+
if (r.checked >= 2 && touched === '') {
|
|
189
|
+
console.log(`ℹ️ tasks.md 尚无任何提交(此前勾选未随交付入库)——上面两行命令已把它带进清单,照抄即闭环`)
|
|
190
|
+
}
|
|
191
|
+
} catch { /* 教学面 best-effort:失败不影响翻格 */ }
|
|
192
|
+
}
|
|
171
193
|
return r
|
|
172
194
|
}
|
|
173
195
|
|
package/src/test-bindings.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { createHash } from 'crypto'
|
|
20
20
|
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
21
|
-
import { join } from 'path'
|
|
21
|
+
import { dirname, join } from 'path'
|
|
22
22
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
23
23
|
import { gitQuiet } from './git-helper.js'
|
|
24
24
|
import { quicklogSidecarPath } from './quicklog.js'
|
|
@@ -193,9 +193,38 @@ export function readChangeTrace(changeDir) {
|
|
|
193
193
|
} catch { return [] }
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
/** 写 trace(幂等:序列化字节不变跳写;原子:writeAtomicSync)
|
|
196
|
+
/** 写 trace(幂等:序列化字节不变跳写;原子:writeAtomicSync)
|
|
197
|
+
*
|
|
198
|
+
* 幽灵绑定过滤(2026-10-15-zcode-engine-appserver 归档期实证,跨仓事故驱动修复):
|
|
199
|
+
* machine 行的 tests 条目若在任一候选根(变更目录上溯项目根 / process.cwd())都不存在——
|
|
200
|
+
* 多为卡片文本误提的不存在路径(如 verify 命令笔误被 deps 推断/绑定扫描捕获)——丢弃该
|
|
201
|
+
* 条目并 console.warn 留痕;agent 行不动(修理工/用户绑定如实保留);行内条目全被滤光则
|
|
202
|
+
* 保留原行不滤(交由 apply/verify 的悬空闸门裁决,不在写侧静默删行)。
|
|
203
|
+
*/
|
|
197
204
|
export function writeChangeTrace(changeDir, changeName, rows) {
|
|
198
|
-
const
|
|
205
|
+
const projectRoots = (() => {
|
|
206
|
+
const cands = []
|
|
207
|
+
let d = changeDir
|
|
208
|
+
for (let i = 0; i < 3; i++) { d = dirname(d); cands.push(d.replace(/\\/g, '/')) }
|
|
209
|
+
cands.push(process.cwd().replace(/\\/g, '/'))
|
|
210
|
+
return [...new Set(cands)]
|
|
211
|
+
})()
|
|
212
|
+
const _existsSomewhere = (f) => projectRoots.some(root => existsSync(join(root, f)))
|
|
213
|
+
let _ghostDropped = []
|
|
214
|
+
const _filtered = rows.map(r => {
|
|
215
|
+
if (r.discovery === 'agent') return r
|
|
216
|
+
const files = (r.tests || []).map(t => testAnchorFile(t))
|
|
217
|
+
const ghosts = files.filter(f => !_existsSomewhere(f))
|
|
218
|
+
if (ghosts.length === 0) return r
|
|
219
|
+
const ghostKeys = new Set(ghosts)
|
|
220
|
+
_ghostDropped.push(...ghosts.map(g => `${r.row_id}: ${g}`))
|
|
221
|
+
const remaining = (r.tests || []).filter(t => !ghostKeys.has(testAnchorFile(t)))
|
|
222
|
+
return remaining.length > 0 ? { ...r, tests: remaining } : r
|
|
223
|
+
})
|
|
224
|
+
if (_ghostDropped.length > 0) {
|
|
225
|
+
console.warn('⚠️ [test-bindings] 幽灵绑定条目过滤:' + _ghostDropped.length + ' 个不存在的 tests 路径已从 machine 行丢弃(agent 行不动):\n ' + _ghostDropped.slice(0, 5).join('\n '))
|
|
226
|
+
}
|
|
227
|
+
const normalized = _filtered.map(normalizeRow)
|
|
199
228
|
const payload = JSON.stringify({
|
|
200
229
|
schemaVersion: 1, change: changeName, rows: normalized,
|
|
201
230
|
}, null, 2) + '\n'
|
package/src/ui-visual.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync, readFileSync } from 'node:fs'
|
|
19
19
|
import { join } from 'node:path'
|
|
20
|
+
import jsYaml from 'js-yaml'
|
|
21
|
+
import { parseRepoRegistry } from './stages/plan-postcheck.js'
|
|
20
22
|
|
|
21
23
|
export const UI_VISUAL_PROBE_HEADING = '#### 探针 12:UI 视觉证据(分级门)'
|
|
22
24
|
export const UI_EVIDENCE_FILENAME = 'visual-evidence.md'
|
|
@@ -48,8 +50,114 @@ export function detectUiTouchInPaths(paths) {
|
|
|
48
50
|
|
|
49
51
|
/* ── flow start 须知(仓中立;证据约定与探针同源)── */
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
/**
|
|
54
|
+
* local.yaml ui_prototype 段解析(2026-10-15-ui-proto-form-config,FR-01)。
|
|
55
|
+
* 每仓原型形态契约:form: real-code | single-html;豁免(single-html)须带 ruling{by,at,reason}
|
|
56
|
+
* 全三字段(用户裁决留痕——缺任一=登记不完整,消费方须警告);repos.<子仓键> 同构 override。
|
|
57
|
+
* fail-soft:段缺席/文件不可读/YAML 坏/值非法 → registered:false 零抛出(与 readUiVisualGate
|
|
58
|
+
* 同根同哲学)。诚实边界:工具只消费登记与注入指引,不校验原型质量。
|
|
59
|
+
*/
|
|
60
|
+
export function readUiPrototypeForm(specBase) {
|
|
61
|
+
const empty = { registered: false, form: null, ruling: null, rulingComplete: false, repos: {} }
|
|
62
|
+
if (!specBase) return empty
|
|
63
|
+
let raw
|
|
64
|
+
try {
|
|
65
|
+
raw = jsYaml.load(readFileSync(join(specBase, 'local.yaml'), 'utf8'))
|
|
66
|
+
} catch { return empty }
|
|
67
|
+
const section = raw && typeof raw === 'object' ? raw.ui_prototype : null
|
|
68
|
+
if (!section || typeof section !== 'object') return empty
|
|
69
|
+
const parseEntry = (e) => {
|
|
70
|
+
if (!e || typeof e !== 'object') return null
|
|
71
|
+
const form = e.form === 'real-code' || e.form === 'single-html' ? e.form : null
|
|
72
|
+
if (!form) return null
|
|
73
|
+
const r = e.ruling && typeof e.ruling === 'object' ? e.ruling : null
|
|
74
|
+
const rulingComplete = !!(r && r.by && r.at && r.reason)
|
|
75
|
+
return { form, ruling: r, rulingComplete }
|
|
76
|
+
}
|
|
77
|
+
const top = parseEntry(section)
|
|
78
|
+
const repos = {}
|
|
79
|
+
if (section.repos && typeof section.repos === 'object') {
|
|
80
|
+
for (const [key, val] of Object.entries(section.repos)) {
|
|
81
|
+
const entry = parseEntry(val)
|
|
82
|
+
if (entry) repos[key] = entry
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!top) return { registered: false, form: null, ruling: null, rulingComplete: false, repos }
|
|
86
|
+
return { registered: true, ...top, repos }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 多仓 override 解析(FR-02):触达路径与 local.yaml repos 注册匹配——子仓若在
|
|
91
|
+
* ui_prototype.repos 有 override 则该形态优先;未命中走仓级兜底。返回形态条目数组(去重):
|
|
92
|
+
* 每项 { scope: '仓级'|'<子仓键>', form, ruling, rulingComplete, registered }。
|
|
93
|
+
* 匹配三通道(评审 P2 清偿:注入语料常为相对路径,纯绝对前缀会让 override 静默失效):
|
|
94
|
+
* ① 绝对前缀:归一(反斜杠→斜杠/去尾斜杠/大小写不敏感)后 startsWith 注册路径;
|
|
95
|
+
* ② 相对键前缀:路径以 `<repoKey>/` 开头(语料常见 `spdemo/src/...` 形态);
|
|
96
|
+
* ③ 相对基名前缀:路径以注册路径 basename + `/` 开头(键与目录名不同名时兜底)。
|
|
97
|
+
*/
|
|
98
|
+
export function resolveUiPrototypeFor(specBase, paths = []) {
|
|
99
|
+
const proto = readUiPrototypeForm(specBase)
|
|
100
|
+
const out = []
|
|
101
|
+
const seen = new Set()
|
|
102
|
+
const push = (entry) => {
|
|
103
|
+
const key = `${entry.form}|${entry.rulingComplete}`
|
|
104
|
+
if (seen.has(key)) return
|
|
105
|
+
seen.add(key)
|
|
106
|
+
out.push(entry)
|
|
107
|
+
}
|
|
108
|
+
if (paths.length > 0) {
|
|
109
|
+
let repoPaths = {}
|
|
110
|
+
try {
|
|
111
|
+
// 复用既有注册解析(plan-postcheck _parseRepoEntries 同语义出口:字符串条目=路径、
|
|
112
|
+
// 对象条目取 path 子键)——不走 js-yaml 裸读,防对象形态静默漏配。
|
|
113
|
+
repoPaths = Object.fromEntries(parseRepoRegistry(readFileSync(join(specBase, 'local.yaml'), 'utf8')))
|
|
114
|
+
} catch { /* 解析失败=零子仓命中,仓级兜底 */ }
|
|
115
|
+
const norm = (p) => String(p).replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase()
|
|
116
|
+
for (const [key, registeredPath] of Object.entries(repoPaths)) {
|
|
117
|
+
if (typeof registeredPath !== 'string' || !registeredPath) continue
|
|
118
|
+
const reg = norm(registeredPath)
|
|
119
|
+
const regBase = reg.split('/').pop()
|
|
120
|
+
const hit = paths.some((p) => {
|
|
121
|
+
const np = norm(p)
|
|
122
|
+
return np.startsWith(reg + '/') || np === reg
|
|
123
|
+
|| np.startsWith(key.toLowerCase() + '/')
|
|
124
|
+
|| np.startsWith(regBase + '/')
|
|
125
|
+
})
|
|
126
|
+
const override = proto.repos[key]
|
|
127
|
+
if (hit && override) push({ scope: key, registered: true, ...override })
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
push(proto.registered
|
|
131
|
+
? { scope: '仓级', registered: true, form: proto.form, ruling: proto.ruling, rulingComplete: proto.rulingComplete }
|
|
132
|
+
: { scope: '仓级', registered: false, form: null, ruling: null, rulingComplete: false })
|
|
133
|
+
return out
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 须知第 5 节:按形态条目组合「本仓原型形态」行(FR-03 四态;生态命令词禁入——neutrality 契约)。 */
|
|
137
|
+
function uiPrototypeFormLines(entries) {
|
|
138
|
+
const lines = [`📐 本仓原型形态(local.yaml ui_prototype 登记;工具只查声明在场不验质量):`]
|
|
139
|
+
let anyContent = false
|
|
140
|
+
for (const e of entries) {
|
|
141
|
+
const scope = e.scope === '仓级' ? '' : `【子仓 ${e.scope}】`
|
|
142
|
+
if (!e.registered) {
|
|
143
|
+
anyContent = true
|
|
144
|
+
lines.push(` ⚠️ ${scope}未登记原型形态——本次按阶梯执行:有真实前端工具链→定稿原型必须真码;查无真码管线→先单开一个轻量变更搭最小入口(管线建设与业务变更分离),或登记豁免(form: single-html + ruling 用户裁决留痕)。收口前把 local.yaml ui_prototype 登记补上(强提醒,不阻断)。`)
|
|
145
|
+
} else if (e.form === 'single-html' && !e.rulingComplete) {
|
|
146
|
+
anyContent = true
|
|
147
|
+
lines.push(` ⚠️ ${scope}豁免登记不完整(single-html 缺 ruling{by,at,reason} 某字段)——这是静默漂移入口:补齐用户裁决留痕(by/at/reason)后再依赖豁免;本次仍按阶梯执行。`)
|
|
148
|
+
} else if (e.form === 'single-html') {
|
|
149
|
+
anyContent = true
|
|
150
|
+
lines.push(` 📋 ${scope}豁免生效(single-html;裁决 by=${e.ruling.by} at=${e.ruling.at}):定稿原型=自包含 HTML + 真值 token(来源须声明:token 源文件/组件参照),反向翻译税为已裁决接受项。`)
|
|
151
|
+
} else {
|
|
152
|
+
anyContent = true
|
|
153
|
+
lines.push(` 🔒 ${scope}真码契约(real-code):定稿原型必须与实现同方言真码;查无管线先单开轻量变更搭最小入口(bootstrap 与业务变更分离)。`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return anyContent ? lines : []
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function buildUiGuidanceLines(specBase = null, paths = []) {
|
|
160
|
+
const lines = [
|
|
53
161
|
`🎨 UI 变更执行须知(本变更触及前端页面或 UI——advisory 引导非门禁):`,
|
|
54
162
|
` 1. 定稿原型必须是可复跑的真码产物(与实现同方言、能被本项目工具链构建)——手绘单文件`,
|
|
55
163
|
` 仅限一次性粗选对比,不作为交付原型(2026-09-28 frontend-apple-style 实证:手绘定稿后每条意见都要反向翻译回真实代码);`,
|
|
@@ -62,6 +170,13 @@ export function buildUiGuidanceLines() {
|
|
|
62
170
|
` 必须停下找用户签字,裁决记录写进 ${UI_EVIDENCE_FILENAME}(含「用户裁决」字样段)——`,
|
|
63
171
|
` 静默降级会在收口被「UI 视觉证据」探针硬拦(error 门)。`,
|
|
64
172
|
]
|
|
173
|
+
if (specBase) {
|
|
174
|
+
try {
|
|
175
|
+
const entries = resolveUiPrototypeFor(specBase, paths)
|
|
176
|
+
lines.push('', ...uiPrototypeFormLines(entries))
|
|
177
|
+
} catch { /* 登记解析失败零打扰(fail-soft 同 readUiVisualGate) */ }
|
|
178
|
+
}
|
|
179
|
+
return lines
|
|
65
180
|
}
|
|
66
181
|
|
|
67
182
|
/* ── 分级探针 ── */
|
|
@@ -173,8 +288,8 @@ export function uiEvidenceExecuteAdvisory({ changeDir, specBase = null }) {
|
|
|
173
288
|
}
|
|
174
289
|
}
|
|
175
290
|
|
|
176
|
-
/** design 文件变更清单行的路径粗提(反引号/裸路径行——探针侧仅作 UI
|
|
177
|
-
function extractQuotedPaths(designText) {
|
|
291
|
+
/** design 文件变更清单行的路径粗提(反引号/裸路径行——探针侧仅作 UI 扩展名兜底命中,不追求全解;导出供 prompt.js 组子仓 override 匹配路径用)。 */
|
|
292
|
+
export function extractQuotedPaths(designText) {
|
|
178
293
|
if (!designText) return []
|
|
179
294
|
const paths = []
|
|
180
295
|
for (const m of designText.matchAll(/`([^`]+)`/g)) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* 无既有 validator 的 required-evidence 与 endpoints(容忍缺省,出场才算)。
|
|
12
12
|
*/
|
|
13
13
|
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
14
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
14
15
|
import { join } from 'node:path'
|
|
15
16
|
|
|
16
17
|
function checkJson(path, fn) {
|
|
@@ -66,7 +67,7 @@ export async function validateChangeArtifacts({ cwd, specBase, changeName, runti
|
|
|
66
67
|
|
|
67
68
|
// ③ task review.json(最新 execute run 的逐 task 产物——既有 validateReviewSchema)
|
|
68
69
|
{
|
|
69
|
-
const markerPath =
|
|
70
|
+
const markerPath = runtimeStatePath(rt, `current-execute-run-id-${changeName}`)
|
|
70
71
|
const runId = existsSync(markerPath) ? readFileSync(markerPath, 'utf8').trim() : null
|
|
71
72
|
if (!runId) {
|
|
72
73
|
checks.push({ artifact: 'task reviews', status: 'skip', reason: '无 execute run marker(未经 execute 或已清理)' })
|