pantheon-opencode 1.5.0-beta.8 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -63
- package/CHANGELOG.md +320 -0
- package/README.md +14 -27
- package/README.pt-BR.md +5 -6
- package/commands/pantheon.md +1 -35
- package/package.json +1 -2
- package/plugin.json +1 -1
- package/pyproject.toml +2 -5
- package/scripts/doctor.mjs +32 -3
- package/scripts/install/plugin.mjs +9 -12
- package/scripts/mcp_persistence_server.py +1 -1
- package/scripts/sync-tui.mjs +8 -11
- package/src/agents/apollo.md +1 -0
- package/src/agents/gaia.md +1 -0
- package/src/agents/zeus.md +9 -52
- package/src/instructions/memory-protocol.instructions.md +6 -3
- package/src/instructions/zeus-anti-stall.instructions.md +2 -4
- package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
- package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
- package/src/mcp/mcp_persistence_server.py +1 -1
- package/src/mcp/toon_codec.py +2 -4
- package/src/pantheon/background-job-board.ts +4 -4
- package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
- package/src/pantheon/cost-command.ts +1 -2
- package/src/pantheon/delegation-enforce.ts +59 -14
- package/src/pantheon/goal-loop.ts +1 -1
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/idle-continuation.ts +4 -6
- package/src/pantheon/model-command.ts +1 -2
- package/src/pantheon/native-task-status.ts +2 -2
- package/src/pantheon/presets.d.mts +0 -20
- package/src/pantheon/presets.mjs +0 -64
- package/src/pantheon/session-guard.ts +2 -2
- package/src/pantheon/session-id.ts +30 -0
- package/src/pantheon/task-result-guard.ts +2 -3
- package/src/pantheon/todo-enforcer.ts +3 -3
- package/src/pantheon/tool-context.ts +23 -0
- package/src/pantheon/v2-bridge.ts +1 -4
- package/src/plugin-v2.ts +8 -21
- package/src/plugin.ts +61 -245
- package/src/plugins/pantheon-hooks.ts +1 -1
- package/src/plugins/tui/dist/tui.d.ts +229 -77
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +509 -259
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +2 -2
- package/src/plugins/tui/src/index.tsx +753 -279
- package/src/routing.yml +0 -16
- package/src/skills/context-compression/SKILL.md +1 -1
- package/scripts/generate-preset-docs.mjs +0 -300
- package/scripts/generate-prompts.sh +0 -222
- package/scripts/hash_verify.py +0 -192
- package/scripts/install/component.mjs +0 -24
- package/scripts/install/migrate-config.mjs +0 -302
- package/scripts/test-context-compression.sh +0 -169
- package/src/pantheon/auto-wake.ts +0 -85
- package/src/pantheon/chat-reminders.ts +0 -9
- package/src/pantheon/comment-checker.ts +0 -48
- package/src/pantheon/compaction-assert.ts +0 -160
- package/src/pantheon/compaction-handoff.ts +0 -304
- package/src/pantheon/cost-tracker.ts +0 -108
- package/src/pantheon/delegate-manager.ts +0 -766
- package/src/pantheon/delegation-classifier.ts +0 -307
- package/src/pantheon/delegation-finalize.ts +0 -406
- package/src/pantheon/delegation-notify.ts +0 -212
- package/src/pantheon/delegation-toast.ts +0 -73
- package/src/pantheon/delegation.ts +0 -1337
- package/src/pantheon/dispatch-guard.ts +0 -109
- package/src/pantheon/native-probe.ts +0 -234
- package/src/pantheon/permission-eval.ts +0 -284
- package/src/pantheon/preemptive-compact.ts +0 -76
- package/src/pantheon/provider-capabilities.ts +0 -221
- package/src/pantheon/provider-stream-tracker.ts +0 -182
- package/src/pantheon/retry-policy.ts +0 -212
- package/src/pantheon/step-cap.ts +0 -110
- package/src/pantheon/token-opt.ts +0 -689
- package/src/pantheon/toon-codec.ts +0 -481
- package/src/pantheon/v2-events.ts +0 -148
- package/src/pantheon/v2-hooks.ts +0 -287
- package/src/pantheon/v2-tool-definitions.ts +0 -331
- package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
- package/src/plugins/tui/dist/tui.tsx +0 -2777
|
@@ -21,9 +21,8 @@ import { homedir } from 'node:os'
|
|
|
21
21
|
import { delimiter, dirname, join } from 'node:path'
|
|
22
22
|
import { fileURLToPath } from 'node:url'
|
|
23
23
|
import { z } from 'zod'
|
|
24
|
-
|
|
25
|
-
import type { ToolContextLike } from './delegation.ts'
|
|
26
24
|
import type { NativeTaskStatus } from './native-task-status.ts'
|
|
25
|
+
import type { ToolContextLike } from './tool-context.ts'
|
|
27
26
|
|
|
28
27
|
const __filename = fileURLToPath(import.meta.url)
|
|
29
28
|
const __dirname = dirname(__filename)
|
|
@@ -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 (
|
|
4
|
-
*
|
|
3
|
+
* were delegated as read-only (an explicit read-only flag, or the agent ∈
|
|
4
|
+
* `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
|
|
@@ -22,8 +22,10 @@
|
|
|
22
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 by
|
|
26
|
-
*
|
|
25
|
+
* The registry is populated by the plugin's `chat.params` hook — the only
|
|
26
|
+
* place the SDK exposes the active agent: when the session's agent is
|
|
27
|
+
* read-only (apollo, gaia — routing.yml `background_delegation.read_only_agents`),
|
|
28
|
+
* the session is registered; `session.deleted` unregisters it.
|
|
27
29
|
*
|
|
28
30
|
* @module delegation-enforce
|
|
29
31
|
*/
|
|
@@ -147,7 +149,6 @@ export function isDelegationAllowed(
|
|
|
147
149
|
function targetFromTool(tool: string, args: unknown): string | undefined {
|
|
148
150
|
if (args === null || typeof args !== 'object') return undefined
|
|
149
151
|
const record = args as Record<string, unknown>
|
|
150
|
-
if (tool === 'pantheon_delegate') return normalizeDelegationAgent(record.agent)
|
|
151
152
|
if (tool === 'task') {
|
|
152
153
|
return normalizeDelegationAgent(record.subagent_type ?? record.subagentType)
|
|
153
154
|
}
|
|
@@ -236,9 +237,10 @@ export function zeusReadGuard(tool: string, args: unknown, agent: string | undef
|
|
|
236
237
|
// ─── Registry ──────────────────────────────────────────────────────────
|
|
237
238
|
|
|
238
239
|
/**
|
|
239
|
-
* Read-only session registry. `
|
|
240
|
-
* sessions
|
|
241
|
-
*
|
|
240
|
+
* Read-only session registry. The plugin's `chat.params` hook registers
|
|
241
|
+
* sessions whose active agent is read-only (see {@link syncReadOnlySession});
|
|
242
|
+
* the `tool.execute.before` guard consults it. Process-wide singleton for
|
|
243
|
+
* plugin wiring + factory for test isolation.
|
|
242
244
|
*/
|
|
243
245
|
export class ReadOnlySessionRegistry {
|
|
244
246
|
private readonly sessions = new Map<string, ReadOnlyEntry>()
|
|
@@ -279,6 +281,50 @@ export class ReadOnlySessionRegistry {
|
|
|
279
281
|
/** Process-wide registry wired into the plugin. */
|
|
280
282
|
export const readOnlyRegistry = new ReadOnlySessionRegistry()
|
|
281
283
|
|
|
284
|
+
// ─── Read-Only Agent Policy ────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Agents that must stay investigation-only. Mirrors
|
|
288
|
+
* `background_delegation.read_only_agents` in routing.yml — the parity is
|
|
289
|
+
* enforced by tests/pantheon/delegation-enforce.test.ts so the two cannot
|
|
290
|
+
* drift silently.
|
|
291
|
+
*/
|
|
292
|
+
export const READ_ONLY_AGENTS: ReadonlySet<string> = new Set(['apollo', 'gaia'])
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Whether an agent identity belongs to the read-only set. Identity is never
|
|
296
|
+
* inferred from prompts or tool names — only the normalized agent name counts.
|
|
297
|
+
*/
|
|
298
|
+
export function isReadOnlyAgent(
|
|
299
|
+
agent: unknown,
|
|
300
|
+
readOnlyAgents: ReadonlySet<string> = READ_ONLY_AGENTS,
|
|
301
|
+
): boolean {
|
|
302
|
+
const normalized = normalizeDelegationAgent(agent)
|
|
303
|
+
return normalized !== undefined && readOnlyAgents.has(normalized)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Sync the read-only registry with a session's active agent. Called from the
|
|
308
|
+
* plugin's `chat.params` hook: a read-only agent registers the session;
|
|
309
|
+
* anything else (unknown/absent/non-read-only) unregisters it, which also
|
|
310
|
+
* covers an in-session agent switch. Returns whether the session is now
|
|
311
|
+
* registered.
|
|
312
|
+
*/
|
|
313
|
+
export function syncReadOnlySession(
|
|
314
|
+
registry: ReadOnlySessionRegistry,
|
|
315
|
+
sessionID: string,
|
|
316
|
+
agent: unknown,
|
|
317
|
+
readOnlyAgents: ReadonlySet<string> = READ_ONLY_AGENTS,
|
|
318
|
+
): boolean {
|
|
319
|
+
const normalized = normalizeDelegationAgent(agent)
|
|
320
|
+
if (normalized === undefined || !readOnlyAgents.has(normalized)) {
|
|
321
|
+
registry.unregister(sessionID)
|
|
322
|
+
return false
|
|
323
|
+
}
|
|
324
|
+
registry.register(sessionID, { agent: normalized })
|
|
325
|
+
return true
|
|
326
|
+
}
|
|
327
|
+
|
|
282
328
|
// ─── Guard ─────────────────────────────────────────────────────────────
|
|
283
329
|
|
|
284
330
|
/**
|
|
@@ -291,19 +337,18 @@ export function createEnforcementGuard(input: {
|
|
|
291
337
|
options?: EnforcementGuardOptions
|
|
292
338
|
}): ToolExecuteBeforeHandler {
|
|
293
339
|
const blockedTools = input.options?.blockedTools ?? DEFAULT_BLOCKED_TOOLS
|
|
294
|
-
const prefix = input.options?.messagePrefix ?? '
|
|
340
|
+
const prefix = input.options?.messagePrefix ?? 'pantheon delegation guard'
|
|
295
341
|
|
|
296
342
|
return async (hook: ToolExecuteBeforeInput, output?: ToolExecuteBeforeOutput): Promise<void> => {
|
|
297
|
-
const isDelegationTool = hook.tool === 'task'
|
|
343
|
+
const isDelegationTool = hook.tool === 'task'
|
|
298
344
|
if (isDelegationTool && input.options?.isChildSession?.(hook.sessionID) === true) {
|
|
299
345
|
throw new Error(
|
|
300
346
|
`${prefix}: session ${hook.sessionID} is a child session — delegation tools are denied ` +
|
|
301
347
|
'to prevent nested delegation',
|
|
302
348
|
)
|
|
303
349
|
}
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
// session-scoped agent learned from chat.params for native task().
|
|
350
|
+
// The hook only has the session-scoped agent learned from chat.params,
|
|
351
|
+
// so the runtime matrix check for native task() lives here.
|
|
307
352
|
if (
|
|
308
353
|
hook.tool === 'task' &&
|
|
309
354
|
input.options?.isRootSession !== undefined &&
|
|
@@ -336,7 +381,7 @@ export function createEnforcementGuard(input: {
|
|
|
336
381
|
`This session was delegated for investigation only (read-only agent); it cannot ` +
|
|
337
382
|
`modify files, run commands, or spawn subagents. ` +
|
|
338
383
|
`If a write result is needed, dispatch a write-capable agent ` +
|
|
339
|
-
`(e.g. hermes, aphrodite) with
|
|
384
|
+
`(e.g. hermes, aphrodite) with native task() instead.`,
|
|
340
385
|
)
|
|
341
386
|
}
|
|
342
387
|
}
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
import { z } from 'zod'
|
|
18
18
|
|
|
19
19
|
import type { BackgroundJobBoard } from './background-job-board.ts'
|
|
20
|
-
import type { ToolContextLike } from './delegation.ts'
|
|
21
20
|
import { GoalStore } from './goal-store.ts'
|
|
22
21
|
import { createPantheonLogger } from './logger.ts'
|
|
23
22
|
import { safeSessionPath } from './session-guard.ts'
|
|
24
23
|
import type { TodoEnforcerMessage } from './todo-enforcer.ts'
|
|
24
|
+
import type { ToolContextLike } from './tool-context.ts'
|
|
25
25
|
|
|
26
26
|
// Silence-by-default TUI policy (pantheon-hooks L42-58): warn → hooks.log,
|
|
27
27
|
// console echo opt-in via PANTHEON_HOOKS_LOG=1. `deps.logger` stays for tests.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
16
16
|
import { join } from 'node:path'
|
|
17
17
|
|
|
18
|
-
import { assertSafeParentSessionID } from './
|
|
18
|
+
import { assertSafeParentSessionID } from './session-id.ts'
|
|
19
19
|
|
|
20
20
|
export type GoalStatus = 'pending' | 'in_progress' | 'done'
|
|
21
21
|
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
* Priority: an ACTIVE GOAL owns the idle — `GoalLoop.onIdle` continues the
|
|
6
6
|
* goal. Without an active goal, the TODO enforcer gets the idle (it
|
|
7
7
|
* self-guards on its own enabled flag + guards). Board-child sessions never
|
|
8
|
-
* reach the dispatcher — the plugin's event hook
|
|
9
|
-
*
|
|
10
|
-
* only calls the dispatcher on the `!delegated` branch.
|
|
8
|
+
* reach the dispatcher — the plugin's event hook owns board transitions and
|
|
9
|
+
* only calls the dispatcher for non-board sessions.
|
|
11
10
|
*
|
|
12
11
|
* Independently testable — the loop and enforcer are structural interfaces;
|
|
13
12
|
* the real GoalLoop/TodoEnforcer satisfy them.
|
|
@@ -34,9 +33,8 @@ export interface IdleDispatcher {
|
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
/**
|
|
37
|
-
* Build the idle router. The plugin event hook calls this only
|
|
38
|
-
*
|
|
39
|
-
* arrive here.
|
|
36
|
+
* Build the idle router. The plugin event hook calls this only for non-board
|
|
37
|
+
* sessions, so board children never arrive here.
|
|
40
38
|
*/
|
|
41
39
|
export function createIdleDispatcher(deps: {
|
|
42
40
|
goalLoop: GoalLoopLike
|
|
@@ -23,8 +23,6 @@ import { lstat, mkdir, open, rename, unlink } from 'node:fs/promises'
|
|
|
23
23
|
import { homedir } from 'node:os'
|
|
24
24
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
25
25
|
import { z } from 'zod'
|
|
26
|
-
|
|
27
|
-
import type { ToolContextLike } from './delegation.ts'
|
|
28
26
|
import {
|
|
29
27
|
capabilityEntry,
|
|
30
28
|
hasVision,
|
|
@@ -32,6 +30,7 @@ import {
|
|
|
32
30
|
normalizeCapability,
|
|
33
31
|
resolveActivePreset,
|
|
34
32
|
} from './presets.mjs'
|
|
33
|
+
import type { ToolContextLike } from './tool-context.ts'
|
|
35
34
|
|
|
36
35
|
export type ModelScope = 'project' | 'global'
|
|
37
36
|
export type ModelAction = 'status' | 'show' | 'set' | 'reset'
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Native Task Status — classifies BackgroundJobRecord results from native
|
|
3
3
|
* task() delegation into actionable status codes.
|
|
4
4
|
*
|
|
5
|
-
* Pure module — zero external dependencies. Consumed by
|
|
6
|
-
*
|
|
5
|
+
* Pure module — zero external dependencies. Consumed by the tool-ceiling and
|
|
6
|
+
* cost layers to decide retry/skip/escalate strategy.
|
|
7
7
|
*
|
|
8
8
|
* @module native-task-status
|
|
9
9
|
*/
|
|
@@ -104,26 +104,6 @@ export interface RoutingRetryPolicyOptions {
|
|
|
104
104
|
logger?: { warn?: (msg: string) => void }
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/**
|
|
108
|
-
* R1: per-error-type retry policy from routing.yml `retry_policy`
|
|
109
|
-
* (auth/rate_limit/timeout/other → max retries). Fail-open → null.
|
|
110
|
-
*/
|
|
111
|
-
export declare function loadRoutingRetryPolicy(
|
|
112
|
-
options?: RoutingRetryPolicyOptions,
|
|
113
|
-
): Record<string, number> | null
|
|
114
|
-
|
|
115
|
-
export interface RoutingCooldownResult {
|
|
116
|
-
allowed_fails: number
|
|
117
|
-
cooldown_time_seconds: number
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* R1: provider cooldown config from routing.yml `cooldown`. Fail-open → null.
|
|
122
|
-
*/
|
|
123
|
-
export declare function loadRoutingCooldown(
|
|
124
|
-
options?: RoutingRetryPolicyOptions,
|
|
125
|
-
): RoutingCooldownResult | null
|
|
126
|
-
|
|
127
107
|
/**
|
|
128
108
|
* R4: per-agent step caps from routing.yml `agents.<name>.max_steps`.
|
|
129
109
|
* Fail-open → {} (no agent capped).
|
package/src/pantheon/presets.mjs
CHANGED
|
@@ -213,70 +213,6 @@ function loadRoutingYaml(routingPath) {
|
|
|
213
213
|
return routing && typeof routing === 'object' ? routing : null
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
const RETRY_ERROR_TYPES = new Set(['auth', 'rate_limit', 'timeout', 'other'])
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Load the R1 per-error-type retry policy from routing.yml (`retry_policy:`).
|
|
220
|
-
* Fail-open: missing/unparseable config yields null (the caller falls back
|
|
221
|
-
* to DEFAULT_RETRY_POLICY) and warns via the optional logger.
|
|
222
|
-
*
|
|
223
|
-
* @param {object} [opts]
|
|
224
|
-
* @param {string} [opts.routingPath]
|
|
225
|
-
* @param {{warn?: Function}} [opts.logger]
|
|
226
|
-
* @returns {Record<string, number>|null} error-type → max retries
|
|
227
|
-
*/
|
|
228
|
-
export function loadRoutingRetryPolicy({ routingPath, logger = console } = {}) {
|
|
229
|
-
try {
|
|
230
|
-
const routing = loadRoutingYaml(routingPath)
|
|
231
|
-
const policy = routing?.retry_policy
|
|
232
|
-
if (!policy || typeof policy !== 'object' || Array.isArray(policy)) return null
|
|
233
|
-
const out = {}
|
|
234
|
-
for (const [type, max] of Object.entries(policy)) {
|
|
235
|
-
if (RETRY_ERROR_TYPES.has(type) && Number.isSafeInteger(max) && max >= 0) {
|
|
236
|
-
out[type] = max
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return Object.keys(out).length > 0 ? out : null
|
|
240
|
-
} catch (err) {
|
|
241
|
-
const reason = err instanceof Error ? err.message : String(err)
|
|
242
|
-
logger.warn?.(`presets: routing.yml retry_policy unavailable (${reason})`)
|
|
243
|
-
return null
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Load the R1 provider cooldown config from routing.yml (`cooldown:`).
|
|
249
|
-
* Fail-open: missing/unparseable config yields null (caller falls back to
|
|
250
|
-
* DEFAULT_COOLDOWN).
|
|
251
|
-
*
|
|
252
|
-
* @param {object} [opts]
|
|
253
|
-
* @param {string} [opts.routingPath]
|
|
254
|
-
* @param {{warn?: Function}} [opts.logger]
|
|
255
|
-
* @returns {{allowed_fails: number, cooldown_time_seconds: number}|null}
|
|
256
|
-
*/
|
|
257
|
-
export function loadRoutingCooldown({ routingPath, logger = console } = {}) {
|
|
258
|
-
try {
|
|
259
|
-
const routing = loadRoutingYaml(routingPath)
|
|
260
|
-
const cooldown = routing?.cooldown
|
|
261
|
-
if (!cooldown || typeof cooldown !== 'object' || Array.isArray(cooldown)) return null
|
|
262
|
-
const allowedFails = cooldown.allowed_fails
|
|
263
|
-
const cooldownTimeSeconds = cooldown.cooldown_time_seconds
|
|
264
|
-
if (
|
|
265
|
-
!Number.isSafeInteger(allowedFails) ||
|
|
266
|
-
allowedFails < 1 ||
|
|
267
|
-
!Number.isSafeInteger(cooldownTimeSeconds) ||
|
|
268
|
-
cooldownTimeSeconds < 1
|
|
269
|
-
) {
|
|
270
|
-
return null
|
|
271
|
-
}
|
|
272
|
-
return { allowed_fails: allowedFails, cooldown_time_seconds: cooldownTimeSeconds }
|
|
273
|
-
} catch (err) {
|
|
274
|
-
const reason = err instanceof Error ? err.message : String(err)
|
|
275
|
-
logger.warn?.(`presets: routing.yml cooldown unavailable (${reason})`)
|
|
276
|
-
return null
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
216
|
/**
|
|
281
217
|
* Load the R4 per-agent step caps from routing.yml
|
|
282
218
|
* (`agents.<name>.max_steps`). Fail-open: missing config yields {}.
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* never throw, never send the placeholder).
|
|
11
11
|
*
|
|
12
12
|
* Mirrors the TUI's `src/plugins/tui/src/index.tsx` contract verbatim so both
|
|
13
|
-
* sides agree on what "valid" means. The TUI
|
|
14
|
-
* (dist/tui.
|
|
13
|
+
* sides agree on what "valid" means. The TUI plugin ships as a bundle
|
|
14
|
+
* (dist/tui.js) without the src/pantheon tree, so its local copy is kept
|
|
15
15
|
* in sync — this module is the shared source for `src/pantheon/*` and
|
|
16
16
|
* `src/plugin.ts`.
|
|
17
17
|
*
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session ID safety — reject session IDs that could escape a per-session
|
|
3
|
+
* output directory.
|
|
4
|
+
*
|
|
5
|
+
* Shared helper for the file-backed stores that remain after the delegate
|
|
6
|
+
* removal (goal-store and friends) — it no longer lives alongside the
|
|
7
|
+
* deleted delegation module.
|
|
8
|
+
*
|
|
9
|
+
* `sessionID` is embedded in a path
|
|
10
|
+
* `<dir>/<sessionID>/file.json` — `path.join` does NOT strip `..`, so a
|
|
11
|
+
* traversing ID would write outside the sandbox. Call this BEFORE any path
|
|
12
|
+
* construction.
|
|
13
|
+
*
|
|
14
|
+
* @module session-id
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reject session IDs that could escape the target directory.
|
|
19
|
+
*
|
|
20
|
+
* @throws {Error} When the ID contains `..`, `/`, or `\`.
|
|
21
|
+
*/
|
|
22
|
+
export function assertSafeParentSessionID(parentSessionID: string): void {
|
|
23
|
+
if (
|
|
24
|
+
parentSessionID.includes('..') ||
|
|
25
|
+
parentSessionID.includes('/') ||
|
|
26
|
+
parentSessionID.includes('\\')
|
|
27
|
+
) {
|
|
28
|
+
throw new Error(`Invalid parentSessionID: ${parentSessionID}`)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -58,14 +58,13 @@ export interface TaskResultGuardOptions {
|
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* The error message injected when a task() result is empty.
|
|
61
|
-
*
|
|
62
|
-
* "Child session produced no assistant or tool output".
|
|
61
|
+
* Keeps the historical "no assistant or tool output" contract text.
|
|
63
62
|
*/
|
|
64
63
|
export const EMPTY_TASK_ERROR =
|
|
65
64
|
'ERROR: task() returned empty output — child session produced no assistant or tool output. ' +
|
|
66
65
|
'This typically means the provider rejected the request (admission control / rate limit) ' +
|
|
67
66
|
'or the child session crashed before producing any response. ' +
|
|
68
|
-
'Consider
|
|
67
|
+
'Consider splitting the work into a smaller task() or retrying the child session.'
|
|
69
68
|
|
|
70
69
|
// ─── Implementation ────────────────────────────────────────────────────
|
|
71
70
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* TODO Enforcer (Wave 1) — keeps root/non-board sessions working on their
|
|
3
3
|
* todo list when the session goes idle.
|
|
4
4
|
*
|
|
5
|
-
* The plugin's `event` hook dispatches `session.idle`: board-child sessions
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The plugin's `event` hook dispatches `session.idle`: board-child sessions
|
|
6
|
+
* are owned by the board transition path, everything else (roots, non-board
|
|
7
|
+
* sessions) goes to `TodoEnforcer.onIdle`. The enforcer
|
|
8
8
|
* re-injects a continuation prompt when the session went idle with incomplete
|
|
9
9
|
* todos, so the agent keeps working on its task list instead of stopping.
|
|
10
10
|
*
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Context — the structural view of the context opencode passes to a
|
|
3
|
+
* tool's `execute()`.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `delegation.ts` (Fase 2 of the delegate removal) so the
|
|
6
|
+
* surviving structural tools (cost-command, model-command, goal-loop) no
|
|
7
|
+
* longer depend on the delegation module. Pure type — zero runtime code.
|
|
8
|
+
*
|
|
9
|
+
* @module tool-context
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Structural view of the tool context opencode passes to execute(). */
|
|
13
|
+
export interface ToolContextLike {
|
|
14
|
+
sessionID: string
|
|
15
|
+
directory?: string
|
|
16
|
+
worktree?: string
|
|
17
|
+
/**
|
|
18
|
+
* The OpenCode SDK's ToolContext declares this as required. It remains
|
|
19
|
+
* optional here because the structural test/embedding surface can be
|
|
20
|
+
* supplied by older hosts; enforcement must skip when it is absent.
|
|
21
|
+
*/
|
|
22
|
+
agent?: string
|
|
23
|
+
}
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import type { BackgroundJobBoard } from './background-job-board.ts'
|
|
20
|
-
import type { DelegationClient } from './delegation-finalize.ts'
|
|
21
20
|
import type { GoalStore } from './goal-store.ts'
|
|
22
21
|
import type { TodoEnforcer } from './todo-enforcer.ts'
|
|
23
22
|
|
|
@@ -57,8 +56,6 @@ export interface VisionHandler {
|
|
|
57
56
|
export interface PantheonV2Bridge {
|
|
58
57
|
/** Background job board — tracks running/completed/cancelled delegated jobs. */
|
|
59
58
|
board?: BackgroundJobBoard
|
|
60
|
-
/** Structural delegation client — creates child sessions and sends prompts. */
|
|
61
|
-
delegationClient?: DelegationClient
|
|
62
59
|
/** Goal store — file-based persistence for full-auto goal loop. */
|
|
63
60
|
goalStore?: GoalStore
|
|
64
61
|
/** Todo enforcer — idle continuation when todos are incomplete. */
|
|
@@ -85,7 +82,7 @@ export const BRIDGE_OPTIONS_KEY = '__pantheonV1Bridge' as const
|
|
|
85
82
|
* @example
|
|
86
83
|
* ```ts
|
|
87
84
|
* // In V1 plugin setup (plugin.ts):
|
|
88
|
-
* const bridge = createV2Bridge({ board,
|
|
85
|
+
* const bridge = createV2Bridge({ board, goalStore, ... })
|
|
89
86
|
* // Then pass bridge to V2 context via options
|
|
90
87
|
* ```
|
|
91
88
|
*/
|
package/src/plugin-v2.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Pantheon V2 Plugin — full orchestration plugin for OpenCode V2.
|
|
3
3
|
*
|
|
4
4
|
* Registers:
|
|
5
|
-
* -
|
|
5
|
+
* - 6 orchestration tools via V2 tool.transform (with V1 bridge fallback)
|
|
6
6
|
* - 4 event subscriptions (session.created, idle, error, compacted)
|
|
7
7
|
* - Session hooks (prompt, context) and tool hooks (execute.before/after)
|
|
8
8
|
* - Permission hooks for custom authorization
|
|
@@ -408,11 +408,11 @@ async function onSessionIdle(_event: V2SessionEvent): Promise<void> {
|
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
async function onSessionError(_event: V2SessionEvent): Promise<void> {
|
|
411
|
-
// Error handling —
|
|
411
|
+
// Error handling — board transition to error.
|
|
412
412
|
const bridge = resolveBridge()
|
|
413
413
|
if (bridge?.board != null) {
|
|
414
|
-
// Bridge available:
|
|
415
|
-
// V2 only logs — V1
|
|
414
|
+
// Bridge available: error handling is done by the V1 event hook.
|
|
415
|
+
// V2 only logs — V1 owns the board transition.
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
418
|
|
|
@@ -640,9 +640,6 @@ function createV2ToolDefinitionsFromContext(_context: PluginContext): V2ToolDef[
|
|
|
640
640
|
'infrastructure through the V2 bridge (src/pantheon/v2-bridge.ts).'
|
|
641
641
|
|
|
642
642
|
const toolNames = [
|
|
643
|
-
'pantheon_delegate',
|
|
644
|
-
'pantheon_delegation_read',
|
|
645
|
-
'pantheon_delegation_list',
|
|
646
643
|
'hashline_edit',
|
|
647
644
|
'pantheon_goal_create',
|
|
648
645
|
'pantheon_goal_get',
|
|
@@ -652,14 +649,6 @@ function createV2ToolDefinitionsFromContext(_context: PluginContext): V2ToolDef[
|
|
|
652
649
|
]
|
|
653
650
|
|
|
654
651
|
const toolDescriptions: Record<string, string> = {
|
|
655
|
-
pantheon_delegate:
|
|
656
|
-
'Dispatch a background agent as a child session and register it on the job board. ' +
|
|
657
|
-
'Returns the readable alias (e.g. "apo-1"); read the result with pantheon_delegation_read.',
|
|
658
|
-
pantheon_delegation_read:
|
|
659
|
-
'Block until a background delegation finishes (completed/error/cancelled), then return ' +
|
|
660
|
-
'its report markdown (with a trailing agent-activity section) and mark the job reconciled.',
|
|
661
|
-
pantheon_delegation_list:
|
|
662
|
-
'List background delegations for the current session, with [unread] for finished jobs.',
|
|
663
652
|
hashline_edit:
|
|
664
653
|
'Edit a file anchored by hashline refs (LINE#TAG) instead of raw line numbers. ' +
|
|
665
654
|
'Ops: replace, append, prepend, delete. ALL refs validated against ORIGINAL file first.',
|
|
@@ -681,12 +670,10 @@ function createV2ToolDefinitionsFromContext(_context: PluginContext): V2ToolDef[
|
|
|
681
670
|
name,
|
|
682
671
|
description: toolDescriptions[name] ?? `Pantheon tool: ${name}`,
|
|
683
672
|
input: { type: 'object' as const, properties: {} },
|
|
684
|
-
// FOLLOW-UP:
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
// are likewise unwired. Until then, placeholders resolve to the object
|
|
689
|
-
// shape so the beta host never sees a bare string.
|
|
673
|
+
// FOLLOW-UP: wire the real tool wrappers (costCommand/goalTools) once V1
|
|
674
|
+
// infrastructure is resolvable in the V2 standalone context. Until then,
|
|
675
|
+
// placeholders resolve to the object shape so the beta host never sees a
|
|
676
|
+
// bare string.
|
|
690
677
|
execute: async (_input: Record<string, unknown>, _context: unknown): Promise<V2ToolResult> => {
|
|
691
678
|
return { output: `${name}: ${v2UnavailableMessage}` }
|
|
692
679
|
},
|