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
package/src/omp.ts CHANGED
@@ -8,17 +8,16 @@
8
8
  * `createAgentSession`, `subscribe` or `abort`, exactly one file breaks.
9
9
  */
10
10
 
11
- /**
12
- * Harness package name. Held in a variable and cast to `string` at the call
13
- * site on purpose: a non-literal specifier stops `tsc` from trying to resolve
14
- * the module, which is the whole reason this shim exists.
15
- */
16
- import { chmodSync, chownSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
11
+ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
17
12
  import { createServer, type Server, type Socket } from "node:net";
18
13
  import { tmpdir } from "node:os";
19
14
  import { dirname, join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
20
16
 
17
+ import { OMP_HARNESS_PACKAGE, packageNodeModulesRoot } from "./host.ts";
18
+ import { harnessVersion, resolveHarnessEntry } from "./harness-loader.ts";
21
19
  import { readOnlySession, worktreeConfinement } from "./confinement.ts";
20
+ import { observeGraphTools } from "./graph.ts";
22
21
  import { releasePolicyTripwire, type GateShape, type ReleaseBlockContext } from "./release-policy.ts";
23
22
  import type {
24
23
  HostToParent,
@@ -26,11 +25,53 @@ import type {
26
25
  SessionHostSpec,
27
26
  } from "./session-host.ts";
28
27
  import { decodeFrames, encodeFrame } from "./session-host.ts";
29
- import type { ResolvedGrants, SessionRole } from "./types.ts";
28
+ import type { GraphToolsObservation, ResolvedGrants, SessionRole } from "./types.ts";
30
29
  import { SESSION_ROLE_ENV } from "./types.ts";
31
30
  import { conductorVerbs } from "./verbs/client.ts";
32
31
 
33
- const OMP_PACKAGE = "@oh-my-pi/pi-coding-agent";
32
+ /**
33
+ * Harness package name. Held in a variable and cast to `string` at the call
34
+ * site on purpose: a non-literal specifier stops `tsc` from trying to resolve
35
+ * the module, which is the whole reason this shim exists.
36
+ */
37
+ const OMP_PACKAGE = OMP_HARNESS_PACKAGE;
38
+
39
+ /**
40
+ * Why the explicitly anchored harness entry is not the one installed beside
41
+ * omp-conductor, or `undefined` when it is — or when there is nothing to
42
+ * compare, because this package is running from a source checkout with no
43
+ * install root of its own.
44
+ *
45
+ * The failure it names is silent otherwise (#828): Bun's ambient bare-package
46
+ * resolution can choose its install cache even when the worker binding is
47
+ * mounted and inode-current. The production resolver is therefore
48
+ * {@link resolveHarnessEntry}, rooted at this module's directory; this check
49
+ * remains the fail-closed attestation that the resolved absolute path stayed
50
+ * inside the same install.
51
+ */
52
+ export function harnessOutsideInstall(
53
+ moduleDir: string = import.meta.dir,
54
+ resolveSpecifier: (specifier: string) => string = () => resolveHarnessEntry(moduleDir),
55
+ ): string | undefined {
56
+ const root = packageNodeModulesRoot(moduleDir);
57
+ if (root === undefined) return undefined;
58
+ let resolved: string;
59
+ try {
60
+ const result = resolveSpecifier(OMP_PACKAGE);
61
+ resolved = result.startsWith("file:") ? fileURLToPath(result) : result;
62
+ } catch {
63
+ // Nothing resolved at all. The import below reports that far better than a
64
+ // guess here could, and reporting it twice in two spellings is worse.
65
+ return undefined;
66
+ }
67
+ if (resolved.startsWith(`${root}/`)) return undefined;
68
+ return (
69
+ `omp-conductor could not load its peer dependency ${OMP_PACKAGE} from its own installation: it resolves to ` +
70
+ `${resolved}, outside ${root}. Refusing Bun's ambient auto-install cache prevents a different harness build ` +
71
+ "and native addon from standing in for the operator's install. Run `omp-conductor setup host` to verify the " +
72
+ "worker binding, or install the harness alongside omp-conductor."
73
+ );
74
+ }
34
75
 
35
76
  /**
36
77
  * The only session surface the dispatcher is allowed to know about: send one
@@ -73,6 +114,39 @@ export interface AgentSessionLike {
73
114
  * downgrade, instead of it vanishing and a run just reading dumber.
74
115
  */
75
116
  modelFallbackMessage?: string;
117
+ /**
118
+ * The code-graph session observation (#726): whether the graph MCP tools
119
+ * were in this session's registry, as a one-shot async fact. The harness
120
+ * tool registry settles late in a session's life (MCP wiring finalises
121
+ * after creation), so the in-process session under {@link createLocalSession}
122
+ * exposes this promise; the session-host child relays the resolved value
123
+ * over the wire, and the proxy session {@link createSession} exposes the
124
+ * settled observation as {@link graphTools}. Resolving `undefined` means no
125
+ * observation could be recorded (surface absent or never readable) — never
126
+ * "graph tools absent", which is the `present: false` truth value.
127
+ */
128
+ graphToolsReady?: Promise<GraphToolsObservation | undefined>;
129
+ /**
130
+ * The code-graph session observation (#726): whether the graph MCP tools
131
+ * were in this session's registry. Set on the proxy session once the child's
132
+ * relayed observation lands. Absent means no observation was recorded — never
133
+ * "graph tools absent", which is the `present: false` truth value.
134
+ */
135
+ graphTools?: GraphToolsObservation;
136
+ /**
137
+ * The version of the omp-conductor module this session process actually
138
+ * LOADED, attested by the live process at session start (#832): the module
139
+ * tree this session's tools execute out of, read from the package this
140
+ * process resolved — never from the package files on disk after the fact.
141
+ * Absent only when the version cannot be read (a hand-built fake, a
142
+ * packaging oddity) — absence is not "the newest", and an upgrade that
143
+ * cannot see the loaded version must not claim the session reloaded.
144
+ */
145
+ extensionVersion?: string;
146
+ /** Absolute entry path of the harness module this session process loaded. */
147
+ harnessPath?: string;
148
+ /** Version read from the package owning {@link harnessPath}. */
149
+ harnessVersion?: string;
76
150
  }
77
151
 
78
152
  /**
@@ -216,15 +290,28 @@ export async function createLocalSession(opts: {
216
290
  */
217
291
  requireYieldTool?: boolean;
218
292
  }): Promise<AgentSessionLike> {
293
+ let harnessPath: string;
294
+ try {
295
+ // An explicit Bun-supported root is the resolver contract. A bare import
296
+ // from this same bound module was observed selecting the worker's ambient
297
+ // auto-install cache even while the peer beside it was readable (#828).
298
+ harnessPath = resolveHarnessEntry(import.meta.dir);
299
+ } catch (cause) {
300
+ throw new Error(
301
+ `omp-conductor could not resolve its peer dependency ${OMP_PACKAGE} from ${import.meta.dir}. Install it alongside omp-conductor (it is deliberately not bundled, so the dispatcher runs the same harness build as the operator).`,
302
+ { cause },
303
+ );
304
+ }
305
+ const stray = harnessOutsideInstall(import.meta.dir, () => harnessPath);
306
+ if (stray !== undefined) throw new Error(stray);
219
307
  let loaded: unknown;
220
308
  try {
221
- // Dynamic import is load-bearing, not laziness: the harness is an optional
222
- // peer dependency that is absent when this package is type-checked or
223
- // published, so a static import would fail the build it must survive.
224
- loaded = await import(OMP_PACKAGE as string);
309
+ // Dynamic import is load-bearing: the runtime-selected absolute path pins
310
+ // the operator's peer without making it a build-time dependency.
311
+ loaded = await import(harnessPath);
225
312
  } catch (cause) {
226
313
  throw new Error(
227
- `omp-conductor could not load its peer dependency ${OMP_PACKAGE}. Install it alongside omp-conductor (it is deliberately not bundled, so the dispatcher runs the same harness build as the operator).`,
314
+ `omp-conductor could not load its peer dependency ${OMP_PACKAGE} from ${harnessPath}. Install it alongside omp-conductor (it is deliberately not bundled, so the dispatcher runs the same harness build as the operator).`,
228
315
  { cause },
229
316
  );
230
317
  }
@@ -329,6 +416,19 @@ export async function createLocalSession(opts: {
329
416
  const modelFallbackMessage =
330
417
  typeof fallback === "string" && fallback !== "" ? fallback : undefined;
331
418
 
419
+ // The code-graph session observation (#726): whether the graph MCP tools are
420
+ // in this session's registry. The harness tool registry is structural and
421
+ // settles late — MCP wiring finalises through a deferred discovery pass after
422
+ // `createAgentSession` resolves, so the read can throw in the creation
423
+ // window, and the graph tools surface under the *enabled* names, never the
424
+ // active ones. `observeGraphTools` polls that surface until it stops
425
+ // throwing, bounded; an absent surface (older harness build) or an
426
+ // unreadable one records no observation — a registry answer is never
427
+ // guessed. The observation resolves asynchronously so the session handshake
428
+ // never waits for it.
429
+ const sessionTools = raw as { getEnabledToolNames?: () => string[] };
430
+ const graphToolsReady = observeGraphTools(sessionTools.getEnabledToolNames?.bind(sessionTools));
431
+
332
432
  // One real subscription fanned out per event type, so N `on()` calls cost one
333
433
  // listener on the harness stream and unknown event types cost nothing.
334
434
  const handlers = new Map<string, ((e: unknown) => void)[]>();
@@ -339,6 +439,7 @@ export async function createLocalSession(opts: {
339
439
  for (const cb of handlers.get("*") ?? []) cb(event);
340
440
  });
341
441
 
442
+ const loadedHarnessVersion = harnessVersion(harnessPath);
342
443
  const session: AgentSessionLike = {
343
444
  prompt: (text, promptOpts) => raw.prompt(text, promptOpts),
344
445
  on(event, cb) {
@@ -354,6 +455,12 @@ export async function createLocalSession(opts: {
354
455
  raw.abort({ goalReason: "interrupted", reason: "operator pause" }),
355
456
  ).then(() => undefined);
356
457
  },
458
+ // The loaded-module attestation (#832): this session process executes the
459
+ // conductor module it resolved at start; the version is a property of this
460
+ // process's loaded tree, deliberately not of what is on disk now.
461
+ ...(conductorModuleVersion() === undefined ? {} : { extensionVersion: conductorModuleVersion() }),
462
+ harnessPath,
463
+ ...(loadedHarnessVersion === undefined ? {} : { harnessVersion: loadedHarnessVersion }),
357
464
  // The path the session actually opened, never one we asked for: the
358
465
  // arm/monitor tooling reads this file as proof of activity, so a path
359
466
  // nothing ever writes to is worse than no path at all.
@@ -361,6 +468,7 @@ export async function createLocalSession(opts: {
361
468
  return raw.sessionFile;
362
469
  },
363
470
  ...(modelFallbackMessage === undefined ? {} : { modelFallbackMessage }),
471
+ graphToolsReady,
364
472
  };
365
473
 
366
474
  disposers.set(session, async () => {
@@ -439,6 +547,28 @@ function asRawSession(created: unknown): RawSession {
439
547
  /** Absolute path of the child entrypoint, resolved against this package. */
440
548
  export const SESSION_HOST = join(import.meta.dir, "session-host.ts");
441
549
 
550
+ /**
551
+ * The version of the omp-conductor package this *process* loaded — the module
552
+ * tree this process's session machinery actually executes, read from the
553
+ * package.json next to `import.meta.dir` (#832). Read once per process and
554
+ * cached: the upgrade's session-reload proof compares this against the target
555
+ * release to tell a genuinely reloaded OMP process from a package install the
556
+ * live session never picked up. `undefined` when the file cannot be read —
557
+ * absence is a missing attestation, never "the newest".
558
+ */
559
+ let loadedConductorVersion: string | undefined;
560
+ export function conductorModuleVersion(): string | undefined {
561
+ if (loadedConductorVersion !== undefined) return loadedConductorVersion;
562
+ try {
563
+ const raw = readFileSync(join(import.meta.dir, "..", "package.json"), "utf8");
564
+ const parsed = JSON.parse(raw) as { version?: unknown };
565
+ loadedConductorVersion = typeof parsed.version === "string" ? parsed.version : undefined;
566
+ } catch {
567
+ loadedConductorVersion = undefined;
568
+ }
569
+ return loadedConductorVersion;
570
+ }
571
+
442
572
  /**
443
573
  * How long the parent waits for a child to connect and report `ready`.
444
574
  *
@@ -625,8 +755,9 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
625
755
  "daemon shutdown began while the session socket was binding",
626
756
  );
627
757
  }
628
- // The socket is the run's own channel, and the daemon's own uid is the only
629
- // one that speaks on it.
758
+ // The socket is the run's own channel, and this process's own uid the
759
+ // fleet account every session runs as (#894) — is the only one that speaks
760
+ // on it.
630
761
  chmodSync(socketPath, 0o600);
631
762
 
632
763
  const spec: SessionHostSpec = {
@@ -646,14 +777,23 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
646
777
  };
647
778
 
648
779
  const log = opts.onChildLog ?? ((line: string) => process.stderr.write(`${line}\n`));
649
- const argv = [process.execPath, opts.hostModule ?? SESSION_HOST, JSON.stringify(spec)];
780
+ const hostModule = opts.hostModule ?? SESSION_HOST;
781
+ // The child is the same installed tree this process runs from, launched
782
+ // under the fleet account (#894). `--no-install` keeps Bun off its ambient
783
+ // auto-install cache, so the peer and its native addon resolve from the
784
+ // operator's exact install or fail loudly.
785
+ const argv = [process.execPath, "--no-install", hostModule, JSON.stringify(spec)];
786
+ // The session's own role is stamped on the child, so a process the agent
787
+ // runs — `omp-conductor report` from its sandbox — can tell a worker session
788
+ // from the operator's shell. Direct CLI runs outside a spawned session
789
+ // inherit nothing and stay the orchestrator surface.
790
+ let env: Record<string, string | undefined> | undefined;
791
+ if (opts.role !== undefined) {
792
+ env = { ...process.env, [SESSION_ROLE_ENV]: opts.role };
793
+ }
650
794
  const child = Bun.spawn(argv, {
651
795
  cwd: opts.cwd,
652
- // Stamp the session's own role on the child, so a process the agent runs —
653
- // `omp-conductor report` from its sandbox — can tell a worker session from
654
- // the operator's shell. Direct CLI runs outside a spawned session inherit
655
- // nothing and stay the orchestrator surface.
656
- env: opts.role === undefined ? undefined : { ...process.env, [SESSION_ROLE_ENV]: opts.role },
796
+ env,
657
797
  stdin: "ignore",
658
798
  // The harness writes progress to stdout; both streams are the daemon's log,
659
799
  // never the protocol. The protocol has its own socket precisely so a chatty
@@ -783,6 +923,10 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
783
923
  const pending = new Map<number, { resolve: () => void; reject: (err: Error) => void }>();
784
924
  let sessionFile: string | undefined;
785
925
  let modelFallbackMessage: string | undefined;
926
+ let graphTools: GraphToolsObservation | undefined;
927
+ let extensionVersion: string | undefined;
928
+ let loadedHarnessPath: string | undefined;
929
+ let loadedHarnessVersion: string | undefined;
786
930
  let promptSeq = 0;
787
931
 
788
932
  const { promise: ready, resolve: onReady, reject: onReadyFail } = Promise.withResolvers<void>();
@@ -886,8 +1030,14 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
886
1030
  case "ready":
887
1031
  sessionFile = message.sessionFile;
888
1032
  modelFallbackMessage = message.modelFallbackMessage;
1033
+ extensionVersion = message.extensionVersion;
1034
+ loadedHarnessPath = message.harnessPath;
1035
+ loadedHarnessVersion = message.harnessVersion;
889
1036
  onReady();
890
1037
  break;
1038
+ case "graph-tools":
1039
+ graphTools = message.graphTools;
1040
+ break;
891
1041
  case "start-error":
892
1042
  fail(message.message);
893
1043
  break;
@@ -981,6 +1131,18 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
981
1131
  get modelFallbackMessage() {
982
1132
  return modelFallbackMessage;
983
1133
  },
1134
+ get graphTools() {
1135
+ return graphTools;
1136
+ },
1137
+ get extensionVersion() {
1138
+ return extensionVersion;
1139
+ },
1140
+ get harnessPath() {
1141
+ return loadedHarnessPath;
1142
+ },
1143
+ get harnessVersion() {
1144
+ return loadedHarnessVersion;
1145
+ },
984
1146
  };
985
1147
 
986
1148
  disposers.set(session, async () => {