weir-harness 1.2.0 → 1.2.2

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 (99) hide show
  1. package/cordis.patch.yml +0 -2
  2. package/lib/client.blackboard-model.js +172 -56
  3. package/lib/client.capability-manager.js +0 -1
  4. package/lib/client.capability-model.js +378 -31
  5. package/lib/client.chain-model.js +80 -0
  6. package/lib/client.disabled-categories-editor.js +22 -0
  7. package/lib/client.edit-lock-maintenance.js +239 -246
  8. package/lib/client.edit-lock-panel.js +110 -0
  9. package/lib/client.hash-edit-model.js +48 -0
  10. package/lib/client.js +267 -1549
  11. package/lib/client.lsp-model.js +92 -0
  12. package/lib/client.lsp-panel.js +11 -16
  13. package/lib/client.lsp-toggle.js +18 -0
  14. package/lib/client.notify-permissions.js +112 -20
  15. package/lib/client.notify-web.js +14 -11
  16. package/lib/client.robash-model.js +26 -0
  17. package/lib/client.settings-page.js +287 -17
  18. package/lib/client.worktree-model.js +225 -0
  19. package/package.json +5 -2
  20. package/src/blackboard/durable.js +12 -0
  21. package/src/blackboard/index.js +2 -2
  22. package/src/blackboard/kernel.js +23 -6
  23. package/src/blackboard/notify.js +10 -3
  24. package/src/blackboard/remote.js +31 -13
  25. package/src/blackboard/storage.js +13 -5
  26. package/src/capabilities/apply-engine.js +189 -101
  27. package/src/capabilities/defaults-transaction.js +136 -1
  28. package/src/capabilities/lifecycle-snapshot.js +83 -204
  29. package/src/capabilities/mcp-admission.js +1 -23
  30. package/src/capabilities/mcp-drain.js +30 -19
  31. package/src/capabilities/mcp-facade.js +23 -3
  32. package/src/capabilities/preset-library.js +247 -0
  33. package/src/capabilities/preset-package.js +212 -1
  34. package/src/capabilities/same-name-policy.js +3 -41
  35. package/src/capabilities/selection-notify.js +324 -21
  36. package/src/capabilities/skill-office-adapter.js +7 -1
  37. package/src/capabilities/skill-selection-plugin.js +315 -729
  38. package/src/capabilities/store/checked-contract.d.ts +5 -0
  39. package/src/capabilities/store/fs-adapter.js +52 -0
  40. package/src/capabilities/store/lock.js +86 -0
  41. package/src/capabilities/store/paths.js +5 -0
  42. package/src/capabilities/store/store.js +105 -5
  43. package/src/creative-guide/guide.js +1 -1
  44. package/src/delegate/child-composition.js +258 -0
  45. package/src/delegate/group-coordinator.js +25 -27
  46. package/src/delegate/index.js +21 -2
  47. package/src/delegate/rehydrate.js +24 -4
  48. package/src/delegate/robash-guard-core.js +2 -2
  49. package/src/delegate/spawn-adapter.js +214 -100
  50. package/src/delegate/status-tool.js +9 -6
  51. package/src/delegate/supervision-mount.js +35 -6
  52. package/src/delegate/tool.js +29 -4
  53. package/src/{shared → delegate}/whitelist-defaults.js +2 -2
  54. package/src/edit-lock/admin-ledger.js +8 -11
  55. package/src/edit-lock/admin-recovery.js +2 -1
  56. package/src/edit-lock/admission.js +12 -2
  57. package/src/edit-lock/binding-guidance.js +28 -10
  58. package/src/edit-lock/binding-machine.js +312 -0
  59. package/src/edit-lock/derived-facts.js +67 -0
  60. package/src/edit-lock/index.js +44 -244
  61. package/src/edit-lock/inspect.js +113 -1
  62. package/src/edit-lock/lifecycle.js +8 -5
  63. package/src/edit-lock/limits.js +61 -0
  64. package/src/edit-lock/maintenance.js +5 -2
  65. package/src/edit-lock/manager.js +6 -2
  66. package/src/edit-lock/operation-history.js +1 -1
  67. package/src/edit-lock/peer-client.js +9 -2
  68. package/src/edit-lock/peer-transport.js +7 -1
  69. package/src/edit-lock/remote.js +11 -4
  70. package/src/edit-lock/state.js +2 -4
  71. package/src/edit-lock/view.js +10 -11
  72. package/src/lsp/admin.js +2 -24
  73. package/src/lsp/index.js +1 -1
  74. package/src/lsp/manager.js +1 -1
  75. package/src/notify/permissions-admin.js +1 -1
  76. package/src/{lsp/child-process.js → process/index.js} +23 -6
  77. package/src/settings/env-admin.js +1 -1
  78. package/src/settings/index.js +1 -1
  79. package/src/settings/sections.js +0 -54
  80. package/src/shared/audit.js +12 -0
  81. package/src/shared/child-scope.js +3 -1
  82. package/src/shared/http-json.js +28 -0
  83. package/src/{delegate/audit-readers.js → shared/terminal-evidence.js} +34 -4
  84. package/src/worktree/errors.js +3 -0
  85. package/src/worktree/index.js +1 -2
  86. package/src/worktree/lane-core.js +641 -0
  87. package/src/worktree/lane-views.js +147 -0
  88. package/src/worktree/lane-workflows.js +853 -0
  89. package/src/worktree/lanes.js +24 -1513
  90. package/src/worktree/runner.js +5 -4
  91. package/src/capabilities/content-pin.js +0 -88
  92. package/src/capabilities/content-refresh.js +0 -331
  93. package/src/capabilities/distribution-manifest.js +0 -78
  94. package/src/capabilities/isolated-exec.js +0 -106
  95. package/src/capabilities/publisher.js +0 -129
  96. package/src/capabilities/skill-admission.js +0 -181
  97. package/src/capabilities/update-lifecycle.js +0 -106
  98. package/src/edit-lock/remote-service.js +0 -28
  99. package/src/worktree/contract-guidance.test.js +0 -100
@@ -12,6 +12,20 @@
12
12
  // wire delivery. The kernel is concept-isomorphic to Edit Lock but shares
13
13
  // none of its code: no persistence, no recovery, no cross-process story.
14
14
 
15
+ /**
16
+ * One live write token; `timer` is the injected timer handle (opaque here).
17
+ * @typedef {{ key: string, holder: string, expiresAt: number, timer: unknown }} Token
18
+ * One registered subscription row (rebuilt from the owning runtime's snapshot).
19
+ * @typedef {{ key: string, subscriberIds: string[] }} Subscription
20
+ * The release-event shape every listener receives (slice 2 delivery wiring).
21
+ * @typedef {{ boardId: string, key: string, holder: string, reason: 'write' | 'delete' | 'expire' | 'terminate', subscriberIds: string[], at: number }} ReleaseEvent
22
+ * A release listener; onRelease returns the matching unregister.
23
+ * @typedef {(event: ReleaseEvent) => void} ReleaseListener
24
+ * One board's volatile state: durable entries plus the arbitration maps.
25
+ * @typedef {{ entries: Map<string, object>, tokens: Map<string, Token>, subscribers: Map<string, Set<string>> }} BoardState
26
+ * The runtime snapshot `restore` rebuilds a board's volatile state from.
27
+ * @typedef {{ tokens: Token[], subscriptions: Subscription[] }} RuntimeState
28
+ */
15
29
  /** The closed entry-type taxonomy (D4); schema `enum` and runtime both pin it. */
16
30
  export const ENTRY_TYPES = Object.freeze(['map', 'contract', 'deadend', 'wiring', 'recipe', 'why'])
17
31
 
@@ -101,9 +115,9 @@ export function createBlackboardKernel({ now = Date.now, setTimer, clearTimer }
101
115
  })
102
116
  const disarm = clearTimer ?? ((/** @type {any} */ handle) => globalThis.clearTimeout(handle))
103
117
 
104
- /** @type {Map<string, { entries: Map<string, object>, tokens: Map<string, object>, subscribers: Map<string, Set<string>> }>} */
118
+ /** @type {Map<string, BoardState>} */
105
119
  const boards = new Map()
106
- /** @type {Set<(event: object) => void>} release listeners (slice 2 delivery wiring) */
120
+ /** @type {Set<ReleaseListener>} release listeners (slice 2 delivery wiring) */
107
121
  const listeners = new Set()
108
122
 
109
123
  /** @param {string} boardId @returns {any} the board, created lazily */
@@ -122,7 +136,7 @@ export function createBlackboardKernel({ now = Date.now, setTimer, clearTimer }
122
136
  * best-effort and NEVER breaks arbitration; events only fire when someone
123
137
  * actually waits (subscriberIds non-empty) so slice 2 gets a signal, not
124
138
  * noise.
125
- * @param {object} event
139
+ * @param {ReleaseEvent} event
126
140
  */
127
141
  function fireRelease(event) {
128
142
  if (!Array.isArray(event.subscriberIds) || event.subscriberIds.length === 0) return
@@ -153,7 +167,7 @@ export function createBlackboardKernel({ now = Date.now, setTimer, clearTimer }
153
167
  const state = boards.get(boardId)
154
168
  const token = state?.tokens.get(key)
155
169
  if (!token || token.expiresAt > at) return
156
- state.tokens.delete(key)
170
+ /** @type {BoardState} */ (state).tokens.delete(key)
157
171
  if (token.timer !== undefined) disarm(token.timer)
158
172
  release(state, boardId, key, token.holder, 'expire', at)
159
173
  }
@@ -438,11 +452,14 @@ export function createBlackboardKernel({ now = Date.now, setTimer, clearTimer }
438
452
 
439
453
  // Internal transaction seam. Only entries are durable; authority comes
440
454
  // exclusively from the owning runtime's snapshot, never from disk.
455
+ /**
456
+ * @param {string} boardId @param {any} entries @param {RuntimeState} [runtime]
457
+ */
441
458
  restore(boardId, entries, runtime = { tokens: [], subscriptions: [] }) {
442
459
  const state = board(boardId)
443
460
  state.entries = new Map(entries.map(entry => [entry.key, structuredClone(entry)]))
444
- state.tokens = new Map(runtime.tokens.map(token => [token.key, { ...token }]))
445
- state.subscribers = new Map(runtime.subscriptions.map(row => [row.key, new Set(row.subscriberIds)]))
461
+ state.tokens = new Map(runtime.tokens.map(token => /** @type {[string, Token]} */ ([token.key, { ...token }])))
462
+ state.subscribers = new Map(runtime.subscriptions.map(row => /** @type {[string, Set<string>]} */ ([row.key, new Set(row.subscriberIds)])))
446
463
  },
447
464
 
448
465
  expire(boardId, at = now()) {
@@ -32,6 +32,12 @@ export const RELEASE_NOTICE_PREFIX = 'Blackboard notice'
32
32
  /** Delivery attempts before the final failure is logged and dropped. */
33
33
  export const MAX_DELIVERY_ATTEMPTS = 3
34
34
 
35
+ /**
36
+ * The subagent delivery face a between-turns subscriber is reached through: asserted
37
+ * present on the one path that needs it (no runtime guard is added, design D6).
38
+ * @typedef {{ sendMessage: (parent: any, childId: string, blocks: Array<{ type: string, text: string }>, options: object) => unknown }} SubagentSendFace
39
+ */
40
+
35
41
  /**
36
42
  * The human-readable release clause per reason. Instance content (the key) is
37
43
  * embedded verbatim; the template is English.
@@ -117,7 +123,7 @@ export function createReleaseNotifier({ agents, parentOf, subagents, logger, set
117
123
  * the live agent works, followup when it is idle, and — for a delegated
118
124
  * child whose agent handle is dropped between turns — the subagents channel
119
125
  * through its live parent.
120
- * @param {string} subscriberId @param {object} message @param {number} attempt
126
+ * @param {string} subscriberId @param {{ content: Array<{ type: string, text: string }> }} message @param {number} attempt
121
127
  */
122
128
  function deliver(subscriberId, message, attempt) {
123
129
  const agent = safeGet(agents, subscriberId)
@@ -137,7 +143,7 @@ export function createReleaseNotifier({ agents, parentOf, subagents, logger, set
137
143
  }
138
144
  const parent = typeof parentId === 'string' && parentId.length > 0 ? safeGet(agents, parentId) : undefined
139
145
  if (!parent) throw new Error(`subscriber "${subscriberId}" is between turns and its parent agent is unavailable`)
140
- return channel.sendMessage(parent, subscriberId, [{ type: 'text', text: message.content[0].text }], { signal: new AbortController().signal })
146
+ return /** @type {SubagentSendFace} */ (channel).sendMessage(parent, subscriberId, [{ type: 'text', text: message.content[0].text }], { signal: new AbortController().signal })
141
147
  }, (error) => {
142
148
  logger?.warn?.(`blackboard: release notice delivery to "${subscriberId}" failed after ${MAX_DELIVERY_ATTEMPTS} attempts: ${error?.message ?? error}`)
143
149
  })
@@ -145,7 +151,8 @@ export function createReleaseNotifier({ agents, parentOf, subagents, logger, set
145
151
 
146
152
  return (event) => {
147
153
  if (!Array.isArray(event.subscriberIds) || event.subscriberIds.length === 0) return
148
- const message = userTextMessage(renderReleaseNotice(event), RELEASE_SOURCE_KIND)
154
+ // userTextMessage declares only `object`; assert the shape deliver reads (S13 UserMessage contract).
155
+ const message = /** @type {{ content: Array<{ type: string, text: string }> }} */ (userTextMessage(renderReleaseNotice(event), RELEASE_SOURCE_KIND))
149
156
  for (const subscriberId of event.subscriberIds) {
150
157
  if (typeof subscriberId !== 'string' || subscriberId.length === 0) continue
151
158
  deliver(subscriberId, message, 1)
@@ -84,7 +84,7 @@ const BLACKBOARD_REMOTE_PACKAGE = 'weir-blackboard'
84
84
  */
85
85
  export class BlackboardRemoteError extends Error {
86
86
  /**
87
- * @param {'bridge-absent' | 'unknown-session' | 'payload-failed'} code
87
+ * @param {'bridge-absent' | 'unknown-session' | 'payload-failed' | 'channel-unavailable'} code
88
88
  * @param {string} message
89
89
  * @param {{ cause?: unknown }} [options]
90
90
  */
@@ -96,29 +96,47 @@ export class BlackboardRemoteError extends Error {
96
96
  }
97
97
 
98
98
  /**
99
- * The bridge: exactly one blackboard plugin mounts per composition (per
100
- * process), and every consumer is the same bundle module instance — the same
101
- * module-level slot pattern as the capability read bridge. Faces:
99
+ * The bridge: EVERY mounted blackboard plugin feeds this module-level slot,
100
+ * and every consumer is the same bundle module instance. The preset registry
101
+ * eagerly mounts BOTH declared presets (weir and weir-creative) at profile
102
+ * load, so two blackboard mounts feeding this bridge is the permanent steady
103
+ * state — ownership is a LIVE-FEED STACK: the latest live feed serves, a
104
+ * feed's unregister removes only itself and restores the previous live feed,
105
+ * and only an empty stack surfaces as bridge-absent. (The sibling capability
106
+ * read bridge keys its feeds by preset instead: its reads route by the
107
+ * session's exact projected preset, while blackboard calls carry no routing
108
+ * dimension and the two presets' faces are equivalent today.)
109
+ * Faces:
102
110
  * kernel() — the shared arbitration kernel (list/read/apply/write/deleteKey)
103
111
  * boardOf(agent) — conversation-root board id for an agent handle
104
112
  * rootOf(sessionId) — conversation-root session id for a BARE session id
105
113
  * ttlMs() — the settings-driven write-token TTL in milliseconds
106
114
  */
107
- let bridgeFaces = null
115
+ /** The faces every live feed provides. `run` is the mount's lifecycle gate (apply/write/delete through one admission), `requestPromotion` the slice-3 promotion requester, and `boardOf` may await that same gate. @typedef {{ kernel: any, boardOf: (agent: any) => string | undefined | Promise<string | undefined>, rootOf?: (sessionId: string) => Promise<string>, ttlMs: () => number, run?: (agent: any, action: (root: string) => any) => Promise<any>, requestPromotion?: (agent: any) => Promise<unknown> }} BridgeFaces */
116
+ /** @type {Array<{ faces: BridgeFaces }>} Live feeds in feed order; the stack top serves. */
117
+ const bridgeStack = []
108
118
 
109
119
  /**
110
- * Feed the bridge (blackboard plugin apply). Returns the unregister the plugin
111
- * disposes with its own lifecycle.
112
- * @param {{ kernel: any, boardOf: (agent: any) => string | undefined, rootOf?: (sessionId: string) => Promise<string>, ttlMs: () => number }} faces
120
+ * Feed the bridge (blackboard plugin apply): push a unique token onto the
121
+ * live-feed stack and return the unregister the plugin disposes with its own
122
+ * lifecycle. The unregister removes exactly its own token — removing the
123
+ * serving token restores the previous live feed — and a repeated call is a
124
+ * no-op.
125
+ * @param {BridgeFaces} faces
113
126
  */
114
127
  export function feedBlackboardRemoteBridge(faces) {
115
- bridgeFaces = faces
116
- return () => { if (bridgeFaces === faces) bridgeFaces = null }
128
+ const token = { faces }
129
+ bridgeStack.push(token)
130
+ return () => {
131
+ const at = bridgeStack.indexOf(token)
132
+ if (at >= 0) bridgeStack.splice(at, 1)
133
+ }
117
134
  }
118
135
 
119
- /** Current bridge faces, or null when unmounted. */
136
+ /** Current bridge faces: the latest live feed's, or null when no live feed remains (the one true bridge-absent). */
120
137
  export function blackboardRemoteBridge() {
121
- return bridgeFaces
138
+ const top = bridgeStack[bridgeStack.length - 1]
139
+ return top ? top.faces : null
122
140
  }
123
141
 
124
142
  /** Strip one kernel row to the pinned wire entry shape (summary is a free-text string, content optional, the promoted marker rides along when present). */
@@ -165,7 +183,7 @@ export function createBlackboardRemoteService(dependencies = {}) {
165
183
  /**
166
184
  * Resolve the acting session and its board from the wire agent.
167
185
  * @param {any} agent
168
- * @returns {{ sessionId: string, boardId: string, faces: { kernel: any, boardOf: Function, ttlMs: Function } }}
186
+ * @returns {Promise<{ sessionId: string, boardId: string | undefined, faces: BridgeFaces }>}
169
187
  */
170
188
  async function resolveActing(agent) {
171
189
  if (!ensure()) throw new BlackboardRemoteError('channel-unavailable', 'blackboard remote is not registered with the gateway')
@@ -12,7 +12,9 @@ export const LOG = /^session\.v([1-9][0-9]*)\.jsonl(?:\.zstd)?$/
12
12
  const SIDE = /^session\.v[1-9][0-9]*\.jsonl(?:\.zstd)?_bb$/
13
13
  const ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/
14
14
  const LOCK = 'weir-blackboard.lock'
15
- const uid = () => process.getuid?.()
15
+ // The checked graph's global `process` contract (capabilities/store/checked-contract.d.ts)
16
+ // is narrow by design; getuid is optional here and asserted locally (kernel.js:99 precedent).
17
+ const uid = () => /** @type {any} */ (process).getuid?.()
16
18
  const code = error => error?.code
17
19
  const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
18
20
 
@@ -55,11 +57,11 @@ export async function locateSession(home, id, expectedCwd) {
55
57
  const logs = (await fs.readdir(dir)).filter(name => LOG.test(name))
56
58
  // Highest generation is DSH's canonical selection. Same-version physical
57
59
  // encodings are ambiguous: never guess which installation owns the log.
58
- if (logs.some(name => !Number.isSafeInteger(Number(LOG.exec(name)[1])))) throw new Error('blackboard: unsupported canonical generation number')
59
- logs.sort((a, b) => Number(LOG.exec(b)[1]) - Number(LOG.exec(a)[1]))
60
+ if (logs.some(name => !Number.isSafeInteger(Number(/** @type {RegExpExecArray} */ (LOG.exec(name))[1])))) throw new Error('blackboard: unsupported canonical generation number')
61
+ logs.sort((a, b) => Number(/** @type {RegExpExecArray} */ (LOG.exec(b))[1]) - Number(/** @type {RegExpExecArray} */ (LOG.exec(a))[1]))
60
62
  if (!logs.length) continue
61
63
  const name = logs[0]
62
- if (logs[1] && LOG.exec(logs[1])[1] === LOG.exec(name)[1]) throw new Error('blackboard: ambiguous canonical log')
64
+ if (logs[1] && /** @type {RegExpExecArray} */ (LOG.exec(logs[1]))[1] === /** @type {RegExpExecArray} */ (LOG.exec(name))[1]) throw new Error('blackboard: ambiguous canonical log')
63
65
  const path = join(dir, name)
64
66
  await trusted(path)
65
67
  const handle = await fs.open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0))
@@ -70,7 +72,7 @@ export async function locateSession(home, id, expectedCwd) {
70
72
  const text = name.endsWith('.zstd') ? zstdDecompressSync(bytes).toString('utf8') : bytes.toString('utf8')
71
73
  header = JSON.parse(text.split('\n')[0])
72
74
  } catch { throw new Error('blackboard: unreadable Session header') }
73
- if (header.type !== 'session' || header.id !== id || typeof header.cwd !== 'string' || !isAbsolute(header.cwd) || header.version !== Number(LOG.exec(name)[1]) || expectedCwd !== undefined && header.cwd !== expectedCwd) throw new Error('blackboard: Session header identity/cwd mismatch')
75
+ if (header.type !== 'session' || header.id !== id || typeof header.cwd !== 'string' || !isAbsolute(header.cwd) || header.version !== Number(/** @type {RegExpExecArray} */ (LOG.exec(name))[1]) || expectedCwd !== undefined && header.cwd !== expectedCwd) throw new Error('blackboard: Session header identity/cwd mismatch')
74
76
  matches.push({ id, dir, log: path, file: join(dir, name + '_bb'), header })
75
77
  }
76
78
  if (matches.length !== 1) throw new Error(`blackboard: ${matches.length ? 'ambiguous Session directories' : 'durable Session log unavailable; sidecars preserved'}`)
@@ -102,6 +104,12 @@ function dead(owner) {
102
104
  const ownerKey = Symbol.for('weir.blackboard.storage.owner.v1')
103
105
  const writerToken = globalThis[ownerKey] ??= randomUUID()
104
106
 
107
+ /**
108
+ * @param {object} [dependencies] test seams
109
+ * @param {string} [dependencies.home] profile home; locateSession fail-closes on an unusable value
110
+ * @param {(op: string, path: string) => unknown} [dependencies.hook] maintenance fault-injection seam
111
+ * @param {number} [dependencies.timeoutMs] writer-lock acquisition budget
112
+ */
105
113
  export function createBlackboardStorage({ home, hook = async (_op, _path) => {}, timeoutMs = 5000 } = {}) {
106
114
  const queue = new Map()
107
115
  async function safeRead(path) {