session-orchestrator 5.2.0 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +5 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +5 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +5 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
  9. package/.agents/skills/close/SKILL.md +5 -1
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +5 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +5 -1
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +5 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +5 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +6 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +6 -2
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +5 -1
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +5 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +5 -1
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +5 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/npm-publish/SKILL.md +4 -2
  41. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  42. package/.agents/skills/persona-panel/SKILL.md +5 -1
  43. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  44. package/.agents/skills/plan/SKILL.md +5 -1
  45. package/.agents/skills/plan/agents/openai.yaml +5 -0
  46. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  47. package/.agents/skills/portfolio/SKILL.md +5 -1
  48. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  49. package/.agents/skills/quality-gates/SKILL.md +3 -1
  50. package/.agents/skills/reconcile/SKILL.md +5 -1
  51. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  52. package/.agents/skills/release/SKILL.md +5 -1
  53. package/.agents/skills/release/agents/openai.yaml +5 -0
  54. package/.agents/skills/remote-offload/SKILL.md +3 -1
  55. package/.agents/skills/repo-audit/SKILL.md +5 -1
  56. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  57. package/.agents/skills/session/SKILL.md +21 -0
  58. package/.agents/skills/session/agents/openai.yaml +5 -0
  59. package/.agents/skills/session-end/SKILL.md +3 -1
  60. package/.agents/skills/session-plan/SKILL.md +3 -1
  61. package/.agents/skills/session-start/SKILL.md +3 -1
  62. package/.agents/skills/spinout/SKILL.md +5 -1
  63. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  64. package/.agents/skills/sunset-review/SKILL.md +5 -1
  65. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  66. package/.agents/skills/templates-ack/SKILL.md +21 -0
  67. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  68. package/.agents/skills/test/SKILL.md +5 -1
  69. package/.agents/skills/test/agents/openai.yaml +5 -0
  70. package/.agents/skills/test-runner/SKILL.md +3 -1
  71. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  72. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  73. package/.agents/skills/ux-grill/SKILL.md +5 -1
  74. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  75. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  76. package/.agents/skills/vault-sync/SKILL.md +3 -1
  77. package/.agents/skills/wave-executor/SKILL.md +3 -1
  78. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  79. package/.claude-plugin/marketplace.json +1 -1
  80. package/.claude-plugin/plugin.json +1 -1
  81. package/.codex-plugin/plugin.json +4 -4
  82. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  83. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  84. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  85. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  86. package/.codex-plugin/skills/session/SKILL.md +1 -1
  87. package/.cursor/commands/eval.md +1 -1
  88. package/.cursor/commands/session.md +1 -1
  89. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  90. package/.cursor/rules/050-plan.mdc +1 -1
  91. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  92. package/.cursor/skills/eval/SKILL.md +1 -1
  93. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  94. package/.cursor-plugin/plugin.json +1 -1
  95. package/.orchestrator/policy/blocked-commands.json +12 -3
  96. package/AGENTS.md +3 -2
  97. package/CHANGELOG.md +136 -0
  98. package/README.md +9 -9
  99. package/SECURITY.md +12 -0
  100. package/agents/dialectic-deriver.md +13 -10
  101. package/agents/eval-judge.md +67 -45
  102. package/agents/skill-applied-judge.md +34 -19
  103. package/commands/session.md +7 -3
  104. package/docs/baseline.md +12 -6
  105. package/docs/codex-setup.md +14 -2
  106. package/docs/components.md +7 -5
  107. package/docs/events-schema.md +56 -9
  108. package/docs/rule-authoring.md +58 -6
  109. package/docs/session-config-reference.md +100 -7
  110. package/docs/session-config-template.md +31 -2
  111. package/docs/telemetry.md +2 -0
  112. package/hooks/_lib/hook-import-set.json +85 -8
  113. package/hooks/_lib/subagent-transcript.mjs +582 -31
  114. package/hooks/config-protection.mjs +11 -3
  115. package/hooks/cwd-change-restore.mjs +11 -3
  116. package/hooks/enforce-commands.mjs +70 -23
  117. package/hooks/enforce-scope.mjs +143 -33
  118. package/hooks/hooks-codex.json +1 -1
  119. package/hooks/hooks.json +1 -1
  120. package/hooks/loop-guard.mjs +11 -3
  121. package/hooks/on-session-end.mjs +58 -23
  122. package/hooks/on-session-start.mjs +48 -11
  123. package/hooks/on-stop.mjs +168 -22
  124. package/hooks/operator-steer.mjs +11 -3
  125. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  126. package/hooks/post-bash-write-verify.mjs +3 -2
  127. package/hooks/post-edit-import-probe.mjs +17 -9
  128. package/hooks/post-edit-validate.mjs +13 -5
  129. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  130. package/hooks/post-tool-batch-wave-signal.mjs +200 -38
  131. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  132. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  133. package/hooks/pre-auq-clarity.mjs +15 -2
  134. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  135. package/hooks/pre-bash-issue-budget.mjs +16 -11
  136. package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
  138. package/hooks/pre-bash-staging-fence.mjs +335 -31
  139. package/hooks/pre-bash-templates-first.mjs +19 -14
  140. package/hooks/pre-task-scope-disjoint.mjs +233 -2
  141. package/hooks/subagent-telemetry.mjs +15 -19
  142. package/hooks/wave-scope-commit-guard.mjs +197 -100
  143. package/monitors/monitors.json +1 -1
  144. package/output-styles/wave-summary.md +1 -1
  145. package/package.json +1 -1
  146. package/pi/prompts/eval.md +1 -1
  147. package/pi/prompts/session.md +1 -1
  148. package/rules/README.md +1 -1
  149. package/rules/opt-in-domain/prompt-caching.md +1 -1
  150. package/rules/opt-in-stack/backend-data.md +1 -1
  151. package/rules/opt-in-stack/backend.md +3 -3
  152. package/rules/opt-in-stack/frontend.md +1 -1
  153. package/rules/opt-in-stack/security-web.md +3 -3
  154. package/rules/opt-in-stack/swift.md +1 -1
  155. package/scripts/autopilot.mjs +23 -2
  156. package/scripts/backfill-abandoned-sessions.mjs +117 -15
  157. package/scripts/check-sessions-integrity.mjs +300 -0
  158. package/scripts/dialectic-deriver.mjs +50 -13
  159. package/scripts/emit-session.mjs +75 -29
  160. package/scripts/eval-session.mjs +65 -3
  161. package/scripts/generate-agents-skills.mjs +102 -29
  162. package/scripts/generate-cursor-adapter.mjs +61 -16
  163. package/scripts/lib/agent-status.mjs +2 -31
  164. package/scripts/lib/auq/clarity.mjs +10 -2
  165. package/scripts/lib/auq/parse.mjs +12 -31
  166. package/scripts/lib/auq/schema.mjs +56 -41
  167. package/scripts/lib/auto-dialectic.mjs +304 -15
  168. package/scripts/lib/autopilot/flags.mjs +12 -1
  169. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  170. package/scripts/lib/autopilot/loop.mjs +14 -1
  171. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  172. package/scripts/lib/ci-status-banner.mjs +376 -16
  173. package/scripts/lib/command-blocker.mjs +275 -28
  174. package/scripts/lib/config/dialectic.mjs +12 -3
  175. package/scripts/lib/config/gate.mjs +74 -0
  176. package/scripts/lib/config/reaper.mjs +162 -0
  177. package/scripts/lib/config.mjs +14 -0
  178. package/scripts/lib/convergence-monitor.mjs +74 -11
  179. package/scripts/lib/ecosystem-health.mjs +11 -0
  180. package/scripts/lib/eval/engine.mjs +421 -53
  181. package/scripts/lib/eval/judge.mjs +463 -40
  182. package/scripts/lib/eval/schema.mjs +10 -1
  183. package/scripts/lib/events-rotation.mjs +221 -25
  184. package/scripts/lib/events-schema.mjs +114 -0
  185. package/scripts/lib/events.mjs +524 -5
  186. package/scripts/lib/frontmatter-guard.mjs +21 -10
  187. package/scripts/lib/gates/gate-baseline.mjs +27 -2
  188. package/scripts/lib/gates/gate-full.mjs +28 -3
  189. package/scripts/lib/gates/gate-helpers.mjs +243 -21
  190. package/scripts/lib/gates/gate-incremental.mjs +28 -3
  191. package/scripts/lib/gates/gate-per-file.mjs +27 -2
  192. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  193. package/scripts/lib/instruction-budget-guard.mjs +146 -4
  194. package/scripts/lib/io.mjs +42 -8
  195. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  196. package/scripts/lib/js-mask.mjs +197 -0
  197. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  198. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  199. package/scripts/lib/orphan-reaper.mjs +1588 -0
  200. package/scripts/lib/peer-cards/merger.mjs +48 -10
  201. package/scripts/lib/peer-cards/reader.mjs +78 -2
  202. package/scripts/lib/process-group.mjs +899 -0
  203. package/scripts/lib/quality-gate.mjs +107 -28
  204. package/scripts/lib/reconcile/backlog.mjs +368 -0
  205. package/scripts/lib/reconcile/engine.mjs +55 -188
  206. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  207. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  208. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  209. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  210. package/scripts/lib/rule-loader.mjs +41 -12
  211. package/scripts/lib/scope-echo.mjs +39 -2
  212. package/scripts/lib/scope-gate.mjs +605 -1
  213. package/scripts/lib/session-close-backfill.mjs +33 -6
  214. package/scripts/lib/session-id.mjs +9 -20
  215. package/scripts/lib/session-invocation.mjs +20 -0
  216. package/scripts/lib/session-schema/constants.mjs +30 -2
  217. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  218. package/scripts/lib/session-schema.mjs +8 -3
  219. package/scripts/lib/session-start-probes.mjs +95 -10
  220. package/scripts/lib/sessions-canonical.mjs +23 -0
  221. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  222. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  223. package/scripts/lib/skill-evidence-window.mjs +891 -0
  224. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  225. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  226. package/scripts/lib/skill-judge.mjs +45 -3
  227. package/scripts/lib/tail-window.mjs +56 -0
  228. package/scripts/lib/telemetry/schema.mjs +30 -0
  229. package/scripts/lib/telemetry/sync.mjs +61 -6
  230. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  231. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  232. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  233. package/scripts/lib/user-invocable-skills.mjs +23 -3
  234. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  235. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  236. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  237. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  238. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  239. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  240. package/scripts/lib/validate/check-rules.mjs +7 -5
  241. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  242. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  243. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  244. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  245. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  246. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  247. package/scripts/lib/validate/confidential-names.mjs +10 -0
  248. package/scripts/lib/validate-vendored-rules.mjs +4 -3
  249. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  250. package/scripts/lib/vault-mirror/process.mjs +10 -3
  251. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  252. package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
  253. package/scripts/lib/vault-yaml.mjs +118 -0
  254. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  255. package/scripts/release-session-lock.mjs +305 -0
  256. package/scripts/release.mjs +30 -5
  257. package/scripts/resolve-session-invocation.mjs +59 -0
  258. package/scripts/run-quality-gate.mjs +156 -17
  259. package/scripts/sweep-expired-rules.mjs +14 -3
  260. package/scripts/validate-plugin.mjs +12 -0
  261. package/scripts/validate-wave-scope.mjs +32 -105
  262. package/scripts/vault-mirror.mjs +9 -1
  263. package/skills/_shared/platform-tools.md +23 -11
  264. package/skills/autopilot/SKILL.md +22 -7
  265. package/skills/claude-md-drift-check/SKILL.md +1 -1
  266. package/skills/convergence-monitoring/README.md +8 -1
  267. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  268. package/skills/convergence-monitoring/SKILL.md +15 -6
  269. package/skills/eval/SKILL.md +39 -24
  270. package/skills/eval/rubric-v1.md +1 -0
  271. package/skills/eval/rubric-v2.md +457 -0
  272. package/skills/evolve/SKILL.md +1 -1
  273. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  274. package/skills/gitlab-ops/SKILL.md +3 -2
  275. package/skills/npm-publish/SKILL.md +1 -1
  276. package/skills/reconcile/SKILL.md +11 -0
  277. package/skills/session-end/SKILL.md +13 -16
  278. package/skills/session-end/discovery-scan.md +1 -1
  279. package/skills/session-end/phase-3-6-tail.md +55 -9
  280. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  281. package/skills/session-end/session-metrics-write.md +10 -0
  282. package/skills/session-plan/SKILL.md +17 -5
  283. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  284. package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
  285. package/skills/ux-grill/SKILL.md +1 -1
  286. package/skills/wave-executor/SKILL.md +8 -4
  287. package/skills/wave-executor/circuit-breaker.md +2 -0
  288. package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
  289. package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
  290. package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
  291. package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
  292. package/.cursor/commands/convergence-monitoring.md +0 -13
  293. package/.cursor/commands/npm-publish.md +0 -13
  294. package/pi/prompts/convergence-monitoring.md +0 -11
  295. package/pi/prompts/npm-publish.md +0 -11
@@ -70,9 +70,10 @@
70
70
  * @module scripts/lib/maintenance-due-banner
71
71
  */
72
72
 
73
- import { closeSync, existsSync, fstatSync, openSync, readSync, statSync } from 'node:fs';
73
+ import { existsSync, statSync } from 'node:fs';
74
74
  import path from 'node:path';
75
75
 
76
+ import { SCAN_CHUNK_BYTES, scanEventsBackwards } from './events.mjs';
76
77
  import { computeReconcileNudge } from './reconcile-nudge-banner.mjs';
77
78
  import { sweepExpiredLearnings } from './learnings/expiry-sweep.mjs';
78
79
  import { shouldDispatchAutoDialectic } from './auto-dialectic.mjs';
@@ -152,104 +153,64 @@ function isoDay(ts) {
152
153
  }
153
154
 
154
155
  /**
155
- * Backwards-scan chunk size (#1290 item 2).
156
- *
157
- * NAMED CEILING: 256 KiB is ~800 records in this repo's ledger, so the common
158
- * case — a repo that ran /evolve within its recent history — answers after a
159
- * handful of reads instead of loading the whole 7.9 MB file. The scan is
160
- * UNBOUNDED in the worst case ON PURPOSE: "never ran" is a claim about every
161
- * line and cannot be made from a tail, so a repo with no `evolve.completed`
162
- * record still walks the file to its start — just in chunks, never all at once
163
- * in one string.
164
- *
165
- * REVISIT TRIGGER: the maintenance probe's median passes 1000 ms (half
166
- * `PROBE_BUDGET_MS`), or one repo's `events.jsonl` passes 50 MB. Either means
167
- * the "never ran" walk has become the cost that matters and the answer needs an
168
- * index rather than a scan.
156
+ * Backwards-scan chunk size (#1290 item 2) — the shared reader's constant, kept
157
+ * under this module's own name because it is the number this probe's cost is
158
+ * reasoned about in. NAMED CEILING + revisit trigger live at
159
+ * {@link SCAN_CHUNK_BYTES}.
169
160
  */
170
- export const TAIL_CHUNK_BYTES = 256 * 1024;
161
+ export const TAIL_CHUNK_BYTES = SCAN_CHUNK_BYTES;
171
162
 
172
163
  /**
173
- * Scan a buffer of COMPLETE lines backwards for the newest evolve record.
164
+ * Wall-clock budget for the "did /evolve ever run?" walk (#1414).
165
+ *
166
+ * NAMED CEILING (BV-004): 1000 ms is half of `PROBE_BUDGET_MS` (2000 ms in
167
+ * `session-start-probes.mjs`), which this probe shares with six other signals.
168
+ * The walk is UNBOUNDED in principle — "never ran" is a claim about every line
169
+ * of every source — so it needs an own limit now that it spans the archives
170
+ * too, and running out must report `truncated`, never a clean "never".
174
171
  *
175
- * @param {Buffer} buf
176
- * @returns {{lastAt: string|null}|null} null ⇒ no record in this buffer
172
+ * REVISIT TRIGGER: a repo where this reports `truncated` more than rarely. That
173
+ * means the answer needs an index rather than a scan, not a bigger budget
174
+ * (HR-101).
177
175
  */
178
- function scanEvolveLines(buf) {
179
- if (buf.length === 0) return null;
180
- const lines = buf.toString('utf8').split('\n');
181
- for (let i = lines.length - 1; i >= 0; i -= 1) {
182
- const line = lines[i];
183
- if (!line || !line.includes(EVOLVE_EVENT)) continue; // cheap pre-filter before JSON.parse
184
- try {
185
- const rec = JSON.parse(line);
186
- if (rec?.event !== EVOLVE_EVENT) continue;
187
- return { lastAt: typeof rec.timestamp === 'string' ? rec.timestamp : null };
188
- } catch {
189
- continue; // a malformed line is not evidence either way — keep scanning
190
- }
191
- }
192
- return null;
193
- }
176
+ export const EVOLVE_SCAN_BUDGET_MS = 1000;
194
177
 
195
178
  /**
196
- * Find the most recent `orchestrator.evolve.completed` record.
197
- *
198
- * Reads the ledger BACKWARDS in {@link TAIL_CHUNK_BYTES} chunks and stops at
199
- * the first hit, because the interesting answer is the LAST occurrence. The
200
- * former implementation `readFileSync`-ed the whole file (7.9 MB here, 30–44 ms)
201
- * to answer a question the last few kilobytes usually settle.
179
+ * Find the most recent `orchestrator.evolve.completed` record — across ROTATION
180
+ * boundaries (#1414).
202
181
  *
203
- * The one bug a naive chunked scan introduces is a record SPLIT across a chunk
204
- * boundary: the bytes before the first newline of a chunk are the tail of a line
205
- * whose head is in the chunk not read yet, so they are CARRIED, never parsed
206
- * here. Splitting on the 0x0A byte is safe on UTF-8 — no continuation byte can
207
- * equal a newline — so a multibyte character never splits a line either.
182
+ * Walks the ledger backwards through every source, newest first, and stops at
183
+ * the first hit, because the interesting answer is the LAST occurrence. Reading
184
+ * the active file alone was the bug: "never ran" is a claim about EVERY line
185
+ * ever written, and after a rotation the only `evolve.completed` record on the
186
+ * host can sit in `_archive/` — the probe then nags a repo that has run
187
+ * /evolve, which is the HR-101 failure mode this module exists to avoid.
208
188
  *
209
189
  * @param {string} repoRoot
210
- * @returns {{ok: boolean, lastAt: string|null}} `ok: false` ⇒ the ledger exists
211
- * but could not be read — the caller must record `undeterminable`, never clean.
190
+ * @returns {{ok: boolean, lastAt: string|null, truncated: boolean}} `ok: false`
191
+ * ⇒ a source exists but could not be read; `truncated: true` ⇒ the budget ran
192
+ * out before the walk finished. In BOTH cases the caller must record
193
+ * `undeterminable`, never clean — a walk that did not finish has not proven
194
+ * "never".
212
195
  */
213
196
  function readLastEvolveRun(repoRoot) {
214
197
  const file = path.join(repoRoot, '.orchestrator', 'metrics', 'events.jsonl');
215
- if (!existsSync(file)) return { ok: true, lastAt: null }; // fresh repo: genuinely never
216
- let fd;
217
- try {
218
- fd = openSync(file, 'r');
219
- let pos = fstatSync(fd).size;
220
- /** Partial line at the FRONT of everything read so far. */
221
- let carry = Buffer.alloc(0);
222
-
223
- while (pos > 0) {
224
- const length = Math.min(TAIL_CHUNK_BYTES, pos);
225
- pos -= length;
226
- const buf = Buffer.alloc(length);
227
- readSync(fd, buf, 0, length, pos);
228
- const block = carry.length > 0 ? Buffer.concat([buf, carry]) : buf;
229
- const firstNewline = block.indexOf(0x0a);
230
- if (firstNewline === -1) {
231
- carry = block; // no complete line yet — a line longer than one chunk
232
- continue;
233
- }
234
- const hit = scanEvolveLines(block.subarray(firstNewline + 1));
235
- if (hit) return { ok: true, lastAt: hit.lastAt };
236
- carry = block.subarray(0, firstNewline);
237
- }
238
-
239
- // pos === 0: the carry is the file's FIRST line, complete by construction.
240
- const hit = scanEvolveLines(carry);
241
- return { ok: true, lastAt: hit ? hit.lastAt : null };
242
- } catch {
243
- return { ok: false, lastAt: null };
244
- } finally {
245
- if (fd !== undefined) {
246
- try {
247
- closeSync(fd);
248
- } catch {
249
- /* best-effort */
250
- }
251
- }
252
- }
198
+ let lastAt = null;
199
+ const scan = scanEventsBackwards({
200
+ filePath: file,
201
+ chunkBytes: TAIL_CHUNK_BYTES,
202
+ budgetMs: EVOLVE_SCAN_BUDGET_MS,
203
+ // Cheap substring pre-filter before JSON.parse — the property the previous
204
+ // hand-rolled scan was written for, kept here rather than re-derived.
205
+ filter: EVOLVE_EVENT,
206
+ onRecord: (rec) => {
207
+ if (rec?.event !== EVOLVE_EVENT) return false;
208
+ lastAt = typeof rec.timestamp === 'string' ? rec.timestamp : null;
209
+ return true;
210
+ },
211
+ });
212
+ if (scan.unreadable.length > 0) return { ok: false, lastAt: null, truncated: false };
213
+ return { ok: true, lastAt, truncated: scan.truncated };
253
214
  }
254
215
 
255
216
  /**
@@ -335,7 +296,9 @@ export async function computeMaintenanceDue(opts = {}) {
335
296
  } else {
336
297
  // --- evolve (S1) -------------------------------------------------------
337
298
  const evolve = readLastEvolveRun(repoRoot);
338
- if (!evolve.ok) {
299
+ // `truncated` is NOT "not found": a walk that ran out of budget proves
300
+ // nothing about the lines it never reached (#1414).
301
+ if (!evolve.ok || evolve.truncated) {
339
302
  undeterminable.push('evolve');
340
303
  } else if (evolve.lastAt === null && nudge.activeLearnings >= MAINTENANCE_MIN_LEARNINGS) {
341
304
  markDue('evolve', `never, ${nudge.activeLearnings} active learnings`);
@@ -344,7 +307,9 @@ export async function computeMaintenanceDue(opts = {}) {
344
307
  // --- reconcile (S3) ----------------------------------------------------
345
308
  if (nudge.nudge === true) {
346
309
  // HR-106: report what the rule JUDGED. `computeReconcileNudge` nudges on
347
- // a BACKLOG (eligible-unmaterialized learnings), never on a date — so
310
+ // a BACKLOG — rule-eligible learnings minus those already materialized
311
+ // in the sidecar or `.claude/rules/` (`countReconcileBacklog`, #1380;
312
+ // before #1380 it counted all eligible ones) — never on a date — so
348
313
  // printing `lastRunAt` here put today's date next to the word "due" and
349
314
  // read as "last run today and already due again". Measured 2026-09-09
350
315
  // (learning 013a45ba): with 108 learnings capped under