@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,312 @@
|
|
|
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
|
+
|
|
49
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
50
|
+
import { homedir } from 'node:os';
|
|
51
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
52
|
+
|
|
53
|
+
import * as YAML from 'yaml';
|
|
54
|
+
|
|
55
|
+
import { sha256OfFile } from './fsPrimitives.js';
|
|
56
|
+
import { sidecarPathFor } from './preserve.js';
|
|
57
|
+
|
|
58
|
+
/** One `tools[].files[]` record, in the shape the manifest writer emits. */
|
|
59
|
+
export type ManifestFileEntry = Record<string, unknown>;
|
|
60
|
+
|
|
61
|
+
/** A recorded entry plus the absolute path it resolves to on this machine. */
|
|
62
|
+
export interface RecordedFile {
|
|
63
|
+
/** The entry exactly as recorded — written back byte-identical. */
|
|
64
|
+
readonly entry: ManifestFileEntry;
|
|
65
|
+
/** `entry.path` with `~` expanded and resolved against the project root. */
|
|
66
|
+
readonly resolved: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** What a previous install recorded for one tool. */
|
|
70
|
+
export interface RecordedToolInventory {
|
|
71
|
+
readonly files: readonly RecordedFile[];
|
|
72
|
+
readonly mergedKeys: readonly ManifestFileEntry[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Tool id → what the manifest records for it. */
|
|
76
|
+
export type RecordedByTool = ReadonlyMap<string, RecordedToolInventory>;
|
|
77
|
+
|
|
78
|
+
/** Build one `files[]` record for `p`. */
|
|
79
|
+
export function fileEntry(p: string, kind: string, hashContent: boolean): ManifestFileEntry {
|
|
80
|
+
return { path: p, kind, sha256: hashContent ? sha256OfFile(p) : null };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A deploy tuple: `[written, skipped, status, paths]`. */
|
|
84
|
+
export type DeployTuple = readonly [number, number, string, string[]];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Turn deploy results into the per-tool inventory this run is authoritative for.
|
|
88
|
+
*
|
|
89
|
+
* `null` — not an empty list — for a tool whose deploy did not complete. The
|
|
90
|
+
* distinction is the whole point: an empty list says "this tool owns no files
|
|
91
|
+
* now", which replaces and therefore erases; `null` says "this run learned
|
|
92
|
+
* nothing about this tool", which is what a failed deploy actually knows, and
|
|
93
|
+
* the caller carries the previous inventory forward instead.
|
|
94
|
+
*/
|
|
95
|
+
export function filesByToolFromDeploy(
|
|
96
|
+
deployResults: Record<string, DeployTuple>,
|
|
97
|
+
): Record<string, ManifestFileEntry[] | null> {
|
|
98
|
+
const out: Record<string, ManifestFileEntry[] | null> = {};
|
|
99
|
+
for (const toolId of Object.keys(deployResults)) {
|
|
100
|
+
const [, , status, paths] = deployResults[toolId] as DeployTuple;
|
|
101
|
+
if (status === 'deployed') {
|
|
102
|
+
out[toolId] = paths.map((p) => fileEntry(p, 'deployed', true));
|
|
103
|
+
} else if (status === 'marker') {
|
|
104
|
+
out[toolId] = paths.map((p) => fileEntry(p, 'marker', true));
|
|
105
|
+
} else {
|
|
106
|
+
out[toolId] = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The bridge marker each selected tool claims, as a one-entry inventory.
|
|
114
|
+
*
|
|
115
|
+
* `bridgeMarker` is injected because the marker table lives with the installer's
|
|
116
|
+
* tool registry; passing it keeps that registry in one place instead of giving
|
|
117
|
+
* this module a second copy to drift from. Bridge entries are recorded WITHOUT
|
|
118
|
+
* a digest — a bridge is a pointer the consumer owns and edits, so there is
|
|
119
|
+
* nothing for this tree to claim ownership of, and `classifyOwnership` reads a
|
|
120
|
+
* null digest as `unknown` and leaves the file alone.
|
|
121
|
+
*/
|
|
122
|
+
export function filesByToolFromBridges(
|
|
123
|
+
tools: Set<string>,
|
|
124
|
+
projectRoot: string,
|
|
125
|
+
scope: string,
|
|
126
|
+
bridgeMarker: (toolId: string, scope: string) => string,
|
|
127
|
+
): Record<string, ManifestFileEntry[]> {
|
|
128
|
+
const out: Record<string, ManifestFileEntry[]> = {};
|
|
129
|
+
for (const toolId of [...tools].sort()) {
|
|
130
|
+
const marker = bridgeMarker(toolId, scope);
|
|
131
|
+
if (!marker) continue;
|
|
132
|
+
const markerPath = isAbsolute(marker) ? marker : join(projectRoot, marker);
|
|
133
|
+
out[toolId] = [fileEntry(markerPath, 'bridge', false)];
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Expand a leading `~`, matching `installed_tools.expanduser`. */
|
|
139
|
+
function expanduser(p: string): string {
|
|
140
|
+
if (p === '~') return homedir();
|
|
141
|
+
if (p.startsWith('~/') || (process.platform === 'win32' && p.startsWith('~\\'))) {
|
|
142
|
+
return join(homedir(), p.slice(2));
|
|
143
|
+
}
|
|
144
|
+
return p;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Resolve a manifest-recorded path the way every other reader of this file does. */
|
|
148
|
+
function resolveRecorded(raw: string, projectRoot: string): string {
|
|
149
|
+
const expanded = expanduser(raw);
|
|
150
|
+
return isAbsolute(expanded) ? resolve(expanded) : resolve(projectRoot, expanded);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Read `tools[].files[]` and `tools[].merged_keys[]` per tool.
|
|
155
|
+
*
|
|
156
|
+
* A missing, unreadable or malformed manifest yields an empty map rather than
|
|
157
|
+
* throwing — an install must never fail because a lockfile is corrupt, and an
|
|
158
|
+
* empty answer degrades to exactly the behaviour this module replaced.
|
|
159
|
+
*/
|
|
160
|
+
export function readRecordedByTool(manifestPath: string, projectRoot: string): RecordedByTool {
|
|
161
|
+
const out = new Map<string, RecordedToolInventory>();
|
|
162
|
+
if (!existsSync(manifestPath)) return out;
|
|
163
|
+
let doc: unknown;
|
|
164
|
+
try {
|
|
165
|
+
doc = YAML.parse(readFileSync(manifestPath, 'utf8'));
|
|
166
|
+
} catch {
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) return out;
|
|
170
|
+
const tools = (doc as Record<string, unknown>)['tools'];
|
|
171
|
+
if (!Array.isArray(tools)) return out;
|
|
172
|
+
|
|
173
|
+
for (const tool of tools) {
|
|
174
|
+
if (tool === null || typeof tool !== 'object' || Array.isArray(tool)) continue;
|
|
175
|
+
const rec = tool as Record<string, unknown>;
|
|
176
|
+
const name = rec['name'];
|
|
177
|
+
if (typeof name !== 'string' || name.length === 0) continue;
|
|
178
|
+
const files: RecordedFile[] = [];
|
|
179
|
+
const rawFiles = rec['files'];
|
|
180
|
+
if (Array.isArray(rawFiles)) {
|
|
181
|
+
for (const entry of rawFiles) {
|
|
182
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
183
|
+
const raw = (entry as Record<string, unknown>)['path'];
|
|
184
|
+
if (typeof raw !== 'string' || raw.length === 0) continue;
|
|
185
|
+
files.push({
|
|
186
|
+
entry: entry as ManifestFileEntry,
|
|
187
|
+
resolved: resolveRecorded(raw, projectRoot),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const rawMerged = rec['merged_keys'];
|
|
192
|
+
const mergedKeys = Array.isArray(rawMerged) ? (rawMerged as ManifestFileEntry[]) : [];
|
|
193
|
+
out.set(name, { files, mergedKeys });
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Put the nested inventories back onto entries read by the degraded parser.
|
|
200
|
+
*
|
|
201
|
+
* `installed_tools.read_manifest` returns every tool with `files: []` and
|
|
202
|
+
* `merged_keys: []` whatever the file held, and those entries are what the
|
|
203
|
+
* rewrite carries forward for tools the current run does not touch. Without
|
|
204
|
+
* this, installing tool A erases tool B's recorded digests — and B's managed
|
|
205
|
+
* files then classify `unknown` and get overwritten the next time B installs.
|
|
206
|
+
*
|
|
207
|
+
* Mutates in place and returns the same array: the caller's `entries` list is
|
|
208
|
+
* already its own copy, and rebuilding it would drop unknown-but-recorded
|
|
209
|
+
* keys that this module has no business deciding about.
|
|
210
|
+
*/
|
|
211
|
+
export function hydrateRecordedInventories(
|
|
212
|
+
entries: ManifestFileEntry[],
|
|
213
|
+
prior: RecordedByTool,
|
|
214
|
+
): ManifestFileEntry[] {
|
|
215
|
+
for (const entry of entries) {
|
|
216
|
+
const name = entry['name'];
|
|
217
|
+
if (typeof name !== 'string') continue;
|
|
218
|
+
const recorded = prior.get(name);
|
|
219
|
+
if (recorded === undefined) continue;
|
|
220
|
+
if (Array.isArray(entry['files']) && (entry['files'] as unknown[]).length === 0) {
|
|
221
|
+
entry['files'] = recorded.files.map((f) => f.entry);
|
|
222
|
+
}
|
|
223
|
+
if (Array.isArray(entry['merged_keys']) && (entry['merged_keys'] as unknown[]).length === 0) {
|
|
224
|
+
entry['merged_keys'] = [...recorded.mergedKeys];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return entries;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The inventory to record for one tool this run.
|
|
232
|
+
*
|
|
233
|
+
* - `fresh === null | undefined` → the previous inventory, unchanged. A run
|
|
234
|
+
* that learned nothing about a tool records what was already known.
|
|
235
|
+
* - otherwise → what this run wrote, plus a carried-forward entry for every
|
|
236
|
+
* PRESERVED path this tool recorded before.
|
|
237
|
+
*
|
|
238
|
+
* The carried entry keeps the path and kind it had, and takes the digest of
|
|
239
|
+
* the content STAGED this run. Both halves are load-bearing.
|
|
240
|
+
*
|
|
241
|
+
* The digest means "the package content this installer last produced for this
|
|
242
|
+
* path". A preserved run produced that content too — it staged it in the
|
|
243
|
+
* sidecar instead of writing it — so recording it keeps the field's meaning
|
|
244
|
+
* exact and makes the documented resolution work: merge the sidecar and the
|
|
245
|
+
* file matches, so the conflict clears and the next install refreshes it
|
|
246
|
+
* normally. Carrying the OLDER digest instead would leave a correctly merged
|
|
247
|
+
* file looking modified forever, resolvable only with `--force`.
|
|
248
|
+
*
|
|
249
|
+
* What it must never be is a re-hash of the target. That records the USER's
|
|
250
|
+
* content as ours, and the run after that classifies the edit
|
|
251
|
+
* `recorded-unchanged` and overwrites it — the same data loss, one
|
|
252
|
+
* indirection further away.
|
|
253
|
+
*/
|
|
254
|
+
export function reconcileToolFiles(
|
|
255
|
+
fresh: readonly ManifestFileEntry[] | null | undefined,
|
|
256
|
+
prior: RecordedToolInventory | undefined,
|
|
257
|
+
preserved: PreservedIndex,
|
|
258
|
+
): ManifestFileEntry[] | null {
|
|
259
|
+
if (fresh === null || fresh === undefined) {
|
|
260
|
+
return prior === undefined ? null : prior.files.map((f) => f.entry);
|
|
261
|
+
}
|
|
262
|
+
if (prior === undefined || preserved.size === 0) return [...fresh];
|
|
263
|
+
const seen = new Set<string>();
|
|
264
|
+
for (const entry of fresh) {
|
|
265
|
+
const raw = entry['path'];
|
|
266
|
+
if (typeof raw === 'string') seen.add(resolve(expanduser(raw)));
|
|
267
|
+
}
|
|
268
|
+
const out = [...fresh];
|
|
269
|
+
for (const file of prior.files) {
|
|
270
|
+
const staged = preserved.get(file.resolved);
|
|
271
|
+
if (staged === undefined || seen.has(file.resolved)) continue;
|
|
272
|
+
out.push(staged.sha256 === null ? file.entry : { ...file.entry, sha256: staged.sha256 });
|
|
273
|
+
if (staged.sidecar !== null) out.push(staged.sidecar);
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Resolved target path → the manifest entry for the sidecar staged beside it.
|
|
280
|
+
*
|
|
281
|
+
* The attribution problem this solves: a preserved path is not in any tool's
|
|
282
|
+
* written list, so nothing in the deploy results says which tool owns it. The
|
|
283
|
+
* PREVIOUS manifest does — it recorded that path under a tool — so
|
|
284
|
+
* {@link reconcileToolFiles} matches on the prior inventory and the sidecar
|
|
285
|
+
* rides along with its target rather than being filed by guesswork.
|
|
286
|
+
*/
|
|
287
|
+
export type PreservedIndex = ReadonlyMap<string, PreservedRecord>;
|
|
288
|
+
|
|
289
|
+
/** What one preserved target contributes to the manifest. */
|
|
290
|
+
export interface PreservedRecord {
|
|
291
|
+
/** Digest of the content staged for it, or `null` when nothing was staged. */
|
|
292
|
+
readonly sha256: string | null;
|
|
293
|
+
/** The sidecar's own entry — this run's proof that it staged that file. */
|
|
294
|
+
readonly sidecar: ManifestFileEntry | null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Build the {@link PreservedIndex} from the tracker's per-run state. */
|
|
298
|
+
export function preservedIndex(
|
|
299
|
+
preservedTargets: readonly string[],
|
|
300
|
+
staged: ReadonlyMap<string, string | null>,
|
|
301
|
+
): PreservedIndex {
|
|
302
|
+
const out = new Map<string, PreservedRecord>();
|
|
303
|
+
for (const target of preservedTargets) {
|
|
304
|
+
const sidecar = sidecarPathFor(target);
|
|
305
|
+
const sha = staged.get(sidecar) ?? null;
|
|
306
|
+
out.set(resolve(target), {
|
|
307
|
+
sha256: sha,
|
|
308
|
+
sidecar: sha === null ? null : { path: sidecar, kind: 'sidecar', sha256: sha },
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
package/src/install/preflight.ts
CHANGED
|
@@ -155,8 +155,22 @@ export function checkConflicts(inputs: PlanInputs): PreflightFinding[] {
|
|
|
155
155
|
id: 'conflicts' as const,
|
|
156
156
|
severity: 'warning' as const,
|
|
157
157
|
path: c.path,
|
|
158
|
-
message:
|
|
159
|
-
|
|
158
|
+
message:
|
|
159
|
+
c.ownership === 'recorded-modified'
|
|
160
|
+
? `managed ${c.kind} file has been edited since we wrote it`
|
|
161
|
+
: `existing file conflicts with planned ${c.kind} content`,
|
|
162
|
+
// A remedy here may only describe what an install actually does. Before
|
|
163
|
+
// the owner ruling of 2026-09-21 the writer overwrote every deployed
|
|
164
|
+
// file unconditionally, so the `recorded-modified` arm told the
|
|
165
|
+
// operator to back their edit up. It now preserves that file and stages
|
|
166
|
+
// the package content beside it, so the same sentence would be false in
|
|
167
|
+
// the opposite direction — it would understate a protection that ships.
|
|
168
|
+
// The `unknown` arm is unchanged: nothing is recorded for that path, so
|
|
169
|
+
// the writer still writes over it.
|
|
170
|
+
remedy:
|
|
171
|
+
c.ownership === 'recorded-modified'
|
|
172
|
+
? 'installing preserves your edit and stages package content as <path>.agent-config.new; --force replaces it instead'
|
|
173
|
+
: 'back up this file — installing writes the planned content over it',
|
|
160
174
|
}));
|
|
161
175
|
}
|
|
162
176
|
|
|
@@ -0,0 +1,205 @@
|
|
|
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
|
+
|
|
43
|
+
import { classifyOwnership } from './recordedOwnership.js';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Suffix appended to a preserved file's path to name its staged package copy.
|
|
47
|
+
*
|
|
48
|
+
* Tool-owned on purpose. A bare `.new` is a namespace editors, patch tools and
|
|
49
|
+
* humans already write into, so an install that claimed it would collide with
|
|
50
|
+
* files it has no relationship to; `.agent-config.new` says who wrote it.
|
|
51
|
+
*/
|
|
52
|
+
export const SIDECAR_SUFFIX = '.agent-config.new';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Exit code for a run that installed everything it could but preserved at
|
|
56
|
+
* least one user-modified file.
|
|
57
|
+
*
|
|
58
|
+
* Distinct from `0` (fully current) and from `1` (the run failed). It is the
|
|
59
|
+
* load-bearing half of the ruling: preserving an edit leaves the active
|
|
60
|
+
* installation stale, and a stale install that exits `0` is silently stale.
|
|
61
|
+
* `2` is already argparse's usage-error code, so this is `3`.
|
|
62
|
+
*/
|
|
63
|
+
export const EXIT_COMPLETED_WITH_CONFLICTS = 3;
|
|
64
|
+
|
|
65
|
+
/** What the writer should do with one planned target. */
|
|
66
|
+
export type DeployWriteDecision = 'write' | 'preserve';
|
|
67
|
+
|
|
68
|
+
/** Inputs to {@link decideDeployWrite}. */
|
|
69
|
+
export interface DeployWriteInputs {
|
|
70
|
+
/** True when the target already exists on disk. */
|
|
71
|
+
readonly exists: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* The SHA-256 the install manifest recorded for this target.
|
|
74
|
+
*
|
|
75
|
+
* `undefined` — not recorded at all. `null` — recorded without a digest
|
|
76
|
+
* (bridge entries carry `sha256: null`). Both mean this tree cannot say
|
|
77
|
+
* whether the bytes on disk are ours.
|
|
78
|
+
*/
|
|
79
|
+
readonly recordedSha256: string | null | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* The SHA-256 of the bytes on disk, computed immediately before the caller
|
|
82
|
+
* mutates the file — not at plan time.
|
|
83
|
+
*
|
|
84
|
+
* The planner's `ConflictEntry` list is advisory state, not a safety
|
|
85
|
+
* capability: minutes can pass between planning and writing, and a file
|
|
86
|
+
* edited in that window would be classified from a digest that no longer
|
|
87
|
+
* describes it. Re-hashing at the write site closes that window.
|
|
88
|
+
*
|
|
89
|
+
* `null` when the file could not be read.
|
|
90
|
+
*/
|
|
91
|
+
readonly onDiskSha256: string | null;
|
|
92
|
+
/** True when the run carries `--force`. */
|
|
93
|
+
readonly force: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Decide whether the writer may replace one managed file.
|
|
98
|
+
*
|
|
99
|
+
* A target that does not exist is written. A forced run writes. Otherwise the
|
|
100
|
+
* recorded-ownership verdict decides: `recorded-modified` preserves, and both
|
|
101
|
+
* `recorded-unchanged` and `unknown` write.
|
|
102
|
+
*
|
|
103
|
+
* The `unknown` arm is the one worth stating out loud, because it is what
|
|
104
|
+
* keeps this change from being a regression. No recorded digest means no
|
|
105
|
+
* divergence to measure — a tree with no manifest, a global anchor that holds
|
|
106
|
+
* none, or a bridge entry recorded without a hash. Preserving there would
|
|
107
|
+
* stage a sidecar beside every pre-existing file on a first install, which is
|
|
108
|
+
* not protection, it is noise. The ruling covers a file this tree can show it
|
|
109
|
+
* wrote and can show has changed since; everything else keeps the behavior it
|
|
110
|
+
* had, and `recordedOwnership` already documents that floor.
|
|
111
|
+
*/
|
|
112
|
+
export function decideDeployWrite(inputs: DeployWriteInputs): DeployWriteDecision {
|
|
113
|
+
if (!inputs.exists) return 'write';
|
|
114
|
+
if (inputs.force) return 'write';
|
|
115
|
+
const ownership = classifyOwnership(inputs.recordedSha256, inputs.onDiskSha256);
|
|
116
|
+
return ownership === 'recorded-modified' ? 'preserve' : 'write';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Path of the sidecar staged beside `target`. */
|
|
120
|
+
export function sidecarPathFor(target: string): string {
|
|
121
|
+
return `${target}${SIDECAR_SUFFIX}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Per-file report line for a preserved target.
|
|
126
|
+
*
|
|
127
|
+
* Says three things in one breath, and all three are load-bearing: the edit
|
|
128
|
+
* survived, the package content is somewhere findable, and the active
|
|
129
|
+
* installation is not current — that last clause is what turns a silently
|
|
130
|
+
* stale tree into a stated one.
|
|
131
|
+
*/
|
|
132
|
+
export function preservedFileMessage(target: string, sidecar: string): string {
|
|
133
|
+
return (
|
|
134
|
+
`Preserved user-modified ${target}. Package content was written to ${sidecar}; ` +
|
|
135
|
+
'the active installation is not current. Review and merge it, or rerun with ' +
|
|
136
|
+
'--force to replace the managed file.'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** End-of-run summary naming the conflict count and what it means. */
|
|
141
|
+
export function conflictSummaryMessage(count: number): string {
|
|
142
|
+
const noun = count === 1 ? 'file' : 'files';
|
|
143
|
+
return (
|
|
144
|
+
`Completed with conflicts: ${count} user-modified ${noun} preserved. ` +
|
|
145
|
+
`Package content is staged alongside as *${SIDECAR_SUFFIX}; the active installation ` +
|
|
146
|
+
`is not current until you merge it, or rerun with --force to replace the managed ${noun}.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Refusal message for a sidecar path already holding bytes this run cannot
|
|
152
|
+
* attribute to itself.
|
|
153
|
+
*
|
|
154
|
+
* The run fails rather than continuing: the package content was never written
|
|
155
|
+
* anywhere, so the target is neither refreshed nor staged, and calling that
|
|
156
|
+
* "completed with conflicts" would report a staging that did not happen.
|
|
157
|
+
*/
|
|
158
|
+
export function foreignSidecarMessage(target: string, sidecar: string): string {
|
|
159
|
+
return (
|
|
160
|
+
`Refusing to replace ${sidecar}: it already exists with different content and this ` +
|
|
161
|
+
`install cannot show it wrote it. Package content for ${target} was NOT staged. ` +
|
|
162
|
+
'Move or delete that file and re-run, or rerun with --force to replace the managed file instead.'
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Summary payload for the GUI wizard's `done` SSE frame.
|
|
168
|
+
*
|
|
169
|
+
* `conflicts` rides on the installer's own `done` frame when the run preserved
|
|
170
|
+
* a user-modified managed file (exit {@link EXIT_COMPLETED_WITH_CONFLICTS}).
|
|
171
|
+
* The wizard forwards it rather than dropping it: a preserved edit means the
|
|
172
|
+
* active installation is not current, and that frame is the browser's only
|
|
173
|
+
* channel for saying so. Omitted when zero, so the historical summary shape is
|
|
174
|
+
* unchanged for every run that has no conflicts.
|
|
175
|
+
*
|
|
176
|
+
* It lives here rather than in the route because the conflict semantics are
|
|
177
|
+
* this module's, and because `src/server/routes/wizard.ts` sits against the
|
|
178
|
+
* 1,500-line source ceiling that `check_source_size_budget` ratchets.
|
|
179
|
+
*/
|
|
180
|
+
/**
|
|
181
|
+
* Did the installer COMPLETE, whatever it exited with?
|
|
182
|
+
*
|
|
183
|
+
* `3` is a completion with preserved files, not a failure. The wizard's
|
|
184
|
+
* fallback path — the one that speaks when the installer dies before its
|
|
185
|
+
* terminal frame — tested `exitCode === 0` and reported everything else as
|
|
186
|
+
* `BRIDGE_FAILED`, so a run that installed everything it could and kept the
|
|
187
|
+
* user's edits was announced to the operator as a failed install.
|
|
188
|
+
*
|
|
189
|
+
* Here rather than in the route for the same reason as {@link installDoneSummary}:
|
|
190
|
+
* the semantics are this module's, and `src/server/routes/wizard.ts` sits
|
|
191
|
+
* against the 1,500-line source ceiling that `check_source_size_budget`
|
|
192
|
+
* ratchets.
|
|
193
|
+
*/
|
|
194
|
+
export function installCompleted(exitCode: number): boolean {
|
|
195
|
+
return exitCode === 0 || exitCode === EXIT_COMPLETED_WITH_CONFLICTS;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function installDoneSummary(
|
|
199
|
+
frame: Record<string, unknown>,
|
|
200
|
+
written: number,
|
|
201
|
+
total: number,
|
|
202
|
+
): { written: number; total: number; conflicts?: number } {
|
|
203
|
+
const conflicts = typeof frame.conflicts === 'number' ? frame.conflicts : 0;
|
|
204
|
+
return conflicts > 0 ? { written, total, conflicts } : { written, total };
|
|
205
|
+
}
|