@event4u/agent-config 16.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 (246) 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 +297 -137
  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 +74 -27
  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 +47 -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/skills/accessibility-auditor/data/manifest.json +3 -3
  40. package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
  41. package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
  42. package/dist/agent-src/skills/database/data/manifest.json +4 -4
  43. package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
  44. package/dist/agent-src/skills/design-review/SKILL.md +50 -0
  45. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
  46. package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
  47. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  48. package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
  49. package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
  50. package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
  51. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
  52. package/dist/agent-src/templates/agent-settings.md +1 -1
  53. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  54. package/dist/agent-src/templates/roadmaps.md +188 -0
  55. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  56. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  57. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  58. package/dist/cli-delegate/check_update_banner.js +40 -2
  59. package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
  60. package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
  61. package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
  62. package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
  63. package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
  64. package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
  65. package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
  66. package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
  67. package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
  68. package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
  69. package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
  70. package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
  71. package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
  72. package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
  73. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  74. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  75. package/dist/cli-delegate/cmd_conformance.js +90 -56
  76. package/dist/cli-delegate/cmd_doctor.js +7 -5
  77. package/dist/cli-delegate/cmd_explain.js +2 -2
  78. package/dist/cli-delegate/cmd_export.js +9 -7
  79. package/dist/cli-delegate/cmd_fleet.js +4 -2
  80. package/dist/cli-delegate/cmd_handoff.js +4 -4
  81. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  82. package/dist/cli-delegate/cmd_migrate.js +2 -2
  83. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  84. package/dist/cli-delegate/cmd_preflight.js +4 -2
  85. package/dist/cli-delegate/cmd_prune.js +10 -8
  86. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  87. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  88. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  89. package/dist/cli-delegate/cmd_settings_get.js +28 -2
  90. package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
  91. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  92. package/dist/cli-delegate/cmd_sync.js +10 -8
  93. package/dist/cli-delegate/cmd_uninstall.js +16 -13
  94. package/dist/cli-delegate/cmd_update.js +1 -1
  95. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  96. package/dist/cli-delegate/cmd_validate.js +12 -10
  97. package/dist/cli-delegate/cmd_versions.js +1 -1
  98. package/dist/discovery/deprecation-report.md +1 -1
  99. package/dist/discovery/discovery-manifest.json +85 -38
  100. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  101. package/dist/discovery/discovery-manifest.summary.md +4 -4
  102. package/dist/discovery/dormancy-report.md +1 -1
  103. package/dist/discovery/orphan-report.md +1 -1
  104. package/dist/discovery/packs.json +10 -8
  105. package/dist/discovery/trust-report.md +3 -3
  106. package/dist/discovery/workspaces.json +8 -6
  107. package/dist/hooks/dispatch.js +1832 -1007
  108. package/dist/install/claudeRuleRewrite.js +14 -3
  109. package/dist/install/conflict.js +60 -10
  110. package/dist/install/conflictTracker.js +218 -0
  111. package/dist/install/fsPrimitives.js +54 -0
  112. package/dist/install/install.mjs +2007 -1481
  113. package/dist/install/manifestFiles.js +259 -0
  114. package/dist/install/preflight.js +14 -2
  115. package/dist/install/preserve.js +157 -0
  116. package/dist/install/recordedOwnership.js +132 -0
  117. package/dist/install/txlog.js +11 -2
  118. package/dist/mcp/registry-manifest.json +3 -3
  119. package/dist/mcp/server.json +2 -2
  120. package/dist/mcp/server.mjs +398 -39
  121. package/dist/router.json +1 -1
  122. package/dist/scripts/_lib/agent_settings.js +3 -2
  123. package/dist/scripts/_lib/fs_atomic.js +167 -0
  124. package/dist/scripts/_lib/installed_tools.js +460 -0
  125. package/dist/scripts/_lib/lean_projection_mode.js +3 -2
  126. package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
  127. package/dist/server/routes/install.js +8 -0
  128. package/dist/server/routes/wizard.js +9 -3
  129. package/dist/server/schemas/settings.js +17 -5
  130. package/dist/shared/missionExecution.js +97 -0
  131. package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
  132. package/dist/ui/index.html +1 -1
  133. package/docs/MIGRATION.md +103 -0
  134. package/docs/contracts/settings-classes.md +26 -7
  135. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  136. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  137. package/docs/guidelines/design-fidelity-routing.md +29 -0
  138. package/package.json +1 -1
  139. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  140. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  141. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  142. package/src/config/agent-settings.template.yml +139 -27
  143. package/src/config/auto-rule-budget.json +16 -0
  144. package/src/config/ci-local-parity.yml +18 -5
  145. package/src/config/continuity-surface.json +21 -0
  146. package/src/config/gate-coverage.yml +146 -13
  147. package/src/config/gate-violation-baselines.json +6 -6
  148. package/src/config/hook-token-budget.json +2 -0
  149. package/src/config/loop-surfaces.yaml +215 -0
  150. package/src/config/platform-anchor.json +3 -2
  151. package/src/config/release-gate-locality.yml +11 -0
  152. package/src/config/routing-coverage-seed.json +18 -4
  153. package/src/config/rule-enforcement-baseline.json +0 -1
  154. package/src/install/claudeRuleRewrite.ts +16 -2
  155. package/src/install/conflict.ts +65 -9
  156. package/src/install/conflictTracker.ts +299 -0
  157. package/src/install/fsPrimitives.ts +54 -0
  158. package/src/install/manifestFiles.ts +312 -0
  159. package/src/install/preflight.ts +16 -2
  160. package/src/install/preserve.ts +205 -0
  161. package/src/install/recordedOwnership.ts +149 -0
  162. package/src/install/txlog.ts +11 -2
  163. package/src/install/types.ts +19 -4
  164. package/src/scripts/_cli/cmd_conformance.ts +88 -15
  165. package/src/scripts/_cli/cmd_doctor.ts +4 -3
  166. package/src/scripts/_cli/cmd_route_explain.ts +9 -2
  167. package/src/scripts/_cli/cmd_settings_get.ts +44 -0
  168. package/src/scripts/_cli/doctor_execution.ts +90 -0
  169. package/src/scripts/_lib/activation_payload.ts +247 -0
  170. package/src/scripts/_lib/agent_settings.ts +4 -2
  171. package/src/scripts/_lib/authority_path.ts +102 -0
  172. package/src/scripts/_lib/cascade_base.ts +94 -0
  173. package/src/scripts/_lib/changelog_eras.ts +7 -2
  174. package/src/scripts/_lib/continuation_ladder.ts +149 -2
  175. package/src/scripts/_lib/council_transport.ts +129 -0
  176. package/src/scripts/_lib/delivery_ready.ts +112 -0
  177. package/src/scripts/_lib/forge_protection.ts +267 -0
  178. package/src/scripts/_lib/hook_platform_keys.ts +27 -0
  179. package/src/scripts/_lib/json_python_parity.ts +127 -0
  180. package/src/scripts/_lib/lean_projection_mode.ts +14 -3
  181. package/src/scripts/_lib/loop_surfaces.ts +382 -0
  182. package/src/scripts/_lib/mission_record.ts +190 -0
  183. package/src/scripts/_lib/obligation_frequency.ts +217 -0
  184. package/src/scripts/_lib/obligations.ts +402 -0
  185. package/src/scripts/_lib/orchestration_record.ts +11 -20
  186. package/src/scripts/_lib/release_highlights.ts +199 -10
  187. package/src/scripts/_lib/release_holds.ts +493 -0
  188. package/src/scripts/_lib/release_migration_gate.ts +184 -0
  189. package/src/scripts/_lib/rides_along.ts +102 -0
  190. package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
  191. package/src/scripts/_lib/test_delta.ts +166 -0
  192. package/src/scripts/_lib/test_provenance.ts +171 -0
  193. package/src/scripts/_lib/typed_op_grant.ts +246 -0
  194. package/src/scripts/_lib/typed_op_watch.ts +185 -0
  195. package/src/scripts/ai_council/config.ts +8 -11
  196. package/src/scripts/ai_council/fallback_config.ts +5 -3
  197. package/src/scripts/ai_council/ownership_classes.ts +118 -0
  198. package/src/scripts/ask_block_census.ts +285 -15
  199. package/src/scripts/check_enforcement_matrix.ts +624 -0
  200. package/src/scripts/check_gate_reachability.ts +173 -10
  201. package/src/scripts/check_generator_sync.ts +954 -0
  202. package/src/scripts/check_preamble_payload_budget.ts +75 -2
  203. package/src/scripts/check_release_highlights.ts +33 -1
  204. package/src/scripts/check_release_holds.ts +567 -0
  205. package/src/scripts/check_rule_projection_integrity.ts +22 -12
  206. package/src/scripts/check_test_delta.ts +294 -0
  207. package/src/scripts/check_test_weakening.ts +299 -0
  208. package/src/scripts/closure_scan.ts +333 -0
  209. package/src/scripts/compile_hook_manifest.ts +43 -3
  210. package/src/scripts/council_record_shape.ts +209 -0
  211. package/src/scripts/hook_manifest.json +1 -1
  212. package/src/scripts/hook_manifest.yaml +88 -9
  213. package/src/scripts/hooks/concern_registry.ts +4 -0
  214. package/src/scripts/hooks/design_pass_hook.ts +128 -3
  215. package/src/scripts/hooks/evidence_independence.ts +96 -0
  216. package/src/scripts/hooks/memory_recall_hook.ts +540 -0
  217. package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
  218. package/src/scripts/hooks/rule_inject_hook.ts +49 -1
  219. package/src/scripts/hooks/run_continuation_hook.ts +17 -20
  220. package/src/scripts/hooks_doctor.ts +85 -0
  221. package/src/scripts/hooks_status.ts +19 -0
  222. package/src/scripts/install.ts +106 -206
  223. package/src/scripts/janitor.ts +7 -0
  224. package/src/scripts/lint_decision_classes.ts +464 -0
  225. package/src/scripts/lint_hook_manifest.ts +11 -0
  226. package/src/scripts/lint_major_migration_sections.ts +450 -0
  227. package/src/scripts/lint_plan_risk_register.ts +47 -3
  228. package/src/scripts/lint_roadmap_blockers.ts +61 -0
  229. package/src/scripts/lint_roadmap_producers.ts +202 -0
  230. package/src/scripts/measure_rule_budget.ts +101 -5
  231. package/src/scripts/new_roadmap.ts +37 -0
  232. package/src/scripts/orchestration_record.ts +0 -5
  233. package/src/scripts/release.ts +14 -98
  234. package/src/scripts/release_drill.ts +267 -1
  235. package/src/scripts/release_publication.ts +51 -0
  236. package/src/scripts/report_enforcement_drift.ts +566 -0
  237. package/src/scripts/report_host_injection_effect.ts +3 -11
  238. package/src/scripts/report_standing_payload_by_host.ts +26 -152
  239. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  240. package/src/scripts/schemas/command.schema.json +4 -0
  241. package/src/scripts/scope_growth.ts +219 -0
  242. package/src/scripts/trigger_eval_grandfather.json +0 -1
  243. package/src/scripts/ui_conformance_probe.ts +622 -0
  244. package/src/server/schemas/settings.ts +31 -7
  245. package/src/shared/missionExecution.ts +126 -0
  246. package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Recorded-ownership classification — road-to-a-conformance-check-that-can-fail
3
+ * Phase 5.
4
+ *
5
+ * The install conflict matrix decided ownership by **path-set membership**:
6
+ * `policy.knownPaths.has(targetPath)`. That answers "did we ever write here",
7
+ * which collapses two materially different trees into one row — a managed file
8
+ * still carrying the bytes we wrote, and a managed file the user has since
9
+ * edited. The second is a data-loss surface and the first is not, and the
10
+ * matrix could not tell them apart because a path set carries no content.
11
+ *
12
+ * This module is the plumbing half: read the per-file SHA-256 the install
13
+ * manifest already records, and compare it against a digest the caller
14
+ * computed. It changes no install behaviour on its own — the matrix wiring
15
+ * lands separately (Phase 5.2), so the behaviour change is reviewable as a
16
+ * diff that contains no hash computation.
17
+ *
18
+ * Why a local YAML read rather than `installed_tools.read_manifest()`: that
19
+ * reader is a hand-rolled parser with no YAML dependency, and it drops nested
20
+ * `files[]` entries by design (a degraded read that ADR-200 documents as
21
+ * intentional). It therefore cannot supply a recorded hash at all. Reading
22
+ * the same file with the `yaml` package — already a runtime dependency — is
23
+ * additive and leaves that reader's byte-exact golden tests untouched.
24
+ *
25
+ * `src/scripts/_lib/install_drift.ts` reads the same field of the same file
26
+ * for the same reason, and this module is a second reader rather than a
27
+ * caller of it because `collect_drift` computes the digests itself and
28
+ * returns only the entries that drifted. The conflict matrix needs the
29
+ * opposite: every recorded digest, and no hashing, because the digest it
30
+ * compares against is one the caller already has. What this module does NOT
31
+ * duplicate is that module's stated invariant — `manifest_path()` stays the
32
+ * one place that decides where the manifest is, so the
33
+ * `AGENT_CONFIG_INSTALLED_TOOLS` override and `~` expansion behave
34
+ * identically in both.
35
+ */
36
+
37
+ import { existsSync, readFileSync } from 'node:fs';
38
+ import { homedir } from 'node:os';
39
+ import { isAbsolute, join, resolve } from 'node:path';
40
+
41
+ import * as YAML from 'yaml';
42
+
43
+ import { manifest_path, type EnvMap } from '../scripts/_lib/installed_tools.js';
44
+
45
+ /**
46
+ * How the install matrix owns a target on disk.
47
+ *
48
+ * - `recorded-unchanged` — the manifest records this path and the bytes on
49
+ * disk still match the digest it recorded. Ours, untouched.
50
+ * - `recorded-modified` — the manifest records this path and the bytes have
51
+ * changed since. Ours, edited by someone. Overwriting loses their work.
52
+ * - `unknown` — the manifest does not record this path, or records
53
+ * it without a digest (bridges carry `sha256: null`), or the bytes could not
54
+ * be read. Not an assertion that the file is foreign — an assertion that
55
+ * this tree cannot say.
56
+ */
57
+ export type RecordedOwnership = 'recorded-unchanged' | 'recorded-modified' | 'unknown';
58
+
59
+ /** Absolute target path → the SHA-256 the manifest recorded (`null` for bridges). */
60
+ export type RecordedHashes = ReadonlyMap<string, string | null>;
61
+
62
+ /** The empty answer — "nothing is recorded", used whenever no manifest resolves. */
63
+ export const NO_RECORDED_HASHES: RecordedHashes = new Map<string, string | null>();
64
+
65
+ /**
66
+ * Classify one target from the recorded digest and the on-disk digest.
67
+ *
68
+ * Pure — the caller supplies both digests. `undefined` for `recordedSha256`
69
+ * means the path is not recorded at all; `null` means recorded without a
70
+ * digest. Both answer `unknown`, because neither supports a comparison.
71
+ */
72
+ export function classifyOwnership(
73
+ recordedSha256: string | null | undefined,
74
+ onDiskSha256: string | null,
75
+ ): RecordedOwnership {
76
+ if (recordedSha256 === undefined || recordedSha256 === null) return 'unknown';
77
+ if (onDiskSha256 === null) return 'unknown';
78
+ return recordedSha256 === onDiskSha256 ? 'recorded-unchanged' : 'recorded-modified';
79
+ }
80
+
81
+ /**
82
+ * Read `tools[].files[]` out of an installed-tools manifest into a
83
+ * path → digest map.
84
+ *
85
+ * Relative paths resolve against `projectRoot` and a leading `~` expands
86
+ * first — the manifest stores user-scope entries absolute or `~`-rooted and
87
+ * project-scope entries relative, and both existing readers
88
+ * (`install_drift.resolve_entry_path`, `cmd_doctor._resolve_path`) resolve
89
+ * them that way. A missing, unreadable or malformed manifest yields the
90
+ * empty map rather than throwing — an install plan must never fail because a
91
+ * lockfile is corrupt.
92
+ */
93
+ export function readRecordedHashes(manifestPath: string, projectRoot: string): RecordedHashes {
94
+ if (!existsSync(manifestPath)) return NO_RECORDED_HASHES;
95
+ let doc: unknown;
96
+ try {
97
+ doc = YAML.parse(readFileSync(manifestPath, 'utf8'));
98
+ } catch {
99
+ return NO_RECORDED_HASHES;
100
+ }
101
+ if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) return NO_RECORDED_HASHES;
102
+ const tools = (doc as Record<string, unknown>)['tools'];
103
+ if (!Array.isArray(tools)) return NO_RECORDED_HASHES;
104
+
105
+ const out = new Map<string, string | null>();
106
+ for (const tool of tools) {
107
+ if (tool === null || typeof tool !== 'object' || Array.isArray(tool)) continue;
108
+ const files = (tool as Record<string, unknown>)['files'];
109
+ if (!Array.isArray(files)) continue;
110
+ for (const entry of files) {
111
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) continue;
112
+ const rec = entry as Record<string, unknown>;
113
+ const raw = rec['path'];
114
+ if (typeof raw !== 'string' || raw.length === 0) continue;
115
+ const sha = rec['sha256'];
116
+ const expanded = expanduser(raw);
117
+ const target = isAbsolute(expanded)
118
+ ? resolve(expanded)
119
+ : resolve(projectRoot, expanded);
120
+ out.set(target, typeof sha === 'string' && sha.length > 0 ? sha : null);
121
+ }
122
+ }
123
+ return out;
124
+ }
125
+
126
+ /** Expand a leading `~`, matching `installed_tools.expanduser`. */
127
+ function expanduser(p: string): string {
128
+ if (p === '~') return homedir();
129
+ if (p.startsWith('~/') || (process.platform === 'win32' && p.startsWith('~\\'))) {
130
+ return join(homedir(), p.slice(2));
131
+ }
132
+ return p;
133
+ }
134
+
135
+ /**
136
+ * {@link readRecordedHashes} for a PROJECT root, via `installed_tools`.
137
+ *
138
+ * `manifest_path` carries the `AGENT_CONFIG_INSTALLED_TOOLS` override, so an
139
+ * install running against a relocated manifest classifies ownership instead
140
+ * of degrading every file to `unknown`.
141
+ *
142
+ * The argument must be a project root. An {@link InstallPlan.root} for a
143
+ * `global` target is the install anchor (`~/.event4u/agent-config/`), which
144
+ * holds no manifest — such a plan gets the empty map and therefore the
145
+ * pre-hash answer, which is correct but is a floor rather than a feature.
146
+ */
147
+ export function recordedHashesForRoot(root: string, env?: EnvMap | null): RecordedHashes {
148
+ return readRecordedHashes(manifest_path(root, env), root);
149
+ }
@@ -2,8 +2,17 @@
2
2
  * Transaction log — Phase A4 (council Finding #18).
3
3
  *
4
4
  * Append-only JSONL at `getLogPath()` (`~/.event4u/agent-config/install-log.jsonl`).
5
- * Each successful atomic write appends one entry; recovery on crash is
6
- * `tail -n` + reverse-apply. No PID lockfile (per Phase-A4 decision 8).
5
+ * Recovery on crash is `tail -n` plus the wizard's resume / dismiss decision —
6
+ * nothing is un-written automatically. No PID lockfile (per Phase-A4
7
+ * decision 8).
8
+ *
9
+ * WHAT ACTUALLY WRITES THIS FILE, as of 2026-09-15: two call sites.
10
+ * `src/server/routes/install.ts`'s recovery-dismiss handler appends a
11
+ * `rollback` marker with an empty path and a null hash; `src/scripts/install.ts`
12
+ * — the surviving headless writer, per-file, through `_log_tx_entry` — appends
13
+ * a `write` or `skip` entry per copied/skipped path during
14
+ * `_copy_dir_dereferencing_symlinks`. Both call `appendTxLog` directly, so the
15
+ * entry shape is identical by construction rather than by convention.
7
16
  *
8
17
  * Rotation: 10 MB OR 30 days, whichever first. Rotated copies move to
9
18
  * `install-log.<ISO>.jsonl.gz` siblings; recovery never scans them.
@@ -16,6 +16,9 @@
16
16
  * - v2 manifest `files_by_tool` → {@link InstallPlan.filesByTool}
17
17
  */
18
18
 
19
+ // Type-only — erased at compile time, so this module stays runtime-free.
20
+ import type { RecordedOwnership } from './recordedOwnership.js';
21
+
19
22
  /**
20
23
  * Where the install lands.
21
24
  *
@@ -130,9 +133,11 @@ export interface InstallPlan {
130
133
  * Phase B3 — surfaced by the `/api/v1/install/plan` route so the wizard's
131
134
  * conflict screen can render single-pick / batch-resolution UI **before**
132
135
  * the apply phase opens the transaction log. A `ConflictEntry` means the
133
- * target exists, its bytes do not match the planned SHA, it is not in
134
- * `policy.knownPaths`, and `policy.force` is false — i.e. the policy
135
- * would surface this file to the UI during apply.
136
+ * target exists, its bytes do not match the planned SHA, `policy.force` is
137
+ * false, and the planner does not hold it as ours-and-unchanged — i.e. the
138
+ * policy would surface this file to the UI. Since `ownership` landed it is
139
+ * NO LONGER equivalent to "not in `policy.knownPaths`": a `recorded-modified`
140
+ * entry is emitted precisely when the path IS in that set.
136
141
  *
137
142
  * `mergeable` is `true` only for `.json` deployed files; the wizard
138
143
  * shows the per-row `merge` button only on those. `existingSha256` is
@@ -145,6 +150,15 @@ export interface ConflictEntry {
145
150
  readonly plannedSha256: string | null;
146
151
  readonly existingSha256: string | null;
147
152
  readonly mergeable: boolean;
153
+ /**
154
+ * Three-state ownership from the manifest's recorded digest — the column
155
+ * path-set membership could not produce (Phase 5.1 of
156
+ * road-to-a-conformance-check-that-can-fail). `recorded-modified` is the
157
+ * row that matters: a file we wrote that someone has since edited.
158
+ * `unknown` reproduces the pre-hash answer and is what every tree without
159
+ * a readable manifest reports.
160
+ */
161
+ readonly ownership: RecordedOwnership;
148
162
  }
149
163
 
150
164
  /**
@@ -164,7 +178,8 @@ export type ConflictResolution = 'skip' | 'overwrite' | 'merge';
164
178
  * Result of applying an {@link InstallPlan}.
165
179
  *
166
180
  * Surfaced to the wizard's progress bar (Phase B1) and recorded in the
167
- * transaction log (Phase A4) so recovery can reverse-apply on crash.
181
+ * transaction log (Phase A4) so a crashed run's tail can be surfaced to
182
+ * the wizard's recovery screen.
168
183
  */
169
184
  export interface ApplyResult {
170
185
  readonly target: InstallTarget;
@@ -6,7 +6,9 @@
6
6
  * consumer-facing checks that answer ONE question deterministically:
7
7
  * "is agent-config installed AND firing in this repo?"
8
8
  *
9
- * (a) txlog-clean — install log tail carries no abandoned run
9
+ * (a) txlog-clean — install log tail carries no abandoned run;
10
+ * an absent log on an installed tree is `unknown`,
11
+ * not green (see `_check_txlog_clean`)
10
12
  * (b) router-pointers — dist/router.json ids + routes_to resolve on disk
11
13
  * (c) hook-dispatcher — dispatcher answers synthetic session_start/stop
12
14
  * (d) lean-projection — lean_projection.mode matches projected artifacts
@@ -75,6 +77,21 @@ export const CONFORMANCE_CHECK_IDS = [
75
77
  /** Marker emitted by `project_thin_rules.thin_entry()` — the thin-stub signature. */
76
78
  export const THIN_STUB_MARKER = 'Routed rule — load the body on trigger-match';
77
79
 
80
+ /**
81
+ * Rendering symbols for this verb's rows — doctor's table plus `unknown`.
82
+ *
83
+ * `unknown` is a conformance concept: the check ran and could not answer.
84
+ * Distinct from `skipped` ("not applicable here") and from `ok` ("answered,
85
+ * green"), and never folded into the exit code, which keys off `fail` alone.
86
+ * It lives here rather than in `cmd_doctor`'s table because doctor emits no
87
+ * such row — and because `cmd_doctor.ts` is already over the source-size
88
+ * ceiling, so a line added there is a ratchet violation.
89
+ */
90
+ const CONFORMANCE_STATUS_SYMBOLS: Record<string, string> = {
91
+ ...STATUS_SYMBOLS,
92
+ unknown: '❔',
93
+ };
94
+
78
95
  function print(s = ''): void {
79
96
  process.stdout.write(`${s}\n`);
80
97
  }
@@ -105,21 +122,75 @@ export function installLogPath(): string {
105
122
  // Check (a) — txlog tail clean.
106
123
  // ---------------------------------------------------------------------------
107
124
 
108
- export function _check_txlog_clean(logPath: string = installLogPath()): Dict {
109
- if (!fs.existsSync(logPath)) {
110
- return {
111
- id: 'txlog-clean',
112
- status: 'ok',
113
- message: 'no install transaction log — nothing to recover',
114
- remedy: '',
115
- };
116
- }
117
- const entries = readRecentEntries(logPath);
125
+ /**
126
+ * True when this tree carries an install manifest — i.e. something was
127
+ * installed here, so an install transaction log was expected.
128
+ *
129
+ * The discriminator for the two absent-log cases below. A tree with no
130
+ * manifest was never installed and has genuinely nothing to recover; a
131
+ * tree WITH a manifest recorded an install and no log for it, which is
132
+ * not the same answer and must not be reported as the same colour.
133
+ */
134
+ export function _installLogExpected(projectRoot: string): boolean {
135
+ return fs.existsSync(installed_tools.manifest_path(projectRoot));
136
+ }
137
+
138
+ /**
139
+ * Check (a) — the install transaction log tail carries no abandoned run.
140
+ *
141
+ * A log with nothing usable in it is two answers, not one
142
+ * (road-to-a-conformance-check-that-can-fail Phase 1). Until this split the
143
+ * branch returned `ok` unconditionally, so every install satisfied the check
144
+ * by having produced nothing.
145
+ *
146
+ * - nothing usable, no manifest → `ok` (nothing was installed here)
147
+ * - nothing usable, manifest → `unknown` (an install recorded nothing)
148
+ * - usable entries → `ok` / `fail` on the tail, as before
149
+ *
150
+ * `unknown` is deliberately NOT `fail`: an install that predates the log, or
151
+ * one written by a path that does not log, is an unanswered question rather
152
+ * than a broken install, and reddening every such consumer at upgrade is the
153
+ * risk this phase's shape exists to avoid. The exit contract keys off `fail`
154
+ * only, so `unknown` leaves exit codes untouched — including the verdict
155
+ * banner, where the per-row symbol is the only signal.
156
+ *
157
+ * Reach, updated 2026-09-15 — Phase 3 landed: the headless installer
158
+ * (`src/scripts/install.ts`, `_copy_dir_dereferencing_symlinks`) now appends
159
+ * a `write` or `skip` entry per copied/skipped path, through the same
160
+ * `appendTxLog` module the recovery-dismiss route calls. So a headless
161
+ * install now leaves the `unknown` branch above and reads `ok` on its own
162
+ * evidence rather than on the absence of any. `unknown` is still the answer
163
+ * for an install that predates this change, or for the wizard's browser
164
+ * path, which does not call the headless copy function; `fail` is still
165
+ * unreachable because nothing writes an `abort` yet.
166
+ */
167
+ export function _check_txlog_clean(
168
+ logPath: string = installLogPath(),
169
+ projectRoot: string | null = null,
170
+ ): Dict {
171
+ const entries = fs.existsSync(logPath) ? readRecentEntries(logPath) : [];
118
172
  if (entries.length === 0) {
173
+ // One branch for all three no-usable-entries shapes — absent, empty,
174
+ // and unreadable. Splitting them let a zero-byte or fully-corrupt log
175
+ // reach `ok` on a tree the absent-log branch would have called
176
+ // `unknown`: `readRecentEntries` drops malformed lines silently, so
177
+ // "the file exists" is not evidence that anything recorded anything.
178
+ if (projectRoot !== null && _installLogExpected(projectRoot)) {
179
+ return {
180
+ id: 'txlog-clean',
181
+ status: 'unknown',
182
+ message:
183
+ 'install manifest present, no usable install transaction log — ' +
184
+ 'nothing recorded this install, so the last run cannot be confirmed complete',
185
+ remedy:
186
+ 'no install path writes this log today; ' +
187
+ 'run `agent-config doctor` for the drift answer this check cannot give',
188
+ };
189
+ }
119
190
  return {
120
191
  id: 'txlog-clean',
121
192
  status: 'ok',
122
- message: 'install transaction log is empty',
193
+ message: 'no usable install transaction log and no install manifest — nothing was installed here',
123
194
  remedy: '',
124
195
  };
125
196
  }
@@ -131,7 +202,9 @@ export function _check_txlog_clean(logPath: string = installLogPath()): Dict {
131
202
  message:
132
203
  `install log tail is an abandoned run (abort at ${last.ts}` +
133
204
  `${last.note ? `: ${last.note}` : ''})`,
134
- remedy: 're-run `agent-config init` (recovery reverse-applies the aborted tail)',
205
+ remedy:
206
+ 're-run `agent-config init` — it re-applies the plan over the partial tail; ' +
207
+ "the aborted run's writes stay on disk and are replaced only where the plan covers them",
135
208
  };
136
209
  }
137
210
  return {
@@ -492,7 +565,7 @@ export function runConformanceChecks(opts: ConformanceRunOptions = {}): Dict[] {
492
565
  const projectRoot = opts.projectRoot ?? process.cwd();
493
566
  const packageRoot = opts.packageRoot ?? PACKAGE_ROOT;
494
567
  const checks: Dict[] = [];
495
- checks.push(_check_txlog_clean());
568
+ checks.push(_check_txlog_clean(installLogPath(), projectRoot));
496
569
  checks.push(_check_router_pointers(packageRoot));
497
570
  if (opts.skipDispatcher === true) {
498
571
  checks.push({
@@ -653,7 +726,7 @@ function main(argv: string[] | null = null): number {
653
726
  print(` 📍 project_root: ${project_root}`);
654
727
  print('conformance:');
655
728
  for (const c of allChecks) {
656
- const sym = STATUS_SYMBOLS[c['status'] as string] ?? '?';
729
+ const sym = CONFORMANCE_STATUS_SYMBOLS[c['status'] as string] ?? '?';
657
730
  print(` ${sym} ${c['id']}: ${c['message']}`);
658
731
  if (c['status'] !== 'ok' && c['remedy']) {
659
732
  print(` fix: ${c['remedy']}`);
@@ -114,6 +114,7 @@ import {
114
114
  resolve_project_root,
115
115
  type TraceRecord,
116
116
  } from '../_lib/agent_settings.js';
117
+ import { executionJson, forgeProtectionJson, UNREAD_FORGE } from './doctor_execution.js';
117
118
  import * as ai_council_clients from '../ai_council/clients.js';
118
119
  import * as ai_council_config from '../ai_council/config.js';
119
120
  import {
@@ -3074,9 +3075,9 @@ function _emit_json(
3074
3075
  foreign,
3075
3076
  tag_drift,
3076
3077
  };
3077
- if (origin !== null) {
3078
- payload['project_root_origin'] = origin;
3079
- }
3078
+ if (origin !== null) payload['project_root_origin'] = origin;
3079
+ payload['execution'] = executionJson(() => iter_setting_overrides({ cwd: project_root }));
3080
+ payload['forge_protection'] = forgeProtectionJson(UNREAD_FORGE); // 3.2
3080
3081
  if (checks !== null) {
3081
3082
  payload['checks'] = checks;
3082
3083
  const drift = missing.length + modified.length + foreign.length + tag_drift.length;
@@ -78,7 +78,14 @@ function _trigger_label(trigger: Trigger): string {
78
78
  return `trigger: ${JSON.stringify(trigger)}`;
79
79
  }
80
80
 
81
- /** Projection mode from settings; the shipped default is eager-all. */
81
+ /**
82
+ * Projection mode from settings.
83
+ *
84
+ * The shipped template carries `delivery` for `claude-code` (ADR-267), so on a
85
+ * readable settings file that is what comes back. The `eager-all` below is the
86
+ * parser fallback for an absent or unreadable value, matching
87
+ * `DEFAULT_LEAN_PROJECTION_MODE` — it is not what the template ships.
88
+ */
82
89
  export function _projection_mode(): string {
83
90
  try {
84
91
  const settings = load_agent_settings({ cwd: REPO_ROOT }) as Record<string, unknown>;
@@ -90,7 +97,7 @@ export function _projection_mode(): string {
90
97
  }
91
98
  }
92
99
  } catch {
93
- // settings unreadable → shipped default
100
+ // settings unreadable → parser fallback, not the template value
94
101
  }
95
102
  return 'eager-all';
96
103
  }
@@ -33,6 +33,10 @@
33
33
  * `2` usage error.
34
34
  */
35
35
 
36
+ import {
37
+ resolveLocalAutoRun,
38
+ type AutoRunResolution,
39
+ } from '../../shared/missionExecution.js';
36
40
  import * as fs from 'node:fs';
37
41
  import * as path from 'node:path';
38
42
  import process from 'node:process';
@@ -142,6 +146,31 @@ export function carveOutFor(key: string): CarveOutKey | undefined {
142
146
  return SETTINGS_CARVE_OUT.find((c) => c.key === key);
143
147
  }
144
148
 
149
+ /**
150
+ * The mission-time resolution of `key`, or `null` when a mission changes nothing.
151
+ *
152
+ * Exactly one key resolves differently inside a mission today, and the function
153
+ * is keyed on that rather than generalised: a second mission-sensitive key would
154
+ * need its own argument about WHY the mission condition applies to it, and a
155
+ * generic table would let one land without that argument.
156
+ *
157
+ * `merged` is the already-loaded settings tree — no second cascade walk, for the
158
+ * same reason `layersFor` delegates rather than re-walking.
159
+ */
160
+ export function missionResolutionFor(
161
+ key: string,
162
+ merged: unknown,
163
+ ): AutoRunResolution | null {
164
+ if (key !== 'quality.local_auto_run') return null;
165
+ const configured = getSettingsLeaf(merged, 'quality.local_auto_run');
166
+ const missionValue = getSettingsLeaf(merged, 'quality.local_auto_run_in_mission');
167
+ return resolveLocalAutoRun({
168
+ configured: configured === true,
169
+ missionValue: typeof missionValue === 'boolean' ? missionValue : undefined,
170
+ inMission: true,
171
+ });
172
+ }
173
+
145
174
  /**
146
175
  * `true` when the key is present in the user-global file but is NOT whitelisted,
147
176
  * i.e. the loader read it and threw it away.
@@ -206,6 +235,7 @@ export function runSettingsGet(opts: SettingsGetOptions): SettingsGetResult {
206
235
  const fallback = templateDefault(opts.packageRoot, opts.key);
207
236
  const carveOut = carveOutFor(opts.key);
208
237
  const drop = userGlobalDrop(opts.key);
238
+ const mission = missionResolutionFor(opts.key, merged);
209
239
 
210
240
  if (opts.json) {
211
241
  out.push(
@@ -223,6 +253,9 @@ export function runSettingsGet(opts: SettingsGetOptions): SettingsGetResult {
223
253
  ? { resolves_to: carveOut.absentResolvesTo, reader: carveOut.reader }
224
254
  : null,
225
255
  user_global_dropped: drop.dropped ? drop.file : null,
256
+ in_mission: mission === null
257
+ ? null
258
+ : { value: mission.value, decided_by: mission.origin },
226
259
  },
227
260
  null,
228
261
  2,
@@ -263,6 +296,17 @@ export function runSettingsGet(opts: SettingsGetOptions): SettingsGetResult {
263
296
  );
264
297
  }
265
298
 
299
+ if (mission !== null) {
300
+ out.push(
301
+ '',
302
+ `ℹ️ Inside a mission this key resolves to: ${JSON.stringify(mission.value)}`,
303
+ ` Decided by: ${mission.origin === 'mission' ? 'quality.local_auto_run_in_mission' : 'the settings layers above'}`,
304
+ ' A mission is an autonomous roadmap run with a claimed contract. The',
305
+ ' resolution is a runtime condition, never a layer, so no file above',
306
+ ' carries the mission value — see src/shared/missionExecution.ts.',
307
+ );
308
+ }
309
+
266
310
  if (drop.dropped) {
267
311
  out.push(
268
312
  '',
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The `execution` block of `agent-config doctor --json`.
3
+ *
4
+ * `road-to-adversarial-verification-and-long-runs` Phase 0.3. Its own module
5
+ * rather than a function inside `cmd_doctor.ts` for the reason
6
+ * `_lib/continuation_ladder.ts` states for itself: `check_source_size_budget` is
7
+ * a shrink-only ratchet and `cmd_doctor.ts` sits far past the 1,500-line cap, so
8
+ * every line added there is an added violation. A phase pays for its additions by
9
+ * putting new code in a file under the cap rather than by raising a baseline.
10
+ *
11
+ * `owner_owned_check` appears here as the ladder's last RUNG rather than as a
12
+ * separate field, because it is not a separate switch: it is the rung that
13
+ * decides whether the residue is owner-owned at all, and lifting it out would
14
+ * suggest it can be toggled independently of the order it sits in.
15
+ */
16
+
17
+ import {
18
+ executionPostureFromOverrides,
19
+ resolveExecutionPosture,
20
+ } from '../../shared/missionExecution.js';
21
+ import {
22
+ forgeProtectionRows,
23
+ protectionActions,
24
+ type ForgeReading,
25
+ } from '../_lib/forge_protection.js';
26
+
27
+ type Dict = Record<string, unknown>;
28
+
29
+ /** A settings-override stream, in the shape `iter_setting_overrides` yields. */
30
+ export type OverrideStream = Iterable<readonly [string, unknown, string]>;
31
+
32
+ /**
33
+ * Build the payload block from a settings-override stream.
34
+ *
35
+ * A stream that throws yields the defaults rather than propagating — a
36
+ * diagnostic that cannot report because its own input is malformed is the one
37
+ * shape `doctor` must never take.
38
+ */
39
+ export function executionJson(overrides: () => OverrideStream): Dict {
40
+ let posture;
41
+ try {
42
+ posture = executionPostureFromOverrides(overrides());
43
+ } catch {
44
+ posture = resolveExecutionPosture({});
45
+ }
46
+ return {
47
+ fix_loop_max: posture.fix_loop_max,
48
+ escalation: [...posture.escalation],
49
+ };
50
+ }
51
+
52
+ /**
53
+ * The `forge_protection` block of `doctor --json` — Phase 3.2.
54
+ *
55
+ * **`doctor` does NOT reach the network on its own.** A diagnostic that makes a
56
+ * `gh` call on every invocation is one nobody runs offline, and the sibling
57
+ * anchor gate already declined that cost for the same reason. So the reading is
58
+ * INJECTED: a caller that has queried the forge passes it, and a caller that has
59
+ * not passes the all-null reading, which produces five `unread` rows that still
60
+ * name the call each value would come from.
61
+ *
62
+ * That is the honest shape for *read, never guessed*: the block is always
63
+ * present, a value appears only when a named call produced it, and `unread` is a
64
+ * third state rather than a quiet `false`. A row that was never looked at and a
65
+ * row that was looked at and failed are different repairs.
66
+ *
67
+ * `actions` carries the human ACTION lines — the step's own words are that a
68
+ * missing row is a blocker entry and NOT a halt, so this reports and returns.
69
+ */
70
+ export function forgeProtectionJson(reading: ForgeReading): Dict {
71
+ const rows = forgeProtectionRows(reading);
72
+ return {
73
+ rows: rows.map((r) => ({
74
+ id: r.id,
75
+ state: r.state,
76
+ source: r.source,
77
+ detail: r.detail,
78
+ })),
79
+ actions: protectionActions(rows),
80
+ read_from_forge: rows.some((r) => r.state !== 'unread'),
81
+ };
82
+ }
83
+
84
+ /** The all-null reading: nothing was queried. */
85
+ export const UNREAD_FORGE: ForgeReading = {
86
+ rulesets: null,
87
+ defaultBranch: null,
88
+ allowAutoMerge: null,
89
+ deployRestricted: null,
90
+ };