omp-conductor 0.3.18 → 0.3.20
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 +352 -182
- package/package.json +1 -1
- package/skills/conductor-onboarding/SKILL.md +43 -50
- package/skills/conductor-update/SKILL.md +15 -165
- package/src/board.ts +734 -0
- package/src/brief-upgrade.ts +102 -19
- package/src/briefs/orchestrator.md +32 -16
- package/src/briefs/worker.md +7 -2
- package/src/cli.ts +138 -53
- package/src/config.ts +22 -6
- package/src/daemon.ts +673 -126
- package/src/fleet.ts +64 -6
- package/src/graph-health.ts +296 -0
- package/src/graph.ts +6 -6
- package/src/omp.ts +15 -4
- package/src/orchestrator-tick.ts +157 -10
- package/src/orchestrator.ts +8 -1
- package/src/plugin.ts +173 -45
- package/src/release-policy.ts +202 -0
- package/src/setup-host.ts +285 -0
- package/src/setup.ts +24 -20
- package/src/store.ts +373 -13
- package/src/tracker/github.ts +171 -3
- package/src/transcript.ts +45 -0
- package/src/types.ts +145 -2
- package/src/unblock.ts +26 -14
- package/src/upgrade.ts +537 -0
- package/src/worker.ts +67 -30
- package/src/worktree.ts +66 -0
- package/systemd/omp-conductor.service.example +5 -3
package/src/worker.ts
CHANGED
|
@@ -11,10 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { createSession, disposeSession } from "./omp.ts";
|
|
14
|
-
import type {
|
|
14
|
+
import type { ReleaseShape } from "./release-policy.ts";
|
|
15
|
+
import type { Caps, ReleasePolicy, RunState } from "./types.ts";
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
const PR_URL_PATTERN =
|
|
17
|
+
/** Structured evidence fields from the worker's final report. */
|
|
18
|
+
const PR_URL_PATTERN = /^pr:\s*(https:\/\/github\.com\/\S+\/pull\/\d+)\s*$/im;
|
|
19
|
+
const HEAD_SHA_PATTERN = /^head:\s*([0-9a-f]{40})\s*$/im;
|
|
20
|
+
const PUSHED_GREEN_PATTERN = /^state:\s*pushed-green\s*$/im;
|
|
21
|
+
const BLOCKED_PATTERN = /^state:\s*blocked\s*$/im;
|
|
18
22
|
|
|
19
23
|
/** `{{KEY}}` placeholders in a brief template. */
|
|
20
24
|
const PLACEHOLDER_PATTERN = /\{\{([A-Za-z0-9_]+)\}\}/g;
|
|
@@ -42,7 +46,20 @@ export interface WorkerOpts {
|
|
|
42
46
|
* the harness to pick, which is what an unconfigured project wants.
|
|
43
47
|
*/
|
|
44
48
|
model?: string;
|
|
49
|
+
/** Effective release/deploy gate for this session. */
|
|
50
|
+
releasePolicy?: ReleasePolicy;
|
|
51
|
+
/** Durable audit sink for rejected release/deploy calls. */
|
|
52
|
+
onReleaseBlocked?: (shape: ReleaseShape) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Reads the effective ceiling at each turn boundary. Omitted, the configured
|
|
55
|
+
* startup cap remains fixed for the run.
|
|
56
|
+
*/
|
|
57
|
+
maxTurns?: () => number;
|
|
58
|
+
/** Synchronous cap latch; called before the session abort begins. */
|
|
59
|
+
onKilled?: (by: KilledBy) => void;
|
|
45
60
|
onTurn?: (n: number) => void;
|
|
61
|
+
/** Cumulative USD spend, reported as each cost-bearing message finishes. */
|
|
62
|
+
onSpend?: (usd: number) => void;
|
|
46
63
|
/**
|
|
47
64
|
* The transcript path, handed over the moment the session opens it rather
|
|
48
65
|
* than at the end with {@link WorkerResult.sessionFile}. Both report the same
|
|
@@ -66,6 +83,7 @@ export interface RunWorkerDeps {
|
|
|
66
83
|
export interface WorkerResult {
|
|
67
84
|
state: RunState;
|
|
68
85
|
prUrl?: string;
|
|
86
|
+
headSha?: string;
|
|
69
87
|
turns: number;
|
|
70
88
|
spendUsd: number;
|
|
71
89
|
report: string;
|
|
@@ -101,23 +119,30 @@ export function renderBrief(template: string, vars: Record<string, string>): str
|
|
|
101
119
|
}
|
|
102
120
|
|
|
103
121
|
/**
|
|
104
|
-
* Read the run's outcome
|
|
122
|
+
* Read the run's structured outcome evidence from its final report.
|
|
105
123
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
124
|
+
* A textual `pushed-green` claim is not success by itself. The exact state line
|
|
125
|
+
* must carry both a PR URL and the head SHA observed after CI; the daemon then
|
|
126
|
+
* asks the tracker to verify those facts independently. Missing or malformed
|
|
127
|
+
* evidence fails closed.
|
|
109
128
|
*/
|
|
110
|
-
export function deriveResult(report: string): {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
129
|
+
export function deriveResult(report: string): {
|
|
130
|
+
state: RunState;
|
|
131
|
+
prUrl?: string;
|
|
132
|
+
headSha?: string;
|
|
133
|
+
} {
|
|
134
|
+
const prUrl = PR_URL_PATTERN.exec(report)?.[1];
|
|
135
|
+
const headSha = HEAD_SHA_PATTERN.exec(report)?.[1]?.toLowerCase();
|
|
136
|
+
if (PUSHED_GREEN_PATTERN.test(report) && prUrl !== undefined && headSha !== undefined) {
|
|
137
|
+
return { state: "pushed-green", prUrl, headSha };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const state: RunState = BLOCKED_PATTERN.test(report) ? "blocked" : "failed";
|
|
141
|
+
return {
|
|
142
|
+
state,
|
|
143
|
+
...(prUrl === undefined ? {} : { prUrl }),
|
|
144
|
+
...(headSha === undefined ? {} : { headSha }),
|
|
145
|
+
};
|
|
121
146
|
}
|
|
122
147
|
|
|
123
148
|
/**
|
|
@@ -145,8 +170,8 @@ export async function runWorker(
|
|
|
145
170
|
o: WorkerOpts,
|
|
146
171
|
deps: RunWorkerDeps = { createSession },
|
|
147
172
|
): Promise<WorkerResult> {
|
|
148
|
-
|
|
149
|
-
const
|
|
173
|
+
const { workerWallClockMs } = o.caps;
|
|
174
|
+
const maxTurns = o.maxTurns ?? (() => o.caps.workerMaxTurns);
|
|
150
175
|
|
|
151
176
|
const session = await deps.createSession({
|
|
152
177
|
cwd: o.cwd,
|
|
@@ -154,6 +179,8 @@ export async function runWorker(
|
|
|
154
179
|
...(o.model === undefined ? {} : { model: o.model }),
|
|
155
180
|
// Prevention half of #24: structured file tools cannot leave this worktree.
|
|
156
181
|
confineToCwd: true,
|
|
182
|
+
...(o.releasePolicy === undefined ? {} : { releasePolicy: o.releasePolicy }),
|
|
183
|
+
...(o.onReleaseBlocked === undefined ? {} : { onReleaseBlocked: o.onReleaseBlocked }),
|
|
157
184
|
});
|
|
158
185
|
|
|
159
186
|
// Before the first turn, not after the last: a caller that only learns the
|
|
@@ -195,6 +222,7 @@ export async function runWorker(
|
|
|
195
222
|
const kill = (by: KilledBy) => {
|
|
196
223
|
if (killedBy !== undefined) return;
|
|
197
224
|
killedBy = by;
|
|
225
|
+
o.onKilled?.(by);
|
|
198
226
|
clearWallClock();
|
|
199
227
|
session.abort();
|
|
200
228
|
// An aborted session may never reach a terminal `agent_end`. The cap is the
|
|
@@ -208,7 +236,7 @@ export async function runWorker(
|
|
|
208
236
|
// `message_end`s and would burn the cap on a run that is behaving.
|
|
209
237
|
turns += 1;
|
|
210
238
|
o.onTurn?.(turns);
|
|
211
|
-
if (turns >
|
|
239
|
+
if (turns > maxTurns()) kill("turns");
|
|
212
240
|
});
|
|
213
241
|
|
|
214
242
|
session.on("message_end", (event) => {
|
|
@@ -223,16 +251,25 @@ export async function runWorker(
|
|
|
223
251
|
// transcripts, 2026-08-07). The earlier agent_end.telemetry path never
|
|
224
252
|
// fired, so every run recorded $0 and the daily cap was theater (#46).
|
|
225
253
|
const cost = costUsdFromMessage(message);
|
|
226
|
-
if (cost !== undefined)
|
|
254
|
+
if (cost !== undefined) {
|
|
255
|
+
spendUsd += cost;
|
|
256
|
+
o.onSpend?.(spendUsd);
|
|
257
|
+
}
|
|
227
258
|
});
|
|
228
259
|
|
|
229
260
|
session.on("agent_end", (event) => {
|
|
230
261
|
// Fallback for harnesses that only attach cost on the terminal event.
|
|
231
262
|
const estimated = field(field(field(event, "telemetry"), "cost"), "estimatedUsd");
|
|
232
|
-
if (
|
|
263
|
+
if (
|
|
264
|
+
spendUsd === 0 &&
|
|
265
|
+
typeof estimated === "number" &&
|
|
266
|
+
Number.isFinite(estimated) &&
|
|
267
|
+
estimated > 0
|
|
268
|
+
) {
|
|
233
269
|
// Prefer message totals when both exist — do not double-count a run that
|
|
234
270
|
// already accumulated per-message costs.
|
|
235
|
-
|
|
271
|
+
spendUsd = estimated;
|
|
272
|
+
o.onSpend?.(spendUsd);
|
|
236
273
|
}
|
|
237
274
|
|
|
238
275
|
// Anything that is not literally `false` — including garbage or nothing at
|
|
@@ -283,12 +320,12 @@ export async function runWorker(
|
|
|
283
320
|
return withSessionFacts({ state: "killed", turns, spendUsd, report, killedBy });
|
|
284
321
|
}
|
|
285
322
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
);
|
|
323
|
+
return withSessionFacts({
|
|
324
|
+
...deriveResult(report),
|
|
325
|
+
turns,
|
|
326
|
+
spendUsd,
|
|
327
|
+
report,
|
|
328
|
+
});
|
|
292
329
|
}
|
|
293
330
|
|
|
294
331
|
/**
|
package/src/worktree.ts
CHANGED
|
@@ -608,3 +608,69 @@ export async function removeWorktree(
|
|
|
608
608
|
|
|
609
609
|
await git(["worktree", "prune"], mirrorPath);
|
|
610
610
|
}
|
|
611
|
+
|
|
612
|
+
export type RetainedWorktreeCleanup =
|
|
613
|
+
| { kind: "removed" }
|
|
614
|
+
| { kind: "retained"; reason: "dirty" | "unpushed" | "unknown"; detail: string };
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Reap a terminal run's tree and local mirror branch without deleting the only
|
|
618
|
+
* copy of work. Tracker state is proved by the caller; this function proves the
|
|
619
|
+
* local half after refreshing remote refs. Any ambiguity retains everything.
|
|
620
|
+
*/
|
|
621
|
+
export async function cleanupRetainedWorktree(
|
|
622
|
+
mirrorPath: string,
|
|
623
|
+
worktreePath: string,
|
|
624
|
+
branch: string,
|
|
625
|
+
): Promise<RetainedWorktreeCleanup> {
|
|
626
|
+
if (!existsSync(mirrorPath)) {
|
|
627
|
+
return existsSync(worktreePath)
|
|
628
|
+
? { kind: "retained", reason: "unknown", detail: "mirror is missing" }
|
|
629
|
+
: { kind: "removed" };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
if (existsSync(worktreePath)) {
|
|
634
|
+
const dirty = await git(["status", "--porcelain"], worktreePath);
|
|
635
|
+
if (dirty !== "") {
|
|
636
|
+
return { kind: "retained", reason: "dirty", detail: "worktree has uncommitted changes" };
|
|
637
|
+
}
|
|
638
|
+
const actual = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath);
|
|
639
|
+
if (actual !== branch) {
|
|
640
|
+
return {
|
|
641
|
+
kind: "retained",
|
|
642
|
+
reason: "unknown",
|
|
643
|
+
detail: `worktree is on ${actual}, expected ${branch}`,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// A deleted remote branch can make a pushed commit look local-only until
|
|
649
|
+
// the default branch is fetched. Failure is ambiguity, never permission.
|
|
650
|
+
await git(["fetch", "--prune", "origin"], mirrorPath);
|
|
651
|
+
|
|
652
|
+
const ref = `refs/heads/${branch}`;
|
|
653
|
+
if (await gitSucceeds(["show-ref", "--verify", "--quiet", ref], mirrorPath)) {
|
|
654
|
+
const unique = await git(["rev-list", ref, "--not", "--remotes"], mirrorPath);
|
|
655
|
+
if (unique !== "") {
|
|
656
|
+
return {
|
|
657
|
+
kind: "retained",
|
|
658
|
+
reason: "unpushed",
|
|
659
|
+
detail: `${branch} has commits absent from every remote ref`,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
await removeWorktree(mirrorPath, worktreePath);
|
|
665
|
+
if (await gitSucceeds(["show-ref", "--verify", "--quiet", ref], mirrorPath)) {
|
|
666
|
+
await git(["branch", "-D", branch], mirrorPath);
|
|
667
|
+
}
|
|
668
|
+
return { kind: "removed" };
|
|
669
|
+
} catch (err) {
|
|
670
|
+
return {
|
|
671
|
+
kind: "retained",
|
|
672
|
+
reason: "unknown",
|
|
673
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
}
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
# fleet on a ≤8 GB box.
|
|
17
17
|
#
|
|
18
18
|
# Install:
|
|
19
|
-
# sudo
|
|
20
|
-
# sudo systemctl daemon-reload
|
|
19
|
+
# sudo install -m 0644 omp-conductor.service.example /etc/systemd/system/omp-conductor.service
|
|
20
|
+
# sudo systemctl daemon-reload
|
|
21
|
+
# sudo systemctl enable omp-conductor.service
|
|
22
|
+
# sudo systemctl restart omp-conductor.service
|
|
21
23
|
|
|
22
24
|
[Unit]
|
|
23
25
|
Description=omp-conductor dispatch daemon
|
|
@@ -38,7 +40,7 @@ WorkingDirectory=/home/fleet
|
|
|
38
40
|
|
|
39
41
|
# Foreground daemon so systemd tracks MainPID. `omp-conductor start` backgrounds;
|
|
40
42
|
# under a unit, call `daemon` directly.
|
|
41
|
-
ExecStart=/home/fleet/.local/bin/omp-conductor daemon --port
|
|
43
|
+
ExecStart=/home/fleet/.local/bin/omp-conductor daemon --port 8787
|
|
42
44
|
|
|
43
45
|
Restart=on-failure
|
|
44
46
|
# Handled SIGTERM exits 143; without this, Restart=on-failure brings the unit
|