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/scan-refresh.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* 无引用论断检不出。refresh 只声称「核对至 HEAD 的检出项已处理」,不声称「一致」。
|
|
26
26
|
*/
|
|
27
27
|
import { existsSync, readFileSync, readdirSync, mkdirSync } from 'node:fs'
|
|
28
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
28
29
|
import { join } from 'node:path'
|
|
29
30
|
import { createHash } from 'node:crypto'
|
|
30
31
|
import { safeGit } from './git-helper.js'
|
|
@@ -227,7 +228,7 @@ export function computeRefreshPlan({ projectRoot, specBase, projectName, force =
|
|
|
227
228
|
try {
|
|
228
229
|
const guardDir = join(specBase, '.runtime')
|
|
229
230
|
mkdirSync(guardDir, { recursive: true })
|
|
230
|
-
writeAtomicSync(
|
|
231
|
+
writeAtomicSync(runtimeStatePath(guardDir, 'scan-guard.json'), JSON.stringify({
|
|
231
232
|
name_zh: '增量刷新守卫',
|
|
232
233
|
mode: 'scan-refresh',
|
|
233
234
|
refreshDocs,
|
|
@@ -381,7 +382,7 @@ export async function finalizeRefresh(opts = {}) {
|
|
|
381
382
|
const scanDir = join(specBase, 'docs', projectName, 'scan')
|
|
382
383
|
// ①拍 guard 会话态必须存在(握手文件=工单受影响集与内容哈希的唯一来源)
|
|
383
384
|
let guard = null
|
|
384
|
-
const guardPath = join(specBase, '.runtime', 'scan-guard.json')
|
|
385
|
+
const guardPath = runtimeStatePath(join(specBase, '.runtime'), 'scan-guard.json')
|
|
385
386
|
try { guard = JSON.parse(readFileSync(guardPath, 'utf8')) } catch { /* 缺失/坏 JSON → 下方拒绝 */ }
|
|
386
387
|
if (!guard || guard.mode !== 'scan-refresh') {
|
|
387
388
|
console.error('scan refresh --done:未找到增量刷新会话态(.runtime/scan-guard.json 无 mode=scan-refresh)')
|
|
@@ -453,7 +454,7 @@ export async function finalizeRefresh(opts = {}) {
|
|
|
453
454
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
454
455
|
mkdirSync(runtimeRoot, { recursive: true })
|
|
455
456
|
const ts = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14)
|
|
456
|
-
auditPath =
|
|
457
|
+
auditPath = runtimeStatePath(runtimeRoot, `scan-refresh-${ts}.json`)
|
|
457
458
|
writeAtomicSync(auditPath, JSON.stringify({
|
|
458
459
|
changeType: 'scan-refresh',
|
|
459
460
|
head: headShort,
|
package/src/scope-audit.js
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* degraded 仓退 v1 ⊘ 形态不炸主仓表;预执行形态不调内核(跨仓行保持清单形态)。
|
|
33
33
|
*/
|
|
34
34
|
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
35
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
35
36
|
import { createHash } from 'node:crypto'
|
|
36
37
|
import { join, dirname } from 'node:path'
|
|
37
38
|
import { locateQuickSessionGuard, auditQuickCompletion, resolveRuntimeRoot, collectOtherQuickSessionDeclarations, ancestorSpecDirs, isQuickMetadata } from './run/shared.js'
|
|
@@ -263,7 +264,7 @@ function readScopeSnapshot(changeDir, runtimeRoot, changeName) {
|
|
|
263
264
|
} catch { /* 缺失/损坏 → 下一源 */ }
|
|
264
265
|
}
|
|
265
266
|
try {
|
|
266
|
-
const snap = JSON.parse(readFileSync(
|
|
267
|
+
const snap = JSON.parse(readFileSync(runtimeStatePath(runtimeRoot, `scope-audit-${changeName}.json`), 'utf8'))
|
|
267
268
|
if (snap && typeof snap === 'object' && Array.isArray(snap.rows)) return snap
|
|
268
269
|
} catch { /* 缺失/损坏 → null */ }
|
|
269
270
|
return null
|
|
@@ -859,10 +860,10 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
859
860
|
})() : false
|
|
860
861
|
const hasSnapshotFile = hasMergedScopeAudit
|
|
861
862
|
|| (changeDirInfo
|
|
862
|
-
? existsSync(join(changeDirInfo.dir, 'scope-audit.json')) || existsSync(
|
|
863
|
-
: existsSync(
|
|
864
|
-
const hasApplyPathspec = existsSync(
|
|
865
|
-
const hasCleanupReceipt = existsSync(
|
|
863
|
+
? existsSync(join(changeDirInfo.dir, 'scope-audit.json')) || existsSync(runtimeStatePath(runtimeRoot, `scope-audit-${changeName}.json`))
|
|
864
|
+
: existsSync(runtimeStatePath(runtimeRoot, `scope-audit-${changeName}.json`)))
|
|
865
|
+
const hasApplyPathspec = existsSync(runtimeStatePath(runtimeRoot, `apply-pathspec-${changeName}.txt`))
|
|
866
|
+
const hasCleanupReceipt = existsSync(runtimeStatePath(runtimeRoot, `execute-cleanup-${changeName}.json`))
|
|
866
867
|
let hasExecuteRunStamp = false
|
|
867
868
|
if (!hasWorktreeMeta && !hasBranch && !hasAuditTag && !hasSnapshotFile && !hasApplyPathspec && !hasCleanupReceipt) {
|
|
868
869
|
try {
|
|
@@ -1016,7 +1017,7 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
1016
1017
|
let narrowNote = null
|
|
1017
1018
|
if (freshActual && collectPatch) {
|
|
1018
1019
|
try {
|
|
1019
|
-
const pathspecFile =
|
|
1020
|
+
const pathspecFile = runtimeStatePath(runtimeRoot, `apply-pathspec-${changeName}.txt`)
|
|
1020
1021
|
const owned = readFileSync(pathspecFile, 'utf8').split('\n').map(l => toPosix(l.trim())).filter(Boolean)
|
|
1021
1022
|
if (owned.length > 0) {
|
|
1022
1023
|
const ownedSet = new Set(owned)
|
|
@@ -207,6 +207,43 @@ export function detectTickBurst(tickLog, opts = {}) {
|
|
|
207
207
|
};
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
/**
|
|
211
|
+
* 勾选时点判定·换源版(2026-10-17-tick-timing-source,纯函数):task-ticks.jsonl 侧车的逐格
|
|
212
|
+
* 墙钟时戳 → 勾选是「边干边勾」还是「末尾集中」。旧判据(tasks.md git 首次提交 == 收口最后
|
|
213
|
+
* 提交)已退役——git 历史形态被官方允许的 reset --soft 压扁无声摧毁(tick-timing-squash-
|
|
214
|
+
* blindspot 坑:边干边勾的渐进提交被并行会话压扁后警告照出,结构性误报;且 tasks.md 不进
|
|
215
|
+
* 交付 pathspec 的纪律下「untracked 直至归档」恒复现)。墙钟时戳是侧车文件事实,压扁/
|
|
216
|
+
* rebase 均不可改写——与「审计真相锚 change.patch sha256、不依赖 git 形态」口径对齐。
|
|
217
|
+
* 判定序(豁免优先):
|
|
218
|
+
* 'no-ticks' 侧车无记录(agent 全程直接 Edit)——不判(调用方给一行 task tick 教学);
|
|
219
|
+
* 'single' 仅 1 格——单格无节奏可言;
|
|
220
|
+
* 'spread' 时戳跨度 > clusterWindowMs(缺省 120s)——天然分散,边干边勾确认
|
|
221
|
+
* (超窗即铁证,无需存活锚);
|
|
222
|
+
* 'no-anchor' lifespanMs 非有限或 ≤0(baseline 缺失)——fail-open 不判(防误报);
|
|
223
|
+
* 'concentrated' 跨度 ≤ clusterWindowMs 且 lifespanMs ≥ ratio×max(跨度,1ms)(缺省 10)
|
|
224
|
+
* ——格子全挤在收口前短窗且变更活得够久,末尾集中勾选形态;
|
|
225
|
+
* 其余(挤短窗但存活比不足)→ 'spread'——快变更快勾是合法形态,不告。
|
|
226
|
+
* 输入 ticks 为 [{ts, task}](parseTickLog 产物或同形);行序不保证,内部按 ts 排序。
|
|
227
|
+
* 坏行(ts 非有限/≤0)静默剔除。watcher 采样事件刻意不作判据(部分观测会把分散勾选采样
|
|
228
|
+
* 成尾窗聚集,与采样合并跳系列坑同源——缺观测宁可教学不判)。
|
|
229
|
+
* @param {{ticks?:Array<{ts:number,task:string}>, lifespanMs?:number, clusterWindowMs?:number, ratio?:number}} args
|
|
230
|
+
* @returns {{status:'spread'|'concentrated'|'single'|'no-ticks'|'no-anchor', count:number, spanMs:number}}
|
|
231
|
+
*/
|
|
232
|
+
export function detectTickTiming({ ticks, lifespanMs, clusterWindowMs = 120_000, ratio = 10 } = {}) {
|
|
233
|
+
const list = (Array.isArray(ticks) ? ticks : [])
|
|
234
|
+
.map((r) => Number(r && r.ts))
|
|
235
|
+
.filter((ts) => Number.isFinite(ts) && ts > 0)
|
|
236
|
+
.sort((a, b) => a - b);
|
|
237
|
+
if (list.length === 0) return { status: 'no-ticks', count: 0, spanMs: 0 };
|
|
238
|
+
if (list.length === 1) return { status: 'single', count: 1, spanMs: 0 };
|
|
239
|
+
const spanMs = list[list.length - 1] - list[0];
|
|
240
|
+
if (spanMs > clusterWindowMs) return { status: 'spread', count: list.length, spanMs };
|
|
241
|
+
const life = Number(lifespanMs);
|
|
242
|
+
if (!Number.isFinite(life) || life <= 0) return { status: 'no-anchor', count: list.length, spanMs };
|
|
243
|
+
if (life >= ratio * Math.max(spanMs, 1)) return { status: 'concentrated', count: list.length, spanMs };
|
|
244
|
+
return { status: 'spread', count: list.length, spanMs };
|
|
245
|
+
}
|
|
246
|
+
|
|
210
247
|
/**
|
|
211
248
|
* 单拍勾选门决策(2026-09-29-batch-tick-gate,纯函数):检出单拍跳(batchTick 非 null)
|
|
212
249
|
* 时的收口动作裁决。镜像豁免分支退役(2026-10-07-thin-tasks-v3——任务面统一为 agent
|
package/src/spec-sync.js
CHANGED
|
@@ -19,6 +19,7 @@ import { createHash } from 'crypto';
|
|
|
19
19
|
import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
|
|
20
20
|
import { join, relative, sep } from 'path';
|
|
21
21
|
import { bindSyncNoiseRoot, syncConnectionWarn, isConnectionClassStatus, noteSyncSuccess } from './sync-noise.js';
|
|
22
|
+
import { runtimeStatePath } from './runtime-paths.js';
|
|
22
23
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
23
24
|
|
|
24
25
|
// 与 sillyhub-daemon/src/spec-sync.ts 共用排除口径(task-07 / FR-06 / D-008@v2):
|
|
@@ -69,7 +70,7 @@ export function describeSyncError(err) {
|
|
|
69
70
|
// 才拿到),且失败走「下次自动重试」无限静默循环——归档半态(本地已归档、平台仍「进行中」)
|
|
70
71
|
// 挂数小时无人知晓。两道护栏:①非 2xx 响应体解析进告警行;②按 label 连续失败计数持久化,
|
|
71
72
|
// 连续 ≥3 次升级 🚨 显式告警,任一次成功清零。
|
|
72
|
-
const FAIL_STREAK_FILE = join('.runtime', 'spec-sync-fail-streak.json');
|
|
73
|
+
const FAIL_STREAK_FILE = join('.runtime', 'state', 'spec-sync-fail-streak.json');
|
|
73
74
|
const FAIL_STREAK_ESCALATE_AT = 3;
|
|
74
75
|
|
|
75
76
|
export function describeErrorBody(status, rawBody) {
|
|
@@ -387,7 +388,7 @@ const LAST_SUCCESS_MARKER = 'spec-sync-last-success.json';
|
|
|
387
388
|
|
|
388
389
|
function readLastSyncTs(specRoot) {
|
|
389
390
|
try {
|
|
390
|
-
const raw = readFileSync(join(specRoot, '.runtime', LAST_SUCCESS_MARKER), 'utf8');
|
|
391
|
+
const raw = readFileSync(runtimeStatePath(join(specRoot, '.runtime'), LAST_SUCCESS_MARKER), 'utf8');
|
|
391
392
|
const ts = JSON.parse(raw).ts;
|
|
392
393
|
return Number.isFinite(ts) ? ts : null;
|
|
393
394
|
} catch {
|
|
@@ -398,7 +399,7 @@ function readLastSyncTs(specRoot) {
|
|
|
398
399
|
function writeLastSyncTs(specRoot) {
|
|
399
400
|
try {
|
|
400
401
|
mkdirSync(join(specRoot, '.runtime'), { recursive: true });
|
|
401
|
-
writeFileSync(join(specRoot, '.runtime', LAST_SUCCESS_MARKER), JSON.stringify({ ts: Date.now() }) + '\n', 'utf8');
|
|
402
|
+
writeFileSync(runtimeStatePath(join(specRoot, '.runtime'), LAST_SUCCESS_MARKER), JSON.stringify({ ts: Date.now() }) + '\n', 'utf8');
|
|
402
403
|
} catch { /* 标记失败只损失下次的防回推过滤,不影响本次同步结果 */ }
|
|
403
404
|
}
|
|
404
405
|
|
|
@@ -415,7 +416,7 @@ const BASE_SNAPSHOT = 'spec-sync-base.json';
|
|
|
415
416
|
|
|
416
417
|
function readBaseSnapshot(specRoot) {
|
|
417
418
|
try {
|
|
418
|
-
const raw = readFileSync(join(specRoot, '.runtime', BASE_SNAPSHOT), 'utf8');
|
|
419
|
+
const raw = readFileSync(runtimeStatePath(join(specRoot, '.runtime'), BASE_SNAPSHOT), 'utf8');
|
|
419
420
|
const hashes = JSON.parse(raw).hashes;
|
|
420
421
|
return hashes && typeof hashes === 'object' ? hashes : null;
|
|
421
422
|
} catch {
|
|
@@ -428,7 +429,7 @@ function writeBaseSnapshot(specRoot, localFiles) {
|
|
|
428
429
|
const hashes = {};
|
|
429
430
|
for (const f of localFiles) hashes[f.path] = f.hash;
|
|
430
431
|
mkdirSync(join(specRoot, '.runtime'), { recursive: true });
|
|
431
|
-
writeFileSync(join(specRoot, '.runtime', BASE_SNAPSHOT), JSON.stringify({ ts: Date.now(), hashes }, null, 2) + '\n', 'utf8');
|
|
432
|
+
writeFileSync(runtimeStatePath(join(specRoot, '.runtime'), BASE_SNAPSHOT), JSON.stringify({ ts: Date.now(), hashes }, null, 2) + '\n', 'utf8');
|
|
432
433
|
} catch { /* 快照失败只损失下次的内容级防回推,不影响本次同步结果 */ }
|
|
433
434
|
}
|
|
434
435
|
|
|
@@ -497,7 +498,7 @@ export function partitionConflictPaths(serverVersions, localFiles, baseHashes, l
|
|
|
497
498
|
*/
|
|
498
499
|
function isSamePendingSpecConflict(specRoot, changeName, realPaths) {
|
|
499
500
|
try {
|
|
500
|
-
const raw = readFileSync(join(specRoot, '.runtime', `spec-sync-conflict-${changeName}.json`), 'utf8');
|
|
501
|
+
const raw = readFileSync(runtimeStatePath(join(specRoot, '.runtime'), `spec-sync-conflict-${changeName}.json`), 'utf8');
|
|
501
502
|
const prev = JSON.parse(raw).conflicting_paths;
|
|
502
503
|
if (!Array.isArray(prev) || prev.length !== realPaths.length) return false;
|
|
503
504
|
const prevSet = new Set(prev);
|
|
@@ -514,7 +515,7 @@ function isSamePendingSpecConflict(specRoot, changeName, realPaths) {
|
|
|
514
515
|
*/
|
|
515
516
|
function clearSpecConflictMarker(specRoot, changeName) {
|
|
516
517
|
try {
|
|
517
|
-
const p = join(specRoot, '.runtime', `spec-sync-conflict-${changeName}.json`);
|
|
518
|
+
const p = runtimeStatePath(join(specRoot, '.runtime'), `spec-sync-conflict-${changeName}.json`);
|
|
518
519
|
if (existsSync(p)) unlinkSync(p);
|
|
519
520
|
} catch { /* 清理失败不影响同步结果 */ }
|
|
520
521
|
}
|
|
@@ -553,7 +554,7 @@ export function writeTombstoneAttribution(specRoot, platformDeleted) {
|
|
|
553
554
|
const byChange = extractTombstonedChanges(platformDeleted);
|
|
554
555
|
const written = [];
|
|
555
556
|
for (const [name, paths] of byChange) {
|
|
556
|
-
const filePath = join(specRoot, '.runtime', `spec-sync-conflict-${name}.json`);
|
|
557
|
+
const filePath = runtimeStatePath(join(specRoot, '.runtime'), `spec-sync-conflict-${name}.json`);
|
|
557
558
|
let prev = null;
|
|
558
559
|
try { prev = JSON.parse(readFileSync(filePath, 'utf8')); } catch { /* 首见 */ }
|
|
559
560
|
const record = {
|
|
@@ -584,13 +585,17 @@ export function writeTombstoneAttribution(specRoot, platformDeleted) {
|
|
|
584
585
|
* 清理条数。挂在两个全绿点:POST 成功无冲突分支与无差异跳过分支。
|
|
585
586
|
*/
|
|
586
587
|
export function clearStaleTombstoneMarkers(specRoot) {
|
|
588
|
+
// 2026-10-18 布局分层:冲突文件落 state/;根目录扫描保留旧布局兜底
|
|
587
589
|
const runtimeDir = join(specRoot, '.runtime');
|
|
588
590
|
let files = [];
|
|
589
|
-
|
|
591
|
+
const conflictDirs = [join(runtimeDir, 'state'), runtimeDir]
|
|
590
592
|
let cleared = 0;
|
|
591
|
-
for (const
|
|
593
|
+
for (const dir of conflictDirs) {
|
|
594
|
+
let dirFiles = [];
|
|
595
|
+
try { dirFiles = readdirSync(dir); } catch { continue; }
|
|
596
|
+
for (const f of dirFiles) {
|
|
592
597
|
if (!f.startsWith('spec-sync-conflict-') || !f.endsWith('.json')) continue;
|
|
593
|
-
const p = join(
|
|
598
|
+
const p = join(dir, f);
|
|
594
599
|
try {
|
|
595
600
|
const j = JSON.parse(readFileSync(p, 'utf8'));
|
|
596
601
|
const cp = Array.isArray(j.conflicting_paths) ? j.conflicting_paths : null;
|
|
@@ -601,6 +606,7 @@ export function clearStaleTombstoneMarkers(specRoot) {
|
|
|
601
606
|
}
|
|
602
607
|
} catch { /* 损坏文件不动 */ }
|
|
603
608
|
}
|
|
609
|
+
}
|
|
604
610
|
if (cleared > 0) {
|
|
605
611
|
console.log(`[spec-sync] 全绿同步清理 ${cleared} 条陈旧墓碑冲突记录(被删变更已收敛)`);
|
|
606
612
|
}
|
|
@@ -624,7 +630,7 @@ const FOLLOW_REPORT_MARKER = 'spec-sync-follow-reported.json';
|
|
|
624
630
|
*/
|
|
625
631
|
export function isFollowerSetChanged(specRoot, key, list) {
|
|
626
632
|
const sorted = [...(list || [])].sort();
|
|
627
|
-
const p = join(specRoot, '.runtime', FOLLOW_REPORT_MARKER);
|
|
633
|
+
const p = runtimeStatePath(join(specRoot, '.runtime'), FOLLOW_REPORT_MARKER);
|
|
628
634
|
let cur = {};
|
|
629
635
|
try { cur = JSON.parse(readFileSync(p, 'utf8')) || {}; } catch { /* 无 marker = 首报 */ }
|
|
630
636
|
const prev = Array.isArray(cur[key]) ? cur[key] : [];
|
|
@@ -665,10 +671,20 @@ function attributeChangeLabel(paths) {
|
|
|
665
671
|
return names.size === 1 ? [...names][0] : 'full-tree'
|
|
666
672
|
}
|
|
667
673
|
|
|
674
|
+
/**
|
|
675
|
+
* spec 树增量同步(以服务器清单为锚做全树 diff → POST ops)。
|
|
676
|
+
*
|
|
677
|
+
* 返回契约(2026-10-17-archive-half-state-location-stuck-active):`ok` 字段表达
|
|
678
|
+
* 「本轮文件树通道是否确认收敛」——ok:true = 确认(无差异 no-op / 推送成功 / 冲突自愈
|
|
679
|
+
* / 未连接本地合法态);ok:false = 确认失败(清单/推送 HTTP 失败、异常);**无 ok 字段
|
|
680
|
+
* = 未确认**(未决冲突待人裁等,fail-closed)。终态强校验消费方
|
|
681
|
+
* (run/shared.js triggerSyncTerminalStrict)按 `ok === true` 判收敛——历史调用方
|
|
682
|
+
* (syncSpecTreeOnly / resolve forcePush 等)不消费该字段,向后兼容。
|
|
683
|
+
*/
|
|
668
684
|
export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
669
685
|
if (!platform || !platform.url || !platform.token) {
|
|
670
686
|
debugLog('[spec-sync] 未连接平台(本地合法状态);跳过 spec 树增量同步');
|
|
671
|
-
return { synced: 0 };
|
|
687
|
+
return { synced: 0, ok: true };
|
|
672
688
|
}
|
|
673
689
|
// 全树入口(platform sync 无 --change,2026-10-11-sync-conflict-tri-fix FR-03)
|
|
674
690
|
// changeName=null:日志用 full-tree 标签,冲突归因见下方 conflictLabel。
|
|
@@ -705,7 +721,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
705
721
|
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
706
722
|
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
707
723
|
}
|
|
708
|
-
return { synced: 0 };
|
|
724
|
+
return { synced: 0, ok: false };
|
|
709
725
|
}
|
|
710
726
|
const body = await res.json().catch(() => ({}));
|
|
711
727
|
serverManifest = body.files || {};
|
|
@@ -716,7 +732,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
716
732
|
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
717
733
|
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
718
734
|
}
|
|
719
|
-
return { synced: 0 };
|
|
735
|
+
return { synced: 0, ok: false };
|
|
720
736
|
}
|
|
721
737
|
|
|
722
738
|
// 2. walk/hash/diff
|
|
@@ -758,7 +774,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
758
774
|
debugLog(`[spec-sync] 无差异,跳过同步: ${logLabel}`);
|
|
759
775
|
clearStaleTombstoneMarkers(specRoot);
|
|
760
776
|
clearFailStreak(specRoot, logLabel);
|
|
761
|
-
return { synced: 0 };
|
|
777
|
+
return { synced: 0, ok: true };
|
|
762
778
|
}
|
|
763
779
|
|
|
764
780
|
// 3. POST ops
|
|
@@ -782,7 +798,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
782
798
|
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
783
799
|
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
784
800
|
}
|
|
785
|
-
return { synced: 0 };
|
|
801
|
+
return { synced: 0, ok: false };
|
|
786
802
|
}
|
|
787
803
|
const body = await res.json().catch(() => ({}));
|
|
788
804
|
if (body.conflict) {
|
|
@@ -843,7 +859,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
843
859
|
debugLog(`[spec-sync] ${followers.length} 个本地未改动文件自动跟随服务器: ${followers.slice(0, 5).join(', ')}${followers.length > 5 ? ' 等' : ''}`);
|
|
844
860
|
}
|
|
845
861
|
console.warn(`⚠️ [spec-sync] 变更 ${conflictLabel} 存在未决 spec 树冲突(${realPaths.length} 个文件待裁决,本轮非冲突改动已照常同步)。处理:sillyspec platform resolve ${conflictLabel} --keep-local | --take-platform | --abort`);
|
|
846
|
-
return { synced: 0, conflict: true, serverVersions: real, autoResolved: followers.length, conflictPath: join(specRoot, '.runtime', `spec-sync-conflict-${conflictLabel}.json`), suppressed: true };
|
|
862
|
+
return { synced: 0, conflict: true, serverVersions: real, autoResolved: followers.length, conflictPath: runtimeStatePath(join(specRoot, '.runtime'), `spec-sync-conflict-${conflictLabel}.json`), suppressed: true };
|
|
847
863
|
}
|
|
848
864
|
if (followers.length > 0) {
|
|
849
865
|
console.warn(`[spec-sync] ${followers.length} 个本地未改动文件自动跟随服务器(正常多端前进,无需人工裁决): ${followers.slice(0, 5).join(', ')}${followers.length > 5 ? ' 等' : ''}`);
|
|
@@ -855,13 +871,13 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
855
871
|
noteSyncSuccess();
|
|
856
872
|
clearFailStreak(specRoot, logLabel);
|
|
857
873
|
console.log(`[spec-sync] 冲突已自动消解(0 个需人工裁决;本会话改动 ${ops.length - followers.length > 0 ? '已同步' : '无差异'}): ${conflictLabel}`);
|
|
858
|
-
return { synced: Math.max(ops.length - followers.length, 0), conflict: false, autoResolved: followers.length };
|
|
874
|
+
return { synced: Math.max(ops.length - followers.length, 0), ok: true, conflict: false, autoResolved: followers.length };
|
|
859
875
|
}
|
|
860
876
|
let conflictPath = null;
|
|
861
877
|
try {
|
|
862
878
|
const runtimeDir = join(specRoot, '.runtime');
|
|
863
879
|
mkdirSync(runtimeDir, { recursive: true });
|
|
864
|
-
conflictPath =
|
|
880
|
+
conflictPath = runtimeStatePath(runtimeDir, `spec-sync-conflict-${conflictLabel}.json`);
|
|
865
881
|
writeFileSync(conflictPath, JSON.stringify({
|
|
866
882
|
change: conflictLabel,
|
|
867
883
|
kind: 'spec-tree',
|
|
@@ -901,7 +917,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
901
917
|
clearStaleTombstoneMarkers(specRoot);
|
|
902
918
|
writeLastSyncTs(specRoot);
|
|
903
919
|
writeBaseSnapshot(specRoot, localFiles);
|
|
904
|
-
return { synced: ops.length };
|
|
920
|
+
return { synced: ops.length, ok: true };
|
|
905
921
|
} catch (err) {
|
|
906
922
|
const failCount = bumpFailStreak(specRoot, logLabel, describeSyncError(err));
|
|
907
923
|
const line = `[spec-sync] 同步异常(文件树本次未同步,下次自动重试): ${logLabel}: ${describeSyncError(err)}`;
|
|
@@ -909,7 +925,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
909
925
|
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
910
926
|
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
911
927
|
}
|
|
912
|
-
return { synced: 0 };
|
|
928
|
+
return { synced: 0, ok: false };
|
|
913
929
|
}
|
|
914
930
|
}
|
|
915
931
|
|
package/src/stage-contract.js
CHANGED
|
@@ -897,7 +897,7 @@ function matrixEvidenceMissing(verdict, evidence) {
|
|
|
897
897
|
*/
|
|
898
898
|
export function extractAcceptanceMatrixSlots(verifyMd) {
|
|
899
899
|
const md = String(verifyMd || '')
|
|
900
|
-
const out = { present: false, rows: [], unfilled: 0, missingEvidence: 0 }
|
|
900
|
+
const out = { present: false, rows: [], unfilled: 0, missingEvidence: 0, strayRows: [] }
|
|
901
901
|
const hm = md.match(/^#### 探针 7[::]/m)
|
|
902
902
|
if (!hm) return out
|
|
903
903
|
out.present = true
|
|
@@ -905,15 +905,27 @@ export function extractAcceptanceMatrixSlots(verifyMd) {
|
|
|
905
905
|
const endMatch = rest.match(/^#{1,4}(?:[ \t]|$)/m)
|
|
906
906
|
const sectionText = endMatch ? rest.slice(0, endMatch.index) : rest
|
|
907
907
|
|
|
908
|
+
// 表头武装(坑 carry-orphan-rows,2026-10-16 下游实证):五列表行只在探针 7 段已被表头行
|
|
909
|
+
// 武装时才计数;refresh 携载注释块解除武装——其后残行(旧模板占位行/键漂移携载行)是无
|
|
910
|
+
// 表头归属的孤行,不栽给最近 **task-NN** 锚误报「判定未填」,进独立 strayRows 桶由
|
|
911
|
+
// validateAcceptanceMatrix 报错要求裁决。武装是段级粘性(首表头武装整段,锚行不重置——
|
|
912
|
+
// 锚下不重复表头的既有手写/历史形态照旧按锚归属);只有携载注释块与其后无新表头重新
|
|
913
|
+
// 武装的窗口内,行才进孤行桶。正常渲染文法(锚行→表头→分隔→数据行)行为与既有口径一致。
|
|
908
914
|
let currentTask = ''
|
|
915
|
+
let tableArmed = false
|
|
909
916
|
for (const rawLine of sectionText.split(/\r?\n/)) {
|
|
910
917
|
const anchor = rawLine.match(/^\*\*\s*(task-[A-Za-z0-9._-]+)\s*\*\*/)
|
|
911
918
|
if (anchor) { currentTask = anchor[1]; continue }
|
|
919
|
+
if (/<!--\s*refresh 携载/.test(rawLine)) { tableArmed = false; continue } // 携载注释块:其后表行是孤行(直至新表头重新武装)
|
|
912
920
|
const cells = splitMatrixRowCells(rawLine)
|
|
913
921
|
if (!cells || cells.length !== 5) continue // 列表防御行/不适用行/非五列表行不计槽
|
|
914
922
|
const c = cells.map(x => x.trim())
|
|
915
923
|
if (c.every(x => /^:?-{3,}:?$/.test(x))) continue // 分隔行 |---|---|…|
|
|
916
|
-
if (c[3] === '判定' && c[4] === '证据') continue //
|
|
924
|
+
if (c[3] === '判定' && c[4] === '证据') { tableArmed = true; continue } // 表头行(武装本段)
|
|
925
|
+
if (!tableArmed) {
|
|
926
|
+
out.strayRows.push({ task: currentTask, acceptance: c[0], verdict: c[3], evidence: c[4] })
|
|
927
|
+
continue
|
|
928
|
+
}
|
|
917
929
|
const verdict = c[3]
|
|
918
930
|
const evidence = c[4]
|
|
919
931
|
const row = { task: currentTask, acceptance: c[0], verdict, evidence }
|
|
@@ -985,6 +997,16 @@ function validateAcceptanceMatrix(cwd, changeName, context = {}) {
|
|
|
985
997
|
`修复:在证据列补测试锚点(如 \`test/foo.test.mjs\` 或 \`src/x.js:42\`)或 non-testable 理由(运行验证移交场景适用:构建回执+移交去向一句话)。`
|
|
986
998
|
)
|
|
987
999
|
}
|
|
1000
|
+
// 孤行报错(坑 carry-orphan-rows,2026-10-16 下游实证):无表头归属的五列残行(refresh
|
|
1001
|
+
// 携载块后的旧模板占位行/键漂移携载行/手误行)不归属任何 task 锚——不栽给最近锚计
|
|
1002
|
+
// unfilled(旧路径误报「文档同步任务判定未填」),独立报错要求裁决:删或补归属。
|
|
1003
|
+
if ((matrix.strayRows || []).length > 0) {
|
|
1004
|
+
const list = matrix.strayRows.map((r) => `${r.task || '(无 task 锚)'}|${clip(r.acceptance)}`).join(';')
|
|
1005
|
+
errors.push(
|
|
1006
|
+
`探针 7 验收×测试覆盖矩阵存在 ${matrix.strayRows.length} 行无表头归属的孤行(携载残行/旧模板占位行/手误——不属于任何 task 锚下的有效表,不计入任务矩阵):${list}。` +
|
|
1007
|
+
`修复:删除这些残行,或补全 **task-NN** 锚行与表头后按五列格式填判定与证据。`
|
|
1008
|
+
)
|
|
1009
|
+
}
|
|
988
1010
|
|
|
989
1011
|
// ── partial/uncovered × facts.handover 联动(D-003/FR-04,2026-09-17-pass-cap-semantics task-03)──
|
|
990
1012
|
// 部分实现必须有移交去向:矩阵含 partial/uncovered 行且 facts.handover 零有效行(任意
|
package/src/stage-review.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { existsSync, readFileSync, mkdirSync, readdirSync } from 'fs'
|
|
17
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
17
18
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
18
19
|
import { join, dirname } from 'path'
|
|
19
20
|
import { createHash } from 'crypto'
|
|
@@ -364,7 +365,7 @@ export function generateStageReviewRunId() {
|
|
|
364
365
|
*/
|
|
365
366
|
export function stageReviewMarkerPath(runtimeRoot, stage, changeName) {
|
|
366
367
|
const suffix = changeName ? `${stage}-${changeName}` : stage
|
|
367
|
-
return
|
|
368
|
+
return runtimeStatePath(runtimeRoot, `current-stage-review-run-id-${suffix}`)
|
|
368
369
|
}
|
|
369
370
|
|
|
370
371
|
/**
|
|
@@ -853,7 +854,7 @@ export function printStageReviewResult(result, context = {}) {
|
|
|
853
854
|
// fail-open:无 changeName / 无记录 / 读失败 → 零输出(旧口径)。
|
|
854
855
|
if (changeName && runtimeRoot) {
|
|
855
856
|
try {
|
|
856
|
-
const recPath =
|
|
857
|
+
const recPath = runtimeStatePath(runtimeRoot, `review-dispatch-${changeName}.json`)
|
|
857
858
|
if (existsSync(recPath)) {
|
|
858
859
|
const rec = JSON.parse(readFileSync(recPath, 'utf8'))
|
|
859
860
|
if (rec && typeof rec === 'object' && (rec.state === 'in-flight' || rec.state === 'dispatching')) {
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -58,7 +58,7 @@ export const definition = {
|
|
|
58
58
|
- **明显 small**(满足:单上下文可吞吐的纯执行类——改文案/修 bug/样式调整/配置微调,预计无 Wave 编排/上下文分片需求)→ 输出:「此变更规模较小,建议直接走轻量变更」+ 一句依据,给出可照抄建议命令(--input 引号内换行合法):
|
|
59
59
|
|
|
60
60
|
\`\`\`
|
|
61
|
-
sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
61
|
+
sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景
|
|
62
62
|
|
|
63
63
|
成功标准:
|
|
64
64
|
- <可验证标准>"
|
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
|