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.
Files changed (62) hide show
  1. package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
  2. package/.github/workflows/ci.yml +11 -3
  3. package/AGENTS.md +1 -1
  4. package/assets/command-cards/flow.md +1 -1
  5. package/package.json +1 -1
  6. package/src/archive-delta.js +4 -3
  7. package/src/change-delete.js +6 -5
  8. package/src/contract-matrix.js +2 -1
  9. package/src/cross-placement.js +5 -1
  10. package/src/db.js +8 -2
  11. package/src/deleted-ledger.js +2 -1
  12. package/src/doctor-diagnostics.js +8 -3
  13. package/src/flow-draft.js +2 -1
  14. package/src/flow.js +111 -62
  15. package/src/friction-ledger.js +2 -1
  16. package/src/friction-tally.js +8 -4
  17. package/src/handoff.js +2 -1
  18. package/src/hooks/worktree-guard.js +5 -5
  19. package/src/index.js +41 -7
  20. package/src/knowledge-hits.js +2 -1
  21. package/src/knowledge-vector.js +2 -1
  22. package/src/progress/consistency-doctor.js +5 -2
  23. package/src/progress/stage-machine.js +6 -2
  24. package/src/progress.js +19 -8
  25. package/src/quicklog.js +194 -9
  26. package/src/review-dispatch.js +4 -3
  27. package/src/review-tier.js +2 -1
  28. package/src/route-hindsight.js +4 -3
  29. package/src/run/bg-sync.js +10 -3
  30. package/src/run/command.js +13 -11
  31. package/src/run/complete-handlers.js +40 -37
  32. package/src/run/complete.js +12 -8
  33. package/src/run/gate-snapshot-ledger.js +2 -1
  34. package/src/run/gate-snapshot.js +57 -11
  35. package/src/run/gates.js +31 -7
  36. package/src/run/prompt.js +6 -5
  37. package/src/run/scan-profile.js +2 -1
  38. package/src/run/shared.js +109 -5
  39. package/src/run/stage.js +5 -4
  40. package/src/run/test-ledger.js +2 -1
  41. package/src/run/verify-quality-scan.js +11 -5
  42. package/src/runtime-paths.js +189 -0
  43. package/src/scan-refresh.js +4 -3
  44. package/src/scope-audit.js +7 -6
  45. package/src/sentinel-assertions.js +37 -0
  46. package/src/spec-sync.js +38 -22
  47. package/src/stage-contract.js +24 -2
  48. package/src/stage-review.js +3 -2
  49. package/src/stages/brainstorm.js +1 -1
  50. package/src/sync-noise.js +3 -2
  51. package/src/sync.js +71 -15
  52. package/src/task-done.js +2 -1
  53. package/src/task-review.js +7 -5
  54. package/src/task-tick.js +23 -1
  55. package/src/validate-artifacts.js +2 -1
  56. package/src/verify-draft.js +2 -1
  57. package/src/verify-postcheck.js +11 -10
  58. package/src/verify-probes.js +157 -52
  59. package/src/watcher.js +15 -9
  60. package/src/worktree-apply.js +2 -1
  61. package/src/write-audit.js +2 -1
  62. package/templates/agents-instruction.md +1 -1
@@ -16,7 +16,7 @@ description: 轻量变更(默认快道)——需求明确的小/中改动两
16
16
  ### ① flow start(立项 + 锁基线)
17
17
 
18
18
  ```bash
19
- sillyspec flow start --change <变更名> --input "<一句中文概括(首行即变更标题);随后动机与背景
19
+ sillyspec flow start --change <变更名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景
20
20
 
21
21
  成功标准:
22
22
  - <可验证标准>"
@@ -20,7 +20,8 @@ jobs:
20
20
  - uses: actions/checkout@v4
21
21
  - uses: actions/setup-node@v4
22
22
  with:
23
- node-version: '22'
23
+ node-version: '24'
24
+ # mock.module 内置模块 named exports 枚举在 node 22 不全(appendFileSync 等缺失实证 38859877373)——统一 24 与本地开发环一致
24
25
  - name: lint(check-syntax 全文件)
25
26
  run: npm run lint --silent
26
27
 
@@ -32,12 +33,18 @@ jobs:
32
33
  - uses: actions/checkout@v4
33
34
  - uses: actions/setup-node@v4
34
35
  with:
35
- node-version: '22'
36
+ node-version: '24'
37
+ # mock.module 内置模块 named exports 枚举在 node 22 不全(appendFileSync 等缺失实证 38859877373)——统一 24 与本地开发环一致
36
38
  # 依赖必须装:测试框架零三方(node:sqlite/test 内置)但被测 CLI 本体 import 7 个
37
39
  # 运行时依赖(js-yaml/chalk/@inquirer/prompts 等,src 下 10+ 文件)——CI 首跑
38
40
  # (121a33ee)无 install 步实证两 job 全灭(Cannot find package 'js-yaml')
39
41
  - name: install(npm ci 锁定 package-lock)
40
42
  run: npm ci --no-audit --no-fund
43
+ - name: 诊断:node/dash 可用性(run-tests 子进程 shell 语义面)
44
+ run: |
45
+ which node
46
+ node -e "0" && echo "sh 直跑 node -e OK"
47
+ /bin/sh -c 'node -e "0"' && echo "sh -c 内嵌引号 OK"
41
48
  - name: test(run-tests 并发池 + TEMP/HOME 套件隔离)
42
49
  run: npm test --silent
43
50
 
@@ -48,7 +55,8 @@ jobs:
48
55
  - uses: actions/checkout@v4
49
56
  - uses: actions/setup-node@v4
50
57
  with:
51
- node-version: '22'
58
+ node-version: '24'
59
+ # mock.module 内置模块 named exports 枚举在 node 22 不全(appendFileSync 等缺失实证 38859877373)——统一 24 与本地开发环一致
52
60
  # docs-check.js 同样 import js-yaml——无 install 首跑 7s 即灭(同 test job 根因)
53
61
  - name: install(npm ci 锁定 package-lock)
54
62
  run: npm ci --no-audit --no-fund
package/AGENTS.md CHANGED
@@ -16,7 +16,7 @@
16
16
  `--input` 过门格式(轻量道首次调用即过,勿靠报错学;引号内换行合法,下例可照抄):
17
17
 
18
18
  ```bash
19
- sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景
19
+ sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文概括(首行即变更标题,必须落在 10-30 字硬区间、建议 ~20 字——不合区间 flow start 拒启);随后动机与背景
20
20
 
21
21
  成功标准:
22
22
  - <可验证标准>"
@@ -11,7 +11,7 @@ description: 轻量变更(默认快道):需求明确的小/中改动两步
11
11
  ```bash
12
12
  export SILLYSPEC_SESSION_ID=<agent名+任务名> # 每条命令都带(shell 状态不持久;多会话并行的所有权判定依赖它)
13
13
  # ① 立项 + 锁基线(--input 引号内换行合法,可照抄;缺「成功标准」条目会被清晰度门拦下):
14
- sillyspec flow start --change <变更名> --input "<一句中文概括(首行即变更标题);随后动机与背景
14
+ sillyspec flow start --change <变更名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景
15
15
 
16
16
  成功标准:
17
17
  - <可验证标准>"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.33.0",
3
+ "version": "3.33.1",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -29,6 +29,7 @@
29
29
  * endpoint-extractor,被依赖者均不反向 import 本模块。
30
30
  */
31
31
  import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'
32
+ import { runtimeStatePath } from './runtime-paths.js'
32
33
  import { basename, dirname, join } from 'node:path'
33
34
  import { parseDecisionDomains, loadModuleMap, deriveActualModules } from './design-facts.js'
34
35
  import { parseModuleMapSimple } from './modules.js'
@@ -153,7 +154,7 @@ export function collectDeltaSources({ changeDir, specRoot, project, runtimeRoot,
153
154
  // 兜底(Gap-2/D-003):全无 reconcile → apply-pathspec-<change>.txt 逐行路径作交付清单
154
155
  let deliverables = []
155
156
  if (!reconcile && runtimeRoot && change) {
156
- const text = readTextSafe(join(runtimeRoot, `apply-pathspec-${change}.txt`))
157
+ const text = readTextSafe(runtimeStatePath(runtimeRoot, `apply-pathspec-${change}.txt`))
157
158
  if (text !== null) {
158
159
  deliverables = text.split(/\r?\n/).map(s => s.trim()).filter(Boolean)
159
160
  }
@@ -454,8 +455,8 @@ export function writeLastDeltaSidecar(runtimeRoot, summary) {
454
455
  updatedAt: new Date().toISOString(),
455
456
  }
456
457
  mkdirSync(runtimeRoot, { recursive: true })
457
- writeFileSync(join(runtimeRoot, 'last-delta.json'), JSON.stringify(payload, null, 2) + '\n')
458
- return { ok: true, path: join(runtimeRoot, 'last-delta.json') }
458
+ writeFileSync(runtimeStatePath(runtimeRoot, 'last-delta.json'), JSON.stringify(payload, null, 2) + '\n')
459
+ return { ok: true, path: runtimeStatePath(runtimeRoot, 'last-delta.json') }
459
460
  } catch (e) {
460
461
  console.error(`[sillyspec] last-delta sidecar 写入失败(advisory 将缺席,不阻断 delta 生成):${e && e.message ? e.message : e}`)
461
462
  return { ok: false, reason: e && e.message ? e.message : String(e) }
@@ -12,7 +12,7 @@
12
12
  * 不因删变更丢用户唯一副本的代码;顺带清 execute/stage-review runId marker
13
13
  * + 该变更的 runtime 取证(pruneArchivedChangeRuntime))
14
14
  * - git:safeGit add -A 暂存目录删除(best-effort,对齐 archive CLI 下沉 git add)
15
- * - 平台:triggerSync 推终态(sync.js 对 status='deleted' 上行墓碑,平台收敛软删)
15
+ * - 平台:triggerSyncTerminalStrict 推终态(sync.js 对 status='deleted' 上行墓碑,平台收敛软删;文件树推送失败显性化)
16
16
  *
17
17
  * 两段式安全(对齐 --cleanup-remnant / --cleanup-ghosts / --align-execute-progress 家族):
18
18
  * 默认 dry-run 只报告将发生什么,--confirm 才执行。
@@ -23,7 +23,7 @@
23
23
  import { existsSync, readdirSync, rmSync, statSync } from 'fs';
24
24
  import { join, relative } from 'path';
25
25
  import { safeGit } from './git-helper.js';
26
- import { assertSafeChangeName, triggerSync } from './run/shared.js';
26
+ import { assertSafeChangeName, triggerSyncTerminalStrict } from './run/shared.js';
27
27
  import { archiveWorktreeCleanup } from './run/complete-handlers.js';
28
28
 
29
29
  function countFiles(dir) {
@@ -156,10 +156,11 @@ export async function deleteChange({ cwd, specDir = null, changeName, confirm =
156
156
  safeGit(cwd, ['add', '-A', '--', relDir]);
157
157
  }
158
158
  } catch { /* 非 git 仓等场景:删除在 git status 可见,暂存非必需 */ }
159
- // 5. 平台终态/墓碑上行(triggerSync 内建 8s 熔断 + 未连接静默;change-delete 是终态
160
- // 一次性命令,await 保证墓碑发出后再退出,输出顺序确定)
159
+ // 5. 平台终态/墓碑上行(终态强校验:内联有界同步,文件树推送失败显性化——🚨 告警 +
160
+ // 自救命令 + exit 1;未连接静默。change-delete 是终态一次性命令,await 保证墓碑
161
+ // 发出后再退出,输出顺序确定)
161
162
  try {
162
- await triggerSync(cwd, changeName, { specRoot: specBase });
163
+ await triggerSyncTerminalStrict(cwd, changeName, { specRoot: specBase });
163
164
  } catch (e) {
164
165
  warnings.push(`平台终态同步失败(不阻断删除,下次常规同步重试):${e.message}`);
165
166
  }
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs'
12
+ import { runtimeStatePath } from './runtime-paths.js'
12
13
  import { join, resolve, basename, dirname, relative, isAbsolute } from 'path'
13
14
  import { gitQuiet } from './git-helper.js'
14
15
  import { splitOwnVsForeignDiffFiles } from './foreign-declared.js'
@@ -440,7 +441,7 @@ function _resolveDiffFilesForParity(specBase, scanRoot, changeName, runtimeRoot)
440
441
  // 主仓 diff 空(apply 后已 commit / cleanup 完毕)→ apply-pathspec 兜底:apply 成功时落盘的
441
442
  // 本变更精确文件清单(与 gates 归因提示②同源)。仍拿不到才回 null(调用方退全仓)。
442
443
  try {
443
- const pathspecFile = join(runtimeRoot || join(specBase, '.runtime'), `apply-pathspec-${changeName}.txt`)
444
+ const pathspecFile = runtimeStatePath(runtimeRoot || join(specBase, '.runtime'), `apply-pathspec-${changeName}.txt`)
444
445
  if (existsSync(pathspecFile)) {
445
446
  const fromPathspec = readFileSync(pathspecFile, 'utf8').split('\n').map(l => l.trim()).filter(Boolean)
446
447
  if (fromPathspec.length > 0) {
@@ -48,9 +48,13 @@ export function crossWorktreePath(specBase, changeName, repoKey, placementRoot =
48
48
  * @param {string} mainRoot 主仓根(相对路径解析基准)
49
49
  * @returns {string} 绝对落位根
50
50
  */
51
+ // Windows 盘符形态判定(坑跨平台 CI:POSIX isAbsolute('C:/x')=false → 带引号的盘符路径
52
+ // 被相对主仓根拼接成 test/C:/quoted 伪路径)。跨仓落位本就面向 Windows 盘符场景设计,
53
+ // 语义上盘符路径恒绝对(与 win32 path.isAbsolute 同判,跨平台自洽)。
54
+ const WIN_DRIVE_ABS_RE = /^[A-Za-z]:[/\\]/;
51
55
  export function resolvePlacementRoot(raw, mainRoot) {
52
56
  const trimmed = String(raw || '').trim().replace(/^['"]|['"]$/g, '');
53
- if (isAbsolute(trimmed)) return resolvePath(trimmed);
57
+ if (isAbsolute(trimmed) || WIN_DRIVE_ABS_RE.test(trimmed)) return resolvePath(trimmed);
54
58
  return resolvePath(mainRoot, trimmed);
55
59
  }
56
60
 
package/src/db.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { openDatabase, applyPragmas, runTransaction } from './db-engine.js';
2
+ import { runtimeTempPath } from './runtime-paths.js'
2
3
  import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
3
4
  import { dirname } from 'path';
4
5
 
@@ -102,7 +103,8 @@ export class DB {
102
103
  * 全新 node:sqlite 项目不产生主 .bak)。
103
104
  */
104
105
  _openWithFallback() {
105
- const bakPath = `${this.dbPath}.bak`;
106
+ // .bak 2026-10-18 布局分层后归 temp/;根路径保留兜底(未迁移旧仓/混版本窗口)
107
+ const bakPath = existsSync(`${this.dbPath}.bak`) ? `${this.dbPath}.bak` : runtimeTempPath(dirname(this.dbPath), 'sillyspec.db.bak');
106
108
  // readValid: null=文件不存在;'empty'=存在但 0 字节;'ok'=有内容
107
109
  const readValid = (p) => {
108
110
  if (!existsSync(p)) return null;
@@ -156,7 +158,11 @@ export class DB {
156
158
  // 覆盖前救援快照(2026-08-21 审查 BUG-7):主库有内容但打不开("损坏")时,其中可能
157
159
  // 含比 .bak 更新的数据——静默覆盖即静默丢。best-effort 留一份 .corrupt-<ts> 副本供人工恢复。
158
160
  if (primaryRaw === 'ok') {
159
- try { copyFileSync(this.dbPath, `${this.dbPath}.corrupt-${Date.now()}`); } catch { /* 救援失败不阻断恢复 */ }
161
+ try {
162
+ const corruptPath = runtimeTempPath(dirname(this.dbPath), `sillyspec.db.corrupt-${Date.now()}`);
163
+ try { mkdirSync(dirname(corruptPath), { recursive: true }); } catch { /* fail-open */ }
164
+ copyFileSync(this.dbPath, corruptPath);
165
+ } catch { /* 救援失败不阻断恢复 */ }
160
166
  }
161
167
  // node:sqlite 绑定路径,无 _save:把 .bak 内容 copy 回主库,统一用 dbPath。
162
168
  // 体检 BUG-18:主库可能正被其他进程(daemon/另一 CLI)打开——Windows 覆盖他人
@@ -14,12 +14,13 @@
14
14
  * 坏行容错:单行 JSON 解析失败跳过该行(外部编辑/半写截断不拖垮读取)。
15
15
  */
16
16
  import { appendFileSync, existsSync, readFileSync } from 'node:fs'
17
+ import { runtimeStatePath } from './runtime-paths.js'
17
18
  import { join } from 'node:path'
18
19
 
19
20
  const LEDGER_FILENAME = 'deleted-changes.jsonl'
20
21
 
21
22
  function ledgerPath(specDir) {
22
- return join(specDir, '.runtime', LEDGER_FILENAME)
23
+ return runtimeStatePath(join(specDir, '.runtime'), LEDGER_FILENAME)
23
24
  }
24
25
 
25
26
  /**
@@ -23,6 +23,7 @@
23
23
  * writer 落盘到 <authoritySpecDir>/.runtime/。
24
24
  */
25
25
  import { openDatabase, pluckGet, pluckAll } from './db-engine.js';
26
+ import { runtimeTempPath } from './runtime-paths.js'
26
27
  import { safeGit } from './git-helper.js';
27
28
  import { createHash } from 'crypto';
28
29
  import { existsSync, statSync, readFileSync, readdirSync, mkdirSync, writeFileSync, unlinkSync, rmSync } from 'fs';
@@ -1109,12 +1110,15 @@ export function detectSelfMaintenanceTax(cwd, authoritySpecDir) {
1109
1110
  const warnings = [];
1110
1111
 
1111
1112
  // ① 活跃非零 tally 列示(events:{type:{count,lastAt}},X-10 结构)
1113
+ // 2026-10-18 布局分层:tally 落 state/;根目录扫描保留旧布局兜底(入口迁移收敛后恒空)
1112
1114
  try {
1113
- for (const f of readdirSync(runtimeRoot)) {
1115
+ const tallyDirs = [join(runtimeRoot, 'state'), runtimeRoot]
1116
+ for (const tallyDir of tallyDirs) {
1117
+ for (const f of (() => { try { return readdirSync(tallyDir) } catch { return [] } })()) {
1114
1118
  const m = f.match(/^friction-tally-(.+)\.json$/);
1115
1119
  if (!m) continue;
1116
1120
  let tally = null;
1117
- try { tally = JSON.parse(readFileSync(join(runtimeRoot, f), 'utf8')); } catch { continue; }
1121
+ try { tally = JSON.parse(readFileSync(join(tallyDir, f), 'utf8')); } catch { continue; }
1118
1122
  const events = tally && typeof tally === 'object' && tally.events && typeof tally.events === 'object' && !Array.isArray(tally.events) ? tally.events : {};
1119
1123
  const segs = [];
1120
1124
  let total = 0;
@@ -1131,6 +1135,7 @@ export function detectSelfMaintenanceTax(cwd, authoritySpecDir) {
1131
1135
  findings.push(`活跃摩擦:${m[1]}(${segs.join('、')}${lastAt ? `,最近 ${lastAt}` : ''})`);
1132
1136
  if (total >= TAX_HEAVY_TOTAL) warnings.push(`税重——评估该机制群简化/退役(对照其 decisions 退役判据):${m[1]}(活跃 total ${total})`);
1133
1137
  }
1138
+ }
1134
1139
  } catch { /* runtimeRoot 不可读 → 无活跃数据,不误报 */ }
1135
1140
 
1136
1141
  // ② 台账聚合(近 90 天 top5 + 累计总量);④ 缺失渲染提示不告警
@@ -1748,7 +1753,7 @@ export function writeDoctorDiagnosis(json, specDir) {
1748
1753
  try {
1749
1754
  const runtimeDir = join(specDir, '.runtime');
1750
1755
  mkdirSync(runtimeDir, { recursive: true });
1751
- const outPath = join(runtimeDir, 'doctor-diagnosis.json');
1756
+ const outPath = runtimeTempPath(runtimeDir, 'doctor-diagnosis.json');
1752
1757
  writeFileSync(outPath, JSON.stringify(json, null, 2) + '\n');
1753
1758
  return outPath;
1754
1759
  } catch (e) {
package/src/flow-draft.js CHANGED
@@ -23,6 +23,7 @@ import { wrapSection, verifyMarkers, reanchorText, bodyHash, parseMarkerBlocks }
23
23
  // 占位句常量真同源(2026-10-03-fr-skeleton-gate 评审 P3 清偿):起草端 Then 兜底与骨架判据
24
24
  // (fr-index.isThinSkeletonBodies)共用同一字面量——两处独立字符串会被评审实证为「镜像非同源」。
25
25
  import { SKELETON_THEN_PLACEHOLDER } from './fr-index.js'
26
+ import { runtimeStatePath } from './runtime-paths.js'
26
27
 
27
28
  /** 读文件并归一化 CRLF→LF(2026-09-25-feedback-fixes④:Python/编辑器写盘 CRLF 会破坏
28
29
  锚定的槽位识别 regex)。 */
@@ -88,7 +89,7 @@ const QUESTION_REPAIR_SIM_THRESHOLD = 0.5
88
89
 
89
90
  /** sidecar 台账路径(.runtime 下,verify-draft sidecar 同族;命名空间=文件:段键)。 */
90
91
  export function draftLedgerPath(runtimeRoot, changeName) {
91
- return join(runtimeRoot, `draft-ledger-${changeName}.json`)
92
+ return runtimeStatePath(runtimeRoot, `draft-ledger-${changeName}.json`)
92
93
  }
93
94
 
94
95
  const AGENT_SLOT = (n, hint) => `<!--AGENT:槽${n} ${hint}——例外裁决书写面(机器段之外合法) -->`
package/src/flow.js CHANGED
@@ -26,12 +26,13 @@
26
26
  * 幂等循 task-done 先例:子步各查自身完成标记,中断半态重入断点续,中段失败精确报告。
27
27
  */
28
28
  import { existsSync, readFileSync, writeFileSync, readdirSync, appendFileSync, renameSync, statSync } from 'node:fs'
29
+ import { runtimeStatePath, runtimeTempPath } from './runtime-paths.js'
29
30
  import { join, relative, dirname } from 'node:path'
30
31
  import { DatabaseSync } from 'node:sqlite'
31
32
  import yaml from 'js-yaml'
32
33
  import { git, gitQuiet } from './git-helper.js'
33
34
  import { writeAtomicSync } from './fs-atomic.js'
34
- import { resolveRuntimeRoot, triggerSync, assertDatedChangeName } from './run/shared.js'
35
+ import { resolveRuntimeRoot, triggerSync, triggerSyncTerminalStrict, assertDatedChangeName } from './run/shared.js'
35
36
  import { detectUiTouch, buildUiGuidanceLines, runUiVisualProbe, readUiVisualGate, UI_EVIDENCE_FILENAME } from './ui-visual.js'
36
37
  import { runHunkAttributionGate, readHunkGate, renderHunkAttributionLines } from './hunk-attribution.js'
37
38
  // 2026-10-13-thin-sync-stage:FLOW_STATE_FILE/readFlowState 单一源上移叶子模块 flow-state.js
@@ -686,18 +687,24 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
686
687
  try {
687
688
  const { spawnWatcher } = await import('./watcher.js')
688
689
  const w = await spawnWatcher(cwd, change, { specBase })
689
- if (w.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/watcher-events-${change}.jsonl(恒带 provisional:true)`)
690
+ if (w.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/state/watcher-events-${change}.jsonl(恒带 provisional:true)`)
690
691
  } catch { /* 观测旁路 best-effort */ }
691
- // adopt 标题(用户需求 2026-09-29):头脑风暴 proposal 的 H1 是人写语义标题,
692
- // deriveTitleFromLinkedChange 提取;--title 优先;兜底变更名。adopt 路径此前完全不
693
- // 注册 changes 行——initChange 补插(幂等),既有行 title 空时回填。
692
+ // adopt 标题(2026-09-29;2026-10-16-short-change-title 提炼;2026-10-16-title-range-gate
693
+ // 硬区间 10-30):头脑风暴 proposal H1 人写语义标题(纯模板 H1 回退动机首句)→ --title
694
+ // 优先(合区间校验,超界 exit 2)→ --input 首行提炼 → 兜底变更名。收编是既成事实的
695
+ // 延续面,非 flag 的超界不硬断(收编半态不可恢复)打 ⚠️ 强提示。
694
696
  try {
695
- const { deriveTitleFromLinkedChange, deriveChangeTitle } = await import('./quicklog.js')
696
- const adoptTitle = titleFlag || deriveTitleFromLinkedChange(specBase, change) || deriveChangeTitle(input) || change
697
+ const { deriveTitleFromLinkedChange, deriveShortChangeTitle, isValidChangeTitle, shortTitleRejectMessage, shortTitleRangeWarnLine } = await import('./quicklog.js')
698
+ if (titleFlag && !isValidChangeTitle(titleFlag)) {
699
+ console.error(shortTitleRejectMessage(titleFlag))
700
+ process.exit(2)
701
+ }
702
+ const adoptTitle = titleFlag || deriveTitleFromLinkedChange(specBase, change) || deriveShortChangeTitle(input) || change
697
703
  pm.initChange(cwd, change, { title: adoptTitle })
698
704
  const _arow = pm._ensureDB(cwd).getDb().prepare('SELECT title FROM changes WHERE name = ?').get(change)
699
705
  if (_arow && !_arow.title) pm.updateChangeMeta(cwd, change, { title: adoptTitle })
700
706
  console.log(`🏷️ 变更标题:${adoptTitle}(收编自头脑风暴产物;重入 --title 可改)`)
707
+ if (!isValidChangeTitle(adoptTitle)) console.log(shortTitleRangeWarnLine(adoptTitle))
701
708
  } catch { /* adopt 标题 best-effort */ }
702
709
  const materials = materialPaths(specBase, change, changeDir)
703
710
  const artifacts = changeArtifactPaths(changeDir)
@@ -719,7 +726,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
719
726
  `② 执行走任务循环:对 tasks.md 每个 pending 行——展示「Working on task N/M: <任务>」→ 做 → 测试绿后当场勾一格(\`- [ ]\`→\`- [x]\`:sillyspec task tick --change ${change} --task task-NN 或直接 Edit)→ 下一行;全勾后 flow done(收口硬门拒单拍多格勾选)。design/decisions 是本变更的承诺锚(flow done 豁免 design 四节槽,以其为准)。`,
720
727
  `requirements 测试绑定槽(收编追加)每条 FR 至少一行作答;写码前后顺手填。`,
721
728
  `✅ 任务面归你(工作分解契约):tasks.md 是你的工作队列(机器种子可增删改,保持 \`- [ ] task-NN:\` 行形态)——CLI 以 checkbox 为进度状态机(task tick/flow status 实时读它),TodoWrite 类工具不替代;`,
722
- ` 勾选是收口证据面(逐格核提交 token/review.json):做一件 → 勾一格(task tick 即时回显进度与下一任务)→ 继续下一条,勿攒一把勾;`,
729
+ ` 勾选是收口证据面(逐格核提交 token/review.json):做一件 → 勾一格(task tick 即时回显进度与下一任务)→ 继续下一条,勿攒一把勾;勾选修改与任务交付同一提交入库(pathspec 带上本变更 tasks.md——task tick 回显给现成命令);`,
723
730
  ` 任务拟法:收口/推送/发布类动作不单列 task(协议自身承担,成功标准照写 requirements);确需单列的,勾选前先有消息预埋 task-NN token 的交付提交(回执类证据写进提交消息)。`,
724
731
  ` flow status --change ${change} 为自愿查看/恢复面(非协议必需,D-007 中间零必需交互;恢复时给下一任务指针与进度)。`,
725
732
  `⚠️ 交付纪律:收口前交付代码显式 pathspec 提交——冻结件范围=baseline..HEAD,未提交不进审计件。`,
@@ -780,17 +787,24 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
780
787
  writeFlowState(changeDir, { autopilot: true })
781
788
  console.log(`🤖 已声明 autopilot:spec 断点人审豁免(用户显式声明留痕)`)
782
789
  }
783
- // 变更标题补写(用户需求 2026-09-29:title=中文概括 ≤50 字,建议 ~20 字)——存量在途
784
- // 变更 title 空/缺失时的回填通道:显式 --title 恒生效(重入改标题);否则从 --input 首
785
- // 行推导、仅当库内 title 为空才写(不覆盖已有语义标题)。best-effort 不阻断恢复简报。
790
+ // 变更标题补写(2026-09-29 title=中文概括;2026-10-16-short-change-title 提炼 ≤30;
791
+ // 2026-10-16-title-range-gate 硬区间 10-30)——存量在途变更 title 空/缺失时的回填通道:
792
+ // 显式 --title 恒生效(重入改标题)且必须合区间(超界 exit 2,参数面最易修正);提炼补写
793
+ // 仅当库内 title 为空才写(首写者胜),超界不阻断(resume 不可断)打 ⚠️ 强提示。
786
794
  try {
787
- const { deriveChangeTitle } = await import('./quicklog.js')
795
+ const { deriveShortChangeTitleEx, isValidChangeTitle, shortTitleRejectMessage, shortTitleRangeWarnLine, SHORT_TITLE_HARDCUT_HINT } = await import('./quicklog.js')
796
+ if (titleFlag && !isValidChangeTitle(titleFlag)) {
797
+ console.error(shortTitleRejectMessage(titleFlag))
798
+ process.exit(2)
799
+ }
788
800
  const _row = pm._ensureDB(cwd).getDb().prepare('SELECT title FROM changes WHERE name = ?').get(change)
789
801
  const _cur = _row ? (_row.title || '') : ''
790
- const _derived = titleFlag || deriveChangeTitle(input)
791
- if (_derived && (titleFlag || !_cur)) {
792
- pm.updateChangeMeta(cwd, change, { title: _derived })
793
- console.log(`🏷️ 变更标题${titleFlag ? '已更新' : '已补写'}:${_derived}(面板显示用;建议 ~20 字中文概括,重入 --title 可改)`)
802
+ const _derived = titleFlag ? { title: titleFlag, hardCut: false } : deriveShortChangeTitleEx(input)
803
+ if (_derived && _derived.title && (titleFlag || !_cur)) {
804
+ pm.updateChangeMeta(cwd, change, { title: _derived.title })
805
+ console.log(`🏷️ 变更标题${titleFlag ? '已更新' : '已补写'}:${_derived.title}(面板显示用;须 10-30 字、建议 ~20 字,重入 --title 可改)`)
806
+ if (_derived.hardCut) console.log(SHORT_TITLE_HARDCUT_HINT)
807
+ if (!titleFlag && !isValidChangeTitle(_derived.title)) console.log(shortTitleRangeWarnLine(_derived.title))
794
808
  }
795
809
  } catch { /* 标题补写 best-effort */ }
796
810
  // 知识注入(2026-09-25-thin-fr-inject-parity):resume 路径域路由走基线以来文件面——
@@ -893,16 +907,29 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
893
907
  // 形态信号:清晰度门管「需求说不清楚」(预段收编),升厚只留用户决策(--upgrade-thick 同意门)
894
908
  // 与运行时证据(实测失败升档/edit_ratio/评审——风险面在收口时点按承诺词/diff 原语/盲维判定)。
895
909
 
896
- // 变更标题(用户需求 2026-09-29:title=中文概括 ≤50 字、建议 ~20 字,agent 总结):
897
- // --title 显式指定优先;否则 --input 首行推导(agent 在 input 首行写一句中文概括即为标题);
898
- // 两者皆缺用变更名兜底(可辨识但不达意——横幅提示下次带 --title)。
899
- let changeTitle = null
910
+ // 变更标题硬区间门(2026-10-16-title-range-gate:10-30 字必须、建议 ~20、强制 agent 命名;
911
+ // 提炼口径见 2026-10-16-short-change-title):--title 显式 > --input 首行提炼 > 变更名兜底。
912
+ // agent 命名时刻强制:不在区间/无 --input 兜底 → 拒绝启动(exit 2,在任何状态写入前——
913
+ // 零半态);平台 writer 预建空目录(自动派发,无 agent 对话面)豁免硬门仅 ⚠️ 留痕。
914
+ let _st = null
915
+ let _rejectMsg = null
900
916
  try {
901
- const { deriveChangeTitle } = await import('./quicklog.js')
902
- changeTitle = titleFlag || deriveChangeTitle(input) || change
903
- } catch { changeTitle = titleFlag || change }
917
+ const _q = await import('./quicklog.js')
918
+ _st = _q.resolveStartTitle({ titleFlag, input, change, enforce: !proceedFreshEmptyDir })
919
+ if (!_st.ok) _rejectMsg = _q.shortTitleRejectMessage(_st.kind === 'fallback' ? _st.rejectedTitle : _st.title)
920
+ } catch { /* quicklog 不可用(实践不可达):降级保持旧行为兜底 */ _st = { title: titleFlag || change, ok: true, kind: titleFlag ? 'flag' : 'fallback', hardCut: false } }
921
+ if (_rejectMsg) {
922
+ console.error(_rejectMsg)
923
+ process.exit(2)
924
+ }
925
+ const changeTitle = _st.title
904
926
  pm.initChange(cwd, change, { title: changeTitle })
905
- console.log(`🏷️ 变更标题:${changeTitle}${changeTitle === change ? '(变更名兜底——下次带 --title "<≤20 字中文概括>" 更达意)' : '(面板显示用;建议 ~20 字中文概括,重入 flow start --title 可改)'}`)
927
+ console.log(`🏷️ 变更标题:${changeTitle}(面板显示用;须 10-30 字、建议 ~20 字,重入 flow start --title 可改)`)
928
+ try {
929
+ const { isValidChangeTitle, shortTitleRangeWarnLine, SHORT_TITLE_HARDCUT_HINT } = await import('./quicklog.js')
930
+ if (_st.hardCut) console.log(SHORT_TITLE_HARDCUT_HINT)
931
+ if (proceedFreshEmptyDir && !isValidChangeTitle(changeTitle)) console.log(shortTitleRangeWarnLine(changeTitle))
932
+ } catch { /* 提示行 best-effort */ }
906
933
  try {
907
934
  const { resolveSessionIdentity } = await import('./progress.js')
908
935
  const { session } = resolveSessionIdentity({ flagSession: null, cwd })
@@ -915,7 +942,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
915
942
  try {
916
943
  const { spawnWatcher } = await import('./watcher.js')
917
944
  const r = await spawnWatcher(cwd, change, { specBase })
918
- if (r.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/watcher-events-${change}.jsonl(恒带 provisional:true)`)
945
+ if (r.status === 'spawned') console.log(`🔄 [watcher] 观测旁路已拉起:事件流 .sillyspec/.runtime/state/watcher-events-${change}.jsonl(恒带 provisional:true)`)
919
946
  } catch { /* 观测旁路 best-effort,绝不阻断协议面 */ }
920
947
 
921
948
  const head = gitQuiet(cwd, ['rev-parse', 'HEAD'])
@@ -1112,13 +1139,13 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
1112
1139
  ` 也有 1/4 抽查采样。要强制/豁免可重启时带 --review / --no-review${reviewForce === true ? '(本变更已声明 --review)' : reviewForce === false ? '(本变更已声明 --no-review)' : ''}。`,
1113
1140
  `⚠️ 交付纪律:收口前先把交付代码用显式 pathspec 提交(git add -- <文件> && git commit)——`,
1114
1141
  ` patch 冻结件范围=baseline..HEAD 提交面,未提交的代码不进审计件(R16 评审 P2 实证);`,
1115
- ` tasks.md 一并显式 pathspec 提交(勾选证据进 git 历史,勿 untracked 直至归档——R19 实证)。`,
1142
+ ` tasks.md 一并显式 pathspec 提交(勾选证据进 git 历史,勿 untracked 直至归档——R19 实证;task tick 回显会给现成命令)。`,
1116
1143
  `✅ 任务面归你(工作分解契约,2026-10-07-thin-tasks-v3):tasks.md 是你的工作队列——机器种子按真实实现路径改写`,
1117
1144
  ` (每行=可独立完成并当场验证的一步:做什么+怎么验证;增删改随意,保持 \`- [ ] task-NN:\` 行形态);`,
1118
1145
  ` 执行循环:对每个未勾行 Working on task N/M → 做一件 → 测试绿后当场勾这一格 → 下一行——CLI 以 checkbox 为进度状态机`,
1119
1146
  ` (sillyspec task tick --change ${change} --task task-NN 即时回显进度 N/M 与下一任务指针,flow status 同源;或直接 Edit 翻格 \`- [ ]\`→\`- [x]\`),watcher 实时上平台。`,
1120
1147
  ` ⚠️ harness 的 TodoWrite 类工具是会话内便利面,不替代 tasks.md——平台进度/收口证据只读它(0/12 事故实证:todo 全 completed 而 tasks.md 0 勾);`,
1121
- ` 勾选是收口证据面:每格勾选要有对应提交(消息带 task-NN——逐个列出或 /、,连写如 task-01/02/03,「task-01..task-05」区间写法不认)或 review.json,全勾但零证据会被拒收;勿攒一把勾(收口硬门拒单拍多格);`,
1148
+ ` 勾选是收口证据面:每格勾选要有对应提交(消息带 task-NN——逐个列出或 /、,连写如 task-01/02/03,「task-01..task-05」区间写法不认;pathspec 带上本变更 tasks.md——勾选修改与任务交付同一提交入库,task tick 回显会给现成命令)或 review.json,全勾但零证据会被拒收;勿攒一把勾(收口硬门拒单拍多格);`,
1122
1149
  // 任务拟法指引(2026-10-12-closeout-task-guidance,三次「哨兵抓收口型任务零证据」复盘):
1123
1150
  // 成功标准≠工作分解——收口/推送/发布是协议自身承担的动作;单列则证据(token 提交)必须先于勾选存在。
1124
1151
  ` 任务拟法:收口/推送/发布类动作不单列 task(提交、归档、推送是 flow start→done 协议自身承担的动作——成功标准照写 requirements,勿逐字搬进任务清单);确需单列的收口/操作型任务,勾选前先做消息预埋 task-NN token 的交付提交——回执类证据(push/publish 回执、构建日志路径)写进提交消息,勿依赖勾选后补;`,
@@ -1177,7 +1204,7 @@ function printRecoveryBriefing({ cwd, specBase, change, changeDir, runtimeRoot,
1177
1204
  ? (gitQuiet(cwd, ['rev-list', '--count', `${st.baseline_commit}..HEAD`]) || '0').toString().trim()
1178
1205
  : '?'
1179
1206
  const dirty = changedFilesSinceBaseline(cwd, st.baseline_commit).length
1180
- const hasLedger = existsSync(join(runtimeRoot, `verify-quality-scan-${change}.json`))
1207
+ const hasLedger = existsSync(runtimeStatePath(runtimeRoot, `verify-quality-scan-${change}.json`))
1181
1208
  console.log([
1182
1209
  `🔁 flow start 恢复简报(重入): ${change}(tier=${st.tier}${st.legacy_fallback ? ',legacy_fallback=true 混跑回退中' : ''})`,
1183
1210
  `══════════════════════════════════════════════════════`,
@@ -1265,7 +1292,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1265
1292
  // 不许在失败面丢失;成功收口走函数末尾同款记录,失败面只多不少)
1266
1293
  const appendTelemetry = (review) => {
1267
1294
  try {
1268
- appendFileSync(join(runtimeRoot, 'flow-telemetry.jsonl'), JSON.stringify({
1295
+ appendFileSync(runtimeTempPath(runtimeRoot, 'flow-telemetry.jsonl'), JSON.stringify({
1269
1296
  ts: new Date().toISOString(), change, protocolCalls: 2,
1270
1297
  draftAmendments: st.edit_ratio != null ? 1 : 0, editRatio: st.edit_ratio ?? null,
1271
1298
  routeHint: st.route_hint ?? null, tier: st.tier, upgraded: st.upgrade_reason ?? null,
@@ -1369,6 +1396,22 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1369
1396
  let _sentinelNonMirrorTasks = null // 镜像豁免面外的任务数(null=哨兵未跑;0=纯镜像任务面——节奏 advisory 静默)
1370
1397
  let _sentinelEvidenceComplete = false // 哨兵证据完备(status=complete——每格已勾任务均有 token/review.json):单拍降级判据(2026-10-13-batchtick-evidence-domain-hint)
1371
1398
  let _autopilotTicked = 0 // 机器代勾格数(>0 时勾选节奏门降 advisory——代勾是单拍多格机械写,非 agent 纪律面)
1399
+ // 勾选观测共享采集(2026-10-17-tick-timing-source):watcher 事件流 + task-ticks.jsonl 侧车
1400
+ // 一次读取——时点判定与勾选节奏门同源消费(此前节奏门单独读,现两门一份数据;检测逻辑
1401
+ // 与优先序不变:事件流先判、空结果回退侧车突发)。侧车=CLI tick 逐格毫秒时戳事实源(随
1402
+ // 归档进 git、不受观测盲窗影响)。任一源缺失按空(fail-open),绝不阻断收口。
1403
+ const _tickObs = await (async () => {
1404
+ try {
1405
+ const { readWatcherEvents } = await import('./watcher.js')
1406
+ const { detectBatchCheckCadence, detectTickBurst } = await import('./sentinel-assertions.js')
1407
+ const stream = readWatcherEvents({ runtimeRoot, change })
1408
+ let ticks = []
1409
+ try { ticks = (await import('./timeline.js')).parseTickLog(readFileSync(join(changeDir, 'task-ticks.jsonl'), 'utf8')) } catch { /* 侧车缺失/不可读按空(fail-open) */ }
1410
+ let batch = stream.exists ? detectBatchCheckCadence(stream.events) : null
1411
+ if (!batch) batch = detectTickBurst(ticks)
1412
+ return { stream, ticks, batch }
1413
+ } catch { return { stream: { exists: false, events: [] }, ticks: [], batch: null } }
1414
+ })()
1372
1415
  // 证据面=整条提交消息(2026-09-25-thin-done-gate-calibration 坑2:%s 只取标题行,正文里的
1373
1416
  // token 被判零证据,与文案「提交带 task-NN」口径漂移)——%B%x1e 按提交切记录,advisory 的
1374
1417
  // 提交计数不因多行正文失真。
@@ -1436,20 +1479,31 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1436
1479
  if (sent.status === 'complete') {
1437
1480
  _sentinelEvidenceComplete = true
1438
1481
  console.log(`🛡️ 哨兵:全勾 ${sent.checked}/${sent.claimTotal} 证据齐(提交 token/review.json)`)
1439
- // 勾选时点判定(2026-09-26-tick-loop-nudge,R19 行为发现):tasks.md 的 git 首次提交
1440
- // == 收口窗口最后提交(一把勾模式)→ 放行但打行为提醒(不阻断——token 证据已验,
1441
- // 这是提醒边干边勾纪律,非假勾)。tasks.md 全程 untracked(未随交付提交)时同款提醒。
1442
- // 首提锚定(2026-10-07-tick-timing-first-commit):--reverse 全 history 首行=首次提交——
1443
- // 旧实现 -n 1 取最近一次提交,每任务随交付提交 tasks.md 的渐进形态(首提早于 HEAD)
1444
- // 被误报一把勾(完整实测实证:历史 0→1→2 渐进勾选仍出警告)。
1482
+ // 勾选时点判定(2026-10-17-tick-timing-source 换源):判据从「tasks.md git 首次提交
1483
+ // == 收口最后提交」整体退役——git 历史形态被官方允许的 reset --soft 压扁无声摧毁
1484
+ // (tick-timing-squash-blindspot 坑:并行会话压扁归档把边干边勾的渐进历史抹平,警告
1485
+ // 照出,结构性误报;且 tasks.md 不进交付 pathspec 的纪律下「untracked 直至归档」恒
1486
+ // 复现)。换源=task-ticks.jsonl 侧车墙钟时戳(detectTickTiming 纯函数:跨度超 120s
1487
+ // =边干边勾确认;挤 ≤120s 短窗且变更存活 ≥10×跨度=末尾集中勾选)——墙钟时戳是文件
1488
+ // 事实,压扁/rebase 不可改写,与「审计真相锚 change.patch sha256、不依赖 git 形态」
1489
+ // 口径对齐。节奏门已检出单拍跳/突发时让位(防双重警告——其判定覆盖同一批格子);
1490
+ // 无侧车观测输出一行 task tick 教学(不再作结构性警告);advisory 不阻断语义不变。
1445
1491
  try {
1446
- const _firstRaw = gitQuiet(cwd, ['log', '--reverse', '--format=%h', '--', tasksPath])
1447
- const tasksIn = _firstRaw ? (String(_firstRaw).split(/\r?\n/).map((x) => x.trim()).filter(Boolean)[0] || null) : null
1448
- const lastIn = gitQuiet(cwd, ['log', '--format=%h', '-n', '1'])
1449
- if (!tasksIn) {
1450
- console.warn(`⚠️ 勾选时点:tasks.md 未随交付提交(untracked 直至归档)——勾选证据链靠归档兜底,git 历史不可回溯;下次收口前 tasks.md 一并显式 pathspec 提交`)
1451
- } else if (String(tasksIn).trim() === String(lastIn).trim()) {
1452
- console.warn(`⚠️ 勾选时点:tasks.md 的首次 git 提交 == 收口最后提交(一把勾模式)——token 证据齐放行,但边干边勾是进度锚(OS Guardrails 同款纪律),下次完成单元即勾`)
1492
+ if (_tickObs.batch) {
1493
+ /* 节奏门将报单拍多格/突发——时点判定让位 */
1494
+ } else {
1495
+ const _baseRaw = st && st.baseline_commit ? gitQuiet(cwd, ['show', '-s', '--format=%ct', String(st.baseline_commit)]) : null
1496
+ const _baseTs = Number(String(_baseRaw ?? '').trim())
1497
+ const { detectTickTiming } = await import('./sentinel-assertions.js')
1498
+ const t = detectTickTiming({ ticks: _tickObs.ticks, lifespanMs: Number.isFinite(_baseTs) && _baseTs > 0 ? Date.now() - _baseTs * 1000 : NaN })
1499
+ if (t.status === 'concentrated') {
1500
+ console.warn(`⚠️ 勾选时点:${t.count} 格勾选时戳集中在 ${Math.max(1, Math.round(t.spanMs / 1000))}s 短窗内(末尾集中勾选形态)——token 证据齐放行,但边干边勾是进度锚,下次完成单元即勾(task tick 回显含现成提交命令)`)
1501
+ } else if (t.status === 'spread') {
1502
+ console.log(`✅ 勾选时点:${t.count} 格勾选时戳跨度 ${Math.max(1, Math.round(t.spanMs / 60000))} 分钟——边干边勾确认(判据=task-ticks 侧车墙钟时戳,不受 git 压扁影响)`)
1503
+ } else if (t.status === 'no-ticks') {
1504
+ console.log(`ℹ️ 勾选时点:无 tick 时戳观测(未走 task tick)——下次勾选建议 sillyspec task tick --change ${change} --task task-NN(即时回显进度与现成提交命令)`)
1505
+ }
1506
+ /* 'single'/'no-anchor' 静默:单格无节奏可言;缺存活锚 fail-open 不判 */
1453
1507
  }
1454
1508
  } catch { /* 时点判定 fail-soft */ }
1455
1509
  }
@@ -1459,7 +1513,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1459
1513
  // 任务面与实际不符可先改写 tasks.md(工作分解归 agent)再逐格勾。token 代勾(governance-
1460
1514
  // autopilot)已在上方先跑:有证据未勾的格子已代勾,此处拒的纯属零证据未完成面。
1461
1515
  if (sent.claimTotal > 0 && sent.checked < sent.claimTotal) {
1462
- console.error(`🚫 任务未全勾(${sent.checked}/${sent.claimTotal})——tasks.md 是完成状态机:完成一件勾一格(每格勾选需对应提交带 task-NN token 或 review.json),全勾后重跑 flow done`)
1516
+ console.error(`🚫 任务未全勾(${sent.checked}/${sent.claimTotal})——tasks.md 是完成状态机:完成一件勾一格(每格勾选需对应提交带 task-NN token 或 review.json;提交 pathspec 带上本变更 tasks.md——勾选修改与任务交付同一提交入库),全勾后重跑 flow done`)
1463
1517
  console.error(' 任务面与实际实现路径不符时,先改写 tasks.md(工作分解归你,增删改随意)再逐格勾;查下一任务:sillyspec task tick --change ' + `${change}` + ' --task task-NN(回显进度与指针)')
1464
1518
  appendTelemetry({ sentinel: 'incomplete-tasks', checked: sent.checked, total: sent.claimTotal })
1465
1519
  reportMidFail('ledger')
@@ -1477,19 +1531,12 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1477
1531
  // 侧车突发回退(2026-10-14-tick-burst-sidecar-gate):CLI 收口端快连点对事件流不可见(单格
1478
1532
  // 精确跳),空结果时回退 task-ticks.jsonl 毫秒时戳判突发——CLI 与直接 Edit 两条路径同判。
1479
1533
  try {
1480
- const { readWatcherEvents } = await import('./watcher.js')
1481
- const { detectBatchCheckCadence, detectTickBurst, resolveBatchTickAction } = await import('./sentinel-assertions.js')
1482
- const stream = readWatcherEvents({ runtimeRoot, change })
1483
- let batch = stream.exists ? detectBatchCheckCadence(stream.events) : null
1484
- if (!batch) {
1485
- // 侧车突发回退(2026-10-14-tick-burst-sidecar-gate):事件流空结果时读 change 目录
1486
- // task-ticks.jsonl(CLI tick 毫秒时戳事实源,随归档进 git、不受观测盲窗影响)判快连点;
1487
- // 事件流结果优先不双重报告;侧车缺失/坏文件按无证据静默(fail-open)。
1488
- try {
1489
- const { parseTickLog } = await import('./timeline.js')
1490
- batch = detectTickBurst(parseTickLog(readFileSync(join(changeDir, 'task-ticks.jsonl'), 'utf8')))
1491
- } catch { /* 侧车缺失/不可读 → 无侧车证据(fail-open) */ }
1492
- }
1534
+ const { resolveBatchTickAction } = await import('./sentinel-assertions.js')
1535
+ // 观测消费共享采集(2026-10-17-tick-timing-source):事件流/侧车/突发判定已在前文哨兵段
1536
+ // 前置采集为 _tickObs(时点判定与节奏门同源一份数据;读取仅前移,检测逻辑与优先序不变
1537
+ // ——事件流先判、空结果回退侧车突发,侧车缺失按空 fail-open)。
1538
+ const stream = _tickObs.stream
1539
+ const batch = _tickObs.batch
1493
1540
  if (batch) {
1494
1541
  // 代勾计数取「本拍实时代勾 ∨ 持久化代勾」较大者(二轮 P2-B:重入漂移防护);决策纯函数
1495
1542
  // 单源(resolveBatchTickAction)四态与豁免优先序见其 JSDoc——代勾须解释整跳才降级
@@ -2449,7 +2496,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2449
2496
  // ⑥ events:事件收口(watcher 观测旁路 best-effort 读回,非真相源)
2450
2497
  if (st.substeps?.events === 'done') { skip('events') } else {
2451
2498
  try {
2452
- const eventsPath = join(runtimeRoot, `watcher-events-${change}.jsonl`)
2499
+ const eventsPath = runtimeStatePath(runtimeRoot, `watcher-events-${change}.jsonl`)
2453
2500
  if (existsSync(eventsPath)) {
2454
2501
  const n = readFileSync(eventsPath, 'utf8').split('\n').filter(Boolean).length
2455
2502
  console.log(`📊 watcher 事件收口:${n} 条 provisional 事件在案(.runtime 留档,平台展示面)`)
@@ -2483,8 +2530,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2483
2530
  console.log(` 注:knowledge「最近确认」hash 为溯源快照,压扁后成孤儿引用属预期(非活引用)。`)
2484
2531
  }
2485
2532
  } catch { /* 指引 best-effort */ }
2486
- // 平台同步(同 flow start 尾部接线——归档后的 docs/knowledge/FR 面随本轮回推平台)
2487
- try { await triggerSync(cwd, change) } catch { /* 同步绝不阻断协议面 */ }
2533
+ // 平台同步(同 flow start 尾部接线——归档后的 docs/knowledge/FR 面随本轮回推平台)。
2534
+ // 终态强校验(2026-10-17-archive-half-state-location-stuck-active):flow done 到此 =
2535
+ // 变更已归档,文件树推送失败不再静默半态(🚨 告警 + 自救命令 + exit 1)
2536
+ try { await triggerSyncTerminalStrict(cwd, change) } catch { /* 同步绝不阻断协议面 */ }
2488
2537
  return { change, substeps: doneList }
2489
2538
  }
2490
2539
 
@@ -2663,7 +2712,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2663
2712
  console.error(`❌ ${e.message}`)
2664
2713
  // 可照抄实例(2026-10-05-input-teach-copyable):引号内换行合法,行级 trim 容忍缩进
2665
2714
  console.error(' 重试(--input 引号内换行合法,可照抄):')
2666
- console.error(' sillyspec flow start --change <YYYY-MM-DD-简短描述> --input "<一句中文概括(首行即变更标题);随后动机与背景')
2715
+ console.error(' sillyspec flow start --change <YYYY-MM-DD-简短描述> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
2667
2716
  console.error('')
2668
2717
  console.error(' 成功标准:')
2669
2718
  console.error(' - <可验证标准>"')
@@ -2892,9 +2941,9 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2892
2941
  console.log(`✅ flow amend-draft 留痕重锚:${r.reanchored.join('、')}——ledger amendment 审计在案;editRatio=${maxRatio}(段级最大;基准=首版原文,AGENT 槽不计入)`)
2893
2942
  return r
2894
2943
  }
2895
- console.error('用法: sillyspec flow start --change <名> --input "<一句中文概括(首行即变更标题)+动机与背景详述+独立一行『成功标准:』+每行一条『- 可验证标准』>" [--thick|--with-tasks] [--no-prestage(hindsight 标记在场时声明需求已含决策,留痕放行)] | sillyspec flow done --change <名> | sillyspec flow status --change <名> [--json] | sillyspec flow diff --change <名> [--json](只读文件统计)')
2944
+ console.error('用法: sillyspec flow start --change <名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字)+动机与背景详述+独立一行『成功标准:』+每行一条『- 可验证标准』>" [--thick|--with-tasks] [--no-prestage(hindsight 标记在场时声明需求已含决策,留痕放行)] | sillyspec flow done --change <名> | sillyspec flow status --change <名> [--json] | sillyspec flow diff --change <名> [--json](只读文件统计)')
2896
2945
  console.error(' --input 可照抄形态(引号内换行合法):')
2897
- console.error(' sillyspec flow start --change <名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
2946
+ console.error(' sillyspec flow start --change <名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
2898
2947
  console.error('')
2899
2948
  console.error(' 成功标准:')
2900
2949
  console.error(' - <可验证标准>"')