sillyspec 3.29.5 → 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-brainstorm.md +1 -1
- package/assets/command-cards/run-quick.md +8 -25
- 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 +18 -5
- 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/optimization-plan.md +165 -0
- package/round4/r4-final-report.html +347 -0
- package/round4/r5-scenario-matrix.md +46 -0
- package/src/change-list.js +9 -0
- package/src/classify-change.js +6 -0
- package/src/command-cards.js +2 -1
- package/src/commit-guard.js +2 -0
- package/src/config-schema.js +64 -12
- package/src/db.js +35 -4
- package/src/decision-distill.js +9 -3
- 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 +570 -16
- 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 +107 -5
- package/src/mcp-server.js +1 -0
- 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 +123 -10
- package/src/run/complete-handlers.js +327 -72
- package/src/run/complete.js +350 -71
- package/src/run/gate-snapshot-ledger.js +213 -0
- package/src/run/gate-snapshot.js +250 -34
- package/src/run/gates.js +370 -233
- 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 +185 -12
- package/src/run/shared.js +120 -3
- 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 +221 -84
- package/src/stages/knowledge.js +74 -1
- package/src/stages/plan-postcheck.js +352 -1
- 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/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 +860 -158
- package/src/verify-probes.js +59 -3
- 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/sync.js
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* HTTP 请求:Node.js 原生 fetch(Node 22+)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync, chmodSync, rmSync } from 'fs';
|
|
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 { join, dirname, basename, isAbsolute, relative } from 'path';
|
|
14
|
+
import { join, dirname, basename, isAbsolute, relative, resolve as pathResolve } from 'path';
|
|
15
15
|
import { homedir } from 'os';
|
|
16
16
|
import { resolvePlatformSpecDir } from './progress.js';
|
|
17
17
|
import { safeGit } from './git-helper.js';
|
|
@@ -68,6 +68,60 @@ function readLocalYaml(cwd) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* 平台凭据解析(含向上回退链,2026-09-24 fr-test-readside 平台断流实证修复):
|
|
73
|
+
* env(SILLYHUB_PLATFORM_URL/TOKEN)最高优先 → cwd 的 local.yaml platform 段 →
|
|
74
|
+
* 向上最多 7 级父目录逐层找 <dir>/.sillyspec/local.yaml 的 platform 段。
|
|
75
|
+
* 治「平台根 worktree 内跑命令」:worktree 的 local.yaml 是 gitignored 凭据不随
|
|
76
|
+
* checkout 跟进 → 无 platform 段 → 判未连接 → triggerSync 静默 no-op(verify/archive
|
|
77
|
+
* 瘦会话 cwd 在平台 worktree 时同步全程断流,bg 日志 13:29 后零同步轮实证)。
|
|
78
|
+
* 向上回退天然覆盖「worktree → 宿主仓」布局(宿主仓 local.yaml 持有连接凭据)。
|
|
79
|
+
* @returns {{url:string, token:string}|null}
|
|
80
|
+
*/
|
|
81
|
+
export function readPlatformConfig(cwd, opts = {}) {
|
|
82
|
+
if (process.env.SILLYHUB_PLATFORM_URL && process.env.SILLYHUB_PLATFORM_TOKEN) {
|
|
83
|
+
return { url: process.env.SILLYHUB_PLATFORM_URL, token: process.env.SILLYHUB_PLATFORM_TOKEN };
|
|
84
|
+
}
|
|
85
|
+
const maxDepth = Number.isInteger(opts.maxDepth) ? opts.maxDepth : 7; // opts.maxDepth 供单测隔离
|
|
86
|
+
const fromDir = (d) => { const p = readLocalYaml(d).platform; return p && p.url && p.token ? p : null };
|
|
87
|
+
const dir = pathResolve(cwd || process.cwd());
|
|
88
|
+
// ① 定 git 顶层(worktree 的 .git 是文件,也算顶层标记):无 git 的目录链只看本层
|
|
89
|
+
// ——回退链只在仓内有界,禁爬出仓外(家目录/祖先仓的凭据不该被任意子目录继承;
|
|
90
|
+
// 坑 check-approval-status 回归:tmpdir fixture 判「未连接」曾被无界回退翻真)。
|
|
91
|
+
let top = null;
|
|
92
|
+
let cur = dir;
|
|
93
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
94
|
+
if (existsSync(join(cur, '.git'))) { top = cur; break; }
|
|
95
|
+
const parent = dirname(cur);
|
|
96
|
+
if (parent === cur) break;
|
|
97
|
+
cur = parent;
|
|
98
|
+
}
|
|
99
|
+
if (!top) return fromDir(dir);
|
|
100
|
+
// ② 仓内 cwd→顶层逐层查
|
|
101
|
+
cur = dir;
|
|
102
|
+
for (;;) {
|
|
103
|
+
const hit = fromDir(cur);
|
|
104
|
+
if (hit) return hit;
|
|
105
|
+
if (cur === top) break;
|
|
106
|
+
cur = dirname(cur);
|
|
107
|
+
}
|
|
108
|
+
// ③ worktree(顶层 .git 是文件)→ 宿主仓根(gitdir: <host>/.git/worktrees/<name>,
|
|
109
|
+
// 从 dirname(gitdir) 再上溯两次到 <host>)
|
|
110
|
+
try {
|
|
111
|
+
const dotGit = join(top, '.git');
|
|
112
|
+
if (statSync(dotGit).isFile()) {
|
|
113
|
+
const m = readFileSync(dotGit, 'utf8').match(/^gitdir:\s*(.+)$/m);
|
|
114
|
+
if (m) {
|
|
115
|
+
let host = pathResolve(dirname(m[1].trim()));
|
|
116
|
+
for (let k = 0; k < 2; k++) host = dirname(host);
|
|
117
|
+
const hostHit = fromDir(host);
|
|
118
|
+
if (hostHit) return hostHit;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch { /* gitdir 指针损坏 → 按无宿主处理 */ }
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
71
125
|
/** 读 local.yaml 原始文本(保留注释/换行/结构),不存在返回 ''。供 connect/disconnect 文本级改写。 */
|
|
72
126
|
function readLocalYamlRaw(cwd) {
|
|
73
127
|
const p = join(cwd, LOCAL_YAML);
|
|
@@ -674,9 +728,10 @@ export class SyncManager {
|
|
|
674
728
|
this._suppressDocsMissingWarn = archivedQuietly;
|
|
675
729
|
|
|
676
730
|
// X1 墓碑(design §5.5,变更 2026-08-29-change-delete-closure-and-spec-pull task-13):
|
|
677
|
-
// 触发判据见循环内 tombstoneDue(DB status='archived')。
|
|
731
|
+
// 触发判据见循环内 tombstoneDue(DB status='archived'|'deleted')。
|
|
678
732
|
// 常规终态推送保持原语义(archived/active 原值照推,platform-sync-archive-final-state 钉死),
|
|
679
|
-
// 墓碑作为推送链成功后的追加 POST(见 _pushTombstone
|
|
733
|
+
// 墓碑作为推送链成功后的追加 POST(见 _pushTombstone;终态透传,archived 链不伪装 deleted——
|
|
734
|
+
// 2026-09-23 archive-tombstone 坑修复)。
|
|
680
735
|
|
|
681
736
|
const MAX_PUSH_ATTEMPTS = 2;
|
|
682
737
|
for (let attempt = 1; attempt <= MAX_PUSH_ATTEMPTS; attempt++) {
|
|
@@ -700,7 +755,10 @@ export class SyncManager {
|
|
|
700
755
|
// (unregisterChange 链——归档收尾/自愈/quick 收尾均置该值)或 status='deleted'
|
|
701
756
|
// (change-delete 命令,2026-08-30——载荷本身携带 deleted,平台 _apply_cli_tombstone
|
|
702
757
|
// 见该值即置 location='deleted';追加墓碑 POST 幂等兜底)触发,常规推送成功后追加
|
|
703
|
-
//
|
|
758
|
+
// 一次终态墓碑(终态透传:archived 链发 status='archived'、deleted 链发 'deleted'——
|
|
759
|
+
// 2026-09-23 前一律写死 'deleted' 致归档链被平台当删除软删,archive-tombstone 坑修复;
|
|
760
|
+
// 平台 task-04 写路径对 'deleted' 置 location='deleted' 收敛镜像,对 'archived' 新平台
|
|
761
|
+
// 置 location='archive'、旧平台读时投影零动作)。
|
|
704
762
|
// 不再把「实体目录双失」当裸删发墓碑:platform pull(进度同步)只写 DB 不建目录 →
|
|
705
763
|
// DB active + 目录双失是合法态,误发墓碑会让平台把活跃变更软删(全体成员生效)。
|
|
706
764
|
// 真实裸删(用户手动 rm -rf 目录)由平台镜像收敛兜底(spec-sync delete ops → scoped
|
|
@@ -971,20 +1029,27 @@ export class SyncManager {
|
|
|
971
1029
|
}
|
|
972
1030
|
|
|
973
1031
|
/**
|
|
974
|
-
* X1
|
|
975
|
-
*
|
|
976
|
-
* Change.location='deleted'
|
|
1032
|
+
* X1:把终态写进墓碑载荷——**终态透传**(2026-09-23 archive-tombstone 坑修复):DB
|
|
1033
|
+
* status='deleted'(change-delete 链)透传 'deleted',平台写路径 `_apply_cli_tombstone`
|
|
1034
|
+
* 见值置 Change.location='deleted' 并镜像软删(行为不变);DB status='archived'
|
|
1035
|
+
* (unregisterChange 链)透传 'archived'——此前一律写死 'deleted',归档链在平台被当
|
|
1036
|
+
* 删除软删、面板「已归档」tab 隐身(MP 仓 docs/sillyspec/archive-tombstone-*.md 实证:
|
|
1037
|
+
* 两工作区受影响,真删除历史仅 3 例且均非 archive 阶段)。'archived' 载荷旧平台走
|
|
1038
|
+
* 读时投影零 location 动作(天然兼容),新平台写路径置 location='archive'。
|
|
977
1039
|
* @param {object} progressData serializeForSync 输出(原地修改,返回同一引用)
|
|
978
1040
|
*/
|
|
979
1041
|
_applyTombstoneStatus(progressData) {
|
|
980
1042
|
if (progressData && Array.isArray(progressData.changes) && progressData.changes[0]) {
|
|
981
|
-
progressData.changes[0].status
|
|
1043
|
+
const terminal = progressData.changes[0].status;
|
|
1044
|
+
progressData.changes[0].status = terminal === 'deleted' ? 'deleted' : 'archived';
|
|
982
1045
|
}
|
|
983
1046
|
return progressData;
|
|
984
1047
|
}
|
|
985
1048
|
|
|
986
1049
|
/**
|
|
987
|
-
* X1 墓碑上行(design §5.5 / task-13):同端点同结构的一次追加 POST
|
|
1050
|
+
* X1 墓碑上行(design §5.5 / task-13):同端点同结构的一次追加 POST(终态透传:
|
|
1051
|
+
* changes[].status='archived'|'deleted',见 _applyTombstoneStatus——2026-09-23 起归档链
|
|
1052
|
+
* 不再伪装成 deleted)。
|
|
988
1053
|
*
|
|
989
1054
|
* 由 sync() 成功路径在常规推送 + 文档/spec 树链之后触发(CLI 单进程顺序推送,base_ts 进程内
|
|
990
1055
|
* 单调,无乐观锁冲突)。fresh 重读 DB 序列化——常规推送成功的 base_ts 回填已落库,本推送带上
|
|
@@ -1022,7 +1087,7 @@ export class SyncManager {
|
|
|
1022
1087
|
signal: opts.signal,
|
|
1023
1088
|
});
|
|
1024
1089
|
if (res.ok) {
|
|
1025
|
-
console.log(`[sync]
|
|
1090
|
+
console.log(`[sync] 已上行终态墓碑(status=${progressData.changes?.[0]?.status || '?'}): ${changeName}`);
|
|
1026
1091
|
try {
|
|
1027
1092
|
const ackTs = res.body && typeof res.body.last_pushed_at === 'string' ? res.body.last_pushed_at : pushedAt;
|
|
1028
1093
|
pm._updatePlatformLastSync(this.cwd, changeName, ackTs);
|
|
@@ -1240,14 +1305,9 @@ export class SyncManager {
|
|
|
1240
1305
|
* (2026-08-26)后平台模式回传的主凭据实为 local.yaml platform 段(env 缺席时)。
|
|
1241
1306
|
*/
|
|
1242
1307
|
_getPlatform() {
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
token: process.env.SILLYHUB_PLATFORM_TOKEN,
|
|
1247
|
-
};
|
|
1248
|
-
}
|
|
1249
|
-
const config = readLocalYaml(this.cwd);
|
|
1250
|
-
return config.platform || null;
|
|
1308
|
+
// 凭据走 readPlatformConfig 回退链(env → 本仓 local.yaml → 向上父目录)——平台根
|
|
1309
|
+
// worktree cwd(gitignored 凭据不随 checkout 跟进)也能借宿主仓凭据连上(同 peek 修法)
|
|
1310
|
+
return readPlatformConfig(this.cwd);
|
|
1251
1311
|
}
|
|
1252
1312
|
|
|
1253
1313
|
/**
|
|
@@ -2287,10 +2347,8 @@ export async function syncSpecTreeOnly(changeName, cwd, opts = {}) {
|
|
|
2287
2347
|
* 未连接时不为一次注定 no-op 的同步 spawn 后台子进程(本地独立用户零开销零行为变化)。
|
|
2288
2348
|
* 只做连接性预判不携带凭据:子进程自行经 _getPlatform 取全量配置(判据同源必一致)。
|
|
2289
2349
|
*/
|
|
2290
|
-
export function peekPlatformConnected(cwd) {
|
|
2291
|
-
|
|
2292
|
-
const p = readLocalYaml(cwd).platform;
|
|
2293
|
-
return Boolean(p && p.url && p.token);
|
|
2350
|
+
export function peekPlatformConnected(cwd, opts = {}) {
|
|
2351
|
+
return Boolean(readPlatformConfig(cwd, opts));
|
|
2294
2352
|
}
|
|
2295
2353
|
|
|
2296
2354
|
// TBD-hub-api: approve/reject 端点路径与请求体以 SillyHub 仓库实际 API 为准;
|
package/src/task-done.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task-done.js — `sillyspec task done` 四合一(r5l-forensic-verdict 方案 1 / 评审护栏#3)。
|
|
3
|
+
*
|
|
4
|
+
* 背景(R5-L 法证桶①):13 任务 × 每任务 4 连往返(task start → 实现 → review write →
|
|
5
|
+
* task finish,wt-commit 另计)≈ 52 次 CLI 调用纯属可合并——每次往返按当时全量上下文计费,
|
|
6
|
+
* 合并后每次省一次 ~300K 重发。本命令把收尾三连(review write 落 review.json + tasks.md
|
|
7
|
+
* 自动勾选 → task finish 清进行中标记 → 可选 wt-commit)收进单进程串行执行,输出合并为
|
|
8
|
+
* 分段结果行(一次调用一段可 grep 的结果,不再三条命令三段散落)。
|
|
9
|
+
*
|
|
10
|
+
* 原子性(评审护栏#3,硬约束):
|
|
11
|
+
* - 每子步先查自身完成标记,幂等跳过——review.json 已在且 verdict 指纹一致 → 跳过该步;
|
|
12
|
+
* finish 标记不在 → 已完工跳过;wt-commit 无变更自然 skip(runWtCommit 既有语义)。
|
|
13
|
+
* - 中段失败精确报告已完成子步(哪些已落盘、断在哪),重入直接重跑同命令从断点续。
|
|
14
|
+
* - verdict 指纹不一致的已存在 review(改判场景)默认拒改写——继承 writeTaskReview 拒覆盖
|
|
15
|
+
* 语义,显式 --force 越过(与 review write 单独通道同款逃生门)。
|
|
16
|
+
*
|
|
17
|
+
* ownership 继承:不新增判定也不旁路既有判定——writeTaskReview 的拒覆盖/evidence 硬校验、
|
|
18
|
+
* runWtCommit 的 worktree 定位/文件锁/显式 pathspec 纪律原样生效(本模块只做编排)。
|
|
19
|
+
* `task start` 的受影响测试族注入不在本命令面(开工前置提示,保留不动)。
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readFileSync, unlinkSync } from 'node:fs'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {{
|
|
26
|
+
* changeName: string, cwd: string, taskId: string, verdict: string,
|
|
27
|
+
* notes?: string, evidence?: string|null,
|
|
28
|
+
* commitMessage?: string|null, pathspecs?: string[], pathspecFile?: string|null,
|
|
29
|
+
* markerDir?: string|null, platformOpts?: object, force?: boolean,
|
|
30
|
+
* }} opts
|
|
31
|
+
* @returns {{ok: boolean, steps: Array<{name: string, status: string, detail: string}>}}
|
|
32
|
+
*/
|
|
33
|
+
export async function runTaskDone({ changeName, cwd, taskId, verdict, notes = '', evidence = null,
|
|
34
|
+
commitMessage = null, pathspecs = [], pathspecFile = null, markerDir = null, platformOpts = {}, force = false,
|
|
35
|
+
baseOverride = null, headOverride = null, changedFilesOverride = null }) {
|
|
36
|
+
const steps = []
|
|
37
|
+
const step = (name, status, detail) => { steps.push({ name, status, detail }) }
|
|
38
|
+
const specBase = (platformOpts && platformOpts.specRoot) || join(cwd, '.sillyspec')
|
|
39
|
+
const { resolveRuntimeRoot } = await import('./run/shared.js')
|
|
40
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
41
|
+
const { writeTaskReview, resolveLatestExecuteRunIdWithTasks, isValidExecuteRunId } = await import('./task-review.js')
|
|
42
|
+
// review.json 路径锚(子步 1 落定;子步 4 提交后回填 head 用——坑 task-done-head-premature:
|
|
43
|
+
// review 写在 wt-commit 之前,head 快照停在基线 → Task Review Gate 的 base..head 路径切片
|
|
44
|
+
// 为空被误判「零改动伪造」(R7 会话 5 份手工重对的机制化根治,2026-09-22)。
|
|
45
|
+
let reviewPathForBackfill = null
|
|
46
|
+
|
|
47
|
+
// ── 子步 1:review.json(幂等标记 = 同 executeRunId 同 task 同双 verdict 已在)──
|
|
48
|
+
// executeRunId 解析与 writeTaskReview 同源:marker 文件优先,缺省回退最新含 tasks 的 run。
|
|
49
|
+
let reviewWritten = false
|
|
50
|
+
let reviewSkipped = false
|
|
51
|
+
try {
|
|
52
|
+
let runId = ''
|
|
53
|
+
const markerFile = join(runtimeRoot, 'current-execute-run-id-' + changeName)
|
|
54
|
+
try {
|
|
55
|
+
if (existsSync(markerFile)) {
|
|
56
|
+
const c = readFileSync(markerFile, 'utf8').trim()
|
|
57
|
+
if (isValidExecuteRunId(c)) runId = c
|
|
58
|
+
}
|
|
59
|
+
} catch { /* 读失败走回退 */ }
|
|
60
|
+
if (!runId) runId = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName }) || ''
|
|
61
|
+
const existingPath = runId ? join(runtimeRoot, 'execute-runs', runId, 'tasks', taskId, 'review.json') : null
|
|
62
|
+
if (existingPath) reviewPathForBackfill = existingPath
|
|
63
|
+
if (existingPath && existsSync(existingPath)) {
|
|
64
|
+
let existing = null
|
|
65
|
+
try { existing = JSON.parse(readFileSync(existingPath, 'utf8')) } catch { /* 损坏按不存在处理 */ }
|
|
66
|
+
if (existing && existing.specVerdict === verdict && existing.qualityVerdict === verdict) {
|
|
67
|
+
reviewSkipped = true
|
|
68
|
+
step('review.json', 'skipped', `已在且 verdict 指纹一致(spec=${verdict} quality=${verdict},${existingPath})——幂等跳过`)
|
|
69
|
+
} else if (!force) {
|
|
70
|
+
step('review.json', 'failed', `已存在但 verdict 不一致(现存 spec=${existing ? existing.specVerdict : '?'} quality=${existing ? existing.qualityVerdict : '?'} ≠ 请求 ${verdict}):${existingPath}——改判需显式 --force(保护既有 reviewer 结论,与 review write 拒覆盖同款)`)
|
|
71
|
+
return finishReport({ ok: false, steps, changeName, taskId })
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!reviewSkipped) {
|
|
75
|
+
const preExisting = existingPath ? existsSync(existingPath) : false
|
|
76
|
+
const r = await writeTaskReview({
|
|
77
|
+
changeName, cwd, taskId,
|
|
78
|
+
specVerdict: verdict, qualityVerdict: verdict,
|
|
79
|
+
reviewerNotes: notes || '',
|
|
80
|
+
requiredEvidence: evidence ? [evidence] : [],
|
|
81
|
+
baseOverride, headOverride, changedFilesOverride,
|
|
82
|
+
force, platformOpts,
|
|
83
|
+
})
|
|
84
|
+
if (r.ok) {
|
|
85
|
+
reviewWritten = true
|
|
86
|
+
reviewPathForBackfill = r.reviewPath || reviewPathForBackfill
|
|
87
|
+
step('review.json', 'done', `${preExisting ? '覆盖写入(--force)' : '写入'} ${r.reviewPath}(executeRunId=${r.executeRunId})`)
|
|
88
|
+
} else {
|
|
89
|
+
step('review.json', 'failed', r.errors.join(';'))
|
|
90
|
+
return finishReport({ ok: false, steps, changeName, taskId })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
step('review.json', 'failed', `解析/写入异常:${e && e.message ? e.message : e}`)
|
|
95
|
+
return finishReport({ ok: false, steps, changeName, taskId })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── 子步 2:tasks.md checkbox 自动勾选(review write 单通道同款内嵌;幂等重跑无害)──
|
|
99
|
+
// task 真源归一(P2-f):review.json verdict 是唯一真源,tasks.md 勾选是显示态,CLI 唯一勾选者。
|
|
100
|
+
try {
|
|
101
|
+
const { autoCheckPlanFromReviews } = await import('./run/complete.js')
|
|
102
|
+
const ac = await autoCheckPlanFromReviews({ stageName: 'execute', changeName, cwd, platformOpts })
|
|
103
|
+
if (ac.autoChecked && ac.checkedCount > 0) {
|
|
104
|
+
step('tasks.md 勾选', 'done', `按 review verdict 自动勾选 ${ac.checkedCount} 个(task 真源 = review.json,勿手动勾选)`)
|
|
105
|
+
} else {
|
|
106
|
+
step('tasks.md 勾选', 'skipped', '无可新勾选项(已勾/无卡——autoCheck 幂等)')
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// 勾选 fail-soft(review write 单通道同款语义):execute --done 时 autoCheck 兜底
|
|
110
|
+
step('tasks.md 勾选', 'skipped', `勾选降级(fail-soft,--done 时 autoCheck 兜底):${e && e.message ? e.message : e}`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── 子步 3:task finish(幂等标记 = 进行中标记文件不在 → 已完工跳过)──
|
|
114
|
+
const markerPath = markerDir ? join(markerDir, `${taskId}.json`) : null
|
|
115
|
+
if (markerPath && existsSync(markerPath)) {
|
|
116
|
+
try {
|
|
117
|
+
unlinkSync(markerPath)
|
|
118
|
+
step('task finish', 'done', `进行中标记已清除(${markerPath})`)
|
|
119
|
+
} catch (e) {
|
|
120
|
+
step('task finish', 'failed', `标记清除失败:${e && e.message ? e.message : e}`)
|
|
121
|
+
return finishReport({ ok: false, steps, changeName, taskId })
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
step('task finish', 'skipped', markerPath ? '无进行中标记(已完工或从未 start)' : '无 markerDir(非 task start 流程,跳过)')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── 子步 4:wt-commit(可选;runWtCommit 自带 worktree 定位/文件锁/无变更 skip)──
|
|
128
|
+
if (!commitMessage) {
|
|
129
|
+
step('wt-commit', 'skipped', '未请求 --commit(如需提交工作区改动:加 --commit -m "<信息>" -- <精确路径>')
|
|
130
|
+
} else {
|
|
131
|
+
try {
|
|
132
|
+
const { runWtCommit } = await import('./wt-commit.js')
|
|
133
|
+
const r = await runWtCommit({ changeName, message: commitMessage, pathspecs, pathspecFile, cwd })
|
|
134
|
+
// head 回填(坑 task-done-head-premature 根治):review 先于 commit 写入,head 停在基线
|
|
135
|
+
// → Task Review Gate base..head 切片空误判零改动。提交成功(HEAD 前移)即把 review.head
|
|
136
|
+
// 重锚为本提交全哈希+headBackfilledAt 审计戳;fail-soft(回填失败只警示不改判提交结果)。
|
|
137
|
+
let backfillNote = ''
|
|
138
|
+
if (!r.skipped && r.head && reviewPathForBackfill && existsSync(reviewPathForBackfill)) {
|
|
139
|
+
try {
|
|
140
|
+
const rev = JSON.parse(readFileSync(reviewPathForBackfill, 'utf8'))
|
|
141
|
+
if (rev && rev.head !== r.head) {
|
|
142
|
+
rev.head = r.head
|
|
143
|
+
rev.headBackfilledAt = new Date().toISOString()
|
|
144
|
+
const { writeAtomicSync } = await import('./fs-atomic.js')
|
|
145
|
+
writeAtomicSync(reviewPathForBackfill, JSON.stringify(rev, null, 2) + '\n')
|
|
146
|
+
backfillNote = `;review.head 已回填 ${r.shortHead}(提交后重锚)`
|
|
147
|
+
}
|
|
148
|
+
} catch (e) {
|
|
149
|
+
backfillNote = `;review.head 回填失败(fail-soft): ${(e && e.message) || e}`
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
step('wt-commit', r.skipped ? 'skipped' : 'done',
|
|
153
|
+
r.skipped ? `无变更跳过(HEAD 不动):${r.shortHead}` : `${r.shortHead}(${r.files.length} 文件,worktree=${r.worktreePath})${backfillNote}`)
|
|
154
|
+
} catch (e) {
|
|
155
|
+
step('wt-commit', 'failed', `${e && e.message ? e.message : e}`)
|
|
156
|
+
return finishReport({ ok: false, steps, changeName, taskId })
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return finishReport({ ok: true, steps, changeName, taskId })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 统一报告渲染:四段结果行 + 失败时已完成子步精确点名与重入指引 */
|
|
164
|
+
function finishReport({ ok, steps, changeName, taskId }) {
|
|
165
|
+
const icon = (s) => (s.status === 'done' ? '✅' : s.status === 'skipped' ? '⏭️ ' : '❌')
|
|
166
|
+
if (ok) {
|
|
167
|
+
console.log(`✅ task done [${taskId}] @ ${changeName}:${steps.length} 个子步全部完成(done/skipped 均为终态)`)
|
|
168
|
+
} else {
|
|
169
|
+
console.error(`❌ task done [${taskId}] @ ${changeName} 中断:`)
|
|
170
|
+
}
|
|
171
|
+
steps.forEach((s, i) => {
|
|
172
|
+
const line = ` ${i + 1}. [${s.name}] ${icon(s)} ${s.status}——${s.detail}`
|
|
173
|
+
if (ok) console.log(line); else console.error(line)
|
|
174
|
+
})
|
|
175
|
+
if (!ok) {
|
|
176
|
+
const done = steps.filter((s) => s.status === 'done' || s.status === 'skipped')
|
|
177
|
+
console.error(` 已完成子步:${done.length > 0 ? done.map((s) => s.name).join('、') : '(无——首子步即失败)'}`)
|
|
178
|
+
console.error(` 重入:修复失败原因后重跑同一条命令——已完成子步幂等跳过,从断点续(review verdict 指纹一致即跳过写入)`)
|
|
179
|
+
}
|
|
180
|
+
return { ok, steps }
|
|
181
|
+
}
|
package/src/task-review.js
CHANGED
|
@@ -815,29 +815,9 @@ export function verifyReviewGitEvidence(review, gitDir, cache = null, opts = {})
|
|
|
815
815
|
return { ok: errors.length === 0, emptyDiff, errors, warnings, unavailable: false }
|
|
816
816
|
}
|
|
817
817
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
*
|
|
822
|
-
* @param {string} changeDir - 变更目录(.sillyspec/changes/<name>)
|
|
823
|
-
* @param {Array<{task: string, verdict: string, evidence: string[]}>} requiredEvidence
|
|
824
|
-
* @returns {string|null} 写入的文件路径,null 表示无需写入
|
|
825
|
-
*/
|
|
826
|
-
export function writeVerifyRequiredEvidence(changeDir, requiredEvidence) {
|
|
827
|
-
if (!requiredEvidence || requiredEvidence.length === 0) return null
|
|
828
|
-
|
|
829
|
-
const filePath = join(changeDir, 'verify-required-evidence.json')
|
|
830
|
-
const data = {
|
|
831
|
-
generatedAt: new Date().toISOString(),
|
|
832
|
-
schemaVersion: 1,
|
|
833
|
-
items: requiredEvidence,
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
mkdirSync(changeDir, { recursive: true })
|
|
837
|
-
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n')
|
|
838
|
-
|
|
839
|
-
return filePath
|
|
840
|
-
}
|
|
818
|
+
// writeVerifyRequiredEvidence(cannot_verify 证据落盘 verify-required-evidence.json)已随
|
|
819
|
+
// Task Review 层退役删除(2026-09-26-task-review-retire,唯一调用方 Execute Task Review Gate):
|
|
820
|
+
// 新变更停写该文件;verify 侧 runVerifyRequiredEvidenceCheck 兼容读在场存量(缺席=无 cannot_verify 任务)。
|
|
841
821
|
|
|
842
822
|
/**
|
|
843
823
|
* 生成 execute run id。
|
|
@@ -1920,14 +1900,14 @@ function detectInterruptedSyntaxResidue(changedFiles, candidateRoots) {
|
|
|
1920
1900
|
}
|
|
1921
1901
|
if (!abs) continue
|
|
1922
1902
|
try {
|
|
1923
|
-
execFileSync(process.execPath, ['--check', abs], { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
1903
|
+
execFileSync(process.execPath, ['--check', abs], { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true })
|
|
1924
1904
|
if (isPlainJs(f)) {
|
|
1925
1905
|
// 无 type:module 上下文的 .js:file-check 通过不代表语法完整(Node v24 实测假阴),
|
|
1926
1906
|
// 再跑 stdin ESM 全量解析兜底——两路都挂才判残留
|
|
1927
1907
|
let src = ''
|
|
1928
1908
|
try { src = readFileSync(abs, 'utf8') } catch { continue }
|
|
1929
1909
|
try {
|
|
1930
|
-
execFileSync(process.execPath, ['--input-type=module', '--check'], { input: src, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
1910
|
+
execFileSync(process.execPath, ['--input-type=module', '--check'], { input: src, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true })
|
|
1931
1911
|
} catch (e2) {
|
|
1932
1912
|
if (e2 && e2.killed) continue
|
|
1933
1913
|
const line = _syntaxErrLine(e2)
|
|
@@ -1947,7 +1927,7 @@ function detectInterruptedSyntaxResidue(changedFiles, candidateRoots) {
|
|
|
1947
1927
|
let src = ''
|
|
1948
1928
|
try { src = readFileSync(abs, 'utf8') } catch { continue }
|
|
1949
1929
|
try {
|
|
1950
|
-
execFileSync(process.execPath, ['--input-type=module', '--check'], { input: src, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
1930
|
+
execFileSync(process.execPath, ['--input-type=module', '--check'], { input: src, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true })
|
|
1951
1931
|
} catch (e2) {
|
|
1952
1932
|
if (e2 && e2.killed) continue
|
|
1953
1933
|
const line = _syntaxErrLine(e2) || _syntaxErrLine(e)
|
|
@@ -2200,32 +2180,5 @@ export function ensureTaskReviewDir(runtimeRoot, executeRunId, taskId) {
|
|
|
2200
2180
|
return dir
|
|
2201
2181
|
}
|
|
2202
2182
|
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
* @param {{ ok: boolean, errors: string[], warnings: string[], requiredEvidence: Array }} result
|
|
2206
|
-
*/
|
|
2207
|
-
export function printReviewResult(result, context = {}) {
|
|
2208
|
-
if (result.ok && result.warnings.length === 0) {
|
|
2209
|
-
console.log('\n✅ Task Review Gate — 所有任务评审通过')
|
|
2210
|
-
return
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
if (result.errors.length > 0) {
|
|
2214
|
-
console.error('\n🚫 Task Review Gate — FAILED')
|
|
2215
|
-
for (const err of result.errors) {
|
|
2216
|
-
console.error(` - ${err}`)
|
|
2217
|
-
}
|
|
2218
|
-
const hint = context.runtimeRoot && context.executeRunId
|
|
2219
|
-
? `期望路径:${context.runtimeRoot}/execute-runs/${context.executeRunId}/tasks/<task-XX>/review.json`
|
|
2220
|
-
: '为缺失/失败的任务补充 review.json'
|
|
2221
|
-
console.error(`\n 提示:${hint},然后重新 --done`)
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
if (result.warnings.length > 0) {
|
|
2225
|
-
console.warn('\n⚠️ Task Review Gate — WARNING')
|
|
2226
|
-
for (const w of result.warnings) {
|
|
2227
|
-
console.warn(` - ${w}`)
|
|
2228
|
-
}
|
|
2229
|
-
console.warn('\n cannot_verify 的 requiredEvidence 将在 verify 阶段校验')
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2183
|
+
// printReviewResult(Task Review 校验结果打印)已随 Task Review 层退役删除
|
|
2184
|
+
// (2026-09-26-task-review-retire,调用方 gates.js 两个门均已退役)。
|