orizu 0.5.30 → 0.5.31

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 (43) hide show
  1. package/dist/gepa-engine-dispatch.d.ts.map +1 -1
  2. package/dist/gepa-engine-dispatch.js +16 -1
  3. package/dist/help.js +2 -2
  4. package/dist/hosted-gepa-cli.d.ts.map +1 -1
  5. package/dist/hosted-gepa-cli.js +7 -1
  6. package/dist/hosted-harness-claude.d.ts.map +1 -1
  7. package/dist/hosted-harness-claude.js +4 -1
  8. package/dist/hosted-harness-opencode.d.ts +1 -0
  9. package/dist/hosted-harness-opencode.d.ts.map +1 -1
  10. package/dist/hosted-harness-opencode.js +23 -7
  11. package/dist/hosted-harness.d.ts +30 -1
  12. package/dist/hosted-harness.d.ts.map +1 -1
  13. package/dist/hosted-harness.js +5 -0
  14. package/dist/hosted-tool-hints.d.ts +56 -0
  15. package/dist/hosted-tool-hints.d.ts.map +1 -0
  16. package/dist/hosted-tool-hints.js +105 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -5
  19. package/dist/instruction-set-gepa-launch.d.ts +21 -2
  20. package/dist/instruction-set-gepa-launch.d.ts.map +1 -1
  21. package/dist/instruction-set-gepa-launch.js +123 -3
  22. package/dist/optimizations-promote-cli.d.ts.map +1 -1
  23. package/dist/optimizations-promote-cli.js +19 -0
  24. package/dist/seed-against-production.d.ts +11 -0
  25. package/dist/seed-against-production.d.ts.map +1 -0
  26. package/dist/seed-against-production.js +1 -0
  27. package/package.json +1 -1
  28. package/src/gepa-engine-dispatch.ts +18 -1
  29. package/src/help.ts +2 -2
  30. package/src/hosted-gepa-cli.ts +7 -1
  31. package/src/hosted-harness-claude.ts +4 -1
  32. package/src/hosted-harness-opencode.ts +24 -7
  33. package/src/hosted-harness.ts +32 -1
  34. package/src/hosted-tool-hints.ts +153 -0
  35. package/src/index.ts +2 -5
  36. package/src/instruction-set-gepa-launch.ts +161 -3
  37. package/src/optimizations-promote-cli.ts +20 -2
  38. package/src/seed-against-production.ts +10 -0
  39. package/vendor/skills/orizu/references/cli-reference.md +1 -1
  40. package/vendor/skills/orizu/references/migrate-existing-gepa-setup.md +1 -2
  41. package/vendor/skills/orizu/references/optimization-reports.md +1 -1
  42. package/vendor/skills/orizu/references/optimization-with-gepa.md +1 -1
  43. package/vendor/skills/orizu/references/prompt-control-plane.md +6 -1
@@ -1,17 +1,137 @@
1
1
  import { authedFetch } from './http.js';
2
+ import { MAX_VERSION_NUMBER, parseSpecifier } from './instruction-set-lock/index.js';
2
3
  import { parseJsonResponse } from './json-response.js';
3
- /** Resolve the requested Profile's Production tuple for optimization. */
4
- export async function resolveGepaInstructionSetProfileVersion(instructionSetName, modelConfigIdentity, project) {
4
+ /**
5
+ * A version runs and promotes only when its component keys equal the set's
6
+ * current shape exactly: promotion refuses any other key set
7
+ * (instruction_set_candidate_shape_mismatch). Returns null when they match.
8
+ */
9
+ function shapeDifference(shape, version) {
10
+ const keys = new Set(version.componentKeys ?? []);
11
+ const shapeKeys = new Set(shape);
12
+ const missing = shape.filter(key => !keys.has(key));
13
+ const extra = [...keys].filter(key => !shapeKeys.has(key)).sort();
14
+ return missing.length === 0 && extra.length === 0 ? null : { missing, extra };
15
+ }
16
+ function describeDifference({ missing, extra }) {
17
+ const quote = (keys) => keys.map(key => `"${key}"`).join(', ');
18
+ return [
19
+ ...(missing.length > 0 ? [`missing ${quote(missing)}`] : []),
20
+ ...(extra.length > 0 ? [`extra ${quote(extra)}`] : []),
21
+ ].join('; ');
22
+ }
23
+ function newestMatchingVersion(shape, versions) {
24
+ return versions
25
+ .filter(candidate => Array.isArray(candidate.componentKeys) && shapeDifference(shape, candidate) === null)
26
+ .reduce((max, candidate) => {
27
+ const number = candidate.versionNumber ?? 0;
28
+ return max === null || number > max ? number : max;
29
+ }, null);
30
+ }
31
+ /**
32
+ * ORI-2128: a version made before a shape change can lack a component of the
33
+ * set's current shape, or carry one the set has since removed. The runner or
34
+ * promotion would refuse it later; refuse it here, before a run is spawned or
35
+ * queued. A server that predates per-version component keys sends none; the
36
+ * runner remains the backstop.
37
+ */
38
+ function requireMatchingSeed(shape, versions, version, modelConfigIdentity, role) {
39
+ if (!Array.isArray(shape) || !Array.isArray(version.componentKeys))
40
+ return;
41
+ const difference = shapeDifference(shape, version);
42
+ if (difference === null)
43
+ return;
44
+ const newest = newestMatchingVersion(shape, versions);
45
+ const detail = describeDifference(difference);
46
+ if (role === 'production') {
47
+ const newestNote = newest === null ? 'no version of this profile matches it yet' : `newest matching: v${newest}`;
48
+ throw new Error(`instruction_set_profile_production_incomplete: ${modelConfigIdentity} production v${version.versionNumber} does not match the current shape (${detail}); promote a version that matches it, or name one with @vN (${newestNote})`);
49
+ }
50
+ const advice = newest === null
51
+ ? 'no version of this profile matches the current shape yet'
52
+ : `pick v${newest}`;
53
+ throw new Error(`instruction_set_profile_version_incomplete: ${modelConfigIdentity} v${version.versionNumber} does not match the current shape (${detail}); ${advice}`);
54
+ }
55
+ function profileVersionNumber(value) {
56
+ const match = /^v?([1-9][0-9]*)$/u.exec(value);
57
+ const versionNumber = match ? Number(match[1]) : Number.NaN;
58
+ if (!Number.isSafeInteger(versionNumber) || versionNumber > MAX_VERSION_NUMBER) {
59
+ throw new Error(`--profile-version must be a version number such as 3 or v3; received ${value}`);
60
+ }
61
+ return versionNumber;
62
+ }
63
+ /**
64
+ * Combine `--instruction-set`, `--model-config` and `--profile-version` into
65
+ * one Seed. A value with `/` or `@` is a specifier (`set/profile@vN`); any
66
+ * other value is a set slug or exact name, passed through unchanged. Values
67
+ * named twice must agree, the same rule `sync` applies to `--version`.
68
+ */
69
+ export function gepaSeedSelector(instructionSetArg, modelConfigFlag, versionFlag) {
70
+ const flagVersion = versionFlag === undefined ? undefined : profileVersionNumber(versionFlag);
71
+ if (!instructionSetArg.includes('/') && !instructionSetArg.includes('@')) {
72
+ return { instructionSetName: instructionSetArg, modelConfigIdentity: modelConfigFlag, versionNumber: flagVersion };
73
+ }
74
+ const parsed = parseSpecifier(instructionSetArg);
75
+ if (parsed.profile !== undefined && modelConfigFlag !== undefined && parsed.profile !== modelConfigFlag) {
76
+ throw new Error(`instruction_set_seed_conflict: specifier names profile ${parsed.profile} but --model-config is ${modelConfigFlag}`);
77
+ }
78
+ if (parsed.versionNumber !== undefined && flagVersion !== undefined && parsed.versionNumber !== flagVersion) {
79
+ throw new Error(`instruction_set_seed_conflict: specifier names v${parsed.versionNumber} but --profile-version is v${flagVersion}`);
80
+ }
81
+ return {
82
+ instructionSetName: parsed.set,
83
+ modelConfigIdentity: parsed.profile ?? modelConfigFlag,
84
+ versionNumber: parsed.versionNumber ?? flagVersion,
85
+ };
86
+ }
87
+ /**
88
+ * Resolve the Seed's profile version id: the named version when one is
89
+ * given, otherwise the requested Profile's Production.
90
+ */
91
+ async function resolveGepaInstructionSetProfileVersion(instructionSetName, modelConfigIdentity, project, versionNumber) {
5
92
  const response = await authedFetch(`/api/cli/instruction-sets/${encodeURIComponent(instructionSetName)}?project=${encodeURIComponent(project)}`);
6
93
  if (!response.ok)
7
94
  throw new Error(`Instruction set resolution failed: ${await response.text()}`);
8
95
  const payload = await parseJsonResponse(response, 'Instruction set resolution');
9
96
  const instructionSet = payload.instructionSet;
10
97
  const profile = instructionSet?.profiles?.find(candidate => candidate.modelConfigIdentity === modelConfigIdentity);
11
- if (!profile)
98
+ // An archived model config's profile is not resolvable: the same refusal
99
+ // the server's resolve_instruction_set gives hosted runs.
100
+ if (!profile || profile.modelConfigArchivedAt)
12
101
  throw new Error(`instruction_set_profile_not_found: ${modelConfigIdentity}`);
102
+ const versions = profile.versions ?? [];
103
+ if (versionNumber !== undefined) {
104
+ const version = versions.find(candidate => candidate.versionNumber === versionNumber);
105
+ if (version?.profileVersionId) {
106
+ requireMatchingSeed(instructionSet?.shape, versions, version, modelConfigIdentity, 'named');
107
+ return version.profileVersionId;
108
+ }
109
+ const newest = versions.reduce((max, candidate) => Math.max(max, candidate.versionNumber ?? 0), 0);
110
+ throw new Error(`instruction_set_profile_version_not_found: ${modelConfigIdentity} has no v${versionNumber}; ${newest > 0 ? `newest is v${newest}` : 'it has no versions'}`);
111
+ }
13
112
  const profileVersionId = profile.production?.profileVersionId;
14
113
  if (!profileVersionId)
15
114
  throw new Error(`instruction_set_profile_not_promoted: ${modelConfigIdentity}`);
115
+ const production = versions.find(candidate => candidate.profileVersionId === profileVersionId);
116
+ if (production)
117
+ requireMatchingSeed(instructionSet?.shape, versions, production, modelConfigIdentity, 'production');
16
118
  return profileVersionId;
17
119
  }
120
+ /**
121
+ * Resolve the Seed that dispatch normalised into the connector environment.
122
+ * Returns undefined when no instruction set was selected. When no version is
123
+ * named, `unversioned: 'production'` resolves the profile's Production here
124
+ * (local runs); `unversioned: 'server'` leaves it to the hosted launch route,
125
+ * which resolves Production itself in the same call that queues the run.
126
+ */
127
+ export async function resolveGepaSeedFromEnvironment(environment, project, unversioned) {
128
+ const instructionSetName = environment.ORIZU_INSTRUCTION_SET_NAME;
129
+ const modelConfigIdentity = environment.ORIZU_MODEL_CONFIG_IDENTITY;
130
+ if (!instructionSetName || !modelConfigIdentity)
131
+ return undefined;
132
+ const rawVersion = environment.ORIZU_INSTRUCTION_SET_PROFILE_VERSION_NUMBER;
133
+ const versionNumber = rawVersion === undefined ? undefined : profileVersionNumber(rawVersion);
134
+ if (versionNumber === undefined && unversioned === 'server')
135
+ return undefined;
136
+ return resolveGepaInstructionSetProfileVersion(instructionSetName, modelConfigIdentity, project, versionNumber);
137
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"optimizations-promote-cli.d.ts","sourceRoot":"","sources":["../src/optimizations-promote-cli.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEpD,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,kBAAkB,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAClE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CACjE;AA0DD,wBAAsB,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiD1G"}
1
+ {"version":3,"file":"optimizations-promote-cli.d.ts","sourceRoot":"","sources":["../src/optimizations-promote-cli.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAGpD,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC7B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,kBAAkB,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAClE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CACjE;AAqED,wBAAsB,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD1G"}
@@ -54,6 +54,17 @@ function productionPromotionReplay(runId, candidateId, project, origin) {
54
54
  : '';
55
55
  return `Promote this profile with: orizu${originArguments} optimizations promote ${shellArgument(runId)} --candidate ${shellArgument(candidateId)} --label production --project ${shellArgument(project)}`;
56
56
  }
57
+ /**
58
+ * ORI-2128: a candidate grown from a Seed older than Production does not
59
+ * contain the changes made since that Seed. Warn; never refuse (ORI-2132).
60
+ */
61
+ function staleSeedWarningFor(data) {
62
+ const seed = data.runSeedVersionNumber;
63
+ const production = data.productionVersionNumber;
64
+ if (typeof seed !== 'number' || typeof production !== 'number' || seed >= production)
65
+ return null;
66
+ return `Warning: this run started from v${seed}; production is v${production}. Changes made since v${seed} are not in this candidate.`;
67
+ }
57
68
  export async function promoteOptimizationCommand(args, io) {
58
69
  const runId = args.find(arg => !arg.startsWith('--') && arg !== option(args, '--candidate') && arg !== option(args, '--label') && arg !== option(args, '--project'));
59
70
  const candidateId = option(args, '--candidate');
@@ -72,15 +83,21 @@ export async function promoteOptimizationCommand(args, io) {
72
83
  const replay = isProfilePromotion && data.appliedLabel !== 'production'
73
84
  ? productionPromotionReplay(runId, candidateId, project, io.origin)
74
85
  : null;
86
+ const staleSeedWarning = isProfilePromotion ? staleSeedWarningFor(data) : null;
75
87
  if (io.json) {
76
88
  io.print(JSON.stringify(isProfilePromotion
77
89
  ? {
78
90
  profileVersionId: data.profileVersionId,
79
91
  versionNumber: data.versionNumber,
80
92
  appliedLabel: data.appliedLabel,
93
+ // JSON.stringify omits these when an older server did not send them.
94
+ runSeedVersionNumber: data.runSeedVersionNumber,
95
+ productionVersionNumber: data.productionVersionNumber,
81
96
  components: data.components,
82
97
  }
83
98
  : { promptVersionId: data.promptVersionId }));
99
+ if (staleSeedWarning)
100
+ io.printErr(staleSeedWarning);
84
101
  if (isProfilePromotion && data.appliedLabel === undefined) {
85
102
  io.printErr(APPLIED_LABEL_VERSION_SKEW_NOTE);
86
103
  io.printErr(replay);
@@ -88,6 +105,8 @@ export async function promoteOptimizationCommand(args, io) {
88
105
  return;
89
106
  }
90
107
  if (isProfilePromotion) {
108
+ if (staleSeedWarning)
109
+ io.printErr(staleSeedWarning);
91
110
  io.print(`Promoted profile version ${sanitizeTerminalText(data.profileVersionId)}`);
92
111
  if (data.appliedLabel === undefined) {
93
112
  io.printErr(APPLIED_LABEL_VERSION_SKEW_NOTE);
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ORI-2128: what `optimizations promote` needs to warn about a stale seed.
3
+ * The run's seed version number, and its profile's production version number
4
+ * as it stood before this promotion (null when the profile had none).
5
+ * Shared by the promote handler, both promote routes, and the CLI.
6
+ */
7
+ export interface SeedAgainstProduction {
8
+ runSeedVersionNumber: number;
9
+ productionVersionNumber: number | null;
10
+ }
11
+ //# sourceMappingURL=seed-against-production.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed-against-production.d.ts","sourceRoot":"","sources":["../src/seed-against-production.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,oBAAoB,EAAE,MAAM,CAAA;IAC5B,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAA;CACvC"}
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orizu",
3
- "version": "0.5.30",
3
+ "version": "0.5.31",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'fs'
2
2
 
3
+ import { gepaSeedSelector } from './instruction-set-gepa-launch.js'
3
4
  import { providerFromIdentity, protocolRequiresOutputCap } from './provider-registry.js'
4
5
  import { normalizedSkilledProposerConfig } from './skilled-proposer-config.js'
5
6
 
@@ -58,6 +59,7 @@ const VALUE_OPTIONS: EnvironmentOption[] = [
58
59
  { flag: '--log-dir', environment: 'ORIZU_LOCAL_LOG_DIR' },
59
60
  { flag: '--instruction-set', environment: 'ORIZU_INSTRUCTION_SET_NAME' },
60
61
  { flag: '--model-config', environment: 'ORIZU_MODEL_CONFIG_IDENTITY' },
62
+ { flag: '--profile-version', environment: 'ORIZU_INSTRUCTION_SET_PROFILE_VERSION_NUMBER' },
61
63
  ]
62
64
 
63
65
  const BOOLEAN_OPTIONS = [
@@ -301,6 +303,21 @@ function translateOfficialOptions(args: string[], project: string, environment:
301
303
  throw new Error('Budget options are mutually exclusive; choose at most one of --budget, --max-metric-calls, --max-full-evals, --max-iterations, --max-candidate-proposals')
302
304
  }
303
305
  if (hasInstructionSet && hasCandidateVersion) throw new Error('--instruction-set and --candidate-version-id are mutually exclusive')
306
+ if (!hasInstructionSet && connectorEnvironment.ORIZU_INSTRUCTION_SET_PROFILE_VERSION_NUMBER !== undefined) {
307
+ throw new Error('--profile-version requires --instruction-set')
308
+ }
309
+ if (hasInstructionSet) {
310
+ // ORI-2128: the Seed may be named by a full specifier, by flags, or both;
311
+ // normalise to the set reference, the profile, and the version number.
312
+ const seed = gepaSeedSelector(
313
+ connectorEnvironment.ORIZU_INSTRUCTION_SET_NAME!,
314
+ connectorEnvironment.ORIZU_MODEL_CONFIG_IDENTITY,
315
+ connectorEnvironment.ORIZU_INSTRUCTION_SET_PROFILE_VERSION_NUMBER,
316
+ )
317
+ connectorEnvironment.ORIZU_INSTRUCTION_SET_NAME = seed.instructionSetName
318
+ if (seed.modelConfigIdentity !== undefined) connectorEnvironment.ORIZU_MODEL_CONFIG_IDENTITY = seed.modelConfigIdentity
319
+ if (seed.versionNumber !== undefined) connectorEnvironment.ORIZU_INSTRUCTION_SET_PROFILE_VERSION_NUMBER = String(seed.versionNumber)
320
+ }
304
321
  if (hasInstructionSet && connectorEnvironment.ORIZU_MODEL_CONFIG_IDENTITY === undefined) throw new Error('--instruction-set requires --model-config')
305
322
  if (hasInstructionSet && connectorEnvironment.ORIZU_COMPONENT_SELECTOR === undefined) connectorEnvironment.ORIZU_COMPONENT_SELECTOR = 'round_robin'
306
323
  if ((connectorEnvironment.ORIZU_PROPOSAL_MAX_CALLS !== undefined
@@ -355,7 +372,7 @@ function validatedMetadata(raw: string | undefined): string {
355
372
  export function dispatchGepaEngine(args: string[], project: string, environment: NodeJS.ProcessEnv): GepaEngineDispatch {
356
373
  const selected = removeEngine(args)
357
374
  if (selected.engine === 'legacy') {
358
- for (const flag of ['--instruction-set', '--model-config', '--component-selector', '--max-candidate-proposals', '--candidate-proposer', '--candidate-proposer-config', '--proposal-max-calls', '--proposal-max-tokens']) {
375
+ for (const flag of ['--instruction-set', '--model-config', '--profile-version', '--component-selector', '--max-candidate-proposals', '--candidate-proposer', '--candidate-proposer-config', '--proposal-max-calls', '--proposal-max-tokens']) {
359
376
  if (selected.args.some(argument => argument === flag || argument.startsWith(`${flag}=`))) {
360
377
  throw new Error(`${flag} is supported by the official GEPA engine only`)
361
378
  }
package/src/help.ts CHANGED
@@ -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
  {
@@ -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
  }
@@ -53,11 +53,13 @@ import { readFileSync } from 'fs'
53
53
 
54
54
  import { findProvider, parseModelIdentity } from './provider-registry.js'
55
55
 
56
+ import { HARNESS_NAMES } from './hosted-harness.js'
56
57
  import type {
57
58
  AgentHarness,
58
59
  HarnessEvent,
59
60
  HarnessPrompt,
60
61
  HarnessStartOptions,
62
+ HarnessToolPayload,
61
63
  } from './hosted-harness.js'
62
64
 
63
65
  // OpenCode is HARD-PINNED to this version everywhere it is installed/launched.
@@ -121,7 +123,9 @@ class PromptAbortedError extends Error {
121
123
  // -- OpenCode ascending id generator (port of OpenCode's id.ts) --------------
122
124
  // Monotonic so our user-message id always sorts AFTER any prior assistant
123
125
  // message id — this is what prevents OpenCode's prompt loop from early-exiting
124
- // on `lastUser.id < lastAssistant.id`.
126
+ // on `lastUser.id < lastAssistant.id`. Shape (opencode-ai@1.14.41): prefix, `_`,
127
+ // 12 hex digits holding the low 48 bits of `ms * 4096 + counter`, then 14
128
+ // base62 characters. `nowMs` is a parameter only so tests can pin the clock.
125
129
  const BASE62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
126
130
  let idLastTimestamp = 0
127
131
  let idCounter = 0
@@ -134,14 +138,15 @@ function randomBase62(length: number): string {
134
138
  return out
135
139
  }
136
140
 
137
- function ascendingId(prefix: 'ses' | 'msg' | 'prt'): string {
138
- const nowMs = Date.now()
141
+ export function ascendingId(prefix: 'ses' | 'msg' | 'prt', nowMs: number = Date.now()): string {
139
142
  if (nowMs !== idLastTimestamp) {
140
143
  idLastTimestamp = nowMs
141
144
  idCounter = 0
142
145
  }
143
146
  idCounter += 1
144
- const encoded = (nowMs * 0x1000 + idCounter) & 0xffffffffffff
147
+ // BigInt, as OpenCode does: the JS `&` operator works on 32 bits, which made
148
+ // short, sometimes negative ids that sorted before OpenCode's own (ORI-2116).
149
+ const encoded = (BigInt(nowMs) * BigInt(0x1000) + BigInt(idCounter)) & BigInt(0xffffffffffff)
145
150
  const hex = encoded.toString(16).padStart(12, '0')
146
151
  return `${prefix}_${hex}${randomBase62(14)}`
147
152
  }
@@ -495,7 +500,7 @@ export function createOpenCodeHarness(options: OpenCodeHarnessOptions): AgentHar
495
500
  }
496
501
 
497
502
  return {
498
- name: 'opencode',
503
+ name: HARNESS_NAMES.opencode,
499
504
 
500
505
  async start(opts: HarnessStartOptions): Promise<{ agentSessionId: string }> {
501
506
  startModel = opts.model
@@ -598,13 +603,25 @@ export function createOpenCodeHarness(options: OpenCodeHarnessOptions): AgentHar
598
603
  if (emittedToolStates.has(toolKey)) return events
599
604
  emittedToolStates.add(toolKey)
600
605
  const terminal = status === 'completed' || status === 'error'
601
- const payload: Record<string, unknown> = {
606
+ const payload: HarnessToolPayload = {
602
607
  tool: typeof part.tool === 'string' ? part.tool : '',
603
608
  args: input,
604
609
  callId,
605
610
  status,
611
+ harness: HARNESS_NAMES.opencode,
612
+ }
613
+ // A failed call keeps its message in `state.error`, not `state.output`
614
+ // (opencode-ai@1.14.41 ToolStateError); it is that call's output.
615
+ if (terminal) {
616
+ payload.output = typeof state.output === 'string' ? state.output
617
+ : typeof state.error === 'string' ? state.error : ''
618
+ }
619
+ // OpenCode's own one-line title and structured extras (ORI-2160),
620
+ // passed through as given; absent when the state has none.
621
+ if (typeof state.title === 'string' && state.title.length > 0) payload.title = state.title
622
+ if (state.metadata && typeof state.metadata === 'object' && !Array.isArray(state.metadata)) {
623
+ payload.metadata = state.metadata as Record<string, unknown>
606
624
  }
607
- if (terminal) payload.output = typeof state.output === 'string' ? state.output : ''
608
625
  events.push({ kind: terminal ? 'tool_result' : 'tool_call', messageId: msgId, payload })
609
626
  } else if (partType === 'step-start') {
610
627
  events.push({ kind: 'step_start', messageId: msgId, payload: {} })
@@ -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,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
+ }