weir-harness 1.2.0 → 1.2.2
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/cordis.patch.yml +0 -2
- package/lib/client.blackboard-model.js +172 -56
- package/lib/client.capability-manager.js +0 -1
- package/lib/client.capability-model.js +378 -31
- package/lib/client.chain-model.js +80 -0
- package/lib/client.disabled-categories-editor.js +22 -0
- package/lib/client.edit-lock-maintenance.js +239 -246
- package/lib/client.edit-lock-panel.js +110 -0
- package/lib/client.hash-edit-model.js +48 -0
- package/lib/client.js +267 -1549
- package/lib/client.lsp-model.js +92 -0
- package/lib/client.lsp-panel.js +11 -16
- package/lib/client.lsp-toggle.js +18 -0
- package/lib/client.notify-permissions.js +112 -20
- package/lib/client.notify-web.js +14 -11
- package/lib/client.robash-model.js +26 -0
- package/lib/client.settings-page.js +287 -17
- package/lib/client.worktree-model.js +225 -0
- package/package.json +5 -2
- package/src/blackboard/durable.js +12 -0
- package/src/blackboard/index.js +2 -2
- package/src/blackboard/kernel.js +23 -6
- package/src/blackboard/notify.js +10 -3
- package/src/blackboard/remote.js +31 -13
- package/src/blackboard/storage.js +13 -5
- package/src/capabilities/apply-engine.js +189 -101
- package/src/capabilities/defaults-transaction.js +136 -1
- package/src/capabilities/lifecycle-snapshot.js +83 -204
- package/src/capabilities/mcp-admission.js +1 -23
- package/src/capabilities/mcp-drain.js +30 -19
- package/src/capabilities/mcp-facade.js +23 -3
- package/src/capabilities/preset-library.js +247 -0
- package/src/capabilities/preset-package.js +212 -1
- package/src/capabilities/same-name-policy.js +3 -41
- package/src/capabilities/selection-notify.js +324 -21
- package/src/capabilities/skill-office-adapter.js +7 -1
- package/src/capabilities/skill-selection-plugin.js +315 -729
- package/src/capabilities/store/checked-contract.d.ts +5 -0
- package/src/capabilities/store/fs-adapter.js +52 -0
- package/src/capabilities/store/lock.js +86 -0
- package/src/capabilities/store/paths.js +5 -0
- package/src/capabilities/store/store.js +105 -5
- package/src/creative-guide/guide.js +1 -1
- package/src/delegate/child-composition.js +258 -0
- package/src/delegate/group-coordinator.js +25 -27
- package/src/delegate/index.js +21 -2
- package/src/delegate/rehydrate.js +24 -4
- package/src/delegate/robash-guard-core.js +2 -2
- package/src/delegate/spawn-adapter.js +214 -100
- package/src/delegate/status-tool.js +9 -6
- package/src/delegate/supervision-mount.js +35 -6
- package/src/delegate/tool.js +29 -4
- package/src/{shared → delegate}/whitelist-defaults.js +2 -2
- package/src/edit-lock/admin-ledger.js +8 -11
- package/src/edit-lock/admin-recovery.js +2 -1
- package/src/edit-lock/admission.js +12 -2
- package/src/edit-lock/binding-guidance.js +28 -10
- package/src/edit-lock/binding-machine.js +312 -0
- package/src/edit-lock/derived-facts.js +67 -0
- package/src/edit-lock/index.js +44 -244
- package/src/edit-lock/inspect.js +113 -1
- package/src/edit-lock/lifecycle.js +8 -5
- package/src/edit-lock/limits.js +61 -0
- package/src/edit-lock/maintenance.js +5 -2
- package/src/edit-lock/manager.js +6 -2
- package/src/edit-lock/operation-history.js +1 -1
- package/src/edit-lock/peer-client.js +9 -2
- package/src/edit-lock/peer-transport.js +7 -1
- package/src/edit-lock/remote.js +11 -4
- package/src/edit-lock/state.js +2 -4
- package/src/edit-lock/view.js +10 -11
- package/src/lsp/admin.js +2 -24
- package/src/lsp/index.js +1 -1
- package/src/lsp/manager.js +1 -1
- package/src/notify/permissions-admin.js +1 -1
- package/src/{lsp/child-process.js → process/index.js} +23 -6
- package/src/settings/env-admin.js +1 -1
- package/src/settings/index.js +1 -1
- package/src/settings/sections.js +0 -54
- package/src/shared/audit.js +12 -0
- package/src/shared/child-scope.js +3 -1
- package/src/shared/http-json.js +28 -0
- package/src/{delegate/audit-readers.js → shared/terminal-evidence.js} +34 -4
- package/src/worktree/errors.js +3 -0
- package/src/worktree/index.js +1 -2
- package/src/worktree/lane-core.js +641 -0
- package/src/worktree/lane-views.js +147 -0
- package/src/worktree/lane-workflows.js +853 -0
- package/src/worktree/lanes.js +24 -1513
- package/src/worktree/runner.js +5 -4
- package/src/capabilities/content-pin.js +0 -88
- package/src/capabilities/content-refresh.js +0 -331
- package/src/capabilities/distribution-manifest.js +0 -78
- package/src/capabilities/isolated-exec.js +0 -106
- package/src/capabilities/publisher.js +0 -129
- package/src/capabilities/skill-admission.js +0 -181
- package/src/capabilities/update-lifecycle.js +0 -106
- package/src/edit-lock/remote-service.js +0 -28
- package/src/worktree/contract-guidance.test.js +0 -100
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { createHash } from 'node:crypto'
|
|
4
4
|
import { realpathSync } from 'node:fs'
|
|
5
5
|
import { isSegment } from './store/paths.js'
|
|
6
|
+
import { createSkillIdentity, skillIdentityKey } from './skill-identity.js'
|
|
7
|
+
import { createMcpRegistry } from './mcp-registry.js'
|
|
6
8
|
|
|
7
9
|
export const PRESET_SCHEMA_VERSION = 1
|
|
8
10
|
|
|
@@ -166,3 +168,248 @@ export function createPresetLibrary({ store }) {
|
|
|
166
168
|
}
|
|
167
169
|
return { load, create, edit, rename, resave, remove, exportPreset, withNamespace, unitOf }
|
|
168
170
|
}
|
|
171
|
+
|
|
172
|
+
// ---- /capabilities preset verbs (capabilities-deepening D2) -----------------
|
|
173
|
+
// The seven preset-verb bodies of the /capabilities command, moved verbatim
|
|
174
|
+
// out of the handler into the module that owns the preset contract (the CAS
|
|
175
|
+
// read-modify-write and the created/edited/deleted/revision-conflict/
|
|
176
|
+
// name-conflict status values above). Per-invocation state arrives through
|
|
177
|
+
// the `call` argument ({ agent, options }); everything the handler used to
|
|
178
|
+
// close over is injected in `wiring` — including the payload builders and the
|
|
179
|
+
// v2 packer, so this module keeps its import graph acyclic (read-payloads.js
|
|
180
|
+
// and preset-package.js both import from here, never the reverse).
|
|
181
|
+
|
|
182
|
+
/** Shared scope validation for the preset verbs (command-surface text). */
|
|
183
|
+
export const presetScopeError = spec => (spec?.scope !== 'global' && spec?.scope !== 'workspace')
|
|
184
|
+
? { kind: 'error', text: `capabilities: scope must be "global" | "workspace", got ${JSON.stringify(spec?.scope ?? null)}` }
|
|
185
|
+
: null
|
|
186
|
+
|
|
187
|
+
/** The shared no-workspace command result (the historical shape: kind error). */
|
|
188
|
+
export const noWorkspaceError = () => ({ kind: 'error', text: JSON.stringify({ status: 'no-workspace' }) })
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The draft branch of the save-time set resolution: the client-supplied
|
|
192
|
+
* arrays, normalized. (from:'applied' never takes this branch — its sets are
|
|
193
|
+
* server-authoritative.)
|
|
194
|
+
*/
|
|
195
|
+
export const draftSelectionSets = spec => ({
|
|
196
|
+
skills: Array.isArray(spec.skills) ? spec.skills : [],
|
|
197
|
+
mcpServers: Array.isArray(spec.mcpServers) ? spec.mcpServers : [],
|
|
198
|
+
unresolvedRefs: Array.isArray(spec.unresolvedRefs) ? spec.unresolvedRefs : [],
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
const message = error => (error instanceof Error ? error.message : String(error))
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The /capabilities preset verb bodies. `wiring` is the handler's former
|
|
205
|
+
* closure surface, injected explicitly:
|
|
206
|
+
* @param {{
|
|
207
|
+
* openStore: () => any,
|
|
208
|
+
* provider: any,
|
|
209
|
+
* inventory: (options: any) => Promise<any>,
|
|
210
|
+
* lifecycle: any,
|
|
211
|
+
* builders: { buildPresetsPayload: Function, buildReceiptPayload: Function },
|
|
212
|
+
* packPresetPackage: Function,
|
|
213
|
+
* }} wiring
|
|
214
|
+
*/
|
|
215
|
+
export function createPresetVerbs(wiring) {
|
|
216
|
+
/**
|
|
217
|
+
* Applied preset writers share one locked source proof. The library
|
|
218
|
+
* enters this callback only under its namespace guard, and holds the
|
|
219
|
+
* selection lock until the target unit CAS has finished.
|
|
220
|
+
*/
|
|
221
|
+
const appliedPresetSource = (spec, sets, store, agent, options) => commit => store.withSelectionSnapshot(agent.id, async current => {
|
|
222
|
+
if (!Number.isSafeInteger(spec.expectedSelectionRevision) || spec.expectedSelectionRevision < 0
|
|
223
|
+
|| typeof spec.expectedSelectionDigest !== 'string' || !/^[a-f0-9]{64}$/.test(spec.expectedSelectionDigest)
|
|
224
|
+
|| current.revision !== spec.expectedSelectionRevision) return { status: 'source-changed', revision: current.revision }
|
|
225
|
+
const receipt = await wiring.builders.buildReceiptPayload({ provider: wiring.provider, lifecycle: wiring.lifecycle, store }, options)
|
|
226
|
+
const effective = receipt.effective
|
|
227
|
+
const selected = effective.skillIdentities
|
|
228
|
+
const raw = current.kind === 'ok' ? current.payload?.skills : null
|
|
229
|
+
if (current.kind === 'ok') {
|
|
230
|
+
const rawKeys = new Set()
|
|
231
|
+
const effectiveKeys = new Set(selected.map(skillIdentityKey))
|
|
232
|
+
const missing = []
|
|
233
|
+
for (const identity of Array.isArray(raw) ? raw : []) {
|
|
234
|
+
try {
|
|
235
|
+
const key = skillIdentityKey(createSkillIdentity(identity))
|
|
236
|
+
rawKeys.add(key)
|
|
237
|
+
if (!effectiveKeys.has(key)) missing.push(identity.name)
|
|
238
|
+
} catch { missing.push(String(identity?.name ?? identity ?? 'unknown')) }
|
|
239
|
+
}
|
|
240
|
+
if (!Array.isArray(raw) || missing.length || rawKeys.size !== selected.length || receipt.warnings.length) {
|
|
241
|
+
return { status: 'source-unavailable', missing, reason: receipt.warnings[0] ?? 'Selected Skills are not completely available' }
|
|
242
|
+
}
|
|
243
|
+
if (!Array.isArray(current.payload?.mcpServers)
|
|
244
|
+
|| JSON.stringify(current.payload.mcpServers) !== JSON.stringify(effective.mcpServers)) {
|
|
245
|
+
return { status: 'source-unavailable', missing: [], reason: 'Applied MCP snapshot is unavailable' }
|
|
246
|
+
}
|
|
247
|
+
} else if (receipt.warnings.length) return { status: 'source-unavailable', missing: [], reason: receipt.warnings[0] }
|
|
248
|
+
// Provider enumeration can short-circuit an empty selection.
|
|
249
|
+
const observed = await wiring.inventory(options)
|
|
250
|
+
if (observed?.complete !== true) return { status: 'source-unavailable', missing: [], reason: 'Skill inventory is incomplete' }
|
|
251
|
+
if (effective.digest !== spec.expectedSelectionDigest || receipt.revision !== current.revision) {
|
|
252
|
+
return { status: 'source-changed', revision: current.revision }
|
|
253
|
+
}
|
|
254
|
+
sets.skills.push(...selected)
|
|
255
|
+
sets.mcpServers.push(...effective.mcpServers)
|
|
256
|
+
return commit()
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
/** presets: thin shell over the shared builder. */
|
|
260
|
+
async function presets(_spec, call) {
|
|
261
|
+
// Thin shell over the shared builder (read-payloads.js): domain failures
|
|
262
|
+
// stay JSON status text (the panel categorizes them); the remote resolves
|
|
263
|
+
// the same object as a plain value.
|
|
264
|
+
try {
|
|
265
|
+
const payload = await wiring.builders.buildPresetsPayload({ store: wiring.openStore() }, call.options)
|
|
266
|
+
if (typeof payload.status === 'string') return { kind: 'error', text: JSON.stringify(payload) }
|
|
267
|
+
return { kind: 'success', text: JSON.stringify(payload) }
|
|
268
|
+
} catch (cause) {
|
|
269
|
+
return { kind: 'error', text: `capabilities presets failed: ${message(cause)}` }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** preset-save / preset-rename / preset-resave (one shared body, per action). */
|
|
274
|
+
async function presetSaveLike(action, spec, call) {
|
|
275
|
+
try {
|
|
276
|
+
const scopeError = presetScopeError(spec)
|
|
277
|
+
if (scopeError) return scopeError
|
|
278
|
+
const workspaceKey = workspaceKeyOf(call.options)
|
|
279
|
+
if (spec.scope === 'workspace' && !isSegment(workspaceKey)) return noWorkspaceError()
|
|
280
|
+
const library = createPresetLibrary({ store: wiring.openStore() })
|
|
281
|
+
if (action === 'preset-rename' || action === 'preset-resave') {
|
|
282
|
+
if (!isSegment(spec.presetId)) return { kind: 'error', text: `${action}: invalid presetId` }
|
|
283
|
+
if (!Number.isSafeInteger(spec.expectedRevision) || spec.expectedRevision < 1) return { kind: 'error', text: `${action}: expectedRevision must be positive` }
|
|
284
|
+
if (action === 'preset-rename') {
|
|
285
|
+
const result = await library.rename({ scope: spec.scope, presetId: spec.presetId, workspaceKey, expectedRevision: spec.expectedRevision, name: spec.name })
|
|
286
|
+
return { kind: 'success', text: JSON.stringify(result) }
|
|
287
|
+
}
|
|
288
|
+
if (spec.from !== 'draft' && spec.from !== 'applied') return { kind: 'error', text: 'preset-resave: from must be draft or applied' }
|
|
289
|
+
if (spec.from === 'draft' && ![spec.skills, spec.mcpServers, spec.unresolvedRefs].every(Array.isArray)) {
|
|
290
|
+
return { kind: 'error', text: JSON.stringify({ status: 'invalid-selection' }) }
|
|
291
|
+
}
|
|
292
|
+
const store = wiring.openStore()
|
|
293
|
+
const sets = spec.from === 'applied' ? { skills: [], mcpServers: [], unresolvedRefs: [] } : draftSelectionSets(spec)
|
|
294
|
+
const withSource = spec.from === 'applied' ? appliedPresetSource(spec, sets, store, call.agent, call.options) : undefined
|
|
295
|
+
const result = await library.resave({
|
|
296
|
+
scope: spec.scope, presetId: spec.presetId, workspaceKey, expectedRevision: spec.expectedRevision,
|
|
297
|
+
selection: sets, withSource,
|
|
298
|
+
})
|
|
299
|
+
return { kind: 'success', text: JSON.stringify(result) }
|
|
300
|
+
}
|
|
301
|
+
if (spec.from !== 'draft' && spec.from !== 'applied') return { kind: 'error', text: 'preset-save: from must be draft or applied' }
|
|
302
|
+
if (typeof spec.name !== 'string' || !spec.name.length) return { kind: 'error', text: 'preset-save: name must be non-empty' }
|
|
303
|
+
if (spec.expectedRevision !== undefined && spec.from === 'draft' && ![spec.skills, spec.mcpServers, spec.unresolvedRefs].every(Array.isArray)) {
|
|
304
|
+
return { kind: 'error', text: JSON.stringify({ status: 'invalid-selection' }) }
|
|
305
|
+
}
|
|
306
|
+
const store = wiring.openStore()
|
|
307
|
+
const sets = spec.from === 'applied' ? { skills: [], mcpServers: [], unresolvedRefs: [] } : draftSelectionSets(spec)
|
|
308
|
+
const selection = { skills: sets.skills, mcpServers: sets.mcpServers, unresolvedRefs: sets.unresolvedRefs }
|
|
309
|
+
const withSource = spec.from === 'applied' ? appliedPresetSource(spec, sets, store, call.agent, call.options) : undefined
|
|
310
|
+
if (spec.expectedRevision !== undefined) {
|
|
311
|
+
if (!isSegment(spec.presetId)) return { kind: 'error', text: 'preset-save: edit needs presetId' }
|
|
312
|
+
const result = await library.edit({ scope: spec.scope, presetId: spec.presetId, workspaceKey,
|
|
313
|
+
expectedRevision: spec.expectedRevision, mutate: () => ({ name: spec.name, selection }), withSource })
|
|
314
|
+
return { kind: 'success', text: JSON.stringify(result) }
|
|
315
|
+
}
|
|
316
|
+
const result = await library.create({ scope: spec.scope, presetId: spec.presetId, workspaceKey,
|
|
317
|
+
document: { name: spec.name, selection }, onNameConflict: spec.onNameConflict ?? 'cancel', replaceTarget: spec.replaceTarget, withSource })
|
|
318
|
+
return { kind: 'success', text: JSON.stringify(result) }
|
|
319
|
+
} catch (cause) {
|
|
320
|
+
return { kind: 'error', text: `capabilities ${action} failed: ${message(cause)}` }
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** preset-load: load one preset document; the client stages its selection. */
|
|
325
|
+
async function presetLoad(spec, call) {
|
|
326
|
+
try {
|
|
327
|
+
const scopeError = presetScopeError(spec)
|
|
328
|
+
if (scopeError) return scopeError
|
|
329
|
+
if (!isSegment(spec.presetId)) return { kind: 'error', text: 'capabilities preset-load: presetId must be a valid store segment' }
|
|
330
|
+
const workspaceKey = workspaceKeyOf(call.options)
|
|
331
|
+
if (spec.scope === 'workspace' && !isSegment(workspaceKey)) return noWorkspaceError()
|
|
332
|
+
const library = createPresetLibrary({ store: wiring.openStore() })
|
|
333
|
+
const loaded = await library.load(spec.scope, spec.presetId, workspaceKey)
|
|
334
|
+
if (loaded.kind !== 'ok') return { kind: 'success', text: JSON.stringify({ status: loaded.kind }) }
|
|
335
|
+
return { kind: 'success', text: JSON.stringify({ status: 'ok', revision: loaded.revision, document: loaded.document }) }
|
|
336
|
+
} catch (cause) {
|
|
337
|
+
return { kind: 'error', text: `capabilities preset-load failed: ${message(cause)}` }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** preset-delete: delete under CAS; a stale revision conflicts. */
|
|
342
|
+
async function presetDelete(spec, call) {
|
|
343
|
+
try {
|
|
344
|
+
const scopeError = presetScopeError(spec)
|
|
345
|
+
if (scopeError) return scopeError
|
|
346
|
+
if (!isSegment(spec.presetId)) return { kind: 'error', text: 'capabilities preset-delete: presetId must be a valid store segment' }
|
|
347
|
+
if (!Number.isSafeInteger(spec.expectedRevision) || spec.expectedRevision < 0) {
|
|
348
|
+
return { kind: 'error', text: 'capabilities preset-delete: expectedRevision must be a non-negative integer' }
|
|
349
|
+
}
|
|
350
|
+
const workspaceKey = workspaceKeyOf(call.options)
|
|
351
|
+
if (spec.scope === 'workspace' && !isSegment(workspaceKey)) return noWorkspaceError()
|
|
352
|
+
const library = createPresetLibrary({ store: wiring.openStore() })
|
|
353
|
+
const result = await library.remove({ scope: spec.scope, presetId: spec.presetId, workspaceKey, expectedRevision: spec.expectedRevision })
|
|
354
|
+
return { kind: 'success', text: JSON.stringify(result) }
|
|
355
|
+
} catch (cause) {
|
|
356
|
+
return { kind: 'error', text: `capabilities preset-delete failed: ${message(cause)}` }
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* preset-export: format:'document' is the v1 portable document (verbatim
|
|
362
|
+
* via library.exportPreset — inspect/backup); the default 'package' is the
|
|
363
|
+
* D6 version-2 package: remote Skills as portable refs, workspace/local
|
|
364
|
+
* Skills as bundled content, builtin Skills by name, managed MCP as
|
|
365
|
+
* {identity,label}.
|
|
366
|
+
*/
|
|
367
|
+
async function presetExport(spec, call) {
|
|
368
|
+
try {
|
|
369
|
+
const scopeError = presetScopeError(spec)
|
|
370
|
+
if (scopeError) return scopeError
|
|
371
|
+
if (!isSegment(spec.presetId)) return { kind: 'error', text: 'capabilities preset-export: presetId must be a valid store segment' }
|
|
372
|
+
const format = spec.format ?? 'package'
|
|
373
|
+
if (format !== 'package' && format !== 'document') {
|
|
374
|
+
return { kind: 'error', text: 'capabilities preset-export: format must be "package" | "document"' }
|
|
375
|
+
}
|
|
376
|
+
const workspaceKey = workspaceKeyOf(call.options)
|
|
377
|
+
if (spec.scope === 'workspace' && !isSegment(workspaceKey)) return noWorkspaceError()
|
|
378
|
+
const library = createPresetLibrary({ store: wiring.openStore() })
|
|
379
|
+
if (format === 'document') {
|
|
380
|
+
const exported = await library.exportPreset(spec.scope, spec.presetId, workspaceKey)
|
|
381
|
+
return { kind: 'success', text: JSON.stringify(exported) }
|
|
382
|
+
}
|
|
383
|
+
const loaded = await library.load(spec.scope, spec.presetId, workspaceKey)
|
|
384
|
+
if (loaded.kind !== 'ok') return { kind: 'success', text: JSON.stringify({ status: loaded.kind }) }
|
|
385
|
+
// Pack against the live inventory (Skill file content comes from the
|
|
386
|
+
// matched candidates' locators) and the managed registry (MCP labels).
|
|
387
|
+
const inventoryResult = await wiring.inventory(call.options)
|
|
388
|
+
const candidates = Array.isArray(inventoryResult?.candidates) ? inventoryResult.candidates : []
|
|
389
|
+
const registry = createMcpRegistry({ store: wiring.openStore() })
|
|
390
|
+
const record = await registry.read()
|
|
391
|
+
/** @type {Record<string, string>} */
|
|
392
|
+
const labels = {}
|
|
393
|
+
if (record.kind === 'ok') for (const entry of Object.values(record.servers)) labels[entry.identity] = entry.label
|
|
394
|
+
const source = /** @type {Record<string, unknown>} */ (loaded.document ?? {})
|
|
395
|
+
const packed = await wiring.packPresetPackage({ name: source.name, selection: source.selection }, {
|
|
396
|
+
candidates,
|
|
397
|
+
mcpLabels: identity => labels[identity],
|
|
398
|
+
})
|
|
399
|
+
if (!packed.ok) return { kind: 'error', text: JSON.stringify({ status: 'rejected', reason: packed.reason }) }
|
|
400
|
+
return { kind: 'success', text: JSON.stringify({ status: 'ok', format: 'package', document: packed.package }) }
|
|
401
|
+
} catch (cause) {
|
|
402
|
+
return { kind: 'error', text: `capabilities preset-export failed: ${message(cause)}` }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
presets,
|
|
408
|
+
'preset-save': (spec, call) => presetSaveLike('preset-save', spec, call),
|
|
409
|
+
'preset-rename': (spec, call) => presetSaveLike('preset-rename', spec, call),
|
|
410
|
+
'preset-resave': (spec, call) => presetSaveLike('preset-resave', spec, call),
|
|
411
|
+
'preset-load': presetLoad,
|
|
412
|
+
'preset-delete': presetDelete,
|
|
413
|
+
'preset-export': presetExport,
|
|
414
|
+
}
|
|
415
|
+
}
|
|
@@ -29,7 +29,13 @@
|
|
|
29
29
|
import { readFile, readdir, realpath, stat } from 'node:fs/promises'
|
|
30
30
|
import { dirname, join, resolve } from 'node:path'
|
|
31
31
|
import { createSkillIdentity, skillIdentityKey } from './skill-identity.js'
|
|
32
|
-
import { validateMcpRef, validateSkillRef } from './portable-refs.js'
|
|
32
|
+
import { validateMcpRef, validateSkillRef, bindImportedSelection, validatePortableDocument } from './portable-refs.js'
|
|
33
|
+
import { createPresetLibrary, workspaceKeyOf, presetScopeError, noWorkspaceError } from './preset-library.js'
|
|
34
|
+
import { installBundledSkills } from './preset-package-install.js'
|
|
35
|
+
import { inspectPresetNamespace } from './store/enumerate.js'
|
|
36
|
+
import { createMcpRegistry } from './mcp-registry.js'
|
|
37
|
+
import { isSegment } from './store/paths.js'
|
|
38
|
+
import { AUDIT_TYPES } from '../shared/audit.js'
|
|
33
39
|
|
|
34
40
|
export const PACKAGE_VERSION = 2
|
|
35
41
|
export const PACKAGE_LIMITS = Object.freeze({ documentBytes: 8 * 1024 * 1024, fileBytes: 512 * 1024, filesPerSkill: 256 })
|
|
@@ -485,3 +491,208 @@ export async function packPresetPackage(input, deps = {}) {
|
|
|
485
491
|
if (!verdict.ok) return { ok: false, reason: verdict.reason }
|
|
486
492
|
return { ok: true, package: pkg }
|
|
487
493
|
}
|
|
494
|
+
|
|
495
|
+
// ---- /capabilities preset-import verb (capabilities-deepening D2) -----------
|
|
496
|
+
// The preset-import verb body and its D6 version-2 orchestration, moved
|
|
497
|
+
// verbatim out of the handler into the module that owns the package contract
|
|
498
|
+
// (validatePresetPackage/bindPackageSelection/planPackageInstall/
|
|
499
|
+
// packPresetPackage above). Per-invocation state arrives through the `call`
|
|
500
|
+
// argument ({ agent, options }); the handler's former closures are injected in
|
|
501
|
+
// `wiring`. The install target roots still resolve through the injected
|
|
502
|
+
// skillRootsFor.
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* The /capabilities preset-import verb body. `wiring`:
|
|
506
|
+
* @param {{
|
|
507
|
+
* openStore: () => any,
|
|
508
|
+
* inventory: (options: any) => Promise<any>,
|
|
509
|
+
* skillRootsFor: (options: any) => Promise<any>,
|
|
510
|
+
* audit: (session: any, type: string, data?: unknown) => void,
|
|
511
|
+
* }} wiring
|
|
512
|
+
*/
|
|
513
|
+
export function createPackageVerbs(wiring) {
|
|
514
|
+
/**
|
|
515
|
+
* D6 version-2 package import (tasks 6.2/6.3). Phase one (dryRun:true) is
|
|
516
|
+
* a read-only summary — the exact install targets, file counts, unresolved
|
|
517
|
+
* refs and collisions, ZERO writes. Phase two (confirmed, onCollision
|
|
518
|
+
* default 'cancel') installs the bundled Skills into the resolved roots
|
|
519
|
+
* and then commits the preset record by CAS. A poison package is rejected
|
|
520
|
+
* atomically: nothing is written, no preset is created, the reason is
|
|
521
|
+
* named. Installed Skills stay UNSELECTED in every established set —
|
|
522
|
+
* selection remains a later draft + Apply.
|
|
523
|
+
*/
|
|
524
|
+
const importPresetPackage = async (spec, call) => {
|
|
525
|
+
const verdict = validatePresetPackage(spec.document)
|
|
526
|
+
if (!verdict.ok) return { kind: 'error', text: JSON.stringify({ status: 'rejected', reason: verdict.reason }) }
|
|
527
|
+
if (spec.dryRun !== undefined && typeof spec.dryRun !== 'boolean') {
|
|
528
|
+
return { kind: 'error', text: 'capabilities preset-import: dryRun must be a boolean' }
|
|
529
|
+
}
|
|
530
|
+
const onCollision = spec.onCollision ?? 'cancel'
|
|
531
|
+
if (onCollision !== 'cancel' && onCollision !== 'replace' && onCollision !== 'coexist') {
|
|
532
|
+
return { kind: 'error', text: 'capabilities preset-import: onCollision must be "cancel" | "replace" | "coexist"' }
|
|
533
|
+
}
|
|
534
|
+
const workspaceKey = workspaceKeyOf(call.options)
|
|
535
|
+
if (spec.scope === 'workspace' && !isSegment(workspaceKey)) return noWorkspaceError()
|
|
536
|
+
const pkg = /** @type {any} */ (spec.document)
|
|
537
|
+
const store = wiring.openStore()
|
|
538
|
+
// Local binding context: the managed MCP identities this machine
|
|
539
|
+
// configured, the builtin names it actually has, and the D6 install roots
|
|
540
|
+
// (project = this workspace, user = user root) via the existing
|
|
541
|
+
// skill-inventory root resolution.
|
|
542
|
+
const registry = createMcpRegistry({ store })
|
|
543
|
+
const record = await registry.read()
|
|
544
|
+
const localMcpIdentities = record.kind === 'ok' ? Object.keys(record.servers) : []
|
|
545
|
+
const bound = bindPackageSelection(pkg, { localMcpIdentities })
|
|
546
|
+
const inventoryResult = await wiring.inventory(call.options)
|
|
547
|
+
const candidates = Array.isArray(inventoryResult?.candidates) ? inventoryResult.candidates : []
|
|
548
|
+
const builtinNames = new Set()
|
|
549
|
+
for (const candidate of candidates) {
|
|
550
|
+
const identity = /** @type {Record<string, any>} */ (candidate)?.identity
|
|
551
|
+
if (identity?.scope === 'weir-builtin' && typeof identity.name === 'string') builtinNames.add(identity.name)
|
|
552
|
+
}
|
|
553
|
+
const missingBuiltin = pkg.builtin.filter(name => !builtinNames.has(name))
|
|
554
|
+
const roots = await wiring.skillRootsFor(call.options)
|
|
555
|
+
const targetRoots = {
|
|
556
|
+
project: roots.find(root => root.source === 'project-dsh')?.path ?? null,
|
|
557
|
+
user: roots.find(root => root.source === 'user-dsh')?.path ?? null,
|
|
558
|
+
}
|
|
559
|
+
const plan = await planPackageInstall(pkg, { roots: targetRoots })
|
|
560
|
+
const carried = [
|
|
561
|
+
...bound.unresolvedRefs,
|
|
562
|
+
...missingBuiltin.map(name => ({ kind: 'skill', ref: { name, source: 'builtin' } })),
|
|
563
|
+
]
|
|
564
|
+
if (spec.dryRun === true) {
|
|
565
|
+
const view = await inspectPresetNamespace(store, spec.scope, workspaceKey)
|
|
566
|
+
if (view.status !== 'ok') return { kind: 'error', text: JSON.stringify(view) }
|
|
567
|
+
const name = typeof spec.name === 'string' && spec.name.length ? spec.name : pkg.name
|
|
568
|
+
const conflicts = view.names.get(name) ?? []
|
|
569
|
+
const presetConflict = conflicts.length ? { with: conflicts[0], withRevision: view.activeById.get(conflicts[0]).revision, duplicates: conflicts.length > 1 } : null
|
|
570
|
+
return { kind: 'success', text: JSON.stringify({ status: 'dry-run', install: plan.install, unresolved: carried, collisions: plan.collisions, presetConflict }) }
|
|
571
|
+
}
|
|
572
|
+
// Confirmed: the install is this import's explicit main effect.
|
|
573
|
+
const neededScopes = [...new Set(pkg.bundled.map(entry => entry.targetScope))]
|
|
574
|
+
const missingRoot = neededScopes.find(scope => targetRoots[scope] === null)
|
|
575
|
+
if (missingRoot !== undefined) {
|
|
576
|
+
return { kind: 'error', text: JSON.stringify({ status: 'no-target-root', targetScope: missingRoot }) }
|
|
577
|
+
}
|
|
578
|
+
const name = typeof spec.name === 'string' && spec.name.length ? spec.name : pkg.name
|
|
579
|
+
const requestedId = spec.presetId
|
|
580
|
+
if (requestedId !== undefined && requestedId !== null && !isSegment(requestedId)) return { kind: 'error', text: `capabilities preset-import: invalid presetId ${JSON.stringify(requestedId)}` }
|
|
581
|
+
const library = createPresetLibrary({ store })
|
|
582
|
+
const guardedResult = await library.withNamespace(spec.scope, workspaceKey, async guarded => {
|
|
583
|
+
const request = { scope: spec.scope, presetId: requestedId, workspaceKey,
|
|
584
|
+
document: { name, selection: { skills: [], mcpServers: [], unresolvedRefs: [] } },
|
|
585
|
+
onNameConflict: spec.onNameConflict ?? 'cancel', replaceTarget: spec.replaceTarget }
|
|
586
|
+
const admission = await guarded.checkCreate(request)
|
|
587
|
+
if (admission.status !== 'ready') return { kind: 'success', text: JSON.stringify(admission) }
|
|
588
|
+
let install
|
|
589
|
+
try {
|
|
590
|
+
install = await installBundledSkills(pkg.bundled, { roots: targetRoots, onCollision }, {
|
|
591
|
+
audit: data => wiring.audit(call.agent.session, AUDIT_TYPES.capabilityPresetImport, data),
|
|
592
|
+
})
|
|
593
|
+
if (!install.ok) {
|
|
594
|
+
// installBundledSkills already rolled back what it wrote.
|
|
595
|
+
return { kind: 'error', text: JSON.stringify({ status: 'install-failed', reason: install.reason, rolledBack: install.rolledBack }) }
|
|
596
|
+
}
|
|
597
|
+
// Resolve what is now local (installed bundled Skills, present builtin
|
|
598
|
+
// names) against a FRESH inventory: the preset record names real
|
|
599
|
+
// identities; everything else stays unresolved.
|
|
600
|
+
const afterResult = await wiring.inventory(call.options)
|
|
601
|
+
const afterCandidates = Array.isArray(afterResult?.candidates) ? afterResult.candidates : []
|
|
602
|
+
const resolveLocal = (scope, name) => {
|
|
603
|
+
const matches = []
|
|
604
|
+
for (const candidate of afterCandidates) {
|
|
605
|
+
const identity = /** @type {Record<string, any>} */ (candidate)?.identity
|
|
606
|
+
if (identity?.scope === scope && identity.name === name) matches.push(identity)
|
|
607
|
+
}
|
|
608
|
+
// An ambiguous name stays unresolved rather than guessing.
|
|
609
|
+
return matches.length === 1 ? matches[0] : null
|
|
610
|
+
}
|
|
611
|
+
const resolvedSkills = []
|
|
612
|
+
const unresolvedRefs = [...carried]
|
|
613
|
+
for (const entry of install.installed) {
|
|
614
|
+
const identity = resolveLocal(entry.targetScope === 'project' ? 'project' : 'user', entry.target)
|
|
615
|
+
if (identity) resolvedSkills.push(identity)
|
|
616
|
+
else unresolvedRefs.push({ kind: 'skill', ref: { name: entry.target, targetScope: entry.targetScope, source: 'bundled' } })
|
|
617
|
+
}
|
|
618
|
+
for (const entry of install.collisions) {
|
|
619
|
+
unresolvedRefs.push({ kind: 'skill', ref: { name: entry.name, targetScope: entry.targetScope, source: 'bundled', collision: entry.decision } })
|
|
620
|
+
}
|
|
621
|
+
for (const name of pkg.builtin) {
|
|
622
|
+
if (missingBuiltin.includes(name)) continue
|
|
623
|
+
const identity = resolveLocal('weir-builtin', name)
|
|
624
|
+
if (identity) resolvedSkills.push(identity)
|
|
625
|
+
else unresolvedRefs.push({ kind: 'skill', ref: { name, source: 'builtin' } })
|
|
626
|
+
}
|
|
627
|
+
const document = { name, selection: { skills: resolvedSkills, mcpServers: bound.mcpServers, unresolvedRefs } }
|
|
628
|
+
const result = await guarded.create({ ...request, document })
|
|
629
|
+
return { kind: 'success', text: JSON.stringify({
|
|
630
|
+
...result,
|
|
631
|
+
bound: { mcpServers: bound.mcpServers.length, unresolvedRefs: unresolvedRefs.length },
|
|
632
|
+
installed: install.installed,
|
|
633
|
+
collisions: install.collisions,
|
|
634
|
+
unresolved: unresolvedRefs,
|
|
635
|
+
...(result.status !== 'created' && result.status !== 'edited' ? { presetStatus: 'unconfirmed' } : {}),
|
|
636
|
+
}) }
|
|
637
|
+
} catch (cause) {
|
|
638
|
+
return { kind: 'error', text: JSON.stringify({ status: 'preset-unconfirmed',
|
|
639
|
+
presetStatus: 'unconfirmed', installed: install?.installed ?? [], reason: cause instanceof Error ? cause.message : String(cause) }) }
|
|
640
|
+
}
|
|
641
|
+
})
|
|
642
|
+
return guardedResult?.kind === 'success' || guardedResult?.kind === 'error'
|
|
643
|
+
? guardedResult : { kind: 'success', text: JSON.stringify(guardedResult) }
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Import a portable document: validate atomically first (a poison document
|
|
648
|
+
* is rejected with zero writes), then bind only the identities the local
|
|
649
|
+
* MCP registry already configured — everything else stays an unresolved
|
|
650
|
+
* ref. Nothing is installed, started or connected here.
|
|
651
|
+
*/
|
|
652
|
+
async function presetImport(spec, call) {
|
|
653
|
+
try {
|
|
654
|
+
const scopeError = presetScopeError(spec)
|
|
655
|
+
if (scopeError) return scopeError
|
|
656
|
+
// Version dispatch (D6): a version-2 package takes the two-phase path
|
|
657
|
+
// (dryRun summary → confirmed install + CAS preset create). Everything
|
|
658
|
+
// else keeps the v1 portable path, whose own gate rejects unknown
|
|
659
|
+
// versions atomically.
|
|
660
|
+
if (spec.document !== null && typeof spec.document === 'object' && !Array.isArray(spec.document)
|
|
661
|
+
&& /** @type {Record<string, unknown>} */ (spec.document).version === 2) {
|
|
662
|
+
return await importPresetPackage(spec, call)
|
|
663
|
+
}
|
|
664
|
+
const verdict = validatePortableDocument(spec.document)
|
|
665
|
+
if (!verdict.ok) return { kind: 'error', text: JSON.stringify({ status: 'rejected', reason: verdict.reason }) }
|
|
666
|
+
const workspaceKey = workspaceKeyOf(call.options)
|
|
667
|
+
if (spec.scope === 'workspace' && !isSegment(workspaceKey)) return noWorkspaceError()
|
|
668
|
+
const presetId = spec.presetId ?? /** @type {Record<string, unknown>} */ (spec.document).presetId
|
|
669
|
+
if (!isSegment(presetId)) return { kind: 'error', text: 'capabilities preset-import: presetId must be a valid store segment' }
|
|
670
|
+
const name = typeof spec.name === 'string' && spec.name.length ? spec.name : /** @type {Record<string, unknown>} */ (spec.document).name
|
|
671
|
+
const store = wiring.openStore()
|
|
672
|
+
const registry = createMcpRegistry({ store })
|
|
673
|
+
const record = await registry.read()
|
|
674
|
+
const localMcpIdentities = record.kind === 'ok' ? Object.keys(record.servers) : []
|
|
675
|
+
const selection = /** @type {Record<string, unknown>} */ (/** @type {Record<string, unknown>} */ (spec.document).selection ?? {})
|
|
676
|
+
const bound = bindImportedSelection(selection, { localMcpIdentities })
|
|
677
|
+
const document = {
|
|
678
|
+
name,
|
|
679
|
+
selection: {
|
|
680
|
+
skills: [],
|
|
681
|
+
mcpServers: bound.mcpServers,
|
|
682
|
+
unresolvedRefs: [
|
|
683
|
+
...(Array.isArray(selection.unresolvedRefs) ? selection.unresolvedRefs : []),
|
|
684
|
+
...bound.unresolvedRefs,
|
|
685
|
+
],
|
|
686
|
+
},
|
|
687
|
+
}
|
|
688
|
+
const library = createPresetLibrary({ store })
|
|
689
|
+
const result = await library.create({ scope: spec.scope, presetId, workspaceKey, document,
|
|
690
|
+
onNameConflict: spec.onNameConflict ?? 'cancel', replaceTarget: spec.replaceTarget })
|
|
691
|
+
return { kind: 'success', text: JSON.stringify({ ...result, bound: { mcpServers: bound.mcpServers.length, unresolvedRefs: bound.unresolvedRefs.length } }) }
|
|
692
|
+
} catch (cause) {
|
|
693
|
+
return { kind: 'error', text: `capabilities preset-import failed: ${cause instanceof Error ? cause.message : String(cause)}` }
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return { 'preset-import': presetImport }
|
|
698
|
+
}
|
|
@@ -1,27 +1,6 @@
|
|
|
1
|
-
// Task 10.3 of the session-capability-manager change:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// informational only, never an authorization signal. A missing reference is
|
|
5
|
-
// never satisfied by a same-named entry, and installing into an occupied
|
|
6
|
-
// name writes ONLY after an explicit replace / coexist-distinct / cancel
|
|
7
|
-
// decision.
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Rank the entries for the informational listing (10.3): discovery
|
|
11
|
-
* precedence is REPORTED, never interpreted as authorization.
|
|
12
|
-
* @param {Array<{ name: string, scope?: string, rank?: number, provenance?: unknown }>} entries
|
|
13
|
-
*/
|
|
14
|
-
export function sameNameListing(entries) {
|
|
15
|
-
return entries
|
|
16
|
-
.filter(entry => entry && typeof entry.name === 'string')
|
|
17
|
-
.map(entry => ({
|
|
18
|
-
name: entry.name,
|
|
19
|
-
scope: entry.scope ?? 'unknown',
|
|
20
|
-
provenance: entry.provenance ?? null,
|
|
21
|
-
precedence: Number.isSafeInteger(entry.rank) ? entry.rank : null,
|
|
22
|
-
}))
|
|
23
|
-
.sort((a, b) => (a.precedence ?? Number.MAX_SAFE_INTEGER) - (b.precedence ?? Number.MAX_SAFE_INTEGER) || a.scope.localeCompare(b.scope))
|
|
24
|
-
}
|
|
1
|
+
// Task 10.3 of the session-capability-manager change: the "no silent
|
|
2
|
+
// overwrite" rule. Installing into an occupied name writes ONLY after an
|
|
3
|
+
// explicit replace / coexist-distinct / cancel decision.
|
|
25
4
|
|
|
26
5
|
/**
|
|
27
6
|
* The install-into-occupied-name decision (10.3): the default is CANCEL —
|
|
@@ -42,20 +21,3 @@ export function installTargetFor({ name, occupied, decision = 'cancel', targetNa
|
|
|
42
21
|
}
|
|
43
22
|
return { write: false, target: null, status: 'cancelled' }
|
|
44
23
|
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* A missing reference is never satisfied by a same-named entry (10.3): the
|
|
48
|
-
* resolution of a ref matches by IDENTITY, and an identity mismatch is
|
|
49
|
-
* always unresolved even when the display name matches.
|
|
50
|
-
* @param {{ name?: string, identity?: unknown }} wanted @param {{ name?: string, identity?: unknown }} candidate
|
|
51
|
-
* @param {(identity: unknown) => string} identityKeyOf
|
|
52
|
-
*/
|
|
53
|
-
export function refSatisfiedBy(wanted, candidate, identityKeyOf) {
|
|
54
|
-
if (wanted?.identity === undefined || wanted.identity === null) return false
|
|
55
|
-
if (candidate?.identity === undefined || candidate.identity === null) return false
|
|
56
|
-
try {
|
|
57
|
-
return identityKeyOf(wanted.identity) === identityKeyOf(candidate.identity)
|
|
58
|
-
} catch {
|
|
59
|
-
return false
|
|
60
|
-
}
|
|
61
|
-
}
|