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