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.
- package/cordis.patch.yml +0 -2
- package/lib/client.blackboard-model.js +172 -56
- package/lib/client.capability-manager.js +0 -1
- package/lib/client.capability-model.js +378 -31
- package/lib/client.chain-model.js +80 -0
- package/lib/client.disabled-categories-editor.js +22 -0
- package/lib/client.edit-lock-maintenance.js +239 -246
- package/lib/client.edit-lock-panel.js +110 -0
- package/lib/client.hash-edit-model.js +48 -0
- package/lib/client.js +267 -1549
- package/lib/client.lsp-model.js +92 -0
- package/lib/client.lsp-panel.js +11 -16
- package/lib/client.lsp-toggle.js +18 -0
- package/lib/client.notify-permissions.js +112 -20
- package/lib/client.notify-web.js +14 -11
- package/lib/client.robash-model.js +26 -0
- package/lib/client.settings-page.js +287 -17
- package/lib/client.worktree-model.js +225 -0
- package/package.json +5 -2
- package/src/blackboard/durable.js +12 -0
- package/src/blackboard/index.js +2 -2
- package/src/blackboard/kernel.js +23 -6
- package/src/blackboard/notify.js +10 -3
- package/src/blackboard/remote.js +31 -13
- package/src/blackboard/storage.js +13 -5
- package/src/capabilities/apply-engine.js +189 -101
- package/src/capabilities/defaults-transaction.js +136 -1
- package/src/capabilities/lifecycle-snapshot.js +83 -204
- package/src/capabilities/mcp-admission.js +1 -23
- package/src/capabilities/mcp-drain.js +30 -19
- package/src/capabilities/mcp-facade.js +23 -3
- package/src/capabilities/preset-library.js +247 -0
- package/src/capabilities/preset-package.js +212 -1
- package/src/capabilities/same-name-policy.js +3 -41
- package/src/capabilities/selection-notify.js +324 -21
- package/src/capabilities/skill-office-adapter.js +7 -1
- package/src/capabilities/skill-selection-plugin.js +315 -729
- package/src/capabilities/store/checked-contract.d.ts +5 -0
- package/src/capabilities/store/fs-adapter.js +52 -0
- package/src/capabilities/store/lock.js +86 -0
- package/src/capabilities/store/paths.js +5 -0
- package/src/capabilities/store/store.js +105 -5
- package/src/creative-guide/guide.js +1 -1
- package/src/delegate/child-composition.js +258 -0
- package/src/delegate/group-coordinator.js +25 -27
- package/src/delegate/index.js +21 -2
- package/src/delegate/rehydrate.js +24 -4
- package/src/delegate/robash-guard-core.js +2 -2
- package/src/delegate/spawn-adapter.js +214 -100
- package/src/delegate/status-tool.js +9 -6
- package/src/delegate/supervision-mount.js +35 -6
- package/src/delegate/tool.js +29 -4
- package/src/{shared → delegate}/whitelist-defaults.js +2 -2
- package/src/edit-lock/admin-ledger.js +8 -11
- package/src/edit-lock/admin-recovery.js +2 -1
- package/src/edit-lock/admission.js +12 -2
- package/src/edit-lock/binding-guidance.js +28 -10
- package/src/edit-lock/binding-machine.js +312 -0
- package/src/edit-lock/derived-facts.js +67 -0
- package/src/edit-lock/index.js +44 -244
- package/src/edit-lock/inspect.js +113 -1
- package/src/edit-lock/lifecycle.js +8 -5
- package/src/edit-lock/limits.js +61 -0
- package/src/edit-lock/maintenance.js +5 -2
- package/src/edit-lock/manager.js +6 -2
- package/src/edit-lock/operation-history.js +1 -1
- package/src/edit-lock/peer-client.js +9 -2
- package/src/edit-lock/peer-transport.js +7 -1
- package/src/edit-lock/remote.js +11 -4
- package/src/edit-lock/state.js +2 -4
- package/src/edit-lock/view.js +10 -11
- package/src/lsp/admin.js +2 -24
- package/src/lsp/index.js +1 -1
- package/src/lsp/manager.js +1 -1
- package/src/notify/permissions-admin.js +1 -1
- package/src/{lsp/child-process.js → process/index.js} +23 -6
- package/src/settings/env-admin.js +1 -1
- package/src/settings/index.js +1 -1
- package/src/settings/sections.js +0 -54
- package/src/shared/audit.js +12 -0
- package/src/shared/child-scope.js +3 -1
- package/src/shared/http-json.js +28 -0
- package/src/{delegate/audit-readers.js → shared/terminal-evidence.js} +34 -4
- package/src/worktree/errors.js +3 -0
- package/src/worktree/index.js +1 -2
- package/src/worktree/lane-core.js +641 -0
- package/src/worktree/lane-views.js +147 -0
- package/src/worktree/lane-workflows.js +853 -0
- package/src/worktree/lanes.js +24 -1513
- package/src/worktree/runner.js +5 -4
- package/src/capabilities/content-pin.js +0 -88
- package/src/capabilities/content-refresh.js +0 -331
- package/src/capabilities/distribution-manifest.js +0 -78
- package/src/capabilities/isolated-exec.js +0 -106
- package/src/capabilities/publisher.js +0 -129
- package/src/capabilities/skill-admission.js +0 -181
- package/src/capabilities/update-lifecycle.js +0 -106
- package/src/edit-lock/remote-service.js +0 -28
- package/src/worktree/contract-guidance.test.js +0 -100
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asBindingFailure } from './binding-guidance.js'
|
|
1
2
|
import { canonicalRequestData } from './request-data.js'
|
|
2
3
|
import { PEER_KINDS } from './peer.js'
|
|
3
4
|
|
|
@@ -39,15 +40,21 @@ export function createEditLockPeerClient(stream, hooks = {}) {
|
|
|
39
40
|
try { hooks.onEvent?.(message.event) } catch { /* notices never imply consent */ }
|
|
40
41
|
continue
|
|
41
42
|
}
|
|
43
|
+
const frameKeys = !message || typeof message !== 'object' ? '' : Object.keys(message).sort().join(',')
|
|
44
|
+
// The error frame carries the structured binding refusal as an additive
|
|
45
|
+
// optional field (change edit-lock-deepening, design D2).
|
|
42
46
|
if (!message || typeof message.callId !== 'string' || typeof message.ok !== 'boolean'
|
|
43
|
-
||
|
|
47
|
+
|| (message.ok ? frameKeys !== 'callId,ok,outcome' : frameKeys !== 'callId,error,ok' && frameKeys !== 'bindingFailure,callId,error,ok')
|
|
44
48
|
|| (!message.ok && typeof message.error !== 'string')) { close(); return }
|
|
45
49
|
const entry = pending.get(message.callId)
|
|
46
50
|
if (!entry) { close(); return }
|
|
47
51
|
pending.delete(message.callId)
|
|
48
52
|
entry.cleanup()
|
|
49
53
|
if (message.ok) entry.resolve(message.outcome)
|
|
50
|
-
else
|
|
54
|
+
else {
|
|
55
|
+
const failure = asBindingFailure(message.bindingFailure)
|
|
56
|
+
entry.reject(Object.assign(new Error(message.error), failure ? { bindingFailure: failure } : {}))
|
|
57
|
+
}
|
|
51
58
|
}
|
|
52
59
|
}
|
|
53
60
|
stream.on('data', receive)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asBindingFailure } from './binding-guidance.js'
|
|
1
2
|
import { canonicalRequestData } from './request-data.js'
|
|
2
3
|
|
|
3
4
|
/** Attach ONLY an authenticated, host-owned byte stream. Never accepts public
|
|
@@ -54,7 +55,12 @@ export function serveEditLockPeer(stream, peer) {
|
|
|
54
55
|
const callId = request.callId
|
|
55
56
|
void peer.receive(request).then(
|
|
56
57
|
outcome => { try { send({callId, ok:true, outcome:outcome ?? null}) } catch { close() } },
|
|
57
|
-
error => {
|
|
58
|
+
error => {
|
|
59
|
+
// The wire carries the structured binding refusal additively (change
|
|
60
|
+
// edit-lock-deepening, design D2); the text stays for display.
|
|
61
|
+
const failure = asBindingFailure(/** @type {any} */ (error)?.bindingFailure)
|
|
62
|
+
try { send({ callId, ok: false, error: String(/** @type {any} */ (error)?.message ?? error).slice(0, 4096), ...(failure ? { bindingFailure: failure } : {}) }) } catch { close() }
|
|
63
|
+
},
|
|
58
64
|
)
|
|
59
65
|
}
|
|
60
66
|
}
|
package/src/edit-lock/remote.js
CHANGED
|
@@ -3,7 +3,7 @@ import { lstatSync, realpathSync, unlinkSync } from 'node:fs'
|
|
|
3
3
|
import { createConnection, createServer } from 'node:net'
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
|
-
import { bindingGuidance, bindingRefusal } from './binding-guidance.js'
|
|
6
|
+
import { asBindingFailure, bindingGuidance, bindingRefusal } from './binding-guidance.js'
|
|
7
7
|
import { createEditLockPeer } from './peer.js'
|
|
8
8
|
import { serveEditLockPeer } from './peer-transport.js'
|
|
9
9
|
import { createEditLockPeerClient } from './peer-client.js'
|
|
@@ -275,12 +275,16 @@ export function createRemoteEditLockDomain(endpoint, hooks) {
|
|
|
275
275
|
for (let attempt = 0; ; attempt++) {
|
|
276
276
|
try { return await openChannel(agent, retry) } catch (error) {
|
|
277
277
|
if (/** @type {any} */ (error)?.answeredRefusal !== true) throw error
|
|
278
|
-
|
|
278
|
+
// The retry budget classifies on the STRUCTURED refusal field, never on
|
|
279
|
+
// the display text (change edit-lock-deepening, design D2): only an
|
|
280
|
+
// answered, publisher-classified duplicate binding is retried — the
|
|
281
|
+
// publisher may not yet have processed the earlier channel's death.
|
|
282
|
+
if (!retry && /** @type {any} */ (error)?.bindingFailure?.code === 'binding-duplicate' && attempt < 20) {
|
|
279
283
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
280
284
|
continue
|
|
281
285
|
}
|
|
282
286
|
const reason = String(/** @type {any} */ (error)?.message ?? error)
|
|
283
|
-
const guidance = bindingGuidance(agent.id,
|
|
287
|
+
const guidance = bindingGuidance(agent.id, /** @type {any} */ (error)?.bindingFailure)
|
|
284
288
|
throw Object.assign(new Error(`edit lock duplicate session binding: ${guidance} (publisher answered: ${reason})`), { bindingFailure: /** @type {any} */ (error)?.bindingFailure })
|
|
285
289
|
}
|
|
286
290
|
}
|
|
@@ -317,7 +321,10 @@ export function createRemoteEditLockDomain(endpoint, hooks) {
|
|
|
317
321
|
let opened
|
|
318
322
|
try {
|
|
319
323
|
opened = await channel.client.request('open', 'open', { sessionId: agent.id, nonce, pid: process.pid, bindingFailures: true, retry }, never)
|
|
320
|
-
|
|
324
|
+
// The answered refusal's structured field is validated on receipt;
|
|
325
|
+
// only the exact minted shape becomes decision input.
|
|
326
|
+
const refused = opened?.state === 'unavailable' ? asBindingFailure(opened.bindingFailure) : undefined
|
|
327
|
+
if (refused) throw Object.assign(new Error(opened.reason), { bindingFailure: refused })
|
|
321
328
|
} catch (error) {
|
|
322
329
|
const message = String(/** @type {any} */ (error)?.message ?? error)
|
|
323
330
|
// No answer ever arrived — the channel died mid-open or the request
|
package/src/edit-lock/state.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* @typedef {{ now?: number, singleMaxMs: number, cumulativeMaxMs: number }} HoldCaps
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
import { settleHoldFacts } from './derived-facts.js'
|
|
27
28
|
/** Private transfer seam; no raw hydration is exported. */
|
|
28
29
|
const cores = new WeakMap()
|
|
29
30
|
/** @param {string} managerIncarnation */
|
|
@@ -165,10 +166,7 @@ export function createEditLockState(managerIncarnation) {
|
|
|
165
166
|
const held = holds.get(sessionId)
|
|
166
167
|
if (!held) return Object.freeze({ sessionId, held: false, expired: false, holdUntil: null, remainingMs: 0, holdCumulativeMs: 0 })
|
|
167
168
|
const owns = [...locks.values()].some(lock => lock.owner === sessionId)
|
|
168
|
-
|
|
169
|
-
const holding = held.holding && owns
|
|
170
|
-
return Object.freeze({ sessionId, held: holding, expired: holding && held.holdUntil !== null && remainingMs === 0,
|
|
171
|
-
holdUntil: held.holdUntil, remainingMs, holdCumulativeMs: held.holdCumulativeMs })
|
|
169
|
+
return Object.freeze(settleHoldFacts(sessionId, held, owns, now))
|
|
172
170
|
},
|
|
173
171
|
}
|
|
174
172
|
|
package/src/edit-lock/view.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* in `technical` and per-lock `detail`, for the expandable detail only.
|
|
11
11
|
*/
|
|
12
12
|
import { isAbsolute, relative, sep } from 'node:path'
|
|
13
|
+
import { revokedOwner, settleHoldFacts } from './derived-facts.js'
|
|
13
14
|
|
|
14
15
|
/** Session states the panel renders, in precedence order. */
|
|
15
16
|
export const VIEW_STATES = Object.freeze(['revoked', 'unavailable', 'stopped', 'attention', 'confirm', 'holding', 'editing', 'idle'])
|
|
@@ -140,21 +141,19 @@ export function buildColdView({ image, sessionId, cwd, root, autoResume = true,
|
|
|
140
141
|
const locks = (Array.isArray(image?.locks) ? image.locks : []).filter(lock => lock !== null && typeof lock === 'object')
|
|
141
142
|
const holds = Array.isArray(image?.holds) ? image.holds : []
|
|
142
143
|
const recoveries = Array.isArray(image?.recovery) ? image.recovery : []
|
|
143
|
-
const adminRecoveries = Array.isArray(image?.adminRecoveries) ? image.adminRecoveries : []
|
|
144
144
|
const session = sessions.find(row => row?.sessionId === sessionId) ?? null
|
|
145
145
|
// The manager derives the revoked flag from the administrative ledger at
|
|
146
|
-
// status time (revokedOwner); the cold read applies the
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
146
|
+
// status time (revokedOwner); the cold read applies the SAME shared rule in
|
|
147
|
+
// its deliberately tolerant partial mode (design D1).
|
|
148
|
+
const revoked = revokedOwner(image, sessionId, { partial: true })
|
|
149
|
+
// Read-time retention settlement through the shared derived fact (design D5,
|
|
150
|
+
// partial mode — byte-equivalent to the kernel's settleHold): a hold counts
|
|
151
|
+
// only while the session owns a lock, and the remaining budget is computed
|
|
152
|
+
// against the read instant, so a missed expiry timer can never leave stale
|
|
153
|
+
// ownership behind a status read. No hold row settles to no retention (null).
|
|
152
154
|
const held = holds.find(row => row?.sessionId === sessionId) ?? null
|
|
153
155
|
const owns = locks.some(lock => lock.owner === sessionId)
|
|
154
|
-
const
|
|
155
|
-
const holding = held?.holding === true && owns
|
|
156
|
-
const retention = held ? { sessionId, held: holding, expired: holding && held.holdUntil !== null && remainingMs === 0,
|
|
157
|
-
holdUntil: held.holdUntil, remainingMs, holdCumulativeMs: held.holdCumulativeMs } : null
|
|
156
|
+
const retention = held ? settleHoldFacts(sessionId, held, owns, now, { partial: true }) : null
|
|
158
157
|
const status = {
|
|
159
158
|
sessionId,
|
|
160
159
|
// Interrupted records a stopped session (a restarted host interrupts every
|
package/src/lsp/admin.js
CHANGED
|
@@ -4,40 +4,18 @@
|
|
|
4
4
|
// resolve through ctx.get so the module mounts harmlessly in compositions
|
|
5
5
|
// without them (headless).
|
|
6
6
|
import { buildRegistry, DEFAULT_SERVERS, displayInstallCommand, installSpecFor, languageIdsForFamily } from './registry.js'
|
|
7
|
-
import { npmGlobalPrefix, resolveExecutable as extendedResolveExecutable, runBounded, spawnArgv } from '
|
|
7
|
+
import { npmGlobalPrefix, resolveExecutable as extendedResolveExecutable, runBounded, spawnArgv } from '../process/index.js'
|
|
8
|
+
import { jsonResponse, readJsonBody } from '../shared/http-json.js'
|
|
8
9
|
|
|
9
10
|
const name = 'weir-lsp-admin'
|
|
10
11
|
const inject = []
|
|
11
12
|
const DEFAULT_INSTALL_TIMEOUT_MS = 600_000
|
|
12
13
|
const VERSION_PROBE_TIMEOUT_MS = 8_000
|
|
13
14
|
|
|
14
|
-
export function jsonResponse(payload, status = 200) {
|
|
15
|
-
return new Response(JSON.stringify(payload), {
|
|
16
|
-
status,
|
|
17
|
-
headers: { 'content-type': 'application/json' },
|
|
18
|
-
})
|
|
19
|
-
}
|
|
20
|
-
|
|
21
15
|
export function errorResponse(message, status = 400) {
|
|
22
16
|
return jsonResponse({ ok: false, error: { code: `weir-lsp/${status === 400 ? 'invalid' : 'internal'}`, message } }, status)
|
|
23
17
|
}
|
|
24
18
|
|
|
25
|
-
/** Read the JSON body of a connection request (Request-like or stream). */
|
|
26
|
-
export async function readJsonBody(request) {
|
|
27
|
-
try {
|
|
28
|
-
if (typeof request?.json === 'function') return await request.json()
|
|
29
|
-
} catch {
|
|
30
|
-
return null
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
const chunks = []
|
|
34
|
-
for await (const chunk of request?.body ?? []) chunks.push(Buffer.from(chunk))
|
|
35
|
-
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
36
|
-
} catch {
|
|
37
|
-
return null
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
19
|
/** Accept only lines that plausibly carry a version (stack traces and
|
|
42
20
|
* paths from servers without --version support are dropped). */
|
|
43
21
|
export function looksLikeVersion(line) {
|
package/src/lsp/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// ESM, ctx-only.
|
|
9
9
|
import { createLspManager, LSP_DEFAULTS } from './manager.js'
|
|
10
10
|
import { buildRegistry } from './registry.js'
|
|
11
|
-
import { resolveExecutable as extendedResolveExecutable } from '
|
|
11
|
+
import { resolveExecutable as extendedResolveExecutable } from '../process/index.js'
|
|
12
12
|
import { createLspTools } from './tools.js'
|
|
13
13
|
|
|
14
14
|
const name = 'weir-lsp'
|
package/src/lsp/manager.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// lazy start, full-document sync, idle shutdown, per-session holder refcounts.
|
|
3
3
|
import { createLspClient, handshake, shutdownClient } from './client.js'
|
|
4
4
|
import { familyForLanguageId } from './registry.js'
|
|
5
|
-
import { childEnvironment, spawnArgv } from '
|
|
5
|
+
import { childEnvironment, spawnArgv } from '../process/index.js'
|
|
6
6
|
import { pathToUri } from './uri.js'
|
|
7
7
|
|
|
8
8
|
export const LSP_DEFAULTS = {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// connection service resolves from that scope (S20). The platform and the
|
|
5
5
|
// process spawner are injected for tests.
|
|
6
6
|
import { execFile as nodeExecFile } from 'node:child_process'
|
|
7
|
-
import { jsonResponse, readJsonBody } from '../
|
|
7
|
+
import { jsonResponse, readJsonBody } from '../shared/http-json.js'
|
|
8
8
|
import { ACTIONS, readNotificationPermission, runPermissionAction, SENDER } from './permissions.js'
|
|
9
9
|
|
|
10
10
|
const STATUS_PATH = '/api/weir-notify/permissions/status'
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Generic process machinery: executable resolution, launch-shape
|
|
2
|
+
// adjudication, and deadline-bounded runs — one concept, three stages
|
|
3
|
+
// (resolve → launch shape → bounded run). The desktop app is
|
|
2
4
|
// a GUI process launched by LaunchServices, so its PATH is the minimal
|
|
3
5
|
// `/usr/bin:/bin:/usr/sbin:/sbin` — Homebrew (/opt/homebrew, /usr/local),
|
|
4
6
|
// nvm, cargo, go, pipx and npm-global bins are all missing, and
|
|
5
7
|
// ctx.subprocess.resolveExecutable() fails for them (S21). This helper
|
|
6
|
-
// falls back to scanning well-known installation directories so
|
|
7
|
-
//
|
|
8
|
+
// falls back to scanning well-known installation directories so spawned
|
|
9
|
+
// tools resolve the same way they do in the user's shell.
|
|
8
10
|
//
|
|
9
11
|
// Windows resolves through the same helper with a different dialect: npm puts
|
|
10
12
|
// its global shims in `%APPDATA%\npm` (not `~/.npm-global/bin`), bare names
|
|
11
13
|
// carry a PATHEXT extension, and the PATH separator is `;`.
|
|
12
|
-
//
|
|
14
|
+
//
|
|
15
|
+
// No LSP-specific semantics live here: the LSP subsystem (server lifecycle,
|
|
16
|
+
// version probe, panel installer) and the worktree git runner are peer
|
|
17
|
+
// consumers. Moved verbatim out of src/lsp/child-process.js
|
|
18
|
+
// (subsystem-seam-hygiene, D1); npm install POLICY stays in src/lsp/.
|
|
13
19
|
import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
14
20
|
import { homedir } from 'node:os'
|
|
15
21
|
import { dirname, join, join as nodeJoin } from 'node:path'
|
|
@@ -77,13 +83,22 @@ export function extraBinDirectories(env = process.env, platform = process.platfo
|
|
|
77
83
|
* User-writable npm global prefix for panel installs: `npm --prefix <dir>
|
|
78
84
|
* install -g …` lands servers here instead of a possibly root-owned global
|
|
79
85
|
* prefix, and this directory is already part of the extended scan.
|
|
86
|
+
*
|
|
87
|
+
* `create` is a test seam: the win32 layout assertion must be runnable on any
|
|
88
|
+
* host WITHOUT materializing a literal `C:\Users\...` directory in the
|
|
89
|
+
* process CWD (the artifact that used to pollute the package directory after
|
|
90
|
+
* every unit run). Production callers keep the default (create the prefix).
|
|
91
|
+
* @param {Record<string, string | undefined>} [env]
|
|
92
|
+
* @param {string} [platform] - 'win32' | 'darwin' | 'linux' (process.platform values)
|
|
93
|
+
* @param {{ create?: boolean }} [options]
|
|
94
|
+
* @returns {string}
|
|
80
95
|
*/
|
|
81
|
-
export function npmGlobalPrefix(env = process.env, platform = process.platform) {
|
|
96
|
+
export function npmGlobalPrefix(env = process.env, platform = process.platform, options = {}) {
|
|
82
97
|
const home = env.HOME ?? homedir()
|
|
83
98
|
const prefix = platform === 'win32'
|
|
84
99
|
? join(env.APPDATA ?? join(home, 'AppData', 'Roaming'), 'npm')
|
|
85
100
|
: join(home, '.npm-global')
|
|
86
|
-
mkdirSync(prefix, { recursive: true })
|
|
101
|
+
if (options.create !== false) mkdirSync(prefix, { recursive: true })
|
|
87
102
|
return prefix
|
|
88
103
|
}
|
|
89
104
|
|
|
@@ -304,6 +319,7 @@ function expandShimToken(token, text, shimDirectory, join) {
|
|
|
304
319
|
function tokenizeShimLine(line) {
|
|
305
320
|
const tokens = []
|
|
306
321
|
let current = ''
|
|
322
|
+
/** @type {string | null} */
|
|
307
323
|
let quote = null
|
|
308
324
|
let escaped = false
|
|
309
325
|
for (const character of line) {
|
|
@@ -495,6 +511,7 @@ export async function runBounded(subprocess, { argv, timeoutMs, unref = false })
|
|
|
495
511
|
})
|
|
496
512
|
let timedOut = false
|
|
497
513
|
let timer
|
|
514
|
+
/** @type {Promise<void>} */
|
|
498
515
|
const deadline = new Promise((resolve) => {
|
|
499
516
|
timer = setTimeout(() => {
|
|
500
517
|
timedOut = true
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// settings row through ctx.inject so no new package subpath is needed (S19),
|
|
5
5
|
// same pattern as wireLspAdmin / wireNotifyPermissions. The platform is
|
|
6
6
|
// injected for tests.
|
|
7
|
-
import { jsonResponse } from '../
|
|
7
|
+
import { jsonResponse } from '../shared/http-json.js'
|
|
8
8
|
|
|
9
9
|
const ENV_PATH = '/api/weir-settings/env'
|
|
10
10
|
|
package/src/settings/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// config contains only the values the user actually set, so the settings
|
|
10
10
|
// service can overlay them over preset-row config without clobbering
|
|
11
11
|
// bundle-shipped defaults (layering: module defaults ← row config ← service).
|
|
12
|
-
import { createWhitelistDefaultsCache, DEFAULT_WHITELIST_PATH } from '../
|
|
12
|
+
import { createWhitelistDefaultsCache, DEFAULT_WHITELIST_PATH } from '../delegate/whitelist-defaults.js'
|
|
13
13
|
import { configValue } from './volatile.js'
|
|
14
14
|
import { computeSections } from './sections.js'
|
|
15
15
|
import { wireLspAdmin } from '../lsp/admin.js'
|
package/src/settings/sections.js
CHANGED
|
@@ -149,60 +149,6 @@ export const FIELD_DEFAULTS = Object.freeze({
|
|
|
149
149
|
blackboardWriteTokenTtlMinutes: 60,
|
|
150
150
|
})
|
|
151
151
|
|
|
152
|
-
/** Resolved Edit Lock retention policy. The keys are declared above; this is the
|
|
153
|
-
* ONE place that turns them into the values the lifecycle consumes, so defaults
|
|
154
|
-
* and validation cannot drift apart. */
|
|
155
|
-
export const EDIT_LOCK_DEFAULTS = Object.freeze({
|
|
156
|
-
holdDefaultMinutes: 30,
|
|
157
|
-
holdSingleMaxMinutes: 30,
|
|
158
|
-
holdCumulativeMaxMinutes: 120,
|
|
159
|
-
nudgeAttempts: 2,
|
|
160
|
-
nudgeFallback: 'release',
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
/** Overlay one Edit Lock section onto the defaults. A key the user never set
|
|
164
|
-
* falls back to its default; a key that is present but unusable (alone or in
|
|
165
|
-
* combination) throws with the flat settings key named, because silently clamping
|
|
166
|
-
* a retention cap would change how long other sessions stay blocked from those
|
|
167
|
-
* files. The Edit Lock composition refuses retention requests with that error and
|
|
168
|
-
* runs every other path (settling, status, release, stop, unlock) on defaults.
|
|
169
|
-
* @param {any} section */
|
|
170
|
-
export function editLockLimits(section) {
|
|
171
|
-
const value = section && typeof section === 'object' ? section : {}
|
|
172
|
-
/** @type {{holdDefaultMinutes: number, holdSingleMaxMinutes: number, holdCumulativeMaxMinutes: number, nudgeAttempts: number, nudgeFallback: 'release'|'abnormal'}} */
|
|
173
|
-
const limits = { ...EDIT_LOCK_DEFAULTS }
|
|
174
|
-
/** @param {'holdDefaultMinutes'|'holdSingleMaxMinutes'|'holdCumulativeMaxMinutes'} key @param {string} name */
|
|
175
|
-
const take = (key, name) => {
|
|
176
|
-
const input = value[key]
|
|
177
|
-
if (input === undefined) return
|
|
178
|
-
if (typeof input !== 'number' || !Number.isFinite(input) || input <= 0) {
|
|
179
|
-
throw new Error(`weir-settings: ${name} must be a positive number`)
|
|
180
|
-
}
|
|
181
|
-
limits[key] = input
|
|
182
|
-
}
|
|
183
|
-
take('holdDefaultMinutes', 'editLockHoldDefaultMinutes')
|
|
184
|
-
take('holdSingleMaxMinutes', 'editLockHoldSingleMaxMinutes')
|
|
185
|
-
take('holdCumulativeMaxMinutes', 'editLockHoldCumulativeMaxMinutes')
|
|
186
|
-
if (limits.holdDefaultMinutes > limits.holdSingleMaxMinutes) {
|
|
187
|
-
throw new Error('weir-settings: editLockHoldDefaultMinutes must not exceed editLockHoldSingleMaxMinutes')
|
|
188
|
-
}
|
|
189
|
-
if (limits.holdCumulativeMaxMinutes < limits.holdSingleMaxMinutes) {
|
|
190
|
-
throw new Error('weir-settings: editLockHoldCumulativeMaxMinutes must be at least editLockHoldSingleMaxMinutes')
|
|
191
|
-
}
|
|
192
|
-
if (value.nudgeAttempts !== undefined) {
|
|
193
|
-
if (!Number.isSafeInteger(value.nudgeAttempts) || value.nudgeAttempts < 0) {
|
|
194
|
-
throw new Error('weir-settings: editLockNudgeAttempts must be a non-negative integer')
|
|
195
|
-
}
|
|
196
|
-
limits.nudgeAttempts = value.nudgeAttempts
|
|
197
|
-
}
|
|
198
|
-
if (value.nudgeFallback !== undefined) {
|
|
199
|
-
if (value.nudgeFallback !== 'release' && value.nudgeFallback !== 'abnormal') {
|
|
200
|
-
throw new Error('weir-settings: editLockNudgeFallback must be "release" or "abnormal"')
|
|
201
|
-
}
|
|
202
|
-
limits.nudgeFallback = value.nudgeFallback
|
|
203
|
-
}
|
|
204
|
-
return Object.freeze(limits)
|
|
205
|
-
}
|
|
206
152
|
const zs = z
|
|
207
153
|
|
|
208
154
|
function fieldSchema(type) {
|
package/src/shared/audit.js
CHANGED
|
@@ -35,10 +35,19 @@ export const AUDIT_TYPES = Object.freeze({
|
|
|
35
35
|
// identity, feed census and provider status, never skill names; the receipt
|
|
36
36
|
// itself stays the only effective-state authority.
|
|
37
37
|
capabilityRead: 'capability-read',
|
|
38
|
+
// The pending model-facing capability notice (durable-pending-notice D7):
|
|
39
|
+
// queue / degrade / deliver / clear outcomes of the durable notice unit.
|
|
40
|
+
// Counts only, never names — the same rule as capability-read above; the
|
|
41
|
+
// accepted selection record stays the only effective-state authority.
|
|
42
|
+
capabilityNotice: 'capability-notice',
|
|
38
43
|
// Session blackboard release facts (design D3): one record per key release
|
|
39
44
|
// that had waiting subscribers; the emission lives in the blackboard plugin
|
|
40
45
|
// and never touches a session log.
|
|
41
46
|
blackboard: 'blackboard',
|
|
47
|
+
// Child-composition spawn facts (DSH 0.2.1 delegate shim): one record per
|
|
48
|
+
// spawn carrying the composition the child's creation window applies, so a
|
|
49
|
+
// host restart can replay it — the shim itself is runtime-only state.
|
|
50
|
+
delegate: 'delegate',
|
|
42
51
|
})
|
|
43
52
|
/**
|
|
44
53
|
* Known dynamic sub-event kinds: `<type>/<kind>` events emitted beside a base
|
|
@@ -53,6 +62,9 @@ export const AUDIT_SUBTYPES = Object.freeze({
|
|
|
53
62
|
// family, plus reconciliation reports.
|
|
54
63
|
worktree: Object.freeze(['open', 'setup', 'bind', 'checked', 'check', 'invalidate', 'ask', 'decline', 'conflict', 'land', 'cleanup', 'abandon', 'reconcile', 'watch']),
|
|
55
64
|
blackboard: Object.freeze(['released']),
|
|
65
|
+
// Child-composition spawn facts: one record per spawn, replayed on cold
|
|
66
|
+
// resume from this same channel (audit only — never a session log).
|
|
67
|
+
delegate: Object.freeze(['spawn']),
|
|
56
68
|
})
|
|
57
69
|
|
|
58
70
|
/**
|
|
@@ -59,7 +59,9 @@ export const CHILD_DENY_TOOLS = [
|
|
|
59
59
|
* returned unchanged — allow semantics stay untouched. Deny-list or absent
|
|
60
60
|
* filters get CHILD_DENY_TOOLS merged in, deduped. Same merge pattern as
|
|
61
61
|
* supervisedToolFilter in src/delegate/spawn-adapter.js, applied AFTER the
|
|
62
|
-
* lane transform at the single
|
|
62
|
+
* lane transform at the single activation-assembly point, and re-applied by
|
|
63
|
+
* the DSH 0.2.1 creation-window shim (src/delegate/child-composition.js) with
|
|
64
|
+
* the CHILD scope's restrictable names before tools.restrict().
|
|
63
65
|
*
|
|
64
66
|
* DSH's tools.restrict() REJECTS deny names the composition never registered
|
|
65
67
|
* (e.g. ask_user_question/present/edit_lock_* are absent from a headless
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Generic HTTP JSON helpers for the host-side admin endpoints: Response
|
|
2
|
+
// shaping and request-body reading shared by the lsp, settings, and notify
|
|
3
|
+
// admin modules. Sunk verbatim out of src/lsp/admin.js
|
|
4
|
+
// (subsystem-seam-hygiene, D2); the LSP-specific error namespace
|
|
5
|
+
// (errorResponse) and endpoint assembly (wireLspAdmin) stay in src/lsp/.
|
|
6
|
+
|
|
7
|
+
export function jsonResponse(payload, status = 200) {
|
|
8
|
+
return new Response(JSON.stringify(payload), {
|
|
9
|
+
status,
|
|
10
|
+
headers: { 'content-type': 'application/json' },
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Read the JSON body of a connection request (Request-like or stream). */
|
|
15
|
+
export async function readJsonBody(request) {
|
|
16
|
+
try {
|
|
17
|
+
if (typeof request?.json === 'function') return await request.json()
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const chunks = []
|
|
23
|
+
for await (const chunk of request?.body ?? []) chunks.push(Buffer.from(chunk))
|
|
24
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
25
|
+
} catch {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,9 +1,39 @@
|
|
|
1
|
-
//
|
|
2
|
-
// child
|
|
3
|
-
//
|
|
1
|
+
// Terminal evidence — the single definition site for what proves a supervised
|
|
2
|
+
// child reached a terminal state: the STATUS/REPORT parser, the terminal-status
|
|
3
|
+
// predicate (its JSDoc carries the one blocked-vs-terminal rule every consumer
|
|
4
|
+
// refers to), and the two best-effort cold readers (the bounded audit-JSONL
|
|
5
|
+
// tail; a child log's final assistant text). Delegate supervision and worktree
|
|
6
|
+
// zombie-lane reconciliation both import from here, so neither subsystem
|
|
7
|
+
// reaches into the other for this concept.
|
|
8
|
+
//
|
|
9
|
+
// Load-bearing tolerance semantics (zombie-lane safety): readAuditTail's 256KB
|
|
10
|
+
// bounded window and error tolerance (missing/unreadable → [], a partial first
|
|
11
|
+
// line dropped, malformed lines skipped) make an aged-out fact degrade to "no
|
|
12
|
+
// evidence", never to a wrong release. Keep them byte-exact.
|
|
13
|
+
//
|
|
14
|
+
// Ports & adapters: the readers take their services as parameters (never ctx).
|
|
15
|
+
// node: imports are the src/shared precedent (audit.js);
|
|
16
|
+
// the node:-free constraint guards the delegate composition root, not shared.
|
|
4
17
|
import { openSync, readSync, closeSync, statSync } from 'node:fs'
|
|
5
18
|
import { join } from 'node:path'
|
|
6
|
-
import { contentText } from '
|
|
19
|
+
import { contentText } from './content-text.js'
|
|
20
|
+
|
|
21
|
+
/** Parse a child's final assistant text into a terminal status, or null. */
|
|
22
|
+
export function parseTerminalStatus(text) {
|
|
23
|
+
if (typeof text !== 'string' || text.length === 0) return null
|
|
24
|
+
const match = /^STATUS:[ \t]*(completed|blocked)[ \t]*$/m.exec(text)
|
|
25
|
+
if (!match) return null
|
|
26
|
+
const reportMatch = /^REPORT:[ \t]*([\s\S]*)$/m.exec(text.slice(match.index))
|
|
27
|
+
return { status: match[1], report: (reportMatch?.[1] ?? '').trim() }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Single exit for the terminal-status predicate: statuses that count toward
|
|
32
|
+
* group completion (blocked waits for a resume, so it is NOT terminal here).
|
|
33
|
+
*/
|
|
34
|
+
export function isTerminalStatus(status) {
|
|
35
|
+
return status === 'completed' || status === 'terminated'
|
|
36
|
+
}
|
|
7
37
|
|
|
8
38
|
/** Audit JSONL location for one session (cold-safe channel; may not exist). */
|
|
9
39
|
export function auditFilePathOf(session) {
|
package/src/worktree/errors.js
CHANGED
|
@@ -18,6 +18,9 @@ export const WORKTREE_CODES = Object.freeze({
|
|
|
18
18
|
UNKNOWN_LANE: 'UNKNOWN_LANE',
|
|
19
19
|
ILLEGAL_TRANSITION: 'ILLEGAL_TRANSITION',
|
|
20
20
|
LANE_NOT_DISPATCHABLE: 'LANE_NOT_DISPATCHABLE',
|
|
21
|
+
// The binding commit's reservation is gone (force-reclaim / reconcile
|
|
22
|
+
// --rebuild) or the lane left `working` before the commit landed.
|
|
23
|
+
LANE_RESERVATION_LOST: 'LANE_RESERVATION_LOST',
|
|
21
24
|
LANE_BUSY: 'LANE_BUSY',
|
|
22
25
|
NOT_LANDABLE: 'NOT_LANDABLE',
|
|
23
26
|
STALE_LANDABLE: 'STALE_LANDABLE',
|
package/src/worktree/index.js
CHANGED
|
@@ -19,8 +19,7 @@ import { DEFAULT_ROOT } from './rules.js'
|
|
|
19
19
|
import { LANES_CONTEXT_NAME, LANES_CONTEXT_ORDER, LANES_SECTION_NAME, LANES_SECTION_TEXT, LANES_VARIABLE_NAME } from './prompts.js'
|
|
20
20
|
import { DEFAULT_WATCH_TIMEOUT_MINUTES } from './watches.js'
|
|
21
21
|
import { APPROVE_MODES, WORKTREE_PROJECTION_KEY, foldWorktreeState, initialWorktreeState, worktreeStateSchema, worktreeView, worktreeViewSchema } from './projection.js'
|
|
22
|
-
import { readAuditTail, readChildFinalText } from '../
|
|
23
|
-
import { isTerminalStatus, parseTerminalStatus } from '../delegate/group-coordinator.js'
|
|
22
|
+
import { isTerminalStatus, parseTerminalStatus, readAuditTail, readChildFinalText } from '../shared/terminal-evidence.js'
|
|
24
23
|
|
|
25
24
|
const name = 'weir-worktree'
|
|
26
25
|
// subprocess is a host-plane service every composition mounts; it is a hard
|