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/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
|
-
|
|
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
|
|
222
|
-
//
|
|
223
|
-
|
|
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
|
|
629
|
-
// one that speaks
|
|
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
|
|
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
|
-
|
|
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 () => {
|