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.
- package/REFERENCE.md +10 -1
- package/agents/to-spec.md +76 -9
- package/package.json +1 -1
- package/schema/config.schema.json +4 -0
- package/src/arm-challenge.ts +204 -85
- package/src/ask.ts +130 -615
- package/src/board.ts +7 -1
- package/src/brief-upgrade.ts +24 -0
- package/src/briefs/console.md +253 -0
- package/src/briefs/correction.md +203 -0
- package/src/briefs/orchestrator.md +167 -97
- package/src/briefs/policy.md +19 -16
- package/src/briefs/to-spec.md +76 -9
- package/src/briefs/worker.md +50 -16
- package/src/cli.ts +4 -0
- package/src/command-manifest.ts +54 -8
- package/src/commands/arm.ts +113 -49
- package/src/commands/console.ts +70 -0
- package/src/commands/context.ts +2 -0
- package/src/commands/epic.ts +132 -0
- package/src/commands/extend.ts +9 -1
- package/src/commands/intake.ts +44 -14
- package/src/commands/stats.ts +19 -4
- package/src/commands/worker.ts +9 -1
- package/src/config-schema.ts +13 -0
- package/src/config.ts +27 -0
- package/src/daemon/ack.ts +159 -0
- package/src/daemon/admission-pass.ts +135 -0
- package/src/daemon/brief.ts +461 -0
- package/src/daemon/deps.ts +539 -0
- package/src/daemon/dispatch.ts +1779 -0
- package/src/daemon/drain.ts +185 -0
- package/src/daemon/groom-pass.ts +412 -0
- package/src/daemon/http.ts +417 -0
- package/src/daemon/integrity.ts +108 -0
- package/src/daemon/panes.ts +180 -0
- package/src/daemon/review.ts +1888 -0
- package/src/daemon/runtime.ts +736 -0
- package/src/daemon/settle-pass.ts +589 -0
- package/src/daemon/supervision.ts +438 -0
- package/src/daemon/tick.ts +968 -0
- package/src/daemon/views.ts +751 -0
- package/src/daemon.ts +105 -7923
- package/src/dashboard/app.js +58 -0
- package/src/dashboard/controls.ts +22 -3
- package/src/dashboard/server.ts +4 -0
- package/src/diff-flags.ts +24 -3
- package/src/failure-class.ts +75 -1
- package/src/fleet.ts +290 -164
- package/src/groom.ts +461 -0
- package/src/http-token.ts +142 -0
- package/src/knowledge.ts +229 -0
- package/src/mining.ts +316 -0
- package/src/orchestrator-tick.ts +428 -1681
- package/src/ready-gate.ts +267 -0
- package/src/settlement.ts +72 -6
- package/src/setup-host.ts +32 -9
- package/src/setup-wizard.ts +55 -7
- package/src/setup.ts +229 -3
- package/src/stats.ts +257 -2
- package/src/status-render.ts +158 -7
- package/src/store.ts +604 -26
- package/src/to-spec.ts +194 -21
- package/src/tracker/github.ts +50 -0
- package/src/types.ts +416 -15
- package/src/verbs/protocol.ts +28 -0
- package/src/verbs/server.ts +330 -39
- package/src/wake.ts +19 -2
- package/src/worker.ts +456 -1
package/src/dashboard/app.js
CHANGED
|
@@ -574,6 +574,62 @@ function orDash(value, format) {
|
|
|
574
574
|
return value === null || value === undefined ? "—" : format(value);
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
+
/**
|
|
578
|
+
* The attribution block (phase 4): where the spend went, as opposed to what it
|
|
579
|
+
* bought. Rendered in the empty case too — a dry queue is exactly when the
|
|
580
|
+
* daemon's own grooming sessions run, and that spend bought no outcome, which
|
|
581
|
+
* is the number an operator most wants to see.
|
|
582
|
+
*/
|
|
583
|
+
function renderAttribution(out, report) {
|
|
584
|
+
const models = report.models ?? [];
|
|
585
|
+
if (models.length > 0) {
|
|
586
|
+
const box = el("div", "chart");
|
|
587
|
+
box.appendChild(el("h4", undefined, "Per model (grouped on the model actually billed)"));
|
|
588
|
+
const rows = el("div", "summary");
|
|
589
|
+
for (const m of models) {
|
|
590
|
+
// Nothing metered at all is stated as unknown: "$0.00 metered" is true and
|
|
591
|
+
// still reads as free, which is the misreading this panel exists to
|
|
592
|
+
// prevent — the same rule the CLI's human form follows.
|
|
593
|
+
const cost =
|
|
594
|
+
m.spendUsd === 0 && m.unmeteredRuns === m.runs
|
|
595
|
+
? "cost unknown — no run on this model metered"
|
|
596
|
+
: `${usd(m.spendUsd)} metered · ${orDash(m.spendPerMerge, usd)} / merge` +
|
|
597
|
+
(m.unmeteredRuns > 0 ? ` · ${m.unmeteredRuns} unmetered` : "");
|
|
598
|
+
rows.appendChild(statLine(m.model, `${m.runs} run(s) · ${m.merges} merge(s) · ${cost}`));
|
|
599
|
+
}
|
|
600
|
+
box.appendChild(rows);
|
|
601
|
+
out.appendChild(box);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const ran = (report.sessions ?? []).filter((s) => s.sessions > 0);
|
|
605
|
+
const unmeteredRoles = report.unmeteredRoles ?? [];
|
|
606
|
+
// Silence rather than a row of zeros: "no grooming ran" and "grooming cost
|
|
607
|
+
// nothing" are different claims, and only the first would be true.
|
|
608
|
+
if (ran.length === 0 && unmeteredRoles.length === 0) return;
|
|
609
|
+
const box = el("div", "chart");
|
|
610
|
+
box.appendChild(el("h4", undefined, "Daemon-owned sessions (not worker runs)"));
|
|
611
|
+
const rows = el("div", "summary");
|
|
612
|
+
for (const s of ran) {
|
|
613
|
+
// An unmetered session's cost is unknown, so it is stated as a count and
|
|
614
|
+
// never folded into the dollar figure as a zero.
|
|
615
|
+
const cost =
|
|
616
|
+
s.spendUsd === 0 && s.unmeteredSessions === s.sessions
|
|
617
|
+
? "cost unknown — no session metered"
|
|
618
|
+
: `${usd(s.spendUsd)} metered` +
|
|
619
|
+
(s.unmeteredSessions > 0
|
|
620
|
+
? ` · ${s.unmeteredSessions} unmetered (cost unknown, not $0.00)`
|
|
621
|
+
: "");
|
|
622
|
+
rows.appendChild(statLine(s.role, `${s.sessions} session(s) · ${s.turns} turn(s) · ${cost}`));
|
|
623
|
+
}
|
|
624
|
+
for (const role of unmeteredRoles) {
|
|
625
|
+
rows.appendChild(
|
|
626
|
+
statLine(role, "unmetered — no usage events observable, so the cost is unknown"),
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
box.appendChild(rows);
|
|
630
|
+
out.appendChild(box);
|
|
631
|
+
}
|
|
632
|
+
|
|
577
633
|
function hours(ms) {
|
|
578
634
|
return `${(ms / 3_600_000).toFixed(1)}h`;
|
|
579
635
|
}
|
|
@@ -594,6 +650,7 @@ async function refreshStats() {
|
|
|
594
650
|
),
|
|
595
651
|
);
|
|
596
652
|
out.appendChild(statLine("GitHub API calls", String(report.ghCalls)));
|
|
653
|
+
renderAttribution(out, report);
|
|
597
654
|
return;
|
|
598
655
|
}
|
|
599
656
|
|
|
@@ -657,6 +714,7 @@ async function refreshStats() {
|
|
|
657
714
|
(v) => String(v),
|
|
658
715
|
),
|
|
659
716
|
);
|
|
717
|
+
renderAttribution(out, report);
|
|
660
718
|
}
|
|
661
719
|
|
|
662
720
|
statsWindowBar.addEventListener("click", (event) => {
|
|
@@ -47,6 +47,7 @@ import { makeTracker } from "../tracker/github.ts";
|
|
|
47
47
|
import { dbPath, openStore } from "../store.ts";
|
|
48
48
|
import { loadConfig, resolveCaps } from "../config.ts";
|
|
49
49
|
import { acknowledgeUpgradeRecovery } from "../upgrade-verify.ts";
|
|
50
|
+
import { httpAuthHeader, missingHttpTokenMessage } from "../http-token.ts";
|
|
50
51
|
import type { ProjectConfig } from "../types.ts";
|
|
51
52
|
|
|
52
53
|
/** The source string every dashboard mutation attributes itself with. */
|
|
@@ -265,8 +266,24 @@ export function dashboardAnswerDecision(id: string, body: unknown, d: ControlDep
|
|
|
265
266
|
|
|
266
267
|
// ------------------------------------------------------- production wiring --
|
|
267
268
|
|
|
268
|
-
/**
|
|
269
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Forward to the living daemon's own HTTP surface, or report there is none.
|
|
271
|
+
*
|
|
272
|
+
* Every route this forwards to is mutating, so the request carries the
|
|
273
|
+
* daemon's bearer token (Phase 4). Note this is deliberately *not* the
|
|
274
|
+
* dashboard's own token: the browser authenticated to the dashboard, and the
|
|
275
|
+
* dashboard authenticates to the daemon as a second, separately-scoped hop.
|
|
276
|
+
* A missing daemon token is its own answer rather than the 502 "no living
|
|
277
|
+
* daemon" — the daemon may be perfectly alive; what is missing is the
|
|
278
|
+
* credential file, and telling the operator "start the daemon" when it is
|
|
279
|
+
* already running would send them chasing the wrong thing.
|
|
280
|
+
*
|
|
281
|
+
* Exported so the suite can drive the *production* forwarder — the one that
|
|
282
|
+
* actually builds the request — against a stub daemon. Every other test injects
|
|
283
|
+
* `ControlDeps.proxy`, which by construction cannot prove this function sends
|
|
284
|
+
* the credential.
|
|
285
|
+
*/
|
|
286
|
+
export async function defaultProxy(
|
|
270
287
|
project: ProjectConfig,
|
|
271
288
|
path: string,
|
|
272
289
|
body: unknown,
|
|
@@ -277,13 +294,15 @@ async function defaultProxy(
|
|
|
277
294
|
// would apply an operator's action to the wrong fleet.
|
|
278
295
|
if (record === undefined) return undefined;
|
|
279
296
|
if (record.project !== undefined && record.project !== project.name) return undefined;
|
|
297
|
+
const auth = httpAuthHeader();
|
|
298
|
+
if (auth === undefined) return { status: 503, body: { error: missingHttpTokenMessage() } };
|
|
280
299
|
// Cheap liveness first, so an unreachable daemon is a 502 rather than a
|
|
281
300
|
// mutation attempt that hangs the browser.
|
|
282
301
|
if (!(await healthCheck(record.port)).ok) return undefined;
|
|
283
302
|
try {
|
|
284
303
|
const res = await fetch(`http://127.0.0.1:${record.port}${path}`, {
|
|
285
304
|
method: "PUT",
|
|
286
|
-
headers: { "content-type": "application/json" },
|
|
305
|
+
headers: { "content-type": "application/json", ...auth },
|
|
287
306
|
body: JSON.stringify(body),
|
|
288
307
|
signal: AbortSignal.timeout(10_000),
|
|
289
308
|
});
|
package/src/dashboard/server.ts
CHANGED
|
@@ -331,6 +331,10 @@ async function statsProducer(name: string, since: string): Promise<StatsReport |
|
|
|
331
331
|
window,
|
|
332
332
|
ghCalls: store.ghCallsBetween(window.sinceDay, window.untilDay),
|
|
333
333
|
runs: store.statsRuns(name, window.sinceEpochMs),
|
|
334
|
+
// Phase 4 attribution: daemon-owned session spend lives in its own table,
|
|
335
|
+
// deliberately outside `runs`, so it is a second read rather than more
|
|
336
|
+
// rows in the first one.
|
|
337
|
+
sessions: store.sessionSpendSince(name, window.sinceEpochMs),
|
|
334
338
|
});
|
|
335
339
|
} finally {
|
|
336
340
|
store.close();
|
package/src/diff-flags.ts
CHANGED
|
@@ -813,14 +813,35 @@ function claimRegion(body: string): string {
|
|
|
813
813
|
return next === null ? rest : rest.slice(0, marker[0].length + next.index);
|
|
814
814
|
}
|
|
815
815
|
|
|
816
|
+
/**
|
|
817
|
+
* An executable token: the shape a claimed command's first word must have to
|
|
818
|
+
* be a program somebody could have run. Either an ordinary command name — an
|
|
819
|
+
* ASCII letter or `_` first, then letters, digits, `_`, `.`, `+`, `-` (`bun`,
|
|
820
|
+
* `bash`, `cd`, `export`, `omp-conductor`, `python3.12`) — or a `./`-relative
|
|
821
|
+
* executable path built from those same characters plus `/`
|
|
822
|
+
* (`./scripts/deploy.sh`).
|
|
823
|
+
*
|
|
824
|
+
* Nothing else is a command, and that exclusion is the whole point (#1039):
|
|
825
|
+
* this predicate used to accept any first word that merely lacked a `/`, so
|
|
826
|
+
* the malformed-JSON fixture `{not json` described in a PR's verified prose
|
|
827
|
+
* became a "claimed" command with no transcript match, and a green PR settled
|
|
828
|
+
* with a false `claimed-proof-missing`. The grammar therefore refuses
|
|
829
|
+
* punctuation-led fragments (`{not`, `{"kind":`, `[1,`), quote-led values
|
|
830
|
+
* (`"some value"`), tokens carrying `:` or `=` (config expressions like
|
|
831
|
+
* `retry.modelFallback: true`, env assignments), digit-led data literals
|
|
832
|
+
* (timestamps like `2026-08-24 09:14Z`) and flag-led spans (`--json`). An
|
|
833
|
+
* absolute path stays excluded as it always was: it carries `/` without the
|
|
834
|
+
* leading `./`.
|
|
835
|
+
*/
|
|
836
|
+
const EXECUTABLE_TOKEN = /^(?:[A-Za-z_][A-Za-z0-9_.+-]*|\.\/[A-Za-z0-9_.+\-/]+)$/;
|
|
837
|
+
|
|
816
838
|
/** One backticked span that reads as a whole command: at least two shell words
|
|
817
|
-
* and a first word that is an
|
|
839
|
+
* and a first word that is an {@link EXECUTABLE_TOKEN}.
|
|
818
840
|
* A span that names a single file or a config key is not a command claim. */
|
|
819
841
|
function claimedCommand(span: string): boolean {
|
|
820
842
|
const words = span.split(/\s+/).filter((word) => word.length > 0);
|
|
821
843
|
if (words.length < 2) return false;
|
|
822
|
-
|
|
823
|
-
return first.startsWith("./") || !first.includes("/");
|
|
844
|
+
return EXECUTABLE_TOKEN.test(words[0] ?? "");
|
|
824
845
|
}
|
|
825
846
|
|
|
826
847
|
/**
|
package/src/failure-class.ts
CHANGED
|
@@ -62,6 +62,16 @@ const INFRA_CHECK_STATES: Record<string, true> = {
|
|
|
62
62
|
/** States that mean "this check has a verdict and it is good". */
|
|
63
63
|
const SUCCESS_CHECK_STATES: Record<string, true> = { success: true, neutral: true };
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The runner's own sentence for "the agent under this job went away": written
|
|
67
|
+
* when the runner service is stopped, auto-updates, or is torn down under a
|
|
68
|
+
* live job. Named once because two different accountings quote it — the
|
|
69
|
+
* implementation-attempt waiver below ({@link INFRA_LOG_SIGNATURES}, #177) and
|
|
70
|
+
* the review-correction waiver ({@link runnerInfraFailure}) — and a second
|
|
71
|
+
* spelling of it would waive one while charging the other.
|
|
72
|
+
*/
|
|
73
|
+
const RUNNER_SHUTDOWN_SIGNAL = "the runner has received a shutdown signal";
|
|
74
|
+
|
|
65
75
|
/**
|
|
66
76
|
* Substrings in a failed check's log that prove the failure was infrastructure,
|
|
67
77
|
* not the diff (#177). Each is a registry/docker/runner fault a worker cannot
|
|
@@ -87,7 +97,7 @@ const INFRA_LOG_SIGNATURES = [
|
|
|
87
97
|
// the runner's, and must not be waived (#637, #639).
|
|
88
98
|
"response status code does not indicate success: 429 (too many requests)",
|
|
89
99
|
"failed to resolve source metadata for",
|
|
90
|
-
|
|
100
|
+
RUNNER_SHUTDOWN_SIGNAL,
|
|
91
101
|
"could not resolve host",
|
|
92
102
|
];
|
|
93
103
|
|
|
@@ -103,6 +113,70 @@ export function infraLogSignature(log: string): string | undefined {
|
|
|
103
113
|
return INFRA_LOG_SIGNATURES.find((signature) => lower.includes(signature));
|
|
104
114
|
}
|
|
105
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Runner-infrastructure wordings: a red check whose own text says the *runner*
|
|
118
|
+
* failed, not the diff (Phase 3, #1043 lane). Matched lowercased as substrings
|
|
119
|
+
* against the failing job's log — the same surface {@link infraLogSignature}
|
|
120
|
+
* reads — and each entry is the real GitHub Actions sentence, verified against
|
|
121
|
+
* the runner's own reports rather than invented:
|
|
122
|
+
*
|
|
123
|
+
* Kept as its own list rather than folded into {@link INFRA_LOG_SIGNATURES}
|
|
124
|
+
* for two reasons. The consumers differ: that list waives an *implementation
|
|
125
|
+
* attempt* for a settled run (#177), this one waives a *correction round* for
|
|
126
|
+
* a live pull request's red check. And {@link infraSignatureVersion} is a
|
|
127
|
+
* persisted cursor fingerprint — growing that list restarts the historical
|
|
128
|
+
* reconciliation, which recognising a runner-lost red has no business doing.
|
|
129
|
+
* The one overlap, {@link RUNNER_SHUTDOWN_SIGNAL}, is shared by reference so
|
|
130
|
+
* the two accountings can never disagree about that wording.
|
|
131
|
+
*/
|
|
132
|
+
const RUNNER_INFRA_SIGNATURES = [
|
|
133
|
+
// "The self-hosted runner: <name> lost communication with the server." and
|
|
134
|
+
// its hosted sibling "The hosted runner: <name> lost communication with the
|
|
135
|
+
// server." — the runner process was killed, starved or cut off mid-job
|
|
136
|
+
// (actions/runner#3539, community#84877, community#173431). The name varies,
|
|
137
|
+
// so only the invariant tail is matched.
|
|
138
|
+
"lost communication with the server",
|
|
139
|
+
// The runner service was stopped or auto-updated under the job: an
|
|
140
|
+
// infrastructure cancellation, not a verdict on the diff.
|
|
141
|
+
RUNNER_SHUTDOWN_SIGNAL,
|
|
142
|
+
// "The job was not acquired by Runner of type hosted even after multiple
|
|
143
|
+
// attempts" — the job never reached a runner at all, so nothing in the diff
|
|
144
|
+
// was ever executed (community#186216, community#165287).
|
|
145
|
+
"was not acquired by runner",
|
|
146
|
+
// "The hosted runner encountered an error while running your job. (Error
|
|
147
|
+
// Type: Failure)." — GitHub's own statement that its runner broke
|
|
148
|
+
// (community#126539). The parenthesised error type varies; the sentence does
|
|
149
|
+
// not.
|
|
150
|
+
"the hosted runner encountered an error while running your job",
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The runner-infrastructure wording a red CI check's text carries, or
|
|
155
|
+
* `undefined` when it carries none.
|
|
156
|
+
*
|
|
157
|
+
* Exists so the review-correction accounting can decline to charge a
|
|
158
|
+
* correction round for a red that the worker's diff did not cause: a lost
|
|
159
|
+
* runner, a runner shut down under the job, or a job no runner ever picked up.
|
|
160
|
+
* The return value is the matched wording, so whatever waives a round can say
|
|
161
|
+
* *which* sentence waived it instead of asserting "infra" unexplained.
|
|
162
|
+
*
|
|
163
|
+
* Fails closed in both directions, and deliberately narrower than it could be:
|
|
164
|
+
*
|
|
165
|
+
* - Unclassifiable, empty or unread text is `undefined` — an unexplained red
|
|
166
|
+
* still counts, and a log-fetch failure hands this function nothing to match,
|
|
167
|
+
* so it never waives.
|
|
168
|
+
* - "The operation was canceled." is *not* here: it is what a failed `needs:`
|
|
169
|
+
* dependency and an operator's own cancel both print, so matching it would
|
|
170
|
+
* waive rounds for ordinary red.
|
|
171
|
+
* - "…has exceeded the maximum execution time of N minutes" is *not* here
|
|
172
|
+
* either: a job that ran until the ceiling is usually a hanging test or an
|
|
173
|
+
* infinite loop, which is exactly the diff's problem to fix.
|
|
174
|
+
*/
|
|
175
|
+
export function runnerInfraFailure(log: string): string | undefined {
|
|
176
|
+
const lower = log.toLowerCase();
|
|
177
|
+
return RUNNER_INFRA_SIGNATURES.find((signature) => lower.includes(signature));
|
|
178
|
+
}
|
|
179
|
+
|
|
106
180
|
/**
|
|
107
181
|
* The cap kills that produced nothing: the worker reached a ceiling with no PR,
|
|
108
182
|
* no observed head and no salvage commit.
|