weir-harness 2.0.0 → 2.1.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.
- package/lib/client.capability-badge.js +2 -0
- package/lib/client.capability-manager.js +46 -179
- package/lib/client.capability-model.js +428 -3
- package/lib/client.file-lease-tool-model.js +189 -0
- package/lib/client.file-lease-tool-view.js +434 -0
- package/lib/client.js +105 -7
- package/lib/client.mcp-manager.js +563 -0
- package/package.json +1 -1
- package/skills/mcp-server-setup/SKILL.md +124 -0
- package/src/capabilities/apply-engine.js +18 -4
- package/src/capabilities/capability-remote.js +296 -45
- package/src/capabilities/initial-selection.js +3 -2
- package/src/capabilities/lifecycle-preload.js +69 -11
- package/src/capabilities/lifecycle-snapshot.js +127 -39
- package/src/capabilities/mcp-admission.js +2 -2
- package/src/capabilities/mcp-config.js +344 -0
- package/src/capabilities/mcp-credentials.js +180 -0
- package/src/capabilities/mcp-diagnostics.js +169 -0
- package/src/capabilities/mcp-drain.js +18 -4
- package/src/capabilities/mcp-facade.js +40 -9
- package/src/capabilities/mcp-gate.js +51 -52
- package/src/capabilities/mcp-install-tool.js +276 -0
- package/src/capabilities/mcp-manager.js +575 -146
- package/src/capabilities/mcp-mount.js +220 -79
- package/src/capabilities/mcp-operations.js +359 -0
- package/src/capabilities/mcp-process.js +43 -0
- package/src/capabilities/mcp-registry.js +74 -60
- package/src/capabilities/mcp-runtime.js +103 -0
- package/src/capabilities/read-payloads.js +8 -3
- package/src/capabilities/skill-applicability.js +185 -0
- package/src/capabilities/skill-builtin-migration.js +2 -1
- package/src/capabilities/skill-selection-plugin.js +170 -57
- package/src/file-lease/client.js +268 -35
- package/src/file-lease/index.js +4 -0
- package/src/file-lease/plugin.js +239 -15
- package/src/file-lease/tools.js +426 -38
- package/src/file-lease/watch.js +473 -0
- package/src/intent-gate/index.js +29 -12
- package/src/intent-gate/matcher.js +36 -2
- package/src/lease-broker/protocol.js +3 -2
- package/src/lease-broker/server.js +6 -3
- package/src/lease-broker/state.js +68 -11
- package/src/process/index.js +4 -2
- package/src/shared/audit.js +3 -0
- package/src/shared/child-scope.js +2 -0
- package/src/worktree/rules.js +1 -1
|
@@ -15,6 +15,7 @@ import { createLifecycleSnapshots } from './lifecycle-snapshot.js'
|
|
|
15
15
|
import { preloadLifecycleSnapshots } from './lifecycle-preload.js'
|
|
16
16
|
import { classifySelectionFailure } from './selection-status.js'
|
|
17
17
|
import { resolveInitialSelection, baselineSkillIdentities, bindDefaultSkillNames } from './initial-selection.js'
|
|
18
|
+
import { partitionUnresolvedSkills } from './skill-applicability.js'
|
|
18
19
|
import { workspaceKeyOf, createPresetVerbs } from './preset-library.js'
|
|
19
20
|
import { createDefaultsVerbs } from './defaults-transaction.js'
|
|
20
21
|
import { buildPresetsPayload, buildDefaultGetPayload } from './read-payloads.js'
|
|
@@ -23,8 +24,11 @@ import { createApplyEngine } from './apply-engine.js'
|
|
|
23
24
|
import { createSessionDrainAdapter } from './mcp-drain.js'
|
|
24
25
|
import { createSelectionNotifier } from './selection-notify.js'
|
|
25
26
|
import { createMcpRegistry } from './mcp-registry.js'
|
|
27
|
+
import { createMcpOperations } from './mcp-operations.js'
|
|
28
|
+
import { mcpDiagnostic } from './mcp-diagnostics.js'
|
|
26
29
|
import { buildReceiptPayload, buildListPayload, buildConditionsPayload } from './read-payloads.js'
|
|
27
|
-
import { feedCapabilityReadBridge } from './capability-remote.js'
|
|
30
|
+
import { feedCapabilityReadBridge, capabilityReadFeed, capabilityOwnership, createCapabilityMutationService, sameMutationOwner } from './capability-remote.js'
|
|
31
|
+
import { createMcpInstallTool } from './mcp-install-tool.js'
|
|
28
32
|
import { parseFrontmatter } from './frontmatter.js'
|
|
29
33
|
import { createAudit } from '../shared/audit.js'
|
|
30
34
|
|
|
@@ -84,8 +88,14 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
84
88
|
// between the host's session publication and our capture, or a
|
|
85
89
|
// hand-built session) is REFUSED — never granted the root
|
|
86
90
|
// baseline (6.4 subagent rule; explicit resume recaptures).
|
|
91
|
+
//
|
|
92
|
+
// Only a TRUE delegated child (depth > 0) is refused: a depth-0
|
|
93
|
+
// fork also carries a parentSession header, and when its source
|
|
94
|
+
// never established any effective snapshot the creation-time
|
|
95
|
+
// capture is skipped on purpose so that this read performs the
|
|
96
|
+
// ordinary root initialization for the fork itself.
|
|
87
97
|
const header = options.scope?.session?.header ?? {}
|
|
88
|
-
const
|
|
98
|
+
const isDelegatedChild = (header.delegationDepth ?? 0) > 0
|
|
89
99
|
let inherited
|
|
90
100
|
try {
|
|
91
101
|
inherited = await store.read({ kind: 'inherited', sessionId })
|
|
@@ -94,7 +104,12 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
94
104
|
}
|
|
95
105
|
if (inherited.kind === 'ok') return inherited.payload?.skills
|
|
96
106
|
if (inherited.kind !== 'absent') throw new Error(`Inherited skill snapshot is ${inherited.kind}`)
|
|
97
|
-
if (
|
|
107
|
+
if (isDelegatedChild) throw new Error('Inherited skill snapshot is absent')
|
|
108
|
+
// Root initialization is only for a PROVEN absent source. A depth-0
|
|
109
|
+
// fork whose capture or existing policy already became BLOCKED must
|
|
110
|
+
// keep the inherited-snapshot failure, never bypass it with a fresh
|
|
111
|
+
// baseline; an explicit accepted Apply remains the recovery path.
|
|
112
|
+
if (lifecycle?.snapshotFor?.(sessionId)?.state === 'blocked') throw new Error('Inherited skill snapshot is absent')
|
|
98
113
|
return initialSelection(options)
|
|
99
114
|
}
|
|
100
115
|
if (record.kind !== 'ok') throw new Error(`Skill selection policy is ${record.kind}`)
|
|
@@ -151,18 +166,26 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
151
166
|
}
|
|
152
167
|
let builtinIdentities = []
|
|
153
168
|
let enabledMcpIdentities = []
|
|
169
|
+
const registry = await createMcpRegistry({ store }).read()
|
|
170
|
+
if (registry.kind !== 'ok' && registry.kind !== 'absent') throw new Error(`MCP registry is ${registry.kind}`)
|
|
171
|
+
const registryServers = registry.kind === 'ok' ? registry.servers : {}
|
|
172
|
+
// Only a FULLY formed initial result may become a lifecycle snapshot:
|
|
173
|
+
// a baseline or a name binding derived from an incomplete inventory is
|
|
174
|
+
// a best-effort view, never an inheritable set (D2).
|
|
175
|
+
let complete = true
|
|
154
176
|
if (defaultsRecord.kind === 'absent') {
|
|
155
177
|
const snapshot = await inventory(options)
|
|
178
|
+
complete = snapshot?.complete !== false
|
|
156
179
|
// Baseline scopes are row-configurable (default builtin-only);
|
|
157
180
|
// the weir-creative preset adds 'custom' for its fused skills.
|
|
158
181
|
builtinIdentities = baselineSkillIdentities(snapshot?.candidates, config.baselineScopes)
|
|
159
|
-
|
|
160
|
-
const registry = await store.read({ kind: 'mcp-registry' })
|
|
161
|
-
const payload = registry.kind === 'ok' ? registry.payload ?? {} : {}
|
|
162
|
-
enabledMcpIdentities = Array.isArray(/** @type {Record<string, unknown>} */ (payload).enabled) ? /** @type {string[]} */ (/** @type {Record<string, unknown>} */ (payload).enabled) : []
|
|
163
|
-
} catch { /* an unreadable registry enables no managed identity by default */ }
|
|
182
|
+
enabledMcpIdentities = registry.kind === 'ok' ? registry.enabled : []
|
|
164
183
|
}
|
|
165
184
|
const resolved = resolveInitialSelection({ defaultsRecord, builtinIdentities, enabledMcpIdentities })
|
|
185
|
+
for (const identity of resolved.selection.mcpServers) {
|
|
186
|
+
if (!registryServers[identity]) resolved.missing.push({ kind: 'mcp', ref: identity, reason: 'configured default is not registered' })
|
|
187
|
+
}
|
|
188
|
+
resolved.selection.mcpIncarnations = Object.fromEntries(resolved.selection.mcpServers.filter(identity => registryServers[identity]).map(identity => [identity, registryServers[identity].incarnation ?? null]))
|
|
166
189
|
// Name-string skill entries in a workspace default (the client
|
|
167
190
|
// draft's wire shape, written by default-save from:'draft') bind
|
|
168
191
|
// against the live inventory by unique name; unbindable entries
|
|
@@ -171,6 +194,7 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
171
194
|
// entirely — it still only runs on the builtin-baseline path.
|
|
172
195
|
if (resolved.source === 'workspace-default' && resolved.selection.skills.some(entry => typeof entry === 'string')) {
|
|
173
196
|
const snapshot = await inventory(options)
|
|
197
|
+
complete = complete && snapshot?.complete !== false
|
|
174
198
|
const bound = bindDefaultSkillNames(resolved.selection.skills, snapshot?.candidates)
|
|
175
199
|
resolved.selection.skills = bound.skills
|
|
176
200
|
resolved.missing = [...resolved.missing, ...bound.missing]
|
|
@@ -178,6 +202,24 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
178
202
|
const sessionId = options.scope?.session?.id
|
|
179
203
|
if (typeof sessionId === 'string' && sessionId.length) {
|
|
180
204
|
initialReports.set(sessionId, { source: resolved.source, missing: resolved.missing, mcpServers: resolved.selection.mcpServers })
|
|
205
|
+
// Effective-set publication for Fork inheritance: a depth-0 fork of a
|
|
206
|
+
// session whose initial set was already resolved must inherit THAT
|
|
207
|
+
// set, not an empty one. The result is published as the session's
|
|
208
|
+
// revision-0 lifecycle snapshot — never persisted, never an accepted
|
|
209
|
+
// record. A snapshot that appeared while this async resolution was
|
|
210
|
+
// in flight (an accepted Apply, a captured inheritance) wins, and a
|
|
211
|
+
// partially resolved/incomplete result is not published at all.
|
|
212
|
+
if (complete && lifecycle && typeof lifecycle.peek === 'function' && typeof lifecycle.publish === 'function'
|
|
213
|
+
&& lifecycle.peek(sessionId) === null) {
|
|
214
|
+
lifecycle.publish(sessionId, {
|
|
215
|
+
revision: 0,
|
|
216
|
+
skills: resolved.selection.skills,
|
|
217
|
+
mcpServers: resolved.selection.mcpServers,
|
|
218
|
+
mcpIncarnations: resolved.selection.mcpIncarnations,
|
|
219
|
+
captured: null,
|
|
220
|
+
})
|
|
221
|
+
ctx.get?.('weirMcpManager')?.syncSession?.(sessionId)
|
|
222
|
+
}
|
|
181
223
|
}
|
|
182
224
|
return resolved.selection.skills
|
|
183
225
|
})
|
|
@@ -262,6 +304,7 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
262
304
|
locateSession: session => session,
|
|
263
305
|
inventory: (options) => inventory(options),
|
|
264
306
|
provider,
|
|
307
|
+
mcpRegistry: createMcpRegistry({ store: storeFor() }),
|
|
265
308
|
...applyEngineHooks({ ctx, lifecycle, notifier, audit }),
|
|
266
309
|
})
|
|
267
310
|
return sharedEngine
|
|
@@ -403,6 +446,11 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
403
446
|
matches.push(candidate)
|
|
404
447
|
parsedByName.set(candidate.name, matches)
|
|
405
448
|
}
|
|
449
|
+
// The unresolved draft entries keep their ORIGINAL shape: a legacy
|
|
450
|
+
// name string is not an identity and cannot be classified by
|
|
451
|
+
// workspace, while an identity object carries the scope/root the
|
|
452
|
+
// cross-workspace partition needs. Collapsing both to a name here
|
|
453
|
+
// is what made a foreign project reference unactionable.
|
|
406
454
|
const missing = []
|
|
407
455
|
const identities = []
|
|
408
456
|
const admitted = new Set()
|
|
@@ -415,7 +463,7 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
415
463
|
} else {
|
|
416
464
|
const identity = createSkillIdentity(entry)
|
|
417
465
|
candidate = byKey.get(skillIdentityKey(identity))
|
|
418
|
-
if (!candidate) missing.push(identity
|
|
466
|
+
if (!candidate) missing.push(identity)
|
|
419
467
|
}
|
|
420
468
|
if (!candidate) continue
|
|
421
469
|
const key = skillIdentityKey(candidate.identity)
|
|
@@ -423,19 +471,26 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
423
471
|
admitted.add(key)
|
|
424
472
|
identities.push(candidate.identity)
|
|
425
473
|
}
|
|
426
|
-
// preset
|
|
427
|
-
// KNOWN-BUT-INAPPLICABLE (resolvable in a profile-supplemental
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
474
|
+
// preset applicability partition (D2 + cross-workspace D4):
|
|
475
|
+
// - KNOWN-BUT-INAPPLICABLE names (resolvable in a profile-supplemental
|
|
476
|
+
// directory, e.g. creative-preset skills carried by a workspace
|
|
477
|
+
// default) are SKIPPED with a per-name reason;
|
|
478
|
+
// - a project identity PROVEN to belong to another workspace (its
|
|
479
|
+
// canonical root is outside every current project Skill root of a
|
|
480
|
+
// COMPLETE inventory) is SKIPPED as not applicable in this
|
|
481
|
+
// workspace — the preset contract, not a same-name conflict, and
|
|
482
|
+
// never a local same-name substitute;
|
|
483
|
+
// - everything else keeps the hard-failure contract with the draft
|
|
484
|
+
// preserved (a local identity the inventory no longer resolves, a
|
|
485
|
+
// project identity of THIS workspace, an incomplete inventory, or
|
|
486
|
+
// an unresolvable current-workspace root set authorizes no skip).
|
|
487
|
+
// Records are never rewritten for applicability, so a preset
|
|
488
|
+
// round-trip stays lossless.
|
|
489
|
+
const { skipped, unknown } = partitionUnresolvedSkills(missing, {
|
|
490
|
+
roots: inventoryResult?.roots,
|
|
491
|
+
complete: inventoryResult?.complete,
|
|
492
|
+
knownSupplemental: name => supplementalSkillKnown(config.supplementalSkillDirs, name),
|
|
493
|
+
})
|
|
439
494
|
if (unknown.length > 0) return { kind: 'error', text: JSON.stringify({ status: 'missing', missing: unknown, ...(skipped.length > 0 ? { skipped } : {}) }) }
|
|
440
495
|
const response = await face.applySelection(
|
|
441
496
|
{ sessionId: agent.id, cwd: agent.session?.header?.cwd, presetId: 'weir' },
|
|
@@ -448,41 +503,36 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
448
503
|
}
|
|
449
504
|
}
|
|
450
505
|
|
|
451
|
-
|
|
452
|
-
* mcp-add: register a new managed MCP server and mount it
|
|
453
|
-
* immediately (8.1 registry + manager start). Payload:
|
|
454
|
-
* {identity, label, command, args?: [], env?: {}, serverName?}
|
|
455
|
-
*/
|
|
456
|
-
const mcpAddVerb = async (spec) => {
|
|
506
|
+
const mcpVerb = (action) => async (spec = {}, { agent }) => {
|
|
457
507
|
try {
|
|
458
|
-
if (typeof spec.identity !== 'string' || typeof spec.label !== 'string' || typeof spec.command !== 'string' || spec.command.length === 0) {
|
|
508
|
+
if (action === 'mcp-add' && (typeof spec.identity !== 'string' || typeof spec.label !== 'string' || typeof spec.command !== 'string' || spec.command.length === 0)) {
|
|
459
509
|
return { kind: 'error', text: 'mcp-add needs identity, label and command strings' }
|
|
460
510
|
}
|
|
461
|
-
const
|
|
462
|
-
const
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
transport: 'stdio',
|
|
472
|
-
serverName: typeof spec.serverName === 'string' && spec.serverName.length ? spec.serverName : spec.identity,
|
|
473
|
-
command: spec.command,
|
|
474
|
-
args: Array.isArray(spec.args) ? spec.args.map(String) : [],
|
|
475
|
-
env: spec.env && typeof spec.env === 'object' && !Array.isArray(spec.env) ? spec.env : {},
|
|
476
|
-
reconnect: { enabled: true },
|
|
477
|
-
},
|
|
511
|
+
const { expectedRevision, confirmNamespaceChange, targetFingerprint, attemptId, batch, mode, refsOnly, surface: _ignoredSurface, ...fields } = spec
|
|
512
|
+
const operation = action === 'mcp-default' ? 'setDefault' : action === 'mcp-preview' ? 'preview' : action === 'mcp-batch' ? 'batch' : action.slice(4)
|
|
513
|
+
const request = {
|
|
514
|
+
...spec,
|
|
515
|
+
action: operation === 'batch' ? 'add' : operation,
|
|
516
|
+
refsOnly: action === 'mcp-add' ? false : true,
|
|
517
|
+
// The command channel is refs-only (legacy mcp-add aside). A
|
|
518
|
+
// caller-supplied `surface` is ignored here: only the Host UI
|
|
519
|
+
// mutation channel may accept transient literal secrets.
|
|
520
|
+
surface: action === 'mcp-add' ? 'legacy' : 'refs',
|
|
478
521
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
522
|
+
if (!spec.config && !spec.document && !['remove', 'restart', 'setDefault', 'recover'].includes(operation)) {
|
|
523
|
+
if (operation === 'batch' || fields.mcpServers) request.document = fields
|
|
524
|
+
else request.config = fields
|
|
525
|
+
}
|
|
526
|
+
const operations = face.mcpOperations(agent.session)
|
|
527
|
+
const result = operation === 'preview'
|
|
528
|
+
? await operations.preview(request)
|
|
529
|
+
: operation === 'batch'
|
|
530
|
+
? await operations.executeBatch(request)
|
|
531
|
+
: await operations.execute(request)
|
|
532
|
+
const successful = new Set(['preview', 'mounted', 'saved', 'removed', 'recovered'])
|
|
533
|
+
return { kind: successful.has(result.status) ? 'success' : 'error', text: JSON.stringify(result) }
|
|
484
534
|
} catch (cause) {
|
|
485
|
-
return { kind: 'error', text:
|
|
535
|
+
return { kind: 'error', text: JSON.stringify({ status: 'validation-failed', saved: false, error: mcpDiagnostic(cause, { code: cause?.code ?? 'validation-failed' }) }) }
|
|
486
536
|
}
|
|
487
537
|
}
|
|
488
538
|
|
|
@@ -498,7 +548,15 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
498
548
|
receipt: { payload: 'none', run: receiptVerb },
|
|
499
549
|
list: { payload: 'none', run: listVerb },
|
|
500
550
|
apply: { payload: 'required', usage: 'Usage: /capabilities apply <json selection>', run: applyVerb },
|
|
501
|
-
'mcp-add': { payload: 'required', usage: 'Usage: /capabilities mcp-add <json {identity,label,command,args?,env?,serverName?}>', run:
|
|
551
|
+
'mcp-add': { payload: 'required', usage: 'Usage: /capabilities mcp-add <json {identity,label,command,args?,env?,serverName?}>', run: mcpVerb('mcp-add') },
|
|
552
|
+
'mcp-preview': { payload: 'required', usage: 'Usage: /capabilities mcp-preview <json server|{mcpServers:{...}}>', run: mcpVerb('mcp-preview') },
|
|
553
|
+
'mcp-batch': { payload: 'required', usage: 'Usage: /capabilities mcp-batch <json {mcpServers:{...},mode?:"add"|"edit"}>', run: mcpVerb('mcp-batch') },
|
|
554
|
+
'mcp-edit': { payload: 'required', usage: 'Usage: /capabilities mcp-edit <json {identity,expectedRevision,...config}>', run: mcpVerb('mcp-edit') },
|
|
555
|
+
'mcp-remove': { payload: 'required', usage: 'Usage: /capabilities mcp-remove <json {identity,expectedRevision}>', run: mcpVerb('mcp-remove') },
|
|
556
|
+
'mcp-restart': { payload: 'required', usage: 'Usage: /capabilities mcp-restart <json {identity,expectedRevision}>', run: mcpVerb('mcp-restart') },
|
|
557
|
+
'mcp-test': { payload: 'required', usage: 'Usage: /capabilities mcp-test <json config>', run: mcpVerb('mcp-test') },
|
|
558
|
+
'mcp-default': { payload: 'required', usage: 'Usage: /capabilities mcp-default <json {identity,enabled,expectedRevision?}>', run: mcpVerb('mcp-default') },
|
|
559
|
+
'mcp-recover': { payload: 'optional', usage: 'Usage: /capabilities mcp-recover [json {identity?}]', run: mcpVerb('mcp-recover') },
|
|
502
560
|
presets: { payload: 'none', run: presetVerbs.presets },
|
|
503
561
|
'preset-save': { payload: 'required', usage: 'Usage: /capabilities preset-save <json>', run: presetVerbs['preset-save'] },
|
|
504
562
|
'preset-rename': { payload: 'required', usage: 'Usage: /capabilities preset-rename <json>', run: presetVerbs['preset-rename'] },
|
|
@@ -515,7 +573,7 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
515
573
|
|
|
516
574
|
ctx.effect(() => commands.register({
|
|
517
575
|
name: 'capabilities',
|
|
518
|
-
description: 'Session capability surface
|
|
576
|
+
description: 'Session capability surface (receipt | list | apply | mcp-preview | mcp-add | mcp-batch | mcp-edit | mcp-remove | mcp-restart | mcp-test | mcp-default | mcp-recover | presets and workspace defaults).',
|
|
519
577
|
handler: async (invocation) => {
|
|
520
578
|
const agent = invocation?.agent
|
|
521
579
|
if (!agent) return { kind: 'error', text: 'capabilities: requires an owning agent session' }
|
|
@@ -528,8 +586,11 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
528
586
|
const head = verb.split(' ', 1)[0]
|
|
529
587
|
const entry = verbTable[head]
|
|
530
588
|
const hasArgs = verb.length > head.length
|
|
531
|
-
if (!entry || (entry.payload === 'none' && hasArgs)
|
|
532
|
-
return { kind: 'error', text: 'Usage: /capabilities receipt|list|
|
|
589
|
+
if (!entry || (entry.payload === 'none' && hasArgs)) {
|
|
590
|
+
return { kind: 'error', text: 'Usage: /capabilities receipt|list|apply|mcp-preview|mcp-add|mcp-batch|mcp-edit|mcp-remove|mcp-restart|mcp-test|mcp-default|mcp-recover|presets|default-get|default-save|default-clear' }
|
|
591
|
+
}
|
|
592
|
+
if (entry.payload === 'required' && !hasArgs) {
|
|
593
|
+
return { kind: 'error', text: entry.usage ?? 'Usage: /capabilities <verb> <json>' }
|
|
533
594
|
}
|
|
534
595
|
let spec
|
|
535
596
|
if (entry.payload !== 'none' && hasArgs) {
|
|
@@ -572,6 +633,14 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
572
633
|
clearFailure: options => provider?.clearFailure(options),
|
|
573
634
|
/** Shared Apply entry (group 12 manager UI commits here). */
|
|
574
635
|
applySelection: (session, request, options) => engineFor().apply(session, request, options),
|
|
636
|
+
mcpOperations: (session, options = {}) => createMcpOperations({
|
|
637
|
+
registry: createMcpRegistry({ store: storeFor() }),
|
|
638
|
+
manager: ctx.get?.('weirMcpManager'),
|
|
639
|
+
credentials: ctx.get?.('credentials'),
|
|
640
|
+
audit,
|
|
641
|
+
session,
|
|
642
|
+
assertOwner: options.assertOwner,
|
|
643
|
+
}),
|
|
575
644
|
/** 12.4 diagnostics: whether a removal notice is queued for a session. */
|
|
576
645
|
hasPendingNotification: sessionId => notifier.has(sessionId),
|
|
577
646
|
}
|
|
@@ -587,11 +656,48 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
587
656
|
inventory: (options, previous) => inventory(options, previous),
|
|
588
657
|
lifecycle,
|
|
589
658
|
mcpManager: () => ctx.get?.('weirMcpManager'),
|
|
659
|
+
mcpOperations: face.mcpOperations,
|
|
590
660
|
profileContext: () => ctx.get?.('profileContext'),
|
|
591
661
|
}), 'weir-capability-read-bridge')
|
|
592
662
|
} catch (cause) {
|
|
593
663
|
ctx.logger?.warn?.(`capability read bridge feed failed: ${cause instanceof Error ? cause.message : String(cause)}`)
|
|
594
664
|
}
|
|
665
|
+
// Primary-Orchestrator AI installation rides the same selection row. All
|
|
666
|
+
// Host services are resolved lazily; missing confirmation/ownership support
|
|
667
|
+
// disables install without affecting inert preview or manual management.
|
|
668
|
+
try {
|
|
669
|
+
const tools = ctx.get?.('tools')
|
|
670
|
+
if (tools?.register) {
|
|
671
|
+
const sessionQuery = () => ctx.get?.('sessionQuery')
|
|
672
|
+
// Two retained revisions of this preset may feed the bridge at once;
|
|
673
|
+
// the owner is resolved per session through the agent's own revision.
|
|
674
|
+
const runtime = { agents: () => ctx.get?.('agents'), agentPresets: () => ctx.get?.('agentPresets') }
|
|
675
|
+
const mutations = createCapabilityMutationService({ sessionQuery, ...runtime })
|
|
676
|
+
const captureOwnership = async sessionId => {
|
|
677
|
+
const lease = await sessionQuery()?.observeSession?.(sessionId)
|
|
678
|
+
if (!lease || typeof lease[Symbol.dispose] !== 'function') throw Object.assign(new Error('MCP install session observation is unavailable'), { code: 'owner-unavailable' })
|
|
679
|
+
try {
|
|
680
|
+
const owner = capabilityOwnership(lease, sessionId)
|
|
681
|
+
const feed = capabilityReadFeed(owner.presetId, sessionId, runtime)
|
|
682
|
+
const faces = feed.faces ?? feed
|
|
683
|
+
return { owner, feed, profile: faces.profileContext?.() }
|
|
684
|
+
} finally { lease[Symbol.dispose]() }
|
|
685
|
+
}
|
|
686
|
+
const tool = createMcpInstallTool({
|
|
687
|
+
operationsFor: session => face.mcpOperations(session),
|
|
688
|
+
mutate: (sessionId, request) => mutations.mutate(sessionId, request),
|
|
689
|
+
captureOwnership,
|
|
690
|
+
sameOwnership: sameMutationOwner,
|
|
691
|
+
describeExecution: entry => ctx.get?.('weirMcpManager')?.describeExecution?.(entry),
|
|
692
|
+
userQuestions: () => ctx.get?.('userQuestions'),
|
|
693
|
+
isLiveRoot: agent => Boolean(agent && ctx.get?.('agents')?.get?.(agent.id) === agent),
|
|
694
|
+
})
|
|
695
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => tools.register(tool), 'weir-mcp-install-tool')
|
|
696
|
+
else tools.register(tool)
|
|
697
|
+
}
|
|
698
|
+
} catch (cause) {
|
|
699
|
+
ctx.logger?.warn?.(`MCP install tool registration failed: ${cause instanceof Error ? cause.message : String(cause)}`)
|
|
700
|
+
}
|
|
595
701
|
}
|
|
596
702
|
}
|
|
597
703
|
|
|
@@ -620,7 +726,14 @@ export function applyEngineHooks({ ctx, lifecycle, notifier, audit = createAudit
|
|
|
620
726
|
// manager row has no managed servers to drain and the adapter reports
|
|
621
727
|
// the empty truth.
|
|
622
728
|
drain: createSessionDrainAdapter(() => ctx.get?.('weirMcpManager')?.drain),
|
|
623
|
-
publishSnapshot: (sessionId, snapshot) =>
|
|
729
|
+
publishSnapshot: (sessionId, snapshot) => {
|
|
730
|
+
lifecycle?.publish?.(sessionId, snapshot)
|
|
731
|
+
// Committed Apply and receipt recovery share this hook. Reconcile the
|
|
732
|
+
// current live agent after lifecycle publication and before the engine's
|
|
733
|
+
// provider invalidation/fence release; thrown failures retain the
|
|
734
|
+
// engine's existing snapshot-publication-degraded semantics.
|
|
735
|
+
ctx.get?.('weirMcpManager')?.syncSession?.(sessionId)
|
|
736
|
+
},
|
|
624
737
|
onAccepted: (sessionId, delta, info) => notifier.queueSync(sessionId, delta, info),
|
|
625
738
|
// The Apply-path convergence emission (5.2 parity with agent/created):
|
|
626
739
|
// an accepted Apply re-emits the preset invalidation event so every
|