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.
Files changed (46) hide show
  1. package/lib/client.capability-badge.js +2 -0
  2. package/lib/client.capability-manager.js +46 -179
  3. package/lib/client.capability-model.js +428 -3
  4. package/lib/client.file-lease-tool-model.js +189 -0
  5. package/lib/client.file-lease-tool-view.js +434 -0
  6. package/lib/client.js +105 -7
  7. package/lib/client.mcp-manager.js +563 -0
  8. package/package.json +1 -1
  9. package/skills/mcp-server-setup/SKILL.md +124 -0
  10. package/src/capabilities/apply-engine.js +18 -4
  11. package/src/capabilities/capability-remote.js +296 -45
  12. package/src/capabilities/initial-selection.js +3 -2
  13. package/src/capabilities/lifecycle-preload.js +69 -11
  14. package/src/capabilities/lifecycle-snapshot.js +127 -39
  15. package/src/capabilities/mcp-admission.js +2 -2
  16. package/src/capabilities/mcp-config.js +344 -0
  17. package/src/capabilities/mcp-credentials.js +180 -0
  18. package/src/capabilities/mcp-diagnostics.js +169 -0
  19. package/src/capabilities/mcp-drain.js +18 -4
  20. package/src/capabilities/mcp-facade.js +3 -3
  21. package/src/capabilities/mcp-gate.js +51 -52
  22. package/src/capabilities/mcp-install-tool.js +276 -0
  23. package/src/capabilities/mcp-manager.js +368 -142
  24. package/src/capabilities/mcp-mount.js +219 -79
  25. package/src/capabilities/mcp-operations.js +359 -0
  26. package/src/capabilities/mcp-process.js +43 -0
  27. package/src/capabilities/mcp-registry.js +74 -60
  28. package/src/capabilities/mcp-runtime.js +103 -0
  29. package/src/capabilities/read-payloads.js +8 -3
  30. package/src/capabilities/skill-applicability.js +185 -0
  31. package/src/capabilities/skill-builtin-migration.js +2 -1
  32. package/src/capabilities/skill-selection-plugin.js +161 -56
  33. package/src/file-lease/client.js +268 -35
  34. package/src/file-lease/index.js +4 -0
  35. package/src/file-lease/plugin.js +239 -15
  36. package/src/file-lease/tools.js +426 -38
  37. package/src/file-lease/watch.js +473 -0
  38. package/src/intent-gate/index.js +29 -12
  39. package/src/intent-gate/matcher.js +36 -2
  40. package/src/lease-broker/protocol.js +3 -2
  41. package/src/lease-broker/server.js +6 -3
  42. package/src/lease-broker/state.js +68 -11
  43. package/src/process/index.js +4 -2
  44. package/src/shared/audit.js +3 -0
  45. package/src/shared/child-scope.js +2 -0
  46. 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, { event: 'settled', requestId: record.requestId, path: record.path, state })
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) leases.delete(path)
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
- leases.delete(path)
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)) continue
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)) return { ok: false, code: 'free' }
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
- leases.delete(path)
411
- droppedLeases += 1
468
+ if (removeLease(path, 'expired')) droppedLeases += 1
412
469
  }
413
470
  }
414
471
  for (const [requestId, record] of [...pending]) {
@@ -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
- `lsp: 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. ` +
480
- `Install the server as a plain .exe, or point lspServers at its node entry 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]
@@ -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
  /**
@@ -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 (AGENTS.md §2, S21).
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
  */