@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.5
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/bin/doflow.js +2 -2
- package/core/.antigravity-plugin/plugin.json +0 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
- package/core/harnesses/gemini/hooks/session-start.sh +5 -3
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
- package/core/harnesses/kiro/hooks/session-start.sh +11 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
- package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
- package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
- package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
- package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
- package/core/registry/assets.json +19 -0
- package/core/registry/contracts.json +2 -2
- package/core/registry/harnesses.json +196 -10
- package/core/registry/lifecycle.json +2626 -7
- package/core/registry/storage.json +110 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- package/core/shared/scripts/doflow/bin/doflow-run +10 -6
- package/core/shared/skills/do-design/SKILL.md +6 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
- package/core/shared/skills/do-implement/SKILL.md +3 -2
- package/core/shared/skills/do-plan/SKILL.md +7 -8
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +141 -105
- package/src/adapters/claude/{index.ts → index.js} +63 -49
- package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
- package/src/adapters/codex/{agents.ts → agents.js} +24 -24
- package/src/adapters/codex/{config.ts → config.js} +49 -49
- package/src/adapters/codex/{hooks.ts → hooks.js} +62 -27
- package/src/adapters/codex/{index.ts → index.js} +148 -105
- package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
- package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
- package/src/adapters/copilot/{index.ts → index.js} +42 -42
- package/src/adapters/{copy-tree.ts → copy-tree.js} +74 -74
- package/src/adapters/gemini/{hooks.ts → hooks.js} +78 -37
- package/src/adapters/gemini/{index.ts → index.js} +96 -57
- package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
- package/src/adapters/{index.ts → index.js} +13 -13
- package/src/adapters/kiro/{index.ts → index.js} +49 -35
- package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
- package/src/adapters/opencode/{index.ts → index.js} +52 -52
- package/src/adapters/pi/hooks.js +119 -0
- package/src/adapters/pi/{index.ts → index.js} +64 -39
- package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
- package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
- package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
- package/src/cli/commands/{install.ts → install.js} +25 -25
- package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
- package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
- package/src/cli/commands/{remove.ts → remove.js} +10 -10
- package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
- package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
- package/src/cli/commands/{status.ts → status.js} +24 -24
- package/src/cli/commands/{tools.ts → tools.js} +16 -16
- package/src/cli/commands/{update.ts → update.js} +20 -20
- package/src/cli/{index.ts → index.js} +43 -31
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +97 -58
- package/src/cli/{shared.ts → shared.js} +40 -40
- package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
- package/src/helper/{git.ts → git.js} +2 -2
- package/src/helper/{guardrails.ts → guardrails.js} +10 -10
- package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
- package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
- package/src/helper/{prompt.ts → prompt.js} +5 -5
- package/src/helper/{repo-root.ts → repo-root.js} +2 -2
- package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
- package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
- package/src/helper/{toml.ts → toml.js} +48 -48
- package/src/install/{backup-set.ts → backup-set.js} +16 -16
- package/src/install/{backup.ts → backup.js} +65 -65
- package/src/install/{context.ts → context.js} +15 -15
- package/src/install/{manifest.ts → manifest.js} +27 -27
- package/src/install/{mcp.ts → mcp.js} +30 -30
- package/src/install/{paths.ts → paths.js} +15 -15
- package/src/install/{targets.ts → targets.js} +10 -10
- package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
- package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
- package/src/lifecycle/coverage.js +57 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/{index.ts → index.js} +160 -146
- package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
- package/src/lifecycle/{policies.ts → policies.js} +11 -11
- package/src/lifecycle/{view.ts → view.js} +28 -28
- package/src/registry/{index.ts → index.js} +195 -66
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
- package/src/runtime/advice/{cli.ts → cli.js} +31 -31
- package/src/runtime/advice/{client.ts → client.js} +44 -44
- package/src/runtime/advice/{config.ts → config.js} +16 -16
- package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
- package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
- package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
- package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
- package/src/runtime/advice/{record.ts → record.js} +19 -19
- package/src/runtime/advice/{registry.ts → registry.js} +16 -16
- package/src/runtime/advice/{strength.ts → strength.js} +7 -7
- package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
- package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
- package/src/runtime/candidate.js +505 -0
- package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +44 -35
- package/src/runtime/{cli-result.ts → cli-result.js} +15 -8
- package/src/runtime/{cli.ts → cli.js} +59 -292
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +102 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context-pack.js +1277 -0
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +32 -25
- package/src/runtime/failure/{capture.ts → capture.js} +29 -29
- package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
- package/src/runtime/failure/{cli.ts → cli.js} +30 -30
- package/src/runtime/failure/{home.ts → home.js} +6 -6
- package/src/runtime/failure/{store.ts → store.js} +32 -32
- package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
- package/src/runtime/{freshness.ts → freshness.js} +59 -22
- package/src/runtime/{health.ts → health.js} +72 -72
- package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
- package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
- package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
- package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
- package/src/runtime/inventory/{index.ts → index.js} +36 -36
- package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
- package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
- package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
- package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
- package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
- package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
- package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
- package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
- package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
- package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
- package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
- package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
- package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
- package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
- package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
- package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
- package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
- package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
- package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
- package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
- package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
- package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
- package/src/runtime/{mask.ts → mask.js} +22 -22
- package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
- package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
- package/src/runtime/{model-router.ts → model-router.js} +20 -20
- package/src/runtime/{outcome.ts → outcome.js} +27 -27
- package/src/runtime/{reach.ts → reach.js} +21 -21
- package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
- package/src/runtime/{readiness.ts → readiness.js} +113 -43
- package/src/runtime/{recovery.ts → recovery.js} +15 -15
- package/src/runtime/{research-request.ts → research-request.js} +34 -34
- package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +453 -104
- package/src/runtime/retrieval-record.js +223 -0
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +22 -19
- package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
- package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
- package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
- package/src/runtime/source-version.js +289 -0
- package/src/runtime/storage/{cli.ts → cli.js} +9 -9
- package/src/runtime/storage/{engine.ts → engine.js} +37 -37
- package/src/runtime/storage/{registry.ts → registry.js} +19 -19
- package/src/runtime/storage/{roots.ts → roots.js} +8 -8
- package/src/runtime/storage/{rules.ts → rules.js} +26 -26
- package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
- package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
- package/src/runtime/{task-attempts.ts → task-attempts.js} +62 -62
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +40 -15
- package/src/runtime/{task-state.ts → task-state.js} +86 -21
- package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
- package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
- package/src/runtime/trace/{render.ts → render.js} +7 -7
- package/src/runtime/trace/{views.ts → views.js} +40 -40
- package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
- package/src/runtime/verification/{engine.ts → engine.js} +143 -143
- package/src/runtime/verification/{registry.ts → registry.js} +6 -6
- package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
- package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
- package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
- package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
- package/src/runtime/{worktree.ts → worktree.js} +31 -31
- package/src/state/{index.ts → index.js} +26 -26
- package/src/state/{lockfile.ts → lockfile.js} +20 -20
- package/src/state/{run-lock.ts → run-lock.js} +44 -44
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
- package/src/runtime/context-pack.ts +0 -492
|
@@ -8,52 +8,53 @@
|
|
|
8
8
|
const fs = require('node:fs');
|
|
9
9
|
const path = require('node:path');
|
|
10
10
|
const crypto = require('node:crypto');
|
|
11
|
-
const { harnessFor, selectAssets, selectMcpServers, mcpCapable } = require('../registry/index.
|
|
12
|
-
const { defaultLedger, ownershipKey, writeLedger, writeRecoveryRecord, upgradeLedger, LEDGER_VERSION } = require('../state/index.
|
|
13
|
-
const { resolveAdapter, projectAdapterInput } = require('../adapters/index.
|
|
14
|
-
const { pruneEmptyAncestors, discoverTree, sameFileIndex, sharedTreeDestDir, walkRelFiles } = require('../adapters/copy-tree.
|
|
15
|
-
const { renderPolicies } = require('./policies.
|
|
16
|
-
const { renderMcpIndex } = require('./mcp-index.
|
|
17
|
-
const { hasBashCapableShell } = require('./bash-availability.
|
|
18
|
-
const { hookTrustFor: geminiHookTrustFor } = require('../adapters/gemini/index.
|
|
11
|
+
const { harnessFor, selectAssets, selectMcpServers, mcpCapable } = require('../registry/index.js');
|
|
12
|
+
const { defaultLedger, ownershipKey, writeLedger, writeRecoveryRecord, upgradeLedger, LEDGER_VERSION } = require('../state/index.js');
|
|
13
|
+
const { resolveAdapter, projectAdapterInput } = require('../adapters/index.js');
|
|
14
|
+
const { pruneEmptyAncestors, discoverTree, sameFileIndex, sharedTreeDestDir, walkRelFiles } = require('../adapters/copy-tree.js');
|
|
15
|
+
const { renderPolicies } = require('./policies.js');
|
|
16
|
+
const { renderMcpIndex } = require('./mcp-index.js');
|
|
17
|
+
const { hasBashCapableShell } = require('./bash-availability.js');
|
|
18
|
+
const { hookTrustFor: geminiHookTrustFor } = require('../adapters/gemini/index.js');
|
|
19
|
+
const { planHookCompanions } = require('./hook-step.js');
|
|
19
20
|
|
|
20
21
|
const OPERATIONS = new Set(['create', 'merge', 'update', 'remove']);
|
|
21
22
|
/** A harness's MCP selection meaning "the servers it owns now", read through its adapter's
|
|
22
23
|
* `discover().mcpOwned`. */
|
|
23
24
|
const MCP_KEEP = 'keep';
|
|
24
25
|
|
|
25
|
-
function registryScope(scope
|
|
26
|
-
function assertScope(scope
|
|
26
|
+
function registryScope(scope ) { return scope === 'global' ? 'user' : scope; }
|
|
27
|
+
function assertScope(scope ) {
|
|
27
28
|
if (scope !== 'project' && scope !== 'global') throw new Error(`Lifecycle scope must be project or global, got '${scope}'`);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function normalizeTargets(registry
|
|
31
|
-
const wanted = targets && targets.length ? targets : registry.harnesses.map((harness
|
|
31
|
+
function normalizeTargets(registry , targets ) {
|
|
32
|
+
const wanted = targets && targets.length ? targets : registry.harnesses.map((harness ) => harness.id);
|
|
32
33
|
if (!Array.isArray(wanted)) throw new Error('Lifecycle targets must be an array');
|
|
33
34
|
return [...new Set(wanted)].map((id) => harnessFor(registry, id));
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
function identityFor(change
|
|
37
|
+
function identityFor(change ) { return change.ownershipIdentity ?? change.ownership; }
|
|
37
38
|
|
|
38
|
-
function normalizeChange(change
|
|
39
|
+
function normalizeChange(change , { harness, assets } ) {
|
|
39
40
|
if (!change || typeof change !== 'object') throw new Error(`Adapter '${harness.id}' returned a non-object change`);
|
|
40
41
|
const ownershipIdentity = identityFor(change);
|
|
41
42
|
if (change.harness !== undefined && change.harness !== harness.id) throw new Error(`Adapter '${harness.id}' returned a change for '${change.harness}'`);
|
|
42
|
-
if (!assets.some((asset
|
|
43
|
+
if (!assets.some((asset ) => asset.id === change.assetId)) throw new Error(`Adapter '${harness.id}' returned unknown asset '${change.assetId}'`);
|
|
43
44
|
if (!OPERATIONS.has(change.operation)) throw new Error(`Adapter '${harness.id}' returned unsupported operation '${change.operation}'`);
|
|
44
45
|
if (typeof change.target !== 'string' || !change.target) throw new Error(`Adapter '${harness.id}' change '${change.assetId}' requires target`);
|
|
45
46
|
if (typeof ownershipIdentity !== 'string' || !ownershipIdentity) throw new Error(`Adapter '${harness.id}' change '${change.assetId}' requires ownershipIdentity`);
|
|
46
47
|
return Object.freeze({ ...change, harness: harness.id, ownershipIdentity });
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
function normalizeVerification(result
|
|
50
|
+
function normalizeVerification(result , harness ) {
|
|
50
51
|
if (!result || typeof result !== 'object') throw new Error(`Adapter '${harness.id}' returned an invalid verification result`);
|
|
51
52
|
const statuses = Array.isArray(result.statuses) ? result.statuses : [];
|
|
52
53
|
const resources = Array.isArray(result.resources) ? result.resources : [];
|
|
53
54
|
return { ...result, harness: harness.id, statuses, resources };
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
function matchesChange(resource
|
|
57
|
+
function matchesChange(resource , change ) {
|
|
57
58
|
if (resource?.assetId !== change.assetId || resource?.target !== change.target) return false;
|
|
58
59
|
const resourceOwnership = resource?.ownershipIdentity ?? resource?.ownership;
|
|
59
60
|
const changeOwnership = change.ownershipIdentity ?? change.ownership;
|
|
@@ -68,9 +69,9 @@ function matchesChange(resource: any, change: any) {
|
|
|
68
69
|
/** Whether one removed change is genuinely gone, and if so, how `statuses` should record that.
|
|
69
70
|
* `statuses` is read live, not a snapshot — an earlier change in the same removal batch can add
|
|
70
71
|
* or update an entry a later change's own lookup then finds. */
|
|
71
|
-
function reconcileRemovedChange(verification
|
|
72
|
-
const reported = verification.resources.some((resource
|
|
73
|
-
const statusIndex = statuses.findIndex((item
|
|
72
|
+
function reconcileRemovedChange(verification , statuses , change ) {
|
|
73
|
+
const reported = verification.resources.some((resource ) => matchesChange(resource, change));
|
|
74
|
+
const statusIndex = statuses.findIndex((item ) => matchesChange(item, change));
|
|
74
75
|
const status = statusIndex >= 0 ? statuses[statusIndex] : null;
|
|
75
76
|
// 'absent' belongs here alongside 'missing': a shared file (settings.json) survives removal
|
|
76
77
|
// holding the user's own content, and its adapter reports 'absent' once none of DoFlow's
|
|
@@ -101,28 +102,28 @@ function reconcileRemovedChange(verification: any, statuses: any, change: any) {
|
|
|
101
102
|
* 'missing' the ledger *does* claim stays a hard failure — that is a removal which genuinely did
|
|
102
103
|
* not happen, and the existing "unrelated missing remains false" contract depends on it. Without a
|
|
103
104
|
* ledger, non-ownership cannot be proven, so the strict reading is kept. */
|
|
104
|
-
function markUnownedAbsences(statuses
|
|
105
|
+
function markUnownedAbsences(statuses , ledger , harness ) {
|
|
105
106
|
if (!ledger) return statuses;
|
|
106
107
|
const owned = new Set((ledger.resources || [])
|
|
107
|
-
.filter((resource
|
|
108
|
-
.map((resource
|
|
108
|
+
.filter((resource ) => resource.harness === harness)
|
|
109
|
+
.map((resource ) => resource.ownershipIdentity)
|
|
109
110
|
.filter(Boolean));
|
|
110
|
-
return statuses.map((status
|
|
111
|
+
return statuses.map((status ) => (
|
|
111
112
|
status?.status === 'missing' && status.ownershipIdentity && !owned.has(status.ownershipIdentity)
|
|
112
113
|
? { ...status, status: 'absent', expectedAbsence: true }
|
|
113
114
|
: status
|
|
114
115
|
));
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
function normalizeRemovalVerification(verification
|
|
118
|
-
const removals = changes.filter((change
|
|
118
|
+
function normalizeRemovalVerification(verification , changes , ledger ) {
|
|
119
|
+
const removals = changes.filter((change ) => change.operation === 'remove' && change.harness === verification.harness);
|
|
119
120
|
if (!removals.length) return verification;
|
|
120
121
|
// A retained removal released this harness's ownership row without deleting the file, because
|
|
121
122
|
// another harness still claims it (see markRetainedRemovals). The file is therefore *expected*
|
|
122
123
|
// to still be on disk, so asking "is it gone?" of it would turn the correct outcome into a
|
|
123
124
|
// verification conflict. Everything the plan actually deleted is still checked as strictly as
|
|
124
125
|
// before.
|
|
125
|
-
const removed = removals.filter((change
|
|
126
|
+
const removed = removals.filter((change ) => !change.retained);
|
|
126
127
|
let statuses = [...verification.statuses];
|
|
127
128
|
const conflicts = [...(verification.conflicts || [])];
|
|
128
129
|
for (const change of removed) {
|
|
@@ -140,35 +141,35 @@ function normalizeRemovalVerification(verification: any, changes: any, ledger: a
|
|
|
140
141
|
* that exact absence has been normalized (an `expectedRemoval` status), recompute rather than
|
|
141
142
|
* carrying the stale false forward. A bare adapter failure, conflict, or any unrelated
|
|
142
143
|
* missing/error status remains a hard verification failure. */
|
|
143
|
-
function removalVerificationOk(ok
|
|
144
|
-
const unresolvedFailure = statuses.some((status
|
|
145
|
-
const onlyExpectedRemovalFailure = ok === false && statuses.some((status
|
|
144
|
+
function removalVerificationOk(ok , statuses , conflicts ) {
|
|
145
|
+
const unresolvedFailure = statuses.some((status ) => ['missing', 'conflict', 'error', 'invalid'].includes(status?.status));
|
|
146
|
+
const onlyExpectedRemovalFailure = ok === false && statuses.some((status ) => status?.expectedRemoval || status?.expectedAbsence) && !unresolvedFailure && conflicts.length === 0;
|
|
146
147
|
return conflicts.length === 0 && !unresolvedFailure && (ok !== false || onlyExpectedRemovalFailure);
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
/** A `failures` entry comes in three shapes from different adapters: a [name, detail] tuple,
|
|
150
151
|
* a bare string, or an object with its own conflicts/errors. */
|
|
151
|
-
function failureReasons(failure
|
|
152
|
+
function failureReasons(failure ) {
|
|
152
153
|
if (Array.isArray(failure)) {
|
|
153
154
|
const [name, detail] = failure;
|
|
154
155
|
const detailReasons = detail?.conflicts || detail?.errors || [];
|
|
155
|
-
return detailReasons.map((reason
|
|
156
|
+
return detailReasons.map((reason ) => `${name}: ${reason}`);
|
|
156
157
|
}
|
|
157
158
|
if (typeof failure === 'string') return [failure];
|
|
158
159
|
if (failure?.conflicts || failure?.errors) return failure.conflicts || failure.errors;
|
|
159
160
|
return [];
|
|
160
161
|
}
|
|
161
162
|
|
|
162
|
-
function componentReasons(name
|
|
163
|
+
function componentReasons(name , component ) {
|
|
163
164
|
if (component?.ok !== false) return [];
|
|
164
165
|
const details = component.conflicts || component.errors || [];
|
|
165
|
-
return details.length ? details.map((reason
|
|
166
|
+
return details.length ? details.map((reason ) => `${name}: ${reason}`) : [`${name}: native plan failed`];
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
/** Promote adapter-declared failed components into the common conflict channel.
|
|
169
170
|
* A false `ok` is never advisory: otherwise a native adapter can decline a
|
|
170
171
|
* dangerous reconciliation while the lifecycle still journals and writes state. */
|
|
171
|
-
function adapterConflicts(result
|
|
172
|
+
function adapterConflicts(result , harness ) {
|
|
172
173
|
const reasons = [
|
|
173
174
|
...(result.conflicts || []),
|
|
174
175
|
...(result.errors || []),
|
|
@@ -182,7 +183,7 @@ function adapterConflicts(result: any, harness: any) {
|
|
|
182
183
|
/** Every ownership row this plan is about to give up, keyed exactly as the neutral ledger keys it.
|
|
183
184
|
* A removal change always releases its harness's claim; whether it also deletes the file is a
|
|
184
185
|
* separate question, answered by markRetainedRemovals below. */
|
|
185
|
-
function releasedOwnershipKeys(harnessPlans
|
|
186
|
+
function releasedOwnershipKeys(harnessPlans , scope ) {
|
|
186
187
|
const keys = new Set();
|
|
187
188
|
for (const target of harnessPlans) {
|
|
188
189
|
for (const change of target.changes || []) {
|
|
@@ -199,7 +200,7 @@ function releasedOwnershipKeys(harnessPlans: any, scope: any) {
|
|
|
199
200
|
* on this file is not a claimant even if it is releasing nothing else, and a harness in the same
|
|
200
201
|
* batch that keeps its row (an install that relocates one asset while a sibling's projection is
|
|
201
202
|
* unchanged) still is. */
|
|
202
|
-
function survivingClaimantsByTarget(ledger
|
|
203
|
+
function survivingClaimantsByTarget(ledger , releasedKeys ) {
|
|
203
204
|
const byTarget = new Map();
|
|
204
205
|
for (const resource of ledger?.resources || []) {
|
|
205
206
|
if (typeof resource?.target !== 'string' || typeof resource?.harness !== 'string') continue;
|
|
@@ -213,7 +214,7 @@ function survivingClaimantsByTarget(ledger: any, releasedKeys: any) {
|
|
|
213
214
|
|
|
214
215
|
/** What one claim covers: a whole file, except an MCP entry, which is claimed on its own because
|
|
215
216
|
* several harnesses keep their servers side by side in one MCP file. */
|
|
216
|
-
function claimKey(record
|
|
217
|
+
function claimKey(record ) {
|
|
217
218
|
return record.kind === 'mcp-server' ? `${record.target}\u0000${record.identity}` : record.target;
|
|
218
219
|
}
|
|
219
220
|
|
|
@@ -232,13 +233,13 @@ function claimKey(record: any) {
|
|
|
232
233
|
* stays, and the last claimant's removal is the one that reclaims it. The neutral ledger is the
|
|
233
234
|
* only thing consulted — it is already the record of who owns what, and a second ownership
|
|
234
235
|
* registry would be one more thing to keep in sync. */
|
|
235
|
-
function markRetainedRemovals(harnessPlans
|
|
236
|
+
function markRetainedRemovals(harnessPlans , ledger , scope ) {
|
|
236
237
|
const claimants = survivingClaimantsByTarget(ledger, releasedOwnershipKeys(harnessPlans, scope));
|
|
237
238
|
if (!claimants.size) return harnessPlans;
|
|
238
|
-
return harnessPlans.map((target
|
|
239
|
+
return harnessPlans.map((target ) => {
|
|
239
240
|
if (target.skipped) return target;
|
|
240
241
|
let retainedAny = false;
|
|
241
|
-
const changes = target.changes.map((change
|
|
242
|
+
const changes = target.changes.map((change ) => {
|
|
242
243
|
if (change.operation !== 'remove') return change;
|
|
243
244
|
const retainedFor = [...(claimants.get(claimKey(change)) ?? [])].filter((harness) => harness !== change.harness).sort();
|
|
244
245
|
if (!retainedFor.length) return change;
|
|
@@ -255,15 +256,15 @@ function markRetainedRemovals(harnessPlans: any, ledger: any, scope: any) {
|
|
|
255
256
|
|
|
256
257
|
/** Whether this plan leaves no ownership row standing anywhere — the only case in which a shared,
|
|
257
258
|
* install-wide artifact that no single harness owns is genuinely unclaimed. */
|
|
258
|
-
function removalIsTotal(plan
|
|
259
|
+
function removalIsTotal(plan ) {
|
|
259
260
|
const released = releasedOwnershipKeys(plan.targets || [], plan.scope);
|
|
260
|
-
return (plan.ledger?.resources || []).every((resource
|
|
261
|
+
return (plan.ledger?.resources || []).every((resource ) => released.has(ownershipKey(resource)));
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
/** What a removal kept and why, ready to print. A command that says "removed" while silently
|
|
264
265
|
* leaving files behind is answering confidently about something it did not do, so the retained
|
|
265
266
|
* count and the harnesses responsible for it are reported, never inferred by the caller. */
|
|
266
|
-
function retentionSummary(retained
|
|
267
|
+
function retentionSummary(retained = []) {
|
|
267
268
|
const groups = new Map();
|
|
268
269
|
for (const item of retained) {
|
|
269
270
|
// No claimant: a release that is reported on its own line (a kept hand edit) or is silent
|
|
@@ -278,26 +279,26 @@ function retentionSummary(retained: any = []) {
|
|
|
278
279
|
});
|
|
279
280
|
}
|
|
280
281
|
|
|
281
|
-
function retainedChanges(changes
|
|
282
|
-
return changes.filter((change
|
|
283
|
-
.map((change
|
|
282
|
+
function retainedChanges(changes ) {
|
|
283
|
+
return changes.filter((change ) => change.retained)
|
|
284
|
+
.map((change ) => ({ harness: change.harness, assetId: change.assetId, target: change.target, retainedFor: change.retainedFor }));
|
|
284
285
|
}
|
|
285
286
|
|
|
286
287
|
/** An adapter's non-blocking notes: one line of at most 200 characters each. A malformed entry is
|
|
287
288
|
* dropped rather than failing the plan, because a notice must never change whether an install runs. */
|
|
288
|
-
function adapterNotices(result
|
|
289
|
+
function adapterNotices(result ) {
|
|
289
290
|
return (Array.isArray(result.notices) ? result.notices : [])
|
|
290
|
-
.filter((notice
|
|
291
|
+
.filter((notice ) => typeof notice === 'string' && notice.length <= 200 && !/[\u0000-\u001f\u007f]/.test(notice));
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
/** The ids of `ids` the catalog declares, in registry order. */
|
|
294
|
-
function catalogIds(registry
|
|
295
|
-
return registry.mcp.filter((server
|
|
295
|
+
function catalogIds(registry , ids ) {
|
|
296
|
+
return registry.mcp.filter((server ) => ids.includes(server.id)).map((server ) => server.id);
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
/** Each harness takes its own MCP selection: an id array, or MCP_KEEP for the servers it owns now.
|
|
299
300
|
* A harness that takes no MCP servers reads `keep` and an absent entry as none and refuses any id. */
|
|
300
|
-
function harnessMcpSelection(registry
|
|
301
|
+
function harnessMcpSelection(registry , harness , raw ) {
|
|
301
302
|
const value = raw ?? [];
|
|
302
303
|
if (value !== MCP_KEEP && !(Array.isArray(value) && value.every((id) => typeof id === 'string'))) {
|
|
303
304
|
throw new Error(`MCP selection for '${harness.id}' must be an array of server ids or '${MCP_KEEP}'`);
|
|
@@ -315,22 +316,22 @@ function harnessMcpSelection(registry: any, harness: any, raw: any) {
|
|
|
315
316
|
* right for them. */
|
|
316
317
|
const RUNTIME_TREE_ASSETS = ['runtime.cli', 'runtime.lib', 'runtime.registry'];
|
|
317
318
|
|
|
318
|
-
function sha256File(file
|
|
319
|
+
function sha256File(file , fsImpl ) {
|
|
319
320
|
return crypto.createHash('sha256').update(fsImpl.readFileSync(file)).digest('hex');
|
|
320
321
|
}
|
|
321
322
|
|
|
322
|
-
function isUnder(file
|
|
323
|
+
function isUnder(file , root ) {
|
|
323
324
|
return file.startsWith(`${root}${path.sep}`);
|
|
324
325
|
}
|
|
325
326
|
|
|
326
327
|
/** Replace some of a harness plan's changes and append others, keeping `requiredNativeResources`
|
|
327
328
|
* in step: the same array when it is one (as markRetainedRemovals does), else with the added
|
|
328
329
|
* changes listed, so a plan holding only those is still applicable (codex lists its own). */
|
|
329
|
-
function withChanges(target
|
|
330
|
+
function withChanges(target , replaced , added , notices ) {
|
|
330
331
|
if (!replaced.size && !added.length && !notices.length) return target;
|
|
331
|
-
const changes = [...target.changes.map((change
|
|
332
|
+
const changes = [...target.changes.map((change ) => replaced.get(change) ?? change), ...added];
|
|
332
333
|
const requiredNativeResources = target.requiredNativeResources === target.changes ? changes
|
|
333
|
-
: [...target.requiredNativeResources, ...added.map((change
|
|
334
|
+
: [...target.requiredNativeResources, ...added.map((change ) => ({ harness: change.harness, component: 'runtimePrune', target: change.target, operation: change.operation, identity: change.identity }))];
|
|
334
335
|
return { ...target, changes, requiredNativeResources, notices: [...target.notices, ...notices] };
|
|
335
336
|
}
|
|
336
337
|
|
|
@@ -353,92 +354,92 @@ function withChanges(target: any, replaced: Map<any, any>, added: any[], notices
|
|
|
353
354
|
* removeTree re-hashes every file just before deleting it, as for any removal, and for the removals
|
|
354
355
|
* this pass plans (`pruneGuard`) re-runs the containment and same-file checks there too.
|
|
355
356
|
*/
|
|
356
|
-
function pruneRetiredRuntimeFiles(harnessPlans
|
|
357
|
-
const planned = harnessPlans.filter((target
|
|
357
|
+
function pruneRetiredRuntimeFiles(harnessPlans , ledger , { scopeRoot, repoRoot, fsImpl = fs } ) {
|
|
358
|
+
const planned = harnessPlans.filter((target ) => !target.skipped);
|
|
358
359
|
if (!planned.length || !repoRoot) return harnessPlans;
|
|
359
|
-
const replaced = new Map
|
|
360
|
-
const added = new Map
|
|
361
|
-
const notices = new Map
|
|
362
|
-
const rows = (ledger?.resources ?? []).filter((row
|
|
363
|
-
const recorded = new Set(rows.map((row
|
|
364
|
-
const shown = (file
|
|
360
|
+
const replaced = new Map (planned.map((target ) => [target.harness, new Map()]));
|
|
361
|
+
const added = new Map (planned.map((target ) => [target.harness, []]));
|
|
362
|
+
const notices = new Map (planned.map((target ) => [target.harness, []]));
|
|
363
|
+
const rows = (ledger?.resources ?? []).filter((row ) => typeof row?.target === 'string');
|
|
364
|
+
const recorded = new Set(rows.map((row ) => row.target));
|
|
365
|
+
const shown = (file ) => path.relative(scopeRoot, file);
|
|
365
366
|
|
|
366
367
|
for (const assetId of RUNTIME_TREE_ASSETS) {
|
|
367
|
-
const executor = planned.find((target
|
|
368
|
+
const executor = planned.find((target ) => target.assets.some((asset ) => asset.id === assetId));
|
|
368
369
|
if (!executor) continue;
|
|
369
|
-
const asset = executor.assets.find((item
|
|
370
|
+
const asset = executor.assets.find((item ) => item.id === assetId);
|
|
370
371
|
const nativeDir = typeof asset.nativeDir === 'string' ? asset.nativeDir : asset.nativeDir?.[executor.harness];
|
|
371
372
|
const destDir = sharedTreeDestDir(scopeRoot, nativeDir);
|
|
372
373
|
const sourceDir = path.resolve(repoRoot, asset.source);
|
|
373
374
|
if (!destDir || !fsImpl.existsSync(sourceDir) || !fsImpl.existsSync(destDir)) continue;
|
|
374
|
-
const produced = discoverTree({ sourceDir, destDir, fsImpl }).files.map((file
|
|
375
|
+
const produced = discoverTree({ sourceDir, destDir, fsImpl }).files.map((file ) => file.destAbs);
|
|
375
376
|
const producedSet = new Set(produced);
|
|
376
377
|
const sameFile = sameFileIndex(produced, fsImpl);
|
|
377
378
|
const realRoot = fsImpl.realpathSync(destDir);
|
|
378
379
|
|
|
379
|
-
const rowsAt = new Map
|
|
380
|
+
const rowsAt = new Map ();
|
|
380
381
|
for (const row of rows) {
|
|
381
382
|
if (!isUnder(row.target, destDir) || producedSet.has(row.target)) continue;
|
|
382
383
|
if (!rowsAt.has(row.target)) rowsAt.set(row.target, []);
|
|
383
|
-
rowsAt.get(row.target)
|
|
384
|
+
rowsAt.get(row.target) .push(row);
|
|
384
385
|
}
|
|
385
|
-
const removalsAt = new Map
|
|
386
|
+
const removalsAt = new Map ();
|
|
386
387
|
for (const target of planned) {
|
|
387
388
|
for (const change of target.changes) {
|
|
388
389
|
if (change.operation !== 'remove' || change.assetId !== assetId || !isUnder(change.target, destDir) || producedSet.has(change.target)) continue;
|
|
389
390
|
if (!removalsAt.has(change.target)) removalsAt.set(change.target, []);
|
|
390
|
-
removalsAt.get(change.target)
|
|
391
|
+
removalsAt.get(change.target) .push(change);
|
|
391
392
|
}
|
|
392
393
|
}
|
|
393
394
|
/** Turn every planned removal of `file` into a release that deletes nothing. */
|
|
394
|
-
const keepFile = (file
|
|
395
|
+
const keepFile = (file ) => {
|
|
395
396
|
for (const change of removalsAt.get(file) ?? []) {
|
|
396
|
-
if (!change.retained) replaced.get(change.harness)
|
|
397
|
+
if (!change.retained) replaced.get(change.harness) .set(change, Object.freeze({ ...change, retained: true, retainedFor: [] }));
|
|
397
398
|
}
|
|
398
399
|
};
|
|
399
400
|
|
|
400
401
|
for (const file of new Set([...rowsAt.keys(), ...removalsAt.keys()])) {
|
|
401
402
|
const fileRows = rowsAt.get(file) ?? [];
|
|
402
403
|
// Another asset's live row on this path is a claim this pass does not judge.
|
|
403
|
-
if (fileRows.some((row
|
|
404
|
+
if (fileRows.some((row ) => row.assetId !== assetId)) continue;
|
|
404
405
|
const removals = removalsAt.get(file) ?? [];
|
|
405
406
|
const identity = path.relative(destDir, file);
|
|
406
407
|
// The rows no planned removal of this file releases on its own.
|
|
407
|
-
const superseded = fileRows.filter((row
|
|
408
|
-
const synthetic = (extra
|
|
408
|
+
const superseded = fileRows.filter((row ) => !removals.some((change ) => change.harness === row.harness)).map((row ) => ownershipKey(row));
|
|
409
|
+
const synthetic = (extra ) => Object.freeze({
|
|
409
410
|
harness: executor.harness, assetId, target: file, operation: 'remove', kind: 'copy-tree-file', identity,
|
|
410
411
|
ownershipIdentity: `doflow:${executor.harness}:copy-tree:${assetId}:${identity}`, projection: { renderer: 'copy-tree' }, ...extra,
|
|
411
412
|
});
|
|
412
413
|
|
|
413
414
|
if (!fsImpl.existsSync(file)) {
|
|
414
415
|
// Already gone (an interrupted run, a user's delete): the rows only point at nothing.
|
|
415
|
-
if (fileRows.length) added.get(executor.harness)
|
|
416
|
+
if (fileRows.length) added.get(executor.harness) .push(synthetic({ fingerprint: null, retained: true, retainedFor: [], supersedes: superseded }));
|
|
416
417
|
continue;
|
|
417
418
|
}
|
|
418
419
|
const real = fsImpl.realpathSync(file);
|
|
419
420
|
if (!isUnder(real, realRoot)) {
|
|
420
421
|
keepFile(file);
|
|
421
|
-
notices.get(executor.harness)
|
|
422
|
+
notices.get(executor.harness) .push(`kept ${shown(file)}: no longer shipped, but it resolves outside ${shown(destDir)}`);
|
|
422
423
|
continue;
|
|
423
424
|
}
|
|
424
425
|
if (sameFile(file)) { keepFile(file); continue; } // the produced file under an old spelling
|
|
425
426
|
const current = sha256File(file, fsImpl);
|
|
426
|
-
if (!fileRows.some((row
|
|
427
|
+
if (!fileRows.some((row ) => row.fingerprint === current)) {
|
|
427
428
|
// A planned removal here was already kept by planTree, and its adapter named it.
|
|
428
|
-
if (!removals.length) notices.get(executor.harness)
|
|
429
|
+
if (!removals.length) notices.get(executor.harness) .push(`kept ${shown(file)}: no longer shipped, changed since DoFlow wrote it`);
|
|
429
430
|
continue;
|
|
430
431
|
}
|
|
431
|
-
notices.get(executor.harness)
|
|
432
|
+
notices.get(executor.harness) .push(`prune ${shown(file)}: no longer shipped, unchanged since DoFlow wrote it`);
|
|
432
433
|
// removeTree re-runs containment and the same-file check from this right before the unlink.
|
|
433
434
|
const pruneGuard = { root: destDir, source: sourceDir };
|
|
434
435
|
if (!removals.length) {
|
|
435
|
-
added.get(executor.harness)
|
|
436
|
+
added.get(executor.harness) .push(synthetic({ fingerprint: current, supersedes: superseded, pruneGuard }));
|
|
436
437
|
continue;
|
|
437
438
|
}
|
|
438
|
-
removals.forEach((change
|
|
439
|
+
removals.forEach((change , index ) => {
|
|
439
440
|
const { retained, retainedFor, ...rest } = change;
|
|
440
441
|
void retained; void retainedFor;
|
|
441
|
-
replaced.get(change.harness)
|
|
442
|
+
replaced.get(change.harness) .set(change, Object.freeze({ ...rest, fingerprint: current, pruneGuard, ...(index === 0 ? { supersedes: superseded } : {}) }));
|
|
442
443
|
});
|
|
443
444
|
}
|
|
444
445
|
|
|
@@ -446,14 +447,26 @@ function pruneRetiredRuntimeFiles(harnessPlans: any[], ledger: any, { scopeRoot,
|
|
|
446
447
|
for (const rel of walkRelFiles(destDir, fsImpl)) {
|
|
447
448
|
const file = path.join(destDir, rel);
|
|
448
449
|
if (producedSet.has(file) || recorded.has(file) || sameFile(file)) continue;
|
|
449
|
-
notices.get(executor.harness)
|
|
450
|
+
notices.get(executor.harness) .push(`kept ${shown(file)}: not shipped by this version and not recorded as DoFlow's`);
|
|
450
451
|
}
|
|
451
452
|
}
|
|
452
|
-
return harnessPlans.map((target
|
|
453
|
-
: withChanges(target, replaced.get(target.harness)
|
|
453
|
+
return harnessPlans.map((target ) => (target.skipped ? target
|
|
454
|
+
: withChanges(target, replaced.get(target.harness) , added.get(target.harness) , notices.get(target.harness) )));
|
|
454
455
|
}
|
|
455
456
|
|
|
456
|
-
|
|
457
|
+
/** One harness's plan entry, from what its adapter returned: the changes checked against the
|
|
458
|
+
* harness's assets, and the conflicts, prerequisites and notices in the lifecycle's common shape. */
|
|
459
|
+
function targetFromResult({ harness, assets, mcp, mcpSelected, policies, adapterInput, discovery, result } ) {
|
|
460
|
+
if (!result || typeof result !== 'object') throw new Error(`Adapter '${harness.id}' returned an invalid plan`);
|
|
461
|
+
const changes = (result.changes || []).map((change ) => normalizeChange(change, { harness, assets }));
|
|
462
|
+
const conflicts = adapterConflicts(result, harness);
|
|
463
|
+
const prerequisites = [...(result.prerequisites || []), ...changes.map((change ) => change.prerequisite).filter(Boolean)];
|
|
464
|
+
const requiredNativeResources = result.requiredNativeResources ?? changes;
|
|
465
|
+
if (!Array.isArray(requiredNativeResources)) throw new Error(`Adapter '${harness.id}' returned invalid requiredNativeResources`);
|
|
466
|
+
return { harness: harness.id, adapter: harness.adapter, assets, mcp, mcpSelected, policies, adapterInput, discovery, changes, requiredNativeResources, conflicts, prerequisites, notices: adapterNotices(result), skipped: false };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelections = {}, mcpAdoptable = {}, retainedMcpIds = [], ledger, context = {} } ) {
|
|
457
470
|
assertScope(scope);
|
|
458
471
|
if (!registry) throw new Error('registry is required');
|
|
459
472
|
const baseLedger = ledger ?? defaultLedger({ scope, scopeRoot });
|
|
@@ -467,7 +480,7 @@ function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelec
|
|
|
467
480
|
const policies = renderPolicies(registry, { harness: harness.id });
|
|
468
481
|
const adoptable = capable ? selectMcpServers(registry, catalogIds(registry, mcpAdoptable[harness.id] ?? [])) : [];
|
|
469
482
|
const adapter = resolveAdapter(adapters, harness);
|
|
470
|
-
const project = (mcp
|
|
483
|
+
const project = (mcp ) => projectAdapterInput({ registry, harness, scope, scopeRoot, assets, mcp, mcpAdoptable: adoptable, policies, context });
|
|
471
484
|
// `keep` asks the adapter what it owns before the real projection: the probe carries no
|
|
472
485
|
// selection, so discovery reads the files as they are.
|
|
473
486
|
const ids = selection === MCP_KEEP
|
|
@@ -478,13 +491,7 @@ function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelec
|
|
|
478
491
|
const input = { ...adapterInput, registry, ledger: baseLedger };
|
|
479
492
|
const discovery = adapter.discover(input);
|
|
480
493
|
const result = adapter.plan({ ...input, discovery });
|
|
481
|
-
|
|
482
|
-
const changes = (result.changes || []).map((change: any) => normalizeChange(change, { harness, assets }));
|
|
483
|
-
const conflicts = adapterConflicts(result, harness);
|
|
484
|
-
const prerequisites = [...(result.prerequisites || []), ...changes.map((change: any) => change.prerequisite).filter(Boolean)];
|
|
485
|
-
const requiredNativeResources = result.requiredNativeResources ?? changes;
|
|
486
|
-
if (!Array.isArray(requiredNativeResources)) throw new Error(`Adapter '${harness.id}' returned invalid requiredNativeResources`);
|
|
487
|
-
return { harness: harness.id, adapter: harness.adapter, assets, mcp, mcpSelected: capable ? mcp.map((server: any) => server.id) : null, policies, adapterInput, discovery, changes, requiredNativeResources, conflicts, prerequisites, notices: adapterNotices(result), skipped: false };
|
|
494
|
+
return targetFromResult({ harness, assets, mcp, mcpSelected: capable ? mcp.map((server ) => server.id) : null, policies, adapterInput, discovery, result });
|
|
488
495
|
});
|
|
489
496
|
// The MCP index names every server a harness of this scope uses: the selections planned here and
|
|
490
497
|
// the recorded selections of the harnesses this run does not touch.
|
|
@@ -495,18 +502,23 @@ function planLifecycle({ registry, adapters, scope, scopeRoot, targets, mcpSelec
|
|
|
495
502
|
const retained = markRetainedRemovals(harnessPlans, baseLedger, scope);
|
|
496
503
|
const annotated = context.operation === 'remove' ? retained
|
|
497
504
|
: pruneRetiredRuntimeFiles(retained, baseLedger, { scopeRoot, repoRoot: context.repoRoot });
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
|
|
505
|
+
// The shared hooks tree may have moved under harnesses this run does not name (IC-037): their
|
|
506
|
+
// front doors come along, as targets of this same plan.
|
|
507
|
+
const companions = planHookCompanions({ registry, adapters, scope, harnessScope: registryScope(scope), scopeRoot, ledger: baseLedger, context,
|
|
508
|
+
targets: annotated, targetFromResult });
|
|
509
|
+
const planned = [...annotated, ...companions];
|
|
510
|
+
const changes = planned.flatMap((item ) => item.changes);
|
|
511
|
+
const conflicts = planned.flatMap((item ) => item.conflicts.map((reason ) => ({ harness: item.harness, reason })));
|
|
512
|
+
const prerequisites = planned.flatMap((item ) => item.prerequisites.map((prerequisite ) => ({ harness: item.harness, prerequisite })));
|
|
513
|
+
const requiredNativeResources = planned.flatMap((item ) => item.requiredNativeResources || []);
|
|
514
|
+
const notices = planned.flatMap((item ) => item.notices.map((notice ) => ({ harness: item.harness, notice })));
|
|
515
|
+
return Object.freeze({ scope, scopeRoot, mcp: selectedMcp, ledger: baseLedger, targets: planned, changes, requiredNativeResources, conflicts, prerequisites, notices,
|
|
504
516
|
retained: retainedChanges(changes), safe: conflicts.length === 0 && prerequisites.length === 0 });
|
|
505
517
|
}
|
|
506
518
|
|
|
507
|
-
function verificationResources(verification
|
|
508
|
-
if (verification.resources.length) return verification.resources.map((resource
|
|
509
|
-
return changes.filter((change
|
|
519
|
+
function verificationResources(verification , changes , scope , recoveryRef ) {
|
|
520
|
+
if (verification.resources.length) return verification.resources.map((resource ) => ({ ...resource, harness: verification.harness, scope, recoveryRef: resource.recoveryRef ?? recoveryRef }));
|
|
521
|
+
return changes.filter((change ) => change.operation !== 'remove').map((change ) => ({
|
|
510
522
|
harness: change.harness, scope, assetId: change.assetId, target: change.target,
|
|
511
523
|
ownershipIdentity: change.ownershipIdentity, fingerprint: change.afterFingerprint ?? null,
|
|
512
524
|
sourceVersion: change.sourceVersion ?? 'unknown', projection: change.projection ?? null, recoveryRef,
|
|
@@ -516,7 +528,7 @@ function verificationResources(verification: any, changes: any, scope: any, reco
|
|
|
516
528
|
/** The guidance-content layer's own version marker (core/shared/guidance/VERSION, copy-treed
|
|
517
529
|
* to <scopeRoot>/.doflow/guidance/VERSION) is independent of this package's version — read it
|
|
518
530
|
* fresh on every ledger update so the ledger reflects whatever is actually on disk. */
|
|
519
|
-
function readGuidanceVersion(scopeRoot
|
|
531
|
+
function readGuidanceVersion(scopeRoot , fsImpl = fs) {
|
|
520
532
|
const file = path.join(scopeRoot, '.doflow', 'guidance', 'VERSION');
|
|
521
533
|
if (!fsImpl.existsSync(file)) return null;
|
|
522
534
|
return fsImpl.readFileSync(file, 'utf8').trim();
|
|
@@ -532,7 +544,7 @@ function readGuidanceVersion(scopeRoot: any, fsImpl = fs) {
|
|
|
532
544
|
* same anchor DOFLOW_CORE.md's own @-imports use. Writing this file into a subdirectory would
|
|
533
545
|
* silently reinterpret every one of those paths against that subdirectory and break them all,
|
|
534
546
|
* with no error at install time. test/mcp-index.test.js pins the anchor from both ends. */
|
|
535
|
-
function mcpIndexPath(scopeRoot
|
|
547
|
+
function mcpIndexPath(scopeRoot ) {
|
|
536
548
|
return path.join(scopeRoot, '.doflow', 'guidance', 'MCP_INDEX.md');
|
|
537
549
|
}
|
|
538
550
|
|
|
@@ -547,7 +559,7 @@ function mcpIndexPath(scopeRoot: any) {
|
|
|
547
559
|
* shared runtime, in the one asset that is generated rather than ledger-tracked and so cannot be
|
|
548
560
|
* protected by ownership counting. The remaining harnesses' MCP selection has not changed, so the
|
|
549
561
|
* file is left exactly as it is rather than re-rendered from this removal's empty selection. */
|
|
550
|
-
function applyMcpIndex({ scopeRoot, selectedMcp, mode, retain = false, fsImpl = fs }
|
|
562
|
+
function applyMcpIndex({ scopeRoot, selectedMcp, mode, retain = false, fsImpl = fs } ) {
|
|
551
563
|
const file = mcpIndexPath(scopeRoot);
|
|
552
564
|
const content = mode === 'remove' ? null : renderMcpIndex(selectedMcp);
|
|
553
565
|
if (content === null) {
|
|
@@ -559,7 +571,7 @@ function applyMcpIndex({ scopeRoot, selectedMcp, mode, retain = false, fsImpl =
|
|
|
559
571
|
fsImpl.writeFileSync(file, content);
|
|
560
572
|
}
|
|
561
573
|
|
|
562
|
-
function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recoveryRef, fsImpl = fs }
|
|
574
|
+
function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recoveryRef, fsImpl = fs } ) {
|
|
563
575
|
// upgradeLedger makes every written ledger v2-shaped (tombstone log present) while v1 inputs
|
|
564
576
|
// stay readable — migration happens on write, never as a separate user-facing step.
|
|
565
577
|
const next = JSON.parse(JSON.stringify(upgradeLedger(ledger ?? defaultLedger({ scope, scopeRoot }))));
|
|
@@ -571,13 +583,13 @@ function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recove
|
|
|
571
583
|
priorByIdentity.set(`${resource.harness}\u0000${resource.assetId}\u0000${resource.identity ?? ''}`, resource);
|
|
572
584
|
}
|
|
573
585
|
for (const verification of verifications) {
|
|
574
|
-
const related = changes.filter((change
|
|
575
|
-
const removed = related.filter((change
|
|
576
|
-
const removeKeys = new Set(removed.map((change
|
|
586
|
+
const related = changes.filter((change ) => change.harness === verification.harness);
|
|
587
|
+
const removed = related.filter((change ) => change.operation === 'remove');
|
|
588
|
+
const removeKeys = new Set(removed.map((change ) => ownershipKey({ harness: change.harness, scope, assetId: change.assetId, target: change.target, ownershipIdentity: change.ownershipIdentity })));
|
|
577
589
|
// A runtime prune also gives up the rows other harnesses hold on the file it deletes
|
|
578
590
|
// (pruneRetiredRuntimeFiles): released here, in the same write, so no row outlives its file.
|
|
579
591
|
for (const change of removed) for (const key of change.supersedes ?? []) removeKeys.add(key);
|
|
580
|
-
next.resources = next.resources.filter((resource
|
|
592
|
+
next.resources = next.resources.filter((resource ) => !removeKeys.has(ownershipKey(resource)));
|
|
581
593
|
for (const resource of verificationResources(verification, related, scope, recoveryRef)) {
|
|
582
594
|
const key = ownershipKey(resource);
|
|
583
595
|
// A verifier enumerates what is on disk, and a released-but-retained file is deliberately
|
|
@@ -590,9 +602,9 @@ function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recove
|
|
|
590
602
|
const prior = priorByIdentity.get(`${resource.harness}\u0000${resource.assetId}\u0000${resource.identity ?? ''}`);
|
|
591
603
|
if (prior && prior.target !== resource.target) {
|
|
592
604
|
const supersededKey = ownershipKey(prior);
|
|
593
|
-
next.resources = next.resources.filter((item
|
|
605
|
+
next.resources = next.resources.filter((item ) => ownershipKey(item) !== supersededKey);
|
|
594
606
|
}
|
|
595
|
-
const index = next.resources.findIndex((item
|
|
607
|
+
const index = next.resources.findIndex((item ) => ownershipKey(item) === key);
|
|
596
608
|
if (index >= 0) next.resources[index] = resource;
|
|
597
609
|
else next.resources.push(resource);
|
|
598
610
|
}
|
|
@@ -605,7 +617,7 @@ function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recove
|
|
|
605
617
|
for (const resource of next.resources) {
|
|
606
618
|
const prior = priorByIdentity.get(`${resource.harness}\u0000${resource.assetId}\u0000${resource.identity ?? ''}`);
|
|
607
619
|
if (!prior || prior.target === resource.target) continue;
|
|
608
|
-
if (!next.tombstones.some((entry
|
|
620
|
+
if (!next.tombstones.some((entry ) => entry.harness === resource.harness
|
|
609
621
|
&& entry.fromTarget === prior.target && entry.toTarget === resource.target)) {
|
|
610
622
|
next.tombstones.push({
|
|
611
623
|
harness: resource.harness, assetId: resource.assetId,
|
|
@@ -614,7 +626,7 @@ function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recove
|
|
|
614
626
|
});
|
|
615
627
|
}
|
|
616
628
|
}
|
|
617
|
-
const claims = new Set(next.resources.map((resource
|
|
629
|
+
const claims = new Set(next.resources.map((resource ) => resource.target));
|
|
618
630
|
for (const entry of next.tombstones) {
|
|
619
631
|
if (entry.sweptAt || !fsImpl.existsSync(entry.fromTarget) || claims.has(entry.fromTarget)) continue;
|
|
620
632
|
try {
|
|
@@ -632,7 +644,7 @@ function updateLedger({ ledger, scope, scopeRoot, verifications, changes, recove
|
|
|
632
644
|
return next;
|
|
633
645
|
}
|
|
634
646
|
|
|
635
|
-
function verifyLifecycle({ plan, adapters, context = {} }
|
|
647
|
+
function verifyLifecycle({ plan, adapters, context = {} } ) {
|
|
636
648
|
const verifications = [];
|
|
637
649
|
for (const target of plan.targets) {
|
|
638
650
|
if (target.skipped) continue;
|
|
@@ -642,19 +654,21 @@ function verifyLifecycle({ plan, adapters, context = {} }: any) {
|
|
|
642
654
|
// After an apply, the files this harness just wrote: a copy-tree verifier reports one that is
|
|
643
655
|
// missing instead of skipping it like a file the user deleted.
|
|
644
656
|
const plannedWrites = context.checkPlannedWrites
|
|
645
|
-
? target.changes.filter((change
|
|
657
|
+
? target.changes.filter((change ) => !change.retained && (change.operation === 'create' || change.operation === 'update')).map((change ) => change.target)
|
|
646
658
|
: undefined;
|
|
647
|
-
|
|
659
|
+
// A hook-front-door companion (IC-037) is verified for its doors alone: the rest of that
|
|
660
|
+
// harness is not part of this run and may legitimately trail the current source.
|
|
661
|
+
let verification = normalizeVerification(adapter[target.hookDoorsOnly ? 'verifyHookDoors' : 'verify']({ ...target.adapterInput,
|
|
648
662
|
registry: context.registry ?? plan.registry, discovery: target.discovery, ledger: plan.ledger,
|
|
649
663
|
operation, context: { ...target.adapterInput.context, operation, ...(plannedWrites ? { plannedWrites } : {}) } }), harness);
|
|
650
664
|
if (operation === 'remove') verification = normalizeRemovalVerification(verification, target.changes, plan.ledger);
|
|
651
665
|
verifications.push({ ...verification, hookWiring: hookWiringStatus(harness, target, verification) });
|
|
652
666
|
}
|
|
653
|
-
const conflicts = verifications.flatMap((result) => (result.conflicts || []).map((reason
|
|
667
|
+
const conflicts = verifications.flatMap((result) => (result.conflicts || []).map((reason ) => ({ harness: result.harness, reason })));
|
|
654
668
|
return { verifications, conflicts, ok: conflicts.length === 0 && verifications.every((result) => result.ok !== false) };
|
|
655
669
|
}
|
|
656
670
|
|
|
657
|
-
function assertPlanApplicable(plan
|
|
671
|
+
function assertPlanApplicable(plan , stateRoot , acceptPrerequisites ) {
|
|
658
672
|
if (!plan.safe && !(acceptPrerequisites && plan.conflicts.length === 0)) throw new Error('Refusing to apply an unsafe lifecycle plan');
|
|
659
673
|
if (!stateRoot) throw new Error('stateRoot is required before lifecycle mutation');
|
|
660
674
|
if (!plan.requiredNativeResources?.length || !plan.changes.length) throw new Error('Refusing to apply a lifecycle plan with no required native resources');
|
|
@@ -664,35 +678,35 @@ function assertPlanApplicable(plan: any, stateRoot: any, acceptPrerequisites: an
|
|
|
664
678
|
* plan selected — currently only claude.hooks-scripts and kiro.hooks-scripts. Codex and Gemini
|
|
665
679
|
* deploy hooks via their own bespoke code paths (see targetNeedsHooks below) rather than a
|
|
666
680
|
* registered asset, so they never appear here. */
|
|
667
|
-
function hookAssetIds(target
|
|
668
|
-
return new Set((target.assets || []).filter((asset
|
|
681
|
+
function hookAssetIds(target ) {
|
|
682
|
+
return new Set((target.assets || []).filter((asset ) => asset.kind === 'hooks').map((asset ) => asset.id));
|
|
669
683
|
}
|
|
670
684
|
|
|
671
685
|
/** Whether this harness's plan includes at least one hooks-bearing change. Claude's hooks are a
|
|
672
686
|
* registry-declared asset (`kind: "hooks"` in core/registry/assets.json); Codex and Gemini deploy
|
|
673
687
|
* hooks via their own bespoke code paths and tag the resulting change with `nativeComponent:
|
|
674
|
-
* 'hooks'` instead (see src/adapters/codex/index.
|
|
688
|
+
* 'hooks'` instead (see src/adapters/codex/index.js and src/adapters/gemini/index.js). Either
|
|
675
689
|
* signal is sufficient — this stays harness-agnostic on purpose. */
|
|
676
|
-
function targetNeedsHooks(target
|
|
690
|
+
function targetNeedsHooks(target ) {
|
|
677
691
|
const hookIds = hookAssetIds(target);
|
|
678
|
-
return (target.changes || []).some((change
|
|
692
|
+
return (target.changes || []).some((change ) => hookIds.has(change.assetId) || change.nativeComponent === 'hooks');
|
|
679
693
|
}
|
|
680
694
|
|
|
681
695
|
/** Whether a verified resource is the hooks resource this harness owns. A registry-declared hooks
|
|
682
696
|
* asset (Claude, Kiro) is matched by assetId; Codex's bespoke hooks-file resource is matched by
|
|
683
697
|
* `kind`; Gemini's bespoke hooks resource (a key inside settings.json, not a standalone file) is
|
|
684
|
-
* matched by its `projection.renderer` tag — see src/adapters/codex/index.
|
|
685
|
-
* `lifecycleResource(..., kind: 'hooks-file', ...)` and src/adapters/gemini/index.
|
|
698
|
+
* matched by its `projection.renderer` tag — see src/adapters/codex/index.js's
|
|
699
|
+
* `lifecycleResource(..., kind: 'hooks-file', ...)` and src/adapters/gemini/index.js's verify(),
|
|
686
700
|
* which tags its hooks resource `projection: { renderer: 'gemini-hooks' }`. */
|
|
687
|
-
function verificationOwnsHooks(verification
|
|
688
|
-
return (verification?.resources || []).some((resource
|
|
701
|
+
function verificationOwnsHooks(verification , hookIds ) {
|
|
702
|
+
return (verification?.resources || []).some((resource ) =>
|
|
689
703
|
hookIds.has(resource.assetId) || resource.kind === 'hooks-file' || resource.projection?.renderer === 'gemini-hooks');
|
|
690
704
|
}
|
|
691
705
|
|
|
692
706
|
/** Gemini's hooks trust, observed through the adapter's own API. How trust is computed — the
|
|
693
707
|
* fingerprint semantics, which context fields matter — lives in src/adapters/gemini (review A3:
|
|
694
708
|
* this file used to import planGeminiHooks and re-derive the plan itself). */
|
|
695
|
-
function geminiHookTrust(target
|
|
709
|
+
function geminiHookTrust(target ) {
|
|
696
710
|
return geminiHookTrustFor(target);
|
|
697
711
|
}
|
|
698
712
|
|
|
@@ -706,7 +720,7 @@ function geminiHookTrust(target: any) {
|
|
|
706
720
|
* trust computation via geminiHookTrust above.
|
|
707
721
|
* - 'active': installed with no unmet prerequisite (Claude and Kiro today, since neither declares
|
|
708
722
|
* any `capabilities.hooks.prerequisites` and Kiro hooks activate automatically per kiro.dev). */
|
|
709
|
-
function hookWiringStatus(harness
|
|
723
|
+
function hookWiringStatus(harness , target , verification ) {
|
|
710
724
|
if (!target || target.skipped) return { status: 'absent', prerequisites: [] };
|
|
711
725
|
if (!verificationOwnsHooks(verification, hookAssetIds(target))) return { status: 'absent', prerequisites: [] };
|
|
712
726
|
if (harness.id === 'gemini') {
|
|
@@ -725,15 +739,15 @@ function hookWiringStatus(harness: any, target: any, verification: any) {
|
|
|
725
739
|
* silently at runtime. Only harnesses whose selected changes actually include hooks trigger
|
|
726
740
|
* this — a plan touching only guidance/skills assets never invokes the check. Removal is
|
|
727
741
|
* exempt: deleting hook files does not require running them. */
|
|
728
|
-
function assertBashAvailableForHooks(plan
|
|
742
|
+
function assertBashAvailableForHooks(plan , mode , hasBashCapableShellFn ) {
|
|
729
743
|
if (mode === 'remove') return;
|
|
730
|
-
const hookHarnesses = plan.targets.filter((target
|
|
744
|
+
const hookHarnesses = plan.targets.filter((target ) => !target.skipped && targetNeedsHooks(target)).map((target ) => target.harness);
|
|
731
745
|
if (hookHarnesses.length === 0) return;
|
|
732
746
|
if (hasBashCapableShellFn()) return;
|
|
733
747
|
throw new Error(`No bash-capable shell detected (checked: bash --version). Install Git Bash for Windows, or run inside WSL, before installing hooks for ${hookHarnesses.join(', ')}.`);
|
|
734
748
|
}
|
|
735
749
|
|
|
736
|
-
function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.ledger, context = {}, mode = 'apply', acceptPrerequisites = false, writeLedgerFn = writeLedger, writeRecoveryRecordFn = writeRecoveryRecord, hasBashCapableShellFn = hasBashCapableShell }
|
|
750
|
+
function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.ledger, context = {}, mode = 'apply', acceptPrerequisites = false, writeLedgerFn = writeLedger, writeRecoveryRecordFn = writeRecoveryRecord, hasBashCapableShellFn = hasBashCapableShell } ) {
|
|
737
751
|
assertPlanApplicable(plan, stateRoot, acceptPrerequisites);
|
|
738
752
|
assertBashAvailableForHooks(plan, mode, hasBashCapableShellFn);
|
|
739
753
|
const recovery = writeRecoveryRecordFn(stateRoot, { status: 'pending', changes: plan.changes });
|
|
@@ -745,14 +759,14 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
|
|
|
745
759
|
// it, because every adapter's job with a removal change is to delete the thing. Filtered
|
|
746
760
|
// here rather than in planLifecycle so the change still reaches updateLedger, which is what
|
|
747
761
|
// actually drops this harness's ownership row.
|
|
748
|
-
const applicable = target.changes.filter((change
|
|
762
|
+
const applicable = target.changes.filter((change ) => !change.retained);
|
|
749
763
|
if (!applicable.length) continue;
|
|
750
764
|
const harness = harnessFor(registry, target.harness);
|
|
751
765
|
const adapter = resolveAdapter(adapters, harness);
|
|
752
766
|
adapter[mode === 'remove' ? 'remove' : 'apply']({ ...target.adapterInput, registry, changes: applicable, ledger, recoveryRef: recovery.id });
|
|
753
767
|
completedHarnesses.push(target.harness);
|
|
754
768
|
}
|
|
755
|
-
} catch (error
|
|
769
|
+
} catch (error ) {
|
|
756
770
|
// A mid-loop throw (fs error, TOCTOU ownership mismatch) leaves some harnesses' native writes
|
|
757
771
|
// already on disk. Recording exactly which ones completed — rather than leaving the record
|
|
758
772
|
// stuck at 'pending' forever — is the difference between a diagnosable partial failure and a
|
|
@@ -768,7 +782,7 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
|
|
|
768
782
|
}
|
|
769
783
|
try {
|
|
770
784
|
applyMcpIndex({ scopeRoot: plan.scopeRoot, selectedMcp: plan.mcp, mode, retain: !removalIsTotal(plan) });
|
|
771
|
-
} catch (error
|
|
785
|
+
} catch (error ) {
|
|
772
786
|
// Same reasoning as the harness-loop catch above: every native resource already applied and
|
|
773
787
|
// verified successfully by this point, so leaving the record at 'pending' on a failure here
|
|
774
788
|
// would hide that success and make a retry indistinguishable from a from-scratch install.
|
|
@@ -789,14 +803,14 @@ function applyLifecycle({ plan, registry, adapters, stateRoot, ledger = plan.led
|
|
|
789
803
|
* selects. Verify only reads; this writes the ledger and nothing native. `dryRun` reports without
|
|
790
804
|
* writing. Returns the ledger and the server ids recorded per harness.
|
|
791
805
|
*/
|
|
792
|
-
function recordMcpOwnership({ plan, registry, adapters, stateRoot, ledger = plan.ledger, dryRun = false, writeLedgerFn = writeLedger }
|
|
806
|
+
function recordMcpOwnership({ plan, registry, adapters, stateRoot, ledger = plan.ledger, dryRun = false, writeLedgerFn = writeLedger } ) {
|
|
793
807
|
const { verifications } = verifyLifecycle({ plan: { ...plan, registry }, adapters, context: { registry, operation: 'apply' } });
|
|
794
|
-
const held = new Set((ledger.resources || []).map((resource
|
|
808
|
+
const held = new Set((ledger.resources || []).map((resource ) => ownershipKey(resource)));
|
|
795
809
|
const added = verifications.flatMap((verification) => (verification.resources || [])
|
|
796
|
-
.filter((resource
|
|
797
|
-
.map((resource
|
|
810
|
+
.filter((resource ) => resource.kind === 'mcp-server')
|
|
811
|
+
.map((resource ) => ({ ...resource, harness: verification.harness, scope: plan.scope, recoveryRef: resource.recoveryRef ?? null })))
|
|
798
812
|
.filter((resource) => !held.has(ownershipKey(resource)));
|
|
799
|
-
const recorded
|
|
813
|
+
const recorded = {};
|
|
800
814
|
for (const resource of added) (recorded[resource.harness] ??= []).push(resource.identity);
|
|
801
815
|
if (!added.length || dryRun) return { ledger, recorded };
|
|
802
816
|
const next = upgradeLedger({ ...ledger, resources: [...(ledger.resources || []), ...added] });
|
|
@@ -804,7 +818,7 @@ function recordMcpOwnership({ plan, registry, adapters, stateRoot, ledger = plan
|
|
|
804
818
|
return { ledger: next, recorded };
|
|
805
819
|
}
|
|
806
820
|
|
|
807
|
-
function removeLifecycle(options
|
|
821
|
+
function removeLifecycle(options ) {
|
|
808
822
|
const plan = planLifecycle({ ...options, context: { ...(options.context || {}), operation: 'remove' } });
|
|
809
823
|
return applyLifecycle({ ...options, plan, mode: 'remove', acceptPrerequisites: options.acceptPrerequisites });
|
|
810
824
|
}
|