pantheon-opencode 1.4.3 → 1.5.0-beta.1
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/.pantheon/code-mode/.gitkeep +0 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +11 -0
- package/.pantheon/code-mode/README.md +93 -0
- package/.pantheon/code-mode/checkpoint-session.py +7 -0
- package/.pantheon/code-mode/checkpoint-session.sh +11 -0
- package/.pantheon/code-mode/checkpoint_session.py +481 -0
- package/.pantheon/code-mode/compress-inline.py +450 -0
- package/.pantheon/code-mode/eval-llm-judge.py +255 -0
- package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
- package/.pantheon/code-mode/eval-run.py +197 -0
- package/.pantheon/code-mode/eval-static.py +321 -0
- package/.pantheon/code-mode/example-sync.sh +5 -0
- package/.pantheon/code-mode/session-end-save.py +186 -0
- package/.pantheon/code-mode/session-end-save.sh +18 -0
- package/.pantheon/code-mode/test-allowed.sh +5 -0
- package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
- package/.pantheon/code-mode/test-env.sh +7 -0
- package/.pantheon/code-mode/test-prlimit.sh +9 -0
- package/.pantheon/code-mode/test-timeout.sh +6 -0
- package/README.md +158 -1101
- package/README.pt-BR.md +222 -0
- package/bin/pantheon-init.mjs +30 -11
- package/opencode.json +1 -0
- package/package.json +14 -3
- package/plugin.json +15 -0
- package/scripts/_pantheon_paths.py +48 -7
- package/scripts/ci-validate-yaml.py +7 -2
- package/scripts/code_mode_server.py +33 -2
- package/scripts/doctor.mjs +135 -127
- package/scripts/generate-preset-docs.mjs +127 -66
- package/scripts/generate-prompts.sh +2 -2
- package/scripts/install/cli-ui.mjs +8 -1
- package/scripts/install/config-migration.mjs +401 -0
- package/scripts/install/health-check.mjs +3 -3
- package/scripts/install/model-picker.mjs +72 -23
- package/scripts/install/opencode-version.mjs +47 -0
- package/scripts/install/opencode.mjs +261 -88
- package/scripts/install/plugin.mjs +112 -42
- package/scripts/install/shared.mjs +13 -158
- package/scripts/install-mcp.mjs +44 -232
- package/scripts/mcp_persistence_server.py +344 -15
- package/scripts/mcp_resources_server.py +144 -40
- package/scripts/memory_mcp_server.py +2 -2
- package/scripts/recover-zenodo-deposition.mjs +146 -0
- package/scripts/sync-tui.mjs +32 -7
- package/scripts/test-opencode-v1-v2-sandbox.sh +591 -0
- package/scripts/uninstall.mjs +298 -954
- package/scripts/validate-zenodo-release.mjs +154 -0
- package/scripts/zenodo-artifact.mjs +21 -0
- package/scripts/zenodo-release-state.mjs +53 -0
- package/src/agents/zeus.md +25 -14
- package/src/mcp/_pantheon_paths.py +48 -7
- package/src/mcp/code_mode_server.py +33 -2
- package/src/mcp/eval_store.py +1 -1
- package/src/mcp/mcp_persistence_server.py +344 -15
- package/src/mcp/mcp_resources_server.py +144 -40
- package/src/mcp/pantheon_vision_server.py +1 -1
- package/src/mcp/toon_codec.py +491 -0
- package/src/pantheon/background-job-board.ts +67 -1
- package/src/pantheon/cost-command.ts +13 -9
- package/src/pantheon/delegate-manager.ts +384 -0
- package/src/pantheon/delegate-task-adapter.ts +444 -0
- package/src/pantheon/delegation-classifier.ts +307 -0
- package/src/pantheon/delegation-enforce.ts +13 -12
- package/src/pantheon/delegation-finalize.ts +374 -0
- package/src/pantheon/delegation-notify.ts +212 -0
- package/src/pantheon/delegation-toast.ts +73 -0
- package/src/pantheon/delegation.ts +1120 -0
- package/src/pantheon/goal-loop.ts +2 -2
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/hashline/tool.ts +2 -2
- package/src/pantheon/model-command.ts +2 -1
- package/src/pantheon/permission-eval.ts +284 -0
- package/src/pantheon/step-cap.ts +110 -0
- package/src/pantheon/token-opt.ts +555 -0
- package/src/pantheon/toon-codec.ts +481 -0
- package/src/pantheon/tui-version-check.ts +1 -3
- package/src/pantheon/v2-bridge.ts +129 -0
- package/src/pantheon/v2-events.ts +131 -0
- package/src/pantheon/v2-hooks.ts +287 -0
- package/src/pantheon/v2-tool-definitions.ts +331 -0
- package/src/pantheon/zeus-delegate-with-retry.ts +406 -0
- package/src/plugin-v2/index.ts +21 -0
- package/src/plugin-v2.ts +836 -0
- package/src/plugin.ts +241 -42
- package/src/plugins/tui/dist/server.d.ts +1 -1
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/server.js +1 -1
- package/src/plugins/tui/dist/server.js.map +1 -1
- package/src/plugins/tui/dist/tui.d.ts +10 -7
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +11 -8
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +22 -16
- package/src/plugins/tui/package-lock.json +51 -51
- package/src/plugins/tui/package.json +7 -7
- package/src/plugins/tui/src/index.tsx +28 -24
- package/src/skills/README.md +0 -2
- package/src/pantheon/tool-context.ts +0 -41
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delegation Classifier — stuck-agent detection for delegation results.
|
|
3
|
+
*
|
|
4
|
+
* Classifies delegation results into actionable statuses:
|
|
5
|
+
* - `content` / `success` — agent returned usable output
|
|
6
|
+
* - `empty` (empty-mode1 / empty-mode2) — no or near-no output; retryable
|
|
7
|
+
* - `budget-exhausted` — agent hit max_steps; partial result available
|
|
8
|
+
* - `timeout` (timeout-with-partial / timeout-empty / timeout-exhausted)
|
|
9
|
+
* - `error` — provider or infrastructure failure
|
|
10
|
+
*
|
|
11
|
+
* Integrates with the existing `dispatch-guard.ts` classification for
|
|
12
|
+
* empty-response detection and provides a structured `DelegationResult`
|
|
13
|
+
* that replaces raw-text returns.
|
|
14
|
+
*
|
|
15
|
+
* @module delegation-classifier
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ─── Types ─────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** Empty-response mode classification (matches dispatch-guard). */
|
|
21
|
+
export type EmptyMode = 'empty-mode1' | 'empty-mode2'
|
|
22
|
+
|
|
23
|
+
/** Timeout sub-type based on partial output and retry history. */
|
|
24
|
+
export type TimeoutSubType = 'timeout-with-partial' | 'timeout-empty' | 'timeout-exhausted'
|
|
25
|
+
|
|
26
|
+
/** Structured delegation result replacing raw-text returns. */
|
|
27
|
+
export interface DelegationResult {
|
|
28
|
+
status:
|
|
29
|
+
| 'success'
|
|
30
|
+
| 'empty'
|
|
31
|
+
| 'budget-exhausted'
|
|
32
|
+
| 'timeout'
|
|
33
|
+
| 'error'
|
|
34
|
+
| 'startup_failed'
|
|
35
|
+
| 'bootstrap_unknown'
|
|
36
|
+
content: string
|
|
37
|
+
retryCount: number
|
|
38
|
+
/** When status is 'empty', which empty mode was detected. */
|
|
39
|
+
classification?: EmptyMode | undefined
|
|
40
|
+
/** When status is 'timeout', the timeout sub-type. */
|
|
41
|
+
subType?: TimeoutSubType | undefined
|
|
42
|
+
/** Available when budget-exhausted or timeout-with-partial. */
|
|
43
|
+
partialResult?: string | undefined
|
|
44
|
+
/** Actionable recommendation for the orchestrator. */
|
|
45
|
+
recommendation?: string | undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Result of empty-response classification. */
|
|
49
|
+
export interface EmptyClassification {
|
|
50
|
+
mode: EmptyMode | 'content'
|
|
51
|
+
hasContent: boolean
|
|
52
|
+
hasTokens: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Timeout classification input. */
|
|
56
|
+
export interface TimeoutClassifyInput {
|
|
57
|
+
/** Report markdown (undefined if no report was written). */
|
|
58
|
+
report: string | undefined
|
|
59
|
+
/** Whether the child session had any messages at all. */
|
|
60
|
+
hasMessages: boolean
|
|
61
|
+
/** How many retries have already been attempted (0 or 1). */
|
|
62
|
+
retryCount: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Result of stuck-agent classification. */
|
|
66
|
+
export interface StuckClassification {
|
|
67
|
+
status: 'success' | 'budget-exhausted' | 'empty'
|
|
68
|
+
partialResult?: string | undefined
|
|
69
|
+
recommendation?: string | undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Patterns ──────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/** Matches budget-exhausted messages from the agent or step-cap system. */
|
|
75
|
+
const BUDGET_EXHAUSTED_RE =
|
|
76
|
+
/maximum\s+(?:steps?|step\s*budget)\s+(?:for\s+this\s+agent\s+)?(?:has|have)\s+been\s+reached|step\s*budget\s+(?:was\s+)?exhausted/i
|
|
77
|
+
|
|
78
|
+
/** Output-captured marker in delegation reports. */
|
|
79
|
+
const NO_OUTPUT_RE = /_No output captured\._/i
|
|
80
|
+
|
|
81
|
+
// ─── Empty-Response Classification ────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Classify whether a response is empty, empty-with-tokens, or has content.
|
|
85
|
+
* Matches the dispatch-guard classification (empty-mode1, empty-mode2).
|
|
86
|
+
*
|
|
87
|
+
* @param text The response text (may be null/undefined).
|
|
88
|
+
* @param tokens Optional token counts for mode2 detection.
|
|
89
|
+
*/
|
|
90
|
+
export function classifyEmptyResponse(
|
|
91
|
+
text: string | null | undefined,
|
|
92
|
+
tokens?: { tokensInput?: number; tokensOutput?: number },
|
|
93
|
+
): EmptyClassification {
|
|
94
|
+
const hasContent = typeof text === 'string' && text.trim() !== ''
|
|
95
|
+
if (hasContent) {
|
|
96
|
+
return { mode: 'content', hasContent: true, hasTokens: hasTokens(tokens) }
|
|
97
|
+
}
|
|
98
|
+
const hasT = hasTokens(tokens)
|
|
99
|
+
return {
|
|
100
|
+
mode: hasT ? 'empty-mode2' : 'empty-mode1',
|
|
101
|
+
hasContent: false,
|
|
102
|
+
hasTokens: hasT,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Whether token counts indicate the model actually ran. */
|
|
107
|
+
function hasTokens(tokens?: { tokensInput?: number; tokensOutput?: number }): boolean {
|
|
108
|
+
if (tokens === undefined) return false
|
|
109
|
+
return (tokens.tokensInput ?? 0) > 0 || (tokens.tokensOutput ?? 0) > 0
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Stuck-Agent Detection ────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Classify a report as success, budget-exhausted, or empty.
|
|
116
|
+
* Detects max-steps patterns in the report text.
|
|
117
|
+
*
|
|
118
|
+
* @param report The delegation report markdown.
|
|
119
|
+
*/
|
|
120
|
+
export function classifyStuckAgent(report: string): StuckClassification {
|
|
121
|
+
const trimmed = report.trim()
|
|
122
|
+
|
|
123
|
+
// Empty output
|
|
124
|
+
if (NO_OUTPUT_RE.test(trimmed) || trimmed === '') {
|
|
125
|
+
return { status: 'empty' }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Budget-exhausted: extract content before the marker as partialResult
|
|
129
|
+
if (BUDGET_EXHAUSTED_RE.test(trimmed)) {
|
|
130
|
+
const outputSection = extractOutputSection(trimmed)
|
|
131
|
+
const partialResult =
|
|
132
|
+
outputSection !== undefined && !NO_OUTPUT_RE.test(outputSection) ? outputSection : undefined
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
status: 'budget-exhausted',
|
|
136
|
+
partialResult,
|
|
137
|
+
recommendation:
|
|
138
|
+
'Agent hit step budget; partial result available. ' +
|
|
139
|
+
'Consider resuming with task_id or dispatching with larger budget.',
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { status: 'success' }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Extract the text after "## Output" in a delegation report, or undefined. */
|
|
147
|
+
function extractOutputSection(report: string): string | undefined {
|
|
148
|
+
const marker = '## Output'
|
|
149
|
+
const idx = report.indexOf(marker)
|
|
150
|
+
if (idx === -1) return undefined
|
|
151
|
+
const afterMarker = report.slice(idx + marker.length)
|
|
152
|
+
// Skip the newline after "## Output"
|
|
153
|
+
const content = afterMarker.replace(/^\n+/, '')
|
|
154
|
+
// Stop at the next section (## or **Error**)
|
|
155
|
+
const endIdx = content.search(/\n(?:##|\*\*Error\*\*|\[TIMEOUT)/)
|
|
156
|
+
return endIdx === -1 ? content.trim() : content.slice(0, endIdx).trim()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Timeout Classification ───────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Classify a timeout based on available output and retry history.
|
|
163
|
+
*
|
|
164
|
+
* - timeout-empty: no report, no messages, not yet retried → retryable
|
|
165
|
+
* - timeout-with-partial: has report or messages → return partial
|
|
166
|
+
* - timeout-exhausted: already retried → escalate
|
|
167
|
+
*
|
|
168
|
+
* @param input Report availability, message presence, and retry count.
|
|
169
|
+
*/
|
|
170
|
+
export function classifyTimeout(input: TimeoutClassifyInput): DelegationResult {
|
|
171
|
+
const { report, hasMessages, retryCount } = input
|
|
172
|
+
const hasPartial =
|
|
173
|
+
(report !== undefined && report.trim() !== '' && !NO_OUTPUT_RE.test(report)) || hasMessages
|
|
174
|
+
|
|
175
|
+
// Already retried → exhausted, no more retries
|
|
176
|
+
if (retryCount >= 1) {
|
|
177
|
+
return {
|
|
178
|
+
status: 'timeout',
|
|
179
|
+
content: report ?? '',
|
|
180
|
+
retryCount,
|
|
181
|
+
subType: 'timeout-exhausted',
|
|
182
|
+
partialResult: hasPartial ? (report ?? '') : undefined,
|
|
183
|
+
recommendation:
|
|
184
|
+
'Agent timed out after retry. Options: ' +
|
|
185
|
+
'(a) try different agent, (b) simplify scope, (c) manual intervention.',
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Has partial output → return it
|
|
190
|
+
if (hasPartial) {
|
|
191
|
+
return {
|
|
192
|
+
status: 'timeout',
|
|
193
|
+
content: report ?? '',
|
|
194
|
+
retryCount,
|
|
195
|
+
subType: 'timeout-with-partial',
|
|
196
|
+
partialResult: report ?? '',
|
|
197
|
+
recommendation:
|
|
198
|
+
'Agent timed out but partial work exists. ' + 'Partial result is available above.',
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// No output at all → retryable
|
|
203
|
+
return {
|
|
204
|
+
status: 'timeout',
|
|
205
|
+
content: report ?? '',
|
|
206
|
+
retryCount,
|
|
207
|
+
subType: 'timeout-empty',
|
|
208
|
+
recommendation: 'Agent timed out with no output. Will retry once with rephrased prompt.',
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Result Helpers ───────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Whether a delegation result is retryable (empty + retryCount < 1,
|
|
216
|
+
* or timeout-empty + retryCount < 1).
|
|
217
|
+
*/
|
|
218
|
+
export function isRetryableResult(result: DelegationResult): boolean {
|
|
219
|
+
if (result.retryCount >= 1) return false
|
|
220
|
+
if (result.status === 'empty') return true
|
|
221
|
+
if (result.status === 'timeout' && result.subType === 'timeout-empty') return true
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Format a DelegationResult into a human-readable string for the orchestrator.
|
|
227
|
+
* Success results are returned as-is (the raw content). Non-success results
|
|
228
|
+
* get a structured status message.
|
|
229
|
+
*/
|
|
230
|
+
export function formatDelegationResult(result: DelegationResult): string {
|
|
231
|
+
if (result.status === 'success') {
|
|
232
|
+
return result.content
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const parts: string[] = []
|
|
236
|
+
|
|
237
|
+
// Status header
|
|
238
|
+
switch (result.status) {
|
|
239
|
+
case 'empty': {
|
|
240
|
+
const modeLabel =
|
|
241
|
+
result.classification === 'empty-mode2'
|
|
242
|
+
? 'empty-mode2 (reasoning tokens present but no text output)'
|
|
243
|
+
: 'empty-mode1 (no tokens generated)'
|
|
244
|
+
parts.push(`[EMPTY RESULT — ${modeLabel}]`)
|
|
245
|
+
if (result.retryCount === 0) {
|
|
246
|
+
parts.push(
|
|
247
|
+
'The agent returned an empty response. ' +
|
|
248
|
+
'A retry with a rephrased prompt has been triggered automatically.',
|
|
249
|
+
)
|
|
250
|
+
} else {
|
|
251
|
+
parts.push(
|
|
252
|
+
'The agent returned an empty response even after retry. ' +
|
|
253
|
+
'Options: (a) try different agent, (b) simplify scope, (c) manual intervention.',
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
break
|
|
257
|
+
}
|
|
258
|
+
case 'budget-exhausted':
|
|
259
|
+
parts.push('[BUDGET EXHAUSTED]')
|
|
260
|
+
if (result.partialResult) {
|
|
261
|
+
parts.push('Partial result:')
|
|
262
|
+
parts.push(result.partialResult)
|
|
263
|
+
}
|
|
264
|
+
parts.push(result.recommendation ?? '')
|
|
265
|
+
break
|
|
266
|
+
case 'timeout': {
|
|
267
|
+
const subLabel =
|
|
268
|
+
result.subType === 'timeout-with-partial'
|
|
269
|
+
? 'with partial output'
|
|
270
|
+
: result.subType === 'timeout-exhausted'
|
|
271
|
+
? 'exhausted after retry'
|
|
272
|
+
: 'no output'
|
|
273
|
+
parts.push(`[TIMEOUT — ${subLabel}]`)
|
|
274
|
+
if (result.partialResult) {
|
|
275
|
+
parts.push('Partial result:')
|
|
276
|
+
parts.push(result.partialResult)
|
|
277
|
+
}
|
|
278
|
+
parts.push(result.recommendation ?? '')
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
case 'error':
|
|
282
|
+
parts.push('[ERROR]')
|
|
283
|
+
parts.push(result.content)
|
|
284
|
+
break
|
|
285
|
+
case 'startup_failed':
|
|
286
|
+
parts.push('[STARTUP FAILED]')
|
|
287
|
+
parts.push(result.content)
|
|
288
|
+
parts.push(
|
|
289
|
+
result.recommendation ??
|
|
290
|
+
'Retry once or inspect the child session before dispatching again.',
|
|
291
|
+
)
|
|
292
|
+
break
|
|
293
|
+
case 'bootstrap_unknown':
|
|
294
|
+
parts.push('[BOOTSTRAP UNKNOWN]')
|
|
295
|
+
parts.push(result.content)
|
|
296
|
+
parts.push(
|
|
297
|
+
result.recommendation ?? 'The host did not expose enough session state to verify startup.',
|
|
298
|
+
)
|
|
299
|
+
break
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Structured fields for non-success results
|
|
303
|
+
parts.push(`status: ${result.status}`)
|
|
304
|
+
parts.push(`retryCount: ${result.retryCount}`)
|
|
305
|
+
|
|
306
|
+
return parts.filter((p) => p !== '').join('\n')
|
|
307
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read-Only Enforcement (Phase 4) — deny mutating tools inside sessions that
|
|
3
|
-
* were delegated as read-only (advisory `read_only: true` on
|
|
4
|
-
*
|
|
3
|
+
* were delegated as read-only (advisory `read_only: true` on
|
|
4
|
+
* pantheon_delegate, or the agent ∈ `readOnlyAgents` from routing.yml).
|
|
5
5
|
*
|
|
6
6
|
* OpenCode hook: `tool.execute.before`. The hook shape is
|
|
7
7
|
* `(input, output) => Promise<void>`; THROWING from the hook denies the tool
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
* - `task` is blocked too, which hard-enforces depth-2: a read-only agent
|
|
19
19
|
* cannot spawn its own subagents.
|
|
20
20
|
*
|
|
21
|
-
* Unknown sessions are allowed for ordinary tools.
|
|
22
|
-
* fail-closed when wired: missing agent identity or
|
|
21
|
+
* Unknown sessions are allowed for ordinary tools. Delegation tools are
|
|
22
|
+
* fail-closed when the runtime matrix is wired: missing agent identity or
|
|
23
23
|
* target metadata is denied rather than inferred.
|
|
24
24
|
*
|
|
25
|
-
* The registry is populated at delegate time: the CHILD
|
|
25
|
+
* The registry is populated by delegation.ts at delegate time: the CHILD
|
|
26
26
|
* session is registered when the delegate call is read-only.
|
|
27
27
|
*
|
|
28
28
|
* @module delegation-enforce
|
|
@@ -147,6 +147,7 @@ export function isDelegationAllowed(
|
|
|
147
147
|
function targetFromTool(tool: string, args: unknown): string | undefined {
|
|
148
148
|
if (args === null || typeof args !== 'object') return undefined
|
|
149
149
|
const record = args as Record<string, unknown>
|
|
150
|
+
if (tool === 'pantheon_delegate') return normalizeDelegationAgent(record.agent)
|
|
150
151
|
if (tool === 'task') {
|
|
151
152
|
return normalizeDelegationAgent(record.subagent_type ?? record.subagentType)
|
|
152
153
|
}
|
|
@@ -235,7 +236,7 @@ export function zeusReadGuard(tool: string, args: unknown, agent: string | undef
|
|
|
235
236
|
// ─── Registry ──────────────────────────────────────────────────────────
|
|
236
237
|
|
|
237
238
|
/**
|
|
238
|
-
* Read-only session registry.
|
|
239
|
+
* Read-only session registry. `createDelegationTools` registers child
|
|
239
240
|
* sessions here; the plugin's `tool.execute.before` guard consults it.
|
|
240
241
|
* Process-wide singleton for plugin wiring + factory for test isolation.
|
|
241
242
|
*/
|
|
@@ -290,19 +291,19 @@ export function createEnforcementGuard(input: {
|
|
|
290
291
|
options?: EnforcementGuardOptions
|
|
291
292
|
}): ToolExecuteBeforeHandler {
|
|
292
293
|
const blockedTools = input.options?.blockedTools ?? DEFAULT_BLOCKED_TOOLS
|
|
293
|
-
const prefix = input.options?.messagePrefix ?? '
|
|
294
|
+
const prefix = input.options?.messagePrefix ?? 'pantheon_delegate guard'
|
|
294
295
|
|
|
295
296
|
return async (hook: ToolExecuteBeforeInput, output?: ToolExecuteBeforeOutput): Promise<void> => {
|
|
296
|
-
const isDelegationTool = hook.tool === 'task'
|
|
297
|
+
const isDelegationTool = hook.tool === 'task' || hook.tool === 'pantheon_delegate'
|
|
297
298
|
if (isDelegationTool && input.options?.isChildSession?.(hook.sessionID) === true) {
|
|
298
299
|
throw new Error(
|
|
299
300
|
`${prefix}: session ${hook.sessionID} is a child session — delegation tools are denied ` +
|
|
300
301
|
'to prevent nested delegation',
|
|
301
302
|
)
|
|
302
303
|
}
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
304
|
+
// pantheon_delegate has the authoritative ToolContext.agent at execution
|
|
305
|
+
// time, so its matrix check lives in delegation.ts. The hook only has the
|
|
306
|
+
// session-scoped agent learned from chat.params for native task().
|
|
306
307
|
if (
|
|
307
308
|
hook.tool === 'task' &&
|
|
308
309
|
input.options?.isRootSession !== undefined &&
|
|
@@ -335,7 +336,7 @@ export function createEnforcementGuard(input: {
|
|
|
335
336
|
`This session was delegated for investigation only (read-only agent); it cannot ` +
|
|
336
337
|
`modify files, run commands, or spawn subagents. ` +
|
|
337
338
|
`If a write result is needed, dispatch a write-capable agent ` +
|
|
338
|
-
`(e.g. hermes, aphrodite) with
|
|
339
|
+
`(e.g. hermes, aphrodite) with pantheon_delegate instead.`,
|
|
339
340
|
)
|
|
340
341
|
}
|
|
341
342
|
}
|