@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
|
@@ -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);
|
package/dist/install/conflict.js
CHANGED
|
@@ -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
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
125
|
-
*
|
|
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
|
-
|
|
142
|
-
|
|
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
|