orizu 0.5.30 → 0.5.32

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 (82) hide show
  1. package/dist/app-commands.d.ts +2 -0
  2. package/dist/app-commands.d.ts.map +1 -1
  3. package/dist/app-commands.js +10 -4
  4. package/dist/gepa-engine-dispatch.d.ts.map +1 -1
  5. package/dist/gepa-engine-dispatch.js +16 -1
  6. package/dist/help.js +7 -7
  7. package/dist/hosted-agent-prompt.d.ts +14 -0
  8. package/dist/hosted-agent-prompt.d.ts.map +1 -0
  9. package/dist/hosted-agent-prompt.js +17 -0
  10. package/dist/hosted-gepa-cli.d.ts.map +1 -1
  11. package/dist/hosted-gepa-cli.js +7 -1
  12. package/dist/hosted-harness-claude.d.ts.map +1 -1
  13. package/dist/hosted-harness-claude.js +4 -1
  14. package/dist/hosted-harness-opencode.d.ts +1 -0
  15. package/dist/hosted-harness-opencode.d.ts.map +1 -1
  16. package/dist/hosted-harness-opencode.js +27 -7
  17. package/dist/hosted-harness.d.ts +30 -1
  18. package/dist/hosted-harness.d.ts.map +1 -1
  19. package/dist/hosted-harness.js +5 -0
  20. package/dist/hosted-optimization-prebaked-marker.d.ts +9 -0
  21. package/dist/hosted-optimization-prebaked-marker.d.ts.map +1 -0
  22. package/dist/hosted-optimization-prebaked-marker.js +22 -0
  23. package/dist/hosted-tool-hints.d.ts +56 -0
  24. package/dist/hosted-tool-hints.d.ts.map +1 -0
  25. package/dist/hosted-tool-hints.js +105 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -5
  28. package/dist/instruction-set-gepa-launch.d.ts +21 -2
  29. package/dist/instruction-set-gepa-launch.d.ts.map +1 -1
  30. package/dist/instruction-set-gepa-launch.js +123 -3
  31. package/dist/markdown-report-input.d.ts.map +1 -1
  32. package/dist/markdown-report-input.js +1 -1
  33. package/dist/optimizations-promote-cli.d.ts.map +1 -1
  34. package/dist/optimizations-promote-cli.js +19 -0
  35. package/dist/preview-runtime.d.ts +7 -0
  36. package/dist/preview-runtime.d.ts.map +1 -1
  37. package/dist/preview-runtime.js +49 -5
  38. package/dist/provider-refusals.js +2 -2
  39. package/dist/report-markdown-blankness.d.ts +6 -0
  40. package/dist/report-markdown-blankness.d.ts.map +1 -1
  41. package/dist/report-markdown-blankness.js +47 -4
  42. package/dist/seed-against-production.d.ts +11 -0
  43. package/dist/seed-against-production.d.ts.map +1 -0
  44. package/dist/seed-against-production.js +1 -0
  45. package/dist/setup-and-skills-commands.d.ts.map +1 -1
  46. package/dist/setup-and-skills-commands.js +123 -9
  47. package/dist/skill-installer.d.ts +32 -0
  48. package/dist/skill-installer.d.ts.map +1 -1
  49. package/dist/skill-installer.js +228 -1
  50. package/package.json +1 -1
  51. package/src/app-commands.ts +13 -3
  52. package/src/gepa-engine-dispatch.ts +18 -1
  53. package/src/help.ts +7 -7
  54. package/src/hosted-agent-prompt.ts +18 -0
  55. package/src/hosted-gepa-cli.ts +7 -1
  56. package/src/hosted-harness-claude.ts +4 -1
  57. package/src/hosted-harness-opencode.ts +28 -7
  58. package/src/hosted-harness.ts +32 -1
  59. package/src/hosted-optimization-prebaked-marker.ts +20 -0
  60. package/src/hosted-tool-hints.ts +153 -0
  61. package/src/index.ts +2 -5
  62. package/src/instruction-set-gepa-launch.ts +161 -3
  63. package/src/markdown-report-input.ts +3 -1
  64. package/src/optimizations-promote-cli.ts +20 -2
  65. package/src/preview-runtime.ts +44 -5
  66. package/src/provider-refusals.ts +2 -2
  67. package/src/report-markdown-blankness.ts +59 -4
  68. package/src/seed-against-production.ts +10 -0
  69. package/src/setup-and-skills-commands.ts +160 -8
  70. package/src/skill-installer.ts +315 -0
  71. package/vendor/skills/orizu/SKILL.md +3 -3
  72. package/vendor/skills/orizu/flows/first-win.md +3 -1
  73. package/vendor/skills/orizu/flows/promote.md +2 -1
  74. package/vendor/skills/orizu/references/building-apps.md +2 -0
  75. package/vendor/skills/orizu/references/building-judges.md +138 -8
  76. package/vendor/skills/orizu/references/cli-reference.md +2 -2
  77. package/vendor/skills/orizu/references/migrate-existing-gepa-setup.md +1 -2
  78. package/vendor/skills/orizu/references/optimization-reports.md +1 -1
  79. package/vendor/skills/orizu/references/optimization-with-gepa.md +22 -4
  80. package/vendor/skills/orizu/references/primer.md +2 -2
  81. package/vendor/skills/orizu/references/prompt-control-plane.md +6 -1
  82. package/vendor/skills/orizu/scripts/test-app.mjs +53 -4
package/src/help.ts CHANGED
@@ -307,13 +307,13 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
307
307
  options: [
308
308
  { name: '--team <slug>', help: 'Choose an existing accessible team.' },
309
309
  { name: '--create-team <name>', help: 'Create and select a new team; mutually exclusive with --team.' },
310
- { name: '--project <slug>', help: 'Choose the project to use as coding-agent handoff context. The workspace still materializes all team projects.' },
311
- { name: '--create-project <name>', help: 'Create and select a project in the chosen team; mutually exclusive with --project.' },
310
+ { name: '--project <slug>', help: 'Choose the project to work in. Only that project is set up in the workspace, and the team-project checklist is skipped, interactive or not.' },
311
+ { name: '--create-project <name>', help: 'Create and select a project in the chosen team, and set up only that project (no checklist); mutually exclusive with --project.' },
312
312
  { name: '--workspace [path]', help: 'Initialize or validate a workspace path, or the current directory when no path is provided. If provided, the value must be a filesystem path.' },
313
313
  { name: '--service-origin <url>', help: 'Service origin written to setup metadata. Defaults to the active CLI server.' },
314
314
  { name: '--attach-workspace <id>', help: 'Record an existing Orizu workspace id for future attachment.' },
315
315
  { name: '--validate', help: 'Inspect the workspace contract without writing files.' },
316
- { name: '--fix', help: 'Apply safe idempotent repairs only.' },
316
+ { name: '--fix', help: 'Apply safe idempotent repairs only, restoring every team project.' },
317
317
  { name: '--no-symlinks', help: 'Write CLAUDE.md as a pointer file instead of a symlink to AGENTS.md.' },
318
318
  { name: '--verbose', help: 'Show per-file setup actions. Default setup output is summary-oriented.' },
319
319
  { name: '--agent <agent>', help: 'Non-interactive opt-in to install the shared global Orizu skill plus any applicable native target. Repeat for multiple agents.', repeatable: true, choices: ['claude', 'codex', 'pi', 'devin', 'droid', 'grok', 'windsurf', 'opencode'] },
@@ -682,8 +682,8 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
682
682
  },
683
683
  {
684
684
  path: ['optimizations', 'run-gepa'],
685
- usage: 'orizu optimizations run-gepa [--hosted [--launch-intent-id <uuid>]] --project <team/project> --optimizer-version-id <id> (--candidate-version-id <id> | --instruction-set <name> --model-config <identity> [--component-selector round-robin|all]) --runner-version-id <id> [--candidate-runner-dir <dir>] --scorer-version-id <id> --scorer-runner-version-id <id> [--scorer-runner-dir <dir>] [--engine official|legacy] [--candidate-proposer skilled-proposer [--candidate-proposer-config <@file|path>] [--proposal-max-calls <n>] [--proposal-max-tokens <n>]] [--scorer-input-contract gepa|flat_row] [--scorer-candidate-field <row-field>] [--allow-degenerate-seed] [--seed <n>] --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--minibatch-size <n>] [--candidate-selection-strategy pareto|current_best|epsilon_greedy] [--epsilon <n>] [--objective <text>] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n> | --max-candidate-proposals <n>] [--num-threads auto|N] [--reflection-model <provider/model>] [--reflection-temperature <n>] [--reflection-prompt-template <text|@file>] [--reflection-max-tokens N] [--reflection-provider-settings <json|@file>] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--skip-perfect-parent-reflection|--no-skip-perfect-parent-reflection] [--disable-evaluation-cache] [--auto-promote] [--promotion-label <label>] [--log-row-snapshots] [--metadata <json>] [--python <command>] [--log-dir logs] [--no-local-log] [--json]',
686
- summary: 'Run official GEPA locally and stream events, or use --hosted for a queued Orizu-managed sandbox launch. Hosted eligibility requires a staff-enabled team, capacity below its concurrent-run cap, a launch-project optimizer version whose validated manifest.optimizer_family is gepa, registered candidate/scorer runners, a reflection model from a registered provider, and a named --budget preset within the team ceiling. Hosted eligibility catalog refusals use stable code/remediation JSON with optional bounded detail, and capacity refusals include monitor URLs for running runs. A successful launch prints a durable monitor URL; queued means accepted, not completed. Hosted mode does not require --candidate-runner-dir or --scorer-runner-dir; when supplied, those directories must byte-match their registered versions. Retry response loss with the same --launch-intent-id. Local runner version ids materialize registered bytes, while optional runner dirs are additionally byte-verified before execution. Pass --engine legacy only for the frozen safety hatch. Skilled-proposer is official-only, its config requires that selection, and the selection cannot be combined with --reflection-prompt-template. --instruction-set requires --model-config and cannot be combined with --candidate-version-id; round-robin updates one component-map entry per round and all updates every component. --max-candidate-proposals is official-engine-only. Validates the scorer contract on the seed before iterating; judge runners built for flat-row score runs need --scorer-input-contract flat_row (ALI-1158).',
685
+ usage: 'orizu optimizations run-gepa [--hosted [--launch-intent-id <uuid>]] --project <team/project> --optimizer-version-id <id> (--candidate-version-id <id> | --instruction-set <set[/profile[@vN]]> [--model-config <identity>] [--profile-version <N>] [--component-selector round-robin|all]) --runner-version-id <id> [--candidate-runner-dir <dir>] --scorer-version-id <id> --scorer-runner-version-id <id> [--scorer-runner-dir <dir>] [--engine official|legacy] [--candidate-proposer skilled-proposer [--candidate-proposer-config <@file|path>] [--proposal-max-calls <n>] [--proposal-max-tokens <n>]] [--scorer-input-contract gepa|flat_row] [--scorer-candidate-field <row-field>] [--allow-degenerate-seed] [--seed <n>] --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--minibatch-size <n>] [--candidate-selection-strategy pareto|current_best|epsilon_greedy] [--epsilon <n>] [--objective <text>] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n> | --max-candidate-proposals <n>] [--num-threads auto|N] [--reflection-model <provider/model>] [--reflection-temperature <n>] [--reflection-prompt-template <text|@file>] [--reflection-max-tokens N] [--reflection-provider-settings <json|@file>] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--skip-perfect-parent-reflection|--no-skip-perfect-parent-reflection] [--disable-evaluation-cache] [--auto-promote] [--promotion-label <label>] [--log-row-snapshots] [--metadata <json>] [--python <command>] [--log-dir logs] [--no-local-log] [--json]',
686
+ summary: 'Run official GEPA locally and stream events, or use --hosted for a queued Orizu-managed sandbox launch. Hosted eligibility requires a staff-enabled team, capacity below its concurrent-run cap, a launch-project optimizer version whose validated manifest.optimizer_family is gepa, registered candidate/scorer runners, a reflection model from a registered provider, and a named --budget preset within the team ceiling. Hosted eligibility catalog refusals use stable code/remediation JSON with optional bounded detail, and capacity refusals include monitor URLs for running runs. A successful launch prints a durable monitor URL; queued means accepted, not completed. Hosted mode does not require --candidate-runner-dir or --scorer-runner-dir; when supplied, those directories must byte-match their registered versions. Retry response loss with the same --launch-intent-id. Local runner version ids materialize registered bytes, while optional runner dirs are additionally byte-verified before execution. Pass --engine legacy only for the frozen safety hatch. Skilled-proposer is official-only, its config requires that selection, and the selection cannot be combined with --reflection-prompt-template. --instruction-set needs a profile, named by --model-config or a set/profile specifier, and cannot be combined with --candidate-version-id. The run starts from the production version of that profile unless @vN or --profile-version <N> names another version (the seed, promoted or not); a specifier and flags that name different values are refused; round-robin updates one component-map entry per round and all updates every component. --max-candidate-proposals is official-engine-only. Validates the scorer contract on the seed before iterating; judge runners built for flat-row score runs need --scorer-input-contract flat_row (ALI-1158).',
687
687
  group: 'Optimizations',
688
688
  },
689
689
  {
@@ -775,8 +775,8 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
775
775
  },
776
776
  {
777
777
  path: ['apps', 'export'],
778
- usage: 'orizu apps export [--app <appId>] [--project <team/project>] [--version <n>] [--out <path>]',
779
- summary: 'Export app source for the selected app version.',
778
+ usage: 'orizu apps export [--app <appId>] [--project <team/project>] [--version <n>] [--out <file-or-directory>]',
779
+ summary: 'Export app source for the selected app version. --out is a file path, or a directory (the file is named <app>.v<version>.tsx inside it).',
780
780
  group: 'Apps',
781
781
  },
782
782
  {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The system prompt for the hosted agent (ORI-2207).
3
+ *
4
+ * OpenCode (the program that runs the agent in a hosted session) ships its own
5
+ * built-in prompt that starts "You are opencode, an interactive CLI tool…", so
6
+ * the agent used to introduce itself as OpenCode. We register our own "primary
7
+ * agent" in the OpenCode config with this text, and every prompt request picks
8
+ * it by name. That replaces OpenCode's built-in prompt entirely, so this text
9
+ * carries the working rules we still want: use the tools, keep answers short.
10
+ * AGENTS.md is still added after it, as before.
11
+ */
12
+ export const HOSTED_AGENT_NAME = 'orizu'
13
+
14
+ export const HOSTED_AGENT_PROMPT = `You are the Orizu agent. You work in the person's repository inside an Orizu workbench, helping them with software engineering tasks. If asked who or what you are, say you are the Orizu agent. Never call yourself OpenCode.
15
+
16
+ - Do the work with your tools: read files, search, edit, and run commands. Do not guess at what a file contains when you can look.
17
+ - Keep answers short and plain. Say what you did and what the person needs to know next.
18
+ - Follow the repository's own conventions and any instructions in its AGENTS.md.`
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
 
3
3
  import { dispatchGepaEngine } from './gepa-engine-dispatch.js'
4
+ import { resolveGepaSeedFromEnvironment } from './instruction-set-gepa-launch.js'
4
5
  import { parseHostedGepaNumericValue } from './hosted-gepa-numeric-values.js'
5
6
  import { authedFetch } from './http.js'
6
7
  import { hostedProviderFromModel, hostedProviderSettingsError, unsupportedHostedProviderMessage } from './hosted-provider-settings.js'
@@ -216,11 +217,16 @@ export async function runHostedGepaOptimization(options: HostedGepaOptions): Pro
216
217
  : kind === 'json' ? JSON.parse(env[name]!) : kind === 'boolean' ? env[name] === '1' : env[name]
217
218
  }
218
219
  const launchIntentId = explicitLaunchIntent || randomUUID()
220
+ // ORI-2128: a named Seed version is resolved here exactly as a local run
221
+ // resolves it, and pinned in the body; without one the server keeps its
222
+ // one-call production lookup.
223
+ const seedProfileVersionId = await resolveGepaSeedFromEnvironment(env, options.project, 'server')
219
224
  const body = {
220
225
  hosted: true, launchIntentId, optimizerVersionId: env.ORIZU_OPTIMIZER_VERSION_ID,
221
226
  promptVersionIds: env.ORIZU_PROMPT_VERSION_ID ? [env.ORIZU_PROMPT_VERSION_ID] : [],
222
227
  ...(env.ORIZU_INSTRUCTION_SET_NAME ? { instructionSetName: env.ORIZU_INSTRUCTION_SET_NAME,
223
- modelConfigIdentity: env.ORIZU_MODEL_CONFIG_IDENTITY, componentSelector: env.ORIZU_COMPONENT_SELECTOR } : {}),
228
+ modelConfigIdentity: env.ORIZU_MODEL_CONFIG_IDENTITY, componentSelector: env.ORIZU_COMPONENT_SELECTOR,
229
+ ...(seedProfileVersionId ? { instructionSetProfileVersionId: seedProfileVersionId } : {}) } : {}),
224
230
  runnerVersionId: env.ORIZU_RUNNER_VERSION_ID,
225
231
  scorers: [{ scorerVersionId: env.ORIZU_SCORER_VERSION_ID,
226
232
  scorerRunnerVersionId: env.ORIZU_SCORER_RUNNER_VERSION_ID, role: 'selection', scorerConfig: {} }],
@@ -95,6 +95,7 @@
95
95
 
96
96
  import { parseModelIdentity } from './provider-registry.js'
97
97
 
98
+ import { HARNESS_NAMES } from './hosted-harness.js'
98
99
  import type {
99
100
  AgentHarness,
100
101
  HarnessEvent,
@@ -323,7 +324,7 @@ export function createClaudeAgentHarness(
323
324
  }
324
325
 
325
326
  return {
326
- name: 'claude-agent-sdk',
327
+ name: HARNESS_NAMES.claudeAgentSdk,
327
328
 
328
329
  async start(opts: HarnessStartOptions): Promise<{ agentSessionId: string }> {
329
330
  startModel = opts.model
@@ -471,6 +472,7 @@ export function createClaudeAgentHarness(
471
472
  args: asRecord(block.input),
472
473
  callId,
473
474
  status: 'running',
475
+ harness: HARNESS_NAMES.claudeAgentSdk,
474
476
  },
475
477
  }
476
478
  }
@@ -495,6 +497,7 @@ export function createClaudeAgentHarness(
495
497
  callId,
496
498
  status: block.is_error === true ? 'error' : 'completed',
497
499
  output: toolResultOutput(block.content),
500
+ harness: HARNESS_NAMES.claudeAgentSdk,
498
501
  },
499
502
  }
500
503
  }
@@ -51,13 +51,16 @@
51
51
 
52
52
  import { readFileSync } from 'fs'
53
53
 
54
+ import { HOSTED_AGENT_NAME, HOSTED_AGENT_PROMPT } from './hosted-agent-prompt.js'
54
55
  import { findProvider, parseModelIdentity } from './provider-registry.js'
55
56
 
57
+ import { HARNESS_NAMES } from './hosted-harness.js'
56
58
  import type {
57
59
  AgentHarness,
58
60
  HarnessEvent,
59
61
  HarnessPrompt,
60
62
  HarnessStartOptions,
63
+ HarnessToolPayload,
61
64
  } from './hosted-harness.js'
62
65
 
63
66
  // OpenCode is HARD-PINNED to this version everywhere it is installed/launched.
@@ -121,7 +124,9 @@ class PromptAbortedError extends Error {
121
124
  // -- OpenCode ascending id generator (port of OpenCode's id.ts) --------------
122
125
  // Monotonic so our user-message id always sorts AFTER any prior assistant
123
126
  // message id — this is what prevents OpenCode's prompt loop from early-exiting
124
- // on `lastUser.id < lastAssistant.id`.
127
+ // on `lastUser.id < lastAssistant.id`. Shape (opencode-ai@1.14.41): prefix, `_`,
128
+ // 12 hex digits holding the low 48 bits of `ms * 4096 + counter`, then 14
129
+ // base62 characters. `nowMs` is a parameter only so tests can pin the clock.
125
130
  const BASE62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
126
131
  let idLastTimestamp = 0
127
132
  let idCounter = 0
@@ -134,14 +139,15 @@ function randomBase62(length: number): string {
134
139
  return out
135
140
  }
136
141
 
137
- function ascendingId(prefix: 'ses' | 'msg' | 'prt'): string {
138
- const nowMs = Date.now()
142
+ export function ascendingId(prefix: 'ses' | 'msg' | 'prt', nowMs: number = Date.now()): string {
139
143
  if (nowMs !== idLastTimestamp) {
140
144
  idLastTimestamp = nowMs
141
145
  idCounter = 0
142
146
  }
143
147
  idCounter += 1
144
- const encoded = (nowMs * 0x1000 + idCounter) & 0xffffffffffff
148
+ // BigInt, as OpenCode does: the JS `&` operator works on 32 bits, which made
149
+ // short, sometimes negative ids that sorted before OpenCode's own (ORI-2116).
150
+ const encoded = (BigInt(nowMs) * BigInt(0x1000) + BigInt(idCounter)) & BigInt(0xffffffffffff)
145
151
  const hex = encoded.toString(16).padStart(12, '0')
146
152
  return `${prefix}_${hex}${randomBase62(14)}`
147
153
  }
@@ -188,6 +194,7 @@ export function buildPromptRequestBody(
188
194
  const body: Record<string, unknown> = {
189
195
  parts: [{ type: 'text', text: content }],
190
196
  messageID: opencodeMessageId,
197
+ agent: HOSTED_AGENT_NAME,
191
198
  }
192
199
  if (!model) return body
193
200
 
@@ -495,7 +502,7 @@ export function createOpenCodeHarness(options: OpenCodeHarnessOptions): AgentHar
495
502
  }
496
503
 
497
504
  return {
498
- name: 'opencode',
505
+ name: HARNESS_NAMES.opencode,
499
506
 
500
507
  async start(opts: HarnessStartOptions): Promise<{ agentSessionId: string }> {
501
508
  startModel = opts.model
@@ -598,13 +605,25 @@ export function createOpenCodeHarness(options: OpenCodeHarnessOptions): AgentHar
598
605
  if (emittedToolStates.has(toolKey)) return events
599
606
  emittedToolStates.add(toolKey)
600
607
  const terminal = status === 'completed' || status === 'error'
601
- const payload: Record<string, unknown> = {
608
+ const payload: HarnessToolPayload = {
602
609
  tool: typeof part.tool === 'string' ? part.tool : '',
603
610
  args: input,
604
611
  callId,
605
612
  status,
613
+ harness: HARNESS_NAMES.opencode,
614
+ }
615
+ // A failed call keeps its message in `state.error`, not `state.output`
616
+ // (opencode-ai@1.14.41 ToolStateError); it is that call's output.
617
+ if (terminal) {
618
+ payload.output = typeof state.output === 'string' ? state.output
619
+ : typeof state.error === 'string' ? state.error : ''
620
+ }
621
+ // OpenCode's own one-line title and structured extras (ORI-2160),
622
+ // passed through as given; absent when the state has none.
623
+ if (typeof state.title === 'string' && state.title.length > 0) payload.title = state.title
624
+ if (state.metadata && typeof state.metadata === 'object' && !Array.isArray(state.metadata)) {
625
+ payload.metadata = state.metadata as Record<string, unknown>
606
626
  }
607
- if (terminal) payload.output = typeof state.output === 'string' ? state.output : ''
608
627
  events.push({ kind: terminal ? 'tool_result' : 'tool_call', messageId: msgId, payload })
609
628
  } else if (partType === 'step-start') {
610
629
  events.push({ kind: 'step_start', messageId: msgId, payload: {} })
@@ -1116,6 +1135,8 @@ export function buildOpenCodeConfigContent(opts: OpenCodeConfigOptions): string
1116
1135
  return JSON.stringify({
1117
1136
  model: opts.model,
1118
1137
  permission: opts.permission ?? defaultHeadlessPermission(),
1138
+ // Replaces OpenCode's built-in "You are opencode…" prompt (ORI-2207).
1139
+ agent: { [HOSTED_AGENT_NAME]: { mode: 'primary', prompt: HOSTED_AGENT_PROMPT } },
1119
1140
  })
1120
1141
  }
1121
1142
 
@@ -117,8 +117,39 @@ export interface HarnessEvent {
117
117
  critical?: boolean
118
118
  }
119
119
 
120
+ /** The drivers' stable names. A tool event's `harness` field is one of these. */
121
+ export const HARNESS_NAMES = {
122
+ opencode: 'opencode',
123
+ claudeAgentSdk: 'claude-agent-sdk',
124
+ } as const
125
+
126
+ type HarnessName = (typeof HARNESS_NAMES)[keyof typeof HARNESS_NAMES]
127
+
128
+ /**
129
+ * Payload of a `tool_call` / `tool_result` event (ORI-2160). One chat renderer
130
+ * shows every tool call whatever harness produced it, so the payload carries:
131
+ * • `title` — the harness's one-line summary of the call (OpenCode's
132
+ * `state.title`). Absent when the harness has none.
133
+ * • `metadata` — structured extras such as a diff, an exit code or match
134
+ * counts (OpenCode's `state.metadata`). Absent when none.
135
+ * • `harness` — which driver produced the event.
136
+ * All three are optional: an event without them is still a valid tool event.
137
+ * The sink and the digest pass them through unchanged.
138
+ */
139
+ export interface HarnessToolPayload {
140
+ [key: string]: unknown
141
+ tool?: string
142
+ args?: Record<string, unknown>
143
+ callId: string
144
+ status: string
145
+ output?: string
146
+ title?: string
147
+ metadata?: Record<string, unknown>
148
+ harness?: HarnessName
149
+ }
150
+
120
151
  export interface AgentHarness {
121
- /** Stable driver name — "opencode" | "claude-agent-sdk". */
152
+ /** Stable driver name — one of HARNESS_NAMES. */
122
153
  readonly name: string
123
154
  /** Boot the agent process/session; returns the resumable agent session id. */
124
155
  start(opts: HarnessStartOptions): Promise<{ agentSessionId: string }>
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The check the session coordinator makes on `/opt/orizu/prebaked.json`, the
3
+ * first bootstrap command of a hosted optimization run. It lives here so the
4
+ * release bake's start check (hosted-runtime-image/check-snapshot-starts.mjs)
5
+ * rejects a snapshot on exactly the rules the coordinator would refuse it on.
6
+ * Worker code imports this module, so it must not use Node APIs.
7
+ */
8
+ export function isHostedOptimizationPrebakedMarker(raw: string): boolean {
9
+ try {
10
+ const marker: unknown = JSON.parse(raw)
11
+ if (!marker || typeof marker !== 'object' || Array.isArray(marker)) return false
12
+ const value = marker as Record<string, unknown>
13
+ return value.builtFor === 'vercel-sandbox' &&
14
+ typeof value.cliVersion === 'string' && value.cliVersion.length > 0 &&
15
+ typeof value.opencodeVersion === 'string' && value.opencodeVersion.length > 0 &&
16
+ typeof value.claudeSdkVersion === 'string' && value.claudeSdkVersion.length > 0
17
+ } catch {
18
+ return false
19
+ }
20
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Tool display hints (ORI-2160, moved here for ORI-2167).
3
+ *
4
+ * Each harness driver has its own built-in tools. This module holds, per
5
+ * driver, what the chat needs to show a call without switching on its name:
6
+ * a category (icon), a label (the verb on the row), the input key that
7
+ * summarizes the call, how to show the output, and input keys to hide.
8
+ *
9
+ * It has no imports with side effects, so both the CLI and the web app
10
+ * (the chat renderer, and the activity projection that picks which input
11
+ * key reaches the browser) import it directly.
12
+ */
13
+ // Type-only, so the web build never has to load the harness module itself.
14
+ import type { HARNESS_NAMES } from './hosted-harness.js'
15
+
16
+ type HarnessNames = typeof HARNESS_NAMES
17
+ const OPENCODE: HarnessNames['opencode'] = 'opencode'
18
+ const CLAUDE_AGENT_SDK: HarnessNames['claudeAgentSdk'] = 'claude-agent-sdk'
19
+
20
+ /** What kind of thing a tool does — the renderer picks an icon/layout from
21
+ * this, never from the tool name. */
22
+ export const TOOL_CATEGORIES = {
23
+ file: 'file',
24
+ command: 'command',
25
+ search: 'search',
26
+ network: 'network',
27
+ question: 'question',
28
+ subAgent: 'sub-agent',
29
+ list: 'list',
30
+ plug: 'plug',
31
+ other: 'other',
32
+ } as const
33
+
34
+ export type ToolCategory = (typeof TOOL_CATEGORIES)[keyof typeof TOOL_CATEGORIES]
35
+
36
+ /** How the tool's output text should be shown. */
37
+ export const TOOL_OUTPUT_KINDS = {
38
+ text: 'text',
39
+ diff: 'diff',
40
+ markdown: 'markdown',
41
+ json: 'json',
42
+ list: 'list',
43
+ } as const
44
+
45
+ type ToolOutputKind = (typeof TOOL_OUTPUT_KINDS)[keyof typeof TOOL_OUTPUT_KINDS]
46
+
47
+ /** Per-tool display hint. Each driver owns a table of these for its built-in
48
+ * tools, so tool-name knowledge lives behind the seam. */
49
+ export interface ToolHint {
50
+ category: ToolCategory
51
+ /** The verb the chat row shows ("Read", "Ran"). A tool without a hint shows
52
+ * its own name instead. */
53
+ label?: string
54
+ /** The input key whose value summarizes the call (e.g. `command`), or null
55
+ * when the tool takes no input worth summarizing. */
56
+ summaryKey: string | null
57
+ outputKind: ToolOutputKind
58
+ /** Input keys whose values must not be shown as-is (large bodies such as a
59
+ * file's full new content, or likely secrets). */
60
+ redactKeys: readonly string[]
61
+ }
62
+
63
+ export type ToolHintTable = Readonly<Record<string, ToolHint>>
64
+
65
+ // Argument names that look like they hold a credential. Redacted for every
66
+ // tool, and never used as the fallback summary.
67
+ const SECRET_LOOKING_KEY = /token|secret|password|passwd|api[_-]?key|authorization|credential/i
68
+
69
+ /**
70
+ * Look a tool up in a driver's hint table. A tool the table does not know (an
71
+ * MCP tool, a plug-in) falls back to the `plug` category, summarized by its
72
+ * first non-empty string argument that does not look like a secret.
73
+ */
74
+ export function resolveToolHint(
75
+ table: ToolHintTable,
76
+ tool: string,
77
+ args: Record<string, unknown> = {}
78
+ ): ToolHint {
79
+ const known = Object.hasOwn(table, tool) ? table[tool] : undefined
80
+ const secretKeys = Object.keys(args).filter(key => SECRET_LOOKING_KEY.test(key))
81
+ const redactKeys = [...new Set([...(known?.redactKeys ?? []), ...secretKeys])]
82
+ if (known) return { ...known, redactKeys }
83
+ const summaryKey =
84
+ Object.keys(args).find(key => {
85
+ const value = args[key]
86
+ return typeof value === 'string' && value.length > 0 && !redactKeys.includes(key)
87
+ }) ?? null
88
+ return { category: TOOL_CATEGORIES.plug, summaryKey, outputKind: TOOL_OUTPUT_KINDS.text, redactKeys }
89
+ }
90
+
91
+ // OpenCode's built-in tools, as the pinned opencode-ai@1.14.41 tool registry
92
+ // lists them, with the input key that summarizes each call. Tools not listed
93
+ // here (MCP tools, plug-ins) resolve to `plug` through resolveToolHint.
94
+ export const OPENCODE_TOOL_HINTS: ToolHintTable = {
95
+ read: { category: 'file', label: 'Read', summaryKey: 'filePath', outputKind: 'text', redactKeys: [] },
96
+ edit: { category: 'file', label: 'Edited', summaryKey: 'filePath', outputKind: 'diff', redactKeys: ['oldString', 'newString'] },
97
+ write: { category: 'file', label: 'Wrote', summaryKey: 'filePath', outputKind: 'text', redactKeys: ['content'] },
98
+ apply_patch: { category: 'file', label: 'Edited', summaryKey: 'patchText', outputKind: 'diff', redactKeys: [] },
99
+ lsp: { category: 'file', label: 'Checked', summaryKey: 'filePath', outputKind: 'json', redactKeys: [] },
100
+ bash: { category: 'command', label: 'Ran', summaryKey: 'command', outputKind: 'text', redactKeys: [] },
101
+ glob: { category: 'search', label: 'Listed', summaryKey: 'pattern', outputKind: 'list', redactKeys: [] },
102
+ grep: { category: 'search', label: 'Searched', summaryKey: 'pattern', outputKind: 'text', redactKeys: [] },
103
+ webfetch: { category: 'network', label: 'Fetched', summaryKey: 'url', outputKind: 'markdown', redactKeys: [] },
104
+ websearch: { category: 'network', label: 'Searched', summaryKey: 'query', outputKind: 'text', redactKeys: [] },
105
+ question: { category: 'question', label: 'Asked', summaryKey: 'questions', outputKind: 'text', redactKeys: [] },
106
+ plan_exit: { category: 'question', label: 'Asked', summaryKey: null, outputKind: 'text', redactKeys: [] },
107
+ task: { category: 'sub-agent', label: 'Sub-agent', summaryKey: 'description', outputKind: 'markdown', redactKeys: [] },
108
+ todowrite: { category: 'list', label: 'Planned', summaryKey: 'todos', outputKind: 'json', redactKeys: [] },
109
+ skill: { category: 'other', label: 'Loaded', summaryKey: 'name', outputKind: 'markdown', redactKeys: [] },
110
+ invalid: { category: 'other', label: 'Invalid', summaryKey: 'tool', outputKind: 'text', redactKeys: [] },
111
+ }
112
+
113
+ // The Claude Agent SDK's built-in tools (SDK 0.3.201: input types in
114
+ // sdk-tools.d.ts, names from the SDK; `Task` is the older name of `Agent`),
115
+ // with the input key that summarizes each call. MCP tools
116
+ // (`mcp__<server>__<tool>`) are not listed and resolve to `plug` through
117
+ // resolveToolHint. This driver sends no `title` or `metadata`: the SDK has no
118
+ // equivalent of OpenCode's.
119
+ export const CLAUDE_TOOL_HINTS: ToolHintTable = {
120
+ Read: { category: 'file', label: 'Read', summaryKey: 'file_path', outputKind: 'text', redactKeys: [] },
121
+ Edit: { category: 'file', label: 'Edited', summaryKey: 'file_path', outputKind: 'text', redactKeys: ['old_string', 'new_string'] },
122
+ Write: { category: 'file', label: 'Wrote', summaryKey: 'file_path', outputKind: 'text', redactKeys: ['content'] },
123
+ NotebookEdit: { category: 'file', label: 'Edited', summaryKey: 'notebook_path', outputKind: 'text', redactKeys: ['new_source'] },
124
+ Bash: { category: 'command', label: 'Ran', summaryKey: 'command', outputKind: 'text', redactKeys: [] },
125
+ TaskOutput: { category: 'command', label: 'Checked', summaryKey: 'task_id', outputKind: 'text', redactKeys: [] },
126
+ TaskStop: { category: 'command', label: 'Stopped', summaryKey: 'task_id', outputKind: 'text', redactKeys: [] },
127
+ Glob: { category: 'search', label: 'Listed', summaryKey: 'pattern', outputKind: 'list', redactKeys: [] },
128
+ Grep: { category: 'search', label: 'Searched', summaryKey: 'pattern', outputKind: 'text', redactKeys: [] },
129
+ WebFetch: { category: 'network', label: 'Fetched', summaryKey: 'url', outputKind: 'markdown', redactKeys: [] },
130
+ WebSearch: { category: 'network', label: 'Searched', summaryKey: 'query', outputKind: 'text', redactKeys: [] },
131
+ AskUserQuestion: { category: 'question', label: 'Asked', summaryKey: 'questions', outputKind: 'text', redactKeys: [] },
132
+ ExitPlanMode: { category: 'question', label: 'Asked', summaryKey: null, outputKind: 'markdown', redactKeys: [] },
133
+ Agent: { category: 'sub-agent', label: 'Sub-agent', summaryKey: 'description', outputKind: 'markdown', redactKeys: [] },
134
+ Task: { category: 'sub-agent', label: 'Sub-agent', summaryKey: 'description', outputKind: 'markdown', redactKeys: [] },
135
+ TodoWrite: { category: 'list', label: 'Planned', summaryKey: 'todos', outputKind: 'text', redactKeys: [] },
136
+ ListMcpResourcesTool: { category: 'plug', label: 'Listed', summaryKey: 'server', outputKind: 'json', redactKeys: [] },
137
+ ReadMcpResourceTool: { category: 'plug', label: 'Read', summaryKey: 'uri', outputKind: 'text', redactKeys: [] },
138
+ Skill: { category: 'other', label: 'Loaded', summaryKey: 'skill', outputKind: 'markdown', redactKeys: [] },
139
+ }
140
+
141
+ const NO_HINTS: ToolHintTable = {}
142
+
143
+ /**
144
+ * The hint table for the harness that produced an event. Events recorded
145
+ * before the seam named its harness came from OpenCode, the default driver.
146
+ * A harness with no table here (a future pi driver, say) gets none, so every
147
+ * tool falls back to its own name and first string argument.
148
+ */
149
+ export function toolHintsFor(harness: string | undefined): ToolHintTable {
150
+ if (harness === undefined || harness === OPENCODE) return OPENCODE_TOOL_HINTS
151
+ if (harness === CLAUDE_AGENT_SDK) return CLAUDE_TOOL_HINTS
152
+ return NO_HINTS
153
+ }
package/src/index.ts CHANGED
@@ -112,7 +112,7 @@ import { resolveAuthTokenForBaseUrl } from './credentials.js'
112
112
  import { applyHostedInstructionSetProfileOverride, dispatchHostedCommands } from './hosted-commands.js'
113
113
  import { dispatchGepaEngine } from './gepa-engine-dispatch.js'
114
114
  import { getGepaPythonPathEntries } from './gepa-python-paths.js'
115
- import { resolveGepaInstructionSetProfileVersion } from './instruction-set-gepa-launch.js'
115
+ import { resolveGepaSeedFromEnvironment } from './instruction-set-gepa-launch.js'
116
116
  import { prepareSkilledProposerLaunch, spawnSkilledProposerChild } from './skilled-proposer-launch.js'
117
117
  import { reportCommentsCommand, throwDeprecatedPromptCommentsCommand } from './report-comments-cli.js'
118
118
  import { getCapabilities, renderHelpForArgs } from './help.js'
@@ -1365,10 +1365,7 @@ async function runGepaOptimization() {
1365
1365
  PYTHONPATH: getGepaPythonPathEntries(process.env.PYTHONPATH).join(delimiter),
1366
1366
  PYTHONUNBUFFERED: process.env.PYTHONUNBUFFERED || '1',
1367
1367
  }))
1368
- const instructionSetName = dispatch.environment.ORIZU_INSTRUCTION_SET_NAME
1369
- const modelConfigIdentity = dispatch.environment.ORIZU_MODEL_CONFIG_IDENTITY
1370
- const instructionSetProfileVersionId = instructionSetName && modelConfigIdentity
1371
- ? await resolveGepaInstructionSetProfileVersion(instructionSetName, modelConfigIdentity, project) : undefined
1368
+ const instructionSetProfileVersionId = await resolveGepaSeedFromEnvironment(dispatch.environment, project, 'production')
1372
1369
  if (instructionSetProfileVersionId) dispatch.environment.ORIZU_INSTRUCTION_SET_PROFILE_VERSION_ID = instructionSetProfileVersionId
1373
1370
  selectedEngine = dispatch.engine
1374
1371
  const launch = prepareSkilledProposerLaunch(python, dispatch.engine, dispatch.environment)