omp-conductor 0.15.9 → 0.15.11
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 -2543
- package/REFERENCE.md +2638 -0
- package/package.json +3 -2
- package/schema/config.schema.json +8 -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 +55 -29
- package/src/briefs/policy.md +14 -5
- package/src/briefs/worker.md +7 -1
- package/src/chain-check.ts +1 -1
- package/src/check-trailing-newlines.ts +82 -0
- package/src/cli.ts +190 -1391
- 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 +49 -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 +98 -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 +134 -0
- package/src/commands/ledger.ts +69 -0
- package/src/commands/message.ts +48 -0
- package/src/commands/report.ts +170 -0
- package/src/commands/restart.ts +76 -0
- package/src/commands/resume.ts +58 -0
- package/src/commands/setup.ts +93 -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 +23 -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 +38 -1
- package/src/config.ts +10 -3
- package/src/daemon.ts +613 -94
- package/src/dashboard/app.js +120 -0
- package/src/dashboard/index.html +34 -0
- package/src/dashboard/server.ts +267 -0
- package/src/dashboard/style.css +180 -0
- package/src/decisions.ts +39 -14
- package/src/diff-flags.ts +131 -241
- package/src/doctor.ts +795 -0
- package/src/escalate.ts +60 -19
- package/src/failure-class.ts +29 -3
- package/src/fleet.ts +58 -1
- package/src/graph-health.ts +1 -1
- package/src/label-projection.ts +1 -1
- package/src/lifecycle.ts +198 -2
- package/src/notices.ts +9 -0
- package/src/omp.ts +2 -0
- package/src/orchestrator-tick.ts +315 -17
- package/src/release-policy.ts +135 -23
- package/src/reports.ts +19 -5
- package/src/setup-host.ts +420 -8
- package/src/setup-install.ts +69 -14
- package/src/setup-wizard.ts +199 -61
- package/src/setup.ts +131 -35
- package/src/stats.ts +331 -0
- package/src/store.ts +206 -21
- package/src/tracker/github.ts +27 -4
- package/src/types.ts +144 -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 +295 -26
- package/src/verbs/actions.ts +73 -1
- package/src/verbs/protocol.ts +29 -4
- package/src/verbs/server.ts +183 -20
- package/systemd/omp-conductor-recover.sh +433 -0
- package/systemd/omp-conductor.service.example +7 -0
- 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
|
/**
|
|
@@ -335,11 +347,16 @@ export const AUTHORITY_HOLDERS = ["human", "orchestrator"] as const;
|
|
|
335
347
|
export type AuthorityHolder = (typeof AUTHORITY_HOLDERS)[number];
|
|
336
348
|
|
|
337
349
|
/**
|
|
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
|
|
350
|
+
* What a project that never answered the question gets: humans keep all three.
|
|
351
|
+
* A default that delegated merging would hand a fresh fleet write access to
|
|
352
|
+
* its own main branch on the strength of an unread config file, and a default
|
|
353
|
+
* that delegated promotion would let the orchestrator start spend unasked.
|
|
341
354
|
*/
|
|
342
|
-
export const DEFAULT_AUTHORITY: ProjectConfig["authority"]
|
|
355
|
+
export const DEFAULT_AUTHORITY: ProjectConfig["authority"] & { promotion: AuthorityHolder } = {
|
|
356
|
+
merge: "human",
|
|
357
|
+
release: "human",
|
|
358
|
+
promotion: "human",
|
|
359
|
+
};
|
|
343
360
|
|
|
344
361
|
/**
|
|
345
362
|
* Whether a merge candidate must be up to date with the branch it targets.
|
|
@@ -621,12 +638,17 @@ export interface ProjectConfig {
|
|
|
621
638
|
*/
|
|
622
639
|
escalation: { telegramChatId?: string; telegramTopicId?: number; fallbackToIssueComment: boolean; orchestrator: OrchestratorMode };
|
|
623
640
|
/**
|
|
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.
|
|
641
|
+
* Who lands green PRs, who cuts releases and who promotes. The daemon never
|
|
642
|
+
* acts on this itself — it words the orchestrator's standing orders and the
|
|
643
|
+
* rendered brief scaffold with it, so config and prompt can never disagree
|
|
644
|
+
* about which of them is holding the merge button.
|
|
645
|
+
*
|
|
646
|
+
* `promotion` is the queue-label sign-off: adding the label to an issue lets
|
|
647
|
+
* a worker claim it and starts spend, so POLICY.md must state its owner and
|
|
648
|
+
* a config that never answered defaults to the human — a gate whose owner is
|
|
649
|
+
* unstated must never resolve to the orchestrator.
|
|
628
650
|
*/
|
|
629
|
-
authority: { merge: AuthorityHolder; release: AuthorityHolder };
|
|
651
|
+
authority: { merge: AuthorityHolder; release: AuthorityHolder; promotion?: AuthorityHolder };
|
|
630
652
|
/**
|
|
631
653
|
* Per-shape tool-call enforcement for releases and deploys. Optional only for
|
|
632
654
|
* configs written before the tripwire existed; both omission and any shape key
|
|
@@ -755,17 +777,10 @@ export interface PrDiff {
|
|
|
755
777
|
* vocabulary that lives in three places drifts in three directions.
|
|
756
778
|
*/
|
|
757
779
|
export const SETTLEMENT_FLAG_KINDS = [
|
|
758
|
-
/** The
|
|
759
|
-
*
|
|
760
|
-
|
|
761
|
-
/** The report carried no usable `changed:` line, so nothing was disclosed. */
|
|
780
|
+
/** The settlement could not read the PR's diff, so no `changed:` file list
|
|
781
|
+
* could be derived — the one disclosure fault that survives, because a
|
|
782
|
+
* tree no one can read has nothing to disclose. */
|
|
762
783
|
"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
784
|
/** A test file left the tree and no rename in the PR accounts for it. */
|
|
770
785
|
"test-file-deleted",
|
|
771
786
|
/** A skip/only/focus marker appears on a line the PR added. */
|
|
@@ -944,8 +959,13 @@ export interface Tracker {
|
|
|
944
959
|
* Verify that a pull request is open, ready, and has a non-empty
|
|
945
960
|
* terminal-success check rollup. When `expectedHead` is supplied, also
|
|
946
961
|
* require the live head to match it.
|
|
962
|
+
*
|
|
963
|
+
* `opts.read` switches from the merge gate's "may this merge" to the read
|
|
964
|
+
* surface's "what is this PR's state": a merged or closed PR is then a
|
|
965
|
+
* reported fact rather than an `expected OPEN` refusal. Mutating verbs never
|
|
966
|
+
* pass `read`, so their open-PR gate is unchanged.
|
|
947
967
|
*/
|
|
948
|
-
verifyPr(url: string, expectedHead?: string): Promise<PrVerification | undefined>;
|
|
968
|
+
verifyPr(url: string, expectedHead?: string, opts?: { read?: boolean }): Promise<PrVerification | undefined>;
|
|
949
969
|
/**
|
|
950
970
|
* The pull request's diff, or undefined when this adapter could not produce
|
|
951
971
|
* one — an unparseable URL, a deleted PR, a flaky network.
|
|
@@ -1010,6 +1030,8 @@ export const FAILURE_CLASSES = [
|
|
|
1010
1030
|
"env-start-failure",
|
|
1011
1031
|
"turn-cap-progress",
|
|
1012
1032
|
"turn-cap-spinning",
|
|
1033
|
+
"wall-clock-cap-progress",
|
|
1034
|
+
"wall-clock-cap-spinning",
|
|
1013
1035
|
"admin-kill",
|
|
1014
1036
|
"ci-infra",
|
|
1015
1037
|
"ci-deterministic",
|
|
@@ -1144,10 +1166,10 @@ export interface RunRecord {
|
|
|
1144
1166
|
* exactly as an unflagged one does, and the flags are evidence for whoever
|
|
1145
1167
|
* reviews the PR. Absent means the audit found nothing, or never ran. */
|
|
1146
1168
|
settlementFlags?: SettlementFlag[];
|
|
1147
|
-
/** The worker's settlement report text for this attempt
|
|
1148
|
-
*
|
|
1149
|
-
* (#
|
|
1150
|
-
* worker returned a report. */
|
|
1169
|
+
/** The worker's settlement report text for this attempt: the worker's own
|
|
1170
|
+
* narrative, with the `changed:` file list rewritten at settlement from
|
|
1171
|
+
* the PR's actual diff (#488). Absent means the run predates the column, or
|
|
1172
|
+
* was killed before the worker returned a report. */
|
|
1151
1173
|
report?: string;
|
|
1152
1174
|
/**
|
|
1153
1175
|
* Why this run ended badly, and what the daemon did about it (#132). Absent
|
|
@@ -1162,6 +1184,15 @@ export interface RunRecord {
|
|
|
1162
1184
|
recoveredAt?: number;
|
|
1163
1185
|
}
|
|
1164
1186
|
|
|
1187
|
+
/**
|
|
1188
|
+
* A partial patch for {@link updateRun}. Every key is optional; an absent key
|
|
1189
|
+
* and an explicitly-`undefined` value both leave the column alone, while an
|
|
1190
|
+
* explicit `null` clears it (#468). `null` exists only for the columns a
|
|
1191
|
+
* caller may legitimately empty — a settle can never widen into a column it
|
|
1192
|
+
* did not mean to write.
|
|
1193
|
+
*/
|
|
1194
|
+
export type RunPatch = { [K in keyof RunRecord]?: RunRecord[K] | null };
|
|
1195
|
+
|
|
1165
1196
|
/** A one-shot turn budget waiting for one issue's next claimed attempt. */
|
|
1166
1197
|
export interface TurnOverride {
|
|
1167
1198
|
project: string;
|
|
@@ -1211,6 +1242,13 @@ export interface DispatchSummary {
|
|
|
1211
1242
|
/** True only for system/API failures, never ordinary policy holds. */
|
|
1212
1243
|
degraded: boolean;
|
|
1213
1244
|
holds: AdmissionHoldSummary[];
|
|
1245
|
+
/** Runs the pass settled while it ran — merged PRs terminalised by the settle
|
|
1246
|
+
* sweep and by classification recovery. Optional: persisted old rows lack
|
|
1247
|
+
* it, so readers use `?? 0`. (#497) */
|
|
1248
|
+
settled?: number;
|
|
1249
|
+
/** True for a held pass's own record (#497): the queue was never routed, so
|
|
1250
|
+
* ready/routed/claimed are absent queue facts, not an empty queue. */
|
|
1251
|
+
paused?: boolean;
|
|
1214
1252
|
}
|
|
1215
1253
|
|
|
1216
1254
|
/** Admission holds that indicate repairable friction rather than normal flow control. */
|
|
@@ -1261,11 +1299,22 @@ export interface FrictionSignal {
|
|
|
1261
1299
|
* today's digest already been handed over?" without asking the model to
|
|
1262
1300
|
* remember, which is the memory #123 found unreliable.
|
|
1263
1301
|
*
|
|
1302
|
+
* The remaining values are the interrupt categories themselves. Handing a
|
|
1303
|
+
* report over as `fleet-stopped` or `tier2` (or `decision-needed`,
|
|
1304
|
+
* `confirmed-failure`) is the escalation handoff: the project's reporting
|
|
1305
|
+
* policy decides between sending now and holding, exactly as it does for a
|
|
1306
|
+
* daemon escalation of that category, and an identical retry is refused only
|
|
1307
|
+
* while the earlier handoff is still undelivered (#453).
|
|
1308
|
+
*
|
|
1264
1309
|
* Declared as data for the same reason as {@link REPORT_SCOPES}: the CLI verb
|
|
1265
1310
|
* that accepts a kind and the outbox that renders one enumerate the same list,
|
|
1266
1311
|
* so a third kind cannot be added while either still knows only two.
|
|
1267
1312
|
*/
|
|
1268
|
-
export const REPORT_KINDS = [
|
|
1313
|
+
export const REPORT_KINDS = [
|
|
1314
|
+
"material",
|
|
1315
|
+
"digest",
|
|
1316
|
+
...INTERRUPT_CATEGORIES.filter((category) => category !== "material"),
|
|
1317
|
+
] as const;
|
|
1269
1318
|
|
|
1270
1319
|
export type ReportKind = (typeof REPORT_KINDS)[number];
|
|
1271
1320
|
|
|
@@ -1377,7 +1426,41 @@ export interface DigestBacklog {
|
|
|
1377
1426
|
heldNoticeCount: number;
|
|
1378
1427
|
/** Subset held only for the next availability window. */
|
|
1379
1428
|
availabilityHeldNoticeCount?: number;
|
|
1429
|
+
/** Oldest held notice across both held groups. */
|
|
1380
1430
|
heldNoticeOldestAt?: number;
|
|
1431
|
+
/** Oldest held notice that is digest-bound (`releaseOnAvailable` false). */
|
|
1432
|
+
digestHeldNoticeOldestAt?: number;
|
|
1433
|
+
/** Oldest held notice waiting only for the availability window. */
|
|
1434
|
+
availabilityHeldNoticeOldestAt?: number;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/** Where one captured raw idea is in its lifecycle (#298). `pending` is a raw
|
|
1438
|
+
* idea awaiting grooming; `groomed` became an issue (#300, which records the
|
|
1439
|
+
* issue URL); `dismissed` was dropped without becoming anything. */
|
|
1440
|
+
export const INTAKE_STATES = ["pending", "groomed", "dismissed"] as const;
|
|
1441
|
+
|
|
1442
|
+
export type IntakeState = (typeof INTAKE_STATES)[number];
|
|
1443
|
+
|
|
1444
|
+
/** One raw idea captured by `omp-conductor intake`, durable in the store so an
|
|
1445
|
+
* idea from 02:00 is still there after a restart (#299). */
|
|
1446
|
+
export interface IntakeItem {
|
|
1447
|
+
id: string;
|
|
1448
|
+
project: string;
|
|
1449
|
+
/** The idea text verbatim, as captured. */
|
|
1450
|
+
text: string;
|
|
1451
|
+
createdAt: number;
|
|
1452
|
+
state: IntakeState;
|
|
1453
|
+
/** The issue this idea became, once #300 grooms it. */
|
|
1454
|
+
issueUrl?: string;
|
|
1455
|
+
/** When the item was resolved into an issue (#300) or dismissed. */
|
|
1456
|
+
groomedAt?: number;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/** What a caller hands over. The store owns the id, the state and the timestamps. */
|
|
1460
|
+
export interface IntakeDraft {
|
|
1461
|
+
project: string;
|
|
1462
|
+
text: string;
|
|
1463
|
+
at: number;
|
|
1381
1464
|
}
|
|
1382
1465
|
|
|
1383
1466
|
/**
|
|
@@ -1460,7 +1543,8 @@ export interface LabelOp {
|
|
|
1460
1543
|
export interface Store {
|
|
1461
1544
|
/** A claimed run atomically consumes and applies its issue's pending turn override. */
|
|
1462
1545
|
createRun(r: Omit<RunRecord, "id">): RunRecord;
|
|
1463
|
-
|
|
1546
|
+
/** Partial patch; an explicit `null` clears a column, `undefined`/absence leaves it alone (#468). */
|
|
1547
|
+
updateRun(id: string, patch: RunPatch): void;
|
|
1464
1548
|
getRun(id: string): RunRecord | undefined;
|
|
1465
1549
|
/** Runs whose issue is occupied: a live worker, or a green PR awaiting merge. */
|
|
1466
1550
|
activeRuns(project: string): RunRecord[];
|
|
@@ -1471,6 +1555,11 @@ export interface Store {
|
|
|
1471
1555
|
/** Newest attempt per issue for the live board. Non-merged work remains
|
|
1472
1556
|
* visible; merged rows are bounded by the supplied recent-history cutoff. */
|
|
1473
1557
|
recentRuns(project: string, mergedSinceEpochMs: number): RunRecord[];
|
|
1558
|
+
/** Every attempt that recorded one PR URL, newest first. Mediated PR verbs
|
|
1559
|
+
* resolve ownership through this, not through `recentRuns`' newest-attempt-
|
|
1560
|
+
* per-issue view — a requeued continuation must never hide the PR its
|
|
1561
|
+
* predecessor opened. Merged rows keep the same recent-history bound. */
|
|
1562
|
+
runsForProjectPr(project: string, prUrl: string, mergedSinceEpochMs: number): RunRecord[];
|
|
1474
1563
|
/** Merged rows whose post-merge workflow verdict is still pending, oldest first. */
|
|
1475
1564
|
runsNeedingBaseCheck(project: string, limit?: number): RunRecord[];
|
|
1476
1565
|
/** Replace the current live-head health row for one routed repository. */
|
|
@@ -1500,6 +1589,9 @@ export interface Store {
|
|
|
1500
1589
|
* tail` resolves an issue number to a transcript through this; the number is
|
|
1501
1590
|
* what an operator has, the run id is not. */
|
|
1502
1591
|
latestRun(project: string, issue: number): RunRecord | undefined;
|
|
1592
|
+
/** Every attempt for one issue, oldest first, so an escalation can group the
|
|
1593
|
+
* continuation budget by failure class (#439). */
|
|
1594
|
+
runsForIssue(project: string, issue: number): RunRecord[];
|
|
1503
1595
|
/** Persist a one-shot ceiling and append the operator action to its audit ledger. */
|
|
1504
1596
|
setTurnOverride(project: string, issue: number, maxTurns: number, setAt?: number): void;
|
|
1505
1597
|
turnOverride(project: string, issue: number): number | undefined;
|
|
@@ -1511,16 +1603,24 @@ export interface Store {
|
|
|
1511
1603
|
project: string,
|
|
1512
1604
|
opts?: { issue?: number; limit?: number },
|
|
1513
1605
|
): 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
1606
|
runsStartedSince(project: string, sinceEpochMs: number): number;
|
|
1520
1607
|
spendSince(project: string, sinceEpochMs: number): number;
|
|
1608
|
+
/** The rows one stats window needs (#282): every run settling at or after
|
|
1609
|
+
* `sinceEpochMs`, plus the full attempt chain of each issue whose merge
|
|
1610
|
+
* settled there (so continuation chains collapse into one journey), ordered
|
|
1611
|
+
* by issue then startedAt. Read-only. */
|
|
1612
|
+
statsRuns(project: string, sinceEpochMs: number): RunRecord[];
|
|
1613
|
+
/** Total tracked `gh` calls between two UTC day keys, inclusive (#198). */
|
|
1614
|
+
ghCallsBetween(sinceDay: string, untilDay: string): number;
|
|
1521
1615
|
/** Idempotence guard so a retry loop cannot page a human repeatedly for the
|
|
1522
1616
|
* same event. */
|
|
1523
1617
|
wasNotified(key: string): boolean;
|
|
1618
|
+
/** Whether an escalation handoff with this project, category and folded text
|
|
1619
|
+
* is still owed: a same-text report still in the outbox, or a same-text
|
|
1620
|
+
* held notice not yet digested. The CLI report surface refuses an identical
|
|
1621
|
+
* retry only while this is true (#453) — once the first handoff lands, the
|
|
1622
|
+
* same text is a new occurrence, not a double-send. */
|
|
1623
|
+
hasOpenEscalationHandoff(project: string, category: InterruptCategory, body: string): boolean;
|
|
1524
1624
|
recordDispatch(project: string, summary: DispatchSummary): void;
|
|
1525
1625
|
latestDispatch(project: string): DispatchSummary | undefined;
|
|
1526
1626
|
/** The newest `digest:` dedupe key a project has run toward without ending in
|
|
@@ -1544,6 +1644,13 @@ export interface Store {
|
|
|
1544
1644
|
getMaterialEvent(id: string): MaterialEvent | undefined;
|
|
1545
1645
|
/** Ordinary material outcomes still owed, oldest first and bounded. */
|
|
1546
1646
|
undigestedMaterialEvents(project: string, limit?: number): MaterialEvent[];
|
|
1647
|
+
/** Persist one raw idea, `pending`, for later grooming (#299). */
|
|
1648
|
+
recordIntake(draft: IntakeDraft): IntakeItem;
|
|
1649
|
+
/** Ideas still `pending`, oldest first — what `intake list` and `status` read. */
|
|
1650
|
+
pendingIntake(project: string): IntakeItem[];
|
|
1651
|
+
/** Resolve one idea to `groomed` (recording the issue URL #300 chose) or
|
|
1652
|
+
* `dismissed`. `false` when the id is unknown. */
|
|
1653
|
+
resolveIntake(id: string, state: "groomed" | "dismissed", issueUrl?: string): boolean;
|
|
1547
1654
|
/** Count and age source for status and digest prompt bounds. */
|
|
1548
1655
|
digestBacklog(project: string): DigestBacklog;
|
|
1549
1656
|
/** Add one bounded observation to the per-day friction rollup. */
|
|
@@ -1655,6 +1762,8 @@ export interface Store {
|
|
|
1655
1762
|
createDecision(draft: DecisionDraft): DecisionRecord;
|
|
1656
1763
|
/** Everything still owed an answer, oldest first — what a tick digest reads. */
|
|
1657
1764
|
openDecisions(project: string): DecisionRecord[];
|
|
1765
|
+
/** One row by id, whatever its state — what a bounded ask polls while waiting. */
|
|
1766
|
+
decision(id: string): DecisionRecord | undefined;
|
|
1658
1767
|
/** Answer or withdraw one. `false` when the id is unknown or already closed,
|
|
1659
1768
|
* so a double-resolve cannot overwrite the first answer. */
|
|
1660
1769
|
resolveDecision(id: string, state: "answered" | "withdrawn", resolution: string, at: number): boolean;
|
|
@@ -1805,6 +1914,10 @@ export const VERB_NAMES = [
|
|
|
1805
1914
|
"conductor_pr_merge",
|
|
1806
1915
|
"conductor_label",
|
|
1807
1916
|
"conductor_release",
|
|
1917
|
+
/** Request the fleet upgrade itself to a published conductor release. The
|
|
1918
|
+
* install shape gates it like a release shape; execution detaches from this
|
|
1919
|
+
* session and the daemon, and the first tick after the restart verifies. */
|
|
1920
|
+
"conductor_install",
|
|
1808
1921
|
/** The one read verb. It answers with the merge gate's own verdict, so
|
|
1809
1922
|
* "pushed-green" is the dispatcher's reading of the PR rather than a claim the
|
|
1810
1923
|
* 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 ` +
|