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.
Files changed (65) hide show
  1. package/README.md +35 -1
  2. package/REFERENCE.md +61 -11
  3. package/agents/to-spec.md +94 -0
  4. package/package.json +2 -1
  5. package/schema/config.schema.json +35 -1
  6. package/src/admission.ts +204 -75
  7. package/src/arm-challenge.ts +250 -57
  8. package/src/ask.ts +268 -7
  9. package/src/board.ts +17 -3
  10. package/src/briefs/orchestrator.md +62 -21
  11. package/src/briefs/to-spec.md +88 -0
  12. package/src/briefs/worker.md +2 -1
  13. package/src/cli.ts +124 -1
  14. package/src/command-help.ts +11 -0
  15. package/src/command-manifest.ts +38 -5
  16. package/src/commands/arm.ts +1 -1
  17. package/src/commands/context.ts +1 -0
  18. package/src/commands/drain.ts +176 -0
  19. package/src/commands/extend.ts +6 -10
  20. package/src/commands/intake.ts +4 -19
  21. package/src/commands/status.ts +5 -1
  22. package/src/commands/watch.ts +51 -16
  23. package/src/commands/worker.ts +9 -10
  24. package/src/config-schema.ts +43 -6
  25. package/src/config.ts +65 -9
  26. package/src/daemon.ts +879 -41
  27. package/src/dashboard/app.js +4 -1
  28. package/src/dashboard/server.ts +5 -2
  29. package/src/decisions.ts +243 -17
  30. package/src/diff-flags.ts +75 -1
  31. package/src/doctor.ts +60 -82
  32. package/src/escalate.ts +31 -14
  33. package/src/failure-class.ts +28 -2
  34. package/src/fleet.ts +239 -240
  35. package/src/gitops.ts +188 -81
  36. package/src/graph-health.ts +35 -1
  37. package/src/graph.ts +66 -1
  38. package/src/harness-loader.ts +59 -0
  39. package/src/host.ts +242 -2
  40. package/src/lifecycle.ts +122 -1
  41. package/src/omp-settings.ts +19 -0
  42. package/src/omp.ts +183 -21
  43. package/src/orchestrator-tick.ts +1591 -32
  44. package/src/orchestrator.ts +12 -0
  45. package/src/privileged.ts +1 -4
  46. package/src/release-policy.ts +503 -9
  47. package/src/session-host.ts +65 -6
  48. package/src/settlement.ts +69 -17
  49. package/src/setup-host.ts +1225 -9
  50. package/src/setup-install.ts +28 -0
  51. package/src/setup-wizard.ts +154 -3
  52. package/src/setup.ts +83 -17
  53. package/src/shell.ts +15 -0
  54. package/src/status-render.ts +216 -12
  55. package/src/store.ts +443 -42
  56. package/src/to-spec.ts +408 -0
  57. package/src/tracker/github.ts +104 -14
  58. package/src/types.ts +405 -19
  59. package/src/upgrade-verify.ts +209 -2
  60. package/src/upgrade.ts +175 -1
  61. package/src/verbs/protocol.ts +39 -0
  62. package/src/verbs/server.ts +765 -56
  63. package/src/verbs/socket.ts +24 -5
  64. package/src/worker.ts +12 -2
  65. package/src/worktree.ts +29 -12
@@ -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
- | { t: "ready"; sessionFile?: string; modelFallbackMessage?: string }
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<void> {
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
- process.exit(0);
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 "./verbs/server.ts";
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, a deleted PR. Settling on it would record a
179
- * merge that never happened, and the next tick asks again for free. An
180
- * ambiguous answer must never settle a row.
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(pr: PrState | undefined, prUrl: string): Settlement | undefined {
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
- // Per row, like admission's held candidate. The GitHub adapter already
319
- // answers undefined instead of throwing, so this catch is the port's
320
- // contract rather than that adapter's behaviour and a tracker that does
321
- // throw must cost its own row, not the whole sweep.
322
- log(`#${run.issue} not settled: PR state lookup failed (${errText(err)}) retrying next tick`);
323
- continue;
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
- // Per row, like every other sweep here: one unreachable PR must not stop
1301
- // the rest from being classified. The next tick asks again for free.
1302
- log(`#${run.issue} not classified: fact gathering failed (${errText(err)}) retrying next tick`);
1303
- continue;
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(