@event4u/agent-config 15.0.0 → 16.1.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 (283) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +301 -347
  5. package/CONTRIBUTING.md +9 -0
  6. package/README.md +2 -2
  7. package/dist/agent-src/commands/analyze/inbox.md +26 -0
  8. package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
  9. package/dist/agent-src/commands/challenge-me/closure.md +178 -0
  10. package/dist/agent-src/commands/challenge-me.md +9 -2
  11. package/dist/agent-src/commands/council/default.md +25 -5
  12. package/dist/agent-src/commands/feature/plan.md +28 -1
  13. package/dist/agent-src/commands/feature/roadmap.md +28 -1
  14. package/dist/agent-src/commands/implement-ticket.md +27 -0
  15. package/dist/agent-src/commands/jira-ticket.md +27 -0
  16. package/dist/agent-src/commands/pr/create.md +29 -0
  17. package/dist/agent-src/commands/pr/merge.md +60 -9
  18. package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
  19. package/dist/agent-src/commands/roadmap/create.md +77 -29
  20. package/dist/agent-src/commands/roadmap/materialize.md +27 -0
  21. package/dist/agent-src/commands/roadmap/process-full.md +123 -18
  22. package/dist/agent-src/commands/team.md +22 -0
  23. package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
  24. package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +97 -0
  25. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
  26. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
  27. package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
  28. package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
  29. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
  30. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
  31. package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  32. package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  33. package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
  34. package/dist/agent-src/rules/autonomous-execution.md +15 -8
  35. package/dist/agent-src/rules/delegation-policy.md +23 -45
  36. package/dist/agent-src/rules/design-fidelity.md +32 -37
  37. package/dist/agent-src/rules/evaluator-independence.md +43 -49
  38. package/dist/agent-src/rules/test-first.md +41 -0
  39. package/dist/agent-src/rules/user-interaction.md +31 -30
  40. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
  41. package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
  42. package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
  43. package/dist/agent-src/skills/database/data/manifest.json +4 -4
  44. package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
  45. package/dist/agent-src/skills/design-review/SKILL.md +50 -0
  46. package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
  47. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
  48. package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
  49. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  50. package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
  51. package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
  52. package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
  53. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
  54. package/dist/agent-src/templates/agent-settings.md +1 -1
  55. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  56. package/dist/agent-src/templates/roadmaps.md +188 -0
  57. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  58. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  59. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  60. package/dist/cli-delegate/check_update_banner.js +40 -2
  61. package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
  62. package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
  63. package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
  64. package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
  65. package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
  66. package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
  67. package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
  68. package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
  69. package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
  70. package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-P6ZHP5W2.js} +15 -90
  71. package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
  72. package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
  73. package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
  74. package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
  75. package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
  76. package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
  77. package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
  78. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  79. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  80. package/dist/cli-delegate/cmd_conformance.js +918 -325
  81. package/dist/cli-delegate/cmd_doctor.js +7 -5
  82. package/dist/cli-delegate/cmd_explain.js +5 -3
  83. package/dist/cli-delegate/cmd_export.js +9 -7
  84. package/dist/cli-delegate/cmd_fleet.js +4 -2
  85. package/dist/cli-delegate/cmd_handoff.js +6 -6
  86. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  87. package/dist/cli-delegate/cmd_migrate.js +2 -2
  88. package/dist/cli-delegate/cmd_packs_active.js +3 -2
  89. package/dist/cli-delegate/cmd_preflight.js +4 -2
  90. package/dist/cli-delegate/cmd_prune.js +10 -8
  91. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  92. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  93. package/dist/cli-delegate/cmd_session_recycle.js +4 -4
  94. package/dist/cli-delegate/cmd_settings_get.js +28 -2
  95. package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
  96. package/dist/cli-delegate/cmd_settings_set.js +14 -1
  97. package/dist/cli-delegate/cmd_sync.js +10 -8
  98. package/dist/cli-delegate/cmd_uninstall.js +16 -13
  99. package/dist/cli-delegate/cmd_update.js +1 -1
  100. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  101. package/dist/cli-delegate/cmd_validate.js +12 -10
  102. package/dist/cli-delegate/cmd_versions.js +1 -1
  103. package/dist/discovery/deprecation-report.md +1 -1
  104. package/dist/discovery/discovery-manifest.json +87 -40
  105. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  106. package/dist/discovery/discovery-manifest.summary.md +4 -4
  107. package/dist/discovery/dormancy-report.md +1 -1
  108. package/dist/discovery/orphan-report.md +1 -1
  109. package/dist/discovery/packs.json +10 -8
  110. package/dist/discovery/trust-report.md +3 -3
  111. package/dist/discovery/workspaces.json +8 -6
  112. package/dist/hooks/dispatch.js +2601 -1073
  113. package/dist/install/claudeRuleRewrite.js +14 -3
  114. package/dist/install/conflict.js +60 -10
  115. package/dist/install/conflictTracker.js +218 -0
  116. package/dist/install/fsPrimitives.js +54 -0
  117. package/dist/install/install.mjs +2008 -1481
  118. package/dist/install/manifestFiles.js +259 -0
  119. package/dist/install/preflight.js +14 -2
  120. package/dist/install/preserve.js +157 -0
  121. package/dist/install/recordedOwnership.js +132 -0
  122. package/dist/install/txlog.js +11 -2
  123. package/dist/mcp/registry-manifest.json +3 -3
  124. package/dist/mcp/server.json +2 -2
  125. package/dist/mcp/server.mjs +1612 -667
  126. package/dist/router.json +1 -1
  127. package/dist/scripts/_lib/agent_settings.js +3 -2
  128. package/dist/scripts/_lib/fs_atomic.js +167 -0
  129. package/dist/scripts/_lib/installed_lock.js +1 -1
  130. package/dist/scripts/_lib/installed_tools.js +460 -0
  131. package/dist/scripts/_lib/lean_projection_mode.js +3 -2
  132. package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
  133. package/dist/server/io/yamlIO.js +50 -0
  134. package/dist/server/routes/install.js +8 -0
  135. package/dist/server/routes/wizard.js +9 -3
  136. package/dist/server/schemas/settings.js +17 -5
  137. package/dist/shared/missionExecution.js +97 -0
  138. package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
  139. package/dist/ui/index.html +1 -1
  140. package/docs/MIGRATION.md +103 -0
  141. package/docs/contracts/settings-classes.md +26 -7
  142. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  143. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  144. package/docs/guidelines/design-fidelity-routing.md +29 -0
  145. package/package.json +1 -1
  146. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  147. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  148. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  149. package/src/config/agent-settings.template.yml +139 -27
  150. package/src/config/auto-rule-budget.json +16 -0
  151. package/src/config/ci-local-parity.yml +18 -5
  152. package/src/config/continuity-surface.json +28 -0
  153. package/src/config/gate-coverage.yml +146 -13
  154. package/src/config/gate-violation-baselines.json +6 -6
  155. package/src/config/hook-token-budget.json +2 -0
  156. package/src/config/host-injection-effect.json +1 -1
  157. package/src/config/loop-surfaces.yaml +215 -0
  158. package/src/config/platform-anchor.json +3 -2
  159. package/src/config/preamble-payload-budget.json +12 -7
  160. package/src/config/preamble-payload-exceptions.json +28 -0
  161. package/src/config/release-gate-locality.yml +11 -0
  162. package/src/config/routing-coverage-seed.json +18 -4
  163. package/src/config/rule-enforcement-baseline.json +0 -1
  164. package/src/install/claudeRuleRewrite.ts +16 -2
  165. package/src/install/conflict.ts +65 -9
  166. package/src/install/conflictTracker.ts +299 -0
  167. package/src/install/fsPrimitives.ts +54 -0
  168. package/src/install/manifestFiles.ts +312 -0
  169. package/src/install/preflight.ts +16 -2
  170. package/src/install/preserve.ts +205 -0
  171. package/src/install/recordedOwnership.ts +149 -0
  172. package/src/install/txlog.ts +11 -2
  173. package/src/install/types.ts +19 -4
  174. package/src/scripts/_cli/cmd_conformance.ts +88 -15
  175. package/src/scripts/_cli/cmd_doctor.ts +4 -3
  176. package/src/scripts/_cli/cmd_route_explain.ts +9 -2
  177. package/src/scripts/_cli/cmd_settings_get.ts +44 -0
  178. package/src/scripts/_cli/doctor_execution.ts +90 -0
  179. package/src/scripts/_dispatch.bash +3 -1
  180. package/src/scripts/_lib/activation_payload.ts +247 -0
  181. package/src/scripts/_lib/agent_settings.ts +4 -2
  182. package/src/scripts/_lib/authority_path.ts +102 -0
  183. package/src/scripts/_lib/base_ref_payload.ts +315 -0
  184. package/src/scripts/_lib/cascade_base.ts +94 -0
  185. package/src/scripts/_lib/changelog_eras.ts +7 -2
  186. package/src/scripts/_lib/continuation_ladder.ts +149 -2
  187. package/src/scripts/_lib/council_transport.ts +129 -0
  188. package/src/scripts/_lib/delivery_ready.ts +112 -0
  189. package/src/scripts/_lib/dropped_decision.ts +142 -0
  190. package/src/scripts/_lib/forge_protection.ts +267 -0
  191. package/src/scripts/_lib/hook_platform_keys.ts +27 -0
  192. package/src/scripts/_lib/installed_lock.ts +1 -1
  193. package/src/scripts/_lib/json_python_parity.ts +127 -0
  194. package/src/scripts/_lib/lean_projection_mode.ts +14 -3
  195. package/src/scripts/_lib/loop_surfaces.ts +382 -0
  196. package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
  197. package/src/scripts/_lib/mission_record.ts +190 -0
  198. package/src/scripts/_lib/obligation_frequency.ts +217 -0
  199. package/src/scripts/_lib/obligations.ts +402 -0
  200. package/src/scripts/_lib/orchestration_record.ts +11 -20
  201. package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
  202. package/src/scripts/_lib/release_findings_ingest.ts +324 -0
  203. package/src/scripts/_lib/release_highlights.ts +199 -10
  204. package/src/scripts/_lib/release_holds.ts +493 -0
  205. package/src/scripts/_lib/release_migration_gate.ts +184 -0
  206. package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
  207. package/src/scripts/_lib/review_baseline.ts +254 -0
  208. package/src/scripts/_lib/review_skipped_record.ts +61 -1
  209. package/src/scripts/_lib/rides_along.ts +102 -0
  210. package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
  211. package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
  212. package/src/scripts/_lib/test_delta.ts +166 -0
  213. package/src/scripts/_lib/test_provenance.ts +171 -0
  214. package/src/scripts/_lib/turn_end_refusals.ts +51 -4
  215. package/src/scripts/_lib/typed_op_grant.ts +246 -0
  216. package/src/scripts/_lib/typed_op_watch.ts +185 -0
  217. package/src/scripts/ai_council/config.ts +8 -11
  218. package/src/scripts/ai_council/fallback_config.ts +5 -3
  219. package/src/scripts/ai_council/ownership_classes.ts +118 -0
  220. package/src/scripts/ask_block_census.ts +285 -15
  221. package/src/scripts/check_beta_review_markers.ts +35 -4
  222. package/src/scripts/check_enforcement_matrix.ts +624 -0
  223. package/src/scripts/check_finding_dispositions.ts +167 -16
  224. package/src/scripts/check_gate_reachability.ts +173 -10
  225. package/src/scripts/check_generator_sync.ts +954 -0
  226. package/src/scripts/check_held_object_arrivals.ts +381 -0
  227. package/src/scripts/check_preamble_payload_budget.ts +474 -124
  228. package/src/scripts/check_release_highlights.ts +33 -1
  229. package/src/scripts/check_release_holds.ts +567 -0
  230. package/src/scripts/check_release_published.ts +1 -1
  231. package/src/scripts/check_reply_consistency.ts +86 -17
  232. package/src/scripts/check_rule_projection_integrity.ts +22 -12
  233. package/src/scripts/check_test_delta.ts +294 -0
  234. package/src/scripts/check_test_weakening.ts +299 -0
  235. package/src/scripts/closure_scan.ts +333 -0
  236. package/src/scripts/compile_hook_manifest.ts +43 -3
  237. package/src/scripts/council_record_shape.ts +209 -0
  238. package/src/scripts/hook_manifest.json +1 -1
  239. package/src/scripts/hook_manifest.yaml +127 -12
  240. package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
  241. package/src/scripts/hooks/concern_registry.ts +6 -0
  242. package/src/scripts/hooks/design_pass_hook.ts +128 -3
  243. package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
  244. package/src/scripts/hooks/evidence_independence.ts +96 -0
  245. package/src/scripts/hooks/memory_recall_hook.ts +540 -0
  246. package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
  247. package/src/scripts/hooks/review_baseline_hook.ts +130 -0
  248. package/src/scripts/hooks/rule_inject_hook.ts +49 -1
  249. package/src/scripts/hooks/run_continuation_hook.ts +17 -20
  250. package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
  251. package/src/scripts/hooks_doctor.ts +89 -1
  252. package/src/scripts/hooks_status.ts +19 -0
  253. package/src/scripts/install.ts +107 -207
  254. package/src/scripts/interruption_report.ts +8 -2
  255. package/src/scripts/janitor.ts +15 -1
  256. package/src/scripts/lint_decision_classes.ts +464 -0
  257. package/src/scripts/lint_hook_manifest.ts +11 -0
  258. package/src/scripts/lint_major_migration_sections.ts +450 -0
  259. package/src/scripts/lint_plan_risk_register.ts +47 -3
  260. package/src/scripts/lint_roadmap_blockers.ts +61 -0
  261. package/src/scripts/lint_roadmap_producers.ts +202 -0
  262. package/src/scripts/measure_rule_budget.ts +101 -5
  263. package/src/scripts/measure_turn_end_gate.ts +302 -5
  264. package/src/scripts/new_roadmap.ts +37 -0
  265. package/src/scripts/orchestration_record.ts +0 -5
  266. package/src/scripts/release.ts +66 -162
  267. package/src/scripts/release_drill.ts +460 -15
  268. package/src/scripts/release_env.ts +1 -1
  269. package/src/scripts/release_publication.ts +52 -1
  270. package/src/scripts/report_enforcement_drift.ts +566 -0
  271. package/src/scripts/report_held_object_arrivals.ts +456 -0
  272. package/src/scripts/report_host_injection_effect.ts +3 -11
  273. package/src/scripts/report_standing_payload_by_host.ts +26 -152
  274. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  275. package/src/scripts/schemas/command.schema.json +4 -0
  276. package/src/scripts/scope_growth.ts +219 -0
  277. package/src/scripts/sync_agent_settings.ts +140 -18
  278. package/src/scripts/trigger_eval_grandfather.json +0 -1
  279. package/src/scripts/ui_conformance_probe.ts +622 -0
  280. package/src/server/io/yamlIO.ts +48 -0
  281. package/src/server/schemas/settings.ts +31 -7
  282. package/src/shared/missionExecution.ts +126 -0
  283. package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The `--ci` fallback dispatch of the three tag-triggered workflows.
3
+ *
4
+ * Extracted from `release.ts` as a lowering commit under
5
+ * `check_source_size_budget`, whose own contract names splitting an
6
+ * over-ceiling file as the improvement it exists to force. The block is
7
+ * self-contained — three workflow names, one loop, one warning — and nothing
8
+ * else in step 10 reads it.
9
+ *
10
+ * WHY IT EXISTS. A tag pushed with the default `GITHUB_TOKEN` fires no
11
+ * tag-triggered workflow: GitHub's recursion guard suppresses the event. All
12
+ * three workflows accept a `tag` input for exactly this recovery, so under
13
+ * `--ci` the release dispatches them rather than waiting for an event that will
14
+ * never arrive. When the tag was pushed with a PAT they already fired and this
15
+ * is redundant.
16
+ *
17
+ * WHY IT NEVER FAILS THE RELEASE. By the time it runs the release is complete —
18
+ * the tag is pushed, the GitHub Release is created, npm publishes
19
+ * asynchronously. Dispatching through the API additionally needs the token's
20
+ * `actions:write` scope, and a 403 for a missing scope must not mark an
21
+ * already-shipped release as failed. So it warns and continues.
22
+ */
23
+
24
+ /** The tag-triggered workflows, in the order the release dispatches them. */
25
+ export const TAG_TRIGGERED_WORKFLOWS = [
26
+ 'release-guard.yml',
27
+ 'publish-npm.yml',
28
+ 'cloud-release.yml',
29
+ ] as const;
30
+
31
+ interface DispatchRunResult {
32
+ returncode: number;
33
+ }
34
+
35
+ export interface TagDispatchDeps {
36
+ run: (argv: string[], opts: { check: boolean }) => DispatchRunResult;
37
+ /** Where a non-fatal dispatch failure is reported. */
38
+ warn: (s: string) => void;
39
+ /** The ref the dispatch runs against — the trunk. */
40
+ trunk: string;
41
+ }
42
+
43
+ /** Why one failed dispatch is a warning and not a stop, said to the operator. */
44
+ export function dispatchWarning(workflow: string, code: number, version: string): string {
45
+ return (
46
+ `⚠️ Could not dispatch ${workflow} (exit ${code}) — the release ${version} is ` +
47
+ `already complete (tag + GitHub Release created; npm publishes async). If the tag was ` +
48
+ `pushed with a PAT, ${workflow} already fired on the tag push. If you rely on the explicit ` +
49
+ `dispatch, grant RELEASE_PR_TOKEN the "Actions: read and write" scope (fine-grained PAT) ` +
50
+ `or the "workflow" scope (classic PAT).\n`
51
+ );
52
+ }
53
+
54
+ export function dispatchTagWorkflows(deps: TagDispatchDeps, version: string): void {
55
+ for (const wf of TAG_TRIGGERED_WORKFLOWS) {
56
+ const r = deps.run(['gh', 'workflow', 'run', wf, '--ref', deps.trunk, '-f', `tag=${version}`], {
57
+ check: false,
58
+ });
59
+ if (r.returncode !== 0) {
60
+ deps.warn(dispatchWarning(wf, r.returncode, version));
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Session review baseline — the shape, the path, and the subtraction.
3
+ *
4
+ * WHY IT EXISTS. `end_review_nudge_hook` measures "did this session mutate
5
+ * enough to owe a review" as `git diff --numstat HEAD` plus untracked non-doc
6
+ * files, and its own header records the defect that reading produces: an
7
+ * uncommitted change that PREDATES the session is charged to the session. The
8
+ * header also records why it was not fixed — "no such baseline exists anywhere a
9
+ * `stop` concern can read it (recording one would be new session-wide state, out
10
+ * of this roadmap phase's one-concern scope)".
11
+ *
12
+ * That reason went stale. `session_start` is bound on seven hosts carrying
13
+ * fourteen concerns (`hook_manifest.yaml`), so a per-session baseline has a slot
14
+ * to be written from. The measured cost of leaving it: on an accumulator branch
15
+ * the nudge fired on 1,771 lines of another session's uncommitted work and
16
+ * became noise, which is worse than silence — an advisory that always fires is
17
+ * an advisory nobody reads.
18
+ *
19
+ * ONE MODULE FOR BOTH SIDES, deliberately. The writer is a `session_start`
20
+ * concern and the reader is a `stop` concern; if each owned its own idea of the
21
+ * path or the field names, the reader would silently find nothing and the whole
22
+ * mechanism would degrade to today's behaviour with no signal. That is the same
23
+ * argument `turn_end_refusals.ts` makes for its own shared path, and it is why
24
+ * `BASELINE_STATE_REL` is here rather than in either hook.
25
+ *
26
+ * ONE SELF-REFERENCE, named because it is invisible: the file this concern
27
+ * writes is itself an untracked non-doc file, so in a workspace where
28
+ * `agents/runtime/` is NOT gitignored it is counted by the very measurement it
29
+ * feeds — about six lines, once. This repository ignores that path, so the
30
+ * effect is zero here; a consumer whose ignore file does not cover it sees the
31
+ * nudge's count six lines high for the session.
32
+ *
33
+ * WHAT IT IS NOT. It is not a change set: no paths, no fingerprints, no patch.
34
+ * It answers exactly the question the nudge asks — how many non-doc lines were
35
+ * already dirty when this session began — and nothing else. A path-level ledger
36
+ * is a bigger object with a bigger privacy surface, and nothing in the nudge
37
+ * consumes one.
38
+ */
39
+ import { spawnSync } from 'node:child_process';
40
+ import * as fs from 'node:fs';
41
+ import * as path from 'node:path';
42
+
43
+ /**
44
+ * HEAD at this moment, or null.
45
+ *
46
+ * It lives HERE rather than in the writer because both sides need it and the
47
+ * writer already imports from the reader's module: putting it in the hook would
48
+ * make `end_review_nudge_hook → review_baseline_hook → end_review_nudge_hook` a
49
+ * cycle. The shared module is the only place with no edge back.
50
+ *
51
+ * Null is a real answer and not an error path — a workspace that is not a git
52
+ * repository, or one with no commit yet, has no HEAD, and `applyBaseline`
53
+ * compares null to null happily. What it must never do is throw: one caller runs
54
+ * on session start and the other on the stop path.
55
+ */
56
+ export function currentHeadSha(cwd: string): string | null {
57
+ try {
58
+ const r = spawnSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8' });
59
+ if (r.status !== 0) return null;
60
+ const out = (r.stdout ?? '').trim();
61
+ return out === '' ? null : out;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /** State directory, relative to a workspace root. */
68
+ export const BASELINE_STATE_REL = path.join('agents', 'runtime', 'state', 'review-baseline');
69
+
70
+ export function baselineStateDir(workspaceRoot: string): string {
71
+ return path.join(workspaceRoot, BASELINE_STATE_REL);
72
+ }
73
+
74
+ export function baselineStateFile(workspaceRoot: string, sessionKey: string): string {
75
+ return path.join(baselineStateDir(workspaceRoot), `${sessionKey}.json`);
76
+ }
77
+
78
+ /**
79
+ * Whether a line count is an exact sum or the capped, guaranteed-over-threshold
80
+ * approximation `end_review_nudge_hook` returns past `UNTRACKED_FILE_CAP`.
81
+ *
82
+ * Structurally identical to that module's `MutationMeasure` and deliberately
83
+ * NOT imported from it: this module is the shared floor both hooks sit on, and
84
+ * importing the consumer would put the cycle back that `currentHeadSha` lives
85
+ * here to avoid.
86
+ */
87
+ export type BaselineMeasure = 'exact' | 'capped_approximation';
88
+
89
+ /** What `session_start` records, and `stop` reads back. */
90
+ export interface ReviewBaseline {
91
+ /** HEAD at session start, or null where there is no repo / no commit yet. */
92
+ head_sha: string | null;
93
+ /** Non-doc mutated lines already present when the session began. */
94
+ baseline_lines: number;
95
+ /**
96
+ * Whether `baseline_lines` is exact or the capped approximation.
97
+ *
98
+ * It is recorded because subtracting one kind from the other is arithmetic
99
+ * over two different quantities — the same defect `head_sha` catches on the
100
+ * other axis. The capped path returns `THRESHOLD + 1 + tracked`, a synthetic
101
+ * number chosen to be over the bar rather than to be true, so a session that
102
+ * starts past the cap and then cleans up would have a real `exact` stop
103
+ * measurement reduced by a number that never counted anything.
104
+ *
105
+ * Optional: a record written before this field existed has no measure, and
106
+ * defaulting it to `exact` would assert the more permissive of the two on no
107
+ * evidence. Absent reads as unknown and refuses to subtract.
108
+ */
109
+ measure?: BaselineMeasure;
110
+ written_at: string;
111
+ }
112
+
113
+ /**
114
+ * Why a `stop` read could not subtract. Each value is written into the
115
+ * `review_skipped` telemetry row, because a fallback nobody can see is
116
+ * indistinguishable from the mechanism never having shipped.
117
+ */
118
+ export type BaselineFallback =
119
+ | 'absent' // no file — a host without session_start, or a session older than this
120
+ | 'unreadable' // present and malformed, or unreadable for any non-ENOENT reason
121
+ | 'head-moved' // the session committed, so the baseline's denominator is gone
122
+ | 'mixed-measure'; // one side is a capped approximation and the other is not
123
+
124
+ export type BaselineOutcome =
125
+ | { readonly applied: true; readonly baseline: ReviewBaseline; readonly lines: number }
126
+ | { readonly applied: false; readonly fallback: BaselineFallback; readonly lines: number };
127
+
128
+ export function parseBaseline(raw: string): ReviewBaseline | null {
129
+ let decoded: unknown;
130
+ try {
131
+ decoded = JSON.parse(raw);
132
+ } catch {
133
+ return null;
134
+ }
135
+ if (typeof decoded !== 'object' || decoded === null || Array.isArray(decoded)) return null;
136
+ const o = decoded as Record<string, unknown>;
137
+ const lines = o['baseline_lines'];
138
+ if (typeof lines !== 'number' || !Number.isFinite(lines) || lines < 0) return null;
139
+ const written = o['written_at'];
140
+ if (typeof written !== 'string') return null;
141
+ const head = o['head_sha'];
142
+ const measure = o['measure'];
143
+ return {
144
+ head_sha: typeof head === 'string' && head !== '' ? head : null,
145
+ baseline_lines: lines,
146
+ ...(measure === 'exact' || measure === 'capped_approximation' ? { measure } : {}),
147
+ written_at: written,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * The baseline, or WHICH of the two not-found states applies.
153
+ *
154
+ * `absent` and `unreadable` are kept apart all the way to the telemetry row,
155
+ * and collapsing them would have been the easy shape: both mean "cannot
156
+ * subtract", both fall back identically. They are different findings. `absent`
157
+ * is the expected steady state on a host with no `session_start` slot and on
158
+ * every session that started before this shipped; `unreadable` is a corrupt
159
+ * write, and a rate of it above noise is a defect in this mechanism rather than
160
+ * a property of the host. A single bucket would hide the second inside the
161
+ * first, which is the population it is smallest against.
162
+ */
163
+ export function readBaseline(
164
+ workspaceRoot: string,
165
+ sessionKey: string,
166
+ ): ReviewBaseline | 'absent' | 'unreadable' {
167
+ const file = baselineStateFile(workspaceRoot, sessionKey);
168
+ let raw: string;
169
+ try {
170
+ raw = fs.readFileSync(file, 'utf-8');
171
+ } catch (exc) {
172
+ // ONLY a genuinely missing file is `absent`. Every other IO failure —
173
+ // EACCES, EISDIR, EMFILE — is a defect in this mechanism, which is the
174
+ // population `unreadable` exists to make visible; routing them all to
175
+ // `absent` would bury them inside the one bucket that is legitimately
176
+ // large on every host without a session_start slot.
177
+ const code = (exc as NodeJS.ErrnoException | null)?.code;
178
+ return code === 'ENOENT' ? 'absent' : 'unreadable';
179
+ }
180
+ return parseBaseline(raw) ?? 'unreadable';
181
+ }
182
+
183
+ /**
184
+ * Apply the baseline to a measured line count.
185
+ *
186
+ * FAIL OPEN, NEVER SILENT — the three not-applied branches all return the
187
+ * UNSUBTRACTED count, so every failure mode of this mechanism degrades to
188
+ * exactly the behaviour that shipped before it. The alternative, returning zero
189
+ * or refusing to fire, would let a missing state file silence a genuine review
190
+ * obligation, and a safety advisory must not be disarmed by its own
191
+ * instrumentation failing.
192
+ *
193
+ * `head-moved` is the branch worth naming, and worth naming honestly: it is
194
+ * TERMINAL for the session. A session that committed mid-run moved HEAD, so
195
+ * `git diff HEAD` now measures against a different base than the baseline did
196
+ * and the subtraction is arithmetic over two different quantities. It is caught
197
+ * rather than absorbed, because absorbing it would UNDER-report on exactly the
198
+ * sessions that did the most work — but the baseline is written once per
199
+ * session and never refreshed, so from the first commit onward that session is
200
+ * charged for the whole pre-existing dirty tree again. The mechanism's useful
201
+ * life is "until this session's first commit", which is stated here rather than
202
+ * discovered later. Re-baselining after a commit is the obvious repair and is
203
+ * NOT taken here: a second write has to distinguish "HEAD moved because we
204
+ * committed our own work" from "a peer session committed underneath us", and
205
+ * nothing in a stop payload answers that.
206
+ *
207
+ * `mixed-measure` is the same defect on the third axis, and it covers the
208
+ * SYMMETRIC case too. Past `UNTRACKED_FILE_CAP` the measurement is a synthetic
209
+ * `THRESHOLD + 1 + tracked` chosen to be over the bar rather than to be true, so
210
+ * subtracting it from — or out of — an exact count is meaningless in both
211
+ * directions; and subtracting one capped reading from another cancels the
212
+ * constant, which is worse than meaningless because it silences the nudge. The
213
+ * name is kept over a more literal `not-both-exact` because the fallback's
214
+ * subject is the measure, and a telemetry enum is read far more often than it is
215
+ * renamed.
216
+ *
217
+ * The clamp at zero is not cosmetic either: a session that reverts part of a
218
+ * pre-existing dirty tree measures below its own baseline, and a negative
219
+ * comparison against the threshold would read as "no mutation" for a session
220
+ * that deleted a thousand lines.
221
+ */
222
+ export function applyBaseline(
223
+ measuredLines: number,
224
+ baseline: ReviewBaseline | 'absent' | 'unreadable',
225
+ headSha: () => string | null,
226
+ measuredMeasure: BaselineMeasure = 'exact',
227
+ ): BaselineOutcome {
228
+ if (baseline === 'absent' || baseline === 'unreadable') {
229
+ return { applied: false, fallback: baseline, lines: measuredLines };
230
+ }
231
+ // The measure check runs BEFORE the HEAD check because it needs no subprocess.
232
+ // `headSha` is a thunk for the same reason: on the two fallback branches above
233
+ // — the steady state on every host without the nudge, and on every session
234
+ // before its first baseline write — the value is discarded, and evaluating it
235
+ // eagerly spent an 11-14 ms `git rev-parse` on each of those stops.
236
+ // Refused when the two measures DISAGREE, and equally when both are capped.
237
+ // The symmetric case is the dangerous one and the first version missed it:
238
+ // the capped path returns `THRESHOLD + 1 + tracked`, so two capped readings
239
+ // cancel their synthetic constant and an untracked-only session that started
240
+ // past the cap subtracts to ZERO — silencing the nudge, which is the one
241
+ // direction this module's own header forbids. Only `exact` minus `exact` is
242
+ // arithmetic over two counts of the same thing.
243
+ if (baseline.measure !== 'exact' || measuredMeasure !== 'exact') {
244
+ return { applied: false, fallback: 'mixed-measure', lines: measuredLines };
245
+ }
246
+ if (baseline.head_sha !== headSha()) {
247
+ return { applied: false, fallback: 'head-moved', lines: measuredLines };
248
+ }
249
+ return {
250
+ applied: true,
251
+ baseline,
252
+ lines: Math.max(0, measuredLines - baseline.baseline_lines),
253
+ };
254
+ }
@@ -34,6 +34,49 @@
34
34
  */
35
35
  export type MutationMeasure = 'exact' | 'capped_approximation';
36
36
 
37
+ /**
38
+ * Whether `diff_lines` had this session's start-of-session baseline subtracted,
39
+ * and when it did not, WHY.
40
+ *
41
+ * A closed enum, never free text, so the privacy floor below still holds by
42
+ * construction. It exists because the subtraction's failure modes all fall back
43
+ * to the unsubtracted count — which is the safe direction and also an invisible
44
+ * one: without this field, a line reading 1,771 cannot be told apart from a
45
+ * session that really did mutate 1,771 lines.
46
+ *
47
+ * WHAT IT MEASURES, and the half it cannot. This line is written only when the
48
+ * nudge FIRES, so the distribution here is the fallback rate among firing
49
+ * sessions. The sessions the subtraction silenced — the mechanism working —
50
+ * produce no line at all, so the silence rate is NOT observable from this
51
+ * stream and no clause here should be read as claiming it is. Recording a row
52
+ * for a nudge that did not fire would put a `review_skipped` event in the audit
53
+ * log for a session where no review was skipped, which is a worse defect than
54
+ * the blind spot; measuring it properly needs a counter of its own, and that is
55
+ * a decision rather than an oversight.
56
+ *
57
+ * `absent` is the expected steady state on a host with no `session_start` slot
58
+ * and on every session that predates the baseline concern; `unreadable` is a
59
+ * corrupt write and a rate above noise is a defect in the mechanism itself;
60
+ * `head_moved` is a session that committed mid-run, which moves `git diff
61
+ * HEAD`'s base and makes the subtraction arithmetic over two different
62
+ * quantities; `mixed_measure` is the same failure on the capped-approximation
63
+ * axis, where one side is a synthetic over-threshold number rather than a count.
64
+ */
65
+ export type BaselineApplication =
66
+ | 'applied'
67
+ | 'absent'
68
+ | 'unreadable'
69
+ | 'head_moved'
70
+ | 'mixed_measure';
71
+
72
+ const BASELINE_APPLICATIONS: readonly BaselineApplication[] = [
73
+ 'applied',
74
+ 'absent',
75
+ 'unreadable',
76
+ 'head_moved',
77
+ 'mixed_measure',
78
+ ];
79
+
37
80
  import { appliedIds } from './audit_field_provenance.js';
38
81
  import type { NoFreeForm } from './runtime_journal.js';
39
82
  import { type PrivacyClass } from './privacy_class.js';
@@ -51,6 +94,13 @@ export interface ReviewSkippedInput {
51
94
  diff_lines: number;
52
95
  /** Whether `diff_lines` is exact or a capped approximation — see above. */
53
96
  mutation_measure: MutationMeasure;
97
+ /**
98
+ * Whether the session baseline was subtracted from `diff_lines`, and if not
99
+ * why. OPTIONAL, and absence is honest rather than a default: a line written
100
+ * by a producer that predates the baseline mechanism recorded nothing about
101
+ * it, and writing `absent` for those would assert a measurement nobody made.
102
+ */
103
+ baseline?: BaselineApplication | undefined;
54
104
  /** ISO-8601 UTC timestamp; caller supplies (keeps this fn pure/deterministic). */
55
105
  ts: string;
56
106
  /** Stable id (ULID, UUID, or content hash); caller supplies. */
@@ -103,6 +153,9 @@ export function buildReviewSkippedLine(input: ReviewSkippedInput): BuiltReviewSk
103
153
  }
104
154
  if (!input.ts) errors.push('ts (ISO-8601 UTC) is required');
105
155
  if (!input.id) errors.push('id (ULID, UUID, or content hash) is required');
156
+ if (input.baseline !== undefined && !BASELINE_APPLICATIONS.includes(input.baseline)) {
157
+ errors.push(`baseline must be one of ${BASELINE_APPLICATIONS.join(', ')}`);
158
+ }
106
159
 
107
160
  if (errors.length) return { line: null, errors };
108
161
 
@@ -131,7 +184,14 @@ export function buildReviewSkippedLine(input: ReviewSkippedInput): BuiltReviewSk
131
184
  persona: null,
132
185
  input_kind: 'prompt',
133
186
  type: 'note',
134
- review_skipped: { diff_lines: input.diff_lines, mutation_measure: input.mutation_measure },
187
+ review_skipped: {
188
+ diff_lines: input.diff_lines,
189
+ mutation_measure: input.mutation_measure,
190
+ // Omitted, not defaulted, when the producer recorded nothing —
191
+ // audit-log-v1's absent-versus-present split is the same one
192
+ // `skills_applied` uses above, for the same reason.
193
+ ...(input.baseline === undefined ? {} : { baseline: input.baseline }),
194
+ },
135
195
  };
136
196
 
137
197
  return { line, errors: [] };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * What rides along during a mission — `road-to-adversarial-verification-and-long-runs`
3
+ * 6.1.
4
+ *
5
+ * `active-remediation`'s ladder is fix-now / note+ask / follow-up PR, and its
6
+ * middle rung is unavailable inside an autonomous mission: there is nobody to
7
+ * ask. Without a replacement the rung collapses in one of two bad directions —
8
+ * every spotted issue becomes a fix (scope creep, which `minimal-safe-diff`
9
+ * exists to stop) or every one becomes silence (which `active-remediation`
10
+ * exists to stop). So under a mission the middle rung becomes **emit a follow-up
11
+ * artefact**, and this module decides which rung a candidate lands on.
12
+ *
13
+ * **Six criteria, ALL of which must hold.** They are `AND`-ed rather than scored
14
+ * because a scored version lets a large change buy its way in with four cheap
15
+ * yeses, which is the shape that turns a boy-scout rule into a refactor licence.
16
+ *
17
+ * Pure: the caller characterises the change, this decides. The characterisation
18
+ * is a judgement and stays the agent's; the DECISION is mechanical so it is the
19
+ * same for the same inputs.
20
+ */
21
+
22
+ /** What the agent observed about a candidate improvement. */
23
+ export interface Candidate {
24
+ /** What it is. Used only in the rendered line. */
25
+ readonly what: string;
26
+ /** In the same request path / module the mission is already touching. */
27
+ readonly local: boolean;
28
+ /** Small: roughly a handful of lines in one production file. */
29
+ readonly small: boolean;
30
+ /** Low blast radius — nothing outside this module observes the change. */
31
+ readonly lowBlastRadius: boolean;
32
+ /** Clearly correct: a reviewer would not need to be persuaded. */
33
+ readonly clearlyCorrect: boolean;
34
+ /** Testable, and the test ships in the same commit. */
35
+ readonly testable: boolean;
36
+ /** Carries NO new product decision — nothing about what the thing should do. */
37
+ readonly noProductDecision: boolean;
38
+ }
39
+
40
+ /** The kinds of improvement a mission may take at all. */
41
+ export const RIDE_ALONG_KINDS: readonly string[] = [
42
+ 'characterization-test',
43
+ 'regression-test',
44
+ 'small-adjacent-bug',
45
+ 'naming',
46
+ 'types',
47
+ 'robustness',
48
+ 'local-dead-code',
49
+ 'simplify-touched-code',
50
+ 'testability',
51
+ ] as const;
52
+
53
+ export type Disposition = 'ride-along' | 'follow-up-artefact';
54
+
55
+ export interface Verdict {
56
+ readonly disposition: Disposition;
57
+ /** Every criterion that failed, in declaration order. Empty on ride-along. */
58
+ readonly failed: readonly string[];
59
+ }
60
+
61
+ const CRITERIA: readonly (readonly [keyof Candidate, string])[] = [
62
+ ['local', 'not local to the path this mission is already touching'],
63
+ ['small', 'not small — more than a handful of lines in one file'],
64
+ ['lowBlastRadius', 'blast radius reaches beyond this module'],
65
+ ['clearlyCorrect', 'not clearly correct — a reviewer would need persuading'],
66
+ ['testable', 'not testable in the same commit'],
67
+ ['noProductDecision', 'carries a new product decision'],
68
+ ];
69
+
70
+ /**
71
+ * Which rung this candidate lands on.
72
+ *
73
+ * Returns EVERY failed criterion rather than the first, because the follow-up
74
+ * artefact has to say why the change was deferred, and "it failed one of six"
75
+ * is not a reason a later reader can act on.
76
+ */
77
+ export function disposition(c: Candidate): Verdict {
78
+ const failed = CRITERIA.filter(([k]) => c[k] !== true).map(([, why]) => why);
79
+ return {
80
+ disposition: failed.length === 0 ? 'ride-along' : 'follow-up-artefact',
81
+ failed,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * The follow-up artefact's body — what was seen, and why it did not ride along.
87
+ *
88
+ * A deferral with no reason is the note-and-forget `active-remediation` already
89
+ * forbids, one layer out: the artefact exists so the issue is still owed, and an
90
+ * artefact that does not say why it was deferred cannot be triaged later.
91
+ */
92
+ export function followUpBody(c: Candidate, v: Verdict): string {
93
+ if (v.disposition === 'ride-along') return '';
94
+ return [
95
+ `Deferred adjacent improvement: ${c.what}`,
96
+ '',
97
+ 'Did not ride along because:',
98
+ ...v.failed.map((f) => ` - ${f}`),
99
+ '',
100
+ 'Spotted during a mission; the code was left alone deliberately.',
101
+ ].join('\n');
102
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Settings keys this package has RENAMED, old path -> new path.
3
+ *
4
+ * Distinct from `REMOVED_KEYS` in the one way that matters to a user: a
5
+ * removed key's value is DISCARDED, and a renamed key's value is CARRIED.
6
+ * Putting a rename in the removed map would silently return an installed
7
+ * `false` to the shipped default, which is the opposite of accepting the old
8
+ * key — so the two maps stay separate even though both warn once.
9
+ *
10
+ * An entry is dropped one minor after it lands. The new key always wins when
11
+ * one layer carries both: a reader who has already migrated is not overridden
12
+ * by a stale line they forgot to delete.
13
+ *
14
+ * Its own module rather than more lines in `agent_settings.ts`, which sits
15
+ * three lines under the 1500-line source ceiling — a sixty-line addition there
16
+ * is sixty units of ratchet, and here it is none.
17
+ */
18
+
19
+ export type SettingsLayer = Record<string, unknown>;
20
+
21
+ export const RENAMED_KEYS: ReadonlyMap<string, string> = new Map([
22
+ ['planning.challenge_on_create', 'planning.closure_pass'],
23
+ ]);
24
+
25
+ /** Keys already warned about in THIS process — the "once per run" dedupe. */
26
+ const _warned = new Set<string>();
27
+
28
+ /** Read a dotted path out of a settings tree. `undefined` when absent. */
29
+ function _read(tree: SettingsLayer, dotted: string): unknown {
30
+ let cur: unknown = tree;
31
+ for (const part of dotted.split('.')) {
32
+ if (cur === null || typeof cur !== 'object' || Array.isArray(cur)) return undefined;
33
+ cur = (cur as SettingsLayer)[part];
34
+ }
35
+ return cur;
36
+ }
37
+
38
+ /** Write a dotted path into a settings tree, creating the intermediate maps. */
39
+ function _write(tree: SettingsLayer, dotted: string, value: unknown): void {
40
+ const parts = dotted.split('.');
41
+ let cur: SettingsLayer = tree;
42
+ for (const part of parts.slice(0, -1)) {
43
+ const next = cur[part];
44
+ if (next === null || typeof next !== 'object' || Array.isArray(next)) cur[part] = {};
45
+ cur = cur[part] as SettingsLayer;
46
+ }
47
+ cur[parts[parts.length - 1] as string] = value;
48
+ }
49
+
50
+ /**
51
+ * Carry every renamed key's value onto its new path within ONE layer, and
52
+ * return that layer.
53
+ *
54
+ * Per LAYER, before the merge, and that ordering is the whole contract: the
55
+ * shipped template already carries the new key, so a rename applied to the
56
+ * MERGED tree would always find the target "already set" and drop the user's
57
+ * value on the floor. Applied per layer, the old key becomes the new key
58
+ * inside the layer that wrote it, and ordinary precedence does the rest.
59
+ *
60
+ * Mutates and returns `layer` so it can wrap an existing merge call without
61
+ * adding a statement. Never throws: a layer that cannot be walked is a layer
62
+ * with nothing to migrate.
63
+ */
64
+ export function applyRenamedKeys<T extends SettingsLayer>(layer: T): T {
65
+ for (const [oldKey, newKey] of RENAMED_KEYS) {
66
+ const oldValue = _read(layer, oldKey);
67
+ if (oldValue === undefined) continue;
68
+ if (_read(layer, newKey) === undefined) _write(layer, newKey, oldValue);
69
+ if (_warned.has(oldKey)) continue;
70
+ _warned.add(oldKey);
71
+ process.stderr.write(
72
+ `⚠️ settings: \`${oldKey}\` was renamed to \`${newKey}\`. The old key still ` +
73
+ `works for one minor; rename it in your .agent-settings.yml.\n`,
74
+ );
75
+ }
76
+ return layer;
77
+ }
78
+
79
+ /** Test seam — the once-per-process warning dedupe is process state. */
80
+ export function _resetRenameWarnings(): void {
81
+ _warned.clear();
82
+ }