@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
@@ -109,8 +109,8 @@ export function _withPreservedKeys(rendered, original) {
109
109
  * extraction and never up through a baseline raise. So the caller gets one call
110
110
  * and a list of strings.
111
111
  */
112
- export function rewriteAndReport(rulesDir, quiet, info, warn, wrappers) {
113
- const result = rewriteClaudeRules(rulesDir);
112
+ export function rewriteAndReport(rulesDir, quiet, info, warn, wrappers, preserved = new Set()) {
113
+ const result = rewriteClaudeRules(rulesDir, preserved);
114
114
  // The flat-command wrapper report rides along rather than sitting at the
115
115
  // call site: `install.ts` is thousands of lines past
116
116
  // `check_source_size_budget`'s ceiling, and this module is the one place
@@ -188,9 +188,18 @@ export function renderClaudeRule(sourceText) {
188
188
  * detected and skipped rather than re-run — re-running is the silent-widening
189
189
  * failure this module exists to fix, applied to its own output.
190
190
  *
191
+ * `preserved` names destinations the conflict tracker kept because the user had
192
+ * edited them. They are skipped outright. This pass re-renders frontmatter and
193
+ * re-adds only `package:` / `source_path:`, so any key the user added is
194
+ * dropped — and every `always` rule and every mixed-trigger rule deploys
195
+ * without a `paths:` block, so the already-host-form guard above lets them
196
+ * straight through. Without this set, the same run that reported a file
197
+ * preserved went on to rewrite it, and the edit survived in the body while
198
+ * disappearing from the frontmatter.
199
+ *
191
200
  * Returns counts rather than printing. The caller owns the reporting surface.
192
201
  */
193
- export function rewriteClaudeRules(rulesDir) {
202
+ export function rewriteClaudeRules(rulesDir, preserved = new Set()) {
194
203
  const result = { rewritten: 0, scoped: [], dropped: [], failed: [] };
195
204
  let entries;
196
205
  try {
@@ -201,6 +210,8 @@ export function rewriteClaudeRules(rulesDir) {
201
210
  }
202
211
  for (const name of entries.filter((n) => n.endsWith('.md')).sort()) {
203
212
  const full = path.join(rulesDir, name);
213
+ if (preserved.has(path.resolve(full)))
214
+ continue;
204
215
  let stat;
205
216
  try {
206
217
  stat = fs.lstatSync(full);
@@ -16,13 +16,14 @@
16
16
  */
17
17
  import { existsSync } from 'node:fs';
18
18
  import { sha256File } from './plan.js';
19
+ import { classifyOwnership, recordedHashesForRoot, } from './recordedOwnership.js';
19
20
  /**
20
21
  * Decide what to do with a single planned target.
21
22
  *
22
23
  * Decision matrix (mirrors the Python legacy with the 3-option prompt
23
24
  * collapsed into `surface`):
24
25
  *
25
- * | Exists? | Idempotent? | Known? | Force? | → Outcome |
26
+ * | Exists? | Idempotent? | Known? | Force? | → Outcome | code-comment-allow report-comment -- pre-existing decision matrix; this file entered the gate's changed-file scope for an unrelated edit
26
27
  * |---------|-------------|--------|--------|------------|
27
28
  * | no | — | — | — | `write` |
28
29
  * | yes | yes | — | — | `skip` |
@@ -31,9 +32,23 @@ import { sha256File } from './plan.js';
31
32
  * | yes | no | no | yes | `write` |
32
33
  * | yes | no | no | no | `surface` |
33
34
  *
34
- * Headless callers (B1 CLI) collapse `surface` to `skip` automatically
35
- * because there is no UI to defer to; the apply layer records the entry
36
- * under {@link ApplyResult.conflicts} so the wizard can pick it up next.
35
+ * Deliberately NOT extended with the recorded-digest ownership that
36
+ * {@link computeConflicts} consumes. This function has no caller in `src/` —
37
+ * the only references are its own definition and the `{@link}` above — so an
38
+ * `ownership` input would be a parameter nobody supplies, on a decision
39
+ * nobody reads. The ownership split lives where it is live: in the report.
40
+ *
41
+ * What this resolver decides is in any case NOT what the installer does. The
42
+ * single writer is `src/scripts/install.ts`, whose conflict resolver
43
+ * (`src/install/conflictTracker.ts`) reads nothing from this module; `skip`
44
+ * here means "the planner would not touch it", never "your edit is safe".
45
+ *
46
+ * Since the owner ruling of 2026-09-21 that writer no longer overwrites
47
+ * unconditionally — a managed file diverging from its recorded digest is
48
+ * preserved and the package content staged beside it (`src/install/preserve.ts`).
49
+ * The two still reach that answer independently: this module decides from a
50
+ * plan, the writer re-hashes the destination at the moment it is about to
51
+ * mutate it. A `skip` here remains a statement about the plan.
37
52
  */
38
53
  export function resolveFileConflict(inputs) {
39
54
  const { targetPath, idempotent, exists, policy } = inputs;
@@ -121,14 +136,38 @@ export const CONFLICT_BATCH_THRESHOLD = 5;
121
136
  *
122
137
  * Pure-ish — reads from the filesystem only to compute idempotency
123
138
  * (`existsSync` + `sha256File`). Skips bridges (`sha256 === null`) since
124
- * bridge writers own a separate idempotency model. Skips entries whose
125
- * `path` is in `policy.knownPaths`, and skips entries that are
126
- * byte-equal to the planned content. Returns an empty array when
139
+ * bridge writers own a separate idempotency model, and skips entries that
140
+ * are byte-equal to the planned content. Returns an empty array when
127
141
  * `policy.force` is true — overwrite mode silences the screen.
142
+ *
143
+ * Ownership (Phase 5.1) replaces the blanket `policy.knownPaths` skip:
144
+ *
145
+ * - `recorded-unchanged` — ours and untouched. Skipped, and now for a reason
146
+ * that survives a package upgrade: the planned bytes differing from the
147
+ * recorded bytes is an upgrade, not a collision. This is a REDUCTION in
148
+ * what a manifest-carrying tree reports — `cmd_preflight` builds an empty
149
+ * `knownPaths`, so every such file used to be a finding — and it is the
150
+ * intended direction: a routine upgrade is not a conflict.
151
+ * - `recorded-modified` — ours and edited since. **Reported**, where before
152
+ * it was silently dropped whenever the path sat in `knownPaths`. Reported
153
+ * is all it is: the installer does not consult this matrix, so being named
154
+ * here is not protection from the next deploy.
155
+ * - `unknown` — no digest recorded (no manifest, an unreadable
156
+ * one, or a bridge entry). Falls back to `policy.knownPaths`, so a tree
157
+ * with no manifest reports exactly what it reported before. A tree WITH a
158
+ * manifest reports differently in both directions, by design: it gains the
159
+ * modified rows above and loses the unchanged ones below.
160
+ *
161
+ * `recorded` is injectable for tests; by default it is read from the
162
+ * manifest at `plan.root`.
128
163
  */
129
- export function computeConflicts(plan) {
164
+ export function computeConflicts(plan, recordedOverride) {
165
+ // Force returns before the manifest is read. As a default parameter the
166
+ // map was built at call entry and discarded on the next line, so every
167
+ // force-mode plan paid a read and a YAML parse for nothing.
130
168
  if (plan.policy.force)
131
169
  return [];
170
+ const recorded = recordedOverride ?? recordedHashesForRoot(plan.root);
132
171
  const out = [];
133
172
  for (const entries of Object.values(plan.filesByTool)) {
134
173
  for (const entry of entries) {
@@ -138,9 +177,19 @@ export function computeConflicts(plan) {
138
177
  continue;
139
178
  if (!existsSync(entry.path))
140
179
  continue;
141
- if (plan.policy.knownPaths.has(entry.path))
142
- continue;
180
+ // The on-disk digest is computed BEFORE the ownership decision so
181
+ // that decision can consume it. Behaviour-identical to computing it
182
+ // after the `knownPaths` test — the remaining guards are unchanged.
183
+ // The added cost is one digest per EXISTING path in
184
+ // `policy.knownPaths`, the set the removed short-circuit used to
185
+ // skip; everything else was already being hashed. That set is empty
186
+ // for `cmd_preflight` and caller-supplied for the plan route.
143
187
  const onDisk = sha256File(entry.path);
188
+ const ownership = classifyOwnership(recorded.get(entry.path), onDisk);
189
+ if (ownership === 'recorded-unchanged')
190
+ continue;
191
+ if (ownership === 'unknown' && plan.policy.knownPaths.has(entry.path))
192
+ continue;
144
193
  if (onDisk === entry.sha256)
145
194
  continue;
146
195
  out.push({
@@ -149,6 +198,7 @@ export function computeConflicts(plan) {
149
198
  plannedSha256: entry.sha256,
150
199
  existingSha256: onDisk,
151
200
  mergeable: isJsonTarget(entry),
201
+ ownership,
152
202
  });
153
203
  }
154
204
  }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Per-run conflict tracking for `agent-config init` — the stateful half of the
3
+ * owner ruling of 2026-09-21 (road-to-a-conformance-check-that-can-fail
4
+ * Phase 5.1). The decision itself is pure and lives in `./preserve.ts`; this
5
+ * module holds the per-run state that decision needs, stages the sidecar, and
6
+ * folds the result into the run's exit code.
7
+ *
8
+ * It sits beside the decision rather than inside `src/scripts/install.ts`
9
+ * because that file is the largest source this package ships and
10
+ * `check_source_size_budget` ratchets exactly that number. A feature's state
11
+ * machine is paid for here, where it is also the coherent unit: everything in
12
+ * this file is about one question — may the writer replace this managed file,
13
+ * and what does it owe the operator when it may not.
14
+ *
15
+ * The installer's reporting surface (`warn`, `fail`, the transaction log, the
16
+ * NDJSON progress stream) is injected rather than imported: importing it would
17
+ * be a cycle, and re-implementing it would give one install run two voices.
18
+ */
19
+ import * as crypto from 'node:crypto';
20
+ import * as fs from 'node:fs';
21
+ import * as path from 'node:path';
22
+ import { mkdirp, pathExists, resolvePath, sha256OfFile } from './fsPrimitives.js';
23
+ import { EXIT_COMPLETED_WITH_CONFLICTS, conflictSummaryMessage, decideDeployWrite, foreignSidecarMessage, preservedFileMessage, sidecarPathFor, } from './preserve.js';
24
+ import { recordedHashesForRoot } from './recordedOwnership.js';
25
+ /**
26
+ * Build a tracker bound to one installer's reporting surface.
27
+ *
28
+ * A factory rather than module-level state so the reporting surface is injected
29
+ * rather than imported. It is NOT what keeps two installs in one process apart:
30
+ * `src/scripts/install.ts` calls this once at module scope, so both runs share
31
+ * the instance and therefore the `preserved` list. `begin()` is what scopes the
32
+ * state to a run, and it is the only thing that does.
33
+ */
34
+ export function createConflictTracker(host) {
35
+ const conflictState = {
36
+ root: null,
37
+ recorded: null,
38
+ preserved: [],
39
+ staged: new Map(),
40
+ };
41
+ /**
42
+ * Recorded digest for `target`, or `undefined` when this tree cannot say.
43
+ *
44
+ * Two keys are tried, and the plain `path.resolve` one comes FIRST because
45
+ * it is the form that actually matches: `_file_entry` records the path
46
+ * verbatim as the copy loop built it, and `readRecordedHashes` re-resolves
47
+ * with `path.resolve` and no realpath. Looking up only the realpath would
48
+ * miss every entry on macOS, where the temp and home trees sit behind the
49
+ * realpath symlink that rewrites a `/var` prefix onto its private twin —
50
+ * the feature would have been inert on the platform it was developed on.
51
+ * (Spelled out rather than quoted: `check_bundle_path_leakage` scans the
52
+ * tracked bundles this file is compiled into, and the literal form is a
53
+ * build-machine path leak there.) The realpath is kept as a second key so
54
+ * a manifest written through a symlinked deploy root still resolves.
55
+ */
56
+ function recordedHashFor(target) {
57
+ if (conflictState.root === null)
58
+ return undefined;
59
+ conflictState.recorded ??= recordedHashesForRoot(conflictState.root);
60
+ const plain = path.resolve(target);
61
+ if (conflictState.recorded.has(plain))
62
+ return conflictState.recorded.get(plain);
63
+ const real = resolvePath(target);
64
+ return real === plain ? undefined : conflictState.recorded.get(real);
65
+ }
66
+ /**
67
+ * Produce the bytes this run would have written, then reconcile them with
68
+ * whatever already sits at the sidecar path.
69
+ *
70
+ * The candidate is built into a temp file FIRST so the comparison is against
71
+ * the bytes that would actually be staged rather than against the raw
72
+ * package source — those differ, because a deployed file is the package
73
+ * bytes plus the install-time `package:` tag, and comparing the untagged
74
+ * source would call every already-staged sidecar stale and rewrite it on
75
+ * every run.
76
+ *
77
+ * The temp name ends in the TARGET's extension because `_inject_package_tag`
78
+ * keys on it: a sidecar is `<name>.md.agent-config.new`, whose extension is
79
+ * `.new`, so tagging it in place would silently do nothing.
80
+ */
81
+ function stageInto(target, sidecar, produce) {
82
+ mkdirp(path.dirname(sidecar));
83
+ const tmp = path.join(path.dirname(sidecar), `.${path.basename(sidecar)}.${process.pid}.${crypto.randomBytes(6).toString('hex')}${path.extname(target)}`);
84
+ const discard = () => {
85
+ try {
86
+ fs.unlinkSync(tmp);
87
+ }
88
+ catch {
89
+ /* swallow — best-effort cleanup of our own temp file */
90
+ }
91
+ };
92
+ let candidate;
93
+ try {
94
+ produce(tmp);
95
+ candidate = sha256OfFile(tmp);
96
+ }
97
+ catch (exc) {
98
+ discard();
99
+ host.fail(`Could not stage package content for ${target} at ${sidecar}: ${String(exc)}. ` +
100
+ 'Nothing was written; the managed file is unchanged.');
101
+ }
102
+ if (pathExists(sidecar)) {
103
+ const onDisk = sha256OfFile(sidecar);
104
+ // Three outcomes, and only the third is a refusal. Identical bytes:
105
+ // already staged, so the file is left alone. Bytes this tree
106
+ // RECORDED staging: an earlier run staged them against an older
107
+ // package version and the user has not merged them yet — the
108
+ // staging workflow behaving as designed, so the stale copy is
109
+ // refreshed. Anything else is unattributable and the run stops.
110
+ //
111
+ // Ownership is decided by the recorded digest, never by whether the
112
+ // bytes match the CURRENT package: that test called every unmerged
113
+ // sidecar foreign the moment a new version shipped, and aborted the
114
+ // whole install saying it could not show it wrote a file it had
115
+ // written one version earlier.
116
+ if (onDisk !== null && onDisk === candidate) {
117
+ discard();
118
+ conflictState.staged.set(sidecar, onDisk);
119
+ return;
120
+ }
121
+ if (onDisk === null || onDisk !== recordedHashFor(sidecar)) {
122
+ discard();
123
+ host.fail(foreignSidecarMessage(target, sidecar));
124
+ }
125
+ }
126
+ try {
127
+ fs.renameSync(tmp, sidecar);
128
+ }
129
+ catch (exc) {
130
+ discard();
131
+ host.fail(`Could not stage package content for ${target} at ${sidecar}: ${String(exc)}. ` +
132
+ 'Nothing was written; the managed file is unchanged.');
133
+ }
134
+ conflictState.staged.set(sidecar, candidate);
135
+ host.logWrite(sidecar);
136
+ }
137
+ /** Record and report one preserved target, once per destination. */
138
+ function record(target, sidecar) {
139
+ // Deduped: two plan entries can resolve to one destination, and a count
140
+ // is a promise about FILES. Double-counting inflates the summary, the
141
+ // NDJSON frame and anything downstream reading either.
142
+ if (conflictState.preserved.includes(target))
143
+ return;
144
+ conflictState.preserved.push(target);
145
+ host.warn(preservedFileMessage(target, sidecar));
146
+ }
147
+ return {
148
+ conflictState,
149
+ begin(root) {
150
+ conflictState.root = root;
151
+ conflictState.recorded = null;
152
+ conflictState.preserved = [];
153
+ conflictState.staged = new Map();
154
+ },
155
+ /**
156
+ * The on-disk digest is computed HERE rather than carried from the
157
+ * plan: this runs immediately before the caller copies over `target`,
158
+ * so a file edited between planning and writing is still classified
159
+ * from the bytes that are actually about to be destroyed.
160
+ */
161
+ resolve(target, force) {
162
+ const exists = pathExists(target);
163
+ return decideDeployWrite({
164
+ exists,
165
+ recordedSha256: exists ? recordedHashFor(target) : undefined,
166
+ onDiskSha256: exists ? sha256OfFile(target) : null,
167
+ force,
168
+ });
169
+ },
170
+ /**
171
+ * Stage the package content beside a preserved target.
172
+ *
173
+ * The staged bytes are what the installer WOULD have written — the
174
+ * package file plus its install-time tag — so merging the sidecar by
175
+ * hand yields a file that matches the digest this run records and still
176
+ * carries the ownership evidence the reaper reads.
177
+ */
178
+ preserve(target, source, packageRoot) {
179
+ const sidecar = sidecarPathFor(target);
180
+ stageInto(target, sidecar, (tmp) => {
181
+ fs.copyFileSync(source, tmp);
182
+ host.tagDeployed(tmp, source, packageRoot);
183
+ });
184
+ record(target, sidecar);
185
+ },
186
+ /**
187
+ * The same, for a managed file the installer GENERATES rather than
188
+ * copies (the Claude Desktop marker).
189
+ *
190
+ * Those writers are recorded in the manifest with a digest, so they are
191
+ * in the class the ruling covers, and one of them wrote unconditionally
192
+ * — ignoring even `--force` — because the tracker was wired only into
193
+ * the copy loop.
194
+ */
195
+ preserveContent(target, content) {
196
+ const sidecar = sidecarPathFor(target);
197
+ stageInto(target, sidecar, (tmp) => fs.writeFileSync(tmp, content, 'utf-8'));
198
+ record(target, sidecar);
199
+ },
200
+ /**
201
+ * Runs before the installer's terminal NDJSON frame so that frame can
202
+ * carry the count. A failing run keeps its own code — a conflict never
203
+ * masks a failure.
204
+ */
205
+ finalizeRc(rc) {
206
+ const count = conflictState.preserved.length;
207
+ if (count === 0)
208
+ return rc;
209
+ host.emitProgress({ type: 'conflicts', count, paths: [...conflictState.preserved] });
210
+ // `warn` writes to stderr and is deliberately NOT gated on QUIET:
211
+ // the wizard runs the installer with QUIET set, and a preserved
212
+ // edit is the one thing a silent run must still say.
213
+ host.warn(conflictSummaryMessage(count));
214
+ return rc === 0 ? EXIT_COMPLETED_WITH_CONFLICTS : rc;
215
+ },
216
+ };
217
+ }
218
+ //# sourceMappingURL=conflictTracker.js.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Filesystem primitives shared by the installer and its conflict tracker.
3
+ *
4
+ * These four moved out of `src/scripts/install.ts` when `conflictTracker.ts`
5
+ * needed them. The tracker cannot import the installer — that is a cycle — and
6
+ * re-declaring `pathExists` / `sha256OfFile` beside it would put two
7
+ * definitions of "does this file exist" and "what are its bytes" on the same
8
+ * install path, which is the one place they have to agree. Passing them in as
9
+ * callbacks was the other option and it buys nothing: they are pure, total
10
+ * functions of their arguments carrying no installer state, so a shared module
11
+ * is both the smaller wiring and the more honest unit.
12
+ *
13
+ * The selection rule is exactly that — shared with `conflictTracker.ts`. The
14
+ * installer's other private helpers (`expanduser`, `isDir`, `readText`, …) stay
15
+ * where they are; moving them too would be a restructure this change does not
16
+ * need and cannot justify.
17
+ */
18
+ import * as crypto from 'node:crypto';
19
+ import * as fs from 'node:fs';
20
+ import * as path from 'node:path';
21
+ /** `Path.resolve()` — absolute, symlink-resolved where possible. */
22
+ export function resolvePath(p) {
23
+ try {
24
+ return fs.realpathSync(path.resolve(p));
25
+ }
26
+ catch {
27
+ return path.resolve(p);
28
+ }
29
+ }
30
+ export function pathExists(p) {
31
+ try {
32
+ fs.statSync(p);
33
+ return true;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ /** `path.mkdir(parents=True, exist_ok=True)`. */
40
+ export function mkdirp(p) {
41
+ fs.mkdirSync(p, { recursive: true });
42
+ }
43
+ /** `hashlib.sha256(data).hexdigest()` of a file's bytes, or null when unreadable. */
44
+ export function sha256OfFile(p) {
45
+ let data;
46
+ try {
47
+ data = fs.readFileSync(p);
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ return crypto.createHash('sha256').update(data).digest('hex');
53
+ }
54
+ //# sourceMappingURL=fsPrimitives.js.map