@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
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// the same reason a file DoFlow cannot read is described by a fixed reason, never parser output.
|
|
15
15
|
const fs = require('node:fs');
|
|
16
16
|
const path = require('node:path');
|
|
17
|
-
const { fingerprint } = require('./copy-tree.
|
|
17
|
+
const { fingerprint } = require('./copy-tree.js');
|
|
18
18
|
|
|
19
19
|
const REASONS = Object.freeze({
|
|
20
20
|
json: 'invalid JSON',
|
|
@@ -22,29 +22,29 @@ const REASONS = Object.freeze({
|
|
|
22
22
|
container: 'server container is not an object',
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
function isObject(value
|
|
25
|
+
function isObject(value ) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); }
|
|
26
26
|
|
|
27
|
-
function entryFingerprint(value
|
|
27
|
+
function entryFingerprint(value ) { return fingerprint(value); }
|
|
28
28
|
|
|
29
|
-
function fingerprintOrNull(value
|
|
29
|
+
function fingerprintOrNull(value ) { return value === undefined ? null : entryFingerprint(value); }
|
|
30
30
|
|
|
31
31
|
/** Deep equality that ignores key order, so a reformatted entry still equals DoFlow's rendering. */
|
|
32
|
-
function sameValue(a
|
|
32
|
+
function sameValue(a , b ) { return a !== undefined && b !== undefined && entryFingerprint(a) === entryFingerprint(b); }
|
|
33
33
|
|
|
34
|
-
function shortId(id
|
|
34
|
+
function shortId(id ) { const text = String(id); return text.length > 40 ? `${text.slice(0, 37)}...` : text; }
|
|
35
35
|
|
|
36
|
-
function shortPath(file
|
|
36
|
+
function shortPath(file ) { return file.length > 80 ? `...${file.slice(-77)}` : file; }
|
|
37
37
|
|
|
38
38
|
const NOTICES = Object.freeze({
|
|
39
|
-
collision: (id
|
|
40
|
-
released: (id
|
|
41
|
-
untouched: (file
|
|
39
|
+
collision: (id , file ) => `MCP: kept your own entry '${shortId(id)}' in ${path.basename(file)} and did not register DoFlow's; rename or remove yours to let DoFlow manage it.`,
|
|
40
|
+
released: (id , file ) => `MCP: entry '${shortId(id)}' in ${path.basename(file)} was changed outside DoFlow; it is yours now and DoFlow no longer updates or removes it.`,
|
|
41
|
+
untouched: (file ) => `MCP: left ${shortPath(file)} untouched because it cannot be edited safely; DoFlow no longer manages the entries in it.`,
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
function refusal(label
|
|
44
|
+
function refusal(label , file , reason ) { return `${label}: ${file}: ${reason}; DoFlow did not change the file`; }
|
|
45
45
|
|
|
46
46
|
/** Read one MCP file as a JSON object. `servers` is null when the file or its container is absent. */
|
|
47
|
-
function readMcpFile(file
|
|
47
|
+
function readMcpFile(file , container , { fsImpl = fs } = {}) {
|
|
48
48
|
if (!fsImpl.existsSync(file)) return { ok: true, doc: null, servers: null };
|
|
49
49
|
let doc;
|
|
50
50
|
try { doc = JSON.parse(fsImpl.readFileSync(file, 'utf8')); } catch { return { ok: false, reason: REASONS.json }; }
|
|
@@ -55,52 +55,52 @@ function readMcpFile(file: string, container: string, { fsImpl = fs }: { fsImpl?
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** The `files` map plan and verify take: the scope's MCP file and every file an own row names. */
|
|
58
|
-
function readMcpFiles({ file, ownRows = [], container, fsImpl = fs }
|
|
59
|
-
const targets = new Set([file, ...ownRows.map((row
|
|
58
|
+
function readMcpFiles({ file, ownRows = [], container, fsImpl = fs } ) {
|
|
59
|
+
const targets = new Set([file, ...ownRows.map((row ) => row.target)]);
|
|
60
60
|
return Object.fromEntries([...targets].map((target) => {
|
|
61
61
|
const read = readMcpFile(target, container, { fsImpl });
|
|
62
62
|
return [target, read.ok ? { ok: true, servers: read.servers } : read];
|
|
63
63
|
}));
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function renderedById(selected
|
|
67
|
-
const rendered = new Map
|
|
66
|
+
function renderedById(selected , adoptable ) {
|
|
67
|
+
const rendered = new Map ();
|
|
68
68
|
for (const { id, entry } of [...selected, ...adoptable]) if (!rendered.has(id)) rendered.set(id, entry);
|
|
69
69
|
return rendered;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/** Shared by plan, verify and ownedMcpIds, so the three read one entry the same way. */
|
|
73
|
-
function ownershipView({ file, files, selected = [], adoptable = [], ownRows = [], foreignRows = [] }
|
|
73
|
+
function ownershipView({ file, files, selected = [], adoptable = [], ownRows = [], foreignRows = [] } ) {
|
|
74
74
|
const rendered = renderedById(selected, adoptable);
|
|
75
|
-
const valueOf = (target
|
|
76
|
-
const matches = (row
|
|
75
|
+
const valueOf = (target , id ) => (files[target]?.ok ? files[target].servers?.[id] : undefined);
|
|
76
|
+
const matches = (row , value ) => {
|
|
77
77
|
if (value === undefined) return false;
|
|
78
78
|
const recorded = row.fingerprint ?? null;
|
|
79
79
|
return recorded !== null ? entryFingerprint(value) === recorded : sameValue(value, rendered.get(row.identity));
|
|
80
80
|
};
|
|
81
|
-
const rowsFor = (id
|
|
81
|
+
const rowsFor = (id ) => ownRows.filter((row ) => row.identity === id && row.target === file);
|
|
82
82
|
// A legacy row is consumed alongside the current one, but never preferred over it.
|
|
83
|
-
const rowFor = (id
|
|
83
|
+
const rowFor = (id ) => { const rows = rowsFor(id); return rows.find((row ) => !row.legacy) ?? rows[0] ?? null; };
|
|
84
84
|
const adoptionOpen = ownRows.length === 0;
|
|
85
|
-
const adoptableIds = new Set(adoptable.map((server
|
|
86
|
-
const selectedIds = new Set(selected.map((server
|
|
85
|
+
const adoptableIds = new Set(adoptable.map((server ) => server.id));
|
|
86
|
+
const selectedIds = new Set(selected.map((server ) => server.id));
|
|
87
87
|
/** An equal entry no own row claims is DoFlow's when another harness's row claims it, or when it
|
|
88
88
|
* may be adopted. */
|
|
89
|
-
const claimable = (id
|
|
89
|
+
const claimable = (id , entry ) => {
|
|
90
90
|
const value = valueOf(file, id);
|
|
91
|
-
return sameValue(value, entry) && (foreignRows.some((row
|
|
91
|
+
return sameValue(value, entry) && (foreignRows.some((row ) => row.identity === id && row.target === file)
|
|
92
92
|
|| (adoptionOpen && adoptableIds.has(id)));
|
|
93
93
|
};
|
|
94
94
|
/** Adoptable entries DoFlow takes out because nothing selects them any more. */
|
|
95
95
|
const adoptedRemovals = () => (adoptionOpen && files[file]?.ok
|
|
96
|
-
? adoptable.filter(({ id, entry }
|
|
96
|
+
? adoptable.filter(({ id, entry } ) => !selectedIds.has(id) && sameValue(valueOf(file, id), entry)) : []);
|
|
97
97
|
return { rendered, valueOf, matches, rowsFor, rowFor, claimable, adoptedRemovals };
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function orderOf(selected
|
|
100
|
+
function orderOf(selected , adoptable ) {
|
|
101
101
|
const ids = [...renderedById(selected, adoptable).keys()];
|
|
102
|
-
const rank = (id
|
|
103
|
-
return (a
|
|
102
|
+
const rank = (id ) => { const index = ids.indexOf(id); return index < 0 ? ids.length : index; };
|
|
103
|
+
return (a , b ) => (a.target < b.target ? -1 : a.target > b.target ? 1
|
|
104
104
|
: rank(a.identity) - rank(b.identity) || (a.identity < b.identity ? -1 : a.identity > b.identity ? 1 : 0));
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -110,17 +110,17 @@ function orderOf(selected: any[], adoptable: any[]): (a: any, b: any) => number
|
|
|
110
110
|
* `foreignRows` other harnesses' rows; `identityFor` composes the adapter's ownership identity.
|
|
111
111
|
*/
|
|
112
112
|
function planMcpEntries({ file, files, selected = [], adoptable = [], ownRows = [], foreignRows = [], removing = false,
|
|
113
|
-
identityFor, assetId, renderer, label }
|
|
113
|
+
identityFor, assetId, renderer, label } ) {
|
|
114
114
|
const view = ownershipView({ file, files, selected, adoptable, ownRows, foreignRows });
|
|
115
|
-
const changes
|
|
116
|
-
const conflicts
|
|
117
|
-
const notices
|
|
118
|
-
const base = (fields
|
|
119
|
-
const write = (operation
|
|
115
|
+
const changes = [];
|
|
116
|
+
const conflicts = [];
|
|
117
|
+
const notices = [];
|
|
118
|
+
const base = (fields ) => ({ assetId, kind: 'mcp-server', sourceVersion: 'registry-v1', projection: { renderer }, ...fields });
|
|
119
|
+
const write = (operation , id , entry ) => base({
|
|
120
120
|
target: file, operation, identity: id, ownershipIdentity: identityFor(id),
|
|
121
121
|
before: fingerprintOrNull(view.valueOf(file, id)), entry, fingerprint: entryFingerprint(entry), afterFingerprint: entryFingerprint(entry),
|
|
122
122
|
});
|
|
123
|
-
const drop = (row
|
|
123
|
+
const drop = (row , release ) => base({
|
|
124
124
|
target: row.target, operation: 'remove', identity: row.identity, ownershipIdentity: row.ownershipIdentity,
|
|
125
125
|
before: fingerprintOrNull(view.valueOf(row.target, row.identity)), fingerprint: row.fingerprint ?? null,
|
|
126
126
|
...(release ? { release: true } : {}),
|
|
@@ -132,7 +132,7 @@ function planMcpEntries({ file, files, selected = [], adoptable = [], ownRows =
|
|
|
132
132
|
else conflicts.push(refusal(label, target, files[target].reason));
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
const consumed = new Set
|
|
135
|
+
const consumed = new Set ();
|
|
136
136
|
if (!removing && files[file]?.ok) {
|
|
137
137
|
for (const { id, entry } of selected) {
|
|
138
138
|
const rows = view.rowsFor(id);
|
|
@@ -184,7 +184,7 @@ function planMcpEntries({ file, files, selected = [], adoptable = [], ownRows =
|
|
|
184
184
|
/** Apply one file's planned changes to the server map read now. Each change goes ahead only while
|
|
185
185
|
* its entry still holds what plan saw, or already holds the change's end state, which makes two
|
|
186
186
|
* harnesses' changes to one shared entry in one run idempotent. Release changes touch nothing. */
|
|
187
|
-
function applyMcpEntries(servers
|
|
187
|
+
function applyMcpEntries(servers , changes ) {
|
|
188
188
|
const next = { ...(servers || {}) };
|
|
189
189
|
for (const change of changes) {
|
|
190
190
|
if (change.release) continue;
|
|
@@ -205,13 +205,13 @@ function applyMcpEntries(servers: any, changes: any[]): any {
|
|
|
205
205
|
* whatever it holds now.
|
|
206
206
|
*/
|
|
207
207
|
function verifyMcpEntries({ file, files, snapshot, selected = [], adoptable = [], ownRows = [], foreignRows = [], removing = false,
|
|
208
|
-
identityFor, assetId, renderer, label, harness, sourceVersion = 'unknown' }
|
|
208
|
+
identityFor, assetId, renderer, label, harness, sourceVersion = 'unknown' } ) {
|
|
209
209
|
const now = ownershipView({ file, files, selected, adoptable, ownRows, foreignRows });
|
|
210
210
|
const then = ownershipView({ file, files: snapshot, selected, adoptable, ownRows, foreignRows });
|
|
211
|
-
const statuses
|
|
212
|
-
const resources
|
|
213
|
-
const conflicts
|
|
214
|
-
const report = (id
|
|
211
|
+
const statuses = [];
|
|
212
|
+
const resources = [];
|
|
213
|
+
const conflicts = [];
|
|
214
|
+
const report = (id , target , ownershipIdentity , status ) => statuses.push({
|
|
215
215
|
harness, assetId, capability: 'mcp', identity: id, target, ownershipIdentity, status,
|
|
216
216
|
});
|
|
217
217
|
|
|
@@ -219,7 +219,7 @@ function verifyMcpEntries({ file, files, snapshot, selected = [], adoptable = []
|
|
|
219
219
|
for (const target of Object.keys(files).filter((item) => !files[item].ok).sort()) conflicts.push(refusal(label, target, files[target].reason));
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
const consumed = new Set
|
|
222
|
+
const consumed = new Set ();
|
|
223
223
|
if (!removing) {
|
|
224
224
|
for (const { id, entry } of selected) {
|
|
225
225
|
for (const row of now.rowsFor(id)) consumed.add(row);
|
|
@@ -239,7 +239,7 @@ function verifyMcpEntries({ file, files, snapshot, selected = [], adoptable = []
|
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
const leftover = (value
|
|
242
|
+
const leftover = (value , matching , target ) => {
|
|
243
243
|
if (!files[target]?.ok) return 'not-managed';
|
|
244
244
|
if (value === undefined) return 'absent';
|
|
245
245
|
return matching ? 'retained' : 'not-managed';
|
|
@@ -261,29 +261,29 @@ function verifyMcpEntries({ file, files, snapshot, selected = [], adoptable = []
|
|
|
261
261
|
/** The ids DoFlow owns in this harness's MCP file now, in rendering order: those an own row matches,
|
|
262
262
|
* and, while the harness holds no row, adoptable entries equal to DoFlow's rendering. A file that
|
|
263
263
|
* cannot be read contributes none. */
|
|
264
|
-
function ownedMcpIds({ file, files, selected = [], adoptable = [], ownRows = [] }
|
|
264
|
+
function ownedMcpIds({ file, files, selected = [], adoptable = [], ownRows = [] } ) {
|
|
265
265
|
const view = ownershipView({ file, files, selected, adoptable, ownRows });
|
|
266
|
-
const owned = new Set
|
|
266
|
+
const owned = new Set (ownRows.filter((row ) => view.matches(row, view.valueOf(row.target, row.identity))).map((row ) => row.identity));
|
|
267
267
|
if (!ownRows.length) {
|
|
268
268
|
for (const { id, entry } of adoptable) if (sameValue(view.valueOf(file, id), entry)) owned.add(id);
|
|
269
269
|
}
|
|
270
|
-
const order
|
|
270
|
+
const order = [...view.rendered.keys()];
|
|
271
271
|
return [...order.filter((id) => owned.has(id)), ...[...owned].filter((id) => !order.includes(id)).sort()];
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
function isSymlink(file
|
|
274
|
+
function isSymlink(file , fsImpl ) {
|
|
275
275
|
try { return fsImpl.lstatSync(file).isSymbolicLink(); } catch { return false; }
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
/** A symlinked MCP file is written through to the file it points at, so the link stays a link. */
|
|
279
|
-
function writePath(file
|
|
279
|
+
function writePath(file , fsImpl ) {
|
|
280
280
|
if (!isSymlink(file, fsImpl)) return file;
|
|
281
281
|
try { return fsImpl.realpathSync(file); } catch { return path.resolve(path.dirname(file), fsImpl.readlinkSync(file)); }
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
/** Write a JSON document atomically, keeping the existing file's permission bits: an MCP file kept
|
|
285
285
|
* private for its tokens stays so. Creates missing directories; never deletes a file. */
|
|
286
|
-
function writeMcpJson(file
|
|
286
|
+
function writeMcpJson(file , doc , { fsImpl = fs } = {}) {
|
|
287
287
|
const dest = writePath(file, fsImpl);
|
|
288
288
|
fsImpl.mkdirSync(path.dirname(dest), { recursive: true });
|
|
289
289
|
const mode = fsImpl.existsSync(dest) ? fsImpl.statSync(dest).mode & 0o7777 : null;
|
|
@@ -303,14 +303,14 @@ function writeMcpJson(file: string, doc: any, { fsImpl = fs }: { fsImpl?: any }
|
|
|
303
303
|
* every file's next document computed before the first write. An emptied container is kept as `{}`
|
|
304
304
|
* or its member deleted, as the harness expects. Returns the number of files written.
|
|
305
305
|
*/
|
|
306
|
-
function writeMcpEntries(changes
|
|
307
|
-
const byFile = new Map
|
|
306
|
+
function writeMcpEntries(changes , { container, keepEmptyContainer = false, fsImpl = fs } = {}) {
|
|
307
|
+
const byFile = new Map ();
|
|
308
308
|
for (const change of changes) {
|
|
309
309
|
if (change.kind !== 'mcp-server' || change.release) continue;
|
|
310
310
|
if (!byFile.has(change.target)) byFile.set(change.target, []);
|
|
311
|
-
byFile.get(change.target)
|
|
311
|
+
byFile.get(change.target) .push(change);
|
|
312
312
|
}
|
|
313
|
-
const writes
|
|
313
|
+
const writes = [];
|
|
314
314
|
for (const [file, group] of byFile) {
|
|
315
315
|
const read = readMcpFile(file, container, { fsImpl });
|
|
316
316
|
if (!read.ok) throw new Error(`MCP: ${file} changed after planning; nothing was written, re-run the command`);
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
// https://opencode.ai/docs/rules
|
|
17
17
|
const fs = require('node:fs');
|
|
18
18
|
const path = require('node:path');
|
|
19
|
-
const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, sharedTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, readJson, sourceDirFor, resolveTransform } = require('../copy-tree.
|
|
20
|
-
const { readMcpFiles, planMcpEntries, verifyMcpEntries, ownedMcpIds, writeMcpEntries } = require('../mcp-entries.
|
|
19
|
+
const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, sharedTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, readJson, sourceDirFor, resolveTransform } = require('../copy-tree.js');
|
|
20
|
+
const { readMcpFiles, planMcpEntries, verifyMcpEntries, ownedMcpIds, writeMcpEntries } = require('../mcp-entries.js');
|
|
21
21
|
|
|
22
22
|
// Only the marker constants: marker-merge.js reads and writes files itself, which cannot be used
|
|
23
23
|
// from plan(), whose contract is to compute changes without touching disk. The gemini adapter
|
|
24
24
|
// solves this the same way, with a pure managedInstruction() over strings.
|
|
25
|
-
const { MARKER_START, MARKER_END } = require('../../helper/marker-merge.
|
|
26
|
-
const { destructiveCommandGlobs } = require('../../helper/guardrails.
|
|
27
|
-
const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.
|
|
25
|
+
const { MARKER_START, MARKER_END } = require('../../helper/marker-merge.js');
|
|
26
|
+
const { destructiveCommandGlobs } = require('../../helper/guardrails.js');
|
|
27
|
+
const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.js');
|
|
28
28
|
|
|
29
29
|
const HARNESS = 'opencode';
|
|
30
30
|
/**
|
|
@@ -48,7 +48,7 @@ const MCP_RENDERER = 'opencode-mcp';
|
|
|
48
48
|
* historical shape.
|
|
49
49
|
*/
|
|
50
50
|
function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS] } = {}) {
|
|
51
|
-
function nativePaths({ scope, scopeRoot, homeDir }
|
|
51
|
+
function nativePaths({ scope, scopeRoot, homeDir } ) {
|
|
52
52
|
return resolveHarnessPaths(declaredPaths, { scope, scopeRoot, homeDir });
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -57,20 +57,20 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
57
57
|
* `opencode.json` are read from the repo root, but the skills tree is namespaced under
|
|
58
58
|
* `.opencode/` per design.md's native-surfaces table. Global scope has no such split —
|
|
59
59
|
* `~/.config/opencode` already IS the skills root. Declared as `treeRoot` in harnesses.json. */
|
|
60
|
-
function copyTreeConfigDir(paths
|
|
60
|
+
function copyTreeConfigDir(paths ) {
|
|
61
61
|
return paths.treeRoot;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/** Agents live under `.opencode/agents/` at project scope and `~/.config/opencode/agents/`
|
|
65
65
|
* globally — a different root from the skills tree in both scopes, so they get their own
|
|
66
66
|
* destination resolution rather than reusing copyTreeConfigDir. Declared as `agentsDirectory`. */
|
|
67
|
-
function agentsDestDir(paths
|
|
67
|
+
function agentsDestDir(paths ) {
|
|
68
68
|
return paths.agentsDirectory;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/** Discovery keeps opencode.json as plan read it (`mcpSnapshot`), which verify compares against,
|
|
72
72
|
* and reports the servers DoFlow owns in its `mcp` object now (`mcpOwned`). */
|
|
73
|
-
function discover({ scope, scopeRoot, mcp = [], mcpAdoptable = [], ledger, context = {}, fsImpl = fs }
|
|
73
|
+
function discover({ scope, scopeRoot, mcp = [], mcpAdoptable = [], ledger, context = {}, fsImpl = fs } ) {
|
|
74
74
|
const paths = nativePaths({ scope, scopeRoot, homeDir: context.homeDir });
|
|
75
75
|
const instruction = fsImpl.existsSync(paths.instruction) ? fsImpl.readFileSync(paths.instruction, 'utf8') : null;
|
|
76
76
|
const entries = mcpEntriesInput({ paths, mcp, mcpAdoptable, ledger });
|
|
@@ -81,7 +81,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
81
81
|
return { paths, instruction, config: readJson(paths.config, { fsImpl }), mcpSnapshot, mcpOwned: ownedMcpIds({ ...entries, files: mcpSnapshot }), mcpConfigured };
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function render({ content = '' }
|
|
84
|
+
function render({ content = '' } = {}) {
|
|
85
85
|
return `${MARKER_START}\n${String(content).trimEnd()}\n${MARKER_END}\n`;
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -90,17 +90,17 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
90
90
|
* every config it discovers — replacing would silently drop a user's own entries. Skills are
|
|
91
91
|
* deliberately absent here: they are a materialised directory now, discovered natively, not a
|
|
92
92
|
* registered path. The `mcp` object is left to the entry changes. */
|
|
93
|
-
function mergeConfig(existing
|
|
93
|
+
function mergeConfig(existing ) {
|
|
94
94
|
const next = { ...(existing || {}) };
|
|
95
|
-
const union = (current
|
|
95
|
+
const union = (current , add ) => [...new Set([...(Array.isArray(current) ? current : []), ...add])];
|
|
96
96
|
next.instructions = union(next.instructions, [INSTRUCTION_FILE]);
|
|
97
97
|
return next;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/** Remove only what mergeConfig added, leaving every user key and every unrelated array entry. */
|
|
101
|
-
function unmergeConfig(existing
|
|
101
|
+
function unmergeConfig(existing ) {
|
|
102
102
|
const next = { ...(existing || {}) };
|
|
103
|
-
const without = (current
|
|
103
|
+
const without = (current , drop ) => (Array.isArray(current) ? current.filter((v) => !drop.includes(v)) : []);
|
|
104
104
|
next.instructions = without(next.instructions, [INSTRUCTION_FILE]);
|
|
105
105
|
if (!next.instructions.length) delete next.instructions;
|
|
106
106
|
return next;
|
|
@@ -116,12 +116,12 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
116
116
|
* comment-free glob we can re-derive) are touched; a user's own bash rules survive untouched.
|
|
117
117
|
* Entries are namespaced by value, not by key, so ownership is decided by re-deriving the list
|
|
118
118
|
* from the single authored source rather than by trusting whatever happens to be present. */
|
|
119
|
-
function guardrailBashRules(context
|
|
119
|
+
function guardrailBashRules(context ) {
|
|
120
120
|
const globs = destructiveCommandGlobs({ repoRoot: context.repoRoot });
|
|
121
|
-
return Object.fromEntries(globs.map((glob
|
|
121
|
+
return Object.fromEntries(globs.map((glob ) => [glob, 'deny']));
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
function mergeGuardrails(existing
|
|
124
|
+
function mergeGuardrails(existing , context ) {
|
|
125
125
|
const next = { ...(existing || {}) };
|
|
126
126
|
const bash = { ...guardrailBashRules(context), ...((next.permission?.bash ?? {})) };
|
|
127
127
|
// Keep user rules, but make sure ours win for identical globs (they are the stricter value).
|
|
@@ -129,7 +129,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
129
129
|
return next;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
function stripGuardrails(existing
|
|
132
|
+
function stripGuardrails(existing , context ) {
|
|
133
133
|
if (!existing?.permission?.bash) return existing;
|
|
134
134
|
const ours = new Set(Object.keys(guardrailBashRules(context)));
|
|
135
135
|
const bash = Object.fromEntries(Object.entries(existing.permission.bash).filter(([glob]) => !ours.has(glob)));
|
|
@@ -141,7 +141,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
141
141
|
return rest;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
function managedInstruction(existing
|
|
144
|
+
function managedInstruction(existing , rendered ) {
|
|
145
145
|
if (existing === null) return { ok: true, operation: 'create', content: rendered };
|
|
146
146
|
const start = existing.indexOf(MARKER_START);
|
|
147
147
|
const end = existing.indexOf(MARKER_END);
|
|
@@ -153,7 +153,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
153
153
|
|
|
154
154
|
/** Strip DoFlow's span, leaving the rest of a shared AGENTS.md intact. Returns null when there is
|
|
155
155
|
* nothing to do, so remove() can skip the write entirely. */
|
|
156
|
-
function strippedInstruction(existing
|
|
156
|
+
function strippedInstruction(existing ) {
|
|
157
157
|
if (typeof existing !== 'string') return null;
|
|
158
158
|
const start = existing.indexOf(MARKER_START);
|
|
159
159
|
const end = existing.indexOf(MARKER_END);
|
|
@@ -168,11 +168,11 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
168
168
|
/** `agents.shared` projects onto OpenCode through its own renderer name (its frontmatter is
|
|
169
169
|
* transformed into OpenCode's markdown-agent vocabulary) but materialises with exactly the same
|
|
170
170
|
* engine and change records as every other copy-tree asset. */
|
|
171
|
-
function opencodeTreeAssets(assets
|
|
172
|
-
return [...copyTreeAssets(assets), ...(assets || []).filter((asset
|
|
171
|
+
function opencodeTreeAssets(assets ) {
|
|
172
|
+
return [...copyTreeAssets(assets), ...(assets || []).filter((asset ) => asset.renderer === 'opencode-agents')];
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
function planCopyTreeAssets({ assets, scope, scopeRoot, context, ledger, removing, fsImpl = fs }
|
|
175
|
+
function planCopyTreeAssets({ assets, scope, scopeRoot, context, ledger, removing, fsImpl = fs } ) {
|
|
176
176
|
const paths = nativePaths({ scope, scopeRoot, homeDir: context.homeDir });
|
|
177
177
|
const treeConfigDir = copyTreeConfigDir(paths);
|
|
178
178
|
const changes = [];
|
|
@@ -190,10 +190,10 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
190
190
|
// is never deleted on removal, forced or not.
|
|
191
191
|
force: !removing && context?.force === true, });
|
|
192
192
|
treeResults.push(result);
|
|
193
|
-
conflicts.push(...result.conflicts.map((reason
|
|
193
|
+
conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
|
|
194
194
|
// A hand-edited file at a path DoFlow no longer writes is the user's: it stays, only its
|
|
195
195
|
// ownership row is released.
|
|
196
|
-
notices.push(...result.kept.map((item
|
|
196
|
+
notices.push(...result.kept.map((item ) => `kept hand-edited ${path.relative(paths.root, item.target)}; DoFlow no longer manages it`));
|
|
197
197
|
for (const change of result.changes) {
|
|
198
198
|
changes.push({
|
|
199
199
|
assetId: asset.id, target: change.target, source: change.source, operation: change.operation,
|
|
@@ -209,7 +209,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
209
209
|
return { changes, conflicts, notices: [...siblingReplacedNotices(treeResults), ...notices] };
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
function applyCopyTreeAssets(changes
|
|
212
|
+
function applyCopyTreeAssets(changes , { fsImpl = fs } = {}) {
|
|
213
213
|
let applied = 0;
|
|
214
214
|
const grouped = new Map();
|
|
215
215
|
for (const change of changes) {
|
|
@@ -224,13 +224,13 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
224
224
|
return applied;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
function removeCopyTreeAssets(changes
|
|
228
|
-
const treeChanges = changes.filter((change
|
|
229
|
-
.map((change
|
|
227
|
+
function removeCopyTreeAssets(changes , { fsImpl = fs } = {}) {
|
|
228
|
+
const treeChanges = changes.filter((change ) => change.kind === 'copy-tree-file' && change.operation === 'remove' && !change.retained)
|
|
229
|
+
.map((change ) => ({ relPath: change.identity, target: change.target, operation: 'remove', fingerprint: change.fingerprint, pruneGuard: change.pruneGuard }));
|
|
230
230
|
return removeTree({ changes: treeChanges, fsImpl }).removed;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
function verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl = fs }
|
|
233
|
+
function verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl = fs } ) {
|
|
234
234
|
const paths = nativePaths({ scope, scopeRoot, homeDir: context.homeDir });
|
|
235
235
|
const treeConfigDir = copyTreeConfigDir(paths);
|
|
236
236
|
const statuses = [];
|
|
@@ -240,7 +240,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
240
240
|
const destDir = asset.renderer === 'opencode-agents' ? agentsDestDir(paths) : sharedTreeDestDir(paths.root, asset.nativeDir) ?? copyTreeDestDir(treeConfigDir, asset);
|
|
241
241
|
const sourceDir = sourceDirFor(asset, context, fsImpl, 'OpenCode');
|
|
242
242
|
const result = verifyTree({ sourceDir, destDir, fsImpl, layout: asset.layout, transform: asset.transform, plannedWrites: context?.plannedWrites });
|
|
243
|
-
conflicts.push(...result.conflicts.map((reason
|
|
243
|
+
conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
|
|
244
244
|
for (const resource of result.resources) {
|
|
245
245
|
resources.push({
|
|
246
246
|
assetId: asset.id, target: resource.target, ownershipIdentity: `doflow:${HARNESS}:copy-tree:${asset.id}:${resource.relPath}`,
|
|
@@ -259,45 +259,45 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
259
259
|
/** OpenCode's settings-merge change has no asset of its own in core/registry/assets.json (it's a
|
|
260
260
|
* registration side-effect of the instructions/mcp assets, not a projected asset), so it piggybacks
|
|
261
261
|
* on a real asset id already routed to this harness — the same "pseudo-component" technique
|
|
262
|
-
* src/adapters/gemini/index.
|
|
262
|
+
* src/adapters/gemini/index.js#hooksAssetId and src/adapters/kiro/index.js#pseudoAssetId use. The
|
|
263
263
|
* lifecycle layer validates every change's assetId against the harness's actual asset list, so an
|
|
264
264
|
* invented id like the literal string 'opencode.config' is rejected as unknown. */
|
|
265
|
-
function pseudoAssetId(assets
|
|
266
|
-
return assets.find((asset
|
|
265
|
+
function pseudoAssetId(assets ) {
|
|
266
|
+
return assets.find((asset ) => asset.capability === 'instructions')?.id ?? assets[0]?.id;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
// ---- MCP entries (opencode.json `mcp`) ----
|
|
270
270
|
|
|
271
271
|
/** OpenCode's own server shape: a local server whose command and arguments form one array. */
|
|
272
|
-
function buildServerEntry(server
|
|
272
|
+
function buildServerEntry(server ) {
|
|
273
273
|
return { type: 'local', command: [server.command, ...(server.args || [])], enabled: true };
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
function ownershipIdentity(id
|
|
276
|
+
function ownershipIdentity(id ) { return `doflow:${HARNESS}:mcp-server:${id}`; }
|
|
277
277
|
|
|
278
278
|
/** Everything the shared entry-ownership rules (../mcp-entries.ts) need from this adapter. */
|
|
279
|
-
function mcpEntriesInput({ paths, mcp = [], mcpAdoptable = [], ledger, assets = [] }
|
|
280
|
-
const rendered = (servers
|
|
281
|
-
const rows = (ledger?.resources || []).filter((row
|
|
279
|
+
function mcpEntriesInput({ paths, mcp = [], mcpAdoptable = [], ledger, assets = [] } ) {
|
|
280
|
+
const rendered = (servers ) => servers.map((server ) => ({ id: server.id, entry: buildServerEntry(server) }));
|
|
281
|
+
const rows = (ledger?.resources || []).filter((row ) => row.kind === 'mcp-server');
|
|
282
282
|
return {
|
|
283
283
|
file: paths.config, selected: rendered(mcp), adoptable: rendered(mcpAdoptable),
|
|
284
|
-
ownRows: rows.filter((row
|
|
284
|
+
ownRows: rows.filter((row ) => row.harness === HARNESS).map((row ) => ({
|
|
285
285
|
identity: row.identity, target: row.target, fingerprint: row.fingerprint ?? null, ownershipIdentity: row.ownershipIdentity, legacy: false,
|
|
286
286
|
})),
|
|
287
|
-
foreignRows: rows.filter((row
|
|
287
|
+
foreignRows: rows.filter((row ) => row.harness !== HARNESS).map((row ) => ({ harness: row.harness, identity: row.identity, target: row.target })),
|
|
288
288
|
identityFor: ownershipIdentity, assetId: pseudoAssetId(assets), renderer: MCP_RENDERER, label: 'OpenCode MCP',
|
|
289
289
|
};
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
/** opencode.json cannot be edited safely while removing: its registration row is released with
|
|
293
293
|
* the entry rows, so the removal completes and leaves the file as it is. */
|
|
294
|
-
function releasedConfigRow({ assets, paths, ledger }
|
|
295
|
-
const owned = (ledger?.resources || []).some((row
|
|
294
|
+
function releasedConfigRow({ assets, paths, ledger } ) {
|
|
295
|
+
const owned = (ledger?.resources || []).some((row ) => row.harness === HARNESS && row.ownershipIdentity === `${HARNESS}:config:registration` && row.target === paths.config);
|
|
296
296
|
return owned ? [{ assetId: pseudoAssetId(assets), target: paths.config, operation: 'remove', release: true,
|
|
297
297
|
ownershipIdentity: `${HARNESS}:config:registration`, harness: HARNESS, projection: { renderer: 'opencode-config' } }] : [];
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
function plan({ scope, scopeRoot, assets = [], mcp = [], mcpAdoptable = [], discovery, context = {}, ledger, fsImpl = fs }
|
|
300
|
+
function plan({ scope, scopeRoot, assets = [], mcp = [], mcpAdoptable = [], discovery, context = {}, ledger, fsImpl = fs } ) {
|
|
301
301
|
const found = discovery || discover({ scope, scopeRoot, mcp, mcpAdoptable, ledger, context, fsImpl });
|
|
302
302
|
const changes = [];
|
|
303
303
|
const conflicts = [];
|
|
@@ -306,7 +306,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
306
306
|
// below, under a fixed reason that never quotes the file.
|
|
307
307
|
const configReadable = found.mcpSnapshot[found.paths.config].ok;
|
|
308
308
|
|
|
309
|
-
const guidance = assets.find((asset
|
|
309
|
+
const guidance = assets.find((asset ) => asset.capability === 'instructions');
|
|
310
310
|
if (guidance) {
|
|
311
311
|
const rendered = render({ content: fsImpl.readFileSync(path.resolve(context.repoRoot, guidance.source), 'utf8') });
|
|
312
312
|
const outcome = removing
|
|
@@ -333,7 +333,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
333
333
|
// Tagged 'remove' during a remove operation (rather than always 'update'/'create') so this
|
|
334
334
|
// adapter's own apply()/remove() dispatch — which routes strictly on `operation` — writes
|
|
335
335
|
// the unmerged file during removal instead of silently skipping it (the same fix
|
|
336
|
-
// src/adapters/copilot/index.
|
|
336
|
+
// src/adapters/copilot/index.js's analogous mcp-merge change already carries).
|
|
337
337
|
operation: removing ? 'remove' : (found.config.exists ? 'update' : 'create'), content: `${JSON.stringify(next, null, 2)}\n`,
|
|
338
338
|
ownershipIdentity: `${HARNESS}:config:registration`, fingerprint: fingerprint(next),
|
|
339
339
|
harness: HARNESS, projection: { renderer: 'opencode-config' } });
|
|
@@ -355,14 +355,14 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
355
355
|
return { changes, conflicts, notices: [...copyTree.notices, ...mcpPlan.notices], paths: found.paths };
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
-
function writeChange(change
|
|
358
|
+
function writeChange(change , fsImpl ) {
|
|
359
359
|
fsImpl.mkdirSync(path.dirname(change.target), { recursive: true });
|
|
360
360
|
fsImpl.writeFileSync(change.target, change.content, 'utf8');
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
// The config change is written first; the entry changes then go to a fresh read of the same file,
|
|
364
364
|
// so they apply on top of it.
|
|
365
|
-
function apply({ changes = [], fsImpl = fs }
|
|
365
|
+
function apply({ changes = [], fsImpl = fs } ) {
|
|
366
366
|
let applied = 0;
|
|
367
367
|
for (const change of changes) {
|
|
368
368
|
if (change.operation === 'remove') continue;
|
|
@@ -378,7 +378,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
378
378
|
return { applied };
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
-
function remove({ changes = [], fsImpl = fs }
|
|
381
|
+
function remove({ changes = [], fsImpl = fs } ) {
|
|
382
382
|
let removed = 0;
|
|
383
383
|
for (const change of changes) {
|
|
384
384
|
if (change.operation !== 'remove') continue;
|
|
@@ -392,13 +392,13 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
392
392
|
return { removed };
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
function verify({ scope, scopeRoot, assets = [], mcp = [], mcpAdoptable = [], discovery, ledger, operation, context = {}, fsImpl = fs }
|
|
395
|
+
function verify({ scope, scopeRoot, assets = [], mcp = [], mcpAdoptable = [], discovery, ledger, operation, context = {}, fsImpl = fs } ) {
|
|
396
396
|
const found = discover({ scope, scopeRoot, mcp, mcpAdoptable, ledger, context, fsImpl });
|
|
397
397
|
const removing = (operation ?? context.operation) === 'remove';
|
|
398
398
|
const statuses = [];
|
|
399
399
|
const resources = [];
|
|
400
400
|
const conflicts = [];
|
|
401
|
-
const instructionsAssetId = assets.find((asset
|
|
401
|
+
const instructionsAssetId = assets.find((asset ) => asset.capability === 'instructions')?.id ?? pseudoAssetId(assets);
|
|
402
402
|
|
|
403
403
|
const hasSection = typeof found.instruction === 'string'
|
|
404
404
|
&& found.instruction.includes(MARKER_START) && found.instruction.includes(MARKER_END);
|
|
@@ -445,7 +445,7 @@ function createOpenCodeAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS]
|
|
|
445
445
|
|
|
446
446
|
const copyTree = verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl });
|
|
447
447
|
resources.push(...copyTree.resources);
|
|
448
|
-
statuses.push(...copyTree.statuses.map((status
|
|
448
|
+
statuses.push(...copyTree.statuses.map((status ) => ({ harness: HARNESS, ...status })));
|
|
449
449
|
conflicts.push(...copyTree.conflicts);
|
|
450
450
|
|
|
451
451
|
return { ok: conflicts.length === 0 && !statuses.some((s) => s.status === 'invalid' || s.status === 'conflict'), resources, statuses, conflicts };
|