sillyspec 3.33.0 → 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-flow/SKILL.md +1 -1
- package/.github/workflows/ci.yml +11 -3
- package/AGENTS.md +1 -1
- package/assets/command-cards/flow.md +1 -1
- package/package.json +1 -1
- package/src/archive-delta.js +4 -3
- 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/deleted-ledger.js +2 -1
- package/src/doctor-diagnostics.js +8 -3
- package/src/flow-draft.js +2 -1
- package/src/flow.js +111 -62
- 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 +41 -7
- package/src/knowledge-hits.js +2 -1
- package/src/knowledge-vector.js +2 -1
- 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 +31 -7
- package/src/run/prompt.js +6 -5
- 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 +11 -5
- 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 +38 -22
- package/src/stage-contract.js +24 -2
- package/src/stage-review.js +3 -2
- package/src/stages/brainstorm.js +1 -1
- 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/validate-artifacts.js +2 -1
- package/src/verify-draft.js +2 -1
- package/src/verify-postcheck.js +11 -10
- package/src/verify-probes.js +157 -52
- package/src/watcher.js +15 -9
- package/src/worktree-apply.js +2 -1
- package/src/write-audit.js +2 -1
- package/templates/agents-instruction.md +1 -1
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
|
|
|
@@ -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 或已清理)' })
|
package/src/verify-draft.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* 纯 --init(无 --draft)路径零变化;无 sidecar → 门禁 not-applicable(存量变更零红)。
|
|
22
22
|
*/
|
|
23
23
|
import { existsSync, readFileSync } from 'node:fs'
|
|
24
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
24
25
|
import { join } from 'node:path'
|
|
25
26
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
26
27
|
// 三件套原语消费方化(R7 切片三 task-04):标记格式/指纹/三态判定/重锚的机械半边在本模块,
|
|
@@ -33,7 +34,7 @@ const AMEND_CMD = 'sillyspec verify-probes --change <变更名> --amend-draft'
|
|
|
33
34
|
|
|
34
35
|
/** sidecar 路径(.runtime 下,与质量扫描记录/探针回执同族) */
|
|
35
36
|
export function verifyDraftSidecarPath(runtimeRoot, changeName) {
|
|
36
|
-
return
|
|
37
|
+
return runtimeStatePath(runtimeRoot, `verify-draft-${changeName}.json`)
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/**
|
package/src/verify-postcheck.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { execSync, spawnSync } from 'child_process'
|
|
26
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
26
27
|
import { createHash } from 'crypto'
|
|
27
28
|
import { IR_STRICT_SINCE } from './constants.js'
|
|
28
29
|
import { gitQuiet } from './git-helper.js'
|
|
@@ -236,7 +237,7 @@ function recordVerifyLintTally({ specBase, result }) {
|
|
|
236
237
|
try {
|
|
237
238
|
const runtimeDir = join(specBase, '.runtime')
|
|
238
239
|
mkdirSync(runtimeDir, { recursive: true })
|
|
239
|
-
const tallyPath =
|
|
240
|
+
const tallyPath = runtimeStatePath(runtimeDir, 'verify-lint-tally.json')
|
|
240
241
|
let tally = { totalRuns: 0, failedRuns: 0, history: [] }
|
|
241
242
|
try {
|
|
242
243
|
tally = { ...tally, ...JSON.parse(readFileSync(tallyPath, 'utf8')) }
|
|
@@ -1424,7 +1425,7 @@ function readTestRerunConfig(specBase) {
|
|
|
1424
1425
|
}
|
|
1425
1426
|
|
|
1426
1427
|
function testRerunLedgerPath(specBase, changeName) {
|
|
1427
|
-
return join(specBase, '.runtime', `test-rerun-${changeName || 'change'}.json`)
|
|
1428
|
+
return runtimeStatePath(join(specBase, '.runtime'), `test-rerun-${changeName || 'change'}.json`)
|
|
1428
1429
|
}
|
|
1429
1430
|
|
|
1430
1431
|
/**
|
|
@@ -1861,7 +1862,7 @@ export function resolveVerifyChangedFiles(cwd, changeName, ctx = null, opts = {}
|
|
|
1861
1862
|
if (!ctx || typeof ctx.repos !== 'object' || ctx.repos === null) {
|
|
1862
1863
|
try {
|
|
1863
1864
|
const rtRoot = resolveRuntimeRoot({ runtimeRoot: opts.runtimeRoot, specDriftAnchor: opts.specDriftAnchor }, opts.specBase || join(cwd, '.sillyspec'))
|
|
1864
|
-
const pathspecFile =
|
|
1865
|
+
const pathspecFile = runtimeStatePath(rtRoot, `apply-pathspec-${changeName}.txt`)
|
|
1865
1866
|
if (existsSync(pathspecFile)) {
|
|
1866
1867
|
const declared = readFileSync(pathspecFile, 'utf8').split('\n').map(l => l.trim()).filter(Boolean)
|
|
1867
1868
|
.map(p => p.replace(/\\/g, '/'))
|
|
@@ -3138,7 +3139,7 @@ export function resolveTraceResidual({ specBase, changeName, cwd }) {
|
|
|
3138
3139
|
const repoKey = r.repo && r.repo !== 'main' ? r.repo : null
|
|
3139
3140
|
for (const t of (r.tests || [])) {
|
|
3140
3141
|
const f = testAnchorFile(t)
|
|
3141
|
-
const key = (repoKey ? repoKey + '
|
|
3142
|
+
const key = (repoKey ? repoKey + '\u0000' : '') + f
|
|
3142
3143
|
if (!seen.has(key)) { seen.add(key); perFile.push({ repo: repoKey, file: f }) }
|
|
3143
3144
|
}
|
|
3144
3145
|
}
|
|
@@ -3230,7 +3231,7 @@ export function applyTraceResidual({ mainResult, action, cwd, specBase, changeNa
|
|
|
3230
3231
|
const _rk = x.repo || null
|
|
3231
3232
|
// worktree 感知(同悬空判定口径):主仓行按 resolvedRoot(主仓或变更 worktree 根)取执行根。
|
|
3232
3233
|
const _root = (_rk && _repoRoots.get(_rk)) || x.resolvedRoot || cwd
|
|
3233
|
-
const _gk = _rk + '
|
|
3234
|
+
const _gk = _rk + '\u0000' + _root
|
|
3234
3235
|
if (!_gmap.has(_gk)) _gmap.set(_gk, { repo: _rk, root: _root, files: [] })
|
|
3235
3236
|
_gmap.get(_gk).files.push(x.file)
|
|
3236
3237
|
}
|
|
@@ -3780,7 +3781,7 @@ export function trackVerifyResultRegression(specBase, changeName, absPath) {
|
|
|
3780
3781
|
if (!absPath || !existsSync(absPath)) return { regressed: false, prevMtime: null }
|
|
3781
3782
|
const st = statSync(absPath)
|
|
3782
3783
|
const hash = createHash('sha256').update(readFileSync(absPath)).digest('hex')
|
|
3783
|
-
const hwmPath = join(specBase, '.runtime', `verify-result-hwm-${changeName}.json`)
|
|
3784
|
+
const hwmPath = runtimeStatePath(join(specBase, '.runtime'), `verify-result-hwm-${changeName}.json`)
|
|
3784
3785
|
let prev = null
|
|
3785
3786
|
try { prev = JSON.parse(readFileSync(hwmPath, 'utf8')) } catch { prev = null }
|
|
3786
3787
|
const regressed = !!(prev && typeof prev.hash === 'string' &&
|
|
@@ -3790,7 +3791,7 @@ export function trackVerifyResultRegression(specBase, changeName, absPath) {
|
|
|
3790
3791
|
if (!prev || st.mtimeMs >= (prev.mtimeMs || 0)) {
|
|
3791
3792
|
try {
|
|
3792
3793
|
mkdirSync(join(specBase, '.runtime'), { recursive: true })
|
|
3793
|
-
writeFileSync(hwmPath, JSON.stringify({ hash, mtimeMs: st.mtimeMs, ts: Date.now() }) + '\n')
|
|
3794
|
+
writeFileSync(hwmPath, JSON.stringify({ hash, mtimeMs: Math.round(st.mtimeMs), ts: Date.now() }) + '\n')
|
|
3794
3795
|
} catch { /* hwm 落盘失败 → 下次当首次(fail-open) */ }
|
|
3795
3796
|
}
|
|
3796
3797
|
return { regressed, prevMtime: prev && prev.mtimeMs ? prev.mtimeMs : null }
|
|
@@ -4307,7 +4308,7 @@ export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, change
|
|
|
4307
4308
|
}
|
|
4308
4309
|
} catch { /* 窗口源失败静默省略 */ }
|
|
4309
4310
|
// B3 apply-pathspec 兜底(存在则并入,读取失败静默忽略——前两源不受影响)
|
|
4310
|
-
const pathspecFile =
|
|
4311
|
+
const pathspecFile = runtimeStatePath(runtimeRoot, `apply-pathspec-${changeName}.txt`)
|
|
4311
4312
|
if (existsSync(pathspecFile)) {
|
|
4312
4313
|
try {
|
|
4313
4314
|
const lines = readFileSync(pathspecFile, 'utf8').split('\n').map(l => l.trim()).filter(Boolean).map(normalizeReconcilePath)
|
|
@@ -4704,7 +4705,7 @@ export async function runCeremonyDualRunCheck({ cwd, specBase = null, changeName
|
|
|
4704
4705
|
const rt = runtimeRoot || join(sb, '.runtime')
|
|
4705
4706
|
|
|
4706
4707
|
// —— 声明侧:开跑定价落档(task-03 产物,tier/components/reasons/transitions)——
|
|
4707
|
-
const tierPath =
|
|
4708
|
+
const tierPath = runtimeStatePath(rt, `ceremony-tier-${changeName}.json`)
|
|
4708
4709
|
let tierFile = null
|
|
4709
4710
|
try { tierFile = JSON.parse(readFileSync(tierPath, 'utf8')) } catch { /* 缺失/坏 JSON → 下方跳过 */ }
|
|
4710
4711
|
if (tierFile == null || typeof tierFile !== 'object' || Array.isArray(tierFile)) {
|
|
@@ -4801,7 +4802,7 @@ export async function runCeremonyDualRunCheck({ cwd, specBase = null, changeName
|
|
|
4801
4802
|
recomputedReasons: [`双跑低报自动追赶(事实面 tier=${verdict.factTier},证据面 seed ${actual.files.length} 文件)`],
|
|
4802
4803
|
frictionCounts: {},
|
|
4803
4804
|
})
|
|
4804
|
-
const tierPathForReprice =
|
|
4805
|
+
const tierPathForReprice = runtimeStatePath(rt, `ceremony-tier-${changeName}.json`)
|
|
4805
4806
|
const { writeAtomicSync } = await import('./fs-atomic.js')
|
|
4806
4807
|
writeAtomicSync(tierPathForReprice, JSON.stringify(caught, null, 2) + '\n')
|
|
4807
4808
|
result.violation.message += `;档位文件已自动按事实面追赶重定价(${String(declaredTier)} → ${caught.tier})——修复低报根因后重跑 --done 即对账通过,勿手动改档(ql-013 死锁修复)`
|