omp-conductor 0.19.7 → 0.20.0

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 (69) hide show
  1. package/REFERENCE.md +10 -1
  2. package/agents/to-spec.md +76 -9
  3. package/package.json +1 -1
  4. package/schema/config.schema.json +4 -0
  5. package/src/arm-challenge.ts +204 -85
  6. package/src/ask.ts +130 -615
  7. package/src/board.ts +7 -1
  8. package/src/brief-upgrade.ts +24 -0
  9. package/src/briefs/console.md +253 -0
  10. package/src/briefs/correction.md +203 -0
  11. package/src/briefs/orchestrator.md +167 -97
  12. package/src/briefs/policy.md +19 -16
  13. package/src/briefs/to-spec.md +76 -9
  14. package/src/briefs/worker.md +50 -16
  15. package/src/cli.ts +4 -0
  16. package/src/command-manifest.ts +54 -8
  17. package/src/commands/arm.ts +113 -49
  18. package/src/commands/console.ts +70 -0
  19. package/src/commands/context.ts +2 -0
  20. package/src/commands/epic.ts +132 -0
  21. package/src/commands/extend.ts +9 -1
  22. package/src/commands/intake.ts +44 -14
  23. package/src/commands/stats.ts +19 -4
  24. package/src/commands/worker.ts +9 -1
  25. package/src/config-schema.ts +13 -0
  26. package/src/config.ts +27 -0
  27. package/src/daemon/ack.ts +159 -0
  28. package/src/daemon/admission-pass.ts +135 -0
  29. package/src/daemon/brief.ts +461 -0
  30. package/src/daemon/deps.ts +539 -0
  31. package/src/daemon/dispatch.ts +1779 -0
  32. package/src/daemon/drain.ts +185 -0
  33. package/src/daemon/groom-pass.ts +412 -0
  34. package/src/daemon/http.ts +417 -0
  35. package/src/daemon/integrity.ts +108 -0
  36. package/src/daemon/panes.ts +180 -0
  37. package/src/daemon/review.ts +1888 -0
  38. package/src/daemon/runtime.ts +736 -0
  39. package/src/daemon/settle-pass.ts +589 -0
  40. package/src/daemon/supervision.ts +438 -0
  41. package/src/daemon/tick.ts +968 -0
  42. package/src/daemon/views.ts +751 -0
  43. package/src/daemon.ts +105 -7923
  44. package/src/dashboard/app.js +58 -0
  45. package/src/dashboard/controls.ts +22 -3
  46. package/src/dashboard/server.ts +4 -0
  47. package/src/diff-flags.ts +24 -3
  48. package/src/failure-class.ts +75 -1
  49. package/src/fleet.ts +290 -164
  50. package/src/groom.ts +461 -0
  51. package/src/http-token.ts +142 -0
  52. package/src/knowledge.ts +229 -0
  53. package/src/mining.ts +316 -0
  54. package/src/orchestrator-tick.ts +428 -1681
  55. package/src/ready-gate.ts +267 -0
  56. package/src/settlement.ts +72 -6
  57. package/src/setup-host.ts +32 -9
  58. package/src/setup-wizard.ts +55 -7
  59. package/src/setup.ts +229 -3
  60. package/src/stats.ts +257 -2
  61. package/src/status-render.ts +158 -7
  62. package/src/store.ts +604 -26
  63. package/src/to-spec.ts +194 -21
  64. package/src/tracker/github.ts +50 -0
  65. package/src/types.ts +416 -15
  66. package/src/verbs/protocol.ts +28 -0
  67. package/src/verbs/server.ts +330 -39
  68. package/src/wake.ts +19 -2
  69. package/src/worker.ts +456 -1
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Worker panes: reconciling the terminal representation of a run against the
3
+ * run rows, and answering who owns a workspace.
4
+ *
5
+ * A pane is a *representation* of a run, not the run itself — the session lives
6
+ * or dies whether or not a pane ever opened. That is exactly why this is its own
7
+ * module: pane reconciliation is a convergence loop with its own bounded
8
+ * reattempts, and the dispatch path must be able to launch, settle and hand back
9
+ * an issue without ever depending on it having converged.
10
+ *
11
+ * `workspaceOwnership` sits here because a pane and a run agree on a workspace
12
+ * or they do not, and all three callers — `handleIssue`, this reconcile, and
13
+ * `runDaemon`'s startup sweep — must ask the question exactly once, the same way.
14
+ */
15
+ import { PANE_REATTEMPT_MAX, reconcileWorkerPanes, type WorkspaceOwnership } from "../fleet.ts";
16
+ import { log } from "../log.ts";
17
+ import type { RunRecord } from "../types.ts";
18
+ import type { Deps } from "./deps.ts";
19
+
20
+ /**
21
+ * Pane re-establishment attempts spent per run, for this daemon process only
22
+ * (#998).
23
+ *
24
+ * Deliberately in memory rather than on the run row: a restart is exactly the
25
+ * event that makes another attempt worth making (Herdr came back), so a fresh
26
+ * process legitimately gets a fresh budget, and giving up never marks a run
27
+ * permanently unrepresentable. Pruned against the live set on every pass, so it
28
+ * cannot outgrow the fleet.
29
+ */
30
+ export const paneAttemptsSpent = new Map<string, number>();
31
+
32
+ /** The store-backed {@link WorkspaceOwnership} adapter (#1035 review): the
33
+ * conductor store is the durable leg of workspace discovery, surviving both
34
+ * daemon and Herdr restarts. One shape for every caller so they cannot
35
+ * disagree about what ownership reads. */
36
+ export function workspaceOwnership(store: Deps["store"]): WorkspaceOwnership {
37
+ return {
38
+ recordedWorkspaces: (project) => store.workerWorkspaceIds(project),
39
+ rememberWorkspace: (project, workspaceId) => store.rememberWorkerWorkspace(project, workspaceId),
40
+ forgetWorkspace: (project, workspaceId) => store.forgetWorkerWorkspace(project, workspaceId),
41
+ };
42
+ }
43
+
44
+ /** Exported for the lifecycle tests (#1035): the pass is pure over the store
45
+ * and the injected Herdr runner, so a test drives it exactly as the tick does. */
46
+ /**
47
+ * Make the worker workspaces agree with the live run set, once per pass
48
+ * (#841, reworked by #1035).
49
+ *
50
+ * Runs on every dispatch pass rather than only at startup, because the thing it
51
+ * repairs — a Herdr restart — is not a conductor event and announces itself
52
+ * nowhere. It is idempotent by construction: a reconciled fleet reports `intact`
53
+ * for every live worker and finds nothing left to close, so repeated passes
54
+ * converge instead of accumulating panes or churning them. This pass owns this
55
+ * project's worker workspace even when no run is live — that is how a settled-out
56
+ * project's stale shells converge to zero and its empty workspace is removed.
57
+ *
58
+ * Nothing here can stop a worker: panes hold followers only; the authoritative
59
+ * child lives in this daemon's process tree and is never signalled.
60
+ */
61
+ export function reconcilePanes(
62
+ d: Pick<Deps, "store" | "herdrRun">,
63
+ project: string,
64
+ log: (message: string) => void,
65
+ ): void {
66
+ const live = d.store.liveRuns(project);
67
+ const liveKeys = new Set(live.map((run) => `${project}\u0000${run.id}`));
68
+ for (const key of [...paneAttemptsSpent.keys()]) {
69
+ if (key.startsWith(`${project}\u0000`) && !liveKeys.has(key)) paneAttemptsSpent.delete(key);
70
+ }
71
+ const attempts = new Map(
72
+ live.map((run) => [run.id, paneAttemptsSpent.get(`${project}\u0000${run.id}`) ?? 0]),
73
+ );
74
+ const result = reconcileWorkerPanes(
75
+ live.map((run) => ({
76
+ runId: run.id,
77
+ issue: run.issue,
78
+ attempt: run.attempt,
79
+ project,
80
+ ...(run.workerPid === undefined ? {} : { pid: run.workerPid }),
81
+ ...(run.paneId === undefined ? {} : { paneId: run.paneId }),
82
+ ...(run.paneLabel === undefined ? {} : { paneLabel: run.paneLabel }),
83
+ ...(run.sessionFile === undefined ? {} : { sessionFile: run.sessionFile }),
84
+ })),
85
+ { ...(d.herdrRun === undefined ? {} : { run: d.herdrRun }), ownership: workspaceOwnership(d.store) },
86
+ attempts,
87
+ PANE_REATTEMPT_MAX,
88
+ [project],
89
+ );
90
+ if (!result.ok) {
91
+ // An unreadable workspace is not evidence that anything is stale, so nothing
92
+ // is released and no run is relabelled — but a fleet that cannot see its own
93
+ // panes says so, on every pass, rather than going quiet.
94
+ log(`worker panes not reconciled: ${result.reason}`);
95
+ return;
96
+ }
97
+ for (const outcome of result.outcomes) {
98
+ switch (outcome.kind) {
99
+ case "intact":
100
+ break;
101
+ case "visual-unreadable":
102
+ // The pane keeps standing: an unreadable follower table is no evidence
103
+ // of death, and a live worker's representation is never destroyed on a
104
+ // guess (#1035 review). Said once per pass, not acted on.
105
+ log(
106
+ `#${runIssue(live, outcome.runId)} herdr pane ${outcome.paneId} visual unreadable, left standing: ${outcome.reason}`,
107
+ );
108
+ break;
109
+ case "reassociated":
110
+ d.store.updateRun(outcome.runId, {
111
+ paneId: outcome.paneId,
112
+ paneLabel: outcome.label,
113
+ paneUnavailable: null,
114
+ });
115
+ log(`#${runIssue(live, outcome.runId)} herdr pane re-associated as ${outcome.paneId} after a restart`);
116
+ break;
117
+ case "untracked": {
118
+ d.store.updateRun(outcome.runId, { paneId: null, paneLabel: null, paneUnavailable: outcome.reason });
119
+ if (outcome.attempted !== true) {
120
+ // Cost no attempt (no recorded pid): say so every pass, as before.
121
+ log(`#${runIssue(live, outcome.runId)} has no herdr pane: ${outcome.reason}`);
122
+ break;
123
+ }
124
+ // #998: a terminal refusing to split will refuse the next one too, and
125
+ // each retry made the pile worse — spend the whole budget at once. The
126
+ // phase is structured now (#1035 review): the old substring sniff died
127
+ // when the reason strings changed and never fired again.
128
+ const key = `${project}\u0000${outcome.runId}`;
129
+ const spent = outcome.phase === "split" ? PANE_REATTEMPT_MAX : (paneAttemptsSpent.get(key) ?? 0) + 1;
130
+ paneAttemptsSpent.set(key, spent);
131
+ // Once per run, at the moment the budget runs out — not once per pass.
132
+ // Four identical lines a minute for #986 is what made the real cause
133
+ // (a leaked pane per attempt) hard to see on 2026-08-23.
134
+ log(
135
+ spent >= PANE_REATTEMPT_MAX
136
+ ? `#${runIssue(live, outcome.runId)} has no herdr pane after ${spent} attempt(s), not retrying: ${outcome.reason}`
137
+ : `#${runIssue(live, outcome.runId)} has no herdr pane (attempt ${spent}/${PANE_REATTEMPT_MAX}): ${outcome.reason}`,
138
+ );
139
+ break;
140
+ }
141
+ case "attempts-exhausted":
142
+ // Already reported when the budget ran out; the run keeps working with
143
+ // no pane, and a later restart gets a fresh budget.
144
+ break;
145
+ case "stale-released": {
146
+ const what =
147
+ outcome.cause === "duplicate"
148
+ ? "closed duplicate herdr pane"
149
+ : outcome.cause === "unidentified"
150
+ ? "closed unidentified herdr pane"
151
+ : "released settled herdr pane";
152
+ log(`${what} ${outcome.paneId} (run ${outcome.runId || "unidentified"}${outcome.cause === "settled" ? " is not live" : ""})`);
153
+ break;
154
+ }
155
+ case "stale-release-failed": {
156
+ const what =
157
+ outcome.cause === "duplicate"
158
+ ? "duplicate herdr pane"
159
+ : outcome.cause === "unidentified"
160
+ ? "unidentified herdr pane"
161
+ : "settled herdr pane";
162
+ log(`${what} ${outcome.paneId} could not be closed: ${outcome.reason}`);
163
+ break;
164
+ }
165
+ case "workspace-removed":
166
+ log(`removed empty herdr worker workspace ${outcome.workspaceId} for ${outcome.project}`);
167
+ break;
168
+ case "workspace-remove-failed":
169
+ log(
170
+ `herdr worker workspace ${outcome.workspaceId} for ${outcome.project} could not be removed: ${outcome.reason}`,
171
+ );
172
+ break;
173
+ }
174
+ }
175
+ }
176
+
177
+ /** The issue a run id belongs to, for a log line a human reads. */
178
+ export function runIssue(live: readonly RunRecord[], runId: string): string {
179
+ return String(live.find((run) => run.id === runId)?.issue ?? "?");
180
+ }