omp-conductor 0.12.0 → 0.14.0
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/README.md +253 -84
- package/package.json +1 -1
- package/src/availability.ts +165 -0
- package/src/board.ts +1 -1
- package/src/briefs/orchestrator.md +95 -28
- package/src/briefs/policy.md +44 -32
- package/src/cli.ts +226 -37
- package/src/config.ts +123 -7
- package/src/daemon.ts +1018 -145
- package/src/diff-flags.ts +77 -4
- package/src/digest-schedule.ts +92 -24
- package/src/escalate.ts +46 -19
- package/src/failure-class.ts +7 -5
- package/src/fleet.ts +39 -3
- package/src/omp.ts +8 -4
- package/src/orchestrator-tick.ts +471 -39
- package/src/orchestrator.ts +3 -2
- package/src/plugin.ts +166 -14
- package/src/release-policy.ts +66 -6
- package/src/reports.ts +202 -5
- package/src/session-host.ts +4 -3
- package/src/setup.ts +197 -35
- package/src/store.ts +785 -112
- package/src/tracker/github.ts +299 -56
- package/src/types.ts +289 -37
- package/src/verbs/actions.ts +245 -15
- package/src/verbs/protocol.ts +7 -6
- package/src/verbs/server.ts +94 -13
- package/src/worker.ts +51 -11
- package/src/worktree.ts +5 -0
package/src/worker.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { createSession, disposeSession } from "./omp.ts";
|
|
14
|
+
import type { ReleaseBlockContext } from "./release-policy.ts";
|
|
14
15
|
import type { Caps, ReleaseShape, ResolvedGrants, RunState } from "./types.ts";
|
|
15
16
|
|
|
16
17
|
/** Structured evidence fields from the worker's final report. */
|
|
@@ -21,6 +22,8 @@ const BLOCKED_PATTERN = /^state:\s*blocked\s*$/im;
|
|
|
21
22
|
|
|
22
23
|
/** Any explicit verdict line, whatever it claims. */
|
|
23
24
|
const STATE_LINE_PATTERN = /^state:\s*\S+\s*$/im;
|
|
25
|
+
/** GitHub PR URLs in unstructured prose; capture their canonical `owner/repo`. */
|
|
26
|
+
const GITHUB_PR_URL_PATTERN = /https:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/\d+\b/gi;
|
|
24
27
|
|
|
25
28
|
/** `{{KEY}}` placeholders in a brief template. */
|
|
26
29
|
const PLACEHOLDER_PATTERN = /\{\{([A-Za-z0-9_]+)\}\}/g;
|
|
@@ -47,6 +50,9 @@ export interface WorkerPauseControl {
|
|
|
47
50
|
/** Returns a parked session to work with a continuation prompt. Throws
|
|
48
51
|
* when the phase is not `paused`. */
|
|
49
52
|
resume(): void;
|
|
53
|
+
/** Terminally ends this run for an operator-supplied reason. The run is
|
|
54
|
+
* recorded as an administrative kill and cannot be resumed. */
|
|
55
|
+
stop(reason: string): void;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
/** What a resumed session is told. One literal so tests can pin it. */
|
|
@@ -59,6 +65,8 @@ export interface WorkerOpts {
|
|
|
59
65
|
brief: string;
|
|
60
66
|
cwd: string;
|
|
61
67
|
caps: Caps;
|
|
68
|
+
/** Canonical `owner/repo` identity used to scope prose-only PR URLs. */
|
|
69
|
+
repoSlug?: string;
|
|
62
70
|
/**
|
|
63
71
|
* Directory the harness writes this run's transcript into — a directory, not
|
|
64
72
|
* a file. The SDK takes no `sessionFile` input, so naming a path here would
|
|
@@ -78,7 +86,7 @@ export interface WorkerOpts {
|
|
|
78
86
|
*/
|
|
79
87
|
releaseGrants?: ResolvedGrants;
|
|
80
88
|
/** Durable audit sink for rejected release/deploy calls. */
|
|
81
|
-
onReleaseBlocked?: (shape: ReleaseShape) => void;
|
|
89
|
+
onReleaseBlocked?: (shape: ReleaseShape, context: ReleaseBlockContext) => void;
|
|
82
90
|
/** The child's pid, the instant it exists. See {@link VerbListener.bindPid}. */
|
|
83
91
|
onSpawn?: (pid: number) => void;
|
|
84
92
|
/** Control socket for that child, beside the run's own session directory. */
|
|
@@ -135,6 +143,8 @@ export interface WorkerResult {
|
|
|
135
143
|
spendUsd: number;
|
|
136
144
|
report: string;
|
|
137
145
|
killedBy?: KilledBy;
|
|
146
|
+
/** Present only when an operator terminally stopped this run. */
|
|
147
|
+
stoppedReason?: string;
|
|
138
148
|
/**
|
|
139
149
|
* Transcript the session actually opened, absent if it opened none. Recorded
|
|
140
150
|
* per run because it is the only readable evidence left once the worktree is
|
|
@@ -173,15 +183,23 @@ export function renderBrief(template: string, vars: Record<string, string>): str
|
|
|
173
183
|
* asks the tracker to verify those facts independently. Missing or malformed
|
|
174
184
|
* evidence fails closed.
|
|
175
185
|
*/
|
|
176
|
-
export function deriveResult(report: string): {
|
|
186
|
+
export function deriveResult(report: string, repoSlug?: string): {
|
|
177
187
|
state: RunState;
|
|
178
188
|
prUrl?: string;
|
|
179
189
|
headSha?: string;
|
|
180
190
|
} {
|
|
181
|
-
const
|
|
191
|
+
const structuredPrUrl = PR_URL_PATTERN.exec(report)?.[1];
|
|
182
192
|
const headSha = HEAD_SHA_PATTERN.exec(report)?.[1]?.toLowerCase();
|
|
183
|
-
if (PUSHED_GREEN_PATTERN.test(report) &&
|
|
184
|
-
return { state: "pushed-green", prUrl, headSha };
|
|
193
|
+
if (PUSHED_GREEN_PATTERN.test(report) && structuredPrUrl !== undefined && headSha !== undefined) {
|
|
194
|
+
return { state: "pushed-green", prUrl: structuredPrUrl, headSha };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let prUrl = structuredPrUrl;
|
|
198
|
+
if (prUrl === undefined && repoSlug !== undefined) {
|
|
199
|
+
const expectedRepo = repoSlug.toLowerCase();
|
|
200
|
+
for (const match of report.matchAll(GITHUB_PR_URL_PATTERN)) {
|
|
201
|
+
if (match[1]?.toLowerCase() === expectedRepo) prUrl = match[0];
|
|
202
|
+
}
|
|
185
203
|
}
|
|
186
204
|
|
|
187
205
|
const state: RunState = BLOCKED_PATTERN.test(report) ? "blocked" : "failed";
|
|
@@ -280,6 +298,7 @@ export async function runWorker(
|
|
|
280
298
|
// merged (#217).
|
|
281
299
|
let claim: { prUrl: string; headSha: string } | undefined;
|
|
282
300
|
let killedBy: KilledBy | undefined;
|
|
301
|
+
let stoppedReason: string | undefined;
|
|
283
302
|
// Canceler for the armed wall clock; invoked on every exit path below.
|
|
284
303
|
let cancelWallClock: (() => void) | undefined;
|
|
285
304
|
let wallClockRemainingMs = workerWallClockMs;
|
|
@@ -309,7 +328,7 @@ export async function runWorker(
|
|
|
309
328
|
};
|
|
310
329
|
|
|
311
330
|
const kill = (by: KilledBy) => {
|
|
312
|
-
if (killedBy !== undefined) return;
|
|
331
|
+
if (killedBy !== undefined || stoppedReason !== undefined) return;
|
|
313
332
|
killedBy = by;
|
|
314
333
|
o.onKilled?.(by);
|
|
315
334
|
clearWallClock();
|
|
@@ -331,7 +350,7 @@ export async function runWorker(
|
|
|
331
350
|
resumeWaiter = undefined;
|
|
332
351
|
pauseRequested = Promise.withResolvers<void>();
|
|
333
352
|
}
|
|
334
|
-
return killedBy !== undefined || done ? undefined : prompt;
|
|
353
|
+
return killedBy !== undefined || stoppedReason !== undefined || done ? undefined : prompt;
|
|
335
354
|
};
|
|
336
355
|
|
|
337
356
|
o.onPauseControl?.({
|
|
@@ -388,6 +407,16 @@ export async function runWorker(
|
|
|
388
407
|
armWallClock();
|
|
389
408
|
resumeWaiter?.resolve(RESUME_PROMPT);
|
|
390
409
|
},
|
|
410
|
+
stop: (reason) => {
|
|
411
|
+
if (killedBy !== undefined || done) {
|
|
412
|
+
throw new Error("the run is settling; nothing left to stop");
|
|
413
|
+
}
|
|
414
|
+
stoppedReason = reason;
|
|
415
|
+
clearWallClock();
|
|
416
|
+
done = true;
|
|
417
|
+
settle();
|
|
418
|
+
session.abort();
|
|
419
|
+
},
|
|
391
420
|
});
|
|
392
421
|
|
|
393
422
|
session.on("turn_start", () => {
|
|
@@ -407,7 +436,7 @@ export async function runWorker(
|
|
|
407
436
|
const text = reportText(field(message, "content"));
|
|
408
437
|
if (text !== "") {
|
|
409
438
|
report = text;
|
|
410
|
-
const stated = deriveResult(text);
|
|
439
|
+
const stated = deriveResult(text, o.repoSlug);
|
|
411
440
|
if (stated.state === "pushed-green" && stated.prUrl !== undefined && stated.headSha !== undefined) {
|
|
412
441
|
claim = { prUrl: stated.prUrl, headSha: stated.headSha };
|
|
413
442
|
}
|
|
@@ -464,7 +493,7 @@ export async function runWorker(
|
|
|
464
493
|
} catch (cause) {
|
|
465
494
|
// Our own abort surfaces here on some paths: a cap kill (existing
|
|
466
495
|
// behavior) or an operator park (new). Anything else is a real failure.
|
|
467
|
-
if (killedBy === undefined && resumeWaiter === undefined) throw cause;
|
|
496
|
+
if (killedBy === undefined && stoppedReason === undefined && resumeWaiter === undefined) throw cause;
|
|
468
497
|
}
|
|
469
498
|
next = undefined;
|
|
470
499
|
if (killedBy !== undefined) break;
|
|
@@ -483,7 +512,7 @@ export async function runWorker(
|
|
|
483
512
|
}
|
|
484
513
|
} catch (cause) {
|
|
485
514
|
// Our own abort surfaces here on some paths; that is a kill, not a crash.
|
|
486
|
-
if (killedBy === undefined) {
|
|
515
|
+
if (killedBy === undefined && stoppedReason === undefined) {
|
|
487
516
|
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
488
517
|
return withSessionFacts({
|
|
489
518
|
state: "failed",
|
|
@@ -504,6 +533,17 @@ export async function runWorker(
|
|
|
504
533
|
}
|
|
505
534
|
}
|
|
506
535
|
|
|
536
|
+
if (stoppedReason !== undefined) {
|
|
537
|
+
return withSessionFacts({
|
|
538
|
+
state: "stopped",
|
|
539
|
+
turns,
|
|
540
|
+
spendUsd,
|
|
541
|
+
report,
|
|
542
|
+
stoppedReason,
|
|
543
|
+
...(claim === undefined ? {} : { prUrl: claim.prUrl, headSha: claim.headSha }),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
507
547
|
if (killedBy !== undefined) {
|
|
508
548
|
// The PR and head are facts the session already established, so they
|
|
509
549
|
// survive the kill. Without them `shouldContinueAfterTurnsCap` sees no
|
|
@@ -526,7 +566,7 @@ export async function runWorker(
|
|
|
526
566
|
return withSessionFacts({ state: "pushed-green", ...claim, turns, spendUsd, report });
|
|
527
567
|
}
|
|
528
568
|
return withSessionFacts({
|
|
529
|
-
...deriveResult(report),
|
|
569
|
+
...deriveResult(report, o.repoSlug),
|
|
530
570
|
turns,
|
|
531
571
|
spendUsd,
|
|
532
572
|
report,
|
package/src/worktree.ts
CHANGED
|
@@ -317,6 +317,11 @@ async function ensureMirrorUnlocked(repo: RepoTarget, mirrorRoot: string): Promi
|
|
|
317
317
|
throw err;
|
|
318
318
|
}
|
|
319
319
|
await configureMirror(mirrorPath);
|
|
320
|
+
// `clone --mirror` writes upstream heads under `refs/heads/*`; the safe
|
|
321
|
+
// normal-clone refspec above uses `refs/remotes/origin/*`. Populate that
|
|
322
|
+
// namespace before returning so first-use callers see the same fresh mirror
|
|
323
|
+
// shape as every later refresh.
|
|
324
|
+
await refreshMirror(mirrorPath);
|
|
320
325
|
return mirrorPath;
|
|
321
326
|
}
|
|
322
327
|
|