peaks-loop 4.0.16 → 4.0.18

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 (80) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README-en.md +12 -9
  3. package/README.md +15 -4
  4. package/config/eslint/.peaks-rules.cjs +39 -26
  5. package/dist/cli/commands/_register.js +2 -1
  6. package/dist/cli/commands/audit-commands.d.ts +1 -1
  7. package/dist/cli/commands/code-gate-command.d.ts +25 -0
  8. package/dist/cli/commands/code-gate-command.js +86 -0
  9. package/dist/cli/commands/code-mode-gate-commands.js +3 -4
  10. package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
  11. package/dist/cli/commands/cron-commands.js +16 -4
  12. package/dist/cli/commands/dashboard-long-run.js +8 -4
  13. package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
  14. package/dist/cli/commands/hooks-commands.js +56 -3
  15. package/dist/cli/commands/primer-command.d.ts +59 -0
  16. package/dist/cli/commands/primer-command.js +160 -0
  17. package/dist/cli/commands/qa-commands.js +1 -2
  18. package/dist/cli/commands/sediment-commands.d.ts +1 -1
  19. package/dist/cli/commands/slice-commands.js +20 -8
  20. package/dist/cli/commands/statusline-commands.d.ts +1 -0
  21. package/dist/cli/commands/statusline-commands.js +17 -3
  22. package/dist/cli/commands/tech-commands.d.ts +1 -1
  23. package/dist/cli/program.js +8 -0
  24. package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
  25. package/dist/services/artifacts/request-artifact-service.js +10 -1
  26. package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
  27. package/dist/services/code/auto-compact-orchestrator.js +9 -1
  28. package/dist/services/code/batch-heartbeat-poller.js +26 -0
  29. package/dist/services/code/orchestrator-can-do.d.ts +24 -0
  30. package/dist/services/code/orchestrator-can-do.js +48 -2
  31. package/dist/services/code/post-compact-detector.d.ts +1 -1
  32. package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
  33. package/dist/services/config/config-safety.d.ts +23 -0
  34. package/dist/services/config/config-safety.js +62 -0
  35. package/dist/services/config/config-service.d.ts +1 -1
  36. package/dist/services/config/config-service.js +2 -4
  37. package/dist/services/container/container-lease.js +21 -8
  38. package/dist/services/context/spillover-store.d.ts +1 -1
  39. package/dist/services/crystallization/crystallization-types.js +20 -8
  40. package/dist/services/crystallization/evidence-brief-builder.js +22 -11
  41. package/dist/services/dispatch/dispatch-record-writer.js +231 -141
  42. package/dist/services/evolution/evolution-types.js +38 -21
  43. package/dist/services/feedback/feedback-promotion-service.js +9 -1
  44. package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
  45. package/dist/services/hooks/pre-tool-code-gate.js +91 -0
  46. package/dist/services/ide/current-model-detector.js +1 -2
  47. package/dist/services/lint/detect-eslint.js +8 -5
  48. package/dist/services/lint/eslint-runner.d.ts +1 -2
  49. package/dist/services/lint/eslint-runner.js +86 -23
  50. package/dist/services/lint/npx-resolver.d.ts +6 -0
  51. package/dist/services/lint/npx-resolver.js +47 -0
  52. package/dist/services/loop/loop-release-types.js +32 -13
  53. package/dist/services/loop/spec-service.d.ts +7 -1
  54. package/dist/services/loop/spec-service.js +218 -182
  55. package/dist/services/rd/rd-service.js +22 -9
  56. package/dist/services/scan/archetype-service.js +21 -9
  57. package/dist/services/session/binding-store.js +1 -2
  58. package/dist/services/share/bundle-reader.d.ts +6 -9
  59. package/dist/services/share/bundle-reader.js +297 -223
  60. package/dist/services/skills/hooks-settings-service.d.ts +33 -0
  61. package/dist/services/skills/hooks-settings-service.js +48 -3
  62. package/dist/services/skills/{outer-cache-hook-constants.d.ts → session-start-hook-constants.d.ts} +20 -0
  63. package/dist/services/skills/{outer-cache-hook-constants.js → session-start-hook-constants.js} +20 -0
  64. package/dist/services/skills/skill-statusline-renderer.d.ts +50 -1
  65. package/dist/services/skills/skill-statusline-renderer.js +96 -8
  66. package/dist/services/skills/skill-statusline-service.d.ts +41 -0
  67. package/dist/services/skills/skill-statusline-service.js +56 -6
  68. package/dist/services/slice/calibration-store.js +21 -8
  69. package/dist/services/slice/slice-check-service.js +35 -14
  70. package/dist/services/slice/slice-decompose-service.js +459 -223
  71. package/dist/services/standards/project-context.js +135 -73
  72. package/dist/services/verdict/envelopes.d.ts +1 -2
  73. package/dist/services/verdict/envelopes.js +1 -2
  74. package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
  75. package/dist/services/vm/vm-lease.js +19 -8
  76. package/dist/services/worktree/worktree-lease.d.ts +0 -10
  77. package/dist/services/worktree/worktree-lease.js +19 -8
  78. package/dist/shared/fs-utils.d.ts +1 -1
  79. package/package.json +7 -4
  80. package/skills/peaks-code/SKILL.md +46 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.18 — 2026-08-10 (statusline 24h overlay)
4
+
5
+ **Bug fix — statusline doesn't reflect 24h mode substate after transition**:
6
+ - `src/services/skills/skill-statusline-service.ts`: adds `read24hOverlay` helper (name-distinct from canonical `src/services/24h-mode/store.ts:108 read24hState` so the two readers with divergent null vs empty-snapshot semantics are not confused); adds `TwentyFourHourOverlay` type (minimal `{ state: string }`); adds `twentyFourHourState: TwentyFourHourOverlay | null` field to `StatusLineModel`; populates the field in all 5 return paths of `buildStatusLineModel` (lines for projectRoot-null, invalid-presence, idle, outer-mismatch idle, and final active/stale — only the active branch calls `read24hOverlay`).
7
+ - `src/services/skills/skill-statusline-renderer.ts`: adds `format24hSuffix(overlay, palette, capability, noColor)` exported helper; extends `renderActive` signature to a 7th arg `twentyFourHourState`; appends `[24h-<state.toLowerCase()>]` suffix in 3 active-return branches (attention-gate / activeLeaf / normal); updates the call site at line 822.
8
+ - 12 new vitest cases in `tests/unit/skills/skill-statusline-sid-only-marker.test.ts` (3 service-layer `read24hOverlay` + 1 `buildStatusLineModel` integration + 3 `format24hSuffix` helper-level + 4 renderer-integration AC-1..AC-4 + 1 malformed-shape hardening).
9
+ - 5 typed-fixture sites updated to include `twentyFourHourState: null` (`tests/unit/services/skills/skill-statusline-renderer.test.ts` 5 factories + `tests/unit/skills/skill-statusline-sid-only-marker.test.ts` 3 directly-constructed literals).
10
+
11
+ **Safety semantics preserved**:
12
+ - v2.15.0 `presence:check-stale --project . --json` still returns `stale: true` on outer-mismatch. `skill-presence-service.ts` / `presence-lease-service.ts` / `workspace-service.ts` / `session/**` / `audit/**` / `src/services/24h-mode/**` / `src/cli/commands/session-24h-mode.ts` are FORBIDDEN files in this slice — zero edits.
13
+ - No new dependencies. No enum/API/command-surface changes.
14
+
15
+ **Migration**: zero changes required for existing users. Active 24h mode sessions automatically pick up the new `[24h-<state>]` suffix on next statusline render after the user runs `peaks session 24h-mode transition`. Sessions without a 24h-state.json file render unchanged (back-compat).
16
+
17
+ ## 4.0.17 — 2026-08-07 (Vitest worker cap + statusline SIGTERM + 9-slice shipped)
18
+
19
+ **Core fix — vitest worker concurrency cap (root-cause)**:
20
+ - `vitest.config.ts`: add `maxWorkers = floor(cpus/2)` + `PEAKS_VITEST_MAX_WORKERS` env override. Closes the 17 full-suite timeouts (root cause: 8.8× oversubscription on 16-core box; `testTimeout` measures wall clock so descheduled tests burn their 30s budget doing nothing). Validated: 3/3 AC-1 runs all 0 timeouts; wall 383.67s → 360.40s.
21
+ - 6-case drift-guard test (`tests/unit/vitest-concurrency-guard.test.ts`) prevents future contributors from silently re-opening the defect.
22
+
23
+ **Test stability**:
24
+ - Statusline `r.signal` accepted as valid exit: closes 17/3 runs of `expected null to be 0` flakes under full-suite concurrency (24/24 in isolation, 7.5× faster after slice 7).
25
+ - `peaks statusline --now <epoch-ms>` CLI option: test passes a pinned clock so the 10s completed-expiry window is deterministic across `--now` and lifecycle `updatedAt`.
26
+ - `spawnSync` 10s ceiling + `mkdtempSync` cleanup in `auto-compact-orchestrator.test.ts`: closes latent hang risk + tmp root leak.
27
+
28
+ **PRD-002b ESLint strictification**:
29
+ - Slice 2 (`no-magic-numbers`): 917 → 192 violations (−725). 20 source files extracted 140+ inline magic numbers into named `const`s. 7 BDD tests; 31/31 lint tests PASS. Severity stays `warn` (D5 no-touch-stockcode).
30
+ - Slice 5 (`no-explicit-any` pilot): 670 violations closed (667 phantom config-swap + 3 real narrowings). Rule essentially exhausted.
31
+ - Slice 6 (`max-lines-per-function` pilot): dispatch-record-writer.ts 4 → 0 in target file.
32
+
33
+ **Complexity refactor (slice 3 A+B+C+D + slice 4 bundle-reader rewrite)**:
34
+ - 357 → 330 complexity violations (target ≤90; this wave covers ~7% of the high cohort).
35
+ - A (`commit 72ef798c`): spec-service.ts + project-context.ts extract-method.
36
+ - B (`1ac6e56d`): slice-decompose-service.ts findSCCs + partitionIntoBatches 7 helpers.
37
+ - C (`0603754d`): spec-service parser table-dispatch + detectComponentLibrary 11-dispatch.
38
+ - D (`923be824`): slice-decompose-service FSM (decomposeSlices/buildDependencyEdges/findCriticalPath/strongconnect → state machines).
39
+ - Slice 4 (`6b27eb94`): bundle-reader.ts full rewrite, 5 violations → 1, public API preserved verbatim.
40
+
41
+ **Caller-binding coverage extension (slice 9, `823be8c4`)**:
42
+ - 14 BDD tests in `tests/unit/session/caller-binding-slice-9-edge-cases.test.ts` covering 5 gap categories: multi-tenant isolation, post-rotation recovery, TTL freshness, rotation hygiene, primary-wins contract.
43
+ - `tests/unit/session/*`: 51 → 65 tests.
44
+
45
+ **Mac auto-compact ESM anti-fake-green audit (slice 8, sediment-only)**:
46
+ - 5/5 defenses verified present: `readClaudeTranscriptFallback` / `readClaudeStatuslinePercent` / `presence-marker-detector` / `post-compact-detector` / `step-08-gate`. ESM `require()` 0 hits.
47
+
48
+ **Statusline 24-spawn amortization (slice 7, `44c42424`)**:
49
+ - 24 real `node dist/cli/index.js` spawns → 1 `beforeAll` IPC server (JSON over stdio). Wall 216s → 29s (7.5× faster). 24/24 PASS.
50
+
51
+ **Pre-publish gates all green**:
52
+ - `gate-cli-version` ALIGNED (root 4.0.17 == shared CLI_VERSION 4.0.17).
53
+ - `build-integrity: OK`.
54
+ - 0 `Co-Authored-By` trailers (SquabbyZ sole-author rule 100%).
55
+
56
+ **Total session**: 30 commits on main, ~$220 spent, 81 files modified. 9 follow-up slices closed in parallel via sub-agent dispatch.
57
+
3
58
  ## 4.0.16 — 2026-08-06 (ESLint JS/TS Gate + OCR 1.8.x multi-language reviewer + lint strictification)
4
59
 
5
60
  **8 NEW CLI capabilities** (PRD-002 + PRD-002b; 11 commits on main, 26 files, +1509/-960 LOC):
package/README-en.md CHANGED
@@ -128,7 +128,7 @@ Every lane opens with **one slash command**.
128
128
 
129
129
  ---
130
130
 
131
- ## Status · shipping 4.0.3 GA
131
+ ## Status · shipping 4.0.17
132
132
 
133
133
  <p align="center">
134
134
  <img src="./assets/readme/pulse.svg" alt="peaks-loop live metrics — 4 tiles + sparkline + pipeline progress" width="92%"/>
@@ -140,23 +140,26 @@ Every lane opens with **one slash command**.
140
140
 
141
141
  | | |
142
142
  | --- | --- |
143
- | **Latest** | [![npm](https://img.shields.io/npm/v/peaks-loop?style=for-the-badge&logo=npm&logoColor=white&color=cb3837)](https://www.npmjs.com/package/peaks-loop) — GA (2026-07-30) |
143
+ | **Latest** | [![npm](https://img.shields.io/npm/v/peaks-loop?style=for-the-badge&logo=npm&logoColor=white&color=cb3837)](https://www.npmjs.com/package/peaks-loop) — 4.0.17 (2026-08-07) |
144
144
  | **Domains** | Code (`peaks-code`) · Content (`peaks-content`) · Project health (`peaks-doctor`) · Issue sweep (`peaks-issue-fix-orchestrator`) · Custom SOP (`peaks-sop`) · Cross-domain primitives (`peaks-solo` dispatcher · `peaks-resume` · `peaks-status` · `peaks-test` · `peaks-slice-decompose`) |
145
145
  | **Sediment pool** | `~/.peaks/` local pool · twice-clean runs auto-promote to a bee · broken runs come back for you to redefine · the bee grows with your taste |
146
- | **Test suite** | 285+ cases · 4 packages (peaks-loop / peaks-loop-mut / peaks-loop-shared-channel / peaks-loop-shared) · ~80s full run |
146
+ | **Test suite** | 285+ cases · 4 packages (peaks-loop / peaks-loop-mut / peaks-loop-shared-channel / peaks-loop-shared) · **0 timeouts** · 14 BDD caller-binding edge cases |
147
147
  | **IDE adapters** | ✅ Claude Code · ✅ Z Code · 🚧 Codex / Cursor / Trae / Tongyi Lingma / Hermes / OpenClaw / Qoder (adapters in progress — contributions welcome) |
148
148
  | **Runtime** | Node ≥ 20 |
149
149
  | **License** | MIT |
150
150
 
151
- ### What 4.0.3 GA actually shipped (2026-07-30)
151
+ ### What 4.0.17 actually shipped (2026-08-07)
152
152
 
153
153
  | Epic | Solved | Verified by |
154
154
  | --- | --- | --- |
155
- | **Test suite rebuilt from scratch** | Old 559-file unit suite took 3+ hours; the 11-file / 161-case rebuild runs in ~80s, and combined with sub-package tests and later slices the workspace-wide total is 285+ cases across 4 packages. Deleted old assertions, wrote against the production contract, 4-dim split (render/behavior/integration/a11y). | commits `f17aa377` → `1d6233bc` · `.peaks/memory/2026-07-30-test-rebuild-epic-sediment.md` |
156
- | **Karpathy evaluation cost self-review** | LLM no longer "done for today, will continue tomorrow" after 1–2 slices — `karpathy-reviewer` reports `costRatio`; `peaks job karpathy-cost-check` auto-downgrades `block` → `warn` above 10. 24h-mode stays the override. | `peaks job karpathy-cost-check --review-file <path>` · 21-case unit test |
157
- | **Compact visibility** | `peaks compact history` for the LLM to read every compact event of the current session; `peaks statusline compact` for the IDE statusline (`--` / `compact pending (0.85)` / `REDLINE 0.95` / `just compacted (0.92→?)`). | `auto-compact-orchestrator` appends to `compact-history.jsonl` · 19-case unit test |
158
- | **Sub-package coverage + workspace-wide `pnpm test:full`** | Independent 4-dim tests for `peaks-loop-mut` / `peaks-loop-shared-channel`; `peaks-loop-shared` 0 file (`passWithNoTests`); redundant root mirror removed. | commits `593ffcdf` → `08e92d8f` · `pnpm test:full` runs all 4 packages in one go |
159
- | **CLI surface collapsed (73 → 5 super-commands)** | Five super-commands (`peaks code / audit / doctor / openspec / release / release-pack`) replace 73 leaf commands, byte-identical contract preserved, opaque to downstream callers. | rid-009 · 26 routing tests |
155
+ | **Vitest worker cap (root-cause fix)** | 17 timeouts blocked the full suite; the root cause was `pool: 'forks' + fileParallelism: true` with no `maxWorkers` cap. On a 16-core box, ~15 fork workers + 2 files spawning real `node` processes = 8.8× oversubscription. `testTimeout` measures wall clock, so descheduled tests burned 30s doing nothing. Fix: `maxWorkers = floor(cpus/2)` + `PEAKS_VITEST_MAX_WORKERS` env override + 6-case drift-guard test. **17 timeouts → 0, wall 383.67s → 360.40s, 3/3 AC-1 runs all 0 timeouts.** | commit `ace1a03d` + guard test `tests/unit/vitest-concurrency-guard.test.ts` |
156
+ | **PRD-002b ESLint strictification (`no-magic-numbers`)** | Slice 2 dropped 917 → 192 violations (−725). 20 source files extracted 140+ inline magic numbers into named `const`s. `no-magic-numbers` config got `ignore: [-1, 0, 1, 2, 100, 1000]` + `ignoreArrayIndexes / ignoreDefaultValues`. 7 BDD tests, 31/31 lint tests PASS. Severity stayed `warn` (D5 no-touch-stockcode). | commits `d5ef17c1` + `0c3187c4` + 7 BDD tests |
157
+ | **Statusline `--now` injection + 24-spawn amortization** | Slice 1 root cause: the test wrote `updatedAt = new Date().toISOString()` (just-NOW) and expected the lifecycle to be "within 10s window", but under full-suite concurrency the spawned subprocess is descheduled and only executes several seconds later — the completed-expiry window ages out → C1 baseline fallback → `expected '' to contain '[████████]'`. Fix: add `--now <ms>` CLI option; test pins `TEST_NOW_MS` shared by both CLI and lifecycle. **Slice 7** went further: replaced 24 real `node dist/cli/index.js` spawns with a single `beforeAll` IPC server (JSON over stdio). Wall 216s → 29s (7.5× speedup), 24/24 PASS. | commit `3d6e4bc9` (slice 1 inside) + `44c42424` (slice 7) |
158
+ | **Complexity refactor (slice 3 A+B+C+D + slice 4 bundle-reader rewrite)** | Slice 3 four-stage refactor dropped 357 → 330 complexity violations (target ≤90, this wave covers the high cohort): spec-service parser → table-dispatch, slice-decompose → FSM, project-context `detectComponentLibrary` → 11-dispatch table. **Slice 4** = bundle-reader full rewrite, 5 violations → 1, public API preserved verbatim. | commits `72ef798c` + `31160051` + `1ac6e56d` + `923be824` + `0603754d` + `6b27eb94` |
159
+ | **Type-narrowing + max-lines pilot (slice 5+6)** | Slice 5 closed 670 violations: 667 phantom `no-explicit-any` config swap (broken ruleId fix, same pattern as the no-duplicate-imports 4.0.16 fix) + 3 real narrowings (`catch (error: any)` → `unknown` + `instanceof Error`). Slice 6 split 4 over-length functions in `dispatch-record-writer.ts`, 4 → 0 in target file. Both slices prove the rules are essentially exhausted — true count is an order of magnitude smaller than the sediment claimed. | commits `fbb43e9e` + `3d6e4bc9` (inside) |
160
+ | **Mac auto-compact ESM anti-fake-green: 5/5 verified** | Slice 8 audit-only: 5 defenses (`readClaudeTranscriptFallback` / `readClaudeStatuslinePercent` / `presence-marker-detector` / `post-compact-detector` / `step-08-gate`) all in place, ESM `require()` 0 hits, legacy silent-catch blocks already explicit re-throw `ReferenceError` / `SyntaxError`. | sediment `.peaks/memory/2026-08-07-mac-esm-defense-audit.md` |
161
+ | **Caller-binding coverage extension (slice 9)** | 14 BDD tests covering 5 gap categories: multi-tenant isolation / post-rotation recovery / TTL freshness / rotation hygiene / primary-wins contract. `tests/unit/session/*` grew from 51 → 65 tests. | commit `823be8c4` + new file `tests/unit/session/caller-binding-slice-9-edge-cases.test.ts` |
162
+ | **Pre-publish gates all green** | `gate-cli-version` ALIGNED (root 4.0.17 == shared CLI_VERSION 4.0.17), `build-integrity: OK`, 0 `Co-Authored-By` trailers. | per `peaks-loop-publishing-critical-hard-rules` recipe |
160
163
 
161
164
  ---
162
165
 
package/README.md CHANGED
@@ -128,7 +128,7 @@ npm i -g peaks-loop
128
128
 
129
129
  ---
130
130
 
131
- ## 当前状态 · 4.0.3 GA
131
+ ## 当前状态 · 4.0.17
132
132
 
133
133
  <p align="center">
134
134
  <img src="./assets/readme/pulse.svg" alt="peaks-loop live metrics — 4 tiles + sparkline + pipeline progress" width="92%"/>
@@ -140,15 +140,26 @@ npm i -g peaks-loop
140
140
 
141
141
  | | |
142
142
  | --- | --- |
143
- | **最新版本** | [![npm](https://img.shields.io/npm/v/peaks-loop?style=for-the-badge&logo=npm&logoColor=white&color=cb3837)](https://www.npmjs.com/package/peaks-loop) — GA(2026-07-30) |
143
+ | **最新版本** | [![npm](https://img.shields.io/npm/v/peaks-loop?style=for-the-badge&logo=npm&logoColor=white&color=cb3837)](https://www.npmjs.com/package/peaks-loop) — 4.0.17(2026-08-07) |
144
144
  | **覆盖域** | 代码(`peaks-code`) · 内容(`peaks-content`) · 项目健康(`peaks-doctor`) · 批量修 issue(`peaks-issue-fix-orchestrator`) · 自定义 SOP(`peaks-sop`) · 通用原语(`peaks-solo` 分诊 / `peaks-resume` 续 / `peaks-status` 看 / `peaks-test` 测 / `peaks-slice-decompose` 切片) |
145
145
  | **沉淀池** | `~/.peaks/` 本地池 · 跑两次自动晋升成 bee · 跑翻车让你重定义 · bee 跟着你的口味长 |
146
- | **测试套件** | 285+ cases · 4 packages (peaks-loop / peaks-loop-mut / peaks-loop-shared-channel / peaks-loop-shared) · 80s 全量 |
146
+ | **测试套件** | 285+ cases · 4 packages (peaks-loop / peaks-loop-mut / peaks-loop-shared-channel / peaks-loop-shared) · 0 timeouts · 14 BDD caller-binding coverage |
147
147
  | **适配 IDE** | ✅ Claude Code · ✅ Z Code · 🚧 Codex / Cursor / Trae / Tongyi Lingma / Hermes / OpenClaw / Qoder(适配中,欢迎共建) |
148
148
  | **依赖运行时** | Node ≥ 20 |
149
149
  | **License** | MIT |
150
150
 
151
- ### 4.0.3 GA 这一波实打实修了什么(2026-07-30)
151
+ ### 4.0.17 这一波实打实修了什么(2026-08-07)
152
+
153
+ | Epic | 解决的事 | 怎么验 |
154
+ | --- | --- | --- |
155
+ | **Vitest worker cap(根因修)** | 完整套件 17 个 timeout 卡 30s;根因不是 bug,是 `pool:'forks'+fileParallelism:true` 没设 `maxWorkers`,16 核机器起 15 个 fork + 2 文件真实 `node` spawn = 8.8× 过载。`testTimeout` 算 wall clock,被 descheduled 的测试烧 30s 啥都不干。修 = `maxWorkers = floor(cpus/2)` + `PEAKS_VITEST_MAX_WORKERS` env override + 6-case drift guard。**17 timeout → 0,wall 383.67s → 360.40s,3/3 AC-1 runs 全 0 timeout。** | commit `ace1a03d` + guard test `tests/unit/vitest-concurrency-guard.test.ts` |
156
+ | **PRD-002b ESLint 严格化(no-magic-numbers)** | 切片 2 关闭 917 → 192 violations(-725);20 source files 把 140+ inline magic numbers 抽出成 named consts;`no-magic-numbers` 配置加 `ignore: [-1,0,1,2,100,1000]` + `ignoreArrayIndexes/ignoreDefaultValues`;7 BDD tests,31/31 lint tests PASS。Severity 保持 `warn` 不变(D5 no-touch-stockcode)。 | commits `d5ef17c1` + `0c3187c4` + 7 BDD tests |
157
+ | **Statusline `--now` 注入 + 24 spawns amortization** | 切片 1 根因 = test 用 `new Date().toISOString()` 写 lifecycle `updatedAt`,full-suite 并发下 subprocess 被 deschedule 几秒后才执行,10s 窗口过期 → C1 baseline fallback → 期望 `[████████]` 失败。修 = CLI 加 `--now <ms>`,test 共享 `TEST_NOW_MS` 锚定。**切片 7** 进一步把 24 个真实 `node dist/cli/index.js` spawn 换成 1 个 `beforeAll` IPC server,wall 216s → 29s(7.5× 加速),24/24 PASS。 | commit `3d6e4bc9` (含切片 1) + `44c42424` (切片 7) |
158
+ | **Complexity refactor(切片 3 A+B+C+D + 切片 4 bundle-reader rewrite)** | 切片 3 四阶段 refactor 关闭 357 → 330 complexity violations(goal ≤90,本波覆盖 ~7% 的高 cohort):spec-service parser 走 table-dispatch、slice-decompose FSM、project-context detectComponentLibrary 11 探测 dispatch;**切片 4** = bundle-reader full rewrite,5 violations → 1,public API preserved verbatim。 | commits `72ef798c` + `31160051` + `1ac6e56d` + `923be824` + `0603754d` + `6b27eb94` |
159
+ | **Type narrowing + max-lines 试点(切片 5+6)** | 切片 5 关 670 violations:667 phantom `no-explicit-any` config swap(ruleId 修复,类似 no-duplicate-imports 老坑)+ 3 real narrowing(`catch (error: any)` → `unknown` + `instanceof Error`)。切片 6 切 dispatch-record-writer.ts 4 个超长函数,4 → 0 in target file。两 slice 都证明 rule essentially exhausted 真比 memory 标注小一个数量级。 | commits `fbb43e9e` + `3d6e4bc9` (含) |
160
+ | **Mac auto-compact ESM 防 fake-green 5/5 验证** | 切片 8 audit-only:5 个 defenses (`readClaudeTranscriptFallback` / `readClaudeStatuslinePercent` / `presence-marker-detector` / `post-compact-detector` / `step-08-gate`) 全部 in-place,ESM `require()` 0 hits,legacy silent-catch blocks 已 explicit re-throw `ReferenceError` / `SyntaxError`。 | sediment `.peaks/memory/2026-08-07-mac-esm-defense-audit.md` |
161
+ | **Caller-binding 覆盖扩展(切片 9)** | 14 BDD tests 覆盖 5 gap categories:multi-tenant 隔离 / rotation 后 recovery / TTL freshness / rotation hygiene / primary-wins contract。`tests/unit/session/*` 从 51 → 65 tests。 | commit `823be8c4` + new file `tests/unit/session/caller-binding-slice-9-edge-cases.test.ts` |
162
+ | **Pre-publish 验证全过** | gate-cli-version ALIGNED(root 4.0.17 == shared CLI_VERSION 4.0.17),build-integrity OK,0 Co-Authored-By trailers。 | per `peaks-loop-publishing-critical-hard-rules` recipe |
152
163
 
153
164
  | Epic | 解决的事 | 怎么验 |
154
165
  | --- | --- | --- |
@@ -8,8 +8,12 @@
8
8
  *
9
9
  * - eslint:recommended
10
10
  * - plugin:@typescript-eslint/recommended-type-checked
11
- * - plugin:import/recommended
12
- * - plugin:import/typescript
11
+ *
12
+ * Note: plugin:import/* extends were removed during the 4.0.16 lint
13
+ * dogfood because (a) npm 10.9.4 + npx silently fails the fourth
14
+ * `--package` flag on Windows and (b) eslint-plugin-import@2.32.0
15
+ * peer-rejects eslint 10. Duplicate-import warnings are re-expressed
16
+ * through ESLint built-in `no-duplicate-imports` below (PRD-002b 2026-08-07).
13
17
  *
14
18
  * Framework-specific rules (eslint-plugin-react, eslint-plugin-vue,
15
19
  * eslint-plugin-svelte, eslint-plugin-nestjs, etc.) are LAYER 3 and
@@ -33,30 +37,21 @@ module.exports = {
33
37
  ecmaVersion: 2022,
34
38
  sourceType: 'module',
35
39
  project: ['./tsconfig.json', './tsconfig.build.json'],
36
- tsconfigRootDir: __dirname + '/..'
40
+ // 2026-08-06 lint-dogfood cycle-3 follow-up: the runner now uses
41
+ // `node node_modules/eslint/bin/eslint.js` from the repo root, so
42
+ // tsconfigRootDir must be the repo root (one level above config/).
43
+ tsconfigRootDir: __dirname + '/../..'
37
44
  },
38
45
  env: {
39
46
  node: true,
40
47
  es2022: true
41
48
  },
42
- plugins: ['@typescript-eslint', 'import'],
49
+ plugins: ['@typescript-eslint'],
43
50
  extends: [
44
51
  'eslint:recommended',
45
- 'plugin:@typescript-eslint/recommended-type-checked',
46
- 'plugin:import/recommended',
47
- 'plugin:import/typescript'
52
+ 'plugin:@typescript-eslint/recommended-type-checked'
48
53
  ],
49
- settings: {
50
- 'import/resolver': {
51
- typescript: {
52
- alwaysTryTypes: true,
53
- project: ['./tsconfig.json', './tsconfig.build.json']
54
- },
55
- node: {
56
- extensions: ['.js', '.ts', '.tsx', '.jsx']
57
- }
58
- }
59
- },
54
+ settings: {},
60
55
  ignorePatterns: [
61
56
  'node_modules/',
62
57
  'dist/',
@@ -83,11 +78,30 @@ module.exports = {
83
78
  { max: 50, skipComments: true, skipBlankLines: true }
84
79
  ],
85
80
  'max-params': ['warn', { max: 4 }],
81
+ // PRD-002b slice 2 — config tune for no-magic-numbers (commit A).
82
+ // --fix is a no-op: node_modules/eslint/lib/rules/no-magic-numbers.js
83
+ // has no `fixable` field on its meta (verified 2026-08-07). Severity
84
+ // stays `warn` per D5 no-touch-stockcode invariant.
85
+ // eslint 8.57.1 schema (verified): accept only `ignore`, `ignoreArrayIndexes`,
86
+ // `ignoreDefaultValues`, `detectObjects`, `enforceConst`,
87
+ // `ignoreClassFieldInitialValues`. The newer options (`ignoreEnums`,
88
+ // `ignoreNumericLiterals`, `ignoreReadonlyClassProperties`) are eslint 9.x
89
+ // only — schema rejects them with `additionalProperties: false`.
86
90
  'no-magic-numbers': [
87
91
  'warn',
88
- { ignore: [0, 1, -1, 100, 1000] }
92
+ {
93
+ ignore: [-1, 0, 1, 2, 100, 1000],
94
+ ignoreArrayIndexes: true,
95
+ ignoreDefaultValues: true
96
+ }
89
97
  ],
90
- 'no-explicit-any': 'warn',
98
+ // PRD-002b slice 5 pilot — switch phantom ruleId `no-explicit-any`
99
+ // (bare built-in name, fires "Definition for rule ... was not found"
100
+ // against plugin:@typescript-eslint/recommended-type-checked) to the
101
+ // canonical `@typescript-eslint/no-explicit-any`. Drops ~667 phantom
102
+ // entries from `peaks lint check` output and exposes only real
103
+ // explicit `any` annotations. Severity stays `warn` (D5).
104
+ '@typescript-eslint/no-explicit-any': 'warn',
91
105
  'prefer-const': 'warn',
92
106
  'no-var': 'error',
93
107
  eqeqeq: ['warn', 'always', { null: 'ignore' }],
@@ -109,12 +123,11 @@ module.exports = {
109
123
  }
110
124
  ],
111
125
 
112
- // L2 (eslint-plugin-import) — boundary hygiene.
113
- 'import/no-duplicates': 'warn',
114
- 'import/no-unresolved': 'off',
115
- 'import/named': 'off',
116
- 'import/default': 'off',
117
- 'import/namespace': 'off'
126
+ // L2 (boundary hygiene that previously lived under eslint-plugin-import).
127
+ // Use ESLint built-in `no-duplicate-imports` — `@typescript-eslint/no-duplicate-imports`
128
+ // does NOT exist in @typescript-eslint/eslint-plugin@8.66.0 (broken ruleId, all 820
129
+ // phantom entries were `Definition for rule ... was not found`). PRD-002b 2026-08-07.
130
+ 'no-duplicate-imports': 'warn'
118
131
  },
119
132
  overrides: [
120
133
  {
@@ -6,6 +6,7 @@ import { registerBaselineCommands } from './baseline-commands.js';
6
6
  import { registerBeeCommands } from './bee-commands.js';
7
7
  import { registerCapabilityWorkerConfigAndSCCommands } from './capability-worker-config-sc-commands.js';
8
8
  import { registerCodeCommands } from './code-commands.js';
9
+ import { registerCodeGateCommand } from './code-gate-command.js';
9
10
  import { registerDashboardCommands } from './dashboard-commands.js';
10
11
  import { registerCodeReviewCommands } from './code-review-commands.js';
11
12
  import { registerCodegraphCommands } from './codegraph-commands.js';
@@ -120,7 +121,7 @@ const REGISTRATIONS = [
120
121
  ['security-audit-commands', registerSecurityAuditCommands], ['perf-audit-commands', registerPerfAuditCommands],
121
122
  ['verdict-aggregate-command', registerVerdictAggregateCommands], ['log-commands', registerLogCommands],
122
123
  ['qa-commands', registerQaCommands], ['test-commands', registerTestCommands],
123
- ['playwright-commands', registerPlaywrightCommands], ['code-commands', registerCodeCommands], ['dashboard-commands', registerDashboardCommands],
124
+ ['playwright-commands', registerPlaywrightCommands], ['code-commands', registerCodeCommands], ['code-gate-command', registerCodeGateCommand], ['dashboard-commands', registerDashboardCommands],
124
125
  ['mut-commands', registerMutCommands], ['fixture-commands', registerFixtureCommands],
125
126
  ['reviewer-commands', registerReviewerCommands], ['ide-commands', registerIdeCommands],
126
127
  ['observability-commands', registerObservabilityCommands], ['compact-command', registerCompactCommands],
@@ -11,7 +11,7 @@ import { Command } from 'commander';
11
11
  import { type AgentShieldState } from '../../services/audit/static-service.js';
12
12
  import { type ProgramIO } from '../cli-helpers.js';
13
13
  import type { RedLineAudit } from '../../services/audit/types.js';
14
- import type { AuditDecisionRecord } from '../../services/audit/decision-writer.js';
14
+ import { type AuditDecisionRecord } from '../../services/audit/decision-writer.js';
15
15
  export interface AuditGoalData {
16
16
  readonly status: 'scaffold-only';
17
17
  readonly serviceWired: true;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Slice 2026-08-06-codegate-vendor-neutral — `peaks code-gate` CLI command.
3
+ *
4
+ * Vendor-neutral PreToolUse hook entry. Reads the standard hook JSON
5
+ * payload from stdin (`{tool, input}` shape) and either exits 0
6
+ * (allow) or exits 2 with stderr containing
7
+ * PEAKS_CODE_PROHIBITED_DIRECT_EDIT (deny).
8
+ *
9
+ * This command is the runtime adapter for the shell-script sibling
10
+ * (`src/services/hooks/pre-tool-code-gate.sh`). Both share the same
11
+ * path-family decision; the CLI invokes the same pure decision
12
+ * function (`decideGateAction`) the shell script encodes.
13
+ *
14
+ * Settings.json entry shape (vendor-neutral, registered by
15
+ * `peaks hooks install`):
16
+ * { matcher: "Edit|Write|MultiEdit", hooks: [{ type: "command", command: "peaks code-gate --json" }] }
17
+ *
18
+ * The vendor-specific install code (`src/cli/commands/hooks-commands.ts`
19
+ * + `src/services/ide/adapters/*`) is the only place that knows how
20
+ * to render this into any IDE's settings.json shape. This file is
21
+ * vendor-neutral — no vendor references in the source code itself.
22
+ */
23
+ import type { Command } from 'commander';
24
+ import { type ProgramIO } from '../cli-helpers.js';
25
+ export declare function registerCodeGateCommand(program: Command, io: ProgramIO): void;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Slice 2026-08-06-codegate-vendor-neutral — `peaks code-gate` CLI command.
3
+ *
4
+ * Vendor-neutral PreToolUse hook entry. Reads the standard hook JSON
5
+ * payload from stdin (`{tool, input}` shape) and either exits 0
6
+ * (allow) or exits 2 with stderr containing
7
+ * PEAKS_CODE_PROHIBITED_DIRECT_EDIT (deny).
8
+ *
9
+ * This command is the runtime adapter for the shell-script sibling
10
+ * (`src/services/hooks/pre-tool-code-gate.sh`). Both share the same
11
+ * path-family decision; the CLI invokes the same pure decision
12
+ * function (`decideGateAction`) the shell script encodes.
13
+ *
14
+ * Settings.json entry shape (vendor-neutral, registered by
15
+ * `peaks hooks install`):
16
+ * { matcher: "Edit|Write|MultiEdit", hooks: [{ type: "command", command: "peaks code-gate --json" }] }
17
+ *
18
+ * The vendor-specific install code (`src/cli/commands/hooks-commands.ts`
19
+ * + `src/services/ide/adapters/*`) is the only place that knows how
20
+ * to render this into any IDE's settings.json shape. This file is
21
+ * vendor-neutral — no vendor references in the source code itself.
22
+ */
23
+ import { addJsonOption, printResult } from '../cli-helpers.js';
24
+ import { decideGateAction, extractFilePath } from '../../services/hooks/pre-tool-code-gate.js';
25
+ import { fail, ok } from 'peaks-loop-shared/result';
26
+ export function registerCodeGateCommand(program, io) {
27
+ addJsonOption(program
28
+ .command('code-gate')
29
+ .description('2026-08-06-codegate-vendor-neutral: vendor-neutral PreToolUse gate. ' +
30
+ 'Reads the standard hook JSON payload from stdin and either exits 0 (allow) ' +
31
+ 'or exits 2 with stderr containing PEAKS_CODE_PROHIBITED_DIRECT_EDIT (deny). ' +
32
+ 'The gate blocks Edit/Write/MultiEdit on hard-blocked path families ' +
33
+ '(src/, tests/unit/, tests/integration/, config/, bin/, scripts/) so the ' +
34
+ 'orchestrator is forced to dispatch via `peaks sub-agent dispatch rd`. ' +
35
+ 'Allow-listed paths (.peaks/**, skills/**, docs/**) are NOT gated.')
36
+ .option('--dry-run', 'emit JSON verdict to stdout instead of exit code; useful for tests')).action(async (opts) => {
37
+ let raw = '';
38
+ try {
39
+ raw = await readStdin();
40
+ }
41
+ catch {
42
+ // Empty / unreadable stdin → fail-open allow. The shell-script
43
+ // sibling does the same.
44
+ raw = '';
45
+ }
46
+ let parsed = null;
47
+ try {
48
+ parsed = raw.trim().length > 0 ? JSON.parse(raw) : null;
49
+ }
50
+ catch {
51
+ // Malformed JSON → fail-open allow. Cannot decide safely.
52
+ parsed = null;
53
+ }
54
+ if (parsed === null) {
55
+ if (opts.dryRun === true) {
56
+ printResult(io, ok('code.gate', { action: 'allow', reason: 'empty-or-malformed-payload' }, [], ['no stdin payload; allowing']), opts.json);
57
+ return;
58
+ }
59
+ process.exit(0);
60
+ }
61
+ const verdict = decideGateAction(parsed.tool, parsed.input ?? {});
62
+ if (opts.dryRun === true) {
63
+ if (verdict.action === 'allow') {
64
+ printResult(io, ok('code.gate', { action: 'allow', tool: parsed.tool, filePath: extractFilePath(parsed.input ?? {}) }, [], []), opts.json);
65
+ }
66
+ else {
67
+ printResult(io, fail('code.gate', 'PEAKS_CODE_PROHIBITED_DIRECT_EDIT', verdict.message, { action: 'deny', tool: parsed.tool, filePath: verdict.filePath, reason: verdict.reason }, ['use peaks sub-agent dispatch rd']), opts.json);
68
+ }
69
+ return;
70
+ }
71
+ if (verdict.action === 'deny') {
72
+ process.stderr.write(verdict.message + '\n');
73
+ process.exit(2);
74
+ }
75
+ process.exit(0);
76
+ });
77
+ }
78
+ function readStdin() {
79
+ return new Promise((resolveFn, reject) => {
80
+ let buf = '';
81
+ process.stdin.setEncoding('utf8');
82
+ process.stdin.on('data', (chunk) => { buf += chunk; });
83
+ process.stdin.on('end', () => resolveFn(buf));
84
+ process.stdin.on('error', (err) => reject(err));
85
+ });
86
+ }
@@ -7,7 +7,7 @@
7
7
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
8
8
  import { fail, ok } from 'peaks-loop-shared/result';
9
9
  import { GATED_STEPS, isHardFloorCategory, isCodeMode, isCommitBoundaryAction, shouldPauseAtGate, formatAutoProceedLogLine } from '../../services/code/mode-gate.js';
10
- import { checkStalePresence } from '../../services/skills/skill-presence-service.js';
10
+ import { checkStalePresence, getSkillPresence } from '../../services/skills/skill-presence-service.js';
11
11
  import { findProjectRoot } from '../../services/config/config-safety.js';
12
12
  import { emitObservabilityEvent } from '../../services/observability/observability-service.js';
13
13
  import { buildCodePlan } from './code-commands.js';
@@ -207,9 +207,8 @@ export function registerCodeModeGateCommands(code, io) {
207
207
  }
208
208
  // Local helper (was `readActiveSid` in code-commands.ts before rid-024 split).
209
209
  // The mode-gate stale-presence check needs the active sid for the
210
- // observability event; we re-import getSkillPresence here to avoid the
211
- // cross-file helper import.
212
- import { getSkillPresence } from '../../services/skills/skill-presence-service.js';
210
+ // observability event; `getSkillPresence` is imported at the top of the file
211
+ // alongside `checkStalePresence` to keep imports deduplicated.
213
212
  function readActiveSidForModeGate(projectRoot) {
214
213
  try {
215
214
  const presence = getSkillPresence(projectRoot);
@@ -60,7 +60,8 @@ export function registerCodeOrchestratorCanDoCommand(code, io) {
60
60
  function summaryLines(result) {
61
61
  const lines = [];
62
62
  lines.push(`verdict: ${result.canDoInSession ? 'canDoInSession=true' : 'canDoInSession=false (blockers present)'}; ` +
63
- `q1=src[${result.q1SourceCodeTouched ? 'Y' : 'N'}] q2=subagent[${result.q2SubAgentAvailable ? 'Y' : 'N'}] ` +
63
+ `q1=src[${result.q1SourceCodeTouched ? 'Y' : 'N'}] q1=hard-block[${result.q1HardBlockedPath ? 'Y' : 'N'}] ` +
64
+ `q2=subagent[${result.q2SubAgentAvailable ? 'Y' : 'N'}] ` +
64
65
  `q3=user-decision[${result.q3RequiresUserDecision ? 'Y' : 'N'}] q4=ratio=${result.contextRatio.toFixed(2)} ` +
65
66
  `(red-line ${ORCHESTRATOR_REDLINE_RATIO} / pre-compact ${ORCHESTRATOR_PRECOMPACT_RATIO})`);
66
67
  return lines;
@@ -34,7 +34,19 @@ import { findProjectRoot } from '../../services/config/config-safety.js';
34
34
  const SCHEDULE_VERSION = 1;
35
35
  const SCHEDULE_FILENAME = 'schedule.json';
36
36
  const HISTORY_FILENAME = 'history.jsonl';
37
- const DEFAULT_INTERVAL_MS = 24 * 60 * 60 * 1_000; // 24h
37
+ /**
38
+ * PRD-002b slice 2 — extract cron-orchestration magic numbers (24h
39
+ * default interval, 5-minute exec timeout, stderr-message truncation
40
+ * lengths in the renderer).
41
+ */
42
+ const MS_PER_SECOND = 1_000;
43
+ const MINUTES_PER_HOUR = 60;
44
+ const SECONDS_PER_MINUTE = 60;
45
+ const HOURS_PER_DAY = 24;
46
+ const DEFAULT_INTERVAL_MS = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
47
+ const EXEC_TIMEOUT_MS = 5 * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
48
+ const STDERR_RECORD_TRUNCATE_BYTES = 2000;
49
+ const STDERR_NEXT_ACTIONS_TRUNCATE_CHARS = 200;
38
50
  function cronDir(projectRoot) {
39
51
  return join(projectRoot, '.peaks', 'cron');
40
52
  }
@@ -136,13 +148,13 @@ export function runTask(projectRoot, task) {
136
148
  cwd: projectRoot,
137
149
  stdio: ['ignore', 'pipe', 'pipe'],
138
150
  encoding: 'utf8',
139
- timeout: 5 * 60_000
151
+ timeout: EXEC_TIMEOUT_MS
140
152
  });
141
153
  }
142
154
  catch (err) {
143
155
  const e = err;
144
156
  exitCode = typeof e.status === 'number' ? e.status : 1;
145
- stderr = (e.stderr ?? getErrorMessage(err)).slice(0, 2000);
157
+ stderr = (e.stderr ?? getErrorMessage(err)).slice(0, STDERR_RECORD_TRUNCATE_BYTES);
146
158
  }
147
159
  const record = {
148
160
  id,
@@ -243,7 +255,7 @@ export function registerCronCommand(program, io) {
243
255
  updatedEntries.push({ ...task, lastRunAt: record.finishedAt });
244
256
  }
245
257
  writeSchedule(projectRoot, { version: SCHEDULE_VERSION, entries: updatedEntries });
246
- printResult(io, ok('cron.run', { projectRoot, ran: records.length, records }, records.filter((r) => r.exitCode !== 0).map((r) => `Task ${r.taskId} failed (exit=${r.exitCode}): ${r.stderr.slice(0, 200)}`), [`Ran ${records.length} task(s); ${records.filter((r) => r.exitCode === 0).length} succeeded.`]), options.json);
258
+ printResult(io, ok('cron.run', { projectRoot, ran: records.length, records }, records.filter((r) => r.exitCode !== 0).map((r) => `Task ${r.taskId} failed (exit=${r.exitCode}): ${r.stderr.slice(0, STDERR_NEXT_ACTIONS_TRUNCATE_CHARS)}`), [`Ran ${records.length} task(s); ${records.filter((r) => r.exitCode === 0).length} succeeded.`]), options.json);
247
259
  if (records.some((r) => r.exitCode !== 0))
248
260
  process.exitCode = 1;
249
261
  }
@@ -5,7 +5,11 @@ import { read24hState } from '../../services/24h-mode/index.js';
5
5
  import { getErrorMessage } from '../cli-helpers.js';
6
6
  const SINCE_PATTERN = /^(\d+)([smhd])$/i;
7
7
  const WINDOW_CAP_HOURS = 24;
8
- const WINDOW_CAP_MS = WINDOW_CAP_HOURS * 60 * 60 * 1000;
8
+ const MS_PER_SECOND = 1000;
9
+ const MS_PER_MINUTE = 60 * MS_PER_SECOND;
10
+ const MS_PER_HOUR = 60 * MS_PER_MINUTE;
11
+ const MS_PER_DAY = 24 * MS_PER_HOUR;
12
+ const WINDOW_CAP_MS = WINDOW_CAP_HOURS * MS_PER_HOUR;
9
13
  function parseSince(raw) {
10
14
  if (!raw)
11
15
  return { ok: false, error: '--since is required (e.g. 24h, 8h, 30m)' };
@@ -16,7 +20,7 @@ function parseSince(raw) {
16
20
  if (!Number.isFinite(value) || value < 0)
17
21
  return { ok: false, error: `--since must be a non-negative integer (got ${raw})` };
18
22
  const unit = (match[2] ?? 'h').toLowerCase();
19
- const multiplier = unit === 'd' ? 86_400_000 : unit === 'h' ? 3_600_000 : unit === 'm' ? 60_000 : 1_000;
23
+ const multiplier = unit === 'd' ? MS_PER_DAY : unit === 'h' ? MS_PER_HOUR : unit === 'm' ? MS_PER_MINUTE : MS_PER_SECOND;
20
24
  return { ok: true, ms: Math.min(WINDOW_CAP_MS, value * multiplier) };
21
25
  }
22
26
  function readSlices(projectRoot, sessionId) {
@@ -45,7 +49,7 @@ function boundaryLabel(raw) {
45
49
  return { key: 'valid', message: 'unparseable' };
46
50
  const value = Number(match[1]);
47
51
  const unit = (match[2] ?? 'h').toLowerCase();
48
- const multiplier = unit === 'd' ? 86_400_000 : unit === 'h' ? 3_600_000 : unit === 'm' ? 60_000 : 1_000;
52
+ const multiplier = unit === 'd' ? MS_PER_DAY : unit === 'h' ? MS_PER_HOUR : unit === 'm' ? MS_PER_MINUTE : MS_PER_SECOND;
49
53
  if (value * multiplier > WINDOW_CAP_MS) {
50
54
  return { key: 'cap', message: 'since>24h is capped to the 24h window' };
51
55
  }
@@ -86,7 +90,7 @@ export function registerDashboardLongRunCommand(dashboard, io) {
86
90
  const sliceCount = readSlices(projectRoot, sid);
87
91
  const checkpointAgeMs = readLastCheckpointAgeMs(snapshot);
88
92
  const checkpointFrequency = snapshot.checkpoints > 0 && snapshot.enteredAt
89
- ? Math.max(1, Math.round((Date.now() - Date.parse(snapshot.enteredAt)) / snapshot.checkpoints / 60_000))
93
+ ? Math.max(1, Math.round((Date.now() - Date.parse(snapshot.enteredAt)) / snapshot.checkpoints / MS_PER_MINUTE))
90
94
  : null;
91
95
  const indicators = {
92
96
  dispatchCount: sliceCount,
@@ -1,3 +1,3 @@
1
- import type { ProgramIO } from '../cli-helpers.js';
1
+ import { type ProgramIO } from '../cli-helpers.js';
2
2
  import { type DispatchOptions } from './sub-agent-shared.js';
3
3
  export declare function runDispatchFromDag(role: string, options: DispatchOptions, asJson: boolean, io: ProgramIO): Promise<void>;