@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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +297 -137
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +74 -27
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +47 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +50 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +188 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/dist/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
- package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +90 -56
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +4 -4
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +85 -38
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +1832 -1007
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2007 -1481
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +398 -39
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +17 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +26 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/src/config/agent-settings.template.yml +139 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +18 -5
- package/src/config/continuity-surface.json +21 -0
- package/src/config/gate-coverage.yml +146 -13
- package/src/config/gate-violation-baselines.json +6 -6
- package/src/config/hook-token-budget.json +2 -0
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +4 -3
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +90 -0
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/forge_protection.ts +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +402 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/release_highlights.ts +199 -10
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -0
- package/src/scripts/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/check_enforcement_matrix.ts +624 -0
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_preamble_payload_budget.ts +75 -2
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/closure_scan.ts +333 -0
- package/src/scripts/compile_hook_manifest.ts +43 -3
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +88 -9
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- package/src/scripts/hooks/evidence_independence.ts +96 -0
- package/src/scripts/hooks/memory_recall_hook.ts +540 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
- package/src/scripts/hooks/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks_doctor.ts +85 -0
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +106 -206
- package/src/scripts/janitor.ts +7 -0
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_hook_manifest.ts +11 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +61 -0
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +14 -98
- package/src/scripts/release_drill.ts +267 -1
- package/src/scripts/release_publication.ts +51 -0
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -0
- package/src/server/schemas/settings.ts +31 -7
- package/src/shared/missionExecution.ts +126 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
package/src/install/conflict.ts
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
import { existsSync } from 'node:fs';
|
|
19
19
|
|
|
20
20
|
import { sha256File } from './plan.js';
|
|
21
|
+
import {
|
|
22
|
+
classifyOwnership,
|
|
23
|
+
recordedHashesForRoot,
|
|
24
|
+
type RecordedHashes,
|
|
25
|
+
} from './recordedOwnership.js';
|
|
21
26
|
import type {
|
|
22
27
|
ConflictEntry,
|
|
23
28
|
ConflictPolicy,
|
|
@@ -47,7 +52,7 @@ export interface ResolveInputs {
|
|
|
47
52
|
* Decision matrix (mirrors the Python legacy with the 3-option prompt
|
|
48
53
|
* collapsed into `surface`):
|
|
49
54
|
*
|
|
50
|
-
* | Exists? | Idempotent? | Known? | Force? | → Outcome |
|
|
55
|
+
* | 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
|
|
51
56
|
* |---------|-------------|--------|--------|------------|
|
|
52
57
|
* | no | — | — | — | `write` |
|
|
53
58
|
* | yes | yes | — | — | `skip` |
|
|
@@ -56,9 +61,23 @@ export interface ResolveInputs {
|
|
|
56
61
|
* | yes | no | no | yes | `write` |
|
|
57
62
|
* | yes | no | no | no | `surface` |
|
|
58
63
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
64
|
+
* Deliberately NOT extended with the recorded-digest ownership that
|
|
65
|
+
* {@link computeConflicts} consumes. This function has no caller in `src/` —
|
|
66
|
+
* the only references are its own definition and the `{@link}` above — so an
|
|
67
|
+
* `ownership` input would be a parameter nobody supplies, on a decision
|
|
68
|
+
* nobody reads. The ownership split lives where it is live: in the report.
|
|
69
|
+
*
|
|
70
|
+
* What this resolver decides is in any case NOT what the installer does. The
|
|
71
|
+
* single writer is `src/scripts/install.ts`, whose conflict resolver
|
|
72
|
+
* (`src/install/conflictTracker.ts`) reads nothing from this module; `skip`
|
|
73
|
+
* here means "the planner would not touch it", never "your edit is safe".
|
|
74
|
+
*
|
|
75
|
+
* Since the owner ruling of 2026-09-21 that writer no longer overwrites
|
|
76
|
+
* unconditionally — a managed file diverging from its recorded digest is
|
|
77
|
+
* preserved and the package content staged beside it (`src/install/preserve.ts`).
|
|
78
|
+
* The two still reach that answer independently: this module decides from a
|
|
79
|
+
* plan, the writer re-hashes the destination at the moment it is about to
|
|
80
|
+
* mutate it. A `skip` here remains a statement about the plan.
|
|
62
81
|
*/
|
|
63
82
|
export function resolveFileConflict(inputs: ResolveInputs): ConflictOutcome {
|
|
64
83
|
const { targetPath, idempotent, exists, policy } = inputs;
|
|
@@ -155,21 +174,57 @@ export const CONFLICT_BATCH_THRESHOLD = 5;
|
|
|
155
174
|
*
|
|
156
175
|
* Pure-ish — reads from the filesystem only to compute idempotency
|
|
157
176
|
* (`existsSync` + `sha256File`). Skips bridges (`sha256 === null`) since
|
|
158
|
-
* bridge writers own a separate idempotency model
|
|
159
|
-
*
|
|
160
|
-
* byte-equal to the planned content. Returns an empty array when
|
|
177
|
+
* bridge writers own a separate idempotency model, and skips entries that
|
|
178
|
+
* are byte-equal to the planned content. Returns an empty array when
|
|
161
179
|
* `policy.force` is true — overwrite mode silences the screen.
|
|
180
|
+
*
|
|
181
|
+
* Ownership (Phase 5.1) replaces the blanket `policy.knownPaths` skip:
|
|
182
|
+
*
|
|
183
|
+
* - `recorded-unchanged` — ours and untouched. Skipped, and now for a reason
|
|
184
|
+
* that survives a package upgrade: the planned bytes differing from the
|
|
185
|
+
* recorded bytes is an upgrade, not a collision. This is a REDUCTION in
|
|
186
|
+
* what a manifest-carrying tree reports — `cmd_preflight` builds an empty
|
|
187
|
+
* `knownPaths`, so every such file used to be a finding — and it is the
|
|
188
|
+
* intended direction: a routine upgrade is not a conflict.
|
|
189
|
+
* - `recorded-modified` — ours and edited since. **Reported**, where before
|
|
190
|
+
* it was silently dropped whenever the path sat in `knownPaths`. Reported
|
|
191
|
+
* is all it is: the installer does not consult this matrix, so being named
|
|
192
|
+
* here is not protection from the next deploy.
|
|
193
|
+
* - `unknown` — no digest recorded (no manifest, an unreadable
|
|
194
|
+
* one, or a bridge entry). Falls back to `policy.knownPaths`, so a tree
|
|
195
|
+
* with no manifest reports exactly what it reported before. A tree WITH a
|
|
196
|
+
* manifest reports differently in both directions, by design: it gains the
|
|
197
|
+
* modified rows above and loses the unchanged ones below.
|
|
198
|
+
*
|
|
199
|
+
* `recorded` is injectable for tests; by default it is read from the
|
|
200
|
+
* manifest at `plan.root`.
|
|
162
201
|
*/
|
|
163
|
-
export function computeConflicts(
|
|
202
|
+
export function computeConflicts(
|
|
203
|
+
plan: InstallPlan,
|
|
204
|
+
recordedOverride?: RecordedHashes,
|
|
205
|
+
): readonly ConflictEntry[] {
|
|
206
|
+
// Force returns before the manifest is read. As a default parameter the
|
|
207
|
+
// map was built at call entry and discarded on the next line, so every
|
|
208
|
+
// force-mode plan paid a read and a YAML parse for nothing.
|
|
164
209
|
if (plan.policy.force) return [];
|
|
210
|
+
const recorded = recordedOverride ?? recordedHashesForRoot(plan.root);
|
|
165
211
|
const out: ConflictEntry[] = [];
|
|
166
212
|
for (const entries of Object.values(plan.filesByTool)) {
|
|
167
213
|
for (const entry of entries) {
|
|
168
214
|
if (entry.kind === 'bridge') continue;
|
|
169
215
|
if (entry.sha256 === null) continue;
|
|
170
216
|
if (!existsSync(entry.path)) continue;
|
|
171
|
-
|
|
217
|
+
// The on-disk digest is computed BEFORE the ownership decision so
|
|
218
|
+
// that decision can consume it. Behaviour-identical to computing it
|
|
219
|
+
// after the `knownPaths` test — the remaining guards are unchanged.
|
|
220
|
+
// The added cost is one digest per EXISTING path in
|
|
221
|
+
// `policy.knownPaths`, the set the removed short-circuit used to
|
|
222
|
+
// skip; everything else was already being hashed. That set is empty
|
|
223
|
+
// for `cmd_preflight` and caller-supplied for the plan route.
|
|
172
224
|
const onDisk = sha256File(entry.path);
|
|
225
|
+
const ownership = classifyOwnership(recorded.get(entry.path), onDisk);
|
|
226
|
+
if (ownership === 'recorded-unchanged') continue;
|
|
227
|
+
if (ownership === 'unknown' && plan.policy.knownPaths.has(entry.path)) continue;
|
|
173
228
|
if (onDisk === entry.sha256) continue;
|
|
174
229
|
out.push({
|
|
175
230
|
path: entry.path,
|
|
@@ -177,6 +232,7 @@ export function computeConflicts(plan: InstallPlan): readonly ConflictEntry[] {
|
|
|
177
232
|
plannedSha256: entry.sha256,
|
|
178
233
|
existingSha256: onDisk,
|
|
179
234
|
mergeable: isJsonTarget(entry),
|
|
235
|
+
ownership,
|
|
180
236
|
});
|
|
181
237
|
}
|
|
182
238
|
}
|
|
@@ -0,0 +1,299 @@
|
|
|
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
|
+
|
|
23
|
+
import { mkdirp, pathExists, resolvePath, sha256OfFile } from './fsPrimitives.js';
|
|
24
|
+
import {
|
|
25
|
+
EXIT_COMPLETED_WITH_CONFLICTS,
|
|
26
|
+
conflictSummaryMessage,
|
|
27
|
+
decideDeployWrite,
|
|
28
|
+
foreignSidecarMessage,
|
|
29
|
+
preservedFileMessage,
|
|
30
|
+
sidecarPathFor,
|
|
31
|
+
type DeployWriteDecision,
|
|
32
|
+
} from './preserve.js';
|
|
33
|
+
import { recordedHashesForRoot, type RecordedHashes } from './recordedOwnership.js';
|
|
34
|
+
|
|
35
|
+
/** The installer-owned reporting surface the tracker borrows. */
|
|
36
|
+
export interface ConflictTrackerHost {
|
|
37
|
+
/** `install.ts::warn` — stderr, deliberately NOT gated on QUIET. */
|
|
38
|
+
warn(msg: string): void;
|
|
39
|
+
/** `install.ts::fail` — prints the doctor hint, then throws `SystemExitError(1)`. */
|
|
40
|
+
fail(msg: string): never;
|
|
41
|
+
/** `install.ts::_log_tx_entry('write', target)`. */
|
|
42
|
+
logWrite(target: string): void;
|
|
43
|
+
/** `install.ts::_emit_progress` — a no-op unless `--progress-ndjson` is set. */
|
|
44
|
+
emitProgress(frame: Record<string, unknown>): void;
|
|
45
|
+
/**
|
|
46
|
+
* `install.ts::_inject_package_tag` — stamps `package:` / `source_path:`
|
|
47
|
+
* into a deployed `.md`.
|
|
48
|
+
*
|
|
49
|
+
* The tracker needs it because the sidecar must be what the installer WOULD
|
|
50
|
+
* have written, and a deployed file is the package bytes PLUS this tag. A
|
|
51
|
+
* raw copy is not that file: merging it by hand leaves content that matches
|
|
52
|
+
* no recorded digest and carries none of the ownership evidence the reaper
|
|
53
|
+
* reads, so the obvious way to resolve a conflict produced a file the tool
|
|
54
|
+
* no longer recognised as its own.
|
|
55
|
+
*/
|
|
56
|
+
tagDeployed(target: string, source: string, packageRoot: string | null): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Mutable per-run state.
|
|
61
|
+
*
|
|
62
|
+
* `recorded` is read lazily and cached for the whole run, so the digests
|
|
63
|
+
* compared against are the ones recorded BEFORE this run started — the manifest
|
|
64
|
+
* is rewritten at the end of the install, and re-reading it mid-run would
|
|
65
|
+
* compare a file against a digest this very run had just recorded for it.
|
|
66
|
+
*/
|
|
67
|
+
export interface ConflictState {
|
|
68
|
+
root: string | null;
|
|
69
|
+
recorded: RecordedHashes | null;
|
|
70
|
+
preserved: string[];
|
|
71
|
+
/**
|
|
72
|
+
* Sidecar path → the digest of the package content staged there this run.
|
|
73
|
+
*
|
|
74
|
+
* Recorded into the manifest beside the preserved target, and it is the
|
|
75
|
+
* only durable proof that this installer wrote that sidecar. Without it,
|
|
76
|
+
* a sidecar staged against package v2 and still unmerged when v3 installs
|
|
77
|
+
* is indistinguishable from a stranger's file at the same path — which is
|
|
78
|
+
* why the run used to abort there.
|
|
79
|
+
*/
|
|
80
|
+
staged: Map<string, string | null>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ConflictTracker {
|
|
84
|
+
/** Live state, exposed so the installer can re-export it for its tests. */
|
|
85
|
+
readonly conflictState: ConflictState;
|
|
86
|
+
/** Reset the tracker and point it at the tree whose manifest records ownership. */
|
|
87
|
+
begin(root: string | null): void;
|
|
88
|
+
/** Decide what the writer does with one deploy target. */
|
|
89
|
+
resolve(target: string, force: boolean): DeployWriteDecision;
|
|
90
|
+
/** Stage package content beside a preserved target, then record and report it. */
|
|
91
|
+
preserve(target: string, source: string, packageRoot: string | null): void;
|
|
92
|
+
/** The same, for a managed file the installer generates rather than copies. */
|
|
93
|
+
preserveContent(target: string, content: string): void;
|
|
94
|
+
/** Fold preserved-file conflicts into the run's exit code and report them. */
|
|
95
|
+
finalizeRc(rc: number): number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build a tracker bound to one installer's reporting surface.
|
|
100
|
+
*
|
|
101
|
+
* A factory rather than module-level state so the reporting surface is injected
|
|
102
|
+
* rather than imported. It is NOT what keeps two installs in one process apart:
|
|
103
|
+
* `src/scripts/install.ts` calls this once at module scope, so both runs share
|
|
104
|
+
* the instance and therefore the `preserved` list. `begin()` is what scopes the
|
|
105
|
+
* state to a run, and it is the only thing that does.
|
|
106
|
+
*/
|
|
107
|
+
export function createConflictTracker(host: ConflictTrackerHost): ConflictTracker {
|
|
108
|
+
const conflictState: ConflictState = {
|
|
109
|
+
root: null,
|
|
110
|
+
recorded: null,
|
|
111
|
+
preserved: [],
|
|
112
|
+
staged: new Map(),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Recorded digest for `target`, or `undefined` when this tree cannot say.
|
|
117
|
+
*
|
|
118
|
+
* Two keys are tried, and the plain `path.resolve` one comes FIRST because
|
|
119
|
+
* it is the form that actually matches: `_file_entry` records the path
|
|
120
|
+
* verbatim as the copy loop built it, and `readRecordedHashes` re-resolves
|
|
121
|
+
* with `path.resolve` and no realpath. Looking up only the realpath would
|
|
122
|
+
* miss every entry on macOS, where the temp and home trees sit behind the
|
|
123
|
+
* realpath symlink that rewrites a `/var` prefix onto its private twin —
|
|
124
|
+
* the feature would have been inert on the platform it was developed on.
|
|
125
|
+
* (Spelled out rather than quoted: `check_bundle_path_leakage` scans the
|
|
126
|
+
* tracked bundles this file is compiled into, and the literal form is a
|
|
127
|
+
* build-machine path leak there.) The realpath is kept as a second key so
|
|
128
|
+
* a manifest written through a symlinked deploy root still resolves.
|
|
129
|
+
*/
|
|
130
|
+
function recordedHashFor(target: string): string | null | undefined {
|
|
131
|
+
if (conflictState.root === null) return undefined;
|
|
132
|
+
conflictState.recorded ??= recordedHashesForRoot(conflictState.root);
|
|
133
|
+
const plain = path.resolve(target);
|
|
134
|
+
if (conflictState.recorded.has(plain)) return conflictState.recorded.get(plain);
|
|
135
|
+
const real = resolvePath(target);
|
|
136
|
+
return real === plain ? undefined : conflictState.recorded.get(real);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Produce the bytes this run would have written, then reconcile them with
|
|
141
|
+
* whatever already sits at the sidecar path.
|
|
142
|
+
*
|
|
143
|
+
* The candidate is built into a temp file FIRST so the comparison is against
|
|
144
|
+
* the bytes that would actually be staged rather than against the raw
|
|
145
|
+
* package source — those differ, because a deployed file is the package
|
|
146
|
+
* bytes plus the install-time `package:` tag, and comparing the untagged
|
|
147
|
+
* source would call every already-staged sidecar stale and rewrite it on
|
|
148
|
+
* every run.
|
|
149
|
+
*
|
|
150
|
+
* The temp name ends in the TARGET's extension because `_inject_package_tag`
|
|
151
|
+
* keys on it: a sidecar is `<name>.md.agent-config.new`, whose extension is
|
|
152
|
+
* `.new`, so tagging it in place would silently do nothing.
|
|
153
|
+
*/
|
|
154
|
+
function stageInto(target: string, sidecar: string, produce: (tmp: string) => void): void {
|
|
155
|
+
mkdirp(path.dirname(sidecar));
|
|
156
|
+
const tmp = path.join(
|
|
157
|
+
path.dirname(sidecar),
|
|
158
|
+
`.${path.basename(sidecar)}.${process.pid}.${crypto.randomBytes(6).toString('hex')}${path.extname(target)}`,
|
|
159
|
+
);
|
|
160
|
+
const discard = (): void => {
|
|
161
|
+
try {
|
|
162
|
+
fs.unlinkSync(tmp);
|
|
163
|
+
} catch {
|
|
164
|
+
/* swallow — best-effort cleanup of our own temp file */
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
let candidate: string | null;
|
|
168
|
+
try {
|
|
169
|
+
produce(tmp);
|
|
170
|
+
candidate = sha256OfFile(tmp);
|
|
171
|
+
} catch (exc) {
|
|
172
|
+
discard();
|
|
173
|
+
host.fail(
|
|
174
|
+
`Could not stage package content for ${target} at ${sidecar}: ${String(exc)}. ` +
|
|
175
|
+
'Nothing was written; the managed file is unchanged.',
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (pathExists(sidecar)) {
|
|
179
|
+
const onDisk = sha256OfFile(sidecar);
|
|
180
|
+
// Three outcomes, and only the third is a refusal. Identical bytes:
|
|
181
|
+
// already staged, so the file is left alone. Bytes this tree
|
|
182
|
+
// RECORDED staging: an earlier run staged them against an older
|
|
183
|
+
// package version and the user has not merged them yet — the
|
|
184
|
+
// staging workflow behaving as designed, so the stale copy is
|
|
185
|
+
// refreshed. Anything else is unattributable and the run stops.
|
|
186
|
+
//
|
|
187
|
+
// Ownership is decided by the recorded digest, never by whether the
|
|
188
|
+
// bytes match the CURRENT package: that test called every unmerged
|
|
189
|
+
// sidecar foreign the moment a new version shipped, and aborted the
|
|
190
|
+
// whole install saying it could not show it wrote a file it had
|
|
191
|
+
// written one version earlier.
|
|
192
|
+
if (onDisk !== null && onDisk === candidate) {
|
|
193
|
+
discard();
|
|
194
|
+
conflictState.staged.set(sidecar, onDisk);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (onDisk === null || onDisk !== recordedHashFor(sidecar)) {
|
|
198
|
+
discard();
|
|
199
|
+
host.fail(foreignSidecarMessage(target, sidecar));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
fs.renameSync(tmp, sidecar);
|
|
204
|
+
} catch (exc) {
|
|
205
|
+
discard();
|
|
206
|
+
host.fail(
|
|
207
|
+
`Could not stage package content for ${target} at ${sidecar}: ${String(exc)}. ` +
|
|
208
|
+
'Nothing was written; the managed file is unchanged.',
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
conflictState.staged.set(sidecar, candidate);
|
|
212
|
+
host.logWrite(sidecar);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Record and report one preserved target, once per destination. */
|
|
216
|
+
function record(target: string, sidecar: string): void {
|
|
217
|
+
// Deduped: two plan entries can resolve to one destination, and a count
|
|
218
|
+
// is a promise about FILES. Double-counting inflates the summary, the
|
|
219
|
+
// NDJSON frame and anything downstream reading either.
|
|
220
|
+
if (conflictState.preserved.includes(target)) return;
|
|
221
|
+
conflictState.preserved.push(target);
|
|
222
|
+
host.warn(preservedFileMessage(target, sidecar));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
conflictState,
|
|
227
|
+
|
|
228
|
+
begin(root: string | null): void {
|
|
229
|
+
conflictState.root = root;
|
|
230
|
+
conflictState.recorded = null;
|
|
231
|
+
conflictState.preserved = [];
|
|
232
|
+
conflictState.staged = new Map();
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The on-disk digest is computed HERE rather than carried from the
|
|
237
|
+
* plan: this runs immediately before the caller copies over `target`,
|
|
238
|
+
* so a file edited between planning and writing is still classified
|
|
239
|
+
* from the bytes that are actually about to be destroyed.
|
|
240
|
+
*/
|
|
241
|
+
resolve(target: string, force: boolean): DeployWriteDecision {
|
|
242
|
+
const exists = pathExists(target);
|
|
243
|
+
return decideDeployWrite({
|
|
244
|
+
exists,
|
|
245
|
+
recordedSha256: exists ? recordedHashFor(target) : undefined,
|
|
246
|
+
onDiskSha256: exists ? sha256OfFile(target) : null,
|
|
247
|
+
force,
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Stage the package content beside a preserved target.
|
|
253
|
+
*
|
|
254
|
+
* The staged bytes are what the installer WOULD have written — the
|
|
255
|
+
* package file plus its install-time tag — so merging the sidecar by
|
|
256
|
+
* hand yields a file that matches the digest this run records and still
|
|
257
|
+
* carries the ownership evidence the reaper reads.
|
|
258
|
+
*/
|
|
259
|
+
preserve(target: string, source: string, packageRoot: string | null): void {
|
|
260
|
+
const sidecar = sidecarPathFor(target);
|
|
261
|
+
stageInto(target, sidecar, (tmp) => {
|
|
262
|
+
fs.copyFileSync(source, tmp);
|
|
263
|
+
host.tagDeployed(tmp, source, packageRoot);
|
|
264
|
+
});
|
|
265
|
+
record(target, sidecar);
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The same, for a managed file the installer GENERATES rather than
|
|
270
|
+
* copies (the Claude Desktop marker).
|
|
271
|
+
*
|
|
272
|
+
* Those writers are recorded in the manifest with a digest, so they are
|
|
273
|
+
* in the class the ruling covers, and one of them wrote unconditionally
|
|
274
|
+
* — ignoring even `--force` — because the tracker was wired only into
|
|
275
|
+
* the copy loop.
|
|
276
|
+
*/
|
|
277
|
+
preserveContent(target: string, content: string): void {
|
|
278
|
+
const sidecar = sidecarPathFor(target);
|
|
279
|
+
stageInto(target, sidecar, (tmp) => fs.writeFileSync(tmp, content, 'utf-8'));
|
|
280
|
+
record(target, sidecar);
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Runs before the installer's terminal NDJSON frame so that frame can
|
|
285
|
+
* carry the count. A failing run keeps its own code — a conflict never
|
|
286
|
+
* masks a failure.
|
|
287
|
+
*/
|
|
288
|
+
finalizeRc(rc: number): number {
|
|
289
|
+
const count = conflictState.preserved.length;
|
|
290
|
+
if (count === 0) return rc;
|
|
291
|
+
host.emitProgress({ type: 'conflicts', count, paths: [...conflictState.preserved] });
|
|
292
|
+
// `warn` writes to stderr and is deliberately NOT gated on QUIET:
|
|
293
|
+
// the wizard runs the installer with QUIET set, and a preserved
|
|
294
|
+
// edit is the one thing a silent run must still say.
|
|
295
|
+
host.warn(conflictSummaryMessage(count));
|
|
296
|
+
return rc === 0 ? EXIT_COMPLETED_WITH_CONFLICTS : rc;
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
@@ -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
|
+
|
|
22
|
+
/** `Path.resolve()` — absolute, symlink-resolved where possible. */
|
|
23
|
+
export function resolvePath(p: string): string {
|
|
24
|
+
try {
|
|
25
|
+
return fs.realpathSync(path.resolve(p));
|
|
26
|
+
} catch {
|
|
27
|
+
return path.resolve(p);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function pathExists(p: string): boolean {
|
|
32
|
+
try {
|
|
33
|
+
fs.statSync(p);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `path.mkdir(parents=True, exist_ok=True)`. */
|
|
41
|
+
export function mkdirp(p: string): void {
|
|
42
|
+
fs.mkdirSync(p, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `hashlib.sha256(data).hexdigest()` of a file's bytes, or null when unreadable. */
|
|
46
|
+
export function sha256OfFile(p: string): string | null {
|
|
47
|
+
let data: Buffer;
|
|
48
|
+
try {
|
|
49
|
+
data = fs.readFileSync(p);
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
54
|
+
}
|