weir-harness 1.2.3 → 2.1.0
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 +7 -18
- 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-badge.js +307 -0
- package/lib/client.file-lease-tool-model.js +189 -0
- package/lib/client.file-lease-tool-view.js +434 -0
- package/lib/client.js +124 -36
- package/lib/client.mcp-manager.js +563 -0
- package/lib/client.settings-page.js +10 -34
- package/package.json +2 -2
- 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 +3 -3
- package/src/capabilities/mcp-gate.js +51 -52
- package/src/capabilities/mcp-install-tool.js +276 -0
- package/src/capabilities/mcp-manager.js +368 -142
- package/src/capabilities/mcp-mount.js +219 -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 +161 -56
- package/src/capabilities/store/checked-contract.d.ts +2 -2
- package/src/capabilities/store/fs-adapter.js +2 -3
- package/src/context-guard/index.js +2 -2
- package/src/delegate/supervision-mount.js +5 -5
- package/src/delegate/tool.js +2 -10
- package/src/file-lease/client.js +1299 -0
- package/src/file-lease/index.js +34 -0
- package/src/file-lease/lineage.js +207 -0
- package/src/file-lease/node-contracts.d.ts +42 -0
- package/src/file-lease/plugin.js +1013 -0
- package/src/file-lease/singleton.js +155 -0
- package/src/file-lease/spawn.js +62 -0
- package/src/file-lease/tools.js +847 -0
- package/src/file-lease/view.js +144 -0
- package/src/file-lease/watch.js +473 -0
- package/src/hashline-edit/index.js +76 -42
- package/src/hashline-edit/write-tool.js +90 -0
- package/src/intent-gate/index.js +29 -12
- package/src/intent-gate/matcher.js +36 -2
- package/src/lease-broker/entry.js +98 -0
- package/src/lease-broker/node-contracts.d.ts +63 -0
- package/src/lease-broker/protocol.js +237 -0
- package/src/lease-broker/server.js +402 -0
- package/src/lease-broker/state.js +482 -0
- package/src/lsp/tools.js +137 -27
- package/src/process/index.js +4 -2
- package/src/settings/index.js +2 -44
- package/src/settings/sections.js +2 -24
- package/src/shared/audit.js +9 -1
- package/src/shared/child-scope.js +2 -0
- package/src/todo-driver/index.js +1 -6
- package/src/worktree/cards.js +6 -21
- package/src/worktree/lane-core.js +1 -19
- package/src/worktree/lane-workflows.js +11 -32
- package/src/worktree/rules.js +1 -1
- package/lib/client.edit-lock-maintenance.js +0 -592
- package/lib/client.edit-lock-panel.js +0 -405
- package/src/edit-lock/adapter.js +0 -57
- package/src/edit-lock/admin-ledger.js +0 -149
- package/src/edit-lock/admin-recovery.js +0 -151
- package/src/edit-lock/admission.js +0 -82
- package/src/edit-lock/binding-attestation.js +0 -29
- package/src/edit-lock/binding-guidance.js +0 -72
- package/src/edit-lock/binding-machine.js +0 -312
- package/src/edit-lock/call-context.js +0 -65
- package/src/edit-lock/derived-facts.js +0 -67
- package/src/edit-lock/domain.js +0 -165
- package/src/edit-lock/domains.js +0 -170
- package/src/edit-lock/host.js +0 -149
- package/src/edit-lock/incarnations.js +0 -150
- package/src/edit-lock/index.js +0 -1303
- package/src/edit-lock/inspect.js +0 -219
- package/src/edit-lock/lifecycle.js +0 -579
- package/src/edit-lock/limits.js +0 -61
- package/src/edit-lock/maintenance.js +0 -310
- package/src/edit-lock/manager.js +0 -875
- package/src/edit-lock/negotiation.js +0 -132
- package/src/edit-lock/operation-history.js +0 -214
- package/src/edit-lock/peer-client.js +0 -86
- package/src/edit-lock/peer-transport.js +0 -76
- package/src/edit-lock/peer.js +0 -87
- package/src/edit-lock/publisher.js +0 -191
- package/src/edit-lock/read-authority.js +0 -101
- package/src/edit-lock/recovery.js +0 -139
- package/src/edit-lock/remote.js +0 -362
- package/src/edit-lock/request-binding.js +0 -42
- package/src/edit-lock/request-data.js +0 -40
- package/src/edit-lock/reservation.js +0 -301
- package/src/edit-lock/reserved-runtime.js +0 -45
- package/src/edit-lock/resource-identity.js +0 -130
- package/src/edit-lock/runtime.js +0 -145
- package/src/edit-lock/settle.js +0 -144
- package/src/edit-lock/snapshot.js +0 -175
- package/src/edit-lock/stale-sweep.js +0 -120
- package/src/edit-lock/state.js +0 -462
- package/src/edit-lock/store.js +0 -327
- package/src/edit-lock/tool-scope.js +0 -29
- package/src/edit-lock/view.js +0 -170
- package/src/edit-lock/write-scopes.js +0 -48
- package/src/edit-lock/write-tool.js +0 -45
- package/src/shared/delegate-edit-lifecycle.js +0 -36
- package/src/worktree/authority.js +0 -56
|
@@ -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,23 @@ 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
|
+
}
|
|
181
222
|
}
|
|
182
223
|
return resolved.selection.skills
|
|
183
224
|
})
|
|
@@ -262,6 +303,7 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
262
303
|
locateSession: session => session,
|
|
263
304
|
inventory: (options) => inventory(options),
|
|
264
305
|
provider,
|
|
306
|
+
mcpRegistry: createMcpRegistry({ store: storeFor() }),
|
|
265
307
|
...applyEngineHooks({ ctx, lifecycle, notifier, audit }),
|
|
266
308
|
})
|
|
267
309
|
return sharedEngine
|
|
@@ -403,6 +445,11 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
403
445
|
matches.push(candidate)
|
|
404
446
|
parsedByName.set(candidate.name, matches)
|
|
405
447
|
}
|
|
448
|
+
// The unresolved draft entries keep their ORIGINAL shape: a legacy
|
|
449
|
+
// name string is not an identity and cannot be classified by
|
|
450
|
+
// workspace, while an identity object carries the scope/root the
|
|
451
|
+
// cross-workspace partition needs. Collapsing both to a name here
|
|
452
|
+
// is what made a foreign project reference unactionable.
|
|
406
453
|
const missing = []
|
|
407
454
|
const identities = []
|
|
408
455
|
const admitted = new Set()
|
|
@@ -415,7 +462,7 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
415
462
|
} else {
|
|
416
463
|
const identity = createSkillIdentity(entry)
|
|
417
464
|
candidate = byKey.get(skillIdentityKey(identity))
|
|
418
|
-
if (!candidate) missing.push(identity
|
|
465
|
+
if (!candidate) missing.push(identity)
|
|
419
466
|
}
|
|
420
467
|
if (!candidate) continue
|
|
421
468
|
const key = skillIdentityKey(candidate.identity)
|
|
@@ -423,19 +470,26 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
423
470
|
admitted.add(key)
|
|
424
471
|
identities.push(candidate.identity)
|
|
425
472
|
}
|
|
426
|
-
// preset
|
|
427
|
-
// KNOWN-BUT-INAPPLICABLE (resolvable in a profile-supplemental
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
473
|
+
// preset applicability partition (D2 + cross-workspace D4):
|
|
474
|
+
// - KNOWN-BUT-INAPPLICABLE names (resolvable in a profile-supplemental
|
|
475
|
+
// directory, e.g. creative-preset skills carried by a workspace
|
|
476
|
+
// default) are SKIPPED with a per-name reason;
|
|
477
|
+
// - a project identity PROVEN to belong to another workspace (its
|
|
478
|
+
// canonical root is outside every current project Skill root of a
|
|
479
|
+
// COMPLETE inventory) is SKIPPED as not applicable in this
|
|
480
|
+
// workspace — the preset contract, not a same-name conflict, and
|
|
481
|
+
// never a local same-name substitute;
|
|
482
|
+
// - everything else keeps the hard-failure contract with the draft
|
|
483
|
+
// preserved (a local identity the inventory no longer resolves, a
|
|
484
|
+
// project identity of THIS workspace, an incomplete inventory, or
|
|
485
|
+
// an unresolvable current-workspace root set authorizes no skip).
|
|
486
|
+
// Records are never rewritten for applicability, so a preset
|
|
487
|
+
// round-trip stays lossless.
|
|
488
|
+
const { skipped, unknown } = partitionUnresolvedSkills(missing, {
|
|
489
|
+
roots: inventoryResult?.roots,
|
|
490
|
+
complete: inventoryResult?.complete,
|
|
491
|
+
knownSupplemental: name => supplementalSkillKnown(config.supplementalSkillDirs, name),
|
|
492
|
+
})
|
|
439
493
|
if (unknown.length > 0) return { kind: 'error', text: JSON.stringify({ status: 'missing', missing: unknown, ...(skipped.length > 0 ? { skipped } : {}) }) }
|
|
440
494
|
const response = await face.applySelection(
|
|
441
495
|
{ sessionId: agent.id, cwd: agent.session?.header?.cwd, presetId: 'weir' },
|
|
@@ -448,41 +502,36 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
448
502
|
}
|
|
449
503
|
}
|
|
450
504
|
|
|
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) => {
|
|
505
|
+
const mcpVerb = (action) => async (spec = {}, { agent }) => {
|
|
457
506
|
try {
|
|
458
|
-
if (typeof spec.identity !== 'string' || typeof spec.label !== 'string' || typeof spec.command !== 'string' || spec.command.length === 0) {
|
|
507
|
+
if (action === 'mcp-add' && (typeof spec.identity !== 'string' || typeof spec.label !== 'string' || typeof spec.command !== 'string' || spec.command.length === 0)) {
|
|
459
508
|
return { kind: 'error', text: 'mcp-add needs identity, label and command strings' }
|
|
460
509
|
}
|
|
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
|
-
},
|
|
510
|
+
const { expectedRevision, confirmNamespaceChange, targetFingerprint, attemptId, batch, mode, refsOnly, surface: _ignoredSurface, ...fields } = spec
|
|
511
|
+
const operation = action === 'mcp-default' ? 'setDefault' : action === 'mcp-preview' ? 'preview' : action === 'mcp-batch' ? 'batch' : action.slice(4)
|
|
512
|
+
const request = {
|
|
513
|
+
...spec,
|
|
514
|
+
action: operation === 'batch' ? 'add' : operation,
|
|
515
|
+
refsOnly: action === 'mcp-add' ? false : true,
|
|
516
|
+
// The command channel is refs-only (legacy mcp-add aside). A
|
|
517
|
+
// caller-supplied `surface` is ignored here: only the Host UI
|
|
518
|
+
// mutation channel may accept transient literal secrets.
|
|
519
|
+
surface: action === 'mcp-add' ? 'legacy' : 'refs',
|
|
478
520
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
521
|
+
if (!spec.config && !spec.document && !['remove', 'restart', 'setDefault', 'recover'].includes(operation)) {
|
|
522
|
+
if (operation === 'batch' || fields.mcpServers) request.document = fields
|
|
523
|
+
else request.config = fields
|
|
524
|
+
}
|
|
525
|
+
const operations = face.mcpOperations(agent.session)
|
|
526
|
+
const result = operation === 'preview'
|
|
527
|
+
? await operations.preview(request)
|
|
528
|
+
: operation === 'batch'
|
|
529
|
+
? await operations.executeBatch(request)
|
|
530
|
+
: await operations.execute(request)
|
|
531
|
+
const successful = new Set(['preview', 'mounted', 'saved', 'removed', 'recovered'])
|
|
532
|
+
return { kind: successful.has(result.status) ? 'success' : 'error', text: JSON.stringify(result) }
|
|
484
533
|
} catch (cause) {
|
|
485
|
-
return { kind: 'error', text:
|
|
534
|
+
return { kind: 'error', text: JSON.stringify({ status: 'validation-failed', saved: false, error: mcpDiagnostic(cause, { code: cause?.code ?? 'validation-failed' }) }) }
|
|
486
535
|
}
|
|
487
536
|
}
|
|
488
537
|
|
|
@@ -498,7 +547,15 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
498
547
|
receipt: { payload: 'none', run: receiptVerb },
|
|
499
548
|
list: { payload: 'none', run: listVerb },
|
|
500
549
|
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:
|
|
550
|
+
'mcp-add': { payload: 'required', usage: 'Usage: /capabilities mcp-add <json {identity,label,command,args?,env?,serverName?}>', run: mcpVerb('mcp-add') },
|
|
551
|
+
'mcp-preview': { payload: 'required', usage: 'Usage: /capabilities mcp-preview <json server|{mcpServers:{...}}>', run: mcpVerb('mcp-preview') },
|
|
552
|
+
'mcp-batch': { payload: 'required', usage: 'Usage: /capabilities mcp-batch <json {mcpServers:{...},mode?:"add"|"edit"}>', run: mcpVerb('mcp-batch') },
|
|
553
|
+
'mcp-edit': { payload: 'required', usage: 'Usage: /capabilities mcp-edit <json {identity,expectedRevision,...config}>', run: mcpVerb('mcp-edit') },
|
|
554
|
+
'mcp-remove': { payload: 'required', usage: 'Usage: /capabilities mcp-remove <json {identity,expectedRevision}>', run: mcpVerb('mcp-remove') },
|
|
555
|
+
'mcp-restart': { payload: 'required', usage: 'Usage: /capabilities mcp-restart <json {identity,expectedRevision}>', run: mcpVerb('mcp-restart') },
|
|
556
|
+
'mcp-test': { payload: 'required', usage: 'Usage: /capabilities mcp-test <json config>', run: mcpVerb('mcp-test') },
|
|
557
|
+
'mcp-default': { payload: 'required', usage: 'Usage: /capabilities mcp-default <json {identity,enabled,expectedRevision?}>', run: mcpVerb('mcp-default') },
|
|
558
|
+
'mcp-recover': { payload: 'optional', usage: 'Usage: /capabilities mcp-recover [json {identity?}]', run: mcpVerb('mcp-recover') },
|
|
502
559
|
presets: { payload: 'none', run: presetVerbs.presets },
|
|
503
560
|
'preset-save': { payload: 'required', usage: 'Usage: /capabilities preset-save <json>', run: presetVerbs['preset-save'] },
|
|
504
561
|
'preset-rename': { payload: 'required', usage: 'Usage: /capabilities preset-rename <json>', run: presetVerbs['preset-rename'] },
|
|
@@ -515,7 +572,7 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
515
572
|
|
|
516
573
|
ctx.effect(() => commands.register({
|
|
517
574
|
name: 'capabilities',
|
|
518
|
-
description: 'Session capability surface
|
|
575
|
+
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
576
|
handler: async (invocation) => {
|
|
520
577
|
const agent = invocation?.agent
|
|
521
578
|
if (!agent) return { kind: 'error', text: 'capabilities: requires an owning agent session' }
|
|
@@ -528,8 +585,11 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
528
585
|
const head = verb.split(' ', 1)[0]
|
|
529
586
|
const entry = verbTable[head]
|
|
530
587
|
const hasArgs = verb.length > head.length
|
|
531
|
-
if (!entry || (entry.payload === 'none' && hasArgs)
|
|
532
|
-
return { kind: 'error', text: 'Usage: /capabilities receipt|list|
|
|
588
|
+
if (!entry || (entry.payload === 'none' && hasArgs)) {
|
|
589
|
+
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' }
|
|
590
|
+
}
|
|
591
|
+
if (entry.payload === 'required' && !hasArgs) {
|
|
592
|
+
return { kind: 'error', text: entry.usage ?? 'Usage: /capabilities <verb> <json>' }
|
|
533
593
|
}
|
|
534
594
|
let spec
|
|
535
595
|
if (entry.payload !== 'none' && hasArgs) {
|
|
@@ -572,6 +632,14 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
572
632
|
clearFailure: options => provider?.clearFailure(options),
|
|
573
633
|
/** Shared Apply entry (group 12 manager UI commits here). */
|
|
574
634
|
applySelection: (session, request, options) => engineFor().apply(session, request, options),
|
|
635
|
+
mcpOperations: (session, options = {}) => createMcpOperations({
|
|
636
|
+
registry: createMcpRegistry({ store: storeFor() }),
|
|
637
|
+
manager: ctx.get?.('weirMcpManager'),
|
|
638
|
+
credentials: ctx.get?.('credentials'),
|
|
639
|
+
audit,
|
|
640
|
+
session,
|
|
641
|
+
assertOwner: options.assertOwner,
|
|
642
|
+
}),
|
|
575
643
|
/** 12.4 diagnostics: whether a removal notice is queued for a session. */
|
|
576
644
|
hasPendingNotification: sessionId => notifier.has(sessionId),
|
|
577
645
|
}
|
|
@@ -587,11 +655,48 @@ export function createSkillSelectionPlugin(dependencies = {}) {
|
|
|
587
655
|
inventory: (options, previous) => inventory(options, previous),
|
|
588
656
|
lifecycle,
|
|
589
657
|
mcpManager: () => ctx.get?.('weirMcpManager'),
|
|
658
|
+
mcpOperations: face.mcpOperations,
|
|
590
659
|
profileContext: () => ctx.get?.('profileContext'),
|
|
591
660
|
}), 'weir-capability-read-bridge')
|
|
592
661
|
} catch (cause) {
|
|
593
662
|
ctx.logger?.warn?.(`capability read bridge feed failed: ${cause instanceof Error ? cause.message : String(cause)}`)
|
|
594
663
|
}
|
|
664
|
+
// Primary-Orchestrator AI installation rides the same selection row. All
|
|
665
|
+
// Host services are resolved lazily; missing confirmation/ownership support
|
|
666
|
+
// disables install without affecting inert preview or manual management.
|
|
667
|
+
try {
|
|
668
|
+
const tools = ctx.get?.('tools')
|
|
669
|
+
if (tools?.register) {
|
|
670
|
+
const sessionQuery = () => ctx.get?.('sessionQuery')
|
|
671
|
+
// Two retained revisions of this preset may feed the bridge at once;
|
|
672
|
+
// the owner is resolved per session through the agent's own revision.
|
|
673
|
+
const runtime = { agents: () => ctx.get?.('agents'), agentPresets: () => ctx.get?.('agentPresets') }
|
|
674
|
+
const mutations = createCapabilityMutationService({ sessionQuery, ...runtime })
|
|
675
|
+
const captureOwnership = async sessionId => {
|
|
676
|
+
const lease = await sessionQuery()?.observeSession?.(sessionId)
|
|
677
|
+
if (!lease || typeof lease[Symbol.dispose] !== 'function') throw Object.assign(new Error('MCP install session observation is unavailable'), { code: 'owner-unavailable' })
|
|
678
|
+
try {
|
|
679
|
+
const owner = capabilityOwnership(lease, sessionId)
|
|
680
|
+
const feed = capabilityReadFeed(owner.presetId, sessionId, runtime)
|
|
681
|
+
const faces = feed.faces ?? feed
|
|
682
|
+
return { owner, feed, profile: faces.profileContext?.() }
|
|
683
|
+
} finally { lease[Symbol.dispose]() }
|
|
684
|
+
}
|
|
685
|
+
const tool = createMcpInstallTool({
|
|
686
|
+
operationsFor: session => face.mcpOperations(session),
|
|
687
|
+
mutate: (sessionId, request) => mutations.mutate(sessionId, request),
|
|
688
|
+
captureOwnership,
|
|
689
|
+
sameOwnership: sameMutationOwner,
|
|
690
|
+
describeExecution: entry => ctx.get?.('weirMcpManager')?.describeExecution?.(entry),
|
|
691
|
+
userQuestions: () => ctx.get?.('userQuestions'),
|
|
692
|
+
isLiveRoot: agent => Boolean(agent && ctx.get?.('agents')?.get?.(agent.id) === agent),
|
|
693
|
+
})
|
|
694
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => tools.register(tool), 'weir-mcp-install-tool')
|
|
695
|
+
else tools.register(tool)
|
|
696
|
+
}
|
|
697
|
+
} catch (cause) {
|
|
698
|
+
ctx.logger?.warn?.(`MCP install tool registration failed: ${cause instanceof Error ? cause.message : String(cause)}`)
|
|
699
|
+
}
|
|
595
700
|
}
|
|
596
701
|
}
|
|
597
702
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Augmentations of the narrow checked-graph platform contract
|
|
2
|
-
// (types/platform.d.ts) for the capability-store liveness/lock modules:
|
|
3
|
-
//
|
|
2
|
+
// (types/platform.d.ts) for the capability-store liveness/lock modules: they
|
|
3
|
+
// pull these declarations into the checked graph.
|
|
4
4
|
// Same discipline as the base contract — runtime modules remain supplied by
|
|
5
5
|
// Node; these declarations only describe what the checked graph uses.
|
|
6
6
|
declare module 'node:fs' {
|
|
@@ -21,9 +21,8 @@ import { link, mkdir, open, readFile, readdir, rename, unlink } from 'node:fs/pr
|
|
|
21
21
|
// Directory fsync is a POSIX crash-durability idiom: on Windows the directory
|
|
22
22
|
// handle opens but sync fails EPERM (verified on Node v24, NTFS). The win32
|
|
23
23
|
// store row (SUPPORTED_PLATFORMS in store.js) therefore commits with
|
|
24
|
-
// file-level fsync + atomic rename only
|
|
25
|
-
//
|
|
26
|
-
// durability tradeoff is documented in docs/features/session-capability-manager.md.
|
|
24
|
+
// file-level fsync + atomic rename only. The durability tradeoff is documented
|
|
25
|
+
// in docs/features/session-capability-manager.md.
|
|
27
26
|
const DIR_SYNC_SUPPORTED = process.platform === 'darwin' || process.platform === 'linux'
|
|
28
27
|
|
|
29
28
|
// Windows file-locking etiquette: antivirus and the search indexer briefly
|
|
@@ -118,7 +118,7 @@ function apply(ctx, config = {}) {
|
|
|
118
118
|
const request = state.request
|
|
119
119
|
if (!live(agent) || request?.kind !== 'applied' || request.delivered) return
|
|
120
120
|
fresh(agent)
|
|
121
|
-
if (request.kind !== 'applied' || request.controller.signal.aborted
|
|
121
|
+
if (request.kind !== 'applied' || request.controller.signal.aborted) {
|
|
122
122
|
state.blocked = true
|
|
123
123
|
request.delivered = true
|
|
124
124
|
record(agent, request, 'resume-blocked')
|
|
@@ -197,7 +197,7 @@ function apply(ctx, config = {}) {
|
|
|
197
197
|
if (decision.kind !== 'enter') return decision
|
|
198
198
|
let resume
|
|
199
199
|
const current = state.request
|
|
200
|
-
if (current?.kind === 'applied' && !current.delivered && !signal?.aborted
|
|
200
|
+
if (current?.kind === 'applied' && !current.delivered && !signal?.aborted) {
|
|
201
201
|
current.delivered = true
|
|
202
202
|
current.detach?.(); current.detachMaintenance?.()
|
|
203
203
|
resume = RESUME_AFTER_COMPACTION
|
|
@@ -10,7 +10,6 @@ import { auditFilePathOf, isTerminalStatus, readAuditTail, readChildFinalText }
|
|
|
10
10
|
import { AUDIT_TYPES } from '../shared/audit.js'
|
|
11
11
|
import { userTextMessage } from '../shared/user-message.js'
|
|
12
12
|
import { contentText } from '../shared/content-text.js'
|
|
13
|
-
import { withSupervisedEditResume, settleDelegateEditLifecycle } from '../shared/delegate-edit-lifecycle.js'
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* Mount the supervision subsystem on the plugin ctx.
|
|
@@ -81,8 +80,8 @@ export function mountSupervision({ ctx, audit, settings, supervisionNow, onChild
|
|
|
81
80
|
sendTo: async (childId, text) => {
|
|
82
81
|
await ctx.subagents.sendMessage(parent, childId, [{ type: 'text', text }], { signal: new AbortController().signal })
|
|
83
82
|
},
|
|
84
|
-
resumeTo: (childId, text) =>
|
|
85
|
-
ctx.subagents.sendMessage(parent, childId, [{ type: 'text', text }], { signal: new AbortController().signal })
|
|
83
|
+
resumeTo: (childId, text) =>
|
|
84
|
+
ctx.subagents.sendMessage(parent, childId, [{ type: 'text', text }], { signal: new AbortController().signal }),
|
|
86
85
|
interruptChild: (childId) => {
|
|
87
86
|
ctx.subagents.interrupt(childId, { kind: 'ancestor', agent: parent })
|
|
88
87
|
},
|
|
@@ -107,8 +106,9 @@ export function mountSupervision({ ctx, audit, settings, supervisionNow, onChild
|
|
|
107
106
|
const runtime = runtimeFor(fact.childId)
|
|
108
107
|
// No observed quiescence means no automatic handoff, including
|
|
109
108
|
// a cold bookkeeping fact. Lane reconciliation owns that case.
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
// Runtime quiescence is the ONLY gate: the retired Edit Lock
|
|
110
|
+
// disposition acknowledgement used to join here as well.
|
|
111
|
+
void runtime.promise
|
|
112
112
|
.then(() => onChildSettled?.(fact.childId, parent))
|
|
113
113
|
.catch(error => {
|
|
114
114
|
const note = `child ${fact.childId}: terminal handoff failed; lane remains bound: ${error?.message ?? error}`
|
package/src/delegate/tool.js
CHANGED
|
@@ -5,7 +5,6 @@ import { parseEscalation } from './escalate.js'
|
|
|
5
5
|
import { continuableLane, oneShotLane, spawnGuardedChild, supervisedLane } from './spawn-adapter.js'
|
|
6
6
|
import { WORKTREE_CODES } from '../worktree/errors.js'
|
|
7
7
|
import { contentText } from '../shared/content-text.js'
|
|
8
|
-
import { settleDelegateEditLifecycle } from '../shared/delegate-edit-lifecycle.js'
|
|
9
8
|
|
|
10
9
|
export const DELEGATE_TOOL_NAME = 'delegate'
|
|
11
10
|
export const BATCH_LIMIT = 16
|
|
@@ -319,16 +318,9 @@ async function spawnBackground(item, args, deps, exec) {
|
|
|
319
318
|
return { job_id: jobId, label }
|
|
320
319
|
}
|
|
321
320
|
|
|
322
|
-
async function settledResult(started, parent) {
|
|
323
|
-
try { return await started.result } finally {
|
|
324
|
-
// subagent/end is emit, not an awaited barrier. Explicitly join the
|
|
325
|
-
// incarnation's standard edit disposition before result/escalation/handoff.
|
|
326
|
-
await settleDelegateEditLifecycle(parent, started.id, started.agent)
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
321
|
/** Await one child, honoring the one-level ESCALATE contract. */
|
|
330
322
|
async function withEscalation(started, item, deps, exec, binding = null) {
|
|
331
|
-
const first = await
|
|
323
|
+
const first = await started.result
|
|
332
324
|
const text = contentText(first.output)
|
|
333
325
|
const escalation = parseEscalation(text)
|
|
334
326
|
if (!escalation) {
|
|
@@ -345,7 +337,7 @@ async function withEscalation(started, item, deps, exec, binding = null) {
|
|
|
345
337
|
// The escalated respawn works in the same lane under the same binding (the
|
|
346
338
|
// lane's bound child stays the first child; it settles once, after this).
|
|
347
339
|
const respawned = await spawnGuardedChild({ target, prompt, parent: exec.agent, signal: exec.signal, label: `${laneLabel(target.label, binding)} (escalated)`, laneGuard: laneGuardOf(binding, deps, exec) }, oneShotLane(), deps)
|
|
348
|
-
const second = await
|
|
340
|
+
const second = await respawned.result
|
|
349
341
|
return {
|
|
350
342
|
id: respawned.id,
|
|
351
343
|
status: second.stopReason,
|