pantheon-opencode 1.6.0-beta.1 → 1.6.0-beta.11
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.global.md +206 -0
- package/AGENTS.md +10 -891
- package/CHANGELOG.md +350 -0
- package/README.md +561 -52
- package/README.pt-BR.md +285 -20
- package/bin/pantheon-init.mjs +70 -4
- package/commands/pantheon-audit.md +7 -1
- package/commands/pantheon-consolidate.md +1 -1
- package/commands/pantheon-deepwork.md +14 -3
- package/commands/pantheon-focus.md +17 -0
- package/commands/pantheon-vision.md +61 -12
- package/package.json +24 -17
- package/plugin.json +2 -2
- package/pyproject.toml +8 -6
- package/scripts/build-agents-md.mjs +428 -56
- package/scripts/changelog-merge.mjs +161 -0
- package/scripts/{code_mode_server.py → code_mode.py} +3 -3
- package/scripts/cost.mjs +244 -41
- package/scripts/doctor.mjs +151 -1
- package/scripts/install/agents-md.mjs +4 -4
- package/scripts/install/backup.mjs +475 -0
- package/scripts/install/cli-ui.mjs +6 -1
- package/scripts/install/config-migration.mjs +317 -4
- package/scripts/install/exit-codes.mjs +26 -0
- package/scripts/install/health-check.mjs +21 -19
- package/scripts/install/interactive.mjs +16 -2
- package/scripts/install/migrate.mjs +41 -22
- package/scripts/install/opencode-version.mjs +246 -4
- package/scripts/install/opencode.mjs +1175 -215
- package/scripts/install/plan.mjs +287 -0
- package/scripts/install/plugin.mjs +62 -14
- package/scripts/install/shared.mjs +190 -48
- package/scripts/install/strings.mjs +20 -0
- package/scripts/install/venv.mjs +13 -3
- package/scripts/install/vision-prompt.mjs +171 -0
- package/scripts/install-mcp.mjs +28 -9
- package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +52 -17
- package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
- package/scripts/{memory_mcp_server.py → memory_mcp.py} +2 -2
- package/scripts/probe-context-rehydrate.mjs +8 -2
- package/scripts/probe-pantheon-cost.mjs +71 -15
- package/scripts/prune.mjs +9 -3
- package/scripts/restore.mjs +222 -0
- package/scripts/run-node-tests.mjs +189 -0
- package/scripts/sync-artifacts.mjs +26 -13
- package/scripts/sync-tui.mjs +24 -3
- package/scripts/test-opencode-v2-sandbox.sh +180 -26
- package/scripts/validate-routing.mjs +3 -3
- package/scripts/validate-sandbox-install.mjs +138 -0
- package/scripts/versioning.mjs +13 -1
- package/scripts/vision-status.mjs +682 -0
- package/skills-lock.json +8 -8
- package/src/agents/aphrodite.md +20 -37
- package/src/agents/apollo.md +7 -3
- package/src/agents/athena.md +63 -35
- package/src/agents/demeter.md +8 -28
- package/src/agents/gaia.md +1 -1
- package/src/agents/hephaestus.md +10 -21
- package/src/agents/hermes.md +15 -38
- package/src/agents/iris.md +7 -1
- package/src/agents/mnemosyne.md +18 -54
- package/src/agents/nyx.md +1 -1
- package/src/agents/prometheus.md +16 -20
- package/src/agents/talos.md +10 -2
- package/src/agents/themis.md +5 -5
- package/src/agents/zeus.md +99 -172
- package/src/instructions/agent-return-format.instructions.md +34 -17
- package/src/instructions/backend-standards.instructions.md +2 -0
- package/src/instructions/frontend-standards.instructions.md +3 -1
- package/src/instructions/memory-protocol.instructions.md +12 -110
- package/src/instructions/test-economy.instructions.md +21 -0
- package/src/instructions/zeus-anti-stall.instructions.md +20 -67
- package/src/instructions/zeus-communication-rules.instructions.md +0 -1
- package/src/instructions/zeus-council-synthesis.instructions.md +31 -260
- package/src/instructions/zeus-memory-operations.instructions.md +30 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +61 -89
- package/src/mcp/{code_mode_server.py → code_mode.py} +3 -3
- package/src/mcp/eval_store.py +2 -2
- package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +52 -17
- package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
- package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +2 -2
- package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +286 -42
- package/src/mcp/requirements-mcp.txt +6 -4
- package/src/mcp/requirements-vision.txt +2 -2
- package/src/pantheon/cost-command.ts +403 -102
- package/src/pantheon/delegation-enforce.ts +10 -1
- package/src/pantheon/model-agents.ts +19 -0
- package/src/pantheon/model-command.ts +218 -56
- package/src/pantheon/presets.d.mts +0 -8
- package/src/pantheon/presets.mjs +5 -32
- package/src/pantheon/v2-bounded-read.ts +640 -0
- package/src/pantheon/v2-delegation-enforce.ts +214 -0
- package/src/pantheon/v2-execute-after.ts +186 -0
- package/src/pantheon/v2-model-overrides.ts +275 -0
- package/src/pantheon/v2-tools.ts +418 -0
- package/src/pantheon/v2-unsupported.d.mts +11 -0
- package/src/pantheon/v2-unsupported.mjs +90 -0
- package/src/pantheon/v2-vision.ts +622 -0
- package/src/pantheon/vision-local.ts +531 -0
- package/src/pantheon/vision.ts +1160 -115
- package/src/plugin-v2.ts +1158 -219
- package/src/plugin.ts +12 -9
- package/src/plugins/tui/dist/server.d.ts +1 -2
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui-v2.d.ts +154 -0
- package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui-v2.js +659 -0
- package/src/plugins/tui/dist/tui-v2.js.map +1 -0
- package/src/plugins/tui/dist/tui.d.ts +85 -82
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +72 -59
- package/src/plugins/tui/index-v2.ts +6 -0
- package/src/plugins/tui/package-lock.json +5516 -1353
- package/src/plugins/tui/package.json +22 -12
- package/src/plugins/tui/src/index-v2.tsx +734 -0
- package/src/plugins/tui/src/index.tsx +106 -85
- package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
- package/src/plugins/tui/tsconfig.build.json +5 -0
- package/src/plugins/tui/tsconfig.typecheck.json +5 -0
- package/src/plugins/tui/tsdown.config.ts +22 -2
- package/src/plugins/tui/tui-v2.ts +2 -0
- package/src/plugins/tui/tui.ts +3 -0
- package/src/routing.yml +11 -21
- package/src/skills/README.md +3 -3
- package/src/skills/agent-coordination/SKILL.md +7 -2
- package/src/skills/artifact-management/SKILL.md +12 -5
- package/src/skills/auto-continue/SKILL.md +52 -35
- package/src/skills/code-review-checklist/SKILL.md +5 -5
- package/src/skills/context-compression/SKILL.md +26 -879
- package/src/skills/session-goal/SKILL.md +14 -5
- package/src/skills/spec-driven-development/SKILL.md +3 -1
- package/src/skills/tdd-with-agents/SKILL.md +18 -10
- package/opencode.json +0 -58
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2 tool surface — the Pantheon tools that are self-sufficient on the
|
|
3
|
+
* OpenCode V2 host, declared for `ctx.tool.transform`'s `draft.add`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why only three tools
|
|
6
|
+
*
|
|
7
|
+
* A V2 tool can only be served here when it needs no V1 infrastructure. The
|
|
8
|
+
* three goal tools (`pantheon_goal_create`, `pantheon_goal_get`,
|
|
9
|
+
* `pantheon_goal_update`) are NOT: `GoalLoopDeps` requires a `GoalStore`, a
|
|
10
|
+
* `GoalLoopClient` and a `BackgroundJobBoard`, none of which exist on the V2
|
|
11
|
+
* `PluginContext`, and `getV2BridgeFromContext` resolves to `null` outside V1.
|
|
12
|
+
* They are therefore absent from this surface rather than registered as
|
|
13
|
+
* placeholders that resolve to a message — a placeholder still fails the host's
|
|
14
|
+
* output contract on every call, so it buys nothing. See
|
|
15
|
+
* `V2_UNSUPPORTED_FEATURES` in `../plugin-v2.ts` (marker `goal-tools`).
|
|
16
|
+
*
|
|
17
|
+
* `hashline_edit` (pure filesystem), `pantheon_cost` (reads `opencode.db`) and
|
|
18
|
+
* `pantheon_model` (reads/writes `active-preset.json`) need none of that, so
|
|
19
|
+
* they are wired to their real V1 implementations.
|
|
20
|
+
*
|
|
21
|
+
* ## The output declaration is mandatory
|
|
22
|
+
*
|
|
23
|
+
* The host enforces a biconditional between a tool's declared `output` and the
|
|
24
|
+
* value its `execute` resolves to (opencode 2.0.22):
|
|
25
|
+
*
|
|
26
|
+
* `def.output === undefined` and `'output' in result` -> throws
|
|
27
|
+
* "Tool result declared output without an output schema"
|
|
28
|
+
* `def.output !== undefined` and `!('output' in result)` -> throws
|
|
29
|
+
* "Tool did not return its declared output"
|
|
30
|
+
*
|
|
31
|
+
* Declaring `output` and returning it are therefore one decision, not two. The
|
|
32
|
+
* first branch is the historical Pantheon defect; the second is why the two
|
|
33
|
+
* must move together. `V2ToolDef` makes the field required so TypeScript
|
|
34
|
+
* rejects a tool that forgets it.
|
|
35
|
+
*
|
|
36
|
+
* @module pantheon/v2-tools
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { createCostCommand } from './cost-command.ts'
|
|
40
|
+
import { createHashlineEditTool, type HashlineToolResult } from './hashline/tool.ts'
|
|
41
|
+
import { createModelCommand } from './model-command.ts'
|
|
42
|
+
import type { ToolContextLike } from './tool-context.ts'
|
|
43
|
+
import { createV2VisionTool, type V2VisionRuntime } from './v2-vision.ts'
|
|
44
|
+
|
|
45
|
+
// ─── Contract types ─────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* V2 tool result shape.
|
|
49
|
+
*
|
|
50
|
+
* The V1 SDK types `ToolResult` as `string | { output: string, ... }`, but the
|
|
51
|
+
* V2 beta host reads a field off the resolved value and throws when a tool
|
|
52
|
+
* resolves to a bare string. Always resolve to the object shape — it satisfies
|
|
53
|
+
* both the SDK union and the beta host.
|
|
54
|
+
*
|
|
55
|
+
* `title` and `metadata` are part of the shape because `hashline_edit` returns
|
|
56
|
+
* them on success (see `HashlineToolResult`); the host spreads `metadata`
|
|
57
|
+
* through when present, so they are carried rather than dropped.
|
|
58
|
+
*/
|
|
59
|
+
export interface V2ToolResult {
|
|
60
|
+
output: string
|
|
61
|
+
title?: string
|
|
62
|
+
metadata?: Record<string, unknown>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A V2 tool definition ready for registration.
|
|
67
|
+
*
|
|
68
|
+
* `output` is REQUIRED, not optional: an omitted declaration is precisely the
|
|
69
|
+
* condition the host rejects at runtime, so making it optional would let the
|
|
70
|
+
* defect return silently.
|
|
71
|
+
*/
|
|
72
|
+
export interface V2ToolDef {
|
|
73
|
+
name: string
|
|
74
|
+
description: string
|
|
75
|
+
input: Record<string, unknown>
|
|
76
|
+
output: Record<string, unknown>
|
|
77
|
+
execute: (input: Record<string, unknown>, context: unknown) => Promise<V2ToolResult>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Context adapter ────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Project the host's V2 tool context onto the structural `ToolContextLike`.
|
|
84
|
+
*
|
|
85
|
+
* The host may omit `directory`/`worktree`; `hashline_edit` then falls back to
|
|
86
|
+
* `process.cwd()` through its own `resolveFile` containment guard, so an absent
|
|
87
|
+
* field degrades safely rather than throwing.
|
|
88
|
+
*/
|
|
89
|
+
function toolContext(context: unknown): ToolContextLike {
|
|
90
|
+
const ctx = (context ?? {}) as Partial<ToolContextLike>
|
|
91
|
+
return {
|
|
92
|
+
sessionID: typeof ctx.sessionID === 'string' ? ctx.sessionID : 'v2-unknown',
|
|
93
|
+
// Spread rather than assign `undefined`: the project compiles with
|
|
94
|
+
// exactOptionalPropertyTypes, so an explicit undefined is not assignable.
|
|
95
|
+
...(typeof ctx.directory === 'string' ? { directory: ctx.directory } : {}),
|
|
96
|
+
...(typeof ctx.worktree === 'string' ? { worktree: ctx.worktree } : {}),
|
|
97
|
+
...(typeof ctx.agent === 'string' ? { agent: ctx.agent } : {}),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Error text for an unexpected throw, without leaking a stack trace. */
|
|
102
|
+
function errorText(prefix: string, err: unknown): string {
|
|
103
|
+
return `${prefix}: ${err instanceof Error ? err.message : String(err)}`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── hashline_edit ──────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/** JSON Schema for `hashline_edit`, mirroring the V1 zod args exactly. */
|
|
109
|
+
const HASHLINE_EDIT_INPUT: Record<string, unknown> = {
|
|
110
|
+
type: 'object',
|
|
111
|
+
additionalProperties: false,
|
|
112
|
+
required: ['file', 'edits'],
|
|
113
|
+
properties: {
|
|
114
|
+
file: {
|
|
115
|
+
type: 'string',
|
|
116
|
+
minLength: 1,
|
|
117
|
+
description: 'File to edit. Absolute, or relative to the worktree.',
|
|
118
|
+
},
|
|
119
|
+
edits: {
|
|
120
|
+
type: 'array',
|
|
121
|
+
minItems: 1,
|
|
122
|
+
description:
|
|
123
|
+
'Edits to apply. All refs are validated against the ORIGINAL file before anything is written.',
|
|
124
|
+
items: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
additionalProperties: false,
|
|
127
|
+
required: ['op', 'ref'],
|
|
128
|
+
properties: {
|
|
129
|
+
op: { type: 'string', enum: ['replace', 'append', 'prepend', 'delete'] },
|
|
130
|
+
ref: {
|
|
131
|
+
type: 'string',
|
|
132
|
+
minLength: 1,
|
|
133
|
+
description: 'Anchor ref "LINE#TAG" against the ORIGINAL snapshot.',
|
|
134
|
+
},
|
|
135
|
+
endRef: {
|
|
136
|
+
type: 'string',
|
|
137
|
+
minLength: 1,
|
|
138
|
+
description: 'Optional range end for replace/delete, as "LINE#TAG".',
|
|
139
|
+
},
|
|
140
|
+
lines: {
|
|
141
|
+
type: 'array',
|
|
142
|
+
items: { type: 'string' },
|
|
143
|
+
description: 'Replacement lines (replace only).',
|
|
144
|
+
},
|
|
145
|
+
content: {
|
|
146
|
+
type: 'string',
|
|
147
|
+
description: 'Text to insert (append/prepend); split on newlines.',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Coerced `hashline_edit` args, structurally equal to the V1 zod inference. */
|
|
156
|
+
interface HashlineEditArgs {
|
|
157
|
+
file: string
|
|
158
|
+
edits: Array<{
|
|
159
|
+
op: 'replace' | 'append' | 'prepend' | 'delete'
|
|
160
|
+
ref: string
|
|
161
|
+
endRef?: string
|
|
162
|
+
lines?: string[]
|
|
163
|
+
content?: string
|
|
164
|
+
}>
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const HASHLINE_OPS = new Set(['replace', 'append', 'prepend', 'delete'])
|
|
168
|
+
|
|
169
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
170
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Re-validate the host-provided input against the declared JSON Schema.
|
|
175
|
+
*
|
|
176
|
+
* The host validates `input` before `execute`, but this wrapper is also the
|
|
177
|
+
* direct programmatic entry point (tests, embedded callers), so it must not
|
|
178
|
+
* assume a pre-validated shape. Failures are returned as error-as-text, the
|
|
179
|
+
* same contract the V1 tool uses for user errors.
|
|
180
|
+
*/
|
|
181
|
+
function coerceHashlineEditArgs(input: Record<string, unknown>):
|
|
182
|
+
| {
|
|
183
|
+
ok: true
|
|
184
|
+
args: HashlineEditArgs
|
|
185
|
+
}
|
|
186
|
+
| { ok: false; error: string } {
|
|
187
|
+
const file = input.file
|
|
188
|
+
if (typeof file !== 'string' || file.length === 0) {
|
|
189
|
+
return { ok: false, error: 'hashline_edit: "file" must be a non-empty string' }
|
|
190
|
+
}
|
|
191
|
+
const rawEdits = input.edits
|
|
192
|
+
if (!Array.isArray(rawEdits) || rawEdits.length === 0) {
|
|
193
|
+
return { ok: false, error: 'hashline_edit: "edits" must be a non-empty array' }
|
|
194
|
+
}
|
|
195
|
+
const edits: HashlineEditArgs['edits'] = []
|
|
196
|
+
for (const [i, raw] of rawEdits.entries()) {
|
|
197
|
+
if (!isRecord(raw)) {
|
|
198
|
+
return { ok: false, error: `hashline_edit: edit (index ${i}) must be an object` }
|
|
199
|
+
}
|
|
200
|
+
const op = raw.op
|
|
201
|
+
if (typeof op !== 'string' || !HASHLINE_OPS.has(op)) {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
error: `hashline_edit: edit (index ${i}) "op" must be one of replace, append, prepend, delete`,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const ref = raw.ref
|
|
208
|
+
if (typeof ref !== 'string' || ref.length === 0) {
|
|
209
|
+
return {
|
|
210
|
+
ok: false,
|
|
211
|
+
error: `hashline_edit: edit (index ${i}) "ref" must be a non-empty string`,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
let lines: string[] | undefined
|
|
215
|
+
if (raw.lines !== undefined) {
|
|
216
|
+
if (!Array.isArray(raw.lines) || raw.lines.some((l) => typeof l !== 'string')) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
error: `hashline_edit: edit (index ${i}) "lines" must be a string array`,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
lines = raw.lines as string[]
|
|
223
|
+
}
|
|
224
|
+
if (raw.content !== undefined && typeof raw.content !== 'string') {
|
|
225
|
+
return { ok: false, error: `hashline_edit: edit (index ${i}) "content" must be a string` }
|
|
226
|
+
}
|
|
227
|
+
if (raw.endRef !== undefined && typeof raw.endRef !== 'string') {
|
|
228
|
+
return { ok: false, error: `hashline_edit: edit (index ${i}) "endRef" must be a string` }
|
|
229
|
+
}
|
|
230
|
+
edits.push({
|
|
231
|
+
op: op as HashlineEditArgs['edits'][number]['op'],
|
|
232
|
+
ref,
|
|
233
|
+
// Conditional spread: exactOptionalPropertyTypes forbids `prop: undefined`.
|
|
234
|
+
...(typeof raw.endRef === 'string' ? { endRef: raw.endRef } : {}),
|
|
235
|
+
...(lines === undefined ? {} : { lines }),
|
|
236
|
+
...(typeof raw.content === 'string' ? { content: raw.content } : {}),
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
return { ok: true, args: { file, edits } }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const hashlineEdit = createHashlineEditTool()
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Wrap the V1 `hashline_edit` tool for the V2 surface.
|
|
246
|
+
*
|
|
247
|
+
* The V1 tool returns error-as-text for user errors and the success object
|
|
248
|
+
* `{ title, output, metadata }`. Both are carried: the `output` string always
|
|
249
|
+
* satisfies the host's declaration, and `title`/`metadata` survive because the
|
|
250
|
+
* host spreads metadata through.
|
|
251
|
+
*/
|
|
252
|
+
async function executeHashlineEdit(
|
|
253
|
+
input: Record<string, unknown>,
|
|
254
|
+
context: unknown,
|
|
255
|
+
): Promise<V2ToolResult> {
|
|
256
|
+
const coerced = coerceHashlineEditArgs(input)
|
|
257
|
+
if (!coerced.ok) return { output: coerced.error }
|
|
258
|
+
let result: HashlineToolResult
|
|
259
|
+
try {
|
|
260
|
+
result = await hashlineEdit.execute(coerced.args, toolContext(context))
|
|
261
|
+
} catch (err: unknown) {
|
|
262
|
+
// The V1 tool does not throw for user errors; this is defence in depth so
|
|
263
|
+
// an unexpected throw surfaces as text instead of an absent tool result.
|
|
264
|
+
return { output: errorText('hashline_edit failed', err) }
|
|
265
|
+
}
|
|
266
|
+
if (typeof result === 'string') return { output: result }
|
|
267
|
+
return {
|
|
268
|
+
output: result.output,
|
|
269
|
+
...(result.title === undefined ? {} : { title: result.title }),
|
|
270
|
+
...(result.metadata === undefined ? {} : { metadata: result.metadata }),
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── pantheon_cost ──────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
/** JSON Schema for `pantheon_cost`, mirroring the V1 zod args. */
|
|
277
|
+
const COST_INPUT: Record<string, unknown> = {
|
|
278
|
+
type: 'object',
|
|
279
|
+
additionalProperties: false,
|
|
280
|
+
properties: {
|
|
281
|
+
days: {
|
|
282
|
+
type: 'integer',
|
|
283
|
+
minimum: 1,
|
|
284
|
+
maximum: 365,
|
|
285
|
+
description: 'Reporting window in days (default 7).',
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Validate `days` exactly as the V1 zod schema does; absent means default. */
|
|
291
|
+
function coerceDays(value: unknown): { ok: true; days?: number } | { ok: false; error: string } {
|
|
292
|
+
if (value === undefined) return { ok: true }
|
|
293
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > 365) {
|
|
294
|
+
return { ok: false, error: 'pantheon_cost: "days" must be an integer between 1 and 365' }
|
|
295
|
+
}
|
|
296
|
+
return { ok: true, days: value }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const costCommand = createCostCommand()
|
|
300
|
+
|
|
301
|
+
async function executeCost(
|
|
302
|
+
input: Record<string, unknown>,
|
|
303
|
+
context: unknown,
|
|
304
|
+
): Promise<V2ToolResult> {
|
|
305
|
+
const days = coerceDays(input.days)
|
|
306
|
+
if (!days.ok) return { output: days.error }
|
|
307
|
+
// The V1 tool already wraps its whole body and reports failures as text.
|
|
308
|
+
const output = await costCommand.pantheon_cost.execute({ days: days.days }, toolContext(context))
|
|
309
|
+
return { output }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ─── pantheon_model ─────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
/** JSON Schema for V2 `pantheon_model`; global scope is intentionally absent. */
|
|
315
|
+
const MODEL_INPUT: Record<string, unknown> = {
|
|
316
|
+
type: 'object',
|
|
317
|
+
additionalProperties: false,
|
|
318
|
+
properties: {
|
|
319
|
+
action: {
|
|
320
|
+
type: 'string',
|
|
321
|
+
enum: ['status', 'show', 'set', 'reset'],
|
|
322
|
+
description: 'Operation to perform. Defaults to status when agent/model are absent.',
|
|
323
|
+
},
|
|
324
|
+
agent: { type: 'string', description: 'Agent name (one of the 14 canonical agents).' },
|
|
325
|
+
model: { type: 'string', description: 'Model in provider/model-id format; used by set.' },
|
|
326
|
+
effort: {
|
|
327
|
+
type: 'string',
|
|
328
|
+
enum: ['low', 'medium', 'high'],
|
|
329
|
+
description: 'Reasoning effort (variant), clamped to model capability.',
|
|
330
|
+
},
|
|
331
|
+
scope: {
|
|
332
|
+
type: 'string',
|
|
333
|
+
enum: ['project'],
|
|
334
|
+
description: 'Project scope only; global overrides are not supported in V2.',
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const modelCommand = createModelCommand({ v2Mode: true })
|
|
340
|
+
|
|
341
|
+
async function executeModel(
|
|
342
|
+
input: Record<string, unknown>,
|
|
343
|
+
context: unknown,
|
|
344
|
+
): Promise<V2ToolResult> {
|
|
345
|
+
if (input.scope !== undefined && input.scope !== 'project') {
|
|
346
|
+
return { output: 'Error: V2 pantheon_model is project-only; global scope is not supported.' }
|
|
347
|
+
}
|
|
348
|
+
if (Object.hasOwn(input, 'confirm') || Object.hasOwn(input, 'authorize_global')) {
|
|
349
|
+
return {
|
|
350
|
+
output:
|
|
351
|
+
'Error: V2 pantheon_model does not accept authorization flags; global authorization is not supported by this tool.',
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// The V1 tool validates its own args (zod) and reports failures as text.
|
|
355
|
+
const output = await modelCommand.pantheon_model.execute(
|
|
356
|
+
input as Parameters<typeof modelCommand.pantheon_model.execute>[0],
|
|
357
|
+
toolContext(context),
|
|
358
|
+
)
|
|
359
|
+
return { output }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ─── Tool set ───────────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Build the V2 tool definitions.
|
|
366
|
+
*
|
|
367
|
+
* Every entry declares `output`. An empty object is the host's own marker for
|
|
368
|
+
* "declared, unconstrained" (its MCP bridge uses `output: g.outputSchema ??
|
|
369
|
+
* {}`), so it satisfies the declaration without adding a second way for a tool
|
|
370
|
+
* to fail validation. The protection against forgetting the field is the
|
|
371
|
+
* required `output` on `V2ToolDef`, not the emptiness of this schema.
|
|
372
|
+
*
|
|
373
|
+
* The three tool instances (`hashlineEdit`, `costCommand`, `modelCommand`) are
|
|
374
|
+
* module-level consts created once at import, not per call. What matters for
|
|
375
|
+
* this function is only that it allocates a fresh descriptor array each call,
|
|
376
|
+
* so a caller cannot mutate another's definitions. None of the three factories
|
|
377
|
+
* performs filesystem or database work at construction, so importing this
|
|
378
|
+
* module stays side-effect free.
|
|
379
|
+
*/
|
|
380
|
+
export function createV2ToolDefinitions(visionRuntime?: V2VisionRuntime): V2ToolDef[] {
|
|
381
|
+
const definitions: V2ToolDef[] = [
|
|
382
|
+
{
|
|
383
|
+
name: 'hashline_edit',
|
|
384
|
+
description:
|
|
385
|
+
'Edit a file anchored by hashline refs (LINE#TAG) instead of raw line numbers. ' +
|
|
386
|
+
'Ops: replace (ref..endRef or single ref → lines), append/prepend (anchor ref → content), ' +
|
|
387
|
+
'delete (ref..endRef). ALL refs must be validated against the ORIGINAL file before ' +
|
|
388
|
+
'anything is written; on mismatch the tool returns an error with a re-tagged excerpt ' +
|
|
389
|
+
'and a Did-you-mean suggestion, and nothing is modified.',
|
|
390
|
+
input: HASHLINE_EDIT_INPUT,
|
|
391
|
+
output: {},
|
|
392
|
+
execute: executeHashlineEdit,
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: 'pantheon_cost',
|
|
396
|
+
description:
|
|
397
|
+
'Report input, output, and total token usage by agent and phase over the last N days, ' +
|
|
398
|
+
'read from opencode.db (read-only, no monetary values).',
|
|
399
|
+
input: COST_INPUT,
|
|
400
|
+
output: {},
|
|
401
|
+
execute: executeCost,
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: 'pantheon_model',
|
|
405
|
+
description:
|
|
406
|
+
'Show, set, or reset explicit per-agent project-scoped model overrides in active-preset.json. ' +
|
|
407
|
+
'V2 ignores static preset names and PANTHEON_MODEL_PRESET; no wizard or implicit model ' +
|
|
408
|
+
'selection. Global overrides are not read or written in V2; future global support requires ' +
|
|
409
|
+
'separate explicit authorization. Pass action="status" to read project-scoped overrides. ' +
|
|
410
|
+
'Never writes .env or top-level model/small_model.',
|
|
411
|
+
input: MODEL_INPUT,
|
|
412
|
+
output: {},
|
|
413
|
+
execute: executeModel,
|
|
414
|
+
},
|
|
415
|
+
]
|
|
416
|
+
if (visionRuntime) definitions.push(createV2VisionTool(visionRuntime))
|
|
417
|
+
return definitions
|
|
418
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for v2-unsupported.mjs (the V2 feature-reduction seed).
|
|
3
|
+
* Consumed by src/plugin-v2.ts via `import ... from './pantheon/v2-unsupported.mjs'`,
|
|
4
|
+
* and by the plain-`.mjs` install/health scripts at runtime.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Statically-known V2 feature reductions, in report order. */
|
|
8
|
+
export declare const V2_UNSUPPORTED_FEATURE_SEED: readonly string[]
|
|
9
|
+
|
|
10
|
+
/** A fresh, mutable copy of the seed, so a reporter cannot mutate the shared list. */
|
|
11
|
+
export declare function listV2UnsupportedFeatures(): string[]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* v2-unsupported.mjs — the SEED of V2 feature reductions, in plain ESM.
|
|
4
|
+
*
|
|
5
|
+
* WHY A SEPARATE PLAIN-`.mjs` MODULE: these strings must be readable by three
|
|
6
|
+
* very different consumers, and only one of them can import TypeScript.
|
|
7
|
+
*
|
|
8
|
+
* - `src/plugin-v2.ts` (TypeScript) — owns the LIVE list, appending to it at
|
|
9
|
+
* runtime via `markUnsupported()` when a host API turns out to be missing,
|
|
10
|
+
* and exposes it through `getUnsupportedFeatures()`.
|
|
11
|
+
* - `scripts/install/opencode.mjs` — reports the reduction at install time
|
|
12
|
+
* for a V2-generation install.
|
|
13
|
+
* - `scripts/doctor.mjs` — reports it in the health gate.
|
|
14
|
+
*
|
|
15
|
+
* The scripts are plain Node ESM and must stay that way, and `.mjs` is a
|
|
16
|
+
* REPO-WIDE INVARIANT rather than a judgement about Node: `tsx` is a
|
|
17
|
+
* devDependency only, and no plain-Node script in `scripts/` or `bin/` imports
|
|
18
|
+
* a `.ts` module — every `.ts` path there is a string to copy, stat or read as
|
|
19
|
+
* text (`scripts/install/opencode.mjs`, `scripts/probe-pantheon-cost.mjs`,
|
|
20
|
+
* `scripts/package-evidence.mjs`, `scripts/check-plugin-v2-coverage.mjs`).
|
|
21
|
+
* So a `.mjs` import is the only form all three consumers can share, and it
|
|
22
|
+
* matches the existing `src/pantheon/presets.mjs` + `.d.mts` precedent.
|
|
23
|
+
*
|
|
24
|
+
* NOT the reason: type-stripping. `package.json#engines` admits
|
|
25
|
+
* `^22.22.2 || ^24.15.0 || >=26.0.0`, every admitted version already defaults to
|
|
26
|
+
* unflagged type-stripping (it landed in 22.18.0), so that argument would not
|
|
27
|
+
* have held on any admitted version. The live hazard is that `engines` is not
|
|
28
|
+
* ENFORCED: a Node 20 user can run `doctor` and would get no type-stripping,
|
|
29
|
+
* which is an argument for a runtime version check, not for this file's shape.
|
|
30
|
+
*
|
|
31
|
+
* The SEED is the statically-known half of `getUnsupportedFeatures()`: what a
|
|
32
|
+
* migrating user loses by choosing the V2 contract, knowable with no host at
|
|
33
|
+
* all. Runtime-appended entries (`tool-transform`, `event-stream`, …) are
|
|
34
|
+
* observations about one live host and are deliberately NOT seeded here — they
|
|
35
|
+
* are not known until the plugin runs against a real V2 host.
|
|
36
|
+
*
|
|
37
|
+
* ONE SOURCE OF TRUTH: install and doctor both read the seed from here, so the
|
|
38
|
+
* two cannot drift apart, and what they report IS what this file lists. Note the
|
|
39
|
+
* scope of that claim: it holds between the SEED and the two reporters. The
|
|
40
|
+
* LIVE list `getUnsupportedFeatures()` returns is this seed PLUS markers appended
|
|
41
|
+
* at runtime (`tool-transform`, `event-stream`, …), so it is a superset and the
|
|
42
|
+
* two are expected to differ there. Do not copy these strings into a script;
|
|
43
|
+
* import them.
|
|
44
|
+
*
|
|
45
|
+
* @module v2-unsupported
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Statically-known V2 feature reductions, in the order a reader should meet
|
|
50
|
+
* them. Frozen: `plugin-v2.ts` copies it into its own mutable live array, and
|
|
51
|
+
* every other consumer goes through {@link listV2UnsupportedFeatures}.
|
|
52
|
+
*
|
|
53
|
+
* @type {readonly string[]}
|
|
54
|
+
*/
|
|
55
|
+
export const V2_UNSUPPORTED_FEATURE_SEED = Object.freeze([
|
|
56
|
+
'legacy-hooks',
|
|
57
|
+
// `ctx.catalog` is the one domain a live 2.0.22 host does NOT have: it is
|
|
58
|
+
// absent from `Object.keys(ctx)`, measured in
|
|
59
|
+
// tests/canary/plugin-v2-tool-canary.test.mjs. (`catalog` IS in the 1.18.33
|
|
60
|
+
// SDK's PluginContext, so the SDK types it and the host does not provide it.)
|
|
61
|
+
'catalog-transform',
|
|
62
|
+
// Adapter support, NOT host availability: 2.0.22 exposes ctx.integration and
|
|
63
|
+
// ctx.skill with callable transforms, and Pantheon deliberately registers
|
|
64
|
+
// neither. These entries record what this plugin does not implement.
|
|
65
|
+
'integration-transform',
|
|
66
|
+
// The inspected SkillEditor shape has no `source()` helper — i.e. no
|
|
67
|
+
// `SkillEditor.source()` — for adding a directory source. This is narrower
|
|
68
|
+
// than (and distinct from) ctx.skill's host availability or callable
|
|
69
|
+
// transform, which a live 2.0.22 host does provide.
|
|
70
|
+
'skill-transform',
|
|
71
|
+
// Adapter limitation, not a host gap: the goal loop needs a GoalStore, a
|
|
72
|
+
// GoalLoopClient and a BackgroundJobBoard, none of which the V2
|
|
73
|
+
// PluginContext exposes, and the V1 bridge resolves to null outside V1.
|
|
74
|
+
// pantheon_goal_create/get/update are therefore absent from the V2 surface
|
|
75
|
+
// rather than registered as non-functional placeholders.
|
|
76
|
+
'goal-tools',
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A fresh, mutable copy of the seed.
|
|
81
|
+
*
|
|
82
|
+
* Callers get a copy so a reporter that sorts, filters or appends cannot reach
|
|
83
|
+
* back into the shared list — this is what makes "both surfaces report the same
|
|
84
|
+
* strings" a structural property rather than a convention.
|
|
85
|
+
*
|
|
86
|
+
* @returns {string[]}
|
|
87
|
+
*/
|
|
88
|
+
export function listV2UnsupportedFeatures() {
|
|
89
|
+
return [...V2_UNSUPPORTED_FEATURE_SEED]
|
|
90
|
+
}
|