@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
|
@@ -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:
|
|
127
|
-
|
|
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
|
package/dist/install/txlog.js
CHANGED
|
@@ -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
|
-
*
|
|
6
|
-
*
|
|
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":
|
|
3
|
+
"artefact_count": 625,
|
|
4
4
|
"scanner_version": "de0a22b4ac4c"
|
|
5
5
|
},
|
|
6
|
-
"generated_at": "2026-09-
|
|
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.
|
|
12
|
+
"version": "16.1.0"
|
|
13
13
|
},
|
|
14
14
|
"registries": [
|
|
15
15
|
{
|
package/dist/mcp/server.json
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"transport": {
|
|
10
10
|
"type": "stdio"
|
|
11
11
|
},
|
|
12
|
-
"version": "16.
|
|
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.
|
|
19
|
+
"version": "16.1.0"
|
|
20
20
|
}
|