omp-conductor 0.18.2 → 0.19.1
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 +105 -40
- package/REFERENCE.md +865 -30
- package/package.json +1 -1
- package/schema/config.schema.json +26 -0
- package/src/admission.ts +212 -26
- package/src/ask.ts +288 -1
- package/src/briefs/orchestrator.md +6 -5
- package/src/cli.ts +5 -1
- package/src/command-help.ts +9 -1
- package/src/command-manifest.ts +36 -3
- package/src/commands/arm.ts +5 -1
- package/src/commands/context.ts +2 -0
- package/src/commands/message.ts +26 -2
- package/src/commands/reconcile-units.ts +104 -0
- package/src/commands/release-composition.ts +232 -0
- package/src/commands/resume.ts +2 -27
- package/src/commands/setup.ts +101 -16
- package/src/commands/stats.ts +11 -30
- package/src/commands/tail.ts +31 -1
- package/src/commands/upgrade.ts +20 -3
- package/src/commands/verb.ts +2 -1
- package/src/config-schema.ts +19 -0
- package/src/config.ts +80 -0
- package/src/credential-class.ts +366 -0
- package/src/daemon.ts +1218 -288
- package/src/dashboard/app.js +504 -2
- package/src/dashboard/controls.ts +336 -0
- package/src/dashboard/index.html +30 -0
- package/src/dashboard/server.ts +271 -30
- package/src/dashboard/style.css +116 -0
- package/src/dashboard/transcript.ts +173 -0
- package/src/doctor.ts +379 -22
- package/src/failure-class.ts +59 -0
- package/src/fleet.ts +511 -101
- package/src/host.ts +6 -130
- package/src/omp.ts +29 -0
- package/src/orchestrator-tick.ts +343 -88
- package/src/pause.ts +233 -0
- package/src/settlement.ts +159 -2
- package/src/setup-answers.ts +97 -0
- package/src/setup-host.ts +325 -1159
- package/src/setup-install.ts +204 -27
- package/src/setup-wizard.ts +111 -50
- package/src/setup.ts +33 -0
- package/src/spend-telemetry.ts +117 -0
- package/src/stats.ts +35 -0
- package/src/status-render.ts +348 -19
- package/src/store.ts +1229 -55
- package/src/telegram-freshness.ts +269 -0
- package/src/to-spec.ts +27 -0
- package/src/types.ts +697 -4
- package/src/unblock.ts +22 -0
- package/src/unit-reconcile.ts +303 -0
- package/src/upgrade-verify.ts +8 -1
- package/src/upgrade.ts +326 -47
- package/src/verbs/actions.ts +124 -10
- package/src/verbs/protocol.ts +70 -2
- package/src/verbs/server.ts +447 -8
- package/src/wake.ts +48 -0
- package/src/worker.ts +403 -3
package/src/verbs/actions.ts
CHANGED
|
@@ -321,6 +321,46 @@ export function githubVerbActions(
|
|
|
321
321
|
: { ok: true, sha };
|
|
322
322
|
};
|
|
323
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Resolve a caller-pinned release commit and prove it belongs on the released
|
|
326
|
+
* branch (#695).
|
|
327
|
+
*
|
|
328
|
+
* Two questions, both fail-closed. `rev-parse <sha>^{commit}` answers "does
|
|
329
|
+
* this commit exist in the released repository at all" — an abbreviation is
|
|
330
|
+
* expanded here, so what the tag receives is always a full id. `merge-base
|
|
331
|
+
* --is-ancestor` answers the one that matters: a tag pointing at a commit
|
|
332
|
+
* that is not on the released branch is a release of code nobody merged, and
|
|
333
|
+
* that is the failure a decorative `sha` argument would create. The ancestor
|
|
334
|
+
* check accepts the branch tip itself, which is the ordinary case.
|
|
335
|
+
*/
|
|
336
|
+
const pinnedReleaseCommit = async (
|
|
337
|
+
mirror: string,
|
|
338
|
+
repo: RepoTarget,
|
|
339
|
+
sha: string,
|
|
340
|
+
): Promise<CommitOutcome> => {
|
|
341
|
+
const resolved = await readCommit(mirror, ["rev-parse", `${sha}^{commit}`], `pinned commit ${sha}`);
|
|
342
|
+
if (!resolved.ok) {
|
|
343
|
+
return {
|
|
344
|
+
ok: false,
|
|
345
|
+
stderr:
|
|
346
|
+
`refusing release: ${sha} is not a commit in ${repo.name}. ` +
|
|
347
|
+
`Pass a commit that exists on ${repo.defaultBranch}.\n${resolved.stderr}`,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const branchRef = `refs/remotes/origin/${repo.defaultBranch}`;
|
|
351
|
+
const reachable = await git(mirror, ["merge-base", "--is-ancestor", resolved.sha, branchRef]);
|
|
352
|
+
if (!reachable.result.ok) {
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
stderr:
|
|
356
|
+
`refusing release: ${resolved.sha} is not reachable from ${repo.name}'s ${repo.defaultBranch}. ` +
|
|
357
|
+
"A tag must never point at a commit that is not on the released branch — check the sha, or " +
|
|
358
|
+
"wait for the commit to land.",
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return resolved;
|
|
362
|
+
};
|
|
363
|
+
|
|
324
364
|
const releaseTargetMoved = (repo: RepoTarget, target: string, live: string): { ok: false; stderr: string } => ({
|
|
325
365
|
ok: false,
|
|
326
366
|
stderr:
|
|
@@ -636,6 +676,38 @@ export function githubVerbActions(
|
|
|
636
676
|
};
|
|
637
677
|
},
|
|
638
678
|
|
|
679
|
+
// Closing a rejected PR is the one destructive half of an adjudication's
|
|
680
|
+
// disposition (#876), and it is REST rather than `gh pr close` for the same
|
|
681
|
+
// reason `updatePrBranch` is: a selective GraphQL outage must not leave a
|
|
682
|
+
// rejected PR open, permanently blocking the issue's reimplementation.
|
|
683
|
+
closePr: async (prUrl, comment) => {
|
|
684
|
+
const parts = prUrlParts(prUrl);
|
|
685
|
+
if (parts === undefined) {
|
|
686
|
+
return { ok: false, stderr: `could not parse ${prUrl} as a pull request URL` };
|
|
687
|
+
}
|
|
688
|
+
// The comment first, and only then the close: a closed PR still shows its
|
|
689
|
+
// comments, but a close that succeeds while the explanation fails leaves a
|
|
690
|
+
// PR nobody can account for. Ordering the durable explanation before the
|
|
691
|
+
// irreversible act is the same rule the merge path follows.
|
|
692
|
+
const said = await gh([
|
|
693
|
+
"api",
|
|
694
|
+
"--method",
|
|
695
|
+
"POST",
|
|
696
|
+
`repos/${parts.owner}/${parts.repo}/issues/${parts.number}/comments`,
|
|
697
|
+
"-f",
|
|
698
|
+
`body=${comment}`,
|
|
699
|
+
]);
|
|
700
|
+
if (!said.ok) return { ok: false, stderr: said.stderr };
|
|
701
|
+
return gh([
|
|
702
|
+
"api",
|
|
703
|
+
"--method",
|
|
704
|
+
"PATCH",
|
|
705
|
+
`repos/${parts.owner}/${parts.repo}/pulls/${parts.number}`,
|
|
706
|
+
"-f",
|
|
707
|
+
"state=closed",
|
|
708
|
+
]);
|
|
709
|
+
},
|
|
710
|
+
|
|
639
711
|
updatePr: async (prUrl, fields) => {
|
|
640
712
|
const argv = ["pr", "edit", prUrl];
|
|
641
713
|
if (fields.title !== undefined) argv.push("--title", fields.title);
|
|
@@ -682,15 +754,31 @@ export function githubVerbActions(
|
|
|
682
754
|
if (!prepared.ok) return prepared;
|
|
683
755
|
const mirror = prepared.path;
|
|
684
756
|
if (execution.shape === "git-tag") {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
757
|
+
// A pinned commit (#695) is the whole point of the argument: the tag
|
|
758
|
+
// points where the caller says, so the branch moving afterwards — which
|
|
759
|
+
// is what reaching a quiet release window requires — cannot strand it.
|
|
760
|
+
// The moved-branch guards below exist to catch main shifting under an
|
|
761
|
+
// implicit "current main" target; for an explicit commit they would
|
|
762
|
+
// refuse exactly the case the pin was asked for, so they are skipped
|
|
763
|
+
// and the reachability check is the invariant instead.
|
|
764
|
+
const pinned =
|
|
765
|
+
execution.sha === undefined
|
|
766
|
+
? undefined
|
|
767
|
+
: await pinnedReleaseCommit(mirror, execution.repo, execution.sha);
|
|
768
|
+
if (pinned !== undefined && !pinned.ok) return pinned;
|
|
769
|
+
const target =
|
|
770
|
+
pinned ??
|
|
771
|
+
(await readCommit(
|
|
772
|
+
mirror,
|
|
773
|
+
["rev-parse", `refs/remotes/origin/${execution.repo.defaultBranch}^{commit}`],
|
|
774
|
+
`refreshed ${execution.repo.defaultBranch}`,
|
|
775
|
+
));
|
|
690
776
|
if (!target.ok) return target;
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
777
|
+
if (pinned === undefined) {
|
|
778
|
+
const liveBefore = await liveDefaultHead(mirror, execution.repo);
|
|
779
|
+
if (!liveBefore.ok) return liveBefore;
|
|
780
|
+
if (target.sha !== liveBefore.sha) return releaseTargetMoved(execution.repo, target.sha, liveBefore.sha);
|
|
781
|
+
}
|
|
694
782
|
|
|
695
783
|
const versionReady = await releaseVersionRefusal(mirror, execution.repo, target.sha, tag);
|
|
696
784
|
if (!versionReady.ok) return versionReady;
|
|
@@ -736,7 +824,12 @@ export function githubVerbActions(
|
|
|
736
824
|
]);
|
|
737
825
|
if (!tagged.result.ok) return failed(tagged.result, tagged.argv);
|
|
738
826
|
|
|
739
|
-
|
|
827
|
+
// Only meaningful for an implicit target: a pinned tag is *expected* to
|
|
828
|
+
// sit behind a branch that has moved on.
|
|
829
|
+
const liveAfter =
|
|
830
|
+
pinned === undefined
|
|
831
|
+
? await liveDefaultHead(mirror, execution.repo)
|
|
832
|
+
: ({ ok: true, sha: target.sha } as const);
|
|
740
833
|
if (!liveAfter.ok || liveAfter.sha !== target.sha) {
|
|
741
834
|
const rollback = previousSha !== undefined
|
|
742
835
|
? await git(mirror, [
|
|
@@ -774,6 +867,27 @@ export function githubVerbActions(
|
|
|
774
867
|
const target = await readCommit(mirror, ["rev-parse", `refs/tags/${tag}^{commit}`], `tag ${tag}`);
|
|
775
868
|
if (!target.ok) return target;
|
|
776
869
|
let tagSha = target.sha;
|
|
870
|
+
// A pinned push publishes the tag EXACTLY as it was cut (#695). Without
|
|
871
|
+
// this the retarget-to-live-main path below would quietly move a pinned
|
|
872
|
+
// tag onto whatever merged since — which is the same stranding, inverted:
|
|
873
|
+
// the caller would get a published tag pointing somewhere it never asked
|
|
874
|
+
// for. The pin is re-proven against the branch and against the local tag,
|
|
875
|
+
// so a mismatch is a refusal rather than a silent move.
|
|
876
|
+
const pinnedPush =
|
|
877
|
+
execution.sha === undefined
|
|
878
|
+
? undefined
|
|
879
|
+
: await pinnedReleaseCommit(mirror, execution.repo, execution.sha);
|
|
880
|
+
if (pinnedPush !== undefined) {
|
|
881
|
+
if (!pinnedPush.ok) return pinnedPush;
|
|
882
|
+
if (pinnedPush.sha !== tagSha) {
|
|
883
|
+
return {
|
|
884
|
+
ok: false,
|
|
885
|
+
stderr:
|
|
886
|
+
`refusing release: ${tag} is at ${tagSha} locally, and you asked to publish it at ` +
|
|
887
|
+
`${pinnedPush.sha}. A published tag is never force-moved; cut the tag you mean to push.`,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
}
|
|
777
891
|
const live = await liveDefaultHead(mirror, execution.repo);
|
|
778
892
|
if (!live.ok) return live;
|
|
779
893
|
|
|
@@ -795,7 +909,7 @@ export function githubVerbActions(
|
|
|
795
909
|
if (!versionReady.ok) return versionReady;
|
|
796
910
|
|
|
797
911
|
let oldSha: string | undefined;
|
|
798
|
-
if (tagSha !== live.sha) {
|
|
912
|
+
if (pinnedPush === undefined && tagSha !== live.sha) {
|
|
799
913
|
const fresh = await readCommit(
|
|
800
914
|
mirror,
|
|
801
915
|
["rev-parse", `refs/remotes/origin/${execution.repo.defaultBranch}^{commit}`],
|
package/src/verbs/protocol.ts
CHANGED
|
@@ -286,6 +286,14 @@ export const VERB_SPECS: Readonly<Record<VerbName, VerbSpec>> = {
|
|
|
286
286
|
required: false,
|
|
287
287
|
description: "Tag to prepare or cut, for version-bump-pr, git-tag, and github-release.",
|
|
288
288
|
},
|
|
289
|
+
sha: {
|
|
290
|
+
type: "string",
|
|
291
|
+
required: false,
|
|
292
|
+
description:
|
|
293
|
+
"Pin the tag to this exact commit instead of whatever the released repo's default branch " +
|
|
294
|
+
"is at cut time (#695), for git-tag and git-push-tags. The commit must be reachable from " +
|
|
295
|
+
"that branch, or the release is refused naming it. Omit it and the behaviour is unchanged.",
|
|
296
|
+
},
|
|
289
297
|
artefact: {
|
|
290
298
|
type: "string",
|
|
291
299
|
required: false,
|
|
@@ -371,6 +379,47 @@ export const VERB_SPECS: Readonly<Record<VerbName, VerbSpec>> = {
|
|
|
371
379
|
roleRefusalText: (role) =>
|
|
372
380
|
`review authority is the orchestrator's; this is a ${role} session, and a worker never returns its own PR for revision.`,
|
|
373
381
|
},
|
|
382
|
+
conductor_pr_review_clear: {
|
|
383
|
+
name: "conductor_pr_review_clear",
|
|
384
|
+
mutating: true,
|
|
385
|
+
allowedRoles: ["orchestrator"],
|
|
386
|
+
description:
|
|
387
|
+
"Record that the durable review findings standing at one exact head are settled, so " +
|
|
388
|
+
"`conductor_pr_merge` stops refusing that head with merge-blocked-by-review (#913). This is the " +
|
|
389
|
+
"only clearance reachable at an UNCHANGED head: a re-review at the same head opens or appends to " +
|
|
390
|
+
"another blocking round rather than superseding one, and nothing settles a round from outside a " +
|
|
391
|
+
"worker. The clearance applies strictly backwards — it suppresses the evidence recorded up to the " +
|
|
392
|
+
"moment it is taken, never a finding recorded afterwards — and to that one head only. It bypasses " +
|
|
393
|
+
"nothing else: authority, pause, base-red-freeze, exact-head, green checks, release composition and " +
|
|
394
|
+
"single-flight all still apply to the merge. Refused for a PR no run of this project owns, a head " +
|
|
395
|
+
"that is not the live head, and a head where nothing is blocking.",
|
|
396
|
+
args: {
|
|
397
|
+
prUrl: {
|
|
398
|
+
type: "string",
|
|
399
|
+
required: true,
|
|
400
|
+
description:
|
|
401
|
+
"Full pull request URL — one a run of this project recorded, in a routed repository.",
|
|
402
|
+
},
|
|
403
|
+
headSha: {
|
|
404
|
+
type: "string",
|
|
405
|
+
required: true,
|
|
406
|
+
description:
|
|
407
|
+
"The exact head being cleared. Must equal the live head; checks are not re-read here " +
|
|
408
|
+
"(the merge gate re-checks green itself).",
|
|
409
|
+
},
|
|
410
|
+
reason: {
|
|
411
|
+
type: "string",
|
|
412
|
+
required: true,
|
|
413
|
+
description:
|
|
414
|
+
"Why this evidence is settled, in your own words. Non-empty, recorded verbatim in the " +
|
|
415
|
+
"ledger and the digest — this is the audit trail for a merge that a review round refused.",
|
|
416
|
+
},
|
|
417
|
+
rationale: RATIONALE_ARG,
|
|
418
|
+
},
|
|
419
|
+
roleRefusalText: (role) =>
|
|
420
|
+
`clearing review findings is the orchestrator's disposition; this is a ${role} session, ` +
|
|
421
|
+
"and a worker never clears the findings standing against its own pull request.",
|
|
422
|
+
},
|
|
374
423
|
conductor_pr_recover: {
|
|
375
424
|
name: "conductor_pr_recover",
|
|
376
425
|
mutating: true,
|
|
@@ -570,13 +619,32 @@ export function parseVerbRequest(raw: unknown): VerbParse {
|
|
|
570
619
|
if (arg.oneOf !== undefined && !arg.oneOf.some((allowed) => allowed === value)) {
|
|
571
620
|
// `reason` is the field #129 closed on purpose, so it gets its own
|
|
572
621
|
// refusal code: an out-of-enum reason is vocabulary drift, not a typo.
|
|
622
|
+
const reasonDrift = key === "reason";
|
|
623
|
+
// Measured on this fleet 2026-08-23: this is the largest single refusal
|
|
624
|
+
// cause in the ledger — 99 of 241, across the four verbs whose `reason`
|
|
625
|
+
// is a closed set, still firing. Every one of the 99 carried a real
|
|
626
|
+
// explanation ("landed directly in PR #940; queue label would re-dispatch
|
|
627
|
+
// finished work"), which is exactly the audit content the ledger exists
|
|
628
|
+
// to hold, and 7578 characters of it were discarded. None of the 99
|
|
629
|
+
// supplied `rationale` — the field built for it — because the moment a
|
|
630
|
+
// caller has a sentence to record is the moment they are told only that
|
|
631
|
+
// `reason` is closed (#968).
|
|
632
|
+
//
|
|
633
|
+
// Keyed on the spec rather than a hardcoded verb list: a verb with an
|
|
634
|
+
// enum `reason` and no `rationale` must not be told to use a field it
|
|
635
|
+
// would then refuse as `unknown-argument`, turning one wasted round trip
|
|
636
|
+
// into two.
|
|
637
|
+
const carriesRationale = reasonDrift && spec.args["rationale"] !== undefined;
|
|
573
638
|
return {
|
|
574
639
|
ok: false,
|
|
575
640
|
verb,
|
|
576
|
-
refusal:
|
|
641
|
+
refusal: reasonDrift ? "reason-not-in-enum" : "malformed-argument",
|
|
577
642
|
detail:
|
|
578
643
|
`refused: ${verb} argument "${key}" must be one of ${arg.oneOf.join(", ")}, ` +
|
|
579
|
-
`got ${JSON.stringify(value)}
|
|
644
|
+
`got ${JSON.stringify(value)}.` +
|
|
645
|
+
(carriesRationale
|
|
646
|
+
? ' Pick the enum value that fits and put that sentence in "rationale", which is logged verbatim.'
|
|
647
|
+
: ""),
|
|
580
648
|
};
|
|
581
649
|
}
|
|
582
650
|
}
|