orizu 0.5.15 → 0.5.16

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 (45) hide show
  1. package/dist/artifact-archive.js +20 -4
  2. package/dist/egress-allowlist-cli.js +72 -10
  3. package/dist/help.js +13 -6
  4. package/dist/hosted-runtime-assets.js +8 -1
  5. package/dist/hosted-session-cli.js +70 -15
  6. package/dist/index.js +88 -61
  7. package/dist/kill-switch-cli.js +10 -0
  8. package/dist/optimizations-list-cli.js +85 -0
  9. package/dist/preview-runtime.js +93 -0
  10. package/dist/prompt-draft-push.js +3 -2
  11. package/dist/registration-tag-warning.js +49 -0
  12. package/dist/run-event-digest.js +319 -0
  13. package/dist/runner-dir-verify.js +848 -0
  14. package/dist/scorer-draft-push.js +3 -2
  15. package/dist/scores-accept-cli.js +87 -0
  16. package/dist/workbench-cli.js +4 -2
  17. package/dist/zip-draft-push.js +3 -2
  18. package/package.json +1 -1
  19. package/src/artifact-archive.ts +29 -4
  20. package/src/egress-allowlist-cli.ts +87 -13
  21. package/src/help.ts +13 -6
  22. package/src/hosted-runtime-assets.ts +14 -1
  23. package/src/hosted-session-cli.ts +76 -14
  24. package/src/index.ts +92 -74
  25. package/src/kill-switch-cli.ts +10 -0
  26. package/src/optimizations-list-cli.ts +117 -0
  27. package/src/preview-runtime.ts +94 -0
  28. package/src/prompt-draft-push.ts +5 -2
  29. package/src/registration-tag-warning.ts +58 -0
  30. package/src/run-event-digest.ts +333 -0
  31. package/src/runner-dir-verify.ts +1029 -0
  32. package/src/scorer-draft-push.ts +5 -2
  33. package/src/scores-accept-cli.ts +124 -0
  34. package/src/workbench-cli.ts +4 -2
  35. package/src/zip-draft-push.ts +5 -2
  36. package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +80 -3
  37. package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +288 -15
  38. package/vendor/orizu-gepa-python/src/orizu_gepa/runner.py +133 -8
  39. package/vendor/skills/orizu-cli/SKILL.md +2 -0
  40. package/vendor/skills/orizu-cli/references/building-apps.md +131 -1
  41. package/vendor/skills/orizu-cli/references/building-judges.md +18 -0
  42. package/vendor/skills/orizu-cli/references/cli-reference.md +2 -1
  43. package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +3 -1
  44. package/vendor/skills/orizu-cli/references/prompt-control-plane.md +75 -0
  45. package/vendor/skills/orizu-cli/scripts/test-app.mjs +3 -0
@@ -11,7 +11,9 @@ const ARTIFACT_EXCLUDED_PATH_NAMES = new Set([
11
11
  function isNodeError(error) {
12
12
  return error instanceof Error;
13
13
  }
14
- function expandHomePath(path) {
14
+ /** Exported for runner-dir-verify (ALI-1159): the pre-exec scan must resolve
15
+ * the same directory this module hashes and the runner later executes. */
16
+ export function expandHomePath(path) {
15
17
  if (path.startsWith('~/')) {
16
18
  const home = process.env.HOME || '';
17
19
  return `${home}/${path.slice(2)}`;
@@ -123,7 +125,13 @@ export function createStoredZip(entries) {
123
125
  endOfCentralDirectory.writeUInt16LE(0, 20);
124
126
  return Buffer.concat([...localParts, centralDirectory, endOfCentralDirectory]);
125
127
  }
126
- export function zipDirectoryToBase64(dirArg) {
128
+ /**
129
+ * Read the exact entry set the deterministic archive covers (sorted relative
130
+ * paths + bytes). Exported for runner-dir-verify (ALI-1159): the verified
131
+ * snapshot must be materialized from the SAME in-memory bytes that were
132
+ * hashed, so a post-hash mutation of the directory cannot change what runs.
133
+ */
134
+ export function readArtifactEntries(dirArg) {
127
135
  const sourceDir = expandHomePath(dirArg);
128
136
  try {
129
137
  const stats = statSync(sourceDir);
@@ -141,12 +149,20 @@ export function zipDirectoryToBase64(dirArg) {
141
149
  if (files.length === 0) {
142
150
  throw new Error(`Directory contains no artifact files: ${sourceDir}`);
143
151
  }
144
- const bytes = createStoredZip(files.map(relativePath => ({
152
+ return files.map(relativePath => ({
145
153
  path: relativePath,
146
154
  data: readFileSync(join(sourceDir, relativePath)),
147
- })));
155
+ mode: statSync(join(sourceDir, relativePath)).mode,
156
+ }));
157
+ }
158
+ /** Deterministic archive of an already-read entry set. */
159
+ export function archiveArtifactEntries(entries) {
160
+ const bytes = createStoredZip(entries);
148
161
  return {
149
162
  zipBase64: bytes.toString('base64'),
150
163
  contentSha256: createHash('sha256').update(bytes).digest('hex'),
151
164
  };
152
165
  }
166
+ export function zipDirectoryToBase64(dirArg) {
167
+ return archiveArtifactEntries(readArtifactEntries(dirArg));
168
+ }
@@ -13,6 +13,12 @@
13
13
  * with an injected fetcher.
14
14
  */
15
15
  import { authedFetch } from './http.js';
16
+ /** Mirrors MAX_TEAM_EGRESS_DOMAINS in the shared read-side validator
17
+ * (workers/session-coordinator/src/egress-domains.ts). The coordinator only
18
+ * applies the first this-many UNIQUE valid domains at policy compose; the
19
+ * CLI is a standalone package and cannot import the worker module, so the
20
+ * cap constant is duplicated here (kept in sync via that file's tests). */
21
+ const MAX_ADDITIVE_EGRESS_DOMAINS = 100;
16
22
  function fetcherFrom(io) {
17
23
  return io.fetcher ?? authedFetch;
18
24
  }
@@ -55,22 +61,72 @@ function splitDomains(value) {
55
61
  .map(d => d.trim())
56
62
  .filter(d => d.length > 0);
57
63
  }
64
+ function stringArray(value) {
65
+ return Array.isArray(value) ? value.filter((d) => typeof d === 'string') : [];
66
+ }
67
+ /**
68
+ * Re-derive the "beyond the 100-domain cap" entries from a stored set the
69
+ * SAME way the GET route does (positional over UNIQUE VALID domains): the
70
+ * coordinator applies only the first MAX_ADDITIVE_EGRESS_DOMAINS valid
71
+ * domains, so every valid entry past that index is configured-but-inert.
72
+ * Stored entries are already canonical + deduped by the write RPC, and the
73
+ * invalid (grandfathered) ones — flagged by the server — are not "valid"
74
+ * and so never consume a cap slot. Used for the PUT response, where a
75
+ * shrink-by-one on an oversized legacy row leaves the returned set still
76
+ * over-cap and the server does not send cappedDomains back.
77
+ */
78
+ function deriveCapped(stored, invalid) {
79
+ const invalidSet = new Set(invalid);
80
+ const capped = [];
81
+ let validCount = 0;
82
+ for (const d of stored) {
83
+ if (invalidSet.has(d))
84
+ continue;
85
+ validCount += 1;
86
+ if (validCount > MAX_ADDITIVE_EGRESS_DOMAINS)
87
+ capped.push(d);
88
+ }
89
+ return capped;
90
+ }
58
91
  async function fetchCurrent(fetcher, path) {
59
92
  const response = await fetcher(path, { method: 'GET' });
60
93
  if (!response.ok) {
61
94
  throw new Error(`show failed (${response.status}): ${await responseMessage(response)}`);
62
95
  }
63
96
  const data = (await response.json());
64
- return Array.isArray(data.extraDomains) ? data.extraDomains.filter((d) => typeof d === 'string') : [];
97
+ return {
98
+ domains: stringArray(data.extraDomains),
99
+ invalid: stringArray(data.invalidDomains),
100
+ capped: stringArray(data.cappedDomains),
101
+ };
65
102
  }
66
- function emit(io, teamSlug, domains) {
103
+ function emit(io, teamSlug, current) {
67
104
  if (io.json) {
68
- io.print(JSON.stringify({ teamSlug, extraDomains: domains }));
105
+ io.print(JSON.stringify({
106
+ teamSlug,
107
+ extraDomains: current.domains,
108
+ invalidDomains: current.invalid,
109
+ cappedDomains: current.capped,
110
+ }));
69
111
  return;
70
112
  }
71
- io.print(domains.length === 0
113
+ const invalid = new Set(current.invalid);
114
+ const capped = new Set(current.capped);
115
+ const lines = current.domains.map((d) => {
116
+ // Round 4 (P3): re-adding a non-grandfathered invalid entry is
117
+ // guaranteed to fail the write RPC's tightened validator (grandfathering
118
+ // is retention-only — see set_team_egress_allowlist), so "remove and
119
+ // re-add" was actively misleading. The only viable remediation is
120
+ // removal.
121
+ if (invalid.has(d))
122
+ return `${d} (invalid — will not take effect; remove it (this value is no longer valid and cannot be re-added))`;
123
+ if (capped.has(d))
124
+ return `${d} (beyond the 100-domain cap — will not take effect)`;
125
+ return d;
126
+ });
127
+ io.print(lines.length === 0
72
128
  ? `${teamSlug}: no additive egress domains (base allowlist only)`
73
- : `${teamSlug} additive egress domains:\n ${domains.join('\n ')}`);
129
+ : `${teamSlug} additive egress domains:\n ${lines.join('\n ')}`);
74
130
  }
75
131
  export async function egressAllowlistCommand(args, io) {
76
132
  const teamSlug = firstPositional(args);
@@ -100,7 +156,7 @@ export async function egressAllowlistCommand(args, io) {
100
156
  }
101
157
  else {
102
158
  const current = await fetchCurrent(fetcher, path);
103
- const set = new Set(current);
159
+ const set = new Set(current.domains);
104
160
  if (addValue)
105
161
  for (const d of splitDomains(addValue))
106
162
  set.add(d);
@@ -119,10 +175,16 @@ export async function egressAllowlistCommand(args, io) {
119
175
  return 1;
120
176
  }
121
177
  const data = (await response.json());
122
- const stored = Array.isArray(data.extraDomains)
123
- ? data.extraDomains.filter((d) => typeof d === 'string')
124
- : next;
125
- emit(io, teamSlug, stored);
178
+ const stored = Array.isArray(data.extraDomains) ? stringArray(data.extraDomains) : next;
179
+ const invalid = stringArray(data.invalidDomains);
180
+ // Grandfathered retention: a successful PUT may still STORE legacy
181
+ // invalid entries the admin retained — the server flags them and we show
182
+ // it. Round 5 (P2): a >100-entry legacy row can only shrink one entry per
183
+ // PUT (the write RPC's GREATEST(100, stored-1) bound), so the returned
184
+ // set can still be OVER the cap. Re-derive the cap annotations from the
185
+ // returned set — same positional logic GET uses — instead of assuming
186
+ // the RPC left it at/under 100.
187
+ emit(io, teamSlug, { domains: stored, invalid, capped: deriveCapped(stored, invalid) });
126
188
  return 0;
127
189
  }
128
190
  catch (error) {
package/dist/help.js CHANGED
@@ -394,6 +394,12 @@ export const COMMAND_DOCS = [
394
394
  summary: 'Submit score results for a scorer against a prompt version or optimization candidate.',
395
395
  group: 'Scorers and runners',
396
396
  },
397
+ {
398
+ path: ['scores', 'accept'],
399
+ usage: 'orizu scores accept <score-run-id> [--project <team/project>] [--json]',
400
+ summary: 'Record a human curator acceptance of an agent-reported score as trusted evidence (human-only).',
401
+ group: 'Scorers and runners',
402
+ },
397
403
  {
398
404
  path: ['runners', 'push'],
399
405
  usage: 'orizu runners push <dir> [--project <team/project>] [--name <name>] [--label <label>] [--json]',
@@ -415,7 +421,7 @@ export const COMMAND_DOCS = [
415
421
  {
416
422
  path: ['runners', 'exec'],
417
423
  usage: 'orizu runners exec (--prompt <prompt-version-id> | --prompt-version <id> --runner-version <id> | --scorer-version <id>) --dataset-version <id> --split-set <id-or-name> --split <name> [--runner-dir <dir>] --out <results.jsonl|results.jsonl.gz>',
418
- summary: 'Execute a runner locally against a dataset split and write row results.',
424
+ summary: 'Execute a runner locally against a dataset split and write row results. --runner-dir bytes must match the registered runner version (ADR-007) — register with `orizu runners push` first.',
419
425
  group: 'Scorers and runners',
420
426
  },
421
427
  {
@@ -450,8 +456,8 @@ export const COMMAND_DOCS = [
450
456
  },
451
457
  {
452
458
  path: ['optimizations', 'run-gepa'],
453
- usage: 'orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n>] [--num-threads auto|N] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--log-dir logs]',
454
- summary: 'Run the bundled GEPA-style optimizer locally and stream events.',
459
+ usage: 'orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> [--scorer-input-contract gepa|flat_row] [--scorer-candidate-field <row-field>] [--allow-degenerate-seed] --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n>] [--num-threads auto|N] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--log-dir logs]',
460
+ summary: 'Run the bundled GEPA-style optimizer locally and stream events. 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).',
455
461
  group: 'Optimizations',
456
462
  },
457
463
  {
@@ -747,7 +753,7 @@ export const COMMAND_DOCS = [
747
753
  },
748
754
  {
749
755
  path: ['session', 'start'],
750
- usage: 'orizu session start [--project <team/project>] [--workspace <dir>] [--json] | orizu session start --hosted (--task <prompt> | --task-file <path>) [--duration <min>] [--project <team/project>] [--tail]',
756
+ usage: 'orizu session start [--project <team/project>] [--workspace <dir>] [--json] | orizu session start --hosted (--task <prompt> | --task-file <path>) [--duration <min>] [--project <team/project>] [--tail [--quiet]]',
751
757
  summary: 'Start a durable workspace session, or add --hosted with a task prompt to start a coordinator-managed hosted agent. Plain sessions check out their remote session branch locally when run inside the workbench clone (or with --workspace).',
752
758
  group: 'Sessions',
753
759
  options: [
@@ -760,7 +766,8 @@ export const COMMAND_DOCS = [
760
766
  { name: '--model <provider/model>', help: 'Optional hosted model override.' },
761
767
  { name: '--reasoning-effort <level>', help: 'Optional hosted model reasoning-effort override.' },
762
768
  { name: '--title <title>', help: 'Optional hosted run title.' },
763
- { name: '--tail', help: 'Follow the hosted run until it reaches a terminal state.' },
769
+ { name: '--tail', help: 'Follow the hosted run until it reaches a terminal state, streaming compact per-event digests (agent text, tool calls with args, results).' },
770
+ { name: '--quiet', help: 'With --tail: stream event types only (the pre-digest output).' },
764
771
  { name: '--json', help: 'Emit the machine-readable session payload.' },
765
772
  ],
766
773
  examples: [
@@ -835,7 +842,7 @@ export const COMMAND_DOCS = [
835
842
  {
836
843
  path: ['run', 'tail'],
837
844
  usage: 'orizu run tail --run <id> [--after <seq>] [--interval <seconds>] [--once] [--json]',
838
- summary: 'Cursor-poll a workbench run event log; JSON mode emits one event object per line.',
845
+ summary: 'Cursor-poll a workbench run event log as compact per-event digest lines; JSON mode emits one full event object per line.',
839
846
  group: 'Workbench runs',
840
847
  options: [
841
848
  { name: '--run <id>', help: 'Workbench run id.', required: true },
@@ -101,7 +101,14 @@ export function parsePrebakedMarker(raw) {
101
101
  typeof claudeSdkVersion !== 'string') {
102
102
  return null;
103
103
  }
104
- return { cliVersion, opencodeVersion, claudeSdkVersion, builtFor: 'vercel-sandbox' };
104
+ const marker = { cliVersion, opencodeVersion, claudeSdkVersion, builtFor: 'vercel-sandbox' };
105
+ // Optional Braintrust pins (ALI-1048): carried through when present as strings;
106
+ // absent/odd-typed values are simply omitted (never invalidate the marker).
107
+ if (typeof rec.braintrustPyVersion === 'string')
108
+ marker.braintrustPyVersion = rec.braintrustPyVersion;
109
+ if (typeof rec.braintrustNpmVersion === 'string')
110
+ marker.braintrustNpmVersion = rec.braintrustNpmVersion;
111
+ return marker;
105
112
  }
106
113
  export function serializeBootContext(ctx) {
107
114
  return `${JSON.stringify(ctx, null, 2)}\n`;
@@ -50,6 +50,7 @@ import { findUnknownOption } from './option-validation.js';
50
50
  import { getWorkspaceRoot } from './workspace.js';
51
51
  import { attachedWorkspaceId } from './workspace-sync.js';
52
52
  import { tailWorkbenchRun } from './workbench-cli.js';
53
+ import { formatRunEventDigest, formatRunEventTypeLine } from './run-event-digest.js';
53
54
  // Default hosted model — SINGLE source of truth in hosted-loop-lifecycle.ts
54
55
  // (ALI-1086); re-exported here so existing importers keep working.
55
56
  export { DEFAULT_HOSTED_MODEL };
@@ -621,7 +622,9 @@ function numFlag(args, flag) {
621
622
  }
622
623
  const HOSTED_USAGE = 'Usage: orizu session start --hosted (--task "<prompt>" | --task-file <path>) [--duration <min> (default 60, max 1440)] ' +
623
624
  '[--model <provider/model>] [--reasoning-effort <level>] [--project <team/project>] ' +
624
- '[--title <title>] [--tail]\n' +
625
+ '[--title <title>] [--tail [--quiet]]\n' +
626
+ '--tail streams compact per-event digests (agent text, tool calls + args, results); ' +
627
+ '--quiet restores the types-only stream; --json keeps full event payloads.\n' +
625
628
  'Default: the Orizu server provisions the sandbox (no VERCEL_* or model key needed on your machine); ' +
626
629
  'the session coordinator owns its lifetime (ALI-1055).\n' +
627
630
  'DEPRECATED escape hatch: --operator provisions from THIS machine (requires VERCEL_* + ANTHROPIC_API_KEY ' +
@@ -640,6 +643,7 @@ const HOSTED_START_OPTIONS = new Set([
640
643
  '--project',
641
644
  '--title',
642
645
  '--tail',
646
+ '--quiet',
643
647
  '--operator',
644
648
  '--provider',
645
649
  '--runtime',
@@ -673,7 +677,24 @@ export const OPERATOR_DEPRECATION_WARNING = '! DEPRECATED: --operator (operator-
673
677
  * route and let the Durable Object coordinator own the sandbox lifetime. The
674
678
  * CLI never reads VERCEL_* or a model key on this path — the server holds them.
675
679
  */
676
- export async function startHostedViaServer(args, workspaceId, task, tail, io, fetcher = (path, init) => authedFetch(path, init)) {
680
+ /** Bounded reconcile of a start that the coordinator reported still-in-flight
681
+ * (R7 P4). Each retry re-pokes the SAME session by its returned sessionId, so
682
+ * a race that momentarily answers start_pending never spawns a duplicate.
683
+ *
684
+ * R8 P2 — the retry budget must OUTLIVE a stale start lease. If a start-lease
685
+ * owner crashed WITHOUT running its release, the lease lingers until its TTL
686
+ * (the coordinator's START_LEASE_TTL_MS = 3 × CALLBACK_TIMEOUT_MS = 30s), and
687
+ * every duplicate poke sees it and answers start_pending. So the CLI's OWN
688
+ * cumulative backoff (independent of server-side watch time) must exceed that
689
+ * TTL plus margin, so the CLI keeps polling until the lease expires and a
690
+ * retry then wins — rather than giving up while the crashed owner's lease
691
+ * still lingers. COORDINATOR_START_LEASE_TTL_MS mirrors the worker constant
692
+ * and is pinned to it by a contract test in the CLI suite. */
693
+ export const COORDINATOR_START_LEASE_TTL_MS = 30_000;
694
+ export const HOSTED_START_PENDING_RETRY_MS = 2_000;
695
+ const HOSTED_START_PENDING_TTL_MARGIN_MS = 6_000;
696
+ export const HOSTED_START_PENDING_MAX_RETRIES = Math.ceil((COORDINATOR_START_LEASE_TTL_MS + HOSTED_START_PENDING_TTL_MARGIN_MS) / HOSTED_START_PENDING_RETRY_MS);
697
+ export async function startHostedViaServer(args, workspaceId, task, tail, io, fetcher = (path, init) => authedFetch(path, init), sleep = ms => new Promise(resolve => setTimeout(resolve, ms))) {
677
698
  const durationMinutes = numFlag(args, '--duration');
678
699
  const model = argVal(args, '--model') ?? undefined;
679
700
  const reasoningEffort = argVal(args, '--reasoning-effort') ?? undefined;
@@ -727,19 +748,39 @@ export async function startHostedViaServer(args, workspaceId, task, tail, io, fe
727
748
  io.printErr?.(message);
728
749
  return 1;
729
750
  }
751
+ // R7 P4: the coordinator may momentarily answer start_pending (a start for
752
+ // this session is racing). It returns the sessionId so we RECONCILE the SAME
753
+ // session — re-poking by that id rather than blindly re-initing a duplicate.
730
754
  let response;
731
- try {
732
- response = await fetcher('/api/cli/hosted-sessions', {
733
- method: 'POST',
734
- headers: { 'Content-Type': 'application/json' },
735
- body: serializedPayload,
736
- });
737
- }
738
- catch (error) {
739
- io.printErr?.(`hosted session request failed: ${error instanceof Error ? error.message : String(error)}`);
740
- return 1;
755
+ let body = {};
756
+ let reconcileSessionId;
757
+ for (let attempt = 0;; attempt++) {
758
+ const attemptBody = reconcileSessionId
759
+ ? JSON.stringify({ ...payload, sessionId: reconcileSessionId })
760
+ : serializedPayload;
761
+ try {
762
+ response = await fetcher('/api/cli/hosted-sessions', {
763
+ method: 'POST',
764
+ headers: { 'Content-Type': 'application/json' },
765
+ body: attemptBody,
766
+ });
767
+ }
768
+ catch (error) {
769
+ io.printErr?.(`hosted session request failed: ${error instanceof Error ? error.message : String(error)}`);
770
+ return 1;
771
+ }
772
+ body = (await response.json().catch(() => ({})));
773
+ const stillPending = response.status === 409 &&
774
+ body.coordinator === 'start_pending' &&
775
+ typeof body.sessionId === 'string' &&
776
+ body.sessionId.length > 0;
777
+ if (stillPending && attempt < HOSTED_START_PENDING_MAX_RETRIES) {
778
+ reconcileSessionId = body.sessionId;
779
+ await sleep(HOSTED_START_PENDING_RETRY_MS);
780
+ continue;
781
+ }
782
+ break;
741
783
  }
742
- const body = (await response.json().catch(() => ({})));
743
784
  const errorMessage = typeof body.error === 'string' ? body.error : `${response.status}`;
744
785
  if (response.status === 503) {
745
786
  io.printErr?.(errorMessage);
@@ -764,12 +805,20 @@ export async function startHostedViaServer(args, workspaceId, task, tail, io, fe
764
805
  io.print(`stream it: orizu run tail --run ${runId}`);
765
806
  }
766
807
  if (tail && runId) {
808
+ // ALI-1045: the attached tail renders the SAME compact per-event digests
809
+ // `run tail` uses (agent text, tool + args, result + first output line) so
810
+ // the operator watching live sees what is happening without a second
811
+ // terminal. `--quiet` keeps the old types-only stream; `--json` keeps full
812
+ // fidelity.
813
+ const quiet = hasFlag(args, '--quiet');
767
814
  await tailWorkbenchRun({
768
815
  fetcher,
769
816
  runId,
770
817
  onEvent: event => io.print(io.json
771
818
  ? JSON.stringify(event)
772
- : `#${String(event.sequence ?? '?')} ${String(event.eventType ?? '')}`),
819
+ : quiet
820
+ ? formatRunEventTypeLine(event)
821
+ : formatRunEventDigest(event)),
773
822
  });
774
823
  }
775
824
  return 0;
@@ -882,7 +931,13 @@ export async function hostedCommand(args, io, deps = {}) {
882
931
  // fail-fast in startHostedSession throws before any session/sandbox is made.
883
932
  modelApiKey: argVal(args, '--model-key') ?? (process.env.ANTHROPIC_API_KEY || undefined),
884
933
  logLine: line => io.print(line),
885
- onTailEvent: event => io.print(io.json ? JSON.stringify(event) : `#${String(event.sequence ?? '?')} ${String(event.eventType ?? '')}`),
934
+ // ALI-1045: compact per-event digests (shared with `run tail`); `--quiet`
935
+ // keeps the old types-only stream, `--json` keeps full fidelity.
936
+ onTailEvent: event => io.print(io.json
937
+ ? JSON.stringify(event)
938
+ : hasFlag(args, '--quiet')
939
+ ? formatRunEventTypeLine(event)
940
+ : formatRunEventDigest(event)),
886
941
  tailImpl: tail
887
942
  ? async ({ fetcher, runId, onEvent }) => {
888
943
  await tailWorkbenchRun({ fetcher, runId, onEvent: event => onEvent?.(event) });
package/dist/index.js CHANGED
@@ -29,6 +29,8 @@ import { connectorsCommand } from './connectors-cli.js';
29
29
  import { ARTIFACT_MAX_BYTES, runnerOptimizerCommand } from './artifact-pull.js';
30
30
  import { killSwitchCommand } from './kill-switch-cli.js';
31
31
  import { egressAllowlistCommand } from './egress-allowlist-cli.js';
32
+ import { acceptScoreRunCommand } from './scores-accept-cli.js';
33
+ import { listOptimizationRunsCommand } from './optimizations-list-cli.js';
32
34
  import { teamConnectorsCommand } from './team-connectors-cli.js';
33
35
  import { manifestsCommand } from './manifests-cli.js';
34
36
  import { workbenchCommand } from './workbench-cli.js';
@@ -36,6 +38,7 @@ import { hostedCommand } from './hosted-session-cli.js';
36
38
  import { workspaceSyncCommand } from './workspace-sync.js';
37
39
  import { runGitCredential } from './git-credential.js';
38
40
  import { pushPromptDraft } from './prompt-draft-push.js';
41
+ import { assertSnapshotManifestConfined, verifyGepaRunnerDirsFromArgs, verifyRunnerDirRegistered } from './runner-dir-verify.js';
39
42
  import { runScorersRegister } from './scorer-draft-push.js';
40
43
  import { runZipArtifactPush } from './zip-draft-push.js';
41
44
  import { runGithubLink, runInteractiveHostedSetup } from './github-setup.js';
@@ -1504,8 +1507,10 @@ async function submitScoreResults() {
1504
1507
  printJson(data);
1505
1508
  return;
1506
1509
  }
1510
+ // ALI-1175: label the stored provenance — a submitted score is a client-reported claim.
1507
1511
  printLine(`Submitted score ${sanitizeTerminalText(data.scoreRun.id)} ` +
1508
- `(${formatPercent(data.scoreRun.scoreValue)})`);
1512
+ `(${formatPercent(data.scoreRun.scoreValue)})` +
1513
+ (data.scoreRun.provenance ? ` [${sanitizeTerminalText(String(data.scoreRun.provenance).replace(/_/g, '-'))}]` : ''));
1509
1514
  }
1510
1515
  function looksLikeAggregateScoreObject(value) {
1511
1516
  return (value.scoreValue !== undefined ||
@@ -1640,11 +1645,23 @@ async function execScorer() {
1640
1645
  : scoreResult
1641
1646
  ? scoreResult.scoreValue
1642
1647
  : null;
1648
+ // ALI-1175 (round 5 thread 6 + round 6 thread 2): a scorers-exec score is an
1649
+ // agent-reported claim whether or not it was submitted — label both the
1650
+ // submitted (scoreRun.provenance) and the --no-submit (top-level provenance)
1651
+ // outputs, so a displayed value is never presented as unlabeled evidence.
1652
+ const execProvenance = scoreRun && typeof scoreRun.provenance === 'string'
1653
+ ? scoreRun.provenance
1654
+ : typeof data.provenance === 'string'
1655
+ ? data.provenance
1656
+ : null;
1657
+ const provenanceSuffix = execProvenance
1658
+ ? ` [${sanitizeTerminalText(execProvenance.replace(/_/g, '-'))}]`
1659
+ : '';
1643
1660
  if (scoreRun && typeof scoreRun.id === 'string') {
1644
- printLine(`Executed scorer ${sanitizeTerminalText(scorerVersionId)} -> score ${sanitizeTerminalText(scoreRun.id)} (${formatPercent(numberFromUnknown(scoreValue))})`);
1661
+ printLine(`Executed scorer ${sanitizeTerminalText(scorerVersionId)} -> score ${sanitizeTerminalText(scoreRun.id)} (${formatPercent(numberFromUnknown(scoreValue))})${provenanceSuffix}`);
1645
1662
  }
1646
1663
  else {
1647
- printLine(`Executed scorer ${sanitizeTerminalText(scorerVersionId)} (${formatPercent(numberFromUnknown(scoreValue))})`);
1664
+ printLine(`Executed scorer ${sanitizeTerminalText(scorerVersionId)} (${formatPercent(numberFromUnknown(scoreValue))})${provenanceSuffix}`);
1648
1665
  }
1649
1666
  if (outPath) {
1650
1667
  printLine(`Wrote scorer result to ${sanitizeTerminalText(outPath)}`);
@@ -1866,34 +1883,8 @@ async function exportOptimizationRun() {
1866
1883
  writeTextFileEnsuringDir(filename, `${JSON.stringify(data, null, 2)}\n`);
1867
1884
  printLine(`Saved optimization export to ${sanitizeTerminalText(filename)}`);
1868
1885
  }
1869
- // ALI-1073: enumerate the project's optimization runs (id, status, refs, and the
1870
- // promoted-version link if any) so an agent/human can discover a prior run.
1871
- async function listOptimizationRuns() {
1872
- const project = getArg('--project') || await resolveProjectSlug(null);
1873
- const response = await authedFetch(`/api/cli/optimization-runs?project=${encodeURIComponent(project)}`);
1874
- if (!response.ok) {
1875
- throw new Error(`Failed to list optimization runs: ${await response.text()}`);
1876
- }
1877
- const data = await parseJsonResponse(response, 'Optimization runs list');
1878
- if (hasJsonFlag()) {
1879
- printJson(data);
1880
- return;
1881
- }
1882
- const rows = data.optimizationRuns || [];
1883
- if (rows.length === 0) {
1884
- printLine('No optimization runs found for this project.');
1885
- return;
1886
- }
1887
- const idWidth = Math.max(2, ...rows.map(row => String(row.id).length));
1888
- const statusWidth = Math.max(6, ...rows.map(row => String(row.status).length));
1889
- printLine(`${'ID'.padEnd(idWidth)} ${'STATUS'.padEnd(statusWidth)} BEST PROMOTED_VERSION`);
1890
- printLine(`${'-'.repeat(idWidth)} ${'-'.repeat(statusWidth)} ${'-'.repeat(5)} ${'-'.repeat('PROMOTED_VERSION'.length)}`);
1891
- for (const row of rows) {
1892
- const best = row.bestScore === null || row.bestScore === undefined ? '-' : String(row.bestScore);
1893
- const promoted = row.resultPromptVersionId || '-';
1894
- printLine(`${String(row.id).padEnd(idWidth)} ${String(row.status).padEnd(statusWidth)} ${best.padEnd(5)} ${promoted}`);
1895
- }
1896
- }
1886
+ // ALI-1073 list moved to optimizations-list-cli.ts (line ratchet, ALI-976);
1887
+ // ALI-1175 labels its best scores as agent-reported evidence there.
1897
1888
  function removeFlagWithValue(args, flag) {
1898
1889
  const filtered = [];
1899
1890
  for (let index = 0; index < args.length; index += 1) {
@@ -1915,31 +1906,50 @@ function bundledOrizuGepaPythonPath() {
1915
1906
  async function runGepaOptimization() {
1916
1907
  const project = getArg('--project') || await resolveProjectSlug(null);
1917
1908
  const baseUrl = getBaseUrl();
1918
- // Uniform resolution (ALI-1090): honors ORIZU_TOKEN / ORIZU_TOKEN_FILE before
1919
- // credentials.json so hosted sandboxes can run optimizations. Read at spawn
1920
- // time — the child gets the freshest bearer available at launch.
1921
- const token = resolveAuthTokenForBaseUrl(baseUrl);
1922
1909
  const python = getArg('--python') || process.env.PYTHON || 'python3';
1923
1910
  const bundledPythonPath = bundledOrizuGepaPythonPath();
1924
1911
  let forwardedArgs = removeFlagWithValue(cliArgs.slice(2), '--python');
1925
- if (!forwardedArgs.includes('--project')) {
1926
- forwardedArgs = ['--project', project, ...forwardedArgs];
1927
- }
1928
- const pythonPathEntries = [
1929
- bundledPythonPath,
1930
- process.env.PYTHONPATH,
1931
- ].filter((entry) => Boolean(entry));
1932
- const result = spawnSync(python, ['-m', 'orizu_gepa.cli', ...forwardedArgs], {
1933
- stdio: 'inherit',
1934
- env: {
1935
- ...process.env,
1936
- ORIZU_API_URL: baseUrl,
1937
- ORIZU_TOKEN: token,
1938
- ORIZU_PROJECT: project,
1939
- PYTHONPATH: pythonPathEntries.join(delimiter),
1940
- PYTHONUNBUFFERED: process.env.PYTHONUNBUFFERED || '1',
1941
- },
1942
- });
1912
+ // ALI-1159 (ADR-007): GEPA executes ad-hoc local runner dirs while
1913
+ // attributing records to registered runner version ids — verify the bytes
1914
+ // ARE those versions before the optimizer spawns. Scans the forwarded argv
1915
+ // itself (both argparse flag forms), fail-closed on half-populated pairs,
1916
+ // and REWRITES the dir flags to verified snapshots (the exact hashed bytes)
1917
+ // so a post-hash mutation of the original dirs cannot change what runs.
1918
+ const verified = await verifyGepaRunnerDirsFromArgs(forwardedArgs);
1919
+ forwardedArgs = verified.args;
1920
+ let result;
1921
+ try {
1922
+ // Uniform resolution (ALI-1090): honors ORIZU_TOKEN / ORIZU_TOKEN_FILE before
1923
+ // credentials.json so hosted sandboxes can run optimizations. Read at spawn
1924
+ // time — AFTER the verification awaits above, so a hosted token-file
1925
+ // rotation crossing the hash + lookup round-trips cannot hand the child a
1926
+ // stale bearer.
1927
+ const token = resolveAuthTokenForBaseUrl(baseUrl);
1928
+ if (!forwardedArgs.includes('--project')) {
1929
+ forwardedArgs = ['--project', project, ...forwardedArgs];
1930
+ }
1931
+ const pythonPathEntries = [
1932
+ bundledPythonPath,
1933
+ process.env.PYTHONPATH,
1934
+ ].filter((entry) => Boolean(entry));
1935
+ result = spawnSync(python, ['-m', 'orizu_gepa.cli', ...forwardedArgs], {
1936
+ stdio: 'inherit',
1937
+ env: {
1938
+ ...process.env,
1939
+ ORIZU_API_URL: baseUrl,
1940
+ ORIZU_TOKEN: token,
1941
+ ORIZU_PROJECT: project,
1942
+ // Integrity handshake (ALI-1159): the Python entrypoint refuses
1943
+ // runner dirs that are not in this wrapper-verified list.
1944
+ ORIZU_VERIFIED_RUNNER_DIRS: JSON.stringify(verified.verifiedDirs),
1945
+ PYTHONPATH: pythonPathEntries.join(delimiter),
1946
+ PYTHONUNBUFFERED: process.env.PYTHONUNBUFFERED || '1',
1947
+ },
1948
+ });
1949
+ }
1950
+ finally {
1951
+ verified.cleanup();
1952
+ }
1943
1953
  if (result.error) {
1944
1954
  throw result.error;
1945
1955
  }
@@ -4635,10 +4645,7 @@ async function materializeRunnerVersion(runnerVersionId) {
4635
4645
  rmSync(tempDir, { recursive: true, force: true });
4636
4646
  throw new Error(`unzip failed: ${sanitizeTerminalText(result.stderr || result.stdout || '')}`);
4637
4647
  }
4638
- return {
4639
- runnerDir,
4640
- cleanup: () => rmSync(tempDir, { recursive: true, force: true }),
4641
- };
4648
+ return { runnerDir, cleanup: () => rmSync(tempDir, { recursive: true, force: true }) };
4642
4649
  }
4643
4650
  async function runnersExec() {
4644
4651
  const prompt = getArg('--prompt');
@@ -4680,12 +4687,28 @@ async function runnersExec() {
4680
4687
  throw new Error(`Failed to fetch runner execution context: ${await contextResponse.text()}`);
4681
4688
  }
4682
4689
  const context = await parseJsonResponse(contextResponse, 'Runner exec context');
4690
+ const resolvedRunnerVersionId = runnerVersion || context.prompt.runnerVersionId;
4691
+ if (!resolvedRunnerVersionId) {
4692
+ throw new Error('Runner exec context did not resolve a runner version id; pass --runner-version explicitly');
4693
+ }
4694
+ // ALI-1159 (ADR-007): results stamp the registered runner_version_id, so
4695
+ // ad-hoc --runner-dir bytes must BE that registered version — and execution
4696
+ // runs the verified SNAPSHOT (exact hashed bytes), never the mutable dir.
4683
4697
  const materializedRunner = runnerDirArg
4684
- ? { runnerDir: expandHomePath(runnerDirArg), cleanup: () => { } }
4685
- : await materializeRunnerVersion(runnerVersion || context.prompt.runnerVersionId);
4698
+ ? await (async () => {
4699
+ const verified = await verifyRunnerDirRegistered({
4700
+ runnerVersionId: resolvedRunnerVersionId,
4701
+ dir: runnerDirArg,
4702
+ flag: '--runner-dir',
4703
+ });
4704
+ return { runnerDir: verified.snapshotDir, cleanup: verified.cleanup };
4705
+ })()
4706
+ : await materializeRunnerVersion(resolvedRunnerVersionId);
4686
4707
  const runnerDir = materializedRunner.runnerDir;
4687
- const manifest = readRunnerManifest(runnerDir);
4688
4708
  try {
4709
+ assertSnapshotManifestConfined(runnerDir, runnerDirArg ? '--runner-dir' : '--runner-version');
4710
+ // Inside the try (codex round-6 P3): a bad manifest must not leak the dir.
4711
+ const manifest = readRunnerManifest(runnerDir);
4689
4712
  if (manifest.supports_body_kind && !manifest.supports_body_kind.includes(context.prompt.bodyKind)) {
4690
4713
  throw new Error(`Runner does not support prompt body kind '${context.prompt.bodyKind}'. Supported kinds: ${manifest.supports_body_kind.join(', ')}`);
4691
4714
  }
@@ -4996,6 +5019,10 @@ export async function main(rawArgs = process.argv.slice(2)) {
4996
5019
  await submitScoreResults();
4997
5020
  return;
4998
5021
  }
5022
+ if (command === 'scores' && subcommand === 'accept') {
5023
+ await acceptScoreRunCommand(cliArgs.slice(2), { json: hasJsonFlag(), print: printLine, resolveProjectSlug });
5024
+ return;
5025
+ }
4999
5026
  if (command === 'optimizations' && subcommand === 'start') {
5000
5027
  await startOptimizationRun();
5001
5028
  return;
@@ -5009,7 +5036,7 @@ export async function main(rawArgs = process.argv.slice(2)) {
5009
5036
  return;
5010
5037
  }
5011
5038
  if (command === 'optimizations' && subcommand === 'list') {
5012
- await listOptimizationRuns();
5039
+ await listOptimizationRunsCommand(cliArgs.slice(2), { json: hasJsonFlag(), print: printLine, resolveProjectSlug });
5013
5040
  return;
5014
5041
  }
5015
5042
  if (command === 'optimizations' &&
@@ -8,6 +8,16 @@
8
8
  * (killed sessions stay ended). Human ADMIN only: the server rejects agent
9
9
  * bearers and non-admins. Pure command logic with an injected fetcher so the
10
10
  * entry point owns argument parsing and human/JSON output.
11
+ *
12
+ * Containment (ALI-1166): the guarantee is session-end + per-request
13
+ * verification + token-row revocation at engage — a bearer bound to an ended
14
+ * session can never verify again, and ended sessions are terminal
15
+ * (DB-enforced; no post-kill resurrection).
16
+ * `tokensRevoked` in the summary counts the persisted agent token rows the
17
+ * engage revoked (trigger cascade + belt, one transaction); it may include
18
+ * expired or already-ended-session stragglers that were never live bearers at
19
+ * kill time. `--json` output is the server's summary passed through, including
20
+ * `containment: "session-end"`.
11
21
  */
12
22
  import { authedFetch } from './http.js';
13
23
  async function responseMessage(response) {