omp-conductor 0.15.10 → 0.15.12
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 +273 -2544
- package/REFERENCE.md +2680 -0
- package/package.json +3 -2
- package/schema/config.schema.json +11 -23
- package/src/arm-challenge.ts +112 -0
- package/src/ask.ts +434 -0
- package/src/board.ts +81 -15
- package/src/brief-upgrade.ts +114 -8
- package/src/briefs/orchestrator.md +113 -34
- package/src/briefs/policy.md +33 -8
- package/src/briefs/worker.md +18 -9
- package/src/chain-check.ts +1 -1
- package/src/check-trailing-newlines.ts +82 -0
- package/src/cli.ts +225 -1406
- package/src/commands/arm.ts +21 -0
- package/src/commands/board.ts +23 -0
- package/src/commands/brief-upgrade.ts +186 -0
- package/src/commands/context.ts +150 -0
- package/src/commands/daemon.ts +71 -0
- package/src/commands/dashboard.ts +74 -0
- package/src/commands/decision.ts +103 -0
- package/src/commands/disarm.ts +21 -0
- package/src/commands/doctor.ts +100 -0
- package/src/commands/event.ts +62 -0
- package/src/commands/extend.ts +64 -0
- package/src/commands/friction.ts +56 -0
- package/src/commands/help.ts +9 -0
- package/src/commands/hold.ts +26 -0
- package/src/commands/intake.ts +155 -0
- package/src/commands/ledger.ts +69 -0
- package/src/commands/message.ts +96 -0
- package/src/commands/report.ts +206 -0
- package/src/commands/restart.ts +76 -0
- package/src/commands/resume.ts +58 -0
- package/src/commands/setup.ts +143 -0
- package/src/commands/start.ts +23 -0
- package/src/commands/stats.ts +131 -0
- package/src/commands/status.ts +48 -0
- package/src/commands/stop.ts +51 -0
- package/src/commands/tail.ts +109 -0
- package/src/commands/unblock.ts +39 -0
- package/src/commands/upgrade-install.ts +31 -0
- package/src/commands/upgrade-rollback.ts +32 -0
- package/src/commands/upgrade.ts +25 -0
- package/src/commands/verb.ts +83 -0
- package/src/commands/version.ts +30 -0
- package/src/commands/worker.ts +100 -0
- package/src/config-schema.ts +47 -1
- package/src/config.ts +45 -4
- package/src/daemon.ts +972 -101
- package/src/dashboard/app.js +459 -0
- package/src/dashboard/index.html +61 -0
- package/src/dashboard/server.ts +481 -0
- package/src/dashboard/style.css +348 -0
- package/src/decisions.ts +39 -14
- package/src/diff-flags.ts +131 -241
- package/src/doctor.ts +932 -0
- package/src/escalate.ts +2 -2
- package/src/failure-class.ts +66 -3
- package/src/fleet.ts +58 -1
- package/src/gitops.ts +157 -0
- package/src/graph-health.ts +1 -1
- package/src/label-projection.ts +1 -1
- package/src/lifecycle.ts +198 -2
- package/src/model-fallback.ts +177 -0
- package/src/notices.ts +9 -0
- package/src/omp.ts +93 -13
- package/src/orchestrator-tick.ts +414 -20
- package/src/orchestrator.ts +4 -4
- package/src/privileged.ts +10 -0
- package/src/release-policy.ts +342 -30
- package/src/reports.ts +19 -5
- package/src/session-host.ts +11 -5
- package/src/setup-host.ts +663 -25
- package/src/setup-install.ts +292 -28
- package/src/setup-wizard.ts +255 -74
- package/src/setup.ts +156 -35
- package/src/stats.ts +331 -0
- package/src/store.ts +219 -22
- package/src/tracker/github.ts +74 -4
- package/src/types.ts +215 -31
- package/src/unblock.ts +55 -11
- package/src/upgrade-journal.ts +220 -0
- package/src/upgrade-verify.ts +506 -0
- package/src/upgrade.ts +385 -58
- package/src/verbs/actions.ts +73 -1
- package/src/verbs/protocol.ts +29 -4
- package/src/verbs/server.ts +183 -20
- package/src/worker.ts +3 -3
- package/systemd/omp-conductor-recover.sh +433 -0
- package/systemd/omp-conductor.service.example +14 -3
- package/systemd/recover-unit-test.sh +428 -0
package/src/types.ts
CHANGED
|
@@ -250,6 +250,14 @@ export const RELEASE_SHAPES = [
|
|
|
250
250
|
"package-publish",
|
|
251
251
|
"github-release",
|
|
252
252
|
"deploy",
|
|
253
|
+
/**
|
|
254
|
+
* Replace the running conductor itself: the Bun-global CLI, the omp plugin
|
|
255
|
+
* and the Herdr plugin, pinned to one published release and executed
|
|
256
|
+
* detached. This is the "nobody patches the running conductor" boundary
|
|
257
|
+
* moved deliberately, not worked around — a session that may cut a release
|
|
258
|
+
* still cannot install one until an operator grants this shape too.
|
|
259
|
+
*/
|
|
260
|
+
"install",
|
|
253
261
|
] as const;
|
|
254
262
|
|
|
255
263
|
export type ReleaseShape = (typeof RELEASE_SHAPES)[number];
|
|
@@ -293,6 +301,7 @@ export const DENIED_RELEASE_GRANTS: ResolvedGrants = {
|
|
|
293
301
|
"package-publish": "human",
|
|
294
302
|
"github-release": "human",
|
|
295
303
|
deploy: "human",
|
|
304
|
+
install: "human",
|
|
296
305
|
};
|
|
297
306
|
|
|
298
307
|
/**
|
|
@@ -308,6 +317,9 @@ export const OPERATOR_BRIEF_GRANTS: ResolvedGrants = {
|
|
|
308
317
|
"package-publish": "orchestrator",
|
|
309
318
|
"github-release": "orchestrator",
|
|
310
319
|
deploy: "human",
|
|
320
|
+
// Nobody expected a legacy "operator-brief" grant to cover patching the
|
|
321
|
+
// running conductor; like `deploy` it stays with the human.
|
|
322
|
+
install: "human",
|
|
311
323
|
};
|
|
312
324
|
|
|
313
325
|
/**
|
|
@@ -324,6 +336,16 @@ export const SESSION_ROLES = ["worker", "orchestrator"] as const;
|
|
|
324
336
|
|
|
325
337
|
export type SessionRole = (typeof SESSION_ROLES)[number];
|
|
326
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Env var the daemon sets on a spawned session so the reporting CLI can tell a
|
|
341
|
+
* worker session from the operator's shell. The daemon stamps it on the
|
|
342
|
+
* session-host child (and thus on every tool process it spawns); a direct CLI
|
|
343
|
+
* run — the operator's shell or a systemd unit — has it absent and is treated
|
|
344
|
+
* as the orchestrator, preserving the historical surface. {@link SESSION_ROLES} is
|
|
345
|
+
* the closed vocabulary: a value outside it must be refused, never assumed.
|
|
346
|
+
*/
|
|
347
|
+
export const SESSION_ROLE_ENV = "OMP_CONDUCTOR_SESSION_ROLE";
|
|
348
|
+
|
|
327
349
|
/**
|
|
328
350
|
* Who holds an authority the daemon itself never exercises. Declared as data
|
|
329
351
|
* for the same reason as {@link REPORT_SCOPES}: the validator, the wizard and
|
|
@@ -335,11 +357,16 @@ export const AUTHORITY_HOLDERS = ["human", "orchestrator"] as const;
|
|
|
335
357
|
export type AuthorityHolder = (typeof AUTHORITY_HOLDERS)[number];
|
|
336
358
|
|
|
337
359
|
/**
|
|
338
|
-
* What a project that never answered the question gets: humans keep
|
|
339
|
-
* default that delegated merging would hand a fresh fleet write access to
|
|
340
|
-
* own main branch on the strength of an unread config file
|
|
360
|
+
* What a project that never answered the question gets: humans keep all three.
|
|
361
|
+
* A default that delegated merging would hand a fresh fleet write access to
|
|
362
|
+
* its own main branch on the strength of an unread config file, and a default
|
|
363
|
+
* that delegated promotion would let the orchestrator start spend unasked.
|
|
341
364
|
*/
|
|
342
|
-
export const DEFAULT_AUTHORITY: ProjectConfig["authority"]
|
|
365
|
+
export const DEFAULT_AUTHORITY: ProjectConfig["authority"] & { promotion: AuthorityHolder } = {
|
|
366
|
+
merge: "human",
|
|
367
|
+
release: "human",
|
|
368
|
+
promotion: "human",
|
|
369
|
+
};
|
|
343
370
|
|
|
344
371
|
/**
|
|
345
372
|
* Whether a merge candidate must be up to date with the branch it targets.
|
|
@@ -615,18 +642,38 @@ export interface ProjectConfig {
|
|
|
615
642
|
* answered the question wants.
|
|
616
643
|
*/
|
|
617
644
|
workerModel?: string;
|
|
645
|
+
/**
|
|
646
|
+
* Ordered fallback models for this project, tried after {@link workerModel}
|
|
647
|
+
* once a run chain has suffered enough consecutive provider-class failures
|
|
648
|
+
* (stream stalls and credit refusals). Each new issue still starts on the
|
|
649
|
+
* primary; recovery is sticky to the chain, never global (#286). Empty or
|
|
650
|
+
* absent preserves the pre-failover dispatch exactly: every attempt stays on
|
|
651
|
+
* the primary until the existing escalation path takes over.
|
|
652
|
+
*/
|
|
653
|
+
modelFallbacks?: string[];
|
|
654
|
+
/**
|
|
655
|
+
* Consecutive provider-class failures on one run chain after which the next
|
|
656
|
+
* dispatch moves to the next model in {@link modelFallbacks}. Defaults to 2
|
|
657
|
+
* when absent or unusable.
|
|
658
|
+
*/
|
|
659
|
+
modelFallbackThreshold?: number;
|
|
618
660
|
/**
|
|
619
661
|
* How a stuck run reaches a human, what to do when it cannot, and who runs
|
|
620
662
|
* the session that triages it. See {@link ORCHESTRATOR_MODES}.
|
|
621
663
|
*/
|
|
622
664
|
escalation: { telegramChatId?: string; telegramTopicId?: number; fallbackToIssueComment: boolean; orchestrator: OrchestratorMode };
|
|
623
665
|
/**
|
|
624
|
-
* Who lands green PRs
|
|
625
|
-
* itself — it words the orchestrator's standing orders and the
|
|
626
|
-
* scaffold with it, so config and prompt can never disagree
|
|
627
|
-
* them is holding the merge button.
|
|
666
|
+
* Who lands green PRs, who cuts releases and who promotes. The daemon never
|
|
667
|
+
* acts on this itself — it words the orchestrator's standing orders and the
|
|
668
|
+
* rendered brief scaffold with it, so config and prompt can never disagree
|
|
669
|
+
* about which of them is holding the merge button.
|
|
670
|
+
*
|
|
671
|
+
* `promotion` is the queue-label sign-off: adding the label to an issue lets
|
|
672
|
+
* a worker claim it and starts spend, so POLICY.md must state its owner and
|
|
673
|
+
* a config that never answered defaults to the human — a gate whose owner is
|
|
674
|
+
* unstated must never resolve to the orchestrator.
|
|
628
675
|
*/
|
|
629
|
-
authority: { merge: AuthorityHolder; release: AuthorityHolder };
|
|
676
|
+
authority: { merge: AuthorityHolder; release: AuthorityHolder; promotion?: AuthorityHolder };
|
|
630
677
|
/**
|
|
631
678
|
* Per-shape tool-call enforcement for releases and deploys. Optional only for
|
|
632
679
|
* configs written before the tripwire existed; both omission and any shape key
|
|
@@ -666,6 +713,20 @@ export interface ProjectConfig {
|
|
|
666
713
|
workspaceRoot: string;
|
|
667
714
|
/** Cache of bare clones, so N runs share one fetch instead of N. */
|
|
668
715
|
mirrorRoot: string;
|
|
716
|
+
/**
|
|
717
|
+
* Git treeish (commit SHAs or refs) on the base branch that every preserved
|
|
718
|
+
* continuation branch must contain before the dispatcher may reattach it
|
|
719
|
+
* (#428). A base safety fix protects only branches forked after it landed; a
|
|
720
|
+
* continuation forked before it still carries the dangerous code and, on a
|
|
721
|
+
* shared host, re-running the lifecycle suite it retains is what SIGTERMed
|
|
722
|
+
* the production daemon. With a marker configured, the dispatcher refuses to
|
|
723
|
+
* reattach any preserved branch missing it, holds the issue as `stale-base`
|
|
724
|
+
* (distinct from capacity or dependency holds), and never silently merges
|
|
725
|
+
* base into the user's work. Absent or empty, ordinary stale continuations
|
|
726
|
+
* keep today's behaviour. Optional and hand-edited, like
|
|
727
|
+
* {@link recoveryMerges}.
|
|
728
|
+
*/
|
|
729
|
+
criticalBase?: string[];
|
|
669
730
|
}
|
|
670
731
|
|
|
671
732
|
/**
|
|
@@ -706,6 +767,16 @@ export interface ReadyIssue {
|
|
|
706
767
|
updatedAt: string;
|
|
707
768
|
}
|
|
708
769
|
|
|
770
|
+
/**
|
|
771
|
+
* One issue comment as the worker brief renders it: author login and verbatim
|
|
772
|
+
* body, in the tracker's own order (oldest first), so a later correction
|
|
773
|
+
* visibly supersedes an earlier note.
|
|
774
|
+
*/
|
|
775
|
+
export interface IssueComment {
|
|
776
|
+
author: string;
|
|
777
|
+
body: string;
|
|
778
|
+
}
|
|
779
|
+
|
|
709
780
|
/**
|
|
710
781
|
* How a pull request ended, in tracker-agnostic terms. Lowercase because the
|
|
711
782
|
* loop's vocabulary is lowercase; mapping GitHub's `MERGED`/`CLOSED`/`OPEN` onto
|
|
@@ -755,17 +826,10 @@ export interface PrDiff {
|
|
|
755
826
|
* vocabulary that lives in three places drifts in three directions.
|
|
756
827
|
*/
|
|
757
828
|
export const SETTLEMENT_FLAG_KINDS = [
|
|
758
|
-
/** The
|
|
759
|
-
*
|
|
760
|
-
|
|
761
|
-
/** The report carried no usable `changed:` line, so nothing was disclosed. */
|
|
829
|
+
/** The settlement could not read the PR's diff, so no `changed:` file list
|
|
830
|
+
* could be derived — the one disclosure fault that survives, because a
|
|
831
|
+
* tree no one can read has nothing to disclose. */
|
|
762
832
|
"changed-line-missing",
|
|
763
|
-
/** `changed:` named a path the PR never touched. The weaker direction. */
|
|
764
|
-
"unmatched-claim",
|
|
765
|
-
/** The same file appears as both claimed-but-untouched and
|
|
766
|
-
* touched-but-unclaimed — the `changed:` line's format defeated the parser;
|
|
767
|
-
* read the diff directly. */
|
|
768
|
-
"report-format-unparsed",
|
|
769
833
|
/** A test file left the tree and no rename in the PR accounts for it. */
|
|
770
834
|
"test-file-deleted",
|
|
771
835
|
/** A skip/only/focus marker appears on a line the PR added. */
|
|
@@ -864,6 +928,16 @@ export interface Tracker {
|
|
|
864
928
|
* Complete — follows pagination to the end.
|
|
865
929
|
*/
|
|
866
930
|
listOpenIssues(): Promise<ReadyIssue[]>;
|
|
931
|
+
/**
|
|
932
|
+
* The issue's comments, oldest first. The dispatcher renders them into the
|
|
933
|
+
* worker brief, so grooming the orchestrator posted as a comment reaches the
|
|
934
|
+
* worker's opening prompt without any runtime read.
|
|
935
|
+
*
|
|
936
|
+
* Throws when the tracker could not be read: the caller must never mistake
|
|
937
|
+
* "could not read" for "no comments", which is exactly the failure mode this
|
|
938
|
+
* read exists to prevent (#517).
|
|
939
|
+
*/
|
|
940
|
+
listComments(issue: number): Promise<IssueComment[]>;
|
|
867
941
|
addLabel(issue: number, label: string): Promise<void>;
|
|
868
942
|
removeLabel(issue: number, label: string): Promise<void>;
|
|
869
943
|
comment(issue: number, body: string): Promise<void>;
|
|
@@ -944,8 +1018,13 @@ export interface Tracker {
|
|
|
944
1018
|
* Verify that a pull request is open, ready, and has a non-empty
|
|
945
1019
|
* terminal-success check rollup. When `expectedHead` is supplied, also
|
|
946
1020
|
* require the live head to match it.
|
|
1021
|
+
*
|
|
1022
|
+
* `opts.read` switches from the merge gate's "may this merge" to the read
|
|
1023
|
+
* surface's "what is this PR's state": a merged or closed PR is then a
|
|
1024
|
+
* reported fact rather than an `expected OPEN` refusal. Mutating verbs never
|
|
1025
|
+
* pass `read`, so their open-PR gate is unchanged.
|
|
947
1026
|
*/
|
|
948
|
-
verifyPr(url: string, expectedHead?: string): Promise<PrVerification | undefined>;
|
|
1027
|
+
verifyPr(url: string, expectedHead?: string, opts?: { read?: boolean }): Promise<PrVerification | undefined>;
|
|
949
1028
|
/**
|
|
950
1029
|
* The pull request's diff, or undefined when this adapter could not produce
|
|
951
1030
|
* one — an unparseable URL, a deleted PR, a flaky network.
|
|
@@ -1010,6 +1089,8 @@ export const FAILURE_CLASSES = [
|
|
|
1010
1089
|
"env-start-failure",
|
|
1011
1090
|
"turn-cap-progress",
|
|
1012
1091
|
"turn-cap-spinning",
|
|
1092
|
+
"wall-clock-cap-progress",
|
|
1093
|
+
"wall-clock-cap-spinning",
|
|
1013
1094
|
"admin-kill",
|
|
1014
1095
|
"ci-infra",
|
|
1015
1096
|
"ci-deterministic",
|
|
@@ -1132,6 +1213,16 @@ export interface RunRecord {
|
|
|
1132
1213
|
* copy of real work, so the tree was kept and the issue is held out of
|
|
1133
1214
|
* dispatch until an operator acknowledges it. */
|
|
1134
1215
|
salvageError?: string;
|
|
1216
|
+
/**
|
|
1217
|
+
* The model this attempt actually dispatched on, written at dispatch when the
|
|
1218
|
+
* project configures a failover chain (`modelFallbacks`). Absent means the
|
|
1219
|
+
* run predates the column or the project has no chain — never "the harness
|
|
1220
|
+
* downgraded"; the harness's own downgrade is carried as
|
|
1221
|
+
* `modelFallbackMessage` on the worker result instead, because that is
|
|
1222
|
+
* evidence about this run while this column is attribution for the chain
|
|
1223
|
+
* (#286).
|
|
1224
|
+
*/
|
|
1225
|
+
model?: string;
|
|
1135
1226
|
/** When an operator accepted the loss or recovered the tree by hand
|
|
1136
1227
|
* (`unblock --force`). Clears the hold without erasing what happened. */
|
|
1137
1228
|
salvageAckAt?: number;
|
|
@@ -1144,10 +1235,10 @@ export interface RunRecord {
|
|
|
1144
1235
|
* exactly as an unflagged one does, and the flags are evidence for whoever
|
|
1145
1236
|
* reviews the PR. Absent means the audit found nothing, or never ran. */
|
|
1146
1237
|
settlementFlags?: SettlementFlag[];
|
|
1147
|
-
/** The worker's settlement report text for this attempt
|
|
1148
|
-
*
|
|
1149
|
-
* (#
|
|
1150
|
-
* worker returned a report. */
|
|
1238
|
+
/** The worker's settlement report text for this attempt: the worker's own
|
|
1239
|
+
* narrative, with the `changed:` file list rewritten at settlement from
|
|
1240
|
+
* the PR's actual diff (#488). Absent means the run predates the column, or
|
|
1241
|
+
* was killed before the worker returned a report. */
|
|
1151
1242
|
report?: string;
|
|
1152
1243
|
/**
|
|
1153
1244
|
* Why this run ended badly, and what the daemon did about it (#132). Absent
|
|
@@ -1162,6 +1253,15 @@ export interface RunRecord {
|
|
|
1162
1253
|
recoveredAt?: number;
|
|
1163
1254
|
}
|
|
1164
1255
|
|
|
1256
|
+
/**
|
|
1257
|
+
* A partial patch for {@link updateRun}. Every key is optional; an absent key
|
|
1258
|
+
* and an explicitly-`undefined` value both leave the column alone, while an
|
|
1259
|
+
* explicit `null` clears it (#468). `null` exists only for the columns a
|
|
1260
|
+
* caller may legitimately empty — a settle can never widen into a column it
|
|
1261
|
+
* did not mean to write.
|
|
1262
|
+
*/
|
|
1263
|
+
export type RunPatch = { [K in keyof RunRecord]?: RunRecord[K] | null };
|
|
1264
|
+
|
|
1165
1265
|
/** A one-shot turn budget waiting for one issue's next claimed attempt. */
|
|
1166
1266
|
export interface TurnOverride {
|
|
1167
1267
|
project: string;
|
|
@@ -1187,6 +1287,7 @@ export type AdmissionHoldReason =
|
|
|
1187
1287
|
| "daily-spend-cap"
|
|
1188
1288
|
| "plan-usage-cap"
|
|
1189
1289
|
| "shutting-down"
|
|
1290
|
+
| "stale-base"
|
|
1190
1291
|
| "unroutable:no-repo-label"
|
|
1191
1292
|
| "unroutable:multiple-repo-labels"
|
|
1192
1293
|
| "unroutable:unknown-repo";
|
|
@@ -1211,6 +1312,13 @@ export interface DispatchSummary {
|
|
|
1211
1312
|
/** True only for system/API failures, never ordinary policy holds. */
|
|
1212
1313
|
degraded: boolean;
|
|
1213
1314
|
holds: AdmissionHoldSummary[];
|
|
1315
|
+
/** Runs the pass settled while it ran — merged PRs terminalised by the settle
|
|
1316
|
+
* sweep and by classification recovery. Optional: persisted old rows lack
|
|
1317
|
+
* it, so readers use `?? 0`. (#497) */
|
|
1318
|
+
settled?: number;
|
|
1319
|
+
/** True for a held pass's own record (#497): the queue was never routed, so
|
|
1320
|
+
* ready/routed/claimed are absent queue facts, not an empty queue. */
|
|
1321
|
+
paused?: boolean;
|
|
1214
1322
|
}
|
|
1215
1323
|
|
|
1216
1324
|
/** Admission holds that indicate repairable friction rather than normal flow control. */
|
|
@@ -1220,6 +1328,7 @@ export type FrictionAdmissionReason =
|
|
|
1220
1328
|
| "parent-lookup-error"
|
|
1221
1329
|
| "issue-state-lookup-error"
|
|
1222
1330
|
| "open-pr-lookup-error"
|
|
1331
|
+
| "stale-base"
|
|
1223
1332
|
| "unroutable:no-repo-label"
|
|
1224
1333
|
| "unroutable:multiple-repo-labels"
|
|
1225
1334
|
| "unroutable:unknown-repo";
|
|
@@ -1261,11 +1370,22 @@ export interface FrictionSignal {
|
|
|
1261
1370
|
* today's digest already been handed over?" without asking the model to
|
|
1262
1371
|
* remember, which is the memory #123 found unreliable.
|
|
1263
1372
|
*
|
|
1373
|
+
* The remaining values are the interrupt categories themselves. Handing a
|
|
1374
|
+
* report over as `fleet-stopped` or `tier2` (or `decision-needed`,
|
|
1375
|
+
* `confirmed-failure`) is the escalation handoff: the project's reporting
|
|
1376
|
+
* policy decides between sending now and holding, exactly as it does for a
|
|
1377
|
+
* daemon escalation of that category, and an identical retry is refused only
|
|
1378
|
+
* while the earlier handoff is still undelivered (#453).
|
|
1379
|
+
*
|
|
1264
1380
|
* Declared as data for the same reason as {@link REPORT_SCOPES}: the CLI verb
|
|
1265
1381
|
* that accepts a kind and the outbox that renders one enumerate the same list,
|
|
1266
1382
|
* so a third kind cannot be added while either still knows only two.
|
|
1267
1383
|
*/
|
|
1268
|
-
export const REPORT_KINDS = [
|
|
1384
|
+
export const REPORT_KINDS = [
|
|
1385
|
+
"material",
|
|
1386
|
+
"digest",
|
|
1387
|
+
...INTERRUPT_CATEGORIES.filter((category) => category !== "material"),
|
|
1388
|
+
] as const;
|
|
1269
1389
|
|
|
1270
1390
|
export type ReportKind = (typeof REPORT_KINDS)[number];
|
|
1271
1391
|
|
|
@@ -1377,7 +1497,41 @@ export interface DigestBacklog {
|
|
|
1377
1497
|
heldNoticeCount: number;
|
|
1378
1498
|
/** Subset held only for the next availability window. */
|
|
1379
1499
|
availabilityHeldNoticeCount?: number;
|
|
1500
|
+
/** Oldest held notice across both held groups. */
|
|
1380
1501
|
heldNoticeOldestAt?: number;
|
|
1502
|
+
/** Oldest held notice that is digest-bound (`releaseOnAvailable` false). */
|
|
1503
|
+
digestHeldNoticeOldestAt?: number;
|
|
1504
|
+
/** Oldest held notice waiting only for the availability window. */
|
|
1505
|
+
availabilityHeldNoticeOldestAt?: number;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/** Where one captured raw idea is in its lifecycle (#298). `pending` is a raw
|
|
1509
|
+
* idea awaiting grooming; `groomed` became an issue (#300, which records the
|
|
1510
|
+
* issue URL); `dismissed` was dropped without becoming anything. */
|
|
1511
|
+
export const INTAKE_STATES = ["pending", "groomed", "dismissed"] as const;
|
|
1512
|
+
|
|
1513
|
+
export type IntakeState = (typeof INTAKE_STATES)[number];
|
|
1514
|
+
|
|
1515
|
+
/** One raw idea captured by `omp-conductor intake`, durable in the store so an
|
|
1516
|
+
* idea from 02:00 is still there after a restart (#299). */
|
|
1517
|
+
export interface IntakeItem {
|
|
1518
|
+
id: string;
|
|
1519
|
+
project: string;
|
|
1520
|
+
/** The idea text verbatim, as captured. */
|
|
1521
|
+
text: string;
|
|
1522
|
+
createdAt: number;
|
|
1523
|
+
state: IntakeState;
|
|
1524
|
+
/** The issue this idea became, once #300 grooms it. */
|
|
1525
|
+
issueUrl?: string;
|
|
1526
|
+
/** When the item was resolved into an issue (#300) or dismissed. */
|
|
1527
|
+
groomedAt?: number;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/** What a caller hands over. The store owns the id, the state and the timestamps. */
|
|
1531
|
+
export interface IntakeDraft {
|
|
1532
|
+
project: string;
|
|
1533
|
+
text: string;
|
|
1534
|
+
at: number;
|
|
1381
1535
|
}
|
|
1382
1536
|
|
|
1383
1537
|
/**
|
|
@@ -1460,7 +1614,8 @@ export interface LabelOp {
|
|
|
1460
1614
|
export interface Store {
|
|
1461
1615
|
/** A claimed run atomically consumes and applies its issue's pending turn override. */
|
|
1462
1616
|
createRun(r: Omit<RunRecord, "id">): RunRecord;
|
|
1463
|
-
|
|
1617
|
+
/** Partial patch; an explicit `null` clears a column, `undefined`/absence leaves it alone (#468). */
|
|
1618
|
+
updateRun(id: string, patch: RunPatch): void;
|
|
1464
1619
|
getRun(id: string): RunRecord | undefined;
|
|
1465
1620
|
/** Runs whose issue is occupied: a live worker, or a green PR awaiting merge. */
|
|
1466
1621
|
activeRuns(project: string): RunRecord[];
|
|
@@ -1471,6 +1626,11 @@ export interface Store {
|
|
|
1471
1626
|
/** Newest attempt per issue for the live board. Non-merged work remains
|
|
1472
1627
|
* visible; merged rows are bounded by the supplied recent-history cutoff. */
|
|
1473
1628
|
recentRuns(project: string, mergedSinceEpochMs: number): RunRecord[];
|
|
1629
|
+
/** Every attempt that recorded one PR URL, newest first. Mediated PR verbs
|
|
1630
|
+
* resolve ownership through this, not through `recentRuns`' newest-attempt-
|
|
1631
|
+
* per-issue view — a requeued continuation must never hide the PR its
|
|
1632
|
+
* predecessor opened. Merged rows keep the same recent-history bound. */
|
|
1633
|
+
runsForProjectPr(project: string, prUrl: string, mergedSinceEpochMs: number): RunRecord[];
|
|
1474
1634
|
/** Merged rows whose post-merge workflow verdict is still pending, oldest first. */
|
|
1475
1635
|
runsNeedingBaseCheck(project: string, limit?: number): RunRecord[];
|
|
1476
1636
|
/** Replace the current live-head health row for one routed repository. */
|
|
@@ -1500,6 +1660,9 @@ export interface Store {
|
|
|
1500
1660
|
* tail` resolves an issue number to a transcript through this; the number is
|
|
1501
1661
|
* what an operator has, the run id is not. */
|
|
1502
1662
|
latestRun(project: string, issue: number): RunRecord | undefined;
|
|
1663
|
+
/** Every attempt for one issue, oldest first, so an escalation can group the
|
|
1664
|
+
* continuation budget by failure class (#439). */
|
|
1665
|
+
runsForIssue(project: string, issue: number): RunRecord[];
|
|
1503
1666
|
/** Persist a one-shot ceiling and append the operator action to its audit ledger. */
|
|
1504
1667
|
setTurnOverride(project: string, issue: number, maxTurns: number, setAt?: number): void;
|
|
1505
1668
|
turnOverride(project: string, issue: number): number | undefined;
|
|
@@ -1511,16 +1674,24 @@ export interface Store {
|
|
|
1511
1674
|
project: string,
|
|
1512
1675
|
opts?: { issue?: number; limit?: number },
|
|
1513
1676
|
): TurnOverride[];
|
|
1514
|
-
/** Every attempt of one issue that settled with a report, in attempt order.
|
|
1515
|
-
* The settlement audit pools these as prior disclosures when a later
|
|
1516
|
-
* attempt is reconciled (#199). Rows with no report (pre-#199, or killed
|
|
1517
|
-
* before the worker returned) are skipped. */
|
|
1518
|
-
attemptReports(project: string, issue: number): { attempt: number; report: string }[];
|
|
1519
1677
|
runsStartedSince(project: string, sinceEpochMs: number): number;
|
|
1520
1678
|
spendSince(project: string, sinceEpochMs: number): number;
|
|
1679
|
+
/** The rows one stats window needs (#282): every run settling at or after
|
|
1680
|
+
* `sinceEpochMs`, plus the full attempt chain of each issue whose merge
|
|
1681
|
+
* settled there (so continuation chains collapse into one journey), ordered
|
|
1682
|
+
* by issue then startedAt. Read-only. */
|
|
1683
|
+
statsRuns(project: string, sinceEpochMs: number): RunRecord[];
|
|
1684
|
+
/** Total tracked `gh` calls between two UTC day keys, inclusive (#198). */
|
|
1685
|
+
ghCallsBetween(sinceDay: string, untilDay: string): number;
|
|
1521
1686
|
/** Idempotence guard so a retry loop cannot page a human repeatedly for the
|
|
1522
1687
|
* same event. */
|
|
1523
1688
|
wasNotified(key: string): boolean;
|
|
1689
|
+
/** Whether an escalation handoff with this project, category and folded text
|
|
1690
|
+
* is still owed: a same-text report still in the outbox, or a same-text
|
|
1691
|
+
* held notice not yet digested. The CLI report surface refuses an identical
|
|
1692
|
+
* retry only while this is true (#453) — once the first handoff lands, the
|
|
1693
|
+
* same text is a new occurrence, not a double-send. */
|
|
1694
|
+
hasOpenEscalationHandoff(project: string, category: InterruptCategory, body: string): boolean;
|
|
1524
1695
|
recordDispatch(project: string, summary: DispatchSummary): void;
|
|
1525
1696
|
latestDispatch(project: string): DispatchSummary | undefined;
|
|
1526
1697
|
/** The newest `digest:` dedupe key a project has run toward without ending in
|
|
@@ -1544,6 +1715,13 @@ export interface Store {
|
|
|
1544
1715
|
getMaterialEvent(id: string): MaterialEvent | undefined;
|
|
1545
1716
|
/** Ordinary material outcomes still owed, oldest first and bounded. */
|
|
1546
1717
|
undigestedMaterialEvents(project: string, limit?: number): MaterialEvent[];
|
|
1718
|
+
/** Persist one raw idea, `pending`, for later grooming (#299). */
|
|
1719
|
+
recordIntake(draft: IntakeDraft): IntakeItem;
|
|
1720
|
+
/** Ideas still `pending`, oldest first — what `intake list` and `status` read. */
|
|
1721
|
+
pendingIntake(project: string): IntakeItem[];
|
|
1722
|
+
/** Resolve one idea to `groomed` (recording the issue URL #300 chose) or
|
|
1723
|
+
* `dismissed`. `false` when the id is unknown. */
|
|
1724
|
+
resolveIntake(id: string, state: "groomed" | "dismissed", issueUrl?: string): boolean;
|
|
1547
1725
|
/** Count and age source for status and digest prompt bounds. */
|
|
1548
1726
|
digestBacklog(project: string): DigestBacklog;
|
|
1549
1727
|
/** Add one bounded observation to the per-day friction rollup. */
|
|
@@ -1655,6 +1833,8 @@ export interface Store {
|
|
|
1655
1833
|
createDecision(draft: DecisionDraft): DecisionRecord;
|
|
1656
1834
|
/** Everything still owed an answer, oldest first — what a tick digest reads. */
|
|
1657
1835
|
openDecisions(project: string): DecisionRecord[];
|
|
1836
|
+
/** One row by id, whatever its state — what a bounded ask polls while waiting. */
|
|
1837
|
+
decision(id: string): DecisionRecord | undefined;
|
|
1658
1838
|
/** Answer or withdraw one. `false` when the id is unknown or already closed,
|
|
1659
1839
|
* so a double-resolve cannot overwrite the first answer. */
|
|
1660
1840
|
resolveDecision(id: string, state: "answered" | "withdrawn", resolution: string, at: number): boolean;
|
|
@@ -1805,6 +1985,10 @@ export const VERB_NAMES = [
|
|
|
1805
1985
|
"conductor_pr_merge",
|
|
1806
1986
|
"conductor_label",
|
|
1807
1987
|
"conductor_release",
|
|
1988
|
+
/** Request the fleet upgrade itself to a published conductor release. The
|
|
1989
|
+
* install shape gates it like a release shape; execution detaches from this
|
|
1990
|
+
* session and the daemon, and the first tick after the restart verifies. */
|
|
1991
|
+
"conductor_install",
|
|
1808
1992
|
/** The one read verb. It answers with the merge gate's own verdict, so
|
|
1809
1993
|
* "pushed-green" is the dispatcher's reading of the PR rather than a claim the
|
|
1810
1994
|
* worker makes about itself from whatever it happened to run. */
|
package/src/unblock.ts
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* failed-attempt budget.
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
+
import { hasContinuationBudget, hasFailedAttemptBudget } from "./daemon.ts";
|
|
38
39
|
import { projectLabels } from "./label-projection.ts";
|
|
39
40
|
import { LIVE_STATES } from "./store.ts";
|
|
40
41
|
import type { Caps, ProjectConfig, RunRecord, Store, Tracker } from "./types.ts";
|
|
@@ -64,11 +65,18 @@ export interface UnblockOutcome {
|
|
|
64
65
|
/** Set when `--force` recorded an operator's acceptance of that loss. */
|
|
65
66
|
forced?: true;
|
|
66
67
|
/** Set when the queue label was re-added (the default); absent on
|
|
67
|
-
* --no-requeue, on the refusal path,
|
|
68
|
-
* running) is still on the issue
|
|
68
|
+
* --no-requeue, on the refusal path, when a live worker (claimed or
|
|
69
|
+
* running) is still on the issue, and when a spent budget withheld the
|
|
70
|
+
* re-queue (requeueWithheld). A pushed-green/pushed-pending occupancy
|
|
69
71
|
* is worker-free, so the label is still restored there (#175 bypasses it;
|
|
70
72
|
* `isEligible` needs the label once the PR resolves). */
|
|
71
73
|
requeued?: true;
|
|
74
|
+
/** Set when the re-queue was withheld because the issue's failed-attempt or
|
|
75
|
+
* continuation budget is spent — the same admission gate the dispatcher
|
|
76
|
+
* holds on (failed-attempts / continuations), so the queue label would
|
|
77
|
+
* manufacture a candidate that can never be admitted (#348). The lifecycle
|
|
78
|
+
* clears still happened; only the add was skipped. */
|
|
79
|
+
requeueWithheld?: true;
|
|
72
80
|
/** Set when `--no-requeue` skipped the queue-label re-add. */
|
|
73
81
|
requeueSkipped?: true;
|
|
74
82
|
/** Set when the tracker refused one or more of this verb's label ops, so
|
|
@@ -121,6 +129,7 @@ export async function unblockIssue(
|
|
|
121
129
|
store: Store,
|
|
122
130
|
issue: number,
|
|
123
131
|
opts: { force?: boolean; requeue?: boolean } = {},
|
|
132
|
+
caps: Caps,
|
|
124
133
|
): Promise<UnblockOutcome> {
|
|
125
134
|
const requeue = opts.requeue !== false;
|
|
126
135
|
// Read before any label is touched: terminality is the whole of the argument
|
|
@@ -145,6 +154,16 @@ export async function unblockIssue(
|
|
|
145
154
|
active,
|
|
146
155
|
};
|
|
147
156
|
|
|
157
|
+
// The dispatcher's own admission gates, as daemon.ts computes them (#348):
|
|
158
|
+
// every failed-implementation slot spent, or the continuation budget
|
|
159
|
+
// exceeded, and the issue is held (`failed-attempts` / `continuations`) on
|
|
160
|
+
// every subsequent tick forever. A re-queued issue in that state is exactly
|
|
161
|
+
// the permanent admission hold this verb must not manufacture, so the
|
|
162
|
+
// re-queue half below is decided on the same predicates that decide the hold.
|
|
163
|
+
const budgetSpent =
|
|
164
|
+
!hasFailedAttemptBudget(counts.failuresUsed, caps.maxAttemptsPerIssue) ||
|
|
165
|
+
!hasContinuationBudget(counts.continuationsUsed, caps.maxContinuationsPerIssue);
|
|
166
|
+
|
|
148
167
|
// The one case where this verb refuses. Clearing the labels here re-queues an
|
|
149
168
|
// issue whose next claim starts by force-removing the worktree that holds the
|
|
150
169
|
// only copy of the last attempt's work (#118) — and unlike every other state
|
|
@@ -185,14 +204,22 @@ export async function unblockIssue(
|
|
|
185
204
|
// its PR is live but no process writes to its branch — so a pushed
|
|
186
205
|
// continuation still gets the label: the dispatcher bypasses worker-free
|
|
187
206
|
// pushed-green rows (#175), and `isEligible` requires the queue label once
|
|
188
|
-
// the PR resolves closed-unmerged.
|
|
207
|
+
// the PR resolves closed-unmerged. And never when the failed-attempt or
|
|
208
|
+
// continuation budget is spent (#348): the dispatcher holds such an issue
|
|
209
|
+
// forever, so the label would claim a candidacy the next admission cannot
|
|
210
|
+
// deliver — the clears still happen, the add is withheld.
|
|
189
211
|
let requeued: true | undefined;
|
|
212
|
+
let requeueWithheld: true | undefined;
|
|
190
213
|
let requeueSkipped: true | undefined;
|
|
191
214
|
let labelSyncQueued: number | undefined;
|
|
192
215
|
if (!live) {
|
|
193
216
|
if (requeue) {
|
|
194
|
-
|
|
195
|
-
|
|
217
|
+
if (budgetSpent) {
|
|
218
|
+
requeueWithheld = true;
|
|
219
|
+
} else {
|
|
220
|
+
ops.push({ issue, op: "add", label: project.queueLabel });
|
|
221
|
+
requeued = true;
|
|
222
|
+
}
|
|
196
223
|
} else {
|
|
197
224
|
requeueSkipped = true;
|
|
198
225
|
}
|
|
@@ -222,6 +249,7 @@ export async function unblockIssue(
|
|
|
222
249
|
...(latest === undefined ? {} : { latest }),
|
|
223
250
|
...(held === undefined ? {} : { forced: true as const }),
|
|
224
251
|
...(requeued === undefined ? {} : { requeued }),
|
|
252
|
+
...(requeueWithheld === undefined ? {} : { requeueWithheld }),
|
|
225
253
|
...(requeueSkipped === undefined ? {} : { requeueSkipped }),
|
|
226
254
|
...(labelSyncQueued === undefined ? {} : { labelSyncQueued }),
|
|
227
255
|
};
|
|
@@ -231,10 +259,14 @@ export async function unblockIssue(
|
|
|
231
259
|
* What the operator reads back. It promises a re-claim only when one can
|
|
232
260
|
* actually happen, because a promise made blindly sends someone away believing
|
|
233
261
|
* work had resumed and they find out by waiting for it. Three things withhold
|
|
234
|
-
*
|
|
235
|
-
* attempt budget makes the next tick escalate
|
|
236
|
-
* the in-progress label is only released for
|
|
237
|
-
* run row at all, where that label may still
|
|
262
|
+
* the re-claim promise: a live run still owns the issue through
|
|
263
|
+
* `agent:in-progress`, a spent attempt budget makes the next tick escalate
|
|
264
|
+
* rather than dispatch, and — since the in-progress label is only released for
|
|
265
|
+
* a terminal run — an issue with no run row at all, where that label may still
|
|
266
|
+
* be sitting there unread. A spent budget also withholds the queue label
|
|
267
|
+
* itself, on the same predicate the dispatcher holds on (#348): re-adding it
|
|
268
|
+
* would manufacture a candidate the next tick can never admit, and the report
|
|
269
|
+
* says so instead of claiming a restore that did not happen. #18 was
|
|
238
270
|
* filed against this function saying `next tick eligible again` in a case where
|
|
239
271
|
* it was not, so the wording is a contract rather than prose.
|
|
240
272
|
*
|
|
@@ -297,16 +329,28 @@ export function formatUnblock(
|
|
|
297
329
|
`"${project.stateLabels.inProgress}" until it ends — nothing is re-claimed before then`,
|
|
298
330
|
` queue "${project.queueLabel}" not restored — a run is still active; re-run unblock once it settles`,
|
|
299
331
|
);
|
|
300
|
-
} else if (o.failuresUsed
|
|
332
|
+
} else if (!hasFailedAttemptBudget(o.failuresUsed, caps.maxAttemptsPerIssue)) {
|
|
301
333
|
lines.push(
|
|
302
334
|
` next tick not eligible: all ${caps.maxAttemptsPerIssue} failed attempts are spent. ` +
|
|
303
335
|
"Rewrite the issue or raise maxAttemptsPerIssue.",
|
|
304
336
|
);
|
|
305
|
-
|
|
337
|
+
if (o.requeueWithheld === true) {
|
|
338
|
+
lines.push(
|
|
339
|
+
` queue "${project.queueLabel}" withheld — re-adding it would manufacture a candidate the ` +
|
|
340
|
+
"dispatcher holds as failed-attempts forever; rewrite the issue or raise maxAttemptsPerIssue before re-queueing",
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
} else if (!hasContinuationBudget(o.continuationsUsed, caps.maxContinuationsPerIssue)) {
|
|
306
344
|
lines.push(
|
|
307
345
|
` next tick not eligible: the ${caps.maxContinuationsPerIssue}-continuation budget was exceeded. ` +
|
|
308
346
|
"Inspect progress or raise maxContinuationsPerIssue.",
|
|
309
347
|
);
|
|
348
|
+
if (o.requeueWithheld === true) {
|
|
349
|
+
lines.push(
|
|
350
|
+
` queue "${project.queueLabel}" withheld — re-adding it would manufacture a candidate the ` +
|
|
351
|
+
"dispatcher holds as continuations forever; inspect progress or raise maxContinuationsPerIssue before re-queueing",
|
|
352
|
+
);
|
|
353
|
+
}
|
|
310
354
|
} else if (latest === undefined) {
|
|
311
355
|
lines.push(
|
|
312
356
|
` next tick eligible once the issue carries "${project.queueLabel}" and no state label — with no ` +
|