omp-conductor 0.17.0 → 0.18.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 +12 -8
- package/package.json +1 -1
- package/schema/config.schema.json +40 -1
- package/src/admission.ts +263 -44
- package/src/ask.ts +39 -3
- package/src/availability.ts +27 -1
- package/src/backups.ts +2 -2
- package/src/briefs/orchestrator.md +1 -0
- package/src/briefs/worker.md +38 -19
- package/src/command-help.ts +8 -1
- package/src/command-manifest.ts +5 -2
- package/src/commands/arm.ts +6 -3
- package/src/commands/message.ts +32 -4
- package/src/commands/watch.ts +62 -3
- package/src/config-schema.ts +53 -0
- package/src/config.ts +97 -1
- package/src/daemon.ts +1479 -1483
- package/src/decisions.ts +51 -6
- package/src/depends-on.ts +261 -1
- package/src/diff-flags.ts +350 -0
- package/src/digest-schedule.ts +37 -0
- package/src/doctor.ts +310 -22
- package/src/escalate.ts +560 -57
- package/src/failure-class.ts +71 -15
- package/src/fleet.ts +189 -34
- package/src/gitops.ts +103 -24
- package/src/graph-health.ts +20 -7
- package/src/graph.ts +313 -68
- package/src/lifecycle.ts +43 -7
- package/src/omp.ts +42 -0
- package/src/orchestrator-tick.ts +430 -162
- package/src/release-policy.ts +177 -5
- package/src/routing.ts +11 -3
- package/src/session-host.ts +16 -0
- package/src/settlement.ts +1728 -0
- package/src/setup-host.ts +193 -4
- package/src/setup-install.ts +91 -30
- package/src/setup-wizard.ts +1257 -78
- package/src/setup.ts +153 -6
- package/src/status-render.ts +36 -4
- package/src/store.ts +411 -17
- package/src/tracker/github.ts +607 -12
- package/src/types.ts +331 -5
- package/src/upgrade.ts +50 -19
- package/src/verbs/actions.ts +66 -18
- package/src/verbs/protocol.ts +45 -0
- package/src/verbs/server.ts +270 -13
- package/src/worker.ts +239 -6
- package/src/worktree.ts +115 -8
- package/systemd/omp-conductor-recover.sh +73 -0
- package/systemd/recover-unit-test.sh +61 -0
package/src/setup.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* least tested code in the package.
|
|
9
9
|
*
|
|
10
10
|
* Two functions here mutate something outside the process: `createMissingLabels`
|
|
11
|
-
* and
|
|
11
|
+
* (and its rollback compensation half, `deleteCreatedLabels`) and
|
|
12
|
+
* `writeOrchestratorBrief`. Everything else reads, or computes. That is what
|
|
12
13
|
* lets the plugin show a complete plan before asking for consent, and it is a
|
|
13
14
|
* property worth preserving — check it before adding a function.
|
|
14
15
|
*
|
|
@@ -38,11 +39,14 @@ import {
|
|
|
38
39
|
import {
|
|
39
40
|
clonePolicy,
|
|
40
41
|
configPath,
|
|
42
|
+
DEFAULT_STATE_LABELS,
|
|
41
43
|
defaultMirrorRoot,
|
|
42
44
|
defaultWorkspaceRoot,
|
|
45
|
+
resolveArmProof,
|
|
43
46
|
resolveCaps,
|
|
44
47
|
resolvePolicy,
|
|
45
48
|
resolveReleaseGrants,
|
|
49
|
+
resolveReview,
|
|
46
50
|
SCOPE_PRESETS,
|
|
47
51
|
stateDir,
|
|
48
52
|
} from "./config.ts";
|
|
@@ -50,13 +54,17 @@ import { graphProjectPath, graphRepos } from "./graph.ts";
|
|
|
50
54
|
import { ompSettingsOverlay } from "./omp-settings.ts";
|
|
51
55
|
import {
|
|
52
56
|
CONFIG_VERSION,
|
|
57
|
+
DEFAULT_ARM_PROOF,
|
|
53
58
|
DEFAULT_AUTHORITY,
|
|
54
59
|
DEFAULT_CAPS,
|
|
55
60
|
DEFAULT_PROJECT_POLICY,
|
|
56
61
|
DEFAULT_REPORT_SCOPE,
|
|
62
|
+
DEFAULT_REVIEW_POLICY,
|
|
57
63
|
DENIED_RELEASE_GRANTS,
|
|
58
64
|
RELEASE_SHAPES,
|
|
65
|
+
REVIEW_STRICTNESS,
|
|
59
66
|
WEEKDAYS,
|
|
67
|
+
type ArmProof,
|
|
60
68
|
type BaseFreshness,
|
|
61
69
|
type BehindBaseAction,
|
|
62
70
|
type AuthorityHolder,
|
|
@@ -71,6 +79,8 @@ import {
|
|
|
71
79
|
type ReleaseRequirement,
|
|
72
80
|
type ReportScopeChoice,
|
|
73
81
|
type ReportingPolicy,
|
|
82
|
+
type ReviewPolicy,
|
|
83
|
+
type ReviewStrictness,
|
|
74
84
|
type Weekday,
|
|
75
85
|
type WeeklyAvailability,
|
|
76
86
|
type RepoTarget,
|
|
@@ -205,6 +215,19 @@ export interface SetupAnswers {
|
|
|
205
215
|
* leave one to be defaulted by whichever reader gets there first.
|
|
206
216
|
*/
|
|
207
217
|
policy: ProjectPolicy;
|
|
218
|
+
/**
|
|
219
|
+
* How `arm` proves a human just approved arming (#613). Always complete:
|
|
220
|
+
* the wizard asks about it in the policy area, so an answers object can never
|
|
221
|
+
* leave it to be defaulted by whichever reader gets there first.
|
|
222
|
+
*/
|
|
223
|
+
armProof: ArmProof;
|
|
224
|
+
/**
|
|
225
|
+
* How green PRs are reviewed and returned (#678): the strictness level and
|
|
226
|
+
* the hard ceiling on review rounds per PR lifecycle. Always complete: the
|
|
227
|
+
* wizard asks about it in the policy area, so an answers object can never
|
|
228
|
+
* leave it to be defaulted by whichever reader gets there first.
|
|
229
|
+
*/
|
|
230
|
+
review: ReviewPolicy;
|
|
208
231
|
/**
|
|
209
232
|
* Hand-edited recovery merge authorizations carried through setup unchanged.
|
|
210
233
|
* The wizard never grants one; forgetting them during an unrelated amend
|
|
@@ -304,6 +327,8 @@ export const SETUP_DEFAULTS = {
|
|
|
304
327
|
releaseGrants: DENIED_RELEASE_GRANTS,
|
|
305
328
|
/** The strictest reading of the prose these conditions replaced (#129). */
|
|
306
329
|
policy: DEFAULT_PROJECT_POLICY,
|
|
330
|
+
/** The recommended review strictness and its default ceiling (#678). */
|
|
331
|
+
review: DEFAULT_REVIEW_POLICY,
|
|
307
332
|
/** The daemon runs its own triage session unless an operator already runs one. */
|
|
308
333
|
orchestratorMode: "embedded",
|
|
309
334
|
} as const;
|
|
@@ -381,8 +406,52 @@ export const BEHIND_BASE_CHOICES: { readonly [K in BehindBaseAction]: string } =
|
|
|
381
406
|
escalate: "page a human rather than guess",
|
|
382
407
|
};
|
|
383
408
|
|
|
409
|
+
/**
|
|
410
|
+
* What each arming proof means to the operator being asked about it, in their
|
|
411
|
+
* words rather than the gate's (conductor #613). The `claim-only` consequence
|
|
412
|
+
* is the one line the issue contract requires the question to state: anything
|
|
413
|
+
* that can invoke the already-privileged `omp-conductor arm` command can start
|
|
414
|
+
* dispatch once the live claim and poller pass. Mapped over the closed union
|
|
415
|
+
* so a third proof fails to compile here instead of reaching a wizard with no
|
|
416
|
+
* question for it.
|
|
417
|
+
*/
|
|
418
|
+
export const ARM_PROOF_CHOICES: { readonly [K in ArmProof]: string } = {
|
|
419
|
+
challenge: "an authenticated Telegram challenge proves a human just approved arming",
|
|
420
|
+
"claim-only": "anyone who can invoke the already-privileged `omp-conductor arm` command can start dispatch once the live claim and poller pass",
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* What each review strictness blocks, in the operator's words, and the single
|
|
425
|
+
* canonical phrasing of the three bars (#678).
|
|
426
|
+
*
|
|
427
|
+
* Mapped over the closed union for the reason {@link MERGE_DUTY} is: a fourth
|
|
428
|
+
* level fails to compile here instead of reaching a wizard with no question for
|
|
429
|
+
* it, an amend row that cannot describe it, and a rendered brief that shows it
|
|
430
|
+
* blank. Each sentence is self-contained — "Low's bar" would reference prose
|
|
431
|
+
* the brief may not be rendering — and it is the *same* text the wizard shows
|
|
432
|
+
* one level at a time and the composed orchestrator brief carries for the
|
|
433
|
+
* configured level, so the definition an operator chose is the threshold a
|
|
434
|
+
* session enforces.
|
|
435
|
+
*
|
|
436
|
+
* The two consumers read this map rather than their own prose: the setup
|
|
437
|
+
* dialog explains every level from it, and {@link reviewDuty} renders the
|
|
438
|
+
* effective one from it. "One canonical implementation, not duplicated prose
|
|
439
|
+
* branches" is this object.
|
|
440
|
+
*/
|
|
441
|
+
export const REVIEW_STRICTNESS_CHOICES: { readonly [K in ReviewStrictness]: string } = {
|
|
442
|
+
low: "correctness, security, data-loss or explicit acceptance-criteria failures — anything else stays a review comment",
|
|
443
|
+
medium:
|
|
444
|
+
"correctness, security, data-loss or explicit acceptance-criteria failures, plus material maintainability or " +
|
|
445
|
+
"reliability defects likely to become incidents within six months — anything else stays a review comment",
|
|
446
|
+
high:
|
|
447
|
+
"correctness, security, data-loss or explicit acceptance-criteria failures, plus material maintainability or " +
|
|
448
|
+
"reliability defects likely to become incidents within six months, plus concrete quality defects — never " +
|
|
449
|
+
"subjective style churn or unbounded refactoring",
|
|
450
|
+
};
|
|
451
|
+
|
|
384
452
|
export const RELEASE_REQUIREMENT_CHOICES: { readonly [K in ReleaseRequirement]: string } = {
|
|
385
|
-
"runs-settled": "every run
|
|
453
|
+
"runs-settled": "every run of the released repo actually merged, not merely reached a green PR",
|
|
454
|
+
"fleet-runs-settled": "every run in the project actually merged — suite-wide strictness for shapes that consume several repos",
|
|
386
455
|
"no-open-prs": "no pull request is still open against the branch being released",
|
|
387
456
|
"queue-drained": "nothing still carries the queue label",
|
|
388
457
|
"base-branch-green": "the newest observed post-merge base-branch workflows are green",
|
|
@@ -440,6 +509,34 @@ export const MERGE_DUTY: { readonly [K in ProjectConfig["authority"]["merge"]]:
|
|
|
440
509
|
" one is a hard boundary, not a preference.",
|
|
441
510
|
};
|
|
442
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Duty 1's review-return contract, worded from `project.review` (#678).
|
|
514
|
+
*
|
|
515
|
+
* One canonical implementation rather than prose branches in the template: the
|
|
516
|
+
* configured level's bar is the same {@link REVIEW_STRICTNESS_CHOICES} text
|
|
517
|
+
* the setup dialog explains each level with, and the ceiling is the configured
|
|
518
|
+
* number — so the strictness an operator chose in setup and the threshold this
|
|
519
|
+
* session enforces cannot diverge. The paragraph names `conductor_pr_review`
|
|
520
|
+
* (the #677 verb) as the only return path and states the three ceiling
|
|
521
|
+
* behaviours: leave the PR open, record the unresolved findings, escalate once.
|
|
522
|
+
*/
|
|
523
|
+
export function reviewDuty(p: ProjectConfig): string {
|
|
524
|
+
const { strictness, maxRounds } = resolveReview(p);
|
|
525
|
+
return (
|
|
526
|
+
"\n" +
|
|
527
|
+
"**Review policy:** before you merge (or settle) a green PR, review it at\n" +
|
|
528
|
+
`this project's strictness. The level is **${strictness}**: ` +
|
|
529
|
+
`${REVIEW_STRICTNESS_CHOICES[strictness]}. ` +
|
|
530
|
+
"`conductor_pr_review` is the verb for findings at or above that bar, and\n" +
|
|
531
|
+
"for nothing else — anything below it stays a review comment on the PR,\n" +
|
|
532
|
+
"never a return. Every corrected head gets a fresh review, up to a hard\n" +
|
|
533
|
+
`ceiling of ${maxRounds} ${maxRounds === 1 ? "round" : "rounds"} per PR lifecycle, visible in ` +
|
|
534
|
+
"`omp-conductor status` as `review-revision N`. At the ceiling\n" +
|
|
535
|
+
"`conductor_pr_review` refuses: leave the PR open, record the unresolved\n" +
|
|
536
|
+
"findings, and escalate once — never a further round."
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
443
540
|
/**
|
|
444
541
|
* The Promotion paragraph, worded from `authority.promotion`.
|
|
445
542
|
*
|
|
@@ -700,11 +797,33 @@ export async function createMissingLabels(trackerRepo: string, plan: LabelPlan[]
|
|
|
700
797
|
continue;
|
|
701
798
|
}
|
|
702
799
|
if (/already exists/i.test(r.stderr)) continue;
|
|
703
|
-
|
|
800
|
+
// The labels created before this one failed ride on the error: the setup
|
|
801
|
+
// transaction compensates the tracker exactly, and a throw without the
|
|
802
|
+
// partial list would orphan them (#652).
|
|
803
|
+
throw Object.assign(
|
|
804
|
+
new Error(`Could not create label "${label.name}" in ${trackerRepo}: ${briefly(r.stderr)}`),
|
|
805
|
+
{ created },
|
|
806
|
+
);
|
|
704
807
|
}
|
|
705
808
|
return created;
|
|
706
809
|
}
|
|
707
810
|
|
|
811
|
+
/**
|
|
812
|
+
* Compensation half of {@link createMissingLabels}: deletes exactly the labels
|
|
813
|
+
* a failed setup apply created, so the tracker returns to its pre-entry state.
|
|
814
|
+
* Runs only on rollback, where a deletion failure is a reported restoration
|
|
815
|
+
* failure — the orphaned label is named, never silently left behind (#652).
|
|
816
|
+
*/
|
|
817
|
+
export async function deleteCreatedLabels(trackerRepo: string, created: string[]): Promise<void> {
|
|
818
|
+
const failures: string[] = [];
|
|
819
|
+
for (const name of created) {
|
|
820
|
+
const r = await gh(["label", "delete", name, "--repo", trackerRepo, "--yes"]);
|
|
821
|
+
if (r.code === 0 || /not found/i.test(r.stderr)) continue;
|
|
822
|
+
failures.push(`could not delete label "${name}" in ${trackerRepo}: ${briefly(r.stderr)}`);
|
|
823
|
+
}
|
|
824
|
+
if (failures.length > 0) throw new Error(failures.join("; "));
|
|
825
|
+
}
|
|
826
|
+
|
|
708
827
|
const QUIET_INTERRUPT_ON: InterruptCategory[] = [
|
|
709
828
|
"tier2",
|
|
710
829
|
"fleet-stopped",
|
|
@@ -854,7 +973,7 @@ export function buildProject(a: SetupAnswers): ProjectConfig {
|
|
|
854
973
|
name: a.projectName,
|
|
855
974
|
tracker: { kind: "github", repo: a.trackerRepo },
|
|
856
975
|
queueLabel: a.queueLabel,
|
|
857
|
-
stateLabels: { ...a.stateLabels },
|
|
976
|
+
stateLabels: { ...a.stateLabels, backlog: DEFAULT_STATE_LABELS.backlog },
|
|
858
977
|
routing: { labelPrefix: a.routingLabelPrefix, repos },
|
|
859
978
|
caps,
|
|
860
979
|
...(a.workerModel !== undefined && a.workerModel.trim().length > 0
|
|
@@ -877,6 +996,14 @@ export function buildProject(a: SetupAnswers): ProjectConfig {
|
|
|
877
996
|
// Written out in full for the same reason: the file then says what a merge
|
|
878
997
|
// and a release require without anyone having to know a default (#129).
|
|
879
998
|
policy: clonePolicy(a.policy),
|
|
999
|
+
// Written out even when it is the default, so an operator amending the
|
|
1000
|
+
// policy has a line in the file to point at — and the recovery playbook
|
|
1001
|
+
// can read which proof the project opted into (#613).
|
|
1002
|
+
arm: { proof: a.armProof },
|
|
1003
|
+
// Written out in full for the same reason as `arm`: the file then carries
|
|
1004
|
+
// the strictness and the ceiling an operator chose, without anyone having
|
|
1005
|
+
// to know a migration rule (#678).
|
|
1006
|
+
review: { ...a.review },
|
|
880
1007
|
...(a.recoveryMerges === undefined
|
|
881
1008
|
? {}
|
|
882
1009
|
: { recoveryMerges: a.recoveryMerges.map((entry) => ({ ...entry })) }),
|
|
@@ -964,6 +1091,13 @@ export function defaultAnswers(projectName: string, opts: { added?: boolean } =
|
|
|
964
1091
|
authority: { ...SETUP_DEFAULTS.authority },
|
|
965
1092
|
releaseGrants: { ...SETUP_DEFAULTS.releaseGrants },
|
|
966
1093
|
policy: clonePolicy(SETUP_DEFAULTS.policy),
|
|
1094
|
+
// The strict reading, matching the loader's absent-key default: a project
|
|
1095
|
+
// that never answered keeps today's authenticated challenge round-trip.
|
|
1096
|
+
armProof: DEFAULT_ARM_PROOF,
|
|
1097
|
+
// First-run default: the recommended strictness and its ceiling, so a
|
|
1098
|
+
// fresh project opens on the answer the issue recommends — and the loader
|
|
1099
|
+
// materialises the same value for a config that has no key at all.
|
|
1100
|
+
review: { ...SETUP_DEFAULTS.review },
|
|
967
1101
|
orchestratorMode: SETUP_DEFAULTS.orchestratorMode,
|
|
968
1102
|
reportScope: SETUP_DEFAULT_REPORT_SCOPE,
|
|
969
1103
|
writeOrchestratorBrief: false,
|
|
@@ -1060,6 +1194,13 @@ export function answersFromProject(p: ProjectConfig): SetupAnswers {
|
|
|
1060
1194
|
authority: { ...p.authority },
|
|
1061
1195
|
releaseGrants: resolveReleaseGrants(p),
|
|
1062
1196
|
policy: resolvePolicy(p),
|
|
1197
|
+
// The loader materialises `arm` complete, so this is the answer the project
|
|
1198
|
+
// actually has — never a default guessed at the amend prompt.
|
|
1199
|
+
armProof: resolveArmProof(p),
|
|
1200
|
+
// Same reason as `armProof`: the loader materialises `review` complete, so
|
|
1201
|
+
// a re-run preserves the configured strictness and ceiling rather than
|
|
1202
|
+
// opening on a default the operator already answered.
|
|
1203
|
+
review: resolveReview(p),
|
|
1063
1204
|
orchestratorMode: p.escalation.orchestrator,
|
|
1064
1205
|
reportScope: reportScopeFromPolicy(p.reporting),
|
|
1065
1206
|
writeOrchestratorBrief: false,
|
|
@@ -1127,6 +1268,7 @@ function briefVarsForProject(p: ProjectConfig): Record<string, string> {
|
|
|
1127
1268
|
QUEUE_LABEL: p.queueLabel,
|
|
1128
1269
|
RELEASES_DEFAULT: RELEASES_DEFAULTS[`${p.authority.merge}/${p.authority.release}`],
|
|
1129
1270
|
MERGE_DUTY: MERGE_DUTY[p.authority.merge],
|
|
1271
|
+
REVIEW_DUTY: reviewDuty(p),
|
|
1130
1272
|
PROMOTION_DUTY: PROMOTION_DUTIES[p.authority.promotion ?? DEFAULT_AUTHORITY.promotion],
|
|
1131
1273
|
POLICY_SOURCE: policySourceLine(p),
|
|
1132
1274
|
};
|
|
@@ -1636,6 +1778,9 @@ export function summarisePlan(
|
|
|
1636
1778
|
` environments ${a.policy.release.environments.join(", ") || "none declared — every deploy target is refused"}`,
|
|
1637
1779
|
);
|
|
1638
1780
|
|
|
1781
|
+
lines.push("", "review", ` strictness ${a.review.strictness} — ${REVIEW_STRICTNESS_CHOICES[a.review.strictness]}`);
|
|
1782
|
+
lines.push(` max rounds/PR ${a.review.maxRounds} — at the ceiling the PR is left open, the findings recorded, and it is escalated once`);
|
|
1783
|
+
|
|
1639
1784
|
const reporting = project.reporting as ReportingPolicy;
|
|
1640
1785
|
const briefPath = orchestratorBriefPath(a);
|
|
1641
1786
|
lines.push(
|
|
@@ -1852,16 +1997,18 @@ export const AMEND_AREAS: {
|
|
|
1852
1997
|
},
|
|
1853
1998
|
policy: {
|
|
1854
1999
|
name: "merge & release preconditions",
|
|
1855
|
-
asks: "the checks, base freshness, draft rule and behind-base action for a merge, then what a release requires and what it ships",
|
|
2000
|
+
asks: "the checks, base freshness, draft rule and behind-base action for a merge, then what a release requires and what it ships — and how green PRs are reviewed, and how many rounds a PR may be returned",
|
|
1856
2001
|
describe: (p) => {
|
|
1857
2002
|
const policy = resolvePolicy(p);
|
|
2003
|
+
const review = resolveReview(p);
|
|
1858
2004
|
// Counted rather than listed: this row is elided at 96 characters, and the
|
|
1859
2005
|
// full table is in the plan summary the consent screen shows next.
|
|
1860
2006
|
return (
|
|
1861
2007
|
`merge: ${policy.merge.requiredChecks.length === 0 ? "every check" : `${policy.merge.requiredChecks.length} check(s)`}, ` +
|
|
1862
2008
|
`base ${policy.merge.baseFreshness}, drafts ${policy.merge.drafts}, behind → ${policy.merge.whenBehindBase}; ` +
|
|
1863
2009
|
`release: ${policy.release.requires.length} must-land, ${policy.release.artefacts.length} artefact(s), ` +
|
|
1864
|
-
`${policy.release.environments.length} env(s)`
|
|
2010
|
+
`${policy.release.environments.length} env(s); ` +
|
|
2011
|
+
`review ${review.strictness} ×${review.maxRounds}`
|
|
1865
2012
|
);
|
|
1866
2013
|
},
|
|
1867
2014
|
},
|
package/src/status-render.ts
CHANGED
|
@@ -22,7 +22,7 @@ import type { DaemonStop } from "./types.ts";
|
|
|
22
22
|
import { settlementFlagSummary } from "./diff-flags.ts";
|
|
23
23
|
import type { CodeGraphHealth } from "./graph-health.ts";
|
|
24
24
|
import { formatDigestBacklog, formatOpenReports } from "./reports.ts";
|
|
25
|
-
import { formatOrchestratorDown } from "./orchestrator-down.ts";
|
|
25
|
+
import { formatDownDuration, formatOrchestratorDown } from "./orchestrator-down.ts";
|
|
26
26
|
import { planUsageLine } from "./usage.ts";
|
|
27
27
|
import { SYSTEMD_UNIT, type UnitOwnership } from "./lifecycle.ts";
|
|
28
28
|
import type { WorkerPausePhase } from "./worker.ts";
|
|
@@ -32,12 +32,12 @@ import {
|
|
|
32
32
|
formatDispatchSummary,
|
|
33
33
|
formatFreezes,
|
|
34
34
|
formatReleaseGrants,
|
|
35
|
-
formatSalvagedRuns,
|
|
36
35
|
isPaused,
|
|
37
36
|
pausedAt,
|
|
38
37
|
pauseProvenance,
|
|
39
38
|
type StatusSnapshot,
|
|
40
39
|
} from "./daemon.ts";
|
|
40
|
+
import { formatQuarantinedRuns, formatSalvagedRuns } from "./settlement.ts";
|
|
41
41
|
|
|
42
42
|
// layered status
|
|
43
43
|
// ---------------------------------------------------------------------------
|
|
@@ -405,6 +405,13 @@ function formatProjectBody(
|
|
|
405
405
|
` continuations ${s.caps.maxContinuationsPerIssue}`,
|
|
406
406
|
"",
|
|
407
407
|
...formatReleaseGrants(s.releaseGrants),
|
|
408
|
+
// The effective review policy (#678), read from the same snapshot surface
|
|
409
|
+
// as the grants: the level is what Duty 1 judges findings against, and the
|
|
410
|
+
// ceiling is its hard bound — an orchestrator reading `review-revision N`
|
|
411
|
+
// below has to see both without opening the config.
|
|
412
|
+
"review",
|
|
413
|
+
` strictness ${s.review.strictness}`,
|
|
414
|
+
` max rounds/PR ${s.review.maxRounds}`,
|
|
408
415
|
"",
|
|
409
416
|
formatDispatchSummary(s.dispatch),
|
|
410
417
|
"",
|
|
@@ -415,10 +422,34 @@ function formatProjectBody(
|
|
|
415
422
|
lines.push("active runs");
|
|
416
423
|
for (const r of s.activeRuns) {
|
|
417
424
|
const phase = workerPhases.get(r.issue);
|
|
418
|
-
|
|
425
|
+
// A run in a live review round reads `review-revision N`, distinct from
|
|
426
|
+
// a failure and from an ordinary continuation, with the round number
|
|
427
|
+
// from the durable revision row (#692). The live pause overlay still
|
|
428
|
+
// wins: an operator pause is the newer fact about the same session.
|
|
429
|
+
const paused = phase === "pausing" || phase === "paused";
|
|
430
|
+
const round = s.reviewRounds?.[r.id];
|
|
431
|
+
const state = paused
|
|
432
|
+
? phase
|
|
433
|
+
: round !== undefined
|
|
434
|
+
? `review-revision ${round}`
|
|
435
|
+
: r.state;
|
|
436
|
+
// Turn rate (#730): a stalled run and a fast one used to render
|
|
437
|
+
// identically as a bare turn count. The rate is the lifetime average
|
|
438
|
+
// over the elapsed shown — the snapshot carries no checkpoint from
|
|
439
|
+
// which a recent-interval rate could be derived — so it is labelled
|
|
440
|
+
// `avg`, and `elapsed` is elapsed since claim, never "remaining wall
|
|
441
|
+
// clock": a paused worker banks its budget, which the snapshot cannot
|
|
442
|
+
// know. A paused run gets neither number, because its elapsed includes
|
|
443
|
+
// banked pause time and its state already says `paused`.
|
|
444
|
+
const elapsedMs = Math.max(0, now - r.startedAt);
|
|
445
|
+
const progress = paused
|
|
446
|
+
? ""
|
|
447
|
+
: ` ${formatDownDuration(elapsedMs)} elapsed ${(
|
|
448
|
+
(r.turns * 60_000) / Math.max(elapsedMs, 1_000)
|
|
449
|
+
).toFixed(1)} turns/min avg`;
|
|
419
450
|
lines.push(
|
|
420
451
|
` #${r.issue} ${r.repo} ${state} attempt ${r.attempt} ` +
|
|
421
|
-
`${r.turns}/${r.maxTurns} turns ${r.spendUsd.toFixed(2)} ${r.branch}` +
|
|
452
|
+
`${r.turns}/${r.maxTurns} turns${progress} ${r.spendUsd.toFixed(2)} ${r.branch}` +
|
|
422
453
|
(r.prUrl ? ` ${r.prUrl}` : ""),
|
|
423
454
|
);
|
|
424
455
|
// The orchestrator's Duty 1 reads this command, and a flagged run's
|
|
@@ -431,6 +462,7 @@ function formatProjectBody(
|
|
|
431
462
|
lines.push(...formatBaseHealth(s.baseHealth));
|
|
432
463
|
lines.push(...formatFreezes(s.freezes));
|
|
433
464
|
lines.push(...formatSalvagedRuns(s.salvagedRuns));
|
|
465
|
+
lines.push(...formatQuarantinedRuns(s.quarantinedRuns));
|
|
434
466
|
lines.push(...formatOpenReports(s.openReports));
|
|
435
467
|
lines.push(...formatDigestBacklog(s.digestBacklog));
|
|
436
468
|
if (s.liveWorkers > 0) {
|