orizu 0.5.14 → 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.
- package/dist/artifact-archive.js +20 -4
- package/dist/egress-allowlist-cli.js +72 -10
- package/dist/github-setup.js +60 -6
- package/dist/help.js +26 -7
- package/dist/hosted-runtime-assets.js +8 -1
- package/dist/hosted-session-cli.js +197 -22
- package/dist/index.js +108 -68
- package/dist/kill-switch-cli.js +10 -0
- package/dist/merge-job.js +26 -3
- package/dist/optimizations-list-cli.js +85 -0
- package/dist/option-validation.js +54 -0
- package/dist/preview-runtime.js +93 -0
- package/dist/prompt-draft-push.js +3 -2
- package/dist/registration-tag-warning.js +49 -0
- package/dist/run-event-digest.js +319 -0
- package/dist/runner-dir-verify.js +848 -0
- package/dist/scorer-draft-push.js +3 -2
- package/dist/scores-accept-cli.js +87 -0
- package/dist/workbench-cli.js +35 -7
- package/dist/zip-draft-push.js +3 -2
- package/package.json +1 -1
- package/src/artifact-archive.ts +29 -4
- package/src/egress-allowlist-cli.ts +87 -13
- package/src/github-setup.ts +66 -7
- package/src/help.ts +26 -7
- package/src/hosted-runtime-assets.ts +14 -1
- package/src/hosted-session-cli.ts +215 -22
- package/src/index.ts +114 -80
- package/src/kill-switch-cli.ts +10 -0
- package/src/merge-job.ts +25 -6
- package/src/optimizations-list-cli.ts +117 -0
- package/src/option-validation.ts +63 -0
- package/src/preview-runtime.ts +94 -0
- package/src/prompt-draft-push.ts +5 -2
- package/src/registration-tag-warning.ts +58 -0
- package/src/run-event-digest.ts +333 -0
- package/src/runner-dir-verify.ts +1029 -0
- package/src/scorer-draft-push.ts +5 -2
- package/src/scores-accept-cli.ts +124 -0
- package/src/workbench-cli.ts +38 -7
- package/src/zip-draft-push.ts +5 -2
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +80 -3
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +288 -15
- package/vendor/orizu-gepa-python/src/orizu_gepa/runner.py +133 -8
- package/vendor/skills/orizu-cli/SKILL.md +4 -2
- package/vendor/skills/orizu-cli/references/building-apps.md +131 -1
- package/vendor/skills/orizu-cli/references/building-judges.md +18 -0
- package/vendor/skills/orizu-cli/references/cli-reference.md +2 -1
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +3 -1
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +75 -0
- package/vendor/skills/orizu-cli/scripts/test-app.mjs +3 -0
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* is injectable, so the whole flow runs in-process against fakes in the
|
|
35
35
|
* local-sim end-to-end test.
|
|
36
36
|
*/
|
|
37
|
+
import { readFileSync } from 'fs';
|
|
37
38
|
import { createLocalSimProvider, } from './sandbox-provider.js';
|
|
38
39
|
import { bootstrapHostedSandbox, createBootstrapRunEventSink, } from './hosted-bootstrap.js';
|
|
39
40
|
import { resumeRunEventSink } from './hosted-run-event-sink.js';
|
|
@@ -45,9 +46,11 @@ import { hostedBootCommand } from './hosted-boot.js';
|
|
|
45
46
|
import { mergeJobCommand } from './merge-job.js';
|
|
46
47
|
import { authedFetch } from './http.js';
|
|
47
48
|
import { resolveBaseUrl } from './http.js';
|
|
49
|
+
import { findUnknownOption } from './option-validation.js';
|
|
48
50
|
import { getWorkspaceRoot } from './workspace.js';
|
|
49
51
|
import { attachedWorkspaceId } from './workspace-sync.js';
|
|
50
52
|
import { tailWorkbenchRun } from './workbench-cli.js';
|
|
53
|
+
import { formatRunEventDigest, formatRunEventTypeLine } from './run-event-digest.js';
|
|
51
54
|
// Default hosted model — SINGLE source of truth in hosted-loop-lifecycle.ts
|
|
52
55
|
// (ALI-1086); re-exported here so existing importers keep working.
|
|
53
56
|
export { DEFAULT_HOSTED_MODEL };
|
|
@@ -62,6 +65,13 @@ export const DEFAULT_AGENT_TOKEN_TTL_MINUTES = 60;
|
|
|
62
65
|
* before declaring the detached launch a failure. */
|
|
63
66
|
export const DEFAULT_READINESS_TIMEOUT_MS = 30 * 1000;
|
|
64
67
|
export const DEFAULT_READINESS_POLL_MS = 500;
|
|
68
|
+
/** Must match the coordinator Worker's session-start request ceiling. */
|
|
69
|
+
export const MAX_HOSTED_START_BODY_BYTES = 128 * 1024;
|
|
70
|
+
/** `workspace_sessions.id` and `workbench_runs.id` are PostgreSQL UUIDs. The
|
|
71
|
+
* server creates both before its coordinator poke, and the Worker injects the
|
|
72
|
+
* session id into the final DO body. A representative UUID makes the local
|
|
73
|
+
* projection byte-exact without creating either server-side row. */
|
|
74
|
+
const GENERATED_HOSTED_UUID_PLACEHOLDER = '00000000-0000-4000-8000-000000000000';
|
|
65
75
|
/** Non-secret placeholder OpenCode uses to form requests; the firewall proxy
|
|
66
76
|
* overrides the real Anthropic header (model-key brokering). NEVER a real key. */
|
|
67
77
|
export const ANTHROPIC_DUMMY_KEY = 'sk-ant-orizu-proxy-broker-placeholder';
|
|
@@ -590,6 +600,10 @@ export async function startHostedSession(opts) {
|
|
|
590
600
|
return base;
|
|
591
601
|
}
|
|
592
602
|
function argVal(args, flag) {
|
|
603
|
+
const inlinePrefix = `${flag}=`;
|
|
604
|
+
const inlineArg = args.find(arg => arg.startsWith(inlinePrefix));
|
|
605
|
+
if (inlineArg)
|
|
606
|
+
return inlineArg.slice(inlinePrefix.length);
|
|
593
607
|
const index = args.indexOf(flag);
|
|
594
608
|
if (index === -1 || index + 1 >= args.length)
|
|
595
609
|
return null;
|
|
@@ -597,7 +611,7 @@ function argVal(args, flag) {
|
|
|
597
611
|
return value.startsWith('--') ? null : value;
|
|
598
612
|
}
|
|
599
613
|
function hasFlag(args, flag) {
|
|
600
|
-
return args.
|
|
614
|
+
return args.some(arg => arg === flag || arg.startsWith(`${flag}=`));
|
|
601
615
|
}
|
|
602
616
|
function numFlag(args, flag) {
|
|
603
617
|
const raw = argVal(args, flag);
|
|
@@ -606,9 +620,11 @@ function numFlag(args, flag) {
|
|
|
606
620
|
const parsed = Number(raw);
|
|
607
621
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
608
622
|
}
|
|
609
|
-
const HOSTED_USAGE = 'Usage: orizu session start --hosted --task "<prompt>" [--duration <min> (default 60, max 1440)] ' +
|
|
623
|
+
const HOSTED_USAGE = 'Usage: orizu session start --hosted (--task "<prompt>" | --task-file <path>) [--duration <min> (default 60, max 1440)] ' +
|
|
610
624
|
'[--model <provider/model>] [--reasoning-effort <level>] [--project <team/project>] ' +
|
|
611
|
-
'[--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' +
|
|
612
628
|
'Default: the Orizu server provisions the sandbox (no VERCEL_* or model key needed on your machine); ' +
|
|
613
629
|
'the session coordinator owns its lifetime (ALI-1055).\n' +
|
|
614
630
|
'DEPRECATED escape hatch: --operator provisions from THIS machine (requires VERCEL_* + ANTHROPIC_API_KEY ' +
|
|
@@ -616,6 +632,41 @@ const HOSTED_USAGE = 'Usage: orizu session start --hosted --task "<prompt>" [--d
|
|
|
616
632
|
'Removal is planned after DO-coordinator validation (ALI-1055).\n' +
|
|
617
633
|
'Note (v0, --operator only): if you disconnect, the agent token expires within its TTL (<=60m) and the run ' +
|
|
618
634
|
'finishes cleanly; reconnect with `orizu run tail --run <id>`. The sandbox timeout is independent (up to 24h).';
|
|
635
|
+
const HOSTED_START_OPTIONS = new Set([
|
|
636
|
+
'--hosted',
|
|
637
|
+
'--task',
|
|
638
|
+
'--task-file',
|
|
639
|
+
'--workspace',
|
|
640
|
+
'--duration',
|
|
641
|
+
'--model',
|
|
642
|
+
'--reasoning-effort',
|
|
643
|
+
'--project',
|
|
644
|
+
'--title',
|
|
645
|
+
'--tail',
|
|
646
|
+
'--quiet',
|
|
647
|
+
'--operator',
|
|
648
|
+
'--provider',
|
|
649
|
+
'--runtime',
|
|
650
|
+
'--image',
|
|
651
|
+
'--snapshot',
|
|
652
|
+
'--model-key',
|
|
653
|
+
'--json',
|
|
654
|
+
]);
|
|
655
|
+
const HOSTED_START_VALUE_OPTIONS = new Set([
|
|
656
|
+
'--task',
|
|
657
|
+
'--task-file',
|
|
658
|
+
'--workspace',
|
|
659
|
+
'--duration',
|
|
660
|
+
'--model',
|
|
661
|
+
'--reasoning-effort',
|
|
662
|
+
'--project',
|
|
663
|
+
'--title',
|
|
664
|
+
'--provider',
|
|
665
|
+
'--runtime',
|
|
666
|
+
'--image',
|
|
667
|
+
'--snapshot',
|
|
668
|
+
'--model-key',
|
|
669
|
+
]);
|
|
619
670
|
/** Printed on EVERY `--operator` use (founder-locked: deprecate NOW, remove
|
|
620
671
|
* after Phase 6 validation). Loud on purpose. */
|
|
621
672
|
export const OPERATOR_DEPRECATION_WARNING = '! DEPRECATED: --operator (operator-provisioned hosted sessions) is deprecated; ' +
|
|
@@ -626,29 +677,110 @@ export const OPERATOR_DEPRECATION_WARNING = '! DEPRECATED: --operator (operator-
|
|
|
626
677
|
* route and let the Durable Object coordinator own the sandbox lifetime. The
|
|
627
678
|
* CLI never reads VERCEL_* or a model key on this path — the server holds them.
|
|
628
679
|
*/
|
|
629
|
-
|
|
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))) {
|
|
698
|
+
const durationMinutes = numFlag(args, '--duration');
|
|
699
|
+
const model = argVal(args, '--model') ?? undefined;
|
|
700
|
+
const reasoningEffort = argVal(args, '--reasoning-effort') ?? undefined;
|
|
701
|
+
const projectSlug = argVal(args, '--project') ?? undefined;
|
|
702
|
+
const title = argVal(args, '--title') ?? undefined;
|
|
630
703
|
const payload = {
|
|
631
704
|
workspaceId,
|
|
632
705
|
task,
|
|
633
|
-
durationMinutes
|
|
634
|
-
model
|
|
635
|
-
reasoningEffort
|
|
636
|
-
projectSlug
|
|
637
|
-
title
|
|
706
|
+
durationMinutes,
|
|
707
|
+
model,
|
|
708
|
+
reasoningEffort,
|
|
709
|
+
projectSlug,
|
|
710
|
+
title,
|
|
638
711
|
};
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
712
|
+
const serializedPayload = JSON.stringify(payload);
|
|
713
|
+
const payloadBytes = new TextEncoder().encode(serializedPayload).byteLength;
|
|
714
|
+
if (payloadBytes > MAX_HOSTED_START_BODY_BYTES) {
|
|
715
|
+
const message = `Hosted task prompt is too large: request body is ${payloadBytes} bytes; ` +
|
|
716
|
+
`maximum is ${MAX_HOSTED_START_BODY_BYTES} bytes (128 KiB).`;
|
|
717
|
+
if (io.json)
|
|
718
|
+
io.print(JSON.stringify({ ok: false, error: message }));
|
|
719
|
+
else
|
|
720
|
+
io.printErr?.(message);
|
|
721
|
+
return 1;
|
|
646
722
|
}
|
|
647
|
-
|
|
648
|
-
|
|
723
|
+
// Mirror the exact JSON shape that reaches the session Durable Object after
|
|
724
|
+
// the API creates its UUID run/session rows, normalizes optional strings, and
|
|
725
|
+
// the Worker injects sessionId. This second guard keeps near-boundary prompts
|
|
726
|
+
// from triggering server-side state that would only be compensated after a
|
|
727
|
+
// coordinator 413. Keep this field list in sync with pokeCoordinatorStart in
|
|
728
|
+
// app/api/cli/hosted-sessions/route.ts.
|
|
729
|
+
const normalizedModel = model?.trim() || undefined;
|
|
730
|
+
const normalizedReasoningEffort = reasoningEffort?.trim() || undefined;
|
|
731
|
+
const coordinatorPayload = JSON.stringify({
|
|
732
|
+
runId: GENERATED_HOSTED_UUID_PLACEHOLDER,
|
|
733
|
+
task,
|
|
734
|
+
durationMinutes: durationMinutes !== undefined && durationMinutes > 0
|
|
735
|
+
? durationMinutes
|
|
736
|
+
: undefined,
|
|
737
|
+
model: normalizedModel,
|
|
738
|
+
reasoningEffort: normalizedReasoningEffort,
|
|
739
|
+
sessionId: GENERATED_HOSTED_UUID_PLACEHOLDER,
|
|
740
|
+
});
|
|
741
|
+
const coordinatorPayloadBytes = new TextEncoder().encode(coordinatorPayload).byteLength;
|
|
742
|
+
if (coordinatorPayloadBytes > MAX_HOSTED_START_BODY_BYTES) {
|
|
743
|
+
const message = `Hosted task prompt is too large: coordinator request body would be ${coordinatorPayloadBytes} bytes ` +
|
|
744
|
+
`after generated run/session IDs; maximum is ${MAX_HOSTED_START_BODY_BYTES} bytes (128 KiB).`;
|
|
745
|
+
if (io.json)
|
|
746
|
+
io.print(JSON.stringify({ ok: false, error: message }));
|
|
747
|
+
else
|
|
748
|
+
io.printErr?.(message);
|
|
649
749
|
return 1;
|
|
650
750
|
}
|
|
651
|
-
|
|
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.
|
|
754
|
+
let response;
|
|
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;
|
|
783
|
+
}
|
|
652
784
|
const errorMessage = typeof body.error === 'string' ? body.error : `${response.status}`;
|
|
653
785
|
if (response.status === 503) {
|
|
654
786
|
io.printErr?.(errorMessage);
|
|
@@ -673,12 +805,20 @@ export async function startHostedViaServer(args, workspaceId, task, tail, io, fe
|
|
|
673
805
|
io.print(`stream it: orizu run tail --run ${runId}`);
|
|
674
806
|
}
|
|
675
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');
|
|
676
814
|
await tailWorkbenchRun({
|
|
677
815
|
fetcher,
|
|
678
816
|
runId,
|
|
679
817
|
onEvent: event => io.print(io.json
|
|
680
818
|
? JSON.stringify(event)
|
|
681
|
-
:
|
|
819
|
+
: quiet
|
|
820
|
+
? formatRunEventTypeLine(event)
|
|
821
|
+
: formatRunEventDigest(event)),
|
|
682
822
|
});
|
|
683
823
|
}
|
|
684
824
|
return 0;
|
|
@@ -709,7 +849,36 @@ export async function hostedCommand(args, io, deps = {}) {
|
|
|
709
849
|
io.printErr?.('Usage: orizu internal <hosted-loop --context <path> | hosted-boot | merge-job>');
|
|
710
850
|
return 1;
|
|
711
851
|
}
|
|
712
|
-
const
|
|
852
|
+
const unknownOption = findUnknownOption(args, HOSTED_START_OPTIONS, HOSTED_START_VALUE_OPTIONS);
|
|
853
|
+
if (unknownOption) {
|
|
854
|
+
const followsTaskFlag = args.some((arg, index) => arg === '--task' &&
|
|
855
|
+
(args[index + 1] === unknownOption || args[index + 1]?.startsWith(`${unknownOption}=`)));
|
|
856
|
+
const taskValueHint = followsTaskFlag
|
|
857
|
+
? '\nTask text beginning with a dash must use --task=<prompt> or --task-file <path>.'
|
|
858
|
+
: '';
|
|
859
|
+
io.printErr?.(`unknown option ${unknownOption}${taskValueHint}\n${HOSTED_USAGE}`);
|
|
860
|
+
return 1;
|
|
861
|
+
}
|
|
862
|
+
const inlineTask = argVal(args, '--task');
|
|
863
|
+
const taskFile = argVal(args, '--task-file');
|
|
864
|
+
if (hasFlag(args, '--task') && hasFlag(args, '--task-file')) {
|
|
865
|
+
io.printErr?.(`Use either --task or --task-file, not both.\n${HOSTED_USAGE}`);
|
|
866
|
+
return 1;
|
|
867
|
+
}
|
|
868
|
+
let task = inlineTask;
|
|
869
|
+
if (taskFile) {
|
|
870
|
+
try {
|
|
871
|
+
task = readFileSync(taskFile, 'utf8');
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
io.printErr?.(`Could not read task file '${taskFile}': ${error instanceof Error ? error.message : String(error)}`);
|
|
875
|
+
return 1;
|
|
876
|
+
}
|
|
877
|
+
if (task.trim().length === 0) {
|
|
878
|
+
io.printErr?.(`Task file '${taskFile}' is empty or contains only whitespace. Add a task prompt and try again.`);
|
|
879
|
+
return 1;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
713
882
|
if (!task) {
|
|
714
883
|
io.printErr?.(HOSTED_USAGE);
|
|
715
884
|
return 1;
|
|
@@ -762,7 +931,13 @@ export async function hostedCommand(args, io, deps = {}) {
|
|
|
762
931
|
// fail-fast in startHostedSession throws before any session/sandbox is made.
|
|
763
932
|
modelApiKey: argVal(args, '--model-key') ?? (process.env.ANTHROPIC_API_KEY || undefined),
|
|
764
933
|
logLine: line => io.print(line),
|
|
765
|
-
|
|
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)),
|
|
766
941
|
tailImpl: tail
|
|
767
942
|
? async ({ fetcher, runId, onEvent }) => {
|
|
768
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
|
|
1870
|
-
//
|
|
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
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
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
|
}
|
|
@@ -2718,12 +2728,25 @@ async function resolveSetupTeam(teamSlugArg, noInput, dryRun) {
|
|
|
2718
2728
|
}
|
|
2719
2729
|
return createTeamOnServer(name);
|
|
2720
2730
|
}
|
|
2721
|
-
async function resolveSetupProjects(team) {
|
|
2731
|
+
async function resolveSetupProjects(team, noInput, dryRun) {
|
|
2722
2732
|
const projects = await fetchProjects(team.slug);
|
|
2723
|
-
if (projects.length
|
|
2724
|
-
|
|
2733
|
+
if (projects.length > 0) {
|
|
2734
|
+
return projects;
|
|
2735
|
+
}
|
|
2736
|
+
// ALI-1140: a just-created team (or a pre-existing empty one) has no projects
|
|
2737
|
+
// by definition — continue the wizard into creating the first project instead
|
|
2738
|
+
// of dead-ending on the path setup itself offered.
|
|
2739
|
+
if (noInput || dryRun) {
|
|
2740
|
+
throw new Error(`Team '${team.slug}' has no projects. Create one with \`orizu projects create --name <name> --team ${team.slug}\`, then re-run setup.`);
|
|
2725
2741
|
}
|
|
2726
|
-
|
|
2742
|
+
printLine(` Team '${sanitizeTerminalText(team.slug)}' has no projects yet. Let's create the first one.`);
|
|
2743
|
+
const name = await askText(' Project name?', '');
|
|
2744
|
+
if (!name) {
|
|
2745
|
+
throw new Error('Project name is required to create a project.');
|
|
2746
|
+
}
|
|
2747
|
+
const project = await createProjectOnServer(team.slug, name);
|
|
2748
|
+
printLine(` Created project ${sanitizeTerminalText(`${project.teamSlug}/${project.slug}`)}`);
|
|
2749
|
+
return [project];
|
|
2727
2750
|
}
|
|
2728
2751
|
function projectSeedsFromProjects(projects) {
|
|
2729
2752
|
return projects.map(project => ({
|
|
@@ -2848,7 +2871,7 @@ async function setupCommand() {
|
|
|
2848
2871
|
let projects;
|
|
2849
2872
|
if (auth.state === 'signed-in' && !validateOnly) {
|
|
2850
2873
|
const team = await resolveSetupTeam(teamSlug, noInput, dryRun);
|
|
2851
|
-
const serverProjects = await resolveSetupProjects(team);
|
|
2874
|
+
const serverProjects = await resolveSetupProjects(team, noInput, dryRun);
|
|
2852
2875
|
teamSlug = team.slug;
|
|
2853
2876
|
teamId = team.id;
|
|
2854
2877
|
projects = projectSeedsFromProjects(serverProjects);
|
|
@@ -2866,10 +2889,10 @@ async function setupCommand() {
|
|
|
2866
2889
|
hostedAttached = (await runInteractiveHostedSetup({ teamSlug: team.slug, targetDir: workspaceRoot, local: hasArg('--local'), activeInstallation: link.status === 'active' }, { print: printLine, fetcher: authedFetch })).attached;
|
|
2867
2890
|
}
|
|
2868
2891
|
catch (error) {
|
|
2869
|
-
throw new Error(`Hosted setup failed: ${getErrorMessage(error)}
|
|
2892
|
+
throw new Error(`Hosted setup failed: ${getErrorMessage(error)} Re-running \`orizu setup\` is safe — provisioning adopts the existing workbench repo and resumes where it left off (or pass --local to scaffold a local-only workspace).`);
|
|
2870
2893
|
}
|
|
2871
2894
|
}
|
|
2872
|
-
}
|
|
2895
|
+
} // hint must not blame local state (ALI-1143): provisioning failures are server-side, and both provisioning (adopt) and the clone step (skip-if-cloned, ALI-1069) are idempotent on re-run
|
|
2873
2896
|
}
|
|
2874
2897
|
else if (!teamSlug && !noInput && !validateOnly) {
|
|
2875
2898
|
teamSlug = await askText(' Team slug?', 'local-team');
|
|
@@ -4622,10 +4645,7 @@ async function materializeRunnerVersion(runnerVersionId) {
|
|
|
4622
4645
|
rmSync(tempDir, { recursive: true, force: true });
|
|
4623
4646
|
throw new Error(`unzip failed: ${sanitizeTerminalText(result.stderr || result.stdout || '')}`);
|
|
4624
4647
|
}
|
|
4625
|
-
return {
|
|
4626
|
-
runnerDir,
|
|
4627
|
-
cleanup: () => rmSync(tempDir, { recursive: true, force: true }),
|
|
4628
|
-
};
|
|
4648
|
+
return { runnerDir, cleanup: () => rmSync(tempDir, { recursive: true, force: true }) };
|
|
4629
4649
|
}
|
|
4630
4650
|
async function runnersExec() {
|
|
4631
4651
|
const prompt = getArg('--prompt');
|
|
@@ -4667,12 +4687,28 @@ async function runnersExec() {
|
|
|
4667
4687
|
throw new Error(`Failed to fetch runner execution context: ${await contextResponse.text()}`);
|
|
4668
4688
|
}
|
|
4669
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.
|
|
4670
4697
|
const materializedRunner = runnerDirArg
|
|
4671
|
-
?
|
|
4672
|
-
|
|
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);
|
|
4673
4707
|
const runnerDir = materializedRunner.runnerDir;
|
|
4674
|
-
const manifest = readRunnerManifest(runnerDir);
|
|
4675
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);
|
|
4676
4712
|
if (manifest.supports_body_kind && !manifest.supports_body_kind.includes(context.prompt.bodyKind)) {
|
|
4677
4713
|
throw new Error(`Runner does not support prompt body kind '${context.prompt.bodyKind}'. Supported kinds: ${manifest.supports_body_kind.join(', ')}`);
|
|
4678
4714
|
}
|
|
@@ -4983,6 +5019,10 @@ export async function main(rawArgs = process.argv.slice(2)) {
|
|
|
4983
5019
|
await submitScoreResults();
|
|
4984
5020
|
return;
|
|
4985
5021
|
}
|
|
5022
|
+
if (command === 'scores' && subcommand === 'accept') {
|
|
5023
|
+
await acceptScoreRunCommand(cliArgs.slice(2), { json: hasJsonFlag(), print: printLine, resolveProjectSlug });
|
|
5024
|
+
return;
|
|
5025
|
+
}
|
|
4986
5026
|
if (command === 'optimizations' && subcommand === 'start') {
|
|
4987
5027
|
await startOptimizationRun();
|
|
4988
5028
|
return;
|
|
@@ -4996,7 +5036,7 @@ export async function main(rawArgs = process.argv.slice(2)) {
|
|
|
4996
5036
|
return;
|
|
4997
5037
|
}
|
|
4998
5038
|
if (command === 'optimizations' && subcommand === 'list') {
|
|
4999
|
-
await
|
|
5039
|
+
await listOptimizationRunsCommand(cliArgs.slice(2), { json: hasJsonFlag(), print: printLine, resolveProjectSlug });
|
|
5000
5040
|
return;
|
|
5001
5041
|
}
|
|
5002
5042
|
if (command === 'optimizations' &&
|
package/dist/kill-switch-cli.js
CHANGED
|
@@ -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) {
|
package/dist/merge-job.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
* (plus the shape-pattern net). Nothing here ever logs a secret.
|
|
40
40
|
*/
|
|
41
41
|
import { mergeBranchWithDeployKey, } from './deploy-key-merge-core.js';
|
|
42
|
+
import { assertSecureTokenTransport } from './http.js';
|
|
42
43
|
import { redactSecrets } from './secret-redaction.js';
|
|
43
44
|
// -- Frozen env contract --------------------------------------------------------
|
|
44
45
|
export const REQUIRED_MERGE_JOB_ENV_VARS = [
|
|
@@ -60,7 +61,27 @@ export function resolveMergeJobEnv(env) {
|
|
|
60
61
|
const coordinatorUrl = req('ORIZU_COORDINATOR_URL');
|
|
61
62
|
const baseUrl = req('ORIZU_BASE_URL');
|
|
62
63
|
if (missing.length > 0)
|
|
63
|
-
return { ok: false, missing };
|
|
64
|
+
return { ok: false, missing, invalid: [] };
|
|
65
|
+
const origins = [
|
|
66
|
+
['ORIZU_COORDINATOR_URL', coordinatorUrl],
|
|
67
|
+
['ORIZU_BASE_URL', baseUrl],
|
|
68
|
+
];
|
|
69
|
+
const invalid = origins.flatMap(([name, value]) => {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = new URL(value);
|
|
72
|
+
// Userinfo is never accepted, including on loopback: rejecting it at the
|
|
73
|
+
// env boundary keeps configured credentials out of every retry/error path.
|
|
74
|
+
if (parsed.username || parsed.password)
|
|
75
|
+
return [name];
|
|
76
|
+
assertSecureTokenTransport(value);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [name];
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
if (invalid.length > 0)
|
|
84
|
+
return { ok: false, missing: [], invalid };
|
|
64
85
|
return {
|
|
65
86
|
ok: true,
|
|
66
87
|
value: {
|
|
@@ -312,8 +333,10 @@ export async function runMergeJob(opts) {
|
|
|
312
333
|
export async function mergeJobCommand(io) {
|
|
313
334
|
const resolved = resolveMergeJobEnv(process.env);
|
|
314
335
|
if (!resolved.ok) {
|
|
315
|
-
|
|
316
|
-
|
|
336
|
+
const detail = resolved.missing.length > 0
|
|
337
|
+
? `missing required env: ${resolved.missing.join(', ')}`
|
|
338
|
+
: `invalid or insecure origin env: ${resolved.invalid.join(', ')}`;
|
|
339
|
+
io.printErr?.(`merge-job: ${detail} (the MergeJobCoordinator sandbox env contract)`);
|
|
317
340
|
return 1;
|
|
318
341
|
}
|
|
319
342
|
const log = (line) => io.printErr?.(`[merge-job] ${line}`);
|