weir-harness 1.2.0 → 1.2.1

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.
Files changed (92) hide show
  1. package/lib/client.blackboard-model.js +172 -56
  2. package/lib/client.capability-manager.js +0 -1
  3. package/lib/client.capability-model.js +378 -31
  4. package/lib/client.chain-model.js +80 -0
  5. package/lib/client.disabled-categories-editor.js +22 -0
  6. package/lib/client.edit-lock-maintenance.js +239 -246
  7. package/lib/client.edit-lock-panel.js +110 -0
  8. package/lib/client.hash-edit-model.js +48 -0
  9. package/lib/client.js +267 -1549
  10. package/lib/client.lsp-model.js +92 -0
  11. package/lib/client.lsp-panel.js +11 -16
  12. package/lib/client.lsp-toggle.js +18 -0
  13. package/lib/client.notify-permissions.js +112 -20
  14. package/lib/client.notify-web.js +14 -11
  15. package/lib/client.robash-model.js +26 -0
  16. package/lib/client.settings-page.js +287 -17
  17. package/lib/client.worktree-model.js +225 -0
  18. package/package.json +1 -1
  19. package/src/blackboard/durable.js +12 -0
  20. package/src/blackboard/index.js +2 -2
  21. package/src/blackboard/kernel.js +23 -6
  22. package/src/blackboard/notify.js +10 -3
  23. package/src/blackboard/remote.js +31 -13
  24. package/src/blackboard/storage.js +13 -5
  25. package/src/capabilities/apply-engine.js +189 -101
  26. package/src/capabilities/defaults-transaction.js +136 -1
  27. package/src/capabilities/lifecycle-snapshot.js +83 -204
  28. package/src/capabilities/mcp-admission.js +1 -23
  29. package/src/capabilities/mcp-drain.js +30 -19
  30. package/src/capabilities/mcp-facade.js +23 -3
  31. package/src/capabilities/preset-library.js +247 -0
  32. package/src/capabilities/preset-package.js +212 -1
  33. package/src/capabilities/same-name-policy.js +3 -41
  34. package/src/capabilities/selection-notify.js +324 -21
  35. package/src/capabilities/skill-selection-plugin.js +315 -729
  36. package/src/capabilities/store/checked-contract.d.ts +5 -0
  37. package/src/capabilities/store/fs-adapter.js +52 -0
  38. package/src/capabilities/store/lock.js +86 -0
  39. package/src/capabilities/store/paths.js +5 -0
  40. package/src/capabilities/store/store.js +105 -5
  41. package/src/delegate/group-coordinator.js +1 -17
  42. package/src/delegate/index.js +4 -2
  43. package/src/delegate/rehydrate.js +2 -1
  44. package/src/delegate/robash-guard-core.js +2 -2
  45. package/src/delegate/supervision-mount.js +5 -4
  46. package/src/delegate/tool.js +26 -3
  47. package/src/{shared → delegate}/whitelist-defaults.js +2 -2
  48. package/src/edit-lock/admin-ledger.js +8 -11
  49. package/src/edit-lock/admin-recovery.js +2 -1
  50. package/src/edit-lock/admission.js +12 -2
  51. package/src/edit-lock/binding-guidance.js +28 -10
  52. package/src/edit-lock/binding-machine.js +312 -0
  53. package/src/edit-lock/derived-facts.js +67 -0
  54. package/src/edit-lock/index.js +44 -244
  55. package/src/edit-lock/inspect.js +113 -1
  56. package/src/edit-lock/lifecycle.js +8 -5
  57. package/src/edit-lock/limits.js +61 -0
  58. package/src/edit-lock/maintenance.js +5 -2
  59. package/src/edit-lock/manager.js +6 -2
  60. package/src/edit-lock/operation-history.js +1 -1
  61. package/src/edit-lock/peer-client.js +9 -2
  62. package/src/edit-lock/peer-transport.js +7 -1
  63. package/src/edit-lock/remote.js +11 -4
  64. package/src/edit-lock/state.js +2 -4
  65. package/src/edit-lock/view.js +10 -11
  66. package/src/lsp/admin.js +2 -24
  67. package/src/lsp/index.js +1 -1
  68. package/src/lsp/manager.js +1 -1
  69. package/src/notify/permissions-admin.js +1 -1
  70. package/src/{lsp/child-process.js → process/index.js} +12 -4
  71. package/src/settings/env-admin.js +1 -1
  72. package/src/settings/index.js +1 -1
  73. package/src/settings/sections.js +0 -54
  74. package/src/shared/audit.js +5 -0
  75. package/src/shared/http-json.js +28 -0
  76. package/src/{delegate/audit-readers.js → shared/terminal-evidence.js} +34 -4
  77. package/src/worktree/errors.js +3 -0
  78. package/src/worktree/index.js +1 -2
  79. package/src/worktree/lane-core.js +641 -0
  80. package/src/worktree/lane-views.js +147 -0
  81. package/src/worktree/lane-workflows.js +853 -0
  82. package/src/worktree/lanes.js +24 -1513
  83. package/src/worktree/runner.js +5 -4
  84. package/src/capabilities/content-pin.js +0 -88
  85. package/src/capabilities/content-refresh.js +0 -331
  86. package/src/capabilities/distribution-manifest.js +0 -78
  87. package/src/capabilities/isolated-exec.js +0 -106
  88. package/src/capabilities/publisher.js +0 -129
  89. package/src/capabilities/skill-admission.js +0 -181
  90. package/src/capabilities/update-lifecycle.js +0 -106
  91. package/src/edit-lock/remote-service.js +0 -28
  92. 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: same-name listing and
2
- // the "no silent substitute / no silent overwrite" rules. Every same-name
3
- // entry is listed with its scope, provenance and discovery precedence —
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
- }