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.
Files changed (133) hide show
  1. package/AGENTS.global.md +206 -0
  2. package/AGENTS.md +10 -891
  3. package/CHANGELOG.md +350 -0
  4. package/README.md +561 -52
  5. package/README.pt-BR.md +285 -20
  6. package/bin/pantheon-init.mjs +70 -4
  7. package/commands/pantheon-audit.md +7 -1
  8. package/commands/pantheon-consolidate.md +1 -1
  9. package/commands/pantheon-deepwork.md +14 -3
  10. package/commands/pantheon-focus.md +17 -0
  11. package/commands/pantheon-vision.md +61 -12
  12. package/package.json +24 -17
  13. package/plugin.json +2 -2
  14. package/pyproject.toml +8 -6
  15. package/scripts/build-agents-md.mjs +428 -56
  16. package/scripts/changelog-merge.mjs +161 -0
  17. package/scripts/{code_mode_server.py → code_mode.py} +3 -3
  18. package/scripts/cost.mjs +244 -41
  19. package/scripts/doctor.mjs +151 -1
  20. package/scripts/install/agents-md.mjs +4 -4
  21. package/scripts/install/backup.mjs +475 -0
  22. package/scripts/install/cli-ui.mjs +6 -1
  23. package/scripts/install/config-migration.mjs +317 -4
  24. package/scripts/install/exit-codes.mjs +26 -0
  25. package/scripts/install/health-check.mjs +21 -19
  26. package/scripts/install/interactive.mjs +16 -2
  27. package/scripts/install/migrate.mjs +41 -22
  28. package/scripts/install/opencode-version.mjs +246 -4
  29. package/scripts/install/opencode.mjs +1175 -215
  30. package/scripts/install/plan.mjs +287 -0
  31. package/scripts/install/plugin.mjs +62 -14
  32. package/scripts/install/shared.mjs +190 -48
  33. package/scripts/install/strings.mjs +20 -0
  34. package/scripts/install/venv.mjs +13 -3
  35. package/scripts/install/vision-prompt.mjs +171 -0
  36. package/scripts/install-mcp.mjs +28 -9
  37. package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +52 -17
  38. package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
  39. package/scripts/{memory_mcp_server.py → memory_mcp.py} +2 -2
  40. package/scripts/probe-context-rehydrate.mjs +8 -2
  41. package/scripts/probe-pantheon-cost.mjs +71 -15
  42. package/scripts/prune.mjs +9 -3
  43. package/scripts/restore.mjs +222 -0
  44. package/scripts/run-node-tests.mjs +189 -0
  45. package/scripts/sync-artifacts.mjs +26 -13
  46. package/scripts/sync-tui.mjs +24 -3
  47. package/scripts/test-opencode-v2-sandbox.sh +180 -26
  48. package/scripts/validate-routing.mjs +3 -3
  49. package/scripts/validate-sandbox-install.mjs +138 -0
  50. package/scripts/versioning.mjs +13 -1
  51. package/scripts/vision-status.mjs +682 -0
  52. package/skills-lock.json +8 -8
  53. package/src/agents/aphrodite.md +20 -37
  54. package/src/agents/apollo.md +7 -3
  55. package/src/agents/athena.md +63 -35
  56. package/src/agents/demeter.md +8 -28
  57. package/src/agents/gaia.md +1 -1
  58. package/src/agents/hephaestus.md +10 -21
  59. package/src/agents/hermes.md +15 -38
  60. package/src/agents/iris.md +7 -1
  61. package/src/agents/mnemosyne.md +18 -54
  62. package/src/agents/nyx.md +1 -1
  63. package/src/agents/prometheus.md +16 -20
  64. package/src/agents/talos.md +10 -2
  65. package/src/agents/themis.md +5 -5
  66. package/src/agents/zeus.md +99 -172
  67. package/src/instructions/agent-return-format.instructions.md +34 -17
  68. package/src/instructions/backend-standards.instructions.md +2 -0
  69. package/src/instructions/frontend-standards.instructions.md +3 -1
  70. package/src/instructions/memory-protocol.instructions.md +12 -110
  71. package/src/instructions/test-economy.instructions.md +21 -0
  72. package/src/instructions/zeus-anti-stall.instructions.md +20 -67
  73. package/src/instructions/zeus-communication-rules.instructions.md +0 -1
  74. package/src/instructions/zeus-council-synthesis.instructions.md +31 -260
  75. package/src/instructions/zeus-memory-operations.instructions.md +30 -0
  76. package/src/instructions/zeus-timeout-retry.instructions.md +61 -89
  77. package/src/mcp/{code_mode_server.py → code_mode.py} +3 -3
  78. package/src/mcp/eval_store.py +2 -2
  79. package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +52 -17
  80. package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
  81. package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +2 -2
  82. package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +286 -42
  83. package/src/mcp/requirements-mcp.txt +6 -4
  84. package/src/mcp/requirements-vision.txt +2 -2
  85. package/src/pantheon/cost-command.ts +403 -102
  86. package/src/pantheon/delegation-enforce.ts +10 -1
  87. package/src/pantheon/model-agents.ts +19 -0
  88. package/src/pantheon/model-command.ts +218 -56
  89. package/src/pantheon/presets.d.mts +0 -8
  90. package/src/pantheon/presets.mjs +5 -32
  91. package/src/pantheon/v2-bounded-read.ts +640 -0
  92. package/src/pantheon/v2-delegation-enforce.ts +214 -0
  93. package/src/pantheon/v2-execute-after.ts +186 -0
  94. package/src/pantheon/v2-model-overrides.ts +275 -0
  95. package/src/pantheon/v2-tools.ts +418 -0
  96. package/src/pantheon/v2-unsupported.d.mts +11 -0
  97. package/src/pantheon/v2-unsupported.mjs +90 -0
  98. package/src/pantheon/v2-vision.ts +622 -0
  99. package/src/pantheon/vision-local.ts +531 -0
  100. package/src/pantheon/vision.ts +1160 -115
  101. package/src/plugin-v2.ts +1158 -219
  102. package/src/plugin.ts +12 -9
  103. package/src/plugins/tui/dist/server.d.ts +1 -2
  104. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  105. package/src/plugins/tui/dist/tui-v2.d.ts +154 -0
  106. package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
  107. package/src/plugins/tui/dist/tui-v2.js +659 -0
  108. package/src/plugins/tui/dist/tui-v2.js.map +1 -0
  109. package/src/plugins/tui/dist/tui.d.ts +85 -82
  110. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  111. package/src/plugins/tui/dist/tui.js +72 -59
  112. package/src/plugins/tui/index-v2.ts +6 -0
  113. package/src/plugins/tui/package-lock.json +5516 -1353
  114. package/src/plugins/tui/package.json +22 -12
  115. package/src/plugins/tui/src/index-v2.tsx +734 -0
  116. package/src/plugins/tui/src/index.tsx +106 -85
  117. package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
  118. package/src/plugins/tui/tsconfig.build.json +5 -0
  119. package/src/plugins/tui/tsconfig.typecheck.json +5 -0
  120. package/src/plugins/tui/tsdown.config.ts +22 -2
  121. package/src/plugins/tui/tui-v2.ts +2 -0
  122. package/src/plugins/tui/tui.ts +3 -0
  123. package/src/routing.yml +11 -21
  124. package/src/skills/README.md +3 -3
  125. package/src/skills/agent-coordination/SKILL.md +7 -2
  126. package/src/skills/artifact-management/SKILL.md +12 -5
  127. package/src/skills/auto-continue/SKILL.md +52 -35
  128. package/src/skills/code-review-checklist/SKILL.md +5 -5
  129. package/src/skills/context-compression/SKILL.md +26 -879
  130. package/src/skills/session-goal/SKILL.md +14 -5
  131. package/src/skills/spec-driven-development/SKILL.md +3 -1
  132. package/src/skills/tdd-with-agents/SKILL.md +18 -10
  133. 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
+ }