weir-harness 2.0.0 → 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/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 +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/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
|
@@ -20,7 +20,14 @@
|
|
|
20
20
|
// connection is a different owner for arbitration purposes (spec: "Same
|
|
21
21
|
// owner identifier from another connection").
|
|
22
22
|
// - Validity is `max(expiresAt, holdUntil ?? 0)`. Once `now() >=` that instant
|
|
23
|
-
// the path is free for anyone, without negotiation and without notification
|
|
23
|
+
// the path is free for anyone, without negotiation and without notification
|
|
24
|
+
// unless a session explicitly subscribed on its own side.
|
|
25
|
+
// - Removal has exactly one outlet (`removeLease`): the row is deleted first and
|
|
26
|
+
// then the fact is announced as `released` to every connected client. Explicit
|
|
27
|
+
// release, holder disconnect, and expiry observed by
|
|
28
|
+
// acquire/status/request/reply/hold/sweep all pass through it, so a lapsed row
|
|
29
|
+
// can never be silently overwritten by a new grant. An atomic transfer never
|
|
30
|
+
// comes through it because the path never becomes unowned.
|
|
24
31
|
// - `release` judges identity only (a lapsed row still belongs to its owner, so
|
|
25
32
|
// releasing it is `released`, keeping the call idempotent), while `hold`
|
|
26
33
|
// additionally requires the row to still be valid — you cannot extend
|
|
@@ -33,7 +40,11 @@
|
|
|
33
40
|
* @typedef {{ path: string, owner: Owner, scope: string, acquiredAt: number, expiresAt: number, holdUntil: number | null }} LeaseView
|
|
34
41
|
* @typedef {{ requestId: string, path: string, from: Owner, connectionId: string, holderConnectionId: string, holderId: string, expiresAt: number }} PendingRequest
|
|
35
42
|
* @typedef {{ event: 'request', requestId: string, path: string, from: Owner, expiresAt: number }} RequestEvent
|
|
36
|
-
* @typedef {{ event: 'settled', requestId: string, path: string, state: 'transferred' | 'declined' | 'expired' }} SettledEvent
|
|
43
|
+
* @typedef {{ event: 'settled', requestId: string, path: string, session: string, state: 'transferred' | 'declined' | 'expired' }} SettledEvent
|
|
44
|
+
* `session` is the requester's own session (copied from the request's `from`),
|
|
45
|
+
* carried so a client can route the outcome to that session's agent without
|
|
46
|
+
* depending on a turn-scoped table of request ids.
|
|
47
|
+
* @typedef {{ event: 'released', path: string, reason: 'released' | 'disconnected' | 'expired' }} ReleasedEvent
|
|
37
48
|
* @typedef {{ connectionId: string, owner: Owner, scope: string, paths: string[], ttlMs?: number }} AcquireInput
|
|
38
49
|
* @typedef {{ connectionId: string, owner: Owner, paths: string[] }} ReleaseInput
|
|
39
50
|
* @typedef {{ connectionId: string, owner: Owner, paths: string[], minutes: number }} HoldInput
|
|
@@ -71,7 +82,7 @@ function view(lease) {
|
|
|
71
82
|
* @param {number} [options.ttlMs] default lease time-to-live
|
|
72
83
|
* @param {number} [options.requestWindowMs] bounded negotiation window
|
|
73
84
|
* @param {number} [options.holdMaxMinutes] retention bound in minutes
|
|
74
|
-
* @param {(connectionId: string, event: RequestEvent | SettledEvent) => void} [options.onEvent] event sink; a missing connection or a throwing sink never affects arbitration
|
|
85
|
+
* @param {(connectionId: string, event: RequestEvent | SettledEvent | ReleasedEvent) => void} [options.onEvent] event sink; a missing connection or a throwing sink never affects arbitration
|
|
75
86
|
*/
|
|
76
87
|
export function createBroker({
|
|
77
88
|
now = Date.now,
|
|
@@ -118,7 +129,7 @@ export function createBroker({
|
|
|
118
129
|
* Deliver one event. Delivery is best effort by contract: an unknown
|
|
119
130
|
* connection is a silent drop and a throwing sink cannot break arbitration.
|
|
120
131
|
* @param {string} connectionId
|
|
121
|
-
* @param {RequestEvent | SettledEvent} event
|
|
132
|
+
* @param {RequestEvent | SettledEvent | ReleasedEvent} event
|
|
122
133
|
*/
|
|
123
134
|
function deliver(connectionId, event) {
|
|
124
135
|
if (!connections.has(connectionId)) return
|
|
@@ -129,13 +140,41 @@ export function createBroker({
|
|
|
129
140
|
}
|
|
130
141
|
}
|
|
131
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The ONE removal outlet for a lease row.
|
|
145
|
+
*
|
|
146
|
+
* The row is deleted before anything is announced, so a later observation of
|
|
147
|
+
* the same lapsed record finds nothing to announce (no duplicate), and every
|
|
148
|
+
* receiver sees the path as unowned at the moment it hears about it. Delivery
|
|
149
|
+
* is a best-effort fact to every currently connected client: a missing or
|
|
150
|
+
* disconnected recipient is skipped, one throwing sink cannot affect the
|
|
151
|
+
* others, and the verdict that caused the removal is already decided.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} path
|
|
154
|
+
* @param {'released' | 'disconnected' | 'expired'} reason
|
|
155
|
+
* @returns {boolean} whether a row was removed
|
|
156
|
+
*/
|
|
157
|
+
function removeLease(path, reason) {
|
|
158
|
+
if (!leases.delete(path)) return false
|
|
159
|
+
for (const connectionId of [...connections]) {
|
|
160
|
+
deliver(connectionId, { event: 'released', path, reason })
|
|
161
|
+
}
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
|
|
132
165
|
/**
|
|
133
166
|
* Announce the end of a request to the requester.
|
|
134
167
|
* @param {PendingRequest} record
|
|
135
168
|
* @param {'transferred' | 'declined' | 'expired'} state
|
|
136
169
|
*/
|
|
137
170
|
function settle(record, state) {
|
|
138
|
-
deliver(record.connectionId, {
|
|
171
|
+
deliver(record.connectionId, {
|
|
172
|
+
event: 'settled',
|
|
173
|
+
requestId: record.requestId,
|
|
174
|
+
path: record.path,
|
|
175
|
+
session: record.from.session,
|
|
176
|
+
state,
|
|
177
|
+
})
|
|
139
178
|
}
|
|
140
179
|
|
|
141
180
|
/** @param {string[]} paths */
|
|
@@ -159,7 +198,7 @@ export function createBroker({
|
|
|
159
198
|
function disconnect(connectionId) {
|
|
160
199
|
connections.delete(connectionId)
|
|
161
200
|
for (const [path, lease] of [...leases]) {
|
|
162
|
-
if (lease.connectionId === connectionId)
|
|
201
|
+
if (lease.connectionId === connectionId) removeLease(path, 'disconnected')
|
|
163
202
|
}
|
|
164
203
|
for (const [requestId, record] of [...pending]) {
|
|
165
204
|
if (record.connectionId === connectionId) {
|
|
@@ -192,6 +231,10 @@ export function createBroker({
|
|
|
192
231
|
for (const path of normalize(paths)) {
|
|
193
232
|
const lease = leases.get(path)
|
|
194
233
|
if (!lease || !isValid(lease)) {
|
|
234
|
+
// A lapsed row nobody observed yet is a free path whose record is still
|
|
235
|
+
// in the table: reclaim it through the ONE removal outlet so the release
|
|
236
|
+
// is announced instead of being silently overwritten by the grant below.
|
|
237
|
+
if (lease !== undefined) removeLease(path, 'expired')
|
|
195
238
|
fresh.push(path)
|
|
196
239
|
continue
|
|
197
240
|
}
|
|
@@ -247,7 +290,9 @@ export function createBroker({
|
|
|
247
290
|
for (const path of normalize(paths)) {
|
|
248
291
|
const lease = leases.get(path)
|
|
249
292
|
if (lease && lease.connectionId === connectionId && lease.owner.id === owner.id) {
|
|
250
|
-
|
|
293
|
+
// Even a lapsed row belongs to its owner until removed, so the explicit
|
|
294
|
+
// release is what takes it out of the table and what gets announced.
|
|
295
|
+
removeLease(path, 'released')
|
|
251
296
|
released.push(path)
|
|
252
297
|
} else {
|
|
253
298
|
notOwned.push(path)
|
|
@@ -273,6 +318,7 @@ export function createBroker({
|
|
|
273
318
|
const missing = []
|
|
274
319
|
for (const path of targets) {
|
|
275
320
|
const lease = leases.get(path)
|
|
321
|
+
if (lease !== undefined && !isValid(lease)) removeLease(path, 'expired')
|
|
276
322
|
if (!lease || lease.connectionId !== connectionId || lease.owner.id !== owner.id || !isValid(lease)) missing.push(path)
|
|
277
323
|
}
|
|
278
324
|
if (missing.length > 0) return { ok: false, code: 'not-owned', paths: missing }
|
|
@@ -298,13 +344,21 @@ export function createBroker({
|
|
|
298
344
|
function status(filter = {}) {
|
|
299
345
|
/** @type {LeaseView[]} */
|
|
300
346
|
const out = []
|
|
347
|
+
/** @type {string[]} */
|
|
348
|
+
const lapsed = []
|
|
301
349
|
for (const lease of leases.values()) {
|
|
302
|
-
if (!isValid(lease))
|
|
350
|
+
if (!isValid(lease)) {
|
|
351
|
+
// Read-only means "takes no valid lease": a record the clock already
|
|
352
|
+
// made free is reclaimed and announced here, exactly like a sweep.
|
|
353
|
+
lapsed.push(lease.path)
|
|
354
|
+
continue
|
|
355
|
+
}
|
|
303
356
|
if (filter.scope !== undefined && lease.scope !== filter.scope) continue
|
|
304
357
|
if (filter.session !== undefined && lease.owner.session !== filter.session) continue
|
|
305
358
|
if (filter.path !== undefined && lease.path !== filter.path) continue
|
|
306
359
|
out.push(view(lease))
|
|
307
360
|
}
|
|
361
|
+
for (const path of lapsed) removeLease(path, 'expired')
|
|
308
362
|
out.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0))
|
|
309
363
|
return { ok: true, leases: out }
|
|
310
364
|
}
|
|
@@ -321,7 +375,10 @@ export function createBroker({
|
|
|
321
375
|
function request({ connectionId, from, path }) {
|
|
322
376
|
const at = now()
|
|
323
377
|
const lease = leases.get(path)
|
|
324
|
-
if (!lease || !isValid(lease))
|
|
378
|
+
if (!lease || !isValid(lease)) {
|
|
379
|
+
if (lease !== undefined) removeLease(path, 'expired')
|
|
380
|
+
return { ok: false, code: 'free' }
|
|
381
|
+
}
|
|
325
382
|
if (lease.owner.id === from.id) return { ok: false, code: 'self' }
|
|
326
383
|
const key = requestKey(path, from.id)
|
|
327
384
|
const knownId = pendingKeys.get(key)
|
|
@@ -370,6 +427,7 @@ export function createBroker({
|
|
|
370
427
|
}
|
|
371
428
|
const lease = leases.get(record.path)
|
|
372
429
|
if (!lease || !isValid(lease)) {
|
|
430
|
+
if (lease !== undefined) removeLease(record.path, 'expired')
|
|
373
431
|
dropRequest(requestId)
|
|
374
432
|
settle(record, 'expired')
|
|
375
433
|
return { ok: false, code: 'stale' }
|
|
@@ -407,8 +465,7 @@ export function createBroker({
|
|
|
407
465
|
let droppedRequests = 0
|
|
408
466
|
for (const [path, lease] of [...leases]) {
|
|
409
467
|
if (at >= validUntil(lease)) {
|
|
410
|
-
|
|
411
|
-
droppedLeases += 1
|
|
468
|
+
if (removeLease(path, 'expired')) droppedLeases += 1
|
|
412
469
|
}
|
|
413
470
|
}
|
|
414
471
|
for (const [requestId, record] of [...pending]) {
|
package/src/process/index.js
CHANGED
|
@@ -475,9 +475,11 @@ export function spawnArgv(command, args = [], platform = process.platform, optio
|
|
|
475
475
|
// Unknown shim, but nothing here can be re-parsed: let cmd run it.
|
|
476
476
|
return ['cmd.exe', '/d', '/c', command, ...args]
|
|
477
477
|
}
|
|
478
|
+
const subject = options.subject ?? 'lsp'
|
|
479
|
+
const setting = options.setting ?? 'lspServers'
|
|
478
480
|
throw new Error(
|
|
479
|
-
|
|
480
|
-
`Install the server as a plain .exe, or point
|
|
481
|
+
`${subject}: cannot start '${command}' on Windows — its .cmd shim could not be unwrapped and the path or an argument contains characters cmd.exe would re-parse. ` +
|
|
482
|
+
`Install the server as a plain .exe, or point ${setting} at its node entry point.`,
|
|
481
483
|
)
|
|
482
484
|
}
|
|
483
485
|
return [command, ...args]
|
package/src/shared/audit.js
CHANGED
|
@@ -34,6 +34,9 @@ export const AUDIT_TYPES = Object.freeze({
|
|
|
34
34
|
// identity, feed census and provider status, never skill names; the receipt
|
|
35
35
|
// itself stays the only effective-state authority.
|
|
36
36
|
capabilityRead: 'capability-read',
|
|
37
|
+
// MCP management mutations: operation/status/identity/generation/incarnation
|
|
38
|
+
// only. Never config, credential refs or values, argv, URLs, or raw errors.
|
|
39
|
+
capabilityMcp: 'capability-mcp',
|
|
37
40
|
// The pending model-facing capability notice (durable-pending-notice D7):
|
|
38
41
|
// queue / degrade / deliver / clear outcomes of the durable notice unit.
|
|
39
42
|
// Counts only, never names — the same rule as capability-read above; the
|
|
@@ -44,6 +44,7 @@ export const CHILD_DENY_TOOLS = [
|
|
|
44
44
|
'worktree_check',
|
|
45
45
|
'exit_plan_mode',
|
|
46
46
|
'ask_user_question',
|
|
47
|
+
'mcp_install',
|
|
47
48
|
'present',
|
|
48
49
|
'edit_lock_acquire',
|
|
49
50
|
'edit_lock_hold',
|
|
@@ -51,6 +52,7 @@ export const CHILD_DENY_TOOLS = [
|
|
|
51
52
|
'edit_lock_release',
|
|
52
53
|
'edit_lock_status',
|
|
53
54
|
'edit_lock_try_steal',
|
|
55
|
+
'edit_lock_watch',
|
|
54
56
|
]
|
|
55
57
|
|
|
56
58
|
/**
|
package/src/worktree/rules.js
CHANGED
|
@@ -91,7 +91,7 @@ import { bareSetupCommand } from './pkgmgr.js'
|
|
|
91
91
|
* Lockfile-derived package MANAGER, or null when the repository has none.
|
|
92
92
|
* Only the manager is decided here: how it is invoked (system tool, DSH
|
|
93
93
|
* bundled runtime, or nothing available) is resolved separately so a bare
|
|
94
|
-
* `pnpm` is never assumed to exist on PATH (
|
|
94
|
+
* `pnpm` is never assumed to exist on PATH (docs/contributing/development.md §1, S21).
|
|
95
95
|
* @param {Iterable<string>} fileNames - names present at the lane root
|
|
96
96
|
* @returns {'pnpm' | 'bun' | 'yarn' | 'npm' | null}
|
|
97
97
|
*/
|