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
@@ -215,21 +215,40 @@ export function unlinkStaleSocket(path: string): void {
215
215
  }
216
216
 
217
217
  /**
218
- * Restrict a bound socket to the daemon's own uid.
218
+ * Restrict a bound socket to the daemon's own uid, or to the worker identity
219
+ * that owns the channel when `owner` is given.
219
220
  *
220
221
  * Returns the guarantee in force, because #126 asks the daemon to *state* its
221
222
  * mechanism at startup rather than guess: the socket is `0600` under a `0711`
222
223
  * daemon-owned parent, so it is one-user, and the unguessable suffix plus that
223
- * parent are the whole story. Saying so is the difference between an audited
224
- * boundary and a hopeful one.
224
+ * parent are the whole story. Under the worker identity the one user is the
225
+ * worker account the socket is chowned to it, because the child that must
226
+ * connect to it runs as that uid and nothing else does. Saying so is the
227
+ * difference between an audited boundary and a hopeful one.
225
228
  */
226
- export type SocketOwnership = "daemon-user";
229
+ export type SocketOwnership = "daemon-user" | "worker-user";
230
+
231
+ export interface SecureBoundSocketOptions {
232
+ /** The chmod call; injected so tests pin the mode without a real socket. */
233
+ chmod?: (p: string, mode: number) => void;
234
+ /**
235
+ * The uid/gid the socket belongs to (the worker identity for a run
236
+ * channel). Absent, the socket stays daemon-owned and only the daemon's
237
+ * uid can connect.
238
+ */
239
+ owner?: { uid: number; gid: number };
240
+ }
227
241
 
228
242
  export function secureBoundSocket(
229
243
  path: string,
230
- chmod: (p: string, mode: number) => void = chmodSync,
244
+ options: SecureBoundSocketOptions = {},
231
245
  ): SocketOwnership {
246
+ const chmod = options.chmod ?? chmodSync;
232
247
  chmod(path, VERB_SOCKET_MODE);
248
+ if (options.owner !== undefined) {
249
+ chownSync(path, options.owner.uid, options.owner.gid);
250
+ return "worker-user";
251
+ }
233
252
  return "daemon-user";
234
253
  }
235
254
 
package/src/worker.ts CHANGED
@@ -14,7 +14,7 @@ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { createSession, disposeSession, SessionAdmissionClosedError, type AgentSessionLike } from "./omp.ts";
16
16
  import type { GateShape, ReleaseBlockContext } from "./release-policy.ts";
17
- import type { Caps, ResolvedGrants, RunState } from "./types.ts";
17
+ import type { Caps, GraphToolsObservation, ResolvedGrants, RunState } from "./types.ts";
18
18
 
19
19
  /** Structured evidence fields from the worker's final report. */
20
20
  const PR_URL_PATTERN = /^pr:\s*(https:\/\/github\.com\/\S+\/pull\/\d+)\s*$/im;
@@ -330,6 +330,15 @@ export interface WorkerResult {
330
330
  * is otherwise indistinguishable from a run that was merely unlucky.
331
331
  */
332
332
  modelFallbackMessage?: string;
333
+ /**
334
+ * The code-graph session observation (#726): what the session's own registry
335
+ * held at start, read off the session exactly like `modelFallbackMessage`.
336
+ * Absent means the session surface did not record one — never "graph tools
337
+ * absent", which is the `present: false` truth value. A dispatched run that
338
+ * records it lets the doctor tell "the model ignored a tool it had" from
339
+ * "the tool was missing" apart.
340
+ */
341
+ graphTools?: GraphToolsObservation;
333
342
  }
334
343
 
335
344
  /**
@@ -613,7 +622,7 @@ export async function runWorker(
613
622
  const withSessionFacts = (
614
623
  result: Omit<WorkerResult, ReliabilityKeys>,
615
624
  ): Omit<WorkerResult, ReliabilityKeys> => {
616
- const { sessionFile, modelFallbackMessage } = session;
625
+ const { sessionFile, modelFallbackMessage, graphTools } = session;
617
626
  if (!advisorSpendFolded) {
618
627
  advisorSpendFolded = true;
619
628
  // Advisor turns live in their own `__advisor*.jsonl` and never surface
@@ -634,6 +643,7 @@ export async function runWorker(
634
643
  spendUsd,
635
644
  ...(sessionFile === undefined ? {} : { sessionFile }),
636
645
  ...(modelFallbackMessage === undefined ? {} : { modelFallbackMessage }),
646
+ ...(graphTools === undefined ? {} : { graphTools }),
637
647
  };
638
648
  };
639
649
 
package/src/worktree.ts CHANGED
@@ -27,7 +27,7 @@
27
27
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
28
28
  import { dirname, join } from "node:path";
29
29
 
30
- import { credentialedEnv, scrubUserinfo } from "./gitops.ts";
30
+ import { credentialedEnv, runRepoSafeDirectoryExemption, scrubUserinfo } from "./gitops.ts";
31
31
  import type { RepoTarget } from "./types.ts";
32
32
 
33
33
  /**
@@ -255,7 +255,9 @@ function refreshManagedExclude(worktree: string): void {
255
255
  try {
256
256
  // `--git-common-dir` is relative for linked worktrees; resolve against the
257
257
  // tree so bare-mirror layouts and plain clones both land on info/exclude.
258
- const common = Bun.spawnSync(["git", "rev-parse", "--git-common-dir"], {
258
+ // The tree is worker-owned by the time salvage runs (#798), so the
259
+ // exact-path exemption rides on the argv (#816).
260
+ const common = Bun.spawnSync(["git", ...runRepoSafeDirectoryExemption(worktree), "rev-parse", "--git-common-dir"], {
259
261
  cwd: worktree,
260
262
  stdin: "ignore",
261
263
  stdout: "pipe",
@@ -640,11 +642,16 @@ export async function salvageWip(
640
642
  // and dies with the next `worktree remove --force` (#44).
641
643
  refreshManagedExclude(worktree);
642
644
 
643
- if ((await git(["status", "--porcelain"], worktree)) === "") return { kind: "nothing" };
645
+ // The tree is worker-owned by the time salvage runs (#798): every daemon
646
+ // git call against it carries the exact per-run ownership exemption —
647
+ // never a wildcard, never a global config entry (#816).
648
+ const exempt = runRepoSafeDirectoryExemption(worktree);
649
+
650
+ if ((await git([...exempt, "status", "--porcelain"], worktree)) === "") return { kind: "nothing" };
644
651
 
645
652
  // The tree's own branch, not one the caller believes it should be on: this
646
653
  // string ends up in an escalation as the place to go looking.
647
- const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktree);
654
+ const branch = await git([...exempt, "rev-parse", "--abbrev-ref", "HEAD"], worktree);
648
655
 
649
656
  // `-A` on purpose: the losses this exists for were mostly *new* files.
650
657
  //
@@ -660,13 +667,13 @@ export async function salvageWip(
660
667
  // A literal `:(exclude)<path>` was also an outright bug: git counts it as
661
668
  // naming the path, so an already-ignored file made `add` exit 1 and every
662
669
  // cap-kill would have reported a salvage *failure*.
663
- await git(["add", "-A"], worktree);
670
+ await git([...exempt, "add", "-A"], worktree);
664
671
 
665
672
  // The dirty check above ran before git applied its ignores, so a tree whose
666
673
  // only changes were ignored scratch had work by that test and none by this.
667
674
  // Without this, `commit` exits non-zero on an empty index and a tree
668
675
  // holding nothing worth keeping gets reported as a salvage *failure*.
669
- const cached = await git(["diff", "--cached", "--name-status"], worktree);
676
+ const cached = await git([...exempt, "diff", "--cached", "--name-status"], worktree);
670
677
  if (cached === "") {
671
678
  return { kind: "nothing" };
672
679
  }
@@ -674,6 +681,7 @@ export async function salvageWip(
674
681
  const msg = salvageCommitMessage(issue, attempt, ending, files, newPaths);
675
682
  await git(
676
683
  [
684
+ ...exempt,
677
685
  ...SALVAGE_COMMIT_CONFIG,
678
686
  "commit",
679
687
  "--no-verify",
@@ -684,7 +692,7 @@ export async function salvageWip(
684
692
  ],
685
693
  worktree,
686
694
  );
687
- const sha = await git(["rev-parse", "HEAD"], worktree);
695
+ const sha = await git([...exempt, "rev-parse", "HEAD"], worktree);
688
696
  const salvaged = { kind: "salvaged" as const, sha, branch, files, newPaths };
689
697
 
690
698
  if (branch === "HEAD") {
@@ -784,7 +792,12 @@ async function repairAlternates(
784
792
  worktreePath: string,
785
793
  mirrorPath: string,
786
794
  ): Promise<{ kind: "ok" } | { kind: "quarantine"; detail: string }> {
787
- const common = await runGit(["rev-parse", "--git-common-dir"], worktreePath);
795
+ // The tree is worker-owned by cleanup time (#798); the exact-path exemption
796
+ // makes this read the daemon's own (#816).
797
+ const common = await runGit(
798
+ [...runRepoSafeDirectoryExemption(worktreePath), "rev-parse", "--git-common-dir"],
799
+ worktreePath,
800
+ );
788
801
  if (common.code !== 0) {
789
802
  return {
790
803
  kind: "quarantine",
@@ -868,11 +881,15 @@ export async function cleanupRetainedWorktree(
868
881
  };
869
882
  }
870
883
 
871
- const dirty = await git(["status", "--porcelain"], worktreePath);
884
+ // The tree is worker-owned by cleanup time (#798): every daemon git call
885
+ // against it carries the exact per-run exemption (#816).
886
+ const exempt = runRepoSafeDirectoryExemption(worktreePath);
887
+
888
+ const dirty = await git([...exempt, "status", "--porcelain"], worktreePath);
872
889
  if (dirty !== "") {
873
890
  return { kind: "retained", reason: "dirty", detail: "worktree has uncommitted changes" };
874
891
  }
875
- const actual = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath);
892
+ const actual = await git([...exempt, "rev-parse", "--abbrev-ref", "HEAD"], worktreePath);
876
893
  if (actual !== branch) {
877
894
  return {
878
895
  kind: "retained",
@@ -907,7 +924,7 @@ export async function cleanupRetainedWorktree(
907
924
  if (existsSync(worktreePath)) {
908
925
  try {
909
926
  await git(
910
- ["fetch", "--no-tags", mirrorPath, "+refs/remotes/origin/*:refs/remotes/origin/*"],
927
+ [...runRepoSafeDirectoryExemption(worktreePath), "fetch", "--no-tags", mirrorPath, "+refs/remotes/origin/*:refs/remotes/origin/*"],
911
928
  worktreePath,
912
929
  );
913
930
  } catch (err) {
@@ -917,7 +934,7 @@ export async function cleanupRetainedWorktree(
917
934
  detail: `the run repository's remote refs could not be read: ${err instanceof Error ? err.message : String(err)}`,
918
935
  };
919
936
  }
920
- const runUnique = await git(["rev-list", ref, "--not", "--remotes"], worktreePath);
937
+ const runUnique = await git([...runRepoSafeDirectoryExemption(worktreePath), "rev-list", ref, "--not", "--remotes"], worktreePath);
921
938
  if (runUnique !== "") {
922
939
  return {
923
940
  kind: "retained",