omp-conductor 0.18.0 → 0.18.2
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 +35 -1
- package/REFERENCE.md +61 -11
- package/agents/to-spec.md +94 -0
- package/package.json +2 -1
- package/schema/config.schema.json +35 -1
- package/src/admission.ts +204 -75
- package/src/arm-challenge.ts +250 -57
- package/src/ask.ts +268 -7
- package/src/board.ts +17 -3
- package/src/briefs/orchestrator.md +62 -21
- package/src/briefs/to-spec.md +88 -0
- package/src/briefs/worker.md +2 -1
- package/src/cli.ts +124 -1
- package/src/command-help.ts +11 -0
- package/src/command-manifest.ts +38 -5
- package/src/commands/arm.ts +1 -1
- package/src/commands/context.ts +1 -0
- package/src/commands/drain.ts +176 -0
- package/src/commands/extend.ts +6 -10
- package/src/commands/intake.ts +4 -19
- package/src/commands/status.ts +5 -1
- package/src/commands/watch.ts +51 -16
- package/src/commands/worker.ts +9 -10
- package/src/config-schema.ts +43 -6
- package/src/config.ts +65 -9
- package/src/daemon.ts +879 -41
- package/src/dashboard/app.js +4 -1
- package/src/dashboard/server.ts +5 -2
- package/src/decisions.ts +243 -17
- package/src/diff-flags.ts +75 -1
- package/src/doctor.ts +60 -82
- package/src/escalate.ts +31 -14
- package/src/failure-class.ts +28 -2
- package/src/fleet.ts +239 -240
- package/src/gitops.ts +188 -81
- package/src/graph-health.ts +35 -1
- package/src/graph.ts +66 -1
- package/src/harness-loader.ts +59 -0
- package/src/host.ts +242 -2
- package/src/lifecycle.ts +122 -1
- package/src/omp-settings.ts +19 -0
- package/src/omp.ts +183 -21
- package/src/orchestrator-tick.ts +1591 -32
- package/src/orchestrator.ts +12 -0
- package/src/privileged.ts +1 -4
- package/src/release-policy.ts +503 -9
- package/src/session-host.ts +65 -6
- package/src/settlement.ts +69 -17
- package/src/setup-host.ts +1225 -9
- package/src/setup-install.ts +28 -0
- package/src/setup-wizard.ts +154 -3
- package/src/setup.ts +83 -17
- package/src/shell.ts +15 -0
- package/src/status-render.ts +216 -12
- package/src/store.ts +443 -42
- package/src/to-spec.ts +408 -0
- package/src/tracker/github.ts +104 -14
- package/src/types.ts +405 -19
- package/src/upgrade-verify.ts +209 -2
- package/src/upgrade.ts +175 -1
- package/src/verbs/protocol.ts +39 -0
- package/src/verbs/server.ts +765 -56
- package/src/verbs/socket.ts +24 -5
- package/src/worker.ts +12 -2
- package/src/worktree.ts +29 -12
package/src/session-host.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { connect } from "node:net";
|
|
23
23
|
|
|
24
24
|
import { createLocalSession, disposeSession, type AgentSessionLike } from "./omp.ts";
|
|
25
|
+
import type { GraphToolsObservation } from "./types.ts";
|
|
25
26
|
import type { GateShape, ReleaseBlockContext } from "./release-policy.ts";
|
|
26
27
|
import type { ResolvedGrants, SessionRole } from "./types.ts";
|
|
27
28
|
|
|
@@ -79,7 +80,35 @@ export type ParentToHost =
|
|
|
79
80
|
|
|
80
81
|
/** Child → parent. */
|
|
81
82
|
export type HostToParent =
|
|
82
|
-
| {
|
|
83
|
+
| {
|
|
84
|
+
t: "ready";
|
|
85
|
+
sessionFile?: string;
|
|
86
|
+
modelFallbackMessage?: string;
|
|
87
|
+
/**
|
|
88
|
+
* The omp-conductor version this child process loaded, attested at the
|
|
89
|
+
* ready handshake (#832): the session host's `ready` is the exact-session
|
|
90
|
+
* handoff the upgrade verifies against, so the live process's own loaded
|
|
91
|
+
* module version travels with it instead of being read off disk later.
|
|
92
|
+
* Absent only when the version cannot be read — absence is "not
|
|
93
|
+
* attested", never "the newest".
|
|
94
|
+
*/
|
|
95
|
+
extensionVersion?: string;
|
|
96
|
+
/** Exact peer entry and package version loaded by this child (#828). */
|
|
97
|
+
harnessPath?: string;
|
|
98
|
+
harnessVersion?: string;
|
|
99
|
+
}
|
|
100
|
+
| {
|
|
101
|
+
t: "graph-tools";
|
|
102
|
+
/**
|
|
103
|
+
* The code-graph session observation (#726): whether the graph MCP tools
|
|
104
|
+
* were in this session's registry. Sent once the session's tool registry
|
|
105
|
+
* settles (it finalises after creation, so the ready handshake never
|
|
106
|
+
* waits for it). Absent value never travels — no message means no
|
|
107
|
+
* observation was recorded, never "graph tools absent", which is the
|
|
108
|
+
* `present: false` truth value.
|
|
109
|
+
*/
|
|
110
|
+
graphTools: GraphToolsObservation;
|
|
111
|
+
}
|
|
83
112
|
| { t: "start-error"; message: string }
|
|
84
113
|
| { t: "event"; event: unknown }
|
|
85
114
|
| { t: "session-file"; path: string }
|
|
@@ -172,12 +201,15 @@ export interface SessionHostDeps {
|
|
|
172
201
|
* into the same `Error` the in-process path would have raised, so a missing
|
|
173
202
|
* peer dependency still reads as a deployment mistake rather than as an
|
|
174
203
|
* unexplained child exit.
|
|
204
|
+
*
|
|
205
|
+
* Always resolves `true` now that the worker-identity refusal is gone (#894):
|
|
206
|
+
* the boolean return survives so the entry point's exit-code contract keeps
|
|
207
|
+
* its shape for any wrapper that learned it.
|
|
175
208
|
*/
|
|
176
209
|
export async function runSessionHost(
|
|
177
210
|
spec: SessionHostSpec,
|
|
178
211
|
deps: SessionHostDeps = { createSession: createLocalSession },
|
|
179
|
-
): Promise<
|
|
180
|
-
|
|
212
|
+
): Promise<boolean> {
|
|
181
213
|
const socket = connect(spec.socket);
|
|
182
214
|
socket.setNoDelay(true);
|
|
183
215
|
const send = (message: HostToParent): void => {
|
|
@@ -220,12 +252,15 @@ export async function runSessionHost(
|
|
|
220
252
|
// The frame must reach the parent before this process exits: `process.exit`
|
|
221
253
|
// does not flush the socket, and a parent that only observes the exit used
|
|
222
254
|
// to report a bare exit code instead of the child's own words.
|
|
255
|
+
// This is a failed start, not an identity refusal: the process still walks
|
|
256
|
+
// away with a clean exit code, because the parent's verdict comes from the
|
|
257
|
+
// frame, not the status — 91 is reserved for the boundary refusal above.
|
|
223
258
|
await new Promise<void>((resolve) => {
|
|
224
259
|
socket.once("finish", () => resolve());
|
|
225
260
|
socket.once("error", () => resolve());
|
|
226
261
|
socket.end();
|
|
227
262
|
});
|
|
228
|
-
return;
|
|
263
|
+
return true;
|
|
229
264
|
}
|
|
230
265
|
|
|
231
266
|
const live = session;
|
|
@@ -248,8 +283,28 @@ export async function runSessionHost(
|
|
|
248
283
|
...(live.modelFallbackMessage === undefined
|
|
249
284
|
? {}
|
|
250
285
|
: { modelFallbackMessage: live.modelFallbackMessage }),
|
|
286
|
+
// The loaded-module attestation (#832): what THIS process's session
|
|
287
|
+
// machinery actually executes, carried in the exact-session handoff the
|
|
288
|
+
// upgrade verifies against.
|
|
289
|
+
...(live.extensionVersion === undefined ? {} : { extensionVersion: live.extensionVersion }),
|
|
290
|
+
...(live.harnessPath === undefined ? {} : { harnessPath: live.harnessPath }),
|
|
291
|
+
...(live.harnessVersion === undefined ? {} : { harnessVersion: live.harnessVersion }),
|
|
251
292
|
});
|
|
252
293
|
|
|
294
|
+
// The code-graph session observation (#726) rides its own frame: the factory
|
|
295
|
+
// reads the session's tool registry asynchronously (the harness finalises
|
|
296
|
+
// MCP wiring after creation), so the ready handshake never waits for it. The
|
|
297
|
+
// child is the session's own process — this is the runtime observation the
|
|
298
|
+
// config check in `resolvePrereqs` deliberately is not, and an observation
|
|
299
|
+
// that never resolves simply never travels: no message is "not recorded",
|
|
300
|
+
// never "graph tools absent".
|
|
301
|
+
const graphToolsReady = live.graphToolsReady;
|
|
302
|
+
if (graphToolsReady !== undefined) {
|
|
303
|
+
void graphToolsReady.then((graphTools) => {
|
|
304
|
+
if (graphTools !== undefined) send({ t: "graph-tools", graphTools });
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
253
308
|
const { promise: finished, resolve: finish } = Promise.withResolvers<void>();
|
|
254
309
|
let buffer = "";
|
|
255
310
|
socket.on("data", (chunk: Buffer) => {
|
|
@@ -316,6 +371,7 @@ export async function runSessionHost(
|
|
|
316
371
|
// Teardown noise must not become the run's outcome.
|
|
317
372
|
}
|
|
318
373
|
socket.end();
|
|
374
|
+
return true;
|
|
319
375
|
}
|
|
320
376
|
|
|
321
377
|
/** Reads the spec the parent handed over. Argv, not stdin: stdin is the harness's. */
|
|
@@ -331,6 +387,9 @@ if (import.meta.main) {
|
|
|
331
387
|
// Exit code is the parent's only signal when the socket never opened, so a
|
|
332
388
|
// spec that will not parse fails loudly here rather than as a silent hang.
|
|
333
389
|
const spec = specFromArgv(process.argv);
|
|
334
|
-
await runSessionHost(spec);
|
|
335
|
-
|
|
390
|
+
const started = await runSessionHost(spec);
|
|
391
|
+
// 91 was the worker-identity boundary refusal; the code stays reserved (and
|
|
392
|
+
// unreachable — runSessionHost always resolves true since #894) so nothing
|
|
393
|
+
// that learned to read it can mistake a future refusal for a clean exit.
|
|
394
|
+
process.exit(started ? 0 : 91);
|
|
336
395
|
}
|
package/src/settlement.ts
CHANGED
|
@@ -23,8 +23,9 @@ import {
|
|
|
23
23
|
deriveChangedLine,
|
|
24
24
|
} from "./diff-flags.ts";
|
|
25
25
|
import { classifyRun, normalise, type ClassifyFacts } from "./failure-class.ts";
|
|
26
|
+
import { GhPrMissingError } from "./tracker/github.ts";
|
|
26
27
|
import { formatModelsTried, modelsTried } from "./model-fallback.ts";
|
|
27
|
-
import { PR_LOOKUP_WINDOW_MS } from "./
|
|
28
|
+
import { PR_LOOKUP_WINDOW_MS } from "./decisions.ts";
|
|
28
29
|
import {
|
|
29
30
|
removeWorktree,
|
|
30
31
|
salvageWip,
|
|
@@ -35,6 +36,7 @@ import type {
|
|
|
35
36
|
Caps,
|
|
36
37
|
Escalation,
|
|
37
38
|
FailureClass,
|
|
39
|
+
FileLane,
|
|
38
40
|
MergedPrInfo,
|
|
39
41
|
OpenCloser,
|
|
40
42
|
PrState,
|
|
@@ -107,7 +109,7 @@ export interface SettlementAuditResult {
|
|
|
107
109
|
*/
|
|
108
110
|
export async function collectSettlementFlags(
|
|
109
111
|
tracker: Pick<Tracker, "prDiff" | "prBody">,
|
|
110
|
-
claim: { prUrl?: string; issueText: string; sessionFile?: string },
|
|
112
|
+
claim: { prUrl?: string; issueText: string; sessionFile?: string; lane?: FileLane },
|
|
111
113
|
): Promise<SettlementAuditResult> {
|
|
112
114
|
if (claim.prUrl === undefined) return { flags: [UNREADABLE_TREE_FLAG], truncated: false };
|
|
113
115
|
// The diff and the claimed-proof body read in parallel; both are advisory,
|
|
@@ -135,6 +137,7 @@ export async function collectSettlementFlags(
|
|
|
135
137
|
diff,
|
|
136
138
|
...(prBody === undefined ? {} : { prBody }),
|
|
137
139
|
...(transcript === undefined ? {} : { transcript }),
|
|
140
|
+
...(claim.lane === undefined ? {} : { lane: claim.lane }),
|
|
138
141
|
}),
|
|
139
142
|
truncated: diff.truncated,
|
|
140
143
|
changedLine: deriveChangedLine(diff),
|
|
@@ -174,14 +177,24 @@ export interface Settlement {
|
|
|
174
177
|
* `returned-for-revision` at settlement. A review decision asks for another
|
|
175
178
|
* implementation pass, not a failure, so it consumes the continuation budget
|
|
176
179
|
* instead of the failed-attempt budget.
|
|
180
|
+
* - `missing` — the claimed PR definitively does not exist (a definitive 404
|
|
181
|
+
* from the tracker, never a transient outage). Work was pushed but no PR
|
|
182
|
+
* ever appeared, so the honest terminal is the same as `closed`: `failed`,
|
|
183
|
+
* work preserved on the branch and the issue returned to the queue as a
|
|
184
|
+
* continuation. Nothing retrying can recover — a missing PR does not grow
|
|
185
|
+
* back (#779).
|
|
177
186
|
* - `open`, and undefined — nothing changes. Undefined is "could not tell": a
|
|
178
|
-
* flaky network, a revoked token
|
|
179
|
-
*
|
|
180
|
-
*
|
|
187
|
+
* flaky network, a revoked token. Settling on it would record a merge that
|
|
188
|
+
* never happened, and the next tick asks again for free. An ambiguous answer
|
|
189
|
+
* must never settle a row.
|
|
181
190
|
*/
|
|
182
|
-
export function settlementFor(
|
|
191
|
+
export function settlementFor(
|
|
192
|
+
pr: PrState | "missing" | undefined,
|
|
193
|
+
prUrl: string,
|
|
194
|
+
): Settlement | undefined {
|
|
183
195
|
if (pr === "merged") return { state: "merged", reason: `${prUrl} merged` };
|
|
184
196
|
if (pr === "closed") return { state: "failed", reason: `${prUrl} closed without merging` };
|
|
197
|
+
if (pr === "missing") return { state: "failed", reason: `PR ${prUrl} does not exist` };
|
|
185
198
|
return undefined;
|
|
186
199
|
}
|
|
187
200
|
|
|
@@ -311,16 +324,27 @@ export async function settlePushedGreen(
|
|
|
311
324
|
// must not buy a `gh` call every five minutes forever.
|
|
312
325
|
if (run.prUrl === undefined) continue;
|
|
313
326
|
|
|
314
|
-
let pr: PrState | undefined;
|
|
327
|
+
let pr: PrState | "missing" | undefined;
|
|
315
328
|
try {
|
|
316
329
|
pr = await tracker.prState(run.prUrl);
|
|
317
330
|
} catch (err) {
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
331
|
+
// The adapter throws exactly one classified error: `GhPrMissingError`,
|
|
332
|
+
// an individual REST 404 the adapter has corroborated with a
|
|
333
|
+
// same-repository pulls-list read, so the claimed PR definitively does
|
|
334
|
+
// not exist (#779). That is not "could not tell" — retrying can never
|
|
335
|
+
// conjure the PR — so the row is settled as a missing PR through the
|
|
336
|
+
// same mapping as a closed one: work preserved on the branch, busy guard
|
|
337
|
+
// released, issue back on the queue. A raw 404 that was never
|
|
338
|
+
// corroborated (a hidden repository, a lost token scope) stays "could
|
|
339
|
+
// not tell": the instance check is the corroboration, and every other
|
|
340
|
+
// failure retries next tick, per row — a tracker that throws
|
|
341
|
+
// unclassified must cost its own row, not the whole sweep.
|
|
342
|
+
if (err instanceof GhPrMissingError) {
|
|
343
|
+
pr = "missing";
|
|
344
|
+
} else {
|
|
345
|
+
log(`#${run.issue} not settled: PR state lookup failed (${errText(err)}) — retrying next tick`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
324
348
|
}
|
|
325
349
|
|
|
326
350
|
const settlement = settlementFor(pr, run.prUrl);
|
|
@@ -1297,10 +1321,23 @@ export async function classifyAndRecover(d: SettlementDeps): Promise<number> {
|
|
|
1297
1321
|
}
|
|
1298
1322
|
}
|
|
1299
1323
|
} catch (err) {
|
|
1300
|
-
//
|
|
1301
|
-
//
|
|
1302
|
-
|
|
1303
|
-
|
|
1324
|
+
// The tracker throws exactly one classified error: `GhPrMissingError`,
|
|
1325
|
+
// an individual REST 404 the adapter corroborated with a
|
|
1326
|
+
// same-repository pulls-list read, so the claimed PR definitively does
|
|
1327
|
+
// not exist (#779). That is a fact, not "could not tell" — retrying can
|
|
1328
|
+
// never conjure the PR — so it is recorded as the absent-PR fact and
|
|
1329
|
+
// classification proceeds instead of leaving the row in
|
|
1330
|
+
// `selectUnclassified` to be offered forever. Everything else (a 5xx, a
|
|
1331
|
+
// revoked token, an opaque 404) is genuinely unreadable and the next
|
|
1332
|
+
// tick asks again for free.
|
|
1333
|
+
if (err instanceof GhPrMissingError) {
|
|
1334
|
+
facts.pr = "missing";
|
|
1335
|
+
} else {
|
|
1336
|
+
// Per row, like every other sweep here: one unreachable PR must not stop
|
|
1337
|
+
// the rest from being classified. The next tick asks again for free.
|
|
1338
|
+
log(`#${run.issue} not classified: fact gathering failed (${errText(err)}) — retrying next tick`);
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1304
1341
|
}
|
|
1305
1342
|
|
|
1306
1343
|
const { cls, recovery, evidence } = classifyRun(classifiedRun, facts, caps);
|
|
@@ -1310,6 +1347,21 @@ export async function classifyAndRecover(d: SettlementDeps): Promise<number> {
|
|
|
1310
1347
|
// base does move under it.
|
|
1311
1348
|
if (run.state === "pushed-green" && cls === "unknown") continue;
|
|
1312
1349
|
|
|
1350
|
+
// A recovery of `none` is the classifier naming no failure class for the
|
|
1351
|
+
// row at all: a terminal `failed` row whose PR is open and every check is
|
|
1352
|
+
// green is the strongest mechanical success evidence there is short of
|
|
1353
|
+
// merge, so it is not a failed attempt. Restore it to `pushed-green` — the
|
|
1354
|
+
// settle sweep owns the merge decision from there, `failuresFor` stops
|
|
1355
|
+
// counting it, and no `[unknown]` escalation is persisted. Nothing else is
|
|
1356
|
+
// written, so the row stays in the normal `pushed-green` lifecycle
|
|
1357
|
+
// (merge-conflict on a moved base, settle on a merged or closed PR) rather
|
|
1358
|
+
// than being pinned by a class it never had (#766).
|
|
1359
|
+
if (recovery === "none") {
|
|
1360
|
+
store.updateRun(run.id, { state: "pushed-green" });
|
|
1361
|
+
log(`#${run.issue} restored to pushed-green: ${evidence}`);
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1313
1365
|
const retry = run.failureClass !== undefined;
|
|
1314
1366
|
store.updateRun(run.id, { failureClass: cls, recoveryAction: recovery });
|
|
1315
1367
|
log(
|