orizu 0.5.43 → 0.5.45
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/hosted-agent-prompt.d.ts +6 -0
- package/dist/hosted-agent-prompt.d.ts.map +1 -1
- package/dist/hosted-agent-prompt.js +11 -0
- package/dist/hosted-boot.d.ts +3 -0
- package/dist/hosted-boot.d.ts.map +1 -1
- package/dist/hosted-boot.js +14 -0
- package/dist/hosted-bootstrap-rehearsal.d.ts +2 -0
- package/dist/hosted-bootstrap-rehearsal.d.ts.map +1 -1
- package/dist/hosted-bootstrap-rehearsal.js +5 -0
- package/dist/hosted-broker-server.d.ts +2 -0
- package/dist/hosted-broker-server.d.ts.map +1 -1
- package/dist/hosted-broker-server.js +4 -1
- package/dist/hosted-harness-opencode.d.ts +2 -0
- package/dist/hosted-harness-opencode.d.ts.map +1 -1
- package/dist/hosted-harness-opencode.js +2 -2
- package/dist/hosted-loop-lifecycle.d.ts +2 -0
- package/dist/hosted-loop-lifecycle.d.ts.map +1 -1
- package/dist/hosted-loop.d.ts.map +1 -1
- package/dist/hosted-loop.js +3 -0
- package/dist/hosted-project-folder-seed.d.ts +18 -0
- package/dist/hosted-project-folder-seed.d.ts.map +1 -0
- package/dist/hosted-project-folder-seed.js +78 -0
- package/dist/hosted-session-cli.d.ts.map +1 -1
- package/dist/hosted-session-cli.js +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/score-save-workflow.d.ts +10 -14
- package/dist/score-save-workflow.d.ts.map +1 -1
- package/dist/score-save-workflow.js +14 -11
- package/dist/secret-redaction.d.ts +14 -0
- package/dist/secret-redaction.d.ts.map +1 -1
- package/dist/secret-redaction.js +25 -0
- package/dist/skilled-proposer-launch.d.ts +5 -0
- package/dist/skilled-proposer-launch.d.ts.map +1 -1
- package/dist/skilled-proposer-launch.js +104 -46
- package/package.json +1 -1
- package/src/hosted-agent-prompt.ts +11 -0
- package/src/hosted-boot.ts +18 -0
- package/src/hosted-bootstrap-rehearsal.ts +6 -0
- package/src/hosted-broker-server.ts +5 -1
- package/src/hosted-harness-opencode.ts +4 -2
- package/src/hosted-loop-lifecycle.ts +2 -0
- package/src/hosted-loop.ts +2 -0
- package/src/hosted-project-folder-seed.ts +95 -0
- package/src/hosted-session-cli.ts +3 -1
- package/src/index.ts +9 -11
- package/src/score-save-workflow.ts +21 -16
- package/src/secret-redaction.ts +30 -0
- package/src/skilled-proposer-launch.ts +78 -36
- package/vendor/orizu-gepa/src/orizu_gepa_connector/runtime.py +123 -0
|
@@ -4,16 +4,19 @@ import { datasetTransferTimeoutMs } from './dataset-transfer-policy.js';
|
|
|
4
4
|
import { extractErrorMessage } from './error-response.js';
|
|
5
5
|
import { appErrorOf, unreachedCode } from './request-outcome.js';
|
|
6
6
|
// Loaded only through `await import(...)`, like the dataset workflows.
|
|
7
|
+
const SavedScore = Schema.Struct({ scoreRun: Schema.Struct({ id: Schema.NonEmptyString }) });
|
|
8
|
+
const SavedRun = Schema.Struct({ run: Schema.Struct({ id: Schema.NonEmptyString }) });
|
|
9
|
+
// `serverMayBeSaved` is what the route says when it cannot tell whether the run was saved.
|
|
7
10
|
const COMMANDS = {
|
|
8
|
-
submit: { label: 'Score submit', failed: 'Failed to submit score' },
|
|
9
|
-
exec: { label: 'Scorer exec', failed: 'Failed to execute scorer' },
|
|
11
|
+
submit: { label: 'Score submit', failed: 'Failed to submit score', serverMayBeSaved: 'Score run may already be saved', saved: SavedScore },
|
|
12
|
+
exec: { label: 'Scorer exec', failed: 'Failed to execute scorer', serverMayBeSaved: 'Score run may already be saved', saved: SavedScore },
|
|
13
|
+
run: { label: 'Run submit', failed: 'Failed to submit run', serverMayBeSaved: 'Run may already be saved', saved: SavedRun },
|
|
10
14
|
};
|
|
11
|
-
// What the server says when it cannot tell whether the run was saved.
|
|
12
|
-
const SERVER_MAY_BE_SAVED = 'Score run may already be saved';
|
|
13
15
|
// Node's fetch publishes this once a request's whole body has been written.
|
|
14
16
|
const BODY_SENT = 'undici:request:bodySent';
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
+
const mayBeSaved = (save) => save.command === 'run'
|
|
18
|
+
? `The run may already be saved: run \`orizu runs list --project ${save.project} --prompt-version ${save.promptVersionId}\` and check its newest runs before running this again.`
|
|
19
|
+
: `The score may already be saved: run \`orizu scorers detail ${save.scorerVersionId} --project ${save.project}\` and check its newest score runs before running this again.`;
|
|
17
20
|
// The run may be saved when the server says so, when the reply was lost, late
|
|
18
21
|
// or unreadable after the request was sent, when a 5xx did not come from the
|
|
19
22
|
// app (a gateway giving up while the app kept working), or when the reply has
|
|
@@ -30,7 +33,7 @@ class ScoreSaveFailure extends Error {
|
|
|
30
33
|
// second run. The deadline starts once the whole body has been sent, so a
|
|
31
34
|
// large upload on a slow link takes as long as it did before.
|
|
32
35
|
function scoreSave(save, request, timeoutMs) {
|
|
33
|
-
const { label, failed } = COMMANDS[save.command];
|
|
36
|
+
const { label, failed, serverMayBeSaved, saved } = COMMANDS[save.command];
|
|
34
37
|
return Effect.suspend(() => {
|
|
35
38
|
let sent = false;
|
|
36
39
|
let timedOut = false;
|
|
@@ -62,7 +65,7 @@ function scoreSave(save, request, timeoutMs) {
|
|
|
62
65
|
if (!response.ok) {
|
|
63
66
|
const appError = await appErrorOf(response.clone());
|
|
64
67
|
const detail = await extractErrorMessage(response);
|
|
65
|
-
if (appError ===
|
|
68
|
+
if (appError === serverMayBeSaved)
|
|
66
69
|
throw new ScoreSaveFailure(appError, true);
|
|
67
70
|
if (response.status < 400 || (response.status >= 500 && appError === null)) {
|
|
68
71
|
throw new ScoreSaveFailure(`${label} got no clear answer (status ${response.status}).`, true);
|
|
@@ -79,7 +82,7 @@ function scoreSave(save, request, timeoutMs) {
|
|
|
79
82
|
data = undefined;
|
|
80
83
|
}
|
|
81
84
|
const usable = save.saves
|
|
82
|
-
? Result.isSuccess(Schema.decodeUnknownResult(
|
|
85
|
+
? Result.isSuccess(Schema.decodeUnknownResult(saved)(data))
|
|
83
86
|
: typeof data === 'object' && data !== null && !Array.isArray(data);
|
|
84
87
|
if (!usable) {
|
|
85
88
|
throw new ScoreSaveFailure(`${label} returned an unexpected response (status ${response.status}).`, true);
|
|
@@ -111,9 +114,9 @@ function scoreSave(save, request, timeoutMs) {
|
|
|
111
114
|
});
|
|
112
115
|
});
|
|
113
116
|
}
|
|
114
|
-
/** `orizu scores submit` and `orizu
|
|
117
|
+
/** `orizu scores submit`, `orizu scorers exec` and `orizu runs submit`: the reply, or an error saying whether the save may have happened. */
|
|
115
118
|
export async function sendScoreSave(save, request) {
|
|
116
|
-
const exit = await Effect.runPromiseExit(scoreSave(save, request, datasetTransferTimeoutMs()).pipe(Effect.mapError(failure => new Error(failure.mayBeSaved && save.saves ? `${failure.message}\n${
|
|
119
|
+
const exit = await Effect.runPromiseExit(scoreSave(save, request, datasetTransferTimeoutMs()).pipe(Effect.mapError(failure => new Error(failure.mayBeSaved && save.saves ? `${failure.message}\n${mayBeSaved(save)}` : failure.message))));
|
|
117
120
|
if (Exit.isSuccess(exit))
|
|
118
121
|
return exit.value;
|
|
119
122
|
throw Cause.squash(exit.cause);
|
|
@@ -28,6 +28,20 @@
|
|
|
28
28
|
* event payload can be passed through before it is sent to the RunAPI.
|
|
29
29
|
*/
|
|
30
30
|
export declare const REDACTION_PLACEHOLDER = "[redacted]";
|
|
31
|
+
/**
|
|
32
|
+
* The shape pass alone, for servers that hold no secret values: the web app
|
|
33
|
+
* and the session coordinator scrub agent text with it before it reaches a
|
|
34
|
+
* browser (ORI-1905). One list of shapes for the CLI and both servers.
|
|
35
|
+
*/
|
|
36
|
+
export declare function redactSecretShapes(input: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* For reply text shown as it is written: each snapshot is the reply so far, so
|
|
39
|
+
* one can end part-way through a token, before it is long enough for its shape
|
|
40
|
+
* to match. Such a tail is redacted too. The saved read shows a reply's last
|
|
41
|
+
* snapshot, so a finished reply that really ends in a bare `sk-` loses those
|
|
42
|
+
* characters there as well.
|
|
43
|
+
*/
|
|
44
|
+
export declare function redactStreamedTextShapes(input: string): string;
|
|
31
45
|
export interface RedactOptions {
|
|
32
46
|
/** Verbatim secret values to strip wherever they appear (bearer, tokens). */
|
|
33
47
|
secrets?: readonly string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-redaction.d.ts","sourceRoot":"","sources":["../src/secret-redaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,eAAO,MAAM,qBAAqB,eAAe,CAAA;AAgDjD,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,GAAE,aAAkB,GAAG,CAAC,CAGzE;AAmBD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAQjF"}
|
|
1
|
+
{"version":3,"file":"secret-redaction.d.ts","sourceRoot":"","sources":["../src/secret-redaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,eAAO,MAAM,qBAAqB,eAAe,CAAA;AAgDjD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAExD;AAUD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI9D;AAED,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,GAAE,aAAkB,GAAG,CAAC,CAGzE;AAmBD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAQjF"}
|
package/dist/secret-redaction.js
CHANGED
|
@@ -70,6 +70,31 @@ function redactString(input, secrets) {
|
|
|
70
70
|
}
|
|
71
71
|
return out;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The shape pass alone, for servers that hold no secret values: the web app
|
|
75
|
+
* and the session coordinator scrub agent text with it before it reaches a
|
|
76
|
+
* browser (ORI-1905). One list of shapes for the CLI and both servers.
|
|
77
|
+
*/
|
|
78
|
+
export function redactSecretShapes(input) {
|
|
79
|
+
return redactString(input, []);
|
|
80
|
+
}
|
|
81
|
+
// A known credential prefix at the end of the text, followed by fewer
|
|
82
|
+
// characters than its shape above needs: the start of a token still being
|
|
83
|
+
// written. Each run is bounded, so the scan stays linear in the text, and a
|
|
84
|
+
// longer run that is not a token (a path, a word) is left alone. `Bearer` only
|
|
85
|
+
// counts in an Authorization header, so prose like "a Bearer token" keeps its
|
|
86
|
+
// last word.
|
|
87
|
+
const OPEN_TOKEN_AT_END = /(?:\bgithub_pat_[A-Za-z0-9_]{0,19}|\bgh[posur]_[A-Za-z0-9_]{0,19}|\borizu_(?:agent|pat)_[A-Za-z0-9_-]{0,9}|\bsk-[A-Za-z0-9_-]{0,19}|\b(authorization:\s*bearer)\s+[A-Za-z0-9._~+/-]{0,15})$/i;
|
|
88
|
+
/**
|
|
89
|
+
* For reply text shown as it is written: each snapshot is the reply so far, so
|
|
90
|
+
* one can end part-way through a token, before it is long enough for its shape
|
|
91
|
+
* to match. Such a tail is redacted too. The saved read shows a reply's last
|
|
92
|
+
* snapshot, so a finished reply that really ends in a bare `sk-` loses those
|
|
93
|
+
* characters there as well.
|
|
94
|
+
*/
|
|
95
|
+
export function redactStreamedTextShapes(input) {
|
|
96
|
+
return redactSecretShapes(input).replace(OPEN_TOKEN_AT_END, (_match, header) => header ? `${header} ${REDACTION_PLACEHOLDER}` : REDACTION_PLACEHOLDER);
|
|
97
|
+
}
|
|
73
98
|
/**
|
|
74
99
|
* Deep-redact a JSON-ish value. Strings are scrubbed; arrays/objects are walked;
|
|
75
100
|
* everything else is returned as-is. Object KEYS are never treated as secrets
|
|
@@ -23,6 +23,11 @@ export interface SkilledProposerChildOutput {
|
|
|
23
23
|
stdout: (chunk: string) => void;
|
|
24
24
|
stderr: (chunk: string) => void;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Run the optimizer, passing a stop signal on to it. On a stop signal the CLI
|
|
28
|
+
* waits until the optimizer exits (capped at CHILD_STOP_WAIT_MS, then kills
|
|
29
|
+
* it) and then ends by that same signal.
|
|
30
|
+
*/
|
|
26
31
|
export declare function spawnSkilledProposerChild(command: string, args: string[], engine: GepaEngine, environment: NodeJS.ProcessEnv, onSpawn?: () => Promise<void>, output?: SkilledProposerChildOutput): Promise<{
|
|
27
32
|
status: number | null;
|
|
28
33
|
error?: Error;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skilled-proposer-launch.d.ts","sourceRoot":"","sources":["../src/skilled-proposer-launch.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAO3D,UAAU,yBAAyB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,iBAAiB,EAAE,MAAM,EAAE,CAAA;IAC3B,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAClD,oBAAoB,EAAE,OAAO,CAAA;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IAC9B,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CAClC;AACD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAC,UAAU,CAAA;IAC9B,MAAM,CAAC,EAAE,yBAAyB,CAAA;CACnC;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;
|
|
1
|
+
{"version":3,"file":"skilled-proposer-launch.d.ts","sourceRoot":"","sources":["../src/skilled-proposer-launch.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAO3D,UAAU,yBAAyB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,iBAAiB,EAAE,MAAM,EAAE,CAAA;IAC3B,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAClD,oBAAoB,EAAE,OAAO,CAAA;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;IAC9B,eAAe,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CAClC;AACD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAC,UAAU,CAAA;IAC9B,MAAM,CAAC,EAAE,yBAAyB,CAAA;CACnC;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAsBD;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,CAAC,UAAU,EACnF,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAC7B,MAAM,CAAC,EAAE,0BAA0B;YA0CX,MAAM,GAAG,IAAI;YAAU,KAAK;GAkDrD;AAwCD,UAAU,aAAa;IAAG,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE;AAQ9F,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,aAAa,EAAE,MAAM,UAAQ,GAAG,MAAM,CAepF;AAED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,MAAM,CAAC,UAAU,GAC7B,qBAAqB,CAyBvB"}
|
|
@@ -23,70 +23,128 @@ function materializeConfigPayload(engine, environment) {
|
|
|
23
23
|
}
|
|
24
24
|
return () => rmSync(root, { recursive: true, force: true });
|
|
25
25
|
}
|
|
26
|
+
const STOP_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT'];
|
|
27
|
+
// The optimizer marks its run stopped before exiting (at most 10 s for the
|
|
28
|
+
// status write); past this it is killed so nothing is left running (ORI-2402).
|
|
29
|
+
const CHILD_STOP_WAIT_MS = 15_000;
|
|
30
|
+
/**
|
|
31
|
+
* Run the optimizer, passing a stop signal on to it. On a stop signal the CLI
|
|
32
|
+
* waits until the optimizer exits (capped at CHILD_STOP_WAIT_MS, then kills
|
|
33
|
+
* it) and then ends by that same signal.
|
|
34
|
+
*/
|
|
26
35
|
export async function spawnSkilledProposerChild(command, args, engine, environment, onSpawn, output) {
|
|
27
36
|
const childEnvironment = { ...environment };
|
|
28
|
-
const shouldMaterialize = engine === 'official'
|
|
29
|
-
&& childEnvironment.ORIZU_CANDIDATE_PROPOSER === 'skilled-proposer'
|
|
30
|
-
&& childEnvironment.ORIZU_SKILLED_PROPOSER_CONFIG !== undefined;
|
|
31
|
-
if (!shouldMaterialize)
|
|
32
|
-
return spawnSync(command, args, { stdio: 'inherit', env: childEnvironment });
|
|
33
37
|
let child;
|
|
34
38
|
let cleanup = () => { };
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
let stopSignal;
|
|
40
|
+
// On the terminal path the optimizer leads its own process group, so a stop
|
|
41
|
+
// also reaches its runners, which may run in worker threads it can't stop.
|
|
42
|
+
let childGroup;
|
|
43
|
+
const forward = (signal) => {
|
|
44
|
+
if (childGroup) {
|
|
39
45
|
try {
|
|
40
|
-
|
|
46
|
+
process.kill(-childGroup, signal);
|
|
47
|
+
return;
|
|
41
48
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
catch { /* group already gone */ }
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
child?.kill(signal);
|
|
53
|
+
}
|
|
54
|
+
catch { /* already exited */ }
|
|
55
|
+
};
|
|
56
|
+
let giveUp;
|
|
57
|
+
const removeHandlers = () => { for (const entry of handlers)
|
|
58
|
+
process.off(entry.signal, entry.handle); };
|
|
59
|
+
const endBySignal = (signal) => {
|
|
60
|
+
if (giveUp)
|
|
61
|
+
clearTimeout(giveUp);
|
|
62
|
+
removeHandlers();
|
|
63
|
+
cleanup();
|
|
64
|
+
// With our handlers gone, the signal's default action ends the process.
|
|
65
|
+
process.kill(process.pid, signal);
|
|
66
|
+
return new Promise(() => { });
|
|
67
|
+
};
|
|
68
|
+
const handlers = STOP_SIGNALS.map(signal => ({
|
|
69
|
+
signal,
|
|
70
|
+
handle: () => {
|
|
71
|
+
if (!child) {
|
|
72
|
+
void endBySignal(signal);
|
|
73
|
+
return;
|
|
51
74
|
}
|
|
75
|
+
forward(signal);
|
|
76
|
+
if (stopSignal)
|
|
77
|
+
return;
|
|
78
|
+
stopSignal = signal;
|
|
79
|
+
// The child read the private payload at start and a stopping run
|
|
80
|
+
// proposes nothing, so it goes now rather than when the child exits.
|
|
81
|
+
cleanup();
|
|
82
|
+
giveUp = setTimeout(() => forward('SIGKILL'), CHILD_STOP_WAIT_MS);
|
|
52
83
|
},
|
|
53
84
|
}));
|
|
54
85
|
for (const entry of handlers)
|
|
55
|
-
process.
|
|
86
|
+
process.on(entry.signal, entry.handle);
|
|
56
87
|
try {
|
|
57
88
|
cleanup = materializeConfigPayload(engine, childEnvironment) ?? cleanup;
|
|
58
89
|
await new Promise(resolve => setImmediate(resolve));
|
|
90
|
+
let result;
|
|
59
91
|
if (output) {
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
const spawnedChild = spawn(command, args, {
|
|
63
|
-
stdio: 'inherit',
|
|
64
|
-
env: childEnvironment,
|
|
65
|
-
});
|
|
66
|
-
child = spawnedChild;
|
|
67
|
-
const spawned = new Promise((resolve, reject) => {
|
|
68
|
-
spawnedChild.once('spawn', resolve);
|
|
69
|
-
spawnedChild.once('error', reject);
|
|
70
|
-
});
|
|
71
|
-
const closed = new Promise(resolve => {
|
|
72
|
-
let error;
|
|
73
|
-
spawnedChild.once('error', cause => { error = cause; });
|
|
74
|
-
spawnedChild.once('close', status => resolve(error ? { status, error } : { status }));
|
|
75
|
-
});
|
|
76
|
-
await spawned;
|
|
77
|
-
try {
|
|
78
|
-
await onSpawn?.();
|
|
92
|
+
result = await runPipedChild(command, args, childEnvironment, async () => { await onSpawn?.(); }, output, spawnedChild => { child = spawnedChild; });
|
|
79
93
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
else {
|
|
95
|
+
// stdin is not passed on: a process outside the terminal's foreground
|
|
96
|
+
// group that read the terminal would be stopped by it.
|
|
97
|
+
// Outside the CLI's group, the optimizer would outlive a SIGKILL of the
|
|
98
|
+
// CLI; it watches for that and stops itself.
|
|
99
|
+
const spawnedChild = spawn(command, args, {
|
|
100
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
101
|
+
env: { ...childEnvironment, ORIZU_STOP_WITH_PARENT: '1' },
|
|
102
|
+
detached: true,
|
|
103
|
+
});
|
|
104
|
+
child = spawnedChild;
|
|
105
|
+
childGroup = spawnedChild.pid;
|
|
106
|
+
const spawned = new Promise((resolve, reject) => {
|
|
107
|
+
spawnedChild.once('spawn', resolve);
|
|
108
|
+
spawnedChild.once('error', reject);
|
|
109
|
+
});
|
|
110
|
+
const closed = new Promise(resolve => {
|
|
111
|
+
let error;
|
|
112
|
+
spawnedChild.once('error', cause => { error = cause; });
|
|
113
|
+
spawnedChild.once('close', status => resolve(error ? { status, error } : { status }));
|
|
114
|
+
});
|
|
115
|
+
// A launch failure (e.g. ENOENT) is returned, not thrown, so callers
|
|
116
|
+
// name the engine as they did with spawnSync.
|
|
117
|
+
try {
|
|
118
|
+
await spawned;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return { status: null, error: error };
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
await onSpawn?.();
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
spawnedChild.kill('SIGTERM');
|
|
128
|
+
await closed;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
result = await closed;
|
|
132
|
+
// A runner that ignored the stop must not outlive the optimizer.
|
|
133
|
+
if (stopSignal) {
|
|
134
|
+
try {
|
|
135
|
+
process.kill(-spawnedChild.pid, 'SIGKILL');
|
|
136
|
+
}
|
|
137
|
+
catch { /* group already gone */ }
|
|
138
|
+
}
|
|
84
139
|
}
|
|
85
|
-
|
|
140
|
+
if (stopSignal)
|
|
141
|
+
return await endBySignal(stopSignal);
|
|
142
|
+
return result;
|
|
86
143
|
}
|
|
87
144
|
finally {
|
|
88
|
-
|
|
89
|
-
|
|
145
|
+
if (giveUp)
|
|
146
|
+
clearTimeout(giveUp);
|
|
147
|
+
removeHandlers();
|
|
90
148
|
cleanup();
|
|
91
149
|
}
|
|
92
150
|
}
|
package/package.json
CHANGED
|
@@ -19,3 +19,14 @@ export const HOSTED_AGENT_PROMPT = `You are the Orizu agent. You work in the per
|
|
|
19
19
|
- Keep answers short and plain. Say what you did and what the person needs to know next.
|
|
20
20
|
- Follow the repository's own conventions and any instructions in its AGENTS.md.
|
|
21
21
|
- An Orizu app's page link is <Orizu server>${appPagePath('<app id>')}, where <Orizu server> is the server the orizu CLI talks to (https://app.orizu.ai by default). To give someone an app's link, run \`orizu projects list\` to get the \`<team>/<project>\` slug, then \`orizu apps list --project <team>/<project>\`: it prints each app's full link. The --project flag is required here: prompts cannot run in this environment. Never guess a link.`
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The prompt for one session. Every project of an approved team shares the
|
|
25
|
+
* team's repository (ORI-2601; ADR-041, added in PR #2292), which starts with one project's folder, so the
|
|
26
|
+
* agent is told which project this session is for (ORI-2601).
|
|
27
|
+
*/
|
|
28
|
+
export function hostedAgentPrompt(projectId?: string | null): string {
|
|
29
|
+
if (!projectId) return HOSTED_AGENT_PROMPT
|
|
30
|
+
return `${HOSTED_AGENT_PROMPT}
|
|
31
|
+
- This session is for the Orizu project whose id is ${projectId}. Run \`orizu projects list --json\` to find its \`<team>/<project>\` slug, and pass that whole slug as \`--project <team>/<project>\` to orizu commands. In this repository the project's files live in the folder \`projects/<project>/\`, named by the part after the slash only (for \`acme/support-bot\`, the folder is \`projects/support-bot/\`). If that folder is missing, do not create it by hand: Orizu writes a project folder with its own files (\`orizu.project.json\`, \`README.md\`, \`memory.md\`). Tell the person this project's folder is not in the workspace yet, and keep working on what they asked. Do not change another project's folder unless the person asks.`
|
|
32
|
+
}
|
package/src/hosted-boot.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
import { DEFAULT_EGRESS_CANARY_HOST, DEFAULT_HOSTED_MODEL } from './hosted-loop-lifecycle.js'
|
|
61
61
|
import { composeHostedAnswerPrompt } from './hosted-question.js'
|
|
62
62
|
import { resumeRunEventSink } from './hosted-run-event-sink.js'
|
|
63
|
+
import { seedProjectFolder } from './hosted-project-folder-seed.js'
|
|
63
64
|
import { stageOrizuSkill } from './hosted-skill-staging.js'
|
|
64
65
|
import { openHostedLiveChannel, type HostedLiveChannel } from './hosted-live-channel.js'
|
|
65
66
|
|
|
@@ -441,6 +442,9 @@ export interface ResolvedSession {
|
|
|
441
442
|
agentSessionId: string | null
|
|
442
443
|
seedOrigin: 'team' | 'session'
|
|
443
444
|
projectId: string | null
|
|
445
|
+
/** The session's own project's slug and name (ORI-2609), when the server sends them. */
|
|
446
|
+
projectSlug: string | null
|
|
447
|
+
projectName: string | null
|
|
444
448
|
/** Session lifetime in minutes (from client_info), if the coordinator recorded
|
|
445
449
|
* it — used to derive the per-prompt max-duration cap (ALI-1061). */
|
|
446
450
|
durationMinutes: number | null
|
|
@@ -547,6 +551,8 @@ export async function resolveSession(opts: {
|
|
|
547
551
|
// A server with copying switched off omits seedOrigin; no source is bound.
|
|
548
552
|
seedOrigin: session.seedOrigin === 'session' ? 'session' : 'team',
|
|
549
553
|
projectId: asString(session.projectId),
|
|
554
|
+
projectSlug: asString(session.projectSlug),
|
|
555
|
+
projectName: asString(session.projectName),
|
|
550
556
|
}
|
|
551
557
|
}
|
|
552
558
|
|
|
@@ -1342,6 +1348,18 @@ const c = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
|
1342
1348
|
exec('git', ['-C', workspaceDir, 'config', 'user.email', AGENT_GIT_IDENTITY.email])
|
|
1343
1349
|
if (!wokenDisk && !sessionCopyFilled) log(`cloned ${session.repoBranch}`)
|
|
1344
1350
|
|
|
1351
|
+
// 5a — ORI-2609: the team copy holds only the projects the team seed knew
|
|
1352
|
+
// about. Write this session's project folder if it is missing, before the
|
|
1353
|
+
// agent starts. Never fails the boot. A woken disk is the agent's own saved
|
|
1354
|
+
// state, so a folder it removed there is not put back.
|
|
1355
|
+
log(wokenDisk
|
|
1356
|
+
? 'project folder left as the woken disk has it'
|
|
1357
|
+
: seedProjectFolder({
|
|
1358
|
+
workTree: workspaceDir,
|
|
1359
|
+
project: { id: session.projectId, slug: session.projectSlug, name: session.projectName },
|
|
1360
|
+
exec: (cmd, args) => exec(cmd, args),
|
|
1361
|
+
}))
|
|
1362
|
+
|
|
1345
1363
|
// 5b — Stage the orizu skill into the cloned repo so the agent discovers the
|
|
1346
1364
|
// Orizu workflows (ALI-1059). SHARED with the operator path via `stageOrizuSkill`
|
|
1347
1365
|
// (one resolution chain + the harvest-safe .git/info/exclude append). Non-fatal:
|
|
@@ -82,6 +82,7 @@ export function brokerEnv(config: BrokerConfig): Record<string, string> {
|
|
|
82
82
|
if (config.tokensFile) env.HB_TOKENS_FILE = config.tokensFile
|
|
83
83
|
if (config.sessionId) env.HB_SESSION_ID = config.sessionId
|
|
84
84
|
if (config.sessionBranch) env.HB_SESSION_BRANCH = config.sessionBranch
|
|
85
|
+
if (config.projectId) env.HB_PROJECT_ID = config.projectId
|
|
85
86
|
if (config.repoTokenStatus) env.HB_REPO_TOKEN_STATUS = String(config.repoTokenStatus)
|
|
86
87
|
if (config.eventsStatus) env.HB_EVENTS_STATUS = String(config.eventsStatus)
|
|
87
88
|
if (config.denyWrite) env.HB_DENY_WRITE = '1'
|
|
@@ -178,6 +179,8 @@ export interface HostedBootstrapRehearsal {
|
|
|
178
179
|
sessionId: string
|
|
179
180
|
runId: string
|
|
180
181
|
sessionBranch: string
|
|
182
|
+
/** The project the fake /sessions endpoint reports for the session. */
|
|
183
|
+
projectId: string
|
|
181
184
|
repoFullName: string
|
|
182
185
|
provider: SandboxProvider
|
|
183
186
|
resolveCloneUrl: (repo: string) => string
|
|
@@ -242,6 +245,7 @@ export async function createHostedBootstrapRehearsal(
|
|
|
242
245
|
|
|
243
246
|
const workspaceId = 'ws-hosted-1'
|
|
244
247
|
const sessionId = 'sess-hosted-1'
|
|
248
|
+
const projectId = '6f1c2d3e-4b5a-4c6d-8e7f-901a2b3c4d5e'
|
|
245
249
|
const runId = 'run-hosted-1'
|
|
246
250
|
const sessionBranch = `orizu/session-${sessionId}`
|
|
247
251
|
|
|
@@ -280,6 +284,7 @@ export async function createHostedBootstrapRehearsal(
|
|
|
280
284
|
fullRunApi: options.fullRunApi,
|
|
281
285
|
sessionId,
|
|
282
286
|
sessionBranch,
|
|
287
|
+
projectId,
|
|
283
288
|
})
|
|
284
289
|
|
|
285
290
|
// Real authenticated smart-HTTP git server on loopback: this is what makes the
|
|
@@ -301,6 +306,7 @@ export async function createHostedBootstrapRehearsal(
|
|
|
301
306
|
sessionId,
|
|
302
307
|
runId,
|
|
303
308
|
sessionBranch,
|
|
309
|
+
projectId,
|
|
304
310
|
repoFullName: repo.repoFullName,
|
|
305
311
|
provider,
|
|
306
312
|
resolveCloneUrl: () => cloneUrl,
|
|
@@ -70,6 +70,8 @@ export interface BrokerConfig {
|
|
|
70
70
|
* a pre-seeded session branch on the local bare repo). */
|
|
71
71
|
sessionId?: string
|
|
72
72
|
sessionBranch?: string
|
|
73
|
+
/** E2E: the project the /sessions endpoint reports, as the real route does. */
|
|
74
|
+
projectId?: string
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
export function readBrokerConfigFromEnv(env: NodeJS.ProcessEnv): BrokerConfig {
|
|
@@ -87,6 +89,7 @@ export function readBrokerConfigFromEnv(env: NodeJS.ProcessEnv): BrokerConfig {
|
|
|
87
89
|
fullRunApi: env.HB_FULL_RUN_API === '1',
|
|
88
90
|
sessionId: env.HB_SESSION_ID,
|
|
89
91
|
sessionBranch: env.HB_SESSION_BRANCH,
|
|
92
|
+
projectId: env.HB_PROJECT_ID,
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
|
|
@@ -151,7 +154,8 @@ export function startBrokerServer(config: BrokerConfig): { port: number; stop: (
|
|
|
151
154
|
const id = config.sessionId ?? `sess-e2e-${sessions}`
|
|
152
155
|
const repoBranch = config.sessionBranch ?? `orizu/session-${id}`
|
|
153
156
|
return json(
|
|
154
|
-
|
|
157
|
+
// Like the real route, a session has a project only when the request names one.
|
|
158
|
+
{ session: { id, workspaceId: sessionsMatch[1], projectId: typeof body.projectSlug === 'string' ? config.projectId ?? null : null, repoBranch, status: 'active' } },
|
|
155
159
|
201
|
|
156
160
|
)
|
|
157
161
|
}
|
|
@@ -53,7 +53,7 @@ import { readFileSync } from 'fs'
|
|
|
53
53
|
|
|
54
54
|
import { refreshHostedModels } from '../src/hosted-model-cache.mjs'
|
|
55
55
|
|
|
56
|
-
import { HOSTED_AGENT_NAME,
|
|
56
|
+
import { HOSTED_AGENT_NAME, hostedAgentPrompt } from './hosted-agent-prompt.js'
|
|
57
57
|
import { parseModelIdentity } from './provider-registry.js'
|
|
58
58
|
|
|
59
59
|
import { HARNESS_NAMES } from './hosted-harness.js'
|
|
@@ -1098,6 +1098,8 @@ export interface OpenCodeConfigOptions {
|
|
|
1098
1098
|
/** OpenCode permission map; defaults to allow-all EXCEPT the headless-denied
|
|
1099
1099
|
* tools below (sandbox is the boundary for everything else). */
|
|
1100
1100
|
permission?: Record<string, unknown>
|
|
1101
|
+
/** The session's Orizu project, named in the agent's prompt (ORI-2601). */
|
|
1102
|
+
projectId?: string | null
|
|
1101
1103
|
}
|
|
1102
1104
|
|
|
1103
1105
|
/**
|
|
@@ -1134,7 +1136,7 @@ export function buildOpenCodeConfigContent(opts: OpenCodeConfigOptions): string
|
|
|
1134
1136
|
model: opts.model,
|
|
1135
1137
|
permission: opts.permission ?? defaultHeadlessPermission(),
|
|
1136
1138
|
// Replaces OpenCode's built-in "You are opencode…" prompt (ORI-2207).
|
|
1137
|
-
agent: { [HOSTED_AGENT_NAME]: { mode: 'primary', prompt:
|
|
1139
|
+
agent: { [HOSTED_AGENT_NAME]: { mode: 'primary', prompt: hostedAgentPrompt(opts.projectId) } },
|
|
1138
1140
|
})
|
|
1139
1141
|
}
|
|
1140
1142
|
|
|
@@ -55,6 +55,8 @@ export interface HostedLoopContext {
|
|
|
55
55
|
taskFile: string
|
|
56
56
|
/** Directory the agent operates in (the cloned session-branch workspace). */
|
|
57
57
|
workspaceDir: string
|
|
58
|
+
/** The session's Orizu project, named in the agent's prompt (ORI-2601). */
|
|
59
|
+
projectId?: string | null
|
|
58
60
|
/** Validated destination branch for the CLI-owned checkpoint push. */
|
|
59
61
|
sessionBranch: string
|
|
60
62
|
/** Credential-free repository locator established by bootstrap. */
|
package/src/hosted-loop.ts
CHANGED
|
@@ -674,6 +674,7 @@ export async function runHostedLoop(opts: RunHostedLoopOptions): Promise<HostedL
|
|
|
674
674
|
spawned = await spawnImpl({
|
|
675
675
|
model: context.model,
|
|
676
676
|
permission: permissionForOrigin(context.sessionOrigin),
|
|
677
|
+
projectId: context.projectId,
|
|
677
678
|
cwd: context.workspaceDir,
|
|
678
679
|
port: context.opencodePort,
|
|
679
680
|
env: buildOpenCodeSpawnEnv(context),
|
|
@@ -1063,6 +1064,7 @@ function validateHostedLoopContext(value: unknown): { context: HostedLoopContext
|
|
|
1063
1064
|
if (record.sandboxBudgetMs !== undefined && (typeof record.sandboxBudgetMs !== 'number' || !Number.isFinite(record.sandboxBudgetMs) || record.sandboxBudgetMs <= 0)) fields.push('sandboxBudgetMs')
|
|
1064
1065
|
if (record.opencodePort !== undefined && (typeof record.opencodePort !== 'number' || !Number.isInteger(record.opencodePort) || record.opencodePort <= 0 || record.opencodePort > 65_535)) fields.push('opencodePort')
|
|
1065
1066
|
for (const name of OPTIONAL_CONTEXT_STRINGS) if (record[name] !== undefined && typeof record[name] !== 'string') fields.push(name)
|
|
1067
|
+
if (record.projectId !== undefined && record.projectId !== null && !isNonemptyString(record.projectId)) fields.push('projectId')
|
|
1066
1068
|
for (const name of ['prebaked', 'runSetupHook'] as const) if (record[name] !== undefined && typeof record[name] !== 'boolean') fields.push(name)
|
|
1067
1069
|
return { context: fields.length === 0 ? value as HostedLoopContext : null, fields: [...new Set(fields)] }
|
|
1068
1070
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORI-2609: every project of an approved team shares the team's repository,
|
|
3
|
+
* but the team seed wrote only the first project's folder. A hosted session
|
|
4
|
+
* for another project writes that project's folder into its own checkout at
|
|
5
|
+
* boot, exactly as the team seed would have, and commits it locally. The
|
|
6
|
+
* session's normal save carries the commit; nothing is pushed here.
|
|
7
|
+
*
|
|
8
|
+
* A folder that is already there, whole or partial, is never touched, so a
|
|
9
|
+
* second session never duplicates or overwrites one. Never throws: a seed
|
|
10
|
+
* failure is reported for the boot to log, and the session still starts.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
buildSeedContractFiles,
|
|
17
|
+
SEED_COMMIT_AUTHOR_EMAIL,
|
|
18
|
+
SEED_COMMIT_AUTHOR_NAME,
|
|
19
|
+
SEED_COMMIT_DATE,
|
|
20
|
+
} from './workspace-bootstrap-seed.js'
|
|
21
|
+
|
|
22
|
+
export interface ProjectFolderSeedProject {
|
|
23
|
+
id: string | null
|
|
24
|
+
slug: string | null
|
|
25
|
+
name: string | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type SeedExec = (cmd: string, args: string[]) => { status: number; stdout: string; stderr: string }
|
|
29
|
+
|
|
30
|
+
// One plain folder name: no separators, no traversal, no leading dot or dash.
|
|
31
|
+
const SAFE_PROJECT_SLUG = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
|
|
32
|
+
|
|
33
|
+
function readTeamFacts(workTree: string): { id: string; slug: string } {
|
|
34
|
+
// The team manifest the team seed wrote is the source of the team facts, so
|
|
35
|
+
// the new project's manifest agrees with the repository it lands in.
|
|
36
|
+
const manifest = JSON.parse(readFileSync(join(workTree, 'orizu.team.json'), 'utf8')) as Record<string, unknown>
|
|
37
|
+
if (typeof manifest.slug !== 'string' || manifest.slug.length === 0) {
|
|
38
|
+
throw new Error('orizu.team.json carries no team slug')
|
|
39
|
+
}
|
|
40
|
+
// The team id does not appear in project files; it is passed for completeness.
|
|
41
|
+
return { id: typeof manifest.teamId === 'string' ? manifest.teamId : '', slug: manifest.slug }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Returns the one line the boot logs. */
|
|
45
|
+
export function seedProjectFolder(input: {
|
|
46
|
+
workTree: string
|
|
47
|
+
project: ProjectFolderSeedProject
|
|
48
|
+
exec: SeedExec
|
|
49
|
+
}): string {
|
|
50
|
+
const { workTree, project, exec } = input
|
|
51
|
+
if (!project.id) return 'project folder skipped: the session names no project'
|
|
52
|
+
// Without the slug there is no folder name; the server leaves it out when it
|
|
53
|
+
// could not read the project.
|
|
54
|
+
if (!project.slug) return 'project folder skipped: the server sent no project slug'
|
|
55
|
+
try {
|
|
56
|
+
const slug = project.slug
|
|
57
|
+
if (!SAFE_PROJECT_SLUG.test(slug)) throw new Error('the project slug is not a plain folder name')
|
|
58
|
+
const folder = `projects/${slug}`
|
|
59
|
+
const projectsDir = join(workTree, 'projects')
|
|
60
|
+
if (existsSync(projectsDir) && !lstatSync(projectsDir).isDirectory()) {
|
|
61
|
+
throw new Error('projects/ is not a folder')
|
|
62
|
+
}
|
|
63
|
+
if (existsSync(join(workTree, folder))) return `project folder ${folder}/ already present`
|
|
64
|
+
|
|
65
|
+
// Byte-for-byte the files the team seed writes for this project.
|
|
66
|
+
const files = buildSeedContractFiles({
|
|
67
|
+
team: readTeamFacts(workTree),
|
|
68
|
+
projects: [{ id: project.id, slug, name: project.name }],
|
|
69
|
+
}).filter(file => file.path.startsWith(`${folder}/`))
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const target = join(workTree, file.path)
|
|
72
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
73
|
+
writeFileSync(target, file.bytes, { mode: 0o644 })
|
|
74
|
+
}
|
|
75
|
+
// No hook the checkout's config names runs inside the boot (as in harvest).
|
|
76
|
+
// The exec seam takes no `env` option, so `env` pins the commit dates.
|
|
77
|
+
const git = (step: string, ...args: string[]) => {
|
|
78
|
+
const result = exec('env', [
|
|
79
|
+
`GIT_AUTHOR_DATE=${SEED_COMMIT_DATE}`, `GIT_COMMITTER_DATE=${SEED_COMMIT_DATE}`,
|
|
80
|
+
'git', '-C', workTree, '-c', 'core.hooksPath=/dev/null', '-c', 'core.fsmonitor=false', ...args,
|
|
81
|
+
])
|
|
82
|
+
if (result.status !== 0) throw new Error(`git ${step} failed: ${(result.stderr || result.stdout).trim()}`)
|
|
83
|
+
}
|
|
84
|
+
git('add', 'add', '--', folder)
|
|
85
|
+
// Paths after `--` commit only this folder, whatever else is staged. A fixed
|
|
86
|
+
// author, committer and date make every session that starts from the same
|
|
87
|
+
// team head produce the same commit, so two sessions' publishes share it
|
|
88
|
+
// rather than conflict on files neither of them touched.
|
|
89
|
+
git('commit', '-c', 'commit.gpgsign=false', '-c', `user.name=${SEED_COMMIT_AUTHOR_NAME}`, '-c', `user.email=${SEED_COMMIT_AUTHOR_EMAIL}`,
|
|
90
|
+
'commit', '--quiet', '--no-verify', '-m', `Add the ${slug} project folder`, '--', folder)
|
|
91
|
+
return `project folder ${folder}/ seeded`
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return `project folder seed failed: ${error instanceof Error ? error.message : String(error)}`
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -678,8 +678,9 @@ export async function startHostedSession(
|
|
|
678
678
|
}),
|
|
679
679
|
}
|
|
680
680
|
)
|
|
681
|
-
const session = sessionResp.session as { id?: unknown; repoBranch?: unknown } | undefined
|
|
681
|
+
const session = sessionResp.session as { id?: unknown; repoBranch?: unknown; projectId?: unknown } | undefined
|
|
682
682
|
const sessionId = requireString(session?.id, 'session id')
|
|
683
|
+
const sessionProjectId = typeof session?.projectId === 'string' && session.projectId ? session.projectId : undefined
|
|
683
684
|
const sessionBranch = requireString(session?.repoBranch, 'session repoBranch')
|
|
684
685
|
log(`session ${sessionId} on ${sessionBranch}`)
|
|
685
686
|
|
|
@@ -821,6 +822,7 @@ export async function startHostedSession(
|
|
|
821
822
|
taskFile: `${paths.runDirAbs}/task.txt`,
|
|
822
823
|
workspaceDir: paths.repoDirRel,
|
|
823
824
|
sessionBranch,
|
|
825
|
+
projectId: sessionProjectId,
|
|
824
826
|
repositoryRemote: repo.cloneUrl,
|
|
825
827
|
repositoryCredentialHelper: bootstrap.credentialHelper,
|
|
826
828
|
model,
|