@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,259 @@
1
+ /**
2
+ * The installed-tools manifest's per-tool `files[]` inventory — building it,
3
+ * reading back what a previous install recorded, and reconciling the two.
4
+ *
5
+ * THE GAP THIS CLOSES.
6
+ *
7
+ * `files[].sha256` is the only record of what this installer wrote where. The
8
+ * conflict tracker added in Phase 5.1 reads exactly that field to tell a
9
+ * managed file still carrying our bytes from one the user has edited
10
+ * (`recordedOwnership.classifyOwnership`). A path with no recorded digest
11
+ * classifies `unknown`, and `unknown` writes.
12
+ *
13
+ * So a run that DROPS a recorded digest does not merely lose bookkeeping — it
14
+ * re-arms the overwrite that preservation exists to prevent, one run later.
15
+ * Three drops were live, and all three end in the same silent data loss:
16
+ *
17
+ * 1. **A preserved file was never recorded.** The writer pushes a path into
18
+ * `written_paths` only when it actually copies; the preserve branch returns
19
+ * early. The manifest is rebuilt from `written_paths`, so the very run that
20
+ * protected an edit erased the digest proving it was an edit. The next run
21
+ * saw `unknown` and overwrote — preservation survived exactly one install.
22
+ * 2. **A failed deploy wiped its tool's whole inventory.** `deploy_failed`
23
+ * produced an empty list, and an empty list is authoritative replacement,
24
+ * not "nothing to say".
25
+ * 3. **Every tool outside the current selection lost its inventory too.**
26
+ * `installed_tools.read_manifest` is a hand-rolled parser that drops nested
27
+ * `files[]` by design (ADR-200), so the entries carried into the rewrite
28
+ * came back empty and were written back empty. `upsert_tool`'s "keep what
29
+ * was there" fallback could never fire, because nothing was ever there by
30
+ * the time it looked.
31
+ *
32
+ * (3) is why this module hydrates rather than patching the preserve branch:
33
+ * the same digest loss reaches the same file through three doors, and repairing
34
+ * one leaves the other two open.
35
+ *
36
+ * WHY A FOURTH READER OF THIS FILE.
37
+ *
38
+ * `installed_tools.read_manifest` (degraded by design), `install_drift`
39
+ * (digests it re-hashes itself) and `recordedOwnership.readRecordedHashes` (a
40
+ * flat path→digest map, no tool attribution, no `kind`) already read it. None
41
+ * answers the question here — *which tool recorded which entries, verbatim,
42
+ * so they can be written back unchanged* — and widening either of the latter
43
+ * two would change a shape their own tests pin. What this module does not
44
+ * duplicate is `manifest_path()`: that stays the single place deciding where
45
+ * the manifest is, so the `AGENT_CONFIG_INSTALLED_TOOLS` override behaves
46
+ * identically in all four.
47
+ */
48
+ import { existsSync, readFileSync } from 'node:fs';
49
+ import { homedir } from 'node:os';
50
+ import { isAbsolute, join, resolve } from 'node:path';
51
+ import * as YAML from 'yaml';
52
+ import { sha256OfFile } from './fsPrimitives.js';
53
+ import { sidecarPathFor } from './preserve.js';
54
+ /** Build one `files[]` record for `p`. */
55
+ export function fileEntry(p, kind, hashContent) {
56
+ return { path: p, kind, sha256: hashContent ? sha256OfFile(p) : null };
57
+ }
58
+ /**
59
+ * Turn deploy results into the per-tool inventory this run is authoritative for.
60
+ *
61
+ * `null` — not an empty list — for a tool whose deploy did not complete. The
62
+ * distinction is the whole point: an empty list says "this tool owns no files
63
+ * now", which replaces and therefore erases; `null` says "this run learned
64
+ * nothing about this tool", which is what a failed deploy actually knows, and
65
+ * the caller carries the previous inventory forward instead.
66
+ */
67
+ export function filesByToolFromDeploy(deployResults) {
68
+ const out = {};
69
+ for (const toolId of Object.keys(deployResults)) {
70
+ const [, , status, paths] = deployResults[toolId];
71
+ if (status === 'deployed') {
72
+ out[toolId] = paths.map((p) => fileEntry(p, 'deployed', true));
73
+ }
74
+ else if (status === 'marker') {
75
+ out[toolId] = paths.map((p) => fileEntry(p, 'marker', true));
76
+ }
77
+ else {
78
+ out[toolId] = null;
79
+ }
80
+ }
81
+ return out;
82
+ }
83
+ /**
84
+ * The bridge marker each selected tool claims, as a one-entry inventory.
85
+ *
86
+ * `bridgeMarker` is injected because the marker table lives with the installer's
87
+ * tool registry; passing it keeps that registry in one place instead of giving
88
+ * this module a second copy to drift from. Bridge entries are recorded WITHOUT
89
+ * a digest — a bridge is a pointer the consumer owns and edits, so there is
90
+ * nothing for this tree to claim ownership of, and `classifyOwnership` reads a
91
+ * null digest as `unknown` and leaves the file alone.
92
+ */
93
+ export function filesByToolFromBridges(tools, projectRoot, scope, bridgeMarker) {
94
+ const out = {};
95
+ for (const toolId of [...tools].sort()) {
96
+ const marker = bridgeMarker(toolId, scope);
97
+ if (!marker)
98
+ continue;
99
+ const markerPath = isAbsolute(marker) ? marker : join(projectRoot, marker);
100
+ out[toolId] = [fileEntry(markerPath, 'bridge', false)];
101
+ }
102
+ return out;
103
+ }
104
+ /** Expand a leading `~`, matching `installed_tools.expanduser`. */
105
+ function expanduser(p) {
106
+ if (p === '~')
107
+ return homedir();
108
+ if (p.startsWith('~/') || (process.platform === 'win32' && p.startsWith('~\\'))) {
109
+ return join(homedir(), p.slice(2));
110
+ }
111
+ return p;
112
+ }
113
+ /** Resolve a manifest-recorded path the way every other reader of this file does. */
114
+ function resolveRecorded(raw, projectRoot) {
115
+ const expanded = expanduser(raw);
116
+ return isAbsolute(expanded) ? resolve(expanded) : resolve(projectRoot, expanded);
117
+ }
118
+ /**
119
+ * Read `tools[].files[]` and `tools[].merged_keys[]` per tool.
120
+ *
121
+ * A missing, unreadable or malformed manifest yields an empty map rather than
122
+ * throwing — an install must never fail because a lockfile is corrupt, and an
123
+ * empty answer degrades to exactly the behaviour this module replaced.
124
+ */
125
+ export function readRecordedByTool(manifestPath, projectRoot) {
126
+ const out = new Map();
127
+ if (!existsSync(manifestPath))
128
+ return out;
129
+ let doc;
130
+ try {
131
+ doc = YAML.parse(readFileSync(manifestPath, 'utf8'));
132
+ }
133
+ catch {
134
+ return out;
135
+ }
136
+ if (doc === null || typeof doc !== 'object' || Array.isArray(doc))
137
+ return out;
138
+ const tools = doc['tools'];
139
+ if (!Array.isArray(tools))
140
+ return out;
141
+ for (const tool of tools) {
142
+ if (tool === null || typeof tool !== 'object' || Array.isArray(tool))
143
+ continue;
144
+ const rec = tool;
145
+ const name = rec['name'];
146
+ if (typeof name !== 'string' || name.length === 0)
147
+ continue;
148
+ const files = [];
149
+ const rawFiles = rec['files'];
150
+ if (Array.isArray(rawFiles)) {
151
+ for (const entry of rawFiles) {
152
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
153
+ continue;
154
+ const raw = entry['path'];
155
+ if (typeof raw !== 'string' || raw.length === 0)
156
+ continue;
157
+ files.push({
158
+ entry: entry,
159
+ resolved: resolveRecorded(raw, projectRoot),
160
+ });
161
+ }
162
+ }
163
+ const rawMerged = rec['merged_keys'];
164
+ const mergedKeys = Array.isArray(rawMerged) ? rawMerged : [];
165
+ out.set(name, { files, mergedKeys });
166
+ }
167
+ return out;
168
+ }
169
+ /**
170
+ * Put the nested inventories back onto entries read by the degraded parser.
171
+ *
172
+ * `installed_tools.read_manifest` returns every tool with `files: []` and
173
+ * `merged_keys: []` whatever the file held, and those entries are what the
174
+ * rewrite carries forward for tools the current run does not touch. Without
175
+ * this, installing tool A erases tool B's recorded digests — and B's managed
176
+ * files then classify `unknown` and get overwritten the next time B installs.
177
+ *
178
+ * Mutates in place and returns the same array: the caller's `entries` list is
179
+ * already its own copy, and rebuilding it would drop unknown-but-recorded
180
+ * keys that this module has no business deciding about.
181
+ */
182
+ export function hydrateRecordedInventories(entries, prior) {
183
+ for (const entry of entries) {
184
+ const name = entry['name'];
185
+ if (typeof name !== 'string')
186
+ continue;
187
+ const recorded = prior.get(name);
188
+ if (recorded === undefined)
189
+ continue;
190
+ if (Array.isArray(entry['files']) && entry['files'].length === 0) {
191
+ entry['files'] = recorded.files.map((f) => f.entry);
192
+ }
193
+ if (Array.isArray(entry['merged_keys']) && entry['merged_keys'].length === 0) {
194
+ entry['merged_keys'] = [...recorded.mergedKeys];
195
+ }
196
+ }
197
+ return entries;
198
+ }
199
+ /**
200
+ * The inventory to record for one tool this run.
201
+ *
202
+ * - `fresh === null | undefined` → the previous inventory, unchanged. A run
203
+ * that learned nothing about a tool records what was already known.
204
+ * - otherwise → what this run wrote, plus a carried-forward entry for every
205
+ * PRESERVED path this tool recorded before.
206
+ *
207
+ * The carried entry keeps the path and kind it had, and takes the digest of
208
+ * the content STAGED this run. Both halves are load-bearing.
209
+ *
210
+ * The digest means "the package content this installer last produced for this
211
+ * path". A preserved run produced that content too — it staged it in the
212
+ * sidecar instead of writing it — so recording it keeps the field's meaning
213
+ * exact and makes the documented resolution work: merge the sidecar and the
214
+ * file matches, so the conflict clears and the next install refreshes it
215
+ * normally. Carrying the OLDER digest instead would leave a correctly merged
216
+ * file looking modified forever, resolvable only with `--force`.
217
+ *
218
+ * What it must never be is a re-hash of the target. That records the USER's
219
+ * content as ours, and the run after that classifies the edit
220
+ * `recorded-unchanged` and overwrites it — the same data loss, one
221
+ * indirection further away.
222
+ */
223
+ export function reconcileToolFiles(fresh, prior, preserved) {
224
+ if (fresh === null || fresh === undefined) {
225
+ return prior === undefined ? null : prior.files.map((f) => f.entry);
226
+ }
227
+ if (prior === undefined || preserved.size === 0)
228
+ return [...fresh];
229
+ const seen = new Set();
230
+ for (const entry of fresh) {
231
+ const raw = entry['path'];
232
+ if (typeof raw === 'string')
233
+ seen.add(resolve(expanduser(raw)));
234
+ }
235
+ const out = [...fresh];
236
+ for (const file of prior.files) {
237
+ const staged = preserved.get(file.resolved);
238
+ if (staged === undefined || seen.has(file.resolved))
239
+ continue;
240
+ out.push(staged.sha256 === null ? file.entry : { ...file.entry, sha256: staged.sha256 });
241
+ if (staged.sidecar !== null)
242
+ out.push(staged.sidecar);
243
+ }
244
+ return out;
245
+ }
246
+ /** Build the {@link PreservedIndex} from the tracker's per-run state. */
247
+ export function preservedIndex(preservedTargets, staged) {
248
+ const out = new Map();
249
+ for (const target of preservedTargets) {
250
+ const sidecar = sidecarPathFor(target);
251
+ const sha = staged.get(sidecar) ?? null;
252
+ out.set(resolve(target), {
253
+ sha256: sha,
254
+ sidecar: sha === null ? null : { path: sidecar, kind: 'sidecar', sha256: sha },
255
+ });
256
+ }
257
+ return out;
258
+ }
259
+ //# sourceMappingURL=manifestFiles.js.map
@@ -123,8 +123,20 @@ export function checkConflicts(inputs) {
123
123
  id: 'conflicts',
124
124
  severity: 'warning',
125
125
  path: c.path,
126
- message: `existing file conflicts with planned ${c.kind} content`,
127
- remedy: 'resolve interactively, or pass --force to overwrite',
126
+ message: c.ownership === 'recorded-modified'
127
+ ? `managed ${c.kind} file has been edited since we wrote it`
128
+ : `existing file conflicts with planned ${c.kind} content`,
129
+ // A remedy here may only describe what an install actually does. Before
130
+ // the owner ruling of 2026-09-21 the writer overwrote every deployed
131
+ // file unconditionally, so the `recorded-modified` arm told the
132
+ // operator to back their edit up. It now preserves that file and stages
133
+ // the package content beside it, so the same sentence would be false in
134
+ // the opposite direction — it would understate a protection that ships.
135
+ // The `unknown` arm is unchanged: nothing is recorded for that path, so
136
+ // the writer still writes over it.
137
+ remedy: c.ownership === 'recorded-modified'
138
+ ? 'installing preserves your edit and stages package content as <path>.agent-config.new; --force replaces it instead'
139
+ : 'back up this file — installing writes the planned content over it',
128
140
  }));
129
141
  }
130
142
  /** Probe 4 — host-detection sanity: report what the target tree looks like. */
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Preserve a user-modified managed file — road-to-a-conformance-check-that-can-fail
3
+ * Phase 5.1, owner ruling 2026-09-21 (option (a), after a 1/1 council split).
4
+ *
5
+ * THE GAP THIS CLOSES. Until this module existed, `agent-config init` overwrote
6
+ * every deployed file unconditionally: the conflict resolver in
7
+ * `src/scripts/install.ts` returned `write` for every target, and the module
8
+ * header recorded `--force` as an accepted no-op "because installs always
9
+ * overwrite". The planner produced a `ConflictEntry` list naming the files the
10
+ * user had edited, and the writer read none of it — so the report named the
11
+ * file and the next install replaced it.
12
+ *
13
+ * WHAT THE RULING CHANGES. A managed file whose bytes diverge from the digest
14
+ * the install manifest recorded is preserved; the package content it would
15
+ * have been replaced with is staged beside it as a sidecar, and the run ends
16
+ * with a distinct exit code so the resulting staleness is loud rather than
17
+ * silent.
18
+ *
19
+ * NO SEMANTIC CLASSIFICATION, DELIBERATELY. Any divergence from the recorded
20
+ * digest gets identical treatment. There is no comment-only detection, no
21
+ * format-aware diff, no three-way merge — each would drag a per-format parser
22
+ * into the installer's trusted computing base, and a syntactically trivial
23
+ * edit can still be an intentional one. One byte of divergence is the whole
24
+ * test.
25
+ *
26
+ * NO FALLBACK TO OVERWRITING. There is no conflict threshold above which
27
+ * preservation stops, no resource-pressure escape, and no bulk accept flag.
28
+ * `--force` is the single escape hatch and it is explicit, per-run and
29
+ * documented. Failing closed here means leaving a file alone, which is the
30
+ * recoverable direction.
31
+ *
32
+ * NO UNPROVEN OWNERSHIP CLAIM OVER AN EXISTING SIDECAR. A pathname matching the
33
+ * convention is not provenance, and destroying an unrecognised file while
34
+ * claiming to protect files would be the defect this module exists to remove,
35
+ * wearing the fix's clothes. So a file at the sidecar path is replaced only
36
+ * when the manifest RECORDS this tree staging those exact bytes; a sidecar
37
+ * still holding an older version's content is therefore refreshed rather than
38
+ * refused, because leaving it unmerged is the staging workflow working. Bytes
39
+ * matching neither the current content nor anything recorded are left alone and
40
+ * the run stops.
41
+ */
42
+ import { classifyOwnership } from './recordedOwnership.js';
43
+ /**
44
+ * Suffix appended to a preserved file's path to name its staged package copy.
45
+ *
46
+ * Tool-owned on purpose. A bare `.new` is a namespace editors, patch tools and
47
+ * humans already write into, so an install that claimed it would collide with
48
+ * files it has no relationship to; `.agent-config.new` says who wrote it.
49
+ */
50
+ export const SIDECAR_SUFFIX = '.agent-config.new';
51
+ /**
52
+ * Exit code for a run that installed everything it could but preserved at
53
+ * least one user-modified file.
54
+ *
55
+ * Distinct from `0` (fully current) and from `1` (the run failed). It is the
56
+ * load-bearing half of the ruling: preserving an edit leaves the active
57
+ * installation stale, and a stale install that exits `0` is silently stale.
58
+ * `2` is already argparse's usage-error code, so this is `3`.
59
+ */
60
+ export const EXIT_COMPLETED_WITH_CONFLICTS = 3;
61
+ /**
62
+ * Decide whether the writer may replace one managed file.
63
+ *
64
+ * A target that does not exist is written. A forced run writes. Otherwise the
65
+ * recorded-ownership verdict decides: `recorded-modified` preserves, and both
66
+ * `recorded-unchanged` and `unknown` write.
67
+ *
68
+ * The `unknown` arm is the one worth stating out loud, because it is what
69
+ * keeps this change from being a regression. No recorded digest means no
70
+ * divergence to measure — a tree with no manifest, a global anchor that holds
71
+ * none, or a bridge entry recorded without a hash. Preserving there would
72
+ * stage a sidecar beside every pre-existing file on a first install, which is
73
+ * not protection, it is noise. The ruling covers a file this tree can show it
74
+ * wrote and can show has changed since; everything else keeps the behavior it
75
+ * had, and `recordedOwnership` already documents that floor.
76
+ */
77
+ export function decideDeployWrite(inputs) {
78
+ if (!inputs.exists)
79
+ return 'write';
80
+ if (inputs.force)
81
+ return 'write';
82
+ const ownership = classifyOwnership(inputs.recordedSha256, inputs.onDiskSha256);
83
+ return ownership === 'recorded-modified' ? 'preserve' : 'write';
84
+ }
85
+ /** Path of the sidecar staged beside `target`. */
86
+ export function sidecarPathFor(target) {
87
+ return `${target}${SIDECAR_SUFFIX}`;
88
+ }
89
+ /**
90
+ * Per-file report line for a preserved target.
91
+ *
92
+ * Says three things in one breath, and all three are load-bearing: the edit
93
+ * survived, the package content is somewhere findable, and the active
94
+ * installation is not current — that last clause is what turns a silently
95
+ * stale tree into a stated one.
96
+ */
97
+ export function preservedFileMessage(target, sidecar) {
98
+ return (`Preserved user-modified ${target}. Package content was written to ${sidecar}; ` +
99
+ 'the active installation is not current. Review and merge it, or rerun with ' +
100
+ '--force to replace the managed file.');
101
+ }
102
+ /** End-of-run summary naming the conflict count and what it means. */
103
+ export function conflictSummaryMessage(count) {
104
+ const noun = count === 1 ? 'file' : 'files';
105
+ return (`Completed with conflicts: ${count} user-modified ${noun} preserved. ` +
106
+ `Package content is staged alongside as *${SIDECAR_SUFFIX}; the active installation ` +
107
+ `is not current until you merge it, or rerun with --force to replace the managed ${noun}.`);
108
+ }
109
+ /**
110
+ * Refusal message for a sidecar path already holding bytes this run cannot
111
+ * attribute to itself.
112
+ *
113
+ * The run fails rather than continuing: the package content was never written
114
+ * anywhere, so the target is neither refreshed nor staged, and calling that
115
+ * "completed with conflicts" would report a staging that did not happen.
116
+ */
117
+ export function foreignSidecarMessage(target, sidecar) {
118
+ return (`Refusing to replace ${sidecar}: it already exists with different content and this ` +
119
+ `install cannot show it wrote it. Package content for ${target} was NOT staged. ` +
120
+ 'Move or delete that file and re-run, or rerun with --force to replace the managed file instead.');
121
+ }
122
+ /**
123
+ * Summary payload for the GUI wizard's `done` SSE frame.
124
+ *
125
+ * `conflicts` rides on the installer's own `done` frame when the run preserved
126
+ * a user-modified managed file (exit {@link EXIT_COMPLETED_WITH_CONFLICTS}).
127
+ * The wizard forwards it rather than dropping it: a preserved edit means the
128
+ * active installation is not current, and that frame is the browser's only
129
+ * channel for saying so. Omitted when zero, so the historical summary shape is
130
+ * unchanged for every run that has no conflicts.
131
+ *
132
+ * It lives here rather than in the route because the conflict semantics are
133
+ * this module's, and because `src/server/routes/wizard.ts` sits against the
134
+ * 1,500-line source ceiling that `check_source_size_budget` ratchets.
135
+ */
136
+ /**
137
+ * Did the installer COMPLETE, whatever it exited with?
138
+ *
139
+ * `3` is a completion with preserved files, not a failure. The wizard's
140
+ * fallback path — the one that speaks when the installer dies before its
141
+ * terminal frame — tested `exitCode === 0` and reported everything else as
142
+ * `BRIDGE_FAILED`, so a run that installed everything it could and kept the
143
+ * user's edits was announced to the operator as a failed install.
144
+ *
145
+ * Here rather than in the route for the same reason as {@link installDoneSummary}:
146
+ * the semantics are this module's, and `src/server/routes/wizard.ts` sits
147
+ * against the 1,500-line source ceiling that `check_source_size_budget`
148
+ * ratchets.
149
+ */
150
+ export function installCompleted(exitCode) {
151
+ return exitCode === 0 || exitCode === EXIT_COMPLETED_WITH_CONFLICTS;
152
+ }
153
+ export function installDoneSummary(frame, written, total) {
154
+ const conflicts = typeof frame.conflicts === 'number' ? frame.conflicts : 0;
155
+ return conflicts > 0 ? { written, total, conflicts } : { written, total };
156
+ }
157
+ //# sourceMappingURL=preserve.js.map
@@ -0,0 +1,132 @@
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
+ import { existsSync, readFileSync } from 'node:fs';
37
+ import { homedir } from 'node:os';
38
+ import { isAbsolute, join, resolve } from 'node:path';
39
+ import * as YAML from 'yaml';
40
+ import { manifest_path } from '../scripts/_lib/installed_tools.js';
41
+ /** The empty answer — "nothing is recorded", used whenever no manifest resolves. */
42
+ export const NO_RECORDED_HASHES = new Map();
43
+ /**
44
+ * Classify one target from the recorded digest and the on-disk digest.
45
+ *
46
+ * Pure — the caller supplies both digests. `undefined` for `recordedSha256`
47
+ * means the path is not recorded at all; `null` means recorded without a
48
+ * digest. Both answer `unknown`, because neither supports a comparison.
49
+ */
50
+ export function classifyOwnership(recordedSha256, onDiskSha256) {
51
+ if (recordedSha256 === undefined || recordedSha256 === null)
52
+ return 'unknown';
53
+ if (onDiskSha256 === null)
54
+ return 'unknown';
55
+ return recordedSha256 === onDiskSha256 ? 'recorded-unchanged' : 'recorded-modified';
56
+ }
57
+ /**
58
+ * Read `tools[].files[]` out of an installed-tools manifest into a
59
+ * path → digest map.
60
+ *
61
+ * Relative paths resolve against `projectRoot` and a leading `~` expands
62
+ * first — the manifest stores user-scope entries absolute or `~`-rooted and
63
+ * project-scope entries relative, and both existing readers
64
+ * (`install_drift.resolve_entry_path`, `cmd_doctor._resolve_path`) resolve
65
+ * them that way. A missing, unreadable or malformed manifest yields the
66
+ * empty map rather than throwing — an install plan must never fail because a
67
+ * lockfile is corrupt.
68
+ */
69
+ export function readRecordedHashes(manifestPath, projectRoot) {
70
+ if (!existsSync(manifestPath))
71
+ return NO_RECORDED_HASHES;
72
+ let doc;
73
+ try {
74
+ doc = YAML.parse(readFileSync(manifestPath, 'utf8'));
75
+ }
76
+ catch {
77
+ return NO_RECORDED_HASHES;
78
+ }
79
+ if (doc === null || typeof doc !== 'object' || Array.isArray(doc))
80
+ return NO_RECORDED_HASHES;
81
+ const tools = doc['tools'];
82
+ if (!Array.isArray(tools))
83
+ return NO_RECORDED_HASHES;
84
+ const out = new Map();
85
+ for (const tool of tools) {
86
+ if (tool === null || typeof tool !== 'object' || Array.isArray(tool))
87
+ continue;
88
+ const files = tool['files'];
89
+ if (!Array.isArray(files))
90
+ continue;
91
+ for (const entry of files) {
92
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
93
+ continue;
94
+ const rec = entry;
95
+ const raw = rec['path'];
96
+ if (typeof raw !== 'string' || raw.length === 0)
97
+ continue;
98
+ const sha = rec['sha256'];
99
+ const expanded = expanduser(raw);
100
+ const target = isAbsolute(expanded)
101
+ ? resolve(expanded)
102
+ : resolve(projectRoot, expanded);
103
+ out.set(target, typeof sha === 'string' && sha.length > 0 ? sha : null);
104
+ }
105
+ }
106
+ return out;
107
+ }
108
+ /** Expand a leading `~`, matching `installed_tools.expanduser`. */
109
+ function expanduser(p) {
110
+ if (p === '~')
111
+ return homedir();
112
+ if (p.startsWith('~/') || (process.platform === 'win32' && p.startsWith('~\\'))) {
113
+ return join(homedir(), p.slice(2));
114
+ }
115
+ return p;
116
+ }
117
+ /**
118
+ * {@link readRecordedHashes} for a PROJECT root, via `installed_tools`.
119
+ *
120
+ * `manifest_path` carries the `AGENT_CONFIG_INSTALLED_TOOLS` override, so an
121
+ * install running against a relocated manifest classifies ownership instead
122
+ * of degrading every file to `unknown`.
123
+ *
124
+ * The argument must be a project root. An {@link InstallPlan.root} for a
125
+ * `global` target is the install anchor (`~/.event4u/agent-config/`), which
126
+ * holds no manifest — such a plan gets the empty map and therefore the
127
+ * pre-hash answer, which is correct but is a floor rather than a feature.
128
+ */
129
+ export function recordedHashesForRoot(root, env) {
130
+ return readRecordedHashes(manifest_path(root, env), root);
131
+ }
132
+ //# sourceMappingURL=recordedOwnership.js.map
@@ -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.
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "discovery": {
3
- "artefact_count": 623,
3
+ "artefact_count": 625,
4
4
  "scanner_version": "de0a22b4ac4c"
5
5
  },
6
- "generated_at": "2026-09-12",
6
+ "generated_at": "2026-09-28",
7
7
  "package": {
8
8
  "description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) \u2014 every claim machine-checked, including the counts in these badges.",
9
9
  "homepage": "https://event4u-app.github.io/agent-config",
10
10
  "name": "@event4u/agent-config",
11
11
  "repository": "https://github.com/event4u-app/agent-config",
12
- "version": "16.0.0"
12
+ "version": "16.1.0"
13
13
  },
14
14
  "registries": [
15
15
  {
@@ -9,12 +9,12 @@
9
9
  "transport": {
10
10
  "type": "stdio"
11
11
  },
12
- "version": "16.0.0"
12
+ "version": "16.1.0"
13
13
  }
14
14
  ],
15
15
  "repository": {
16
16
  "source": "github",
17
17
  "url": "https://github.com/event4u-app/agent-config"
18
18
  },
19
- "version": "16.0.0"
19
+ "version": "16.1.0"
20
20
  }