weir-harness 1.2.1 → 1.2.3
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/package.json +5 -2
- package/src/capabilities/skill-office-adapter.js +7 -1
- package/src/creative-guide/guide.js +1 -1
- package/src/delegate/child-composition.js +258 -0
- package/src/delegate/group-coordinator.js +24 -10
- package/src/delegate/index.js +17 -0
- package/src/delegate/rehydrate.js +22 -3
- package/src/delegate/spawn-adapter.js +214 -100
- package/src/delegate/status-tool.js +9 -6
- package/src/delegate/supervision-mount.js +30 -2
- package/src/delegate/tool.js +3 -1
- package/src/intent-gate/matcher.js +27 -0
- package/src/lsp/admin.js +24 -13
- package/src/lsp/index.js +5 -2
- package/src/lsp/registry.js +83 -17
- package/src/notify/commands.js +15 -2
- package/src/notify/index.js +48 -10
- package/src/notify/web-channel.js +26 -3
- package/src/process/index.js +22 -2
- package/src/settings/sections.js +27 -4
- package/src/shared/audit.js +7 -0
- package/src/shared/child-scope.js +3 -1
package/cordis.patch.yml
CHANGED
|
@@ -104,7 +104,6 @@
|
|
|
104
104
|
You are the Orchestrator, the user's single point of collaboration, powered by the {{model}} model.
|
|
105
105
|
You understand intent, decompose work, delegate to specialist sub-agents, choose parallel fan-out
|
|
106
106
|
or sequenced waiting, verify evidence, and report results.
|
|
107
|
-
suffix: Your working directory is {{cwd}}.
|
|
108
107
|
- id: weir-core
|
|
109
108
|
name: 'weir-harness/core'
|
|
110
109
|
- id: agent-instructions
|
|
@@ -294,7 +293,6 @@
|
|
|
294
293
|
You are the Orchestrator, the user's single point of collaboration, powered by the {{model}} model.
|
|
295
294
|
You understand intent, decompose work, delegate to specialist sub-agents, choose parallel fan-out
|
|
296
295
|
or sequenced waiting, verify evidence, and report results.
|
|
297
|
-
suffix: Your working directory is {{cwd}}.
|
|
298
296
|
- id: weir-core
|
|
299
297
|
name: 'weir-harness/core'
|
|
300
298
|
# Creative-only: how to use the fused tooling (inspection order,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weir-harness",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Weir \u2014 orchestrator-centric DSH agent preset: intent gate, category delegation, todo driver, pull-based background jobs, auto-compaction, hashline edit.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,8 +28,11 @@
|
|
|
28
28
|
"whitelist-defaults.json",
|
|
29
29
|
"icon.svg"
|
|
30
30
|
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"dsh": ">=0.2.1-rc.1 <0.3.0"
|
|
33
|
+
},
|
|
31
34
|
"peerDependencies": {
|
|
32
|
-
"@deepseek-ai/dsh": ">=0.2.
|
|
35
|
+
"@deepseek-ai/dsh": ">=0.2.1-rc.1 <0.3.0"
|
|
33
36
|
},
|
|
34
37
|
"exports": {
|
|
35
38
|
"./package.json": "./package.json",
|
|
@@ -30,7 +30,13 @@ export function createOfficeAdapter(skills, machineId) {
|
|
|
30
30
|
const observation = Array.isArray(output) ? { candidates: output, complete: true } : output
|
|
31
31
|
const candidates = []
|
|
32
32
|
for (const raw of observation.candidates) {
|
|
33
|
-
|
|
33
|
+
// DSH 0.2.1's office provider lists MORE bundled siblings than the three
|
|
34
|
+
// Weir composes (`office`, `office-render`, `office-recalculate`,
|
|
35
|
+
// `office-export-pdf`). This adapter only owns OFFICE_NAMES: a foreign
|
|
36
|
+
// sibling is skipped, never an error — while a candidate that CLAIMS one
|
|
37
|
+
// of our names with an unexpected shape still fails closed below.
|
|
38
|
+
if (!OFFICE_NAMES.includes(raw.name)) continue
|
|
39
|
+
if (raw.provider !== 'dsh-office' || raw.source !== 'bundled' || typeof raw.locator !== 'string') {
|
|
34
40
|
throw new Error('Unsupported Office provider candidate')
|
|
35
41
|
}
|
|
36
42
|
const path = await realpath(raw.locator)
|
|
@@ -20,7 +20,7 @@ Call \`cordis_inspect_list\` first to enumerate the Host and Client Inspect Prov
|
|
|
20
20
|
|
|
21
21
|
## Manage plugins deliberately
|
|
22
22
|
|
|
23
|
-
\`plugin_manager\` lists, installs, enables, disables, and removes profile bundles and plugins. List first to obtain exact identifiers. Changes affect every session in the profile and apply immediately in a live profile. To re-apply a changed bundle, disable then enable it (\`set_bundle\`) — repeating \`install_bundle\` for
|
|
23
|
+
\`plugin_manager\` lists, installs, enables, disables, and removes profile bundles and plugins. List first to obtain exact identifiers. Changes affect every session in the profile and apply immediately in a live profile. To re-apply a changed bundle, disable then enable it (\`set_bundle\`) — repeating \`install_bundle\` for a known, already-installed bundle is refused up front as \`already-installed\`. A version exemption risks crashes and data loss: warn the user and obtain explicit permission for the exact plugin and runtime versions before granting one.
|
|
24
24
|
|
|
25
25
|
## Load the development skills
|
|
26
26
|
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// Child composition shim: the creation-window replacement for the
|
|
2
|
+
// request-level `persona`/`toolFilter` fields DSH 0.2.1 removed from
|
|
3
|
+
// `subagents.startActivation`. The delegate module reserves the childId before
|
|
4
|
+
// spawning, registers the composition here, and the `agent/created` listener
|
|
5
|
+
// applies it inside the child's creation window — before the subagent runtime
|
|
6
|
+
// delivers the initial prompt (startLocal → materialize → agents.create →
|
|
7
|
+
// setup → announce → submitMaterialized, verified against 0.2.1).
|
|
8
|
+
//
|
|
9
|
+
// Timing is race-free by construction and fail-closed: a fresh spawn waits for
|
|
10
|
+
// the recorded verdict and tears the child down when the composition did not
|
|
11
|
+
// land, so no child ever runs with the orchestrator-tool deny list missing.
|
|
12
|
+
//
|
|
13
|
+
// Cold resume: the applied sections/restrictions are runtime state of one
|
|
14
|
+
// agent incarnation, so a host restart loses them. Every spawn therefore also
|
|
15
|
+
// writes a `delegate/spawn` fact on the shared audit channel (the cold-safe
|
|
16
|
+
// channel — never a custom session event type, S13) and this listener replays
|
|
17
|
+
// the composition for a resumed child whose fact is still in the audit tail.
|
|
18
|
+
// Missing fact: persona/allow-lists degrade fail-open (same posture as the
|
|
19
|
+
// supervision rebuild), while the orchestrator-tool deny floor is still applied
|
|
20
|
+
// whenever the child is attributable to a weir-managed parent session.
|
|
21
|
+
import { AUDIT_TYPES, AUDIT_SUBTYPES } from '../shared/audit.js'
|
|
22
|
+
import { auditFilePathOf, readAuditTail } from '../shared/terminal-evidence.js'
|
|
23
|
+
import { CHILD_DENY_TOOLS, childToolFilter } from '../shared/child-scope.js'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The spawn-fact audit type AS PASSED TO the audit sink: UNPREFIXED, because
|
|
27
|
+
* createAudit prepends `weir/` itself (see src/shared/audit.js). Mirrors the
|
|
28
|
+
* AUDIT_TYPES/AUDIT_SUBTYPES registry; passing a pre-prefixed value here is
|
|
29
|
+
* what produced `weir/weir/delegate/spawn` records in the wild.
|
|
30
|
+
*/
|
|
31
|
+
export const SPAWN_FACT_TYPE = `${AUDIT_TYPES.delegate}/${AUDIT_SUBTYPES.delegate[0]}`
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The spawn-fact type AS STORED in the audit channel (JSONL record + cordis
|
|
35
|
+
* event name): the sink's single `weir/` prefix is part of the record, so every
|
|
36
|
+
* comparison against a stored `record.type` must use THIS constant.
|
|
37
|
+
*/
|
|
38
|
+
export const SPAWN_FACT_RECORD_TYPE = `weir/${SPAWN_FACT_TYPE}`
|
|
39
|
+
|
|
40
|
+
/** Deployment persona prefix section (dsh-system-prompt / dsh-persona contract). */
|
|
41
|
+
const PERSONA_PREFIX_SECTION = 'deployment:persona-prefix'
|
|
42
|
+
const PERSONA_PREFIX_ORDER = 'DEPLOYMENT_PERSONA_PREFIX'
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {object} SpawnComposition
|
|
46
|
+
* @property {string} [childId]
|
|
47
|
+
* @property {string} [lane] - one of 'one-shot' | 'continuable' | 'supervised'
|
|
48
|
+
* @property {string} [label]
|
|
49
|
+
* @property {string} persona - full persona text applied as the child's prefix section
|
|
50
|
+
* @property {{ allow?: string[], deny?: string[] }} [toolFilter] - requested child tool filter
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mount the composition listener on one plugin ctx.
|
|
55
|
+
* @param {object} deps
|
|
56
|
+
* @param {any} deps.ctx - plugin context (on / tools)
|
|
57
|
+
* @param {(session: object | null, type: string, data?: unknown) => void} deps.audit - createAudit sink
|
|
58
|
+
* @param {{ warn?: (message: string) => void }} [deps.logger]
|
|
59
|
+
* @param {() => Set<string> | undefined} [deps.restrictableNames] - fallback composition view
|
|
60
|
+
* @param {(fact: { session?: object | null, childId: string, lane?: string }) => void} [deps.onSpawn] - live spawn-fact sink (supervision catalog cross-check)
|
|
61
|
+
* @returns {{ beginSpawn: (composition: SpawnComposition & { session?: object | null }) => { outcome: () => { ok: boolean, error?: Error }, forget: () => void }, dispose: () => void }}
|
|
62
|
+
*/
|
|
63
|
+
export function createChildComposition({ ctx, audit, logger, restrictableNames, onSpawn }) {
|
|
64
|
+
/** @type {Map<string, { session: object | null, childId: string, lane: string, label: string, persona: string, toolFilter?: object, applied: boolean, error?: Error }>} */
|
|
65
|
+
const pending = new Map()
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reserve one composition and write its cold-safe spawn fact. Called
|
|
69
|
+
* synchronously BEFORE startActivation: the child's creation window runs
|
|
70
|
+
* inside that call, so the entry is always present when the listener fires.
|
|
71
|
+
* @param {SpawnComposition & { session?: object | null }} composition
|
|
72
|
+
*/
|
|
73
|
+
function beginSpawn(composition) {
|
|
74
|
+
const entry = {
|
|
75
|
+
session: composition.session ?? null,
|
|
76
|
+
childId: composition.childId,
|
|
77
|
+
lane: composition.lane ?? 'one-shot',
|
|
78
|
+
label: composition.label ?? '',
|
|
79
|
+
persona: composition.persona ?? '',
|
|
80
|
+
toolFilter: composition.toolFilter,
|
|
81
|
+
applied: false,
|
|
82
|
+
error: undefined,
|
|
83
|
+
}
|
|
84
|
+
pending.set(entry.childId, entry)
|
|
85
|
+
// Audit mirrors the fact to <cwd>/.weir/audit.jsonl (cold-safe replay) and
|
|
86
|
+
// emits it in-process. Never throws.
|
|
87
|
+
const fact = {
|
|
88
|
+
childId: entry.childId,
|
|
89
|
+
lane: entry.lane,
|
|
90
|
+
label: entry.label,
|
|
91
|
+
persona: entry.persona,
|
|
92
|
+
...(entry.toolFilter ? { toolFilter: entry.toolFilter } : {}),
|
|
93
|
+
}
|
|
94
|
+
audit(entry.session, SPAWN_FACT_TYPE, fact)
|
|
95
|
+
// Live consumers (the supervision catalog cross-check) see the same fact
|
|
96
|
+
// without re-reading the file.
|
|
97
|
+
try {
|
|
98
|
+
onSpawn?.({ session: entry.session, childId: entry.childId, lane: entry.lane })
|
|
99
|
+
} catch {
|
|
100
|
+
// a live sink failure must never break the spawn
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
outcome: () => (entry.applied
|
|
104
|
+
? { ok: true }
|
|
105
|
+
: {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: entry.error ?? new Error(`child "${entry.childId}": the composition was not applied inside the creation window`),
|
|
108
|
+
}),
|
|
109
|
+
forget: () => {
|
|
110
|
+
if (pending.get(entry.childId) === entry) pending.delete(entry.childId)
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Apply a composition to one live child agent (throws on any failure). */
|
|
116
|
+
function apply(agent, composition) {
|
|
117
|
+
const systemPrompt = agent?.ctx?.systemPrompt
|
|
118
|
+
const tools = agent?.ctx?.tools
|
|
119
|
+
if (typeof composition.persona === 'string' && composition.persona.length > 0) {
|
|
120
|
+
if (typeof systemPrompt?.section !== 'function') {
|
|
121
|
+
throw new Error('the child agent exposes no systemPrompt service')
|
|
122
|
+
}
|
|
123
|
+
// Same section name the deployment/preset persona uses, so the child's
|
|
124
|
+
// own layer shadows it instead of duplicating a prompt paragraph.
|
|
125
|
+
systemPrompt.section({
|
|
126
|
+
name: PERSONA_PREFIX_SECTION,
|
|
127
|
+
order: systemPrompt.getSectionOrder(PERSONA_PREFIX_ORDER),
|
|
128
|
+
text: composition.persona,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
const filter = composition.toolFilter
|
|
132
|
+
if (!filter) return
|
|
133
|
+
if (typeof tools?.restrict !== 'function') {
|
|
134
|
+
// An allow filter with no restriction channel would silently widen the
|
|
135
|
+
// child surface; the deny list is the safety floor. Fail closed.
|
|
136
|
+
throw new Error('the child agent exposes no tools service')
|
|
137
|
+
}
|
|
138
|
+
// tools.restrict() rejects unknown deny/allow names, so the deny list is
|
|
139
|
+
// merged against the CHILD scope's restrictable set (the composition's own
|
|
140
|
+
// view is only a fallback). childToolFilter keeps the existing merge
|
|
141
|
+
// semantics: allow-list filters pass through, every other filter gets
|
|
142
|
+
// CHILD_DENY_TOOLS intersected in, so the orchestrator-tool deny never
|
|
143
|
+
// degrades with the requested filter. The final deny list is then
|
|
144
|
+
// intersected with that same child set: a name the child scope never
|
|
145
|
+
// registered needs no deny (it is absent either way) and naming it would
|
|
146
|
+
// make restrict() throw.
|
|
147
|
+
const known = restrictableNamesOf(agent) ?? restrictableNames?.()
|
|
148
|
+
const merged = childToolFilter(filter, known)
|
|
149
|
+
const deny = Array.isArray(merged.deny) && known ? merged.deny.filter((name) => known.has(name)) : merged.deny
|
|
150
|
+
const effective = deny === merged.deny ? merged : { ...merged, deny }
|
|
151
|
+
const hasAllow = effective.allow !== undefined
|
|
152
|
+
const hasDeny = Array.isArray(effective.deny) && effective.deny.length > 0
|
|
153
|
+
if (!hasAllow && !hasDeny) return
|
|
154
|
+
tools.restrict(effective)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Resolve the child scope's restrictable tool names (undefined when unavailable). */
|
|
158
|
+
function restrictableNamesOf(agent) {
|
|
159
|
+
try {
|
|
160
|
+
const view = agent?.ctx?.tools?.view?.(agent)
|
|
161
|
+
if (view?.restrictableNames instanceof Set) return view.restrictableNames
|
|
162
|
+
} catch {
|
|
163
|
+
// view() failures fall back to the composition view
|
|
164
|
+
}
|
|
165
|
+
return undefined
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Replay the composition of a cold-resumed child from the audit tail.
|
|
170
|
+
* @param {any} agent
|
|
171
|
+
* @param {object} header - the child session header
|
|
172
|
+
*/
|
|
173
|
+
function replayFromAudit(agent, header) {
|
|
174
|
+
const file = auditFilePathOf(agent.session)
|
|
175
|
+
const records = readAuditTail(file)
|
|
176
|
+
if (records.length === 0) return
|
|
177
|
+
let fact
|
|
178
|
+
let parentManaged = false
|
|
179
|
+
for (const record of records) {
|
|
180
|
+
if (!record || typeof record !== 'object') continue
|
|
181
|
+
if (record.session !== header.parentSession) continue
|
|
182
|
+
if (typeof record.type !== 'string' || !record.type.startsWith('weir/')) continue
|
|
183
|
+
parentManaged = true
|
|
184
|
+
if (record.type === SPAWN_FACT_RECORD_TYPE && record.data?.childId === agent.id) fact = record.data
|
|
185
|
+
}
|
|
186
|
+
if (!parentManaged) return // not a weir-managed parent session: not our child
|
|
187
|
+
if (!fact) {
|
|
188
|
+
// Degraded: the spawn fact aged out of the tail (or the audit channel was
|
|
189
|
+
// unavailable). Persona and allow-lists stay unapplied, but the
|
|
190
|
+
// orchestrator-tool deny floor never does.
|
|
191
|
+
logger?.warn?.(`weir-delegate: child "${agent.id}" has no delegate/spawn fact in the audit tail — replaying the static orchestrator-tool deny floor only`)
|
|
192
|
+
applyDenyFloor(agent)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
apply(agent, fact)
|
|
197
|
+
} catch (error) {
|
|
198
|
+
logger?.warn?.(`weir-delegate: child "${agent.id}" composition replay failed (fail-open): ${String(error?.message ?? error)}`)
|
|
199
|
+
applyDenyFloor(agent)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Apply only the static orchestrator-tool deny floor (best-effort, warns). */
|
|
204
|
+
function applyDenyFloor(agent) {
|
|
205
|
+
try {
|
|
206
|
+
const known = restrictableNamesOf(agent) ?? restrictableNames?.()
|
|
207
|
+
const deny = known ? CHILD_DENY_TOOLS.filter((name) => known.has(name)) : [...CHILD_DENY_TOOLS]
|
|
208
|
+
if (deny.length === 0) return
|
|
209
|
+
agent.ctx.tools.restrict({ deny })
|
|
210
|
+
} catch (error) {
|
|
211
|
+
logger?.warn?.(`weir-delegate: child "${agent.id}" deny-floor application failed: ${String(error?.message ?? error)}`)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* `agent/created` listener. A failure on the fresh-spawn path is thrown so
|
|
217
|
+
* the spawn call itself rejects (the runtime disposes the unpublished child
|
|
218
|
+
* after a rejected creation announcement); the recorded verdict lets the
|
|
219
|
+
* spawn's own fail-closed branch also catch a listener that never ran.
|
|
220
|
+
*/
|
|
221
|
+
function onAgentCreated(payload) {
|
|
222
|
+
const agent = payload?.agent
|
|
223
|
+
const header = agent?.session?.header
|
|
224
|
+
if (!header) return
|
|
225
|
+
if ((header.delegationDepth ?? 0) < 1) return
|
|
226
|
+
const childId = agent.id
|
|
227
|
+
const entry = pending.get(childId)
|
|
228
|
+
if (entry) {
|
|
229
|
+
pending.delete(childId)
|
|
230
|
+
try {
|
|
231
|
+
apply(agent, entry)
|
|
232
|
+
entry.applied = true
|
|
233
|
+
} catch (error) {
|
|
234
|
+
entry.error = compositionError(childId, error)
|
|
235
|
+
throw entry.error
|
|
236
|
+
}
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
replayFromAudit(agent, header)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const off = ctx.on('agent/created', onAgentCreated)
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
beginSpawn,
|
|
246
|
+
dispose: () => {
|
|
247
|
+
pending.clear()
|
|
248
|
+
off?.()
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Wrap one composition failure with the delegate-facing cause. */
|
|
254
|
+
function compositionError(childId, error) {
|
|
255
|
+
const wrapped = new Error(`delegate: failed to apply the child composition for "${childId}" — ${String(error?.message ?? error)}`)
|
|
256
|
+
wrapped.cause = error
|
|
257
|
+
return wrapped
|
|
258
|
+
}
|
|
@@ -47,18 +47,31 @@ export function createGroupRecord(name, { sealed = false, settled = false, membe
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* Single exit for the untracked-catalog predicate:
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* Single exit for the untracked-catalog predicate: catalog children the
|
|
51
|
+
* supervision registry does not track AND no one-shot/continuable spawn fact
|
|
52
|
+
* claims. The row shape is { id, label, mode } (mode is passed through
|
|
53
|
+
* verbatim: DSH 0.2.1 reports every local child as 'continuable' and may also
|
|
54
|
+
* report 'external', so the catalog mode no longer discriminates lanes — the
|
|
55
|
+
* durable spawn facts do).
|
|
56
|
+
* `isTracked(id)` is the caller's registry lookup. `spawnFactOf(id)` returns
|
|
57
|
+
* the durable delegate/spawn fact for a weir-spawned child (lane included):
|
|
58
|
+
* one-shot and plain-continuable children must never be resurrected as
|
|
59
|
+
* supervision orphans, while a supervised child normally IS tracked and a
|
|
60
|
+
* supervised child missing from the fact chain stays visible as an orphan
|
|
61
|
+
* (never hidden). Only a fact-less orphan (not spawned by this weir build)
|
|
62
|
+
* keeps the promoted/recovered semantics.
|
|
63
|
+
* @param {object[]} entries - DSH catalog children
|
|
64
|
+
* @param {(id: string) => boolean} isTracked
|
|
65
|
+
* @param {(id: string) => { lane?: string } | undefined} [spawnFactOf]
|
|
53
66
|
*/
|
|
54
|
-
export function untrackedCatalogEntries(entries, isTracked) {
|
|
67
|
+
export function untrackedCatalogEntries(entries, isTracked, spawnFactOf) {
|
|
55
68
|
const untracked = []
|
|
56
69
|
for (const entry of entries ?? []) {
|
|
57
70
|
if (!entry || typeof entry !== 'object') continue
|
|
58
|
-
const mode = entry.mode === undefined ? 'continuable' : entry.mode
|
|
59
|
-
if (mode !== 'continuable') continue
|
|
60
71
|
if (isTracked(entry.id)) continue
|
|
61
|
-
|
|
72
|
+
const lane = spawnFactOf?.(entry.id)?.lane
|
|
73
|
+
if (lane === 'one-shot' || lane === 'continuable') continue
|
|
74
|
+
untracked.push({ id: entry.id, label: entry.label ?? '', mode: entry.mode })
|
|
62
75
|
}
|
|
63
76
|
return untracked
|
|
64
77
|
}
|
|
@@ -73,7 +86,8 @@ export function untrackedCatalogEntries(entries, isTracked) {
|
|
|
73
86
|
* @param {(delayMs: number, fn: () => void) => void} deps.schedule - delayed retry scheduler
|
|
74
87
|
* @param {(reason: string) => void} [deps.onAudit]
|
|
75
88
|
* @param {(fact: object) => void} [deps.onFact] - structured supervision facts (durability)
|
|
76
|
-
|
|
89
|
+
* @param {(text: string) => void} [deps.notifyParent] - deliver a parent-facing signal (timer-deferred followup)
|
|
90
|
+
* @param {(childId: string) => { lane?: string } | undefined} [deps.spawnFactOf] - durable delegate/spawn fact for one child id (lane decides orphan resurrection; see untrackedCatalogEntries)
|
|
77
91
|
*/
|
|
78
92
|
export function createGroupCoordinator(deps, config = {}) {
|
|
79
93
|
const cfg = { ...DEFAULT_SUPERVISION, ...config }
|
|
@@ -174,9 +188,9 @@ export function createGroupCoordinator(deps, config = {}) {
|
|
|
174
188
|
}
|
|
175
189
|
}
|
|
176
190
|
|
|
177
|
-
/** Catalog cross-check:
|
|
191
|
+
/** Catalog cross-check: catalog children this registry does not track (spawn-fact aware). */
|
|
178
192
|
function untrackedAgainstCatalog(entries) {
|
|
179
|
-
return untrackedCatalogEntries(entries, (id) => children.has(id))
|
|
193
|
+
return untrackedCatalogEntries(entries, (id) => children.has(id), deps.spawnFactOf)
|
|
180
194
|
}
|
|
181
195
|
|
|
182
196
|
function noteAssistantText(childId, text) {
|
package/src/delegate/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { createDelegateTool } from './tool.js'
|
|
|
18
18
|
import { attachWorktreeModeGuard } from './worktree-mode.js'
|
|
19
19
|
import { DOCTRINE_SECTION_ORDER } from '../core/doctrine.js'
|
|
20
20
|
import { isDelegatedChild } from '../shared/child-scope.js'
|
|
21
|
+
import { createChildComposition } from './child-composition.js'
|
|
21
22
|
import {
|
|
22
23
|
DELEGATE_TARGETS_SECTION_NAME,
|
|
23
24
|
DELEGATE_TARGETS_SECTION_ORDER_OFFSET,
|
|
@@ -97,6 +98,20 @@ function apply(ctx, config = {}) {
|
|
|
97
98
|
onAdaptersUpdated: (fn) => ctx.on('llm/adapters-updated', fn),
|
|
98
99
|
})
|
|
99
100
|
|
|
101
|
+
// Creation-window child composition (DSH 0.2.1 removed the request-level
|
|
102
|
+
// persona/toolFilter): the shim applies the reserved spawn's composition in
|
|
103
|
+
// the child's agent/created window and replays it from the audit channel on
|
|
104
|
+
// cold resume. One instance per plugin context; dispose unregisters it.
|
|
105
|
+
const composition = createChildComposition({
|
|
106
|
+
ctx,
|
|
107
|
+
audit,
|
|
108
|
+
logger: ctx.logger,
|
|
109
|
+
restrictableNames: () => ctx.tools.view?.(undefined)?.restrictableNames,
|
|
110
|
+
// Feed the supervision catalog cross-check the same durable fact without a
|
|
111
|
+
// file re-read (spawn-fact lane decides orphan resurrection).
|
|
112
|
+
onSpawn: mount.noteSpawnFact,
|
|
113
|
+
})
|
|
114
|
+
|
|
100
115
|
ctx.tools.register(
|
|
101
116
|
createDelegateTool({
|
|
102
117
|
resolveTarget: targetResolver.resolveTarget,
|
|
@@ -114,6 +129,7 @@ function apply(ctx, config = {}) {
|
|
|
114
129
|
// names, so the child deny list names only tools this composition
|
|
115
130
|
// actually registers (tools.restrict() rejects unknown deny names).
|
|
116
131
|
restrictableNames: () => ctx.tools.view?.(undefined)?.restrictableNames,
|
|
132
|
+
composition,
|
|
117
133
|
}),
|
|
118
134
|
)
|
|
119
135
|
|
|
@@ -165,6 +181,7 @@ function apply(ctx, config = {}) {
|
|
|
165
181
|
return () => {
|
|
166
182
|
offModeGuard()
|
|
167
183
|
mount.dispose()
|
|
184
|
+
composition.dispose()
|
|
168
185
|
}
|
|
169
186
|
}
|
|
170
187
|
|
|
@@ -9,21 +9,37 @@ import { parseTerminalStatus, isTerminalStatus } from '../shared/terminal-eviden
|
|
|
9
9
|
* Rebuild supervision state by replaying structured audit facts and
|
|
10
10
|
* cross-checking the DSH subagent catalog.
|
|
11
11
|
*
|
|
12
|
+
* Two fact channels ride the same audit file: the supervision facts
|
|
13
|
+
* (`weir/supervision/<kind>`) and the child-composition spawn facts
|
|
14
|
+
* (`weir/delegate/spawn`, which carry the lane). The spawn facts decide which
|
|
15
|
+
* catalog children are supervision orphans — DSH 0.2.1 reports every local
|
|
16
|
+
* child as mode 'continuable', so the catalog mode can no longer tell a
|
|
17
|
+
* one-shot child from a supervised one.
|
|
18
|
+
*
|
|
12
19
|
* @param {object} input
|
|
13
20
|
* @param {string} input.parentId - the parent session id (record filter key)
|
|
14
21
|
* @param {object[]} input.records - parsed audit JSONL records (already tail-read; may contain other sessions/types)
|
|
15
22
|
* @param {object[]} input.catalogChildren - DSH catalog entries ({ id, label?, mode? }); best-effort
|
|
16
|
-
* @returns {{ children: object[], groups: object[], untracked: object[], confidence: 'full'|'partial', factsSeen: number }}
|
|
23
|
+
* @returns {{ children: object[], groups: object[], untracked: object[], spawnFacts: object[], confidence: 'full'|'partial', factsSeen: number }}
|
|
17
24
|
*/
|
|
18
25
|
export function rehydrateSupervision({ parentId, records, catalogChildren }) {
|
|
19
26
|
const children = new Map()
|
|
20
27
|
const groups = new Map()
|
|
28
|
+
/** @type {Map<string, { childId: string, lane?: string }>} */
|
|
29
|
+
const spawnFacts = new Map()
|
|
21
30
|
let factsSeen = 0
|
|
22
31
|
|
|
23
32
|
for (const record of records ?? []) {
|
|
24
33
|
if (!record || typeof record !== 'object') continue
|
|
25
34
|
if (record.session !== undefined && record.session !== null && record.session !== parentId) continue
|
|
26
35
|
const type = typeof record.type === 'string' ? record.type : ''
|
|
36
|
+
if (type === 'weir/delegate/spawn') {
|
|
37
|
+
const fact = record.data
|
|
38
|
+
if (fact && typeof fact === 'object' && typeof fact.childId === 'string' && fact.childId.length > 0) {
|
|
39
|
+
spawnFacts.set(fact.childId, { childId: fact.childId, lane: typeof fact.lane === 'string' ? fact.lane : undefined })
|
|
40
|
+
}
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
27
43
|
if (!type.startsWith('weir/supervision/')) continue
|
|
28
44
|
const fact = record.data
|
|
29
45
|
if (!fact || typeof fact !== 'object' || typeof fact.kind !== 'string') continue
|
|
@@ -37,13 +53,16 @@ export function rehydrateSupervision({ parentId, records, catalogChildren }) {
|
|
|
37
53
|
group.settled = group.sealed === true && group.memberIds.length > 0 && terminal
|
|
38
54
|
}
|
|
39
55
|
|
|
40
|
-
// Catalog cross-check:
|
|
41
|
-
|
|
56
|
+
// Catalog cross-check: only fact-less orphans remain untracked (the spawn
|
|
57
|
+
// facts identify the one-shot/continuable children this registry must never
|
|
58
|
+
// resurrect, and keep a supervised child whose fact chain is broken visible).
|
|
59
|
+
const untracked = untrackedCatalogEntries(catalogChildren, (id) => children.has(id), (id) => spawnFacts.get(id))
|
|
42
60
|
|
|
43
61
|
return {
|
|
44
62
|
children: [...children.values()],
|
|
45
63
|
groups: [...groups.values()],
|
|
46
64
|
untracked,
|
|
65
|
+
spawnFacts: [...spawnFacts.values()],
|
|
47
66
|
confidence: factsSeen > 0 ? 'full' : 'partial',
|
|
48
67
|
factsSeen,
|
|
49
68
|
}
|