omp-conductor 0.19.7 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/REFERENCE.md +10 -1
- package/agents/to-spec.md +76 -9
- package/package.json +1 -1
- package/schema/config.schema.json +4 -0
- package/src/arm-challenge.ts +204 -85
- package/src/ask.ts +130 -615
- package/src/board.ts +7 -1
- package/src/brief-upgrade.ts +24 -0
- package/src/briefs/console.md +253 -0
- package/src/briefs/correction.md +203 -0
- package/src/briefs/orchestrator.md +167 -97
- package/src/briefs/policy.md +19 -16
- package/src/briefs/to-spec.md +76 -9
- package/src/briefs/worker.md +50 -16
- package/src/cli.ts +4 -0
- package/src/command-manifest.ts +54 -8
- package/src/commands/arm.ts +113 -49
- package/src/commands/console.ts +70 -0
- package/src/commands/context.ts +2 -0
- package/src/commands/epic.ts +132 -0
- package/src/commands/extend.ts +9 -1
- package/src/commands/intake.ts +44 -14
- package/src/commands/stats.ts +19 -4
- package/src/commands/worker.ts +9 -1
- package/src/config-schema.ts +13 -0
- package/src/config.ts +27 -0
- package/src/daemon/ack.ts +159 -0
- package/src/daemon/admission-pass.ts +135 -0
- package/src/daemon/brief.ts +461 -0
- package/src/daemon/deps.ts +539 -0
- package/src/daemon/dispatch.ts +1779 -0
- package/src/daemon/drain.ts +185 -0
- package/src/daemon/groom-pass.ts +412 -0
- package/src/daemon/http.ts +417 -0
- package/src/daemon/integrity.ts +108 -0
- package/src/daemon/panes.ts +180 -0
- package/src/daemon/review.ts +1888 -0
- package/src/daemon/runtime.ts +736 -0
- package/src/daemon/settle-pass.ts +589 -0
- package/src/daemon/supervision.ts +438 -0
- package/src/daemon/tick.ts +968 -0
- package/src/daemon/views.ts +751 -0
- package/src/daemon.ts +105 -7923
- package/src/dashboard/app.js +58 -0
- package/src/dashboard/controls.ts +22 -3
- package/src/dashboard/server.ts +4 -0
- package/src/diff-flags.ts +24 -3
- package/src/failure-class.ts +75 -1
- package/src/fleet.ts +290 -164
- package/src/groom.ts +461 -0
- package/src/http-token.ts +142 -0
- package/src/knowledge.ts +229 -0
- package/src/mining.ts +316 -0
- package/src/orchestrator-tick.ts +428 -1681
- package/src/ready-gate.ts +267 -0
- package/src/settlement.ts +72 -6
- package/src/setup-host.ts +32 -9
- package/src/setup-wizard.ts +55 -7
- package/src/setup.ts +229 -3
- package/src/stats.ts +257 -2
- package/src/status-render.ts +158 -7
- package/src/store.ts +604 -26
- package/src/to-spec.ts +194 -21
- package/src/tracker/github.ts +50 -0
- package/src/types.ts +416 -15
- package/src/verbs/protocol.ts +28 -0
- package/src/verbs/server.ts +330 -39
- package/src/wake.ts +19 -2
- package/src/worker.ts +456 -1
package/src/types.ts
CHANGED
|
@@ -680,6 +680,18 @@ export const REVIEW_ADJUDICATOR_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
|
680
680
|
*/
|
|
681
681
|
export const DEFAULT_REVIEW_ADJUDICATOR_ROLE = "task";
|
|
682
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Which OMP model role the daemon's own to-spec grooming scouts run under
|
|
685
|
+
* (#1041) when no project names one. Same value and same reasoning as
|
|
686
|
+
* {@link DEFAULT_REVIEW_ADJUDICATOR_ROLE} — the general session role every
|
|
687
|
+
* install can launch — but a separate constant because the two are separate
|
|
688
|
+
* operator choices: grooming judgement and review adjudication are answered
|
|
689
|
+
* independently in setup, and collapsing them would silently move one when the
|
|
690
|
+
* other is retuned. Validated with {@link REVIEW_ADJUDICATOR_RE}: one role-name
|
|
691
|
+
* grammar for every role token conductor stores.
|
|
692
|
+
*/
|
|
693
|
+
export const DEFAULT_GROOM_ROLE = "task";
|
|
694
|
+
|
|
683
695
|
/**
|
|
684
696
|
* One project's review policy (#678): the strictness its orchestrator applies
|
|
685
697
|
* when deciding whether a green PR is returned to its worker, the hard
|
|
@@ -830,15 +842,89 @@ export type ReviewAdjudicationAdmission =
|
|
|
830
842
|
| { kind: "existing"; record: ReviewAdjudicationRecord }
|
|
831
843
|
| { kind: "refused"; block: "in-flight-different-head"; record: ReviewAdjudicationRecord };
|
|
832
844
|
|
|
845
|
+
/**
|
|
846
|
+
* How one review-correction round was launched (#1045, child of #1044).
|
|
847
|
+
*
|
|
848
|
+
* `resume-original` is the pre-#1045 behaviour and stays the healthy default
|
|
849
|
+
* path: the findings are delivered into the *same* implementation transcript,
|
|
850
|
+
* which is why the round costs no attempt and needs no re-orientation.
|
|
851
|
+
* `fresh-correction` is the escape hatch #1035 proved necessary — the original
|
|
852
|
+
* session was exhausted (turn/wall cap, `model-empty-stop`) or the fleet's
|
|
853
|
+
* worker model changed after the rejected run, so replaying that transcript
|
|
854
|
+
* spends the round on a session that cannot take another turn or answers from
|
|
855
|
+
* a model nobody would dispatch today.
|
|
856
|
+
*
|
|
857
|
+
* A closed vocabulary rather than a boolean: the two modes read differently in
|
|
858
|
+
* a digest, and a third mode must be a compile-time change everywhere at once.
|
|
859
|
+
*/
|
|
860
|
+
export type ReviewLaunchMode = "resume-original" | "fresh-correction";
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* The durable provenance of one review-correction round's launch (#1045): what
|
|
864
|
+
* the daemon decided, which model it asked for, which model the harness
|
|
865
|
+
* actually resolved, and the two session lineages involved.
|
|
866
|
+
*
|
|
867
|
+
* One definition, shared by the stored row ({@link ReviewRevisionRecord}) and
|
|
868
|
+
* the status projection ({@link ReviewCorrectionRound}, #1048), so a renderer
|
|
869
|
+
* and the row it renders cannot drift apart.
|
|
870
|
+
*
|
|
871
|
+
* *Every field is optional on purpose.* Rows written before #1045 recorded no
|
|
872
|
+
* decision at all, and the honest projection of one is "unknown" — never a
|
|
873
|
+
* fabricated `resume-original`, which would put an invented explanation in the
|
|
874
|
+
* very audit these fields exist to make truthful. Absent means nobody wrote it
|
|
875
|
+
* down, and a reader must say so.
|
|
876
|
+
*/
|
|
877
|
+
export interface ReviewCorrectionProvenance {
|
|
878
|
+
/** The decision, written once per round at dispatch time. */
|
|
879
|
+
launchMode?: ReviewLaunchMode;
|
|
880
|
+
/** What the daemon asked the harness for (a role alias, or a concrete
|
|
881
|
+
* provider/model), as resolved from escalation/config at dispatch. */
|
|
882
|
+
requestedModel?: string;
|
|
883
|
+
/** What the harness actually resolved and ran — the answer to "which model
|
|
884
|
+
* wrote this correction", which a role alias alone cannot give. */
|
|
885
|
+
resolvedModel?: string;
|
|
886
|
+
/** The implementation session lineage this round was reviewed *from*. Kept
|
|
887
|
+
* even for a fresh correction: it is the transcript the findings came out
|
|
888
|
+
* of, and losing it is losing the thread of the whole PR. */
|
|
889
|
+
originSessionRef?: string;
|
|
890
|
+
/** The fresh correction's own lineage, once the launch reports it. Only a
|
|
891
|
+
* `fresh-correction` round has one — a resumed round *is* its origin. */
|
|
892
|
+
correctionSessionRef?: string;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* One review-correction round exactly as the status snapshot carries it
|
|
897
|
+
* (#1048): the round's position, its lifecycle state, and the #1045 launch
|
|
898
|
+
* provenance the renderer prints beside it.
|
|
899
|
+
*
|
|
900
|
+
* Derived from the same `review_revisions` rows the dispatcher decides on, so
|
|
901
|
+
* status can never describe a round differently from the way it was launched —
|
|
902
|
+
* which is precisely the #1035 failure: the fleet showed `review-revision N`
|
|
903
|
+
* and nothing else while every one of those rounds replayed an exhausted
|
|
904
|
+
* transcript under a model the fleet had already stopped using.
|
|
905
|
+
*/
|
|
906
|
+
export interface ReviewCorrectionRound extends ReviewCorrectionProvenance {
|
|
907
|
+
round: number;
|
|
908
|
+
/** `pending` = enqueued, never dispatched; `dispatched` = live or
|
|
909
|
+
* restart-recovered; `settled` = an outcome is recorded. */
|
|
910
|
+
state: "pending" | "dispatched" | "settled";
|
|
911
|
+
outcome?: ReviewRevisionOutcome;
|
|
912
|
+
dispatchedAt?: number;
|
|
913
|
+
}
|
|
914
|
+
|
|
833
915
|
/**
|
|
834
916
|
* One durable review-revision request (#677): the orchestrator returned a
|
|
835
917
|
* green, run-owned pull request to its worker with blocking findings, and
|
|
836
|
-
* everything the daemon needs to
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
918
|
+
* everything the daemon needs to reach the *same work* is recorded here before
|
|
919
|
+
* any worker is woken. The round numbers successive revisions of one run
|
|
920
|
+
* (1, 2, …); the findings text is delivered to the launched session verbatim.
|
|
921
|
+
*
|
|
922
|
+
* Since #1045 the row also carries how the round was launched — resumed into
|
|
923
|
+
* the recorded transcript, or opened as a fresh correction on the same
|
|
924
|
+
* branch/PR — together with the models and session lineages involved, so a
|
|
925
|
+
* restart re-reads that decision instead of making it a second time.
|
|
840
926
|
*/
|
|
841
|
-
export interface ReviewRevisionRecord {
|
|
927
|
+
export interface ReviewRevisionRecord extends ReviewCorrectionProvenance {
|
|
842
928
|
id: string;
|
|
843
929
|
project: string;
|
|
844
930
|
/** The run whose pushed-green row this revises. The row is reused, never
|
|
@@ -866,8 +952,29 @@ export interface ReviewRevisionRecord {
|
|
|
866
952
|
* `failed` — and this counter is what bounds that, durably, so an outage
|
|
867
953
|
* loop cannot retry one round forever. Absent means zero. */
|
|
868
954
|
infraRetries?: number;
|
|
955
|
+
/** When the launch decision was made (#1045). Stamped by the deciding
|
|
956
|
+
* dispatch and never moved afterwards, so a re-affirming retry cannot make
|
|
957
|
+
* an old decision look new. Absent on a round nobody has decided yet, and
|
|
958
|
+
* on every pre-#1045 row. */
|
|
959
|
+
launchDecidedAt?: number;
|
|
869
960
|
}
|
|
870
961
|
|
|
962
|
+
/**
|
|
963
|
+
* What a caller may hand {@link Store.enqueueReviewRevision} (#1045).
|
|
964
|
+
*
|
|
965
|
+
* Deliberately narrower than the record: the launch decision and its
|
|
966
|
+
* provenance belong to *dispatch*, not to the review that requested the round,
|
|
967
|
+
* and the enqueue INSERT does not persist them. Accepting them here would let
|
|
968
|
+
* a caller pass a `launchMode`, read it straight back off the returned record,
|
|
969
|
+
* and never notice the row does not carry it — the exact silent fake #1045
|
|
970
|
+
* names. They are written afterwards, once, by
|
|
971
|
+
* {@link Store.recordReviewRevisionLaunch}.
|
|
972
|
+
*/
|
|
973
|
+
export type ReviewRevisionDraft = Omit<
|
|
974
|
+
ReviewRevisionRecord,
|
|
975
|
+
"id" | keyof ReviewCorrectionProvenance | "launchDecidedAt"
|
|
976
|
+
>;
|
|
977
|
+
|
|
871
978
|
/**
|
|
872
979
|
* The outcome of recording one `conductor_pr_review` finding against a run's
|
|
873
980
|
* review-revision outbox (#786). `created` opened a new revision round for the
|
|
@@ -1034,6 +1141,15 @@ export interface ProjectConfig {
|
|
|
1034
1141
|
* every tick (#988). Optional; defaults to {@link DEFAULT_GROOM_BELOW} in
|
|
1035
1142
|
* orchestrator-tick.ts. */
|
|
1036
1143
|
groomBelow?: GroomTrigger;
|
|
1144
|
+
/**
|
|
1145
|
+
* The OMP model role the daemon's own to-spec grooming scouts run under
|
|
1146
|
+
* (#1041): a role token in the same grammar the review adjudicator uses
|
|
1147
|
+
* ({@link REVIEW_ADJUDICATOR_RE}), never a provider/model — OMP owns model
|
|
1148
|
+
* selection. Absent loads as {@link DEFAULT_GROOM_ROLE} (`"task"`), the
|
|
1149
|
+
* general session role every install can launch without pinning a provider,
|
|
1150
|
+
* which is what every project that never answered the question wants.
|
|
1151
|
+
*/
|
|
1152
|
+
groomRole?: string;
|
|
1037
1153
|
/** Labels the dispatcher writes back so the tracker alone shows live state
|
|
1038
1154
|
* to a human who never opens the daemon's logs. `backlog` is the
|
|
1039
1155
|
* operator's own park gesture (#507) — not dispatcher-written, but read by
|
|
@@ -2036,11 +2152,13 @@ export interface RunRecord {
|
|
|
2036
2152
|
*/
|
|
2037
2153
|
outputTokens?: number;
|
|
2038
2154
|
reasoningTokens?: number;
|
|
2039
|
-
/** The run id
|
|
2040
|
-
* (#536, #567)
|
|
2041
|
-
*
|
|
2042
|
-
*
|
|
2043
|
-
*
|
|
2155
|
+
/** The run id whose session this row resumes: the orphan-clean attempt it
|
|
2156
|
+
* inherited (#536, #567), or — since the blocked-continuation resume — the
|
|
2157
|
+
* answered-block run whose transcript this continuation carries on. Set only
|
|
2158
|
+
* when a dispatch-time resume verdict fired; absent means a fresh dispatch,
|
|
2159
|
+
* never "resumed from unknown". Readable without opening the transcript, so
|
|
2160
|
+
* "how often did resume fire and did it save turns" is a query, not a file
|
|
2161
|
+
* walk. */
|
|
2044
2162
|
resumedFromRunId?: string;
|
|
2045
2163
|
prUrl?: string;
|
|
2046
2164
|
/** Pull request head the worker observed after its deterministic CI watcher exited. */
|
|
@@ -2548,6 +2666,14 @@ export interface IntakeItem {
|
|
|
2548
2666
|
issueUrl?: string;
|
|
2549
2667
|
/** When the item was resolved into an issue (#300) or dismissed. */
|
|
2550
2668
|
groomedAt?: number;
|
|
2669
|
+
/**
|
|
2670
|
+
* Provenance of a machine-filed item (Phase 4 signal mining) — the stable
|
|
2671
|
+
* key of the signal that produced it, e.g.
|
|
2672
|
+
* `mined:settlement-weakening:omp/src/foo.ts`. Absent means an operator
|
|
2673
|
+
* typed this idea themselves, which surfaces matching on it must render as
|
|
2674
|
+
* their own idea rather than as an unattributed machine signal.
|
|
2675
|
+
*/
|
|
2676
|
+
source?: string;
|
|
2551
2677
|
}
|
|
2552
2678
|
|
|
2553
2679
|
/** What a caller hands over. The store owns the id, the state and the timestamps. */
|
|
@@ -2555,6 +2681,72 @@ export interface IntakeDraft {
|
|
|
2555
2681
|
project: string;
|
|
2556
2682
|
text: string;
|
|
2557
2683
|
at: number;
|
|
2684
|
+
/**
|
|
2685
|
+
* Provenance and dedupe key in one (Phase 4). When present, the store
|
|
2686
|
+
* derives the item's id DETERMINISTICALLY from `project` + `source` and
|
|
2687
|
+
* inserts only if that id is new, so an hourly mining pass re-filing a
|
|
2688
|
+
* signal it already filed is a no-op rather than an hourly duplicate.
|
|
2689
|
+
*
|
|
2690
|
+
* The consequence is intended: an item the operator dismissed stays
|
|
2691
|
+
* dismissed instead of reappearing every hour, because the second filing
|
|
2692
|
+
* cannot resurrect the row it collides with. The trap is the same fact from
|
|
2693
|
+
* the other side — a signal genuinely worth re-raising later needs a
|
|
2694
|
+
* DIFFERENT key, which is what a source carrying a bucket (a week, a run
|
|
2695
|
+
* count) is for. A bare `mined:<kind>:<path>` is a once-ever claim.
|
|
2696
|
+
*
|
|
2697
|
+
* Absent for an operator's own idea, which keeps a random id and is
|
|
2698
|
+
* therefore always a new row: two identical thoughts on two days are two
|
|
2699
|
+
* thoughts.
|
|
2700
|
+
*/
|
|
2701
|
+
source?: string;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
/**
|
|
2705
|
+
* Which daemon-owned session spent this (Phase 4 attribution). Exactly the two
|
|
2706
|
+
* the daemon runs itself: to-spec grooming scouts and review adjudication.
|
|
2707
|
+
*
|
|
2708
|
+
* The orchestrator's own tick turns are deliberately NOT a member. The tick
|
|
2709
|
+
* extension observes no spend or usage events at all — its `message_start`
|
|
2710
|
+
* adapter reads content text — so there is no number to record, and a `0` row
|
|
2711
|
+
* per tick would report the orchestrator as free. Orchestrator spend is
|
|
2712
|
+
* reported as unmetered instead of invented.
|
|
2713
|
+
*/
|
|
2714
|
+
export const SESSION_SPEND_ROLES = ["groom", "adjudicator"] as const;
|
|
2715
|
+
|
|
2716
|
+
export type SessionSpendRole = (typeof SESSION_SPEND_ROLES)[number];
|
|
2717
|
+
|
|
2718
|
+
/**
|
|
2719
|
+
* One daemon-owned session's turns and cost (Phase 4 attribution).
|
|
2720
|
+
*
|
|
2721
|
+
* The same shape going in and coming out: the store adds nothing but a rowid,
|
|
2722
|
+
* because every field here is something the caller actually observed.
|
|
2723
|
+
*
|
|
2724
|
+
* `spendUsd` is OPTIONAL, and that is the contract rather than convenience.
|
|
2725
|
+
* In the `runs` table an unmetered request and a genuinely free one are both
|
|
2726
|
+
* `0`, which is why `spend-telemetry.ts` has to infer a subscription verdict
|
|
2727
|
+
* from the share of zeros instead of reading a fact. Here the caller
|
|
2728
|
+
* distinguishes them: omit `spendUsd` when the provider reported no cost for
|
|
2729
|
+
* the session (subscription billing, or a harness that emitted no usage
|
|
2730
|
+
* event), pass it when it reported a number — including a real `0`. A reader
|
|
2731
|
+
* MUST render an absent `spendUsd` as unmetered and never as `$0.00`.
|
|
2732
|
+
*/
|
|
2733
|
+
export interface SessionSpendRow {
|
|
2734
|
+
project: string;
|
|
2735
|
+
role: SessionSpendRole;
|
|
2736
|
+
/** The issue the session was about, when it was about one. Absent for a
|
|
2737
|
+
* grooming batch that spanned several. */
|
|
2738
|
+
issue?: number;
|
|
2739
|
+
/** The model the caller asked for. */
|
|
2740
|
+
model?: string;
|
|
2741
|
+
/** The model the harness actually ran, which is the one that was billed and
|
|
2742
|
+
* the one a per-model breakdown groups on. Absent when unobserved — never
|
|
2743
|
+
* filled in from `model`, because a requested model is not evidence of a
|
|
2744
|
+
* resolved one. */
|
|
2745
|
+
resolvedModel?: string;
|
|
2746
|
+
turns: number;
|
|
2747
|
+
/** Absent means the provider reported no cost. See the interface note. */
|
|
2748
|
+
spendUsd?: number;
|
|
2749
|
+
at: number;
|
|
2558
2750
|
}
|
|
2559
2751
|
|
|
2560
2752
|
/**
|
|
@@ -2569,6 +2761,16 @@ export const GROOMING_VERDICTS = ["promotable", "blocked", "considered"] as cons
|
|
|
2569
2761
|
|
|
2570
2762
|
export type GroomingVerdict = (typeof GROOMING_VERDICTS)[number];
|
|
2571
2763
|
|
|
2764
|
+
/**
|
|
2765
|
+
* Who acted on a grooming verdict by queueing the issue (#1041). Three actors,
|
|
2766
|
+
* because the promotion audit's whole question is which of them did it: the
|
|
2767
|
+
* daemon promotes unattended once the ready gate passes, an orchestrator tick
|
|
2768
|
+
* promotes as part of its own reasoning, and an operator promotes by hand. A
|
|
2769
|
+
* fourth spelling would mean a fourth promotion path exists, which is exactly
|
|
2770
|
+
* what this closed union is here to prevent.
|
|
2771
|
+
*/
|
|
2772
|
+
export type PromotedBy = "daemon" | "orchestrator" | "operator";
|
|
2773
|
+
|
|
2572
2774
|
/**
|
|
2573
2775
|
* One issue's current grooming verdict, durable across restarts and keyed by
|
|
2574
2776
|
* project + issue — one row per issue, replaced in place by upsert, never a
|
|
@@ -2587,6 +2789,24 @@ export interface GroomingRecord {
|
|
|
2587
2789
|
* or a scout summary. Free text, bounded at the write site. */
|
|
2588
2790
|
evidence: string;
|
|
2589
2791
|
recordedAt: number;
|
|
2792
|
+
/**
|
|
2793
|
+
* When this verdict was acted on by queueing the issue (#1041). Absent on
|
|
2794
|
+
* every verdict nobody promoted, which is the honest reading of a row from
|
|
2795
|
+
* before mechanical promotion existed: nothing recorded who queued it, so
|
|
2796
|
+
* nothing may claim to know.
|
|
2797
|
+
*
|
|
2798
|
+
* Set exactly once per verdict, by {@link Store.markGroomingPromoted}, and
|
|
2799
|
+
* cleared by the next `upsertGrooming` — a re-recorded verdict is a new
|
|
2800
|
+
* judgement about the issue, and provenance that outlived the judgement it
|
|
2801
|
+
* describes would audit the wrong decision.
|
|
2802
|
+
*/
|
|
2803
|
+
promotedAt?: number;
|
|
2804
|
+
/** Who queued it: the daemon's own ready gate, an orchestrator tick, or an
|
|
2805
|
+
* operator at the CLI. Stored rather than inferred — the tick's promotion
|
|
2806
|
+
* audit exists precisely to review what the daemon did unattended, and a
|
|
2807
|
+
* guess would make that review circular. Always present when
|
|
2808
|
+
* {@link promotedAt} is, and absent when it is not. */
|
|
2809
|
+
promotedBy?: PromotedBy;
|
|
2590
2810
|
}
|
|
2591
2811
|
|
|
2592
2812
|
/** What a caller hands over. The store owns the timestamp and the replace-in-
|
|
@@ -2600,6 +2820,32 @@ export interface GroomingDraft {
|
|
|
2600
2820
|
at: number;
|
|
2601
2821
|
}
|
|
2602
2822
|
|
|
2823
|
+
/**
|
|
2824
|
+
* One operator's standing approval of an epic's scope (#1041).
|
|
2825
|
+
*
|
|
2826
|
+
* Deliberately its own durable fact rather than a resolved decision row, for
|
|
2827
|
+
* two reasons that both bite in production. First, approving an epic is an
|
|
2828
|
+
* explicit, revocable act with exactly one meaning — "the children of this
|
|
2829
|
+
* epic may be queued without asking me again" — while a decision row's
|
|
2830
|
+
* `resolution` is free text a human wrote for a human, and no amount of
|
|
2831
|
+
* parsing turns "yes, but do the migration first" into a machine gate. Second,
|
|
2832
|
+
* decision rows expire ({@link DECISION_TTL_MS}) and an approval must not:
|
|
2833
|
+
* scope consent does not lapse after a week just because nobody looked at it.
|
|
2834
|
+
*
|
|
2835
|
+
* Carries no `project` field because it is always read through a
|
|
2836
|
+
* project-scoped accessor — the scoping is the key, not a payload column an
|
|
2837
|
+
* unwary caller could compare against the wrong project.
|
|
2838
|
+
*/
|
|
2839
|
+
export interface EpicApproval {
|
|
2840
|
+
issue: number;
|
|
2841
|
+
approvedAt: number;
|
|
2842
|
+
/** Who approved it, in the same spelling the report-withdrawal path already
|
|
2843
|
+
* uses: the session role when one is set, else `orchestrator` inside a
|
|
2844
|
+
* Herdr pane, else `operator`. Free text on purpose — it is provenance for
|
|
2845
|
+
* a human reading an audit line, never a gate anything branches on. */
|
|
2846
|
+
approvedBy: string;
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2603
2849
|
/**
|
|
2604
2850
|
* Where one operator decision stands (#136).
|
|
2605
2851
|
*
|
|
@@ -2781,7 +3027,7 @@ export interface Store {
|
|
|
2781
3027
|
* two concurrent same-head findings cannot both create a row or overwrite
|
|
2782
3028
|
* each other (the run row's state is the other half once the revision is
|
|
2783
3029
|
* dispatched). */
|
|
2784
|
-
enqueueReviewRevision(draft:
|
|
3030
|
+
enqueueReviewRevision(draft: ReviewRevisionDraft): ReviewRevisionEnqueue;
|
|
2785
3031
|
/** The pending revision for one run, if any — the header the verb reads to
|
|
2786
3032
|
* tell "this call folds into the in-flight round" from "this call opens a
|
|
2787
3033
|
* new round" before the round-ceiling gate applies. */
|
|
@@ -2793,6 +3039,51 @@ export interface Store {
|
|
|
2793
3039
|
latestReviewRound(project: string, runId: string): number;
|
|
2794
3040
|
/** Record that a revision was handed to a worker. */
|
|
2795
3041
|
markReviewRevisionDispatched(id: string, at: number): void;
|
|
3042
|
+
/** Record, once, how this review round is being launched (#1045).
|
|
3043
|
+
*
|
|
3044
|
+
* Write-once and atomic — one guarded UPDATE, never read-then-write, so two
|
|
3045
|
+
* dispatch passes cannot each decide. The FIRST decision for a round wins:
|
|
3046
|
+
*
|
|
3047
|
+
* - no decision yet ⇒ written, `true`;
|
|
3048
|
+
* - the identical decision again (a dispatch retry, a restart re-deciding
|
|
3049
|
+
* the same way) ⇒ no-op, `true`, and the original `launchDecidedAt`
|
|
3050
|
+
* stands;
|
|
3051
|
+
* - a *contradictory* decision (different mode, requested model or origin
|
|
3052
|
+
* lineage) ⇒ nothing written, `false`. The caller must re-read the
|
|
3053
|
+
* recorded decision and honour it rather than launching its own;
|
|
3054
|
+
* - no such round in this project ⇒ `false`.
|
|
3055
|
+
*
|
|
3056
|
+
* Touches the decision columns only. The issue, run, attempt/continuation
|
|
3057
|
+
* charges, round number, branch, PR URL, reviewed head, findings and
|
|
3058
|
+
* settlement history are the round's identity and are never written here. */
|
|
3059
|
+
recordReviewRevisionLaunch(
|
|
3060
|
+
project: string,
|
|
3061
|
+
id: string,
|
|
3062
|
+
decision: {
|
|
3063
|
+
launchMode: ReviewLaunchMode;
|
|
3064
|
+
requestedModel?: string;
|
|
3065
|
+
originSessionRef?: string;
|
|
3066
|
+
at: number;
|
|
3067
|
+
},
|
|
3068
|
+
): boolean;
|
|
3069
|
+
/** Attach the fresh correction's own session lineage — and the model the
|
|
3070
|
+
* harness actually resolved — to a round already decided
|
|
3071
|
+
* `fresh-correction` (#1045). `false`, writing nothing, on a round decided
|
|
3072
|
+
* `resume-original`, on an undecided round, or on no such round: a resumed
|
|
3073
|
+
* round *is* its origin session, so recording a separate correction
|
|
3074
|
+
* lineage for one would invent a session that never existed.
|
|
3075
|
+
*
|
|
3076
|
+
* Not write-once, and deliberately so: an infra-killed round relaunches on
|
|
3077
|
+
* the same decision, and the newest fresh session is then the truth about
|
|
3078
|
+
* what is running. `resolvedModel` is only ever added, never cleared — a
|
|
3079
|
+
* later call without one leaves a known resolution in place. */
|
|
3080
|
+
recordReviewCorrectionSession(project: string, id: string, ref: string, resolvedModel?: string): boolean;
|
|
3081
|
+
/** The recorded launch decision and provenance for one round, or `undefined`
|
|
3082
|
+
* when no such round exists (#1045). Every field is absent on a round
|
|
3083
|
+
* nobody has decided and on every pre-#1045 row — the explicit unknown a
|
|
3084
|
+
* claim/retry/restart path must read *before* deciding, and the honest
|
|
3085
|
+
* projection a renderer must print instead of guessing. */
|
|
3086
|
+
reviewRevisionLaunch(project: string, id: string): ReviewCorrectionProvenance | undefined;
|
|
2796
3087
|
/** Record how a revision ended. */
|
|
2797
3088
|
settleReviewRevision(id: string, outcome: ReviewRevisionOutcome, at: number): void;
|
|
2798
3089
|
/** Every revision row not yet settled — queued (`dispatchedAt` unset) and
|
|
@@ -3118,15 +3409,37 @@ export interface Store {
|
|
|
3118
3409
|
getMaterialEvent(id: string): MaterialEvent | undefined;
|
|
3119
3410
|
/** Ordinary material outcomes still owed, oldest first and bounded. */
|
|
3120
3411
|
undigestedMaterialEvents(project: string, limit?: number): MaterialEvent[];
|
|
3121
|
-
/**
|
|
3412
|
+
/**
|
|
3413
|
+
* Persist one raw idea, `pending`, for later grooming (#299).
|
|
3414
|
+
*
|
|
3415
|
+
* Idempotent when {@link IntakeDraft.source} is present (Phase 4): the id is
|
|
3416
|
+
* derived from `project` + `source` and the insert is ignored if that id
|
|
3417
|
+
* already exists, so the hourly mining pass is safe to re-run. A collision
|
|
3418
|
+
* returns the EXISTING row untouched — which is how a caller tells a new
|
|
3419
|
+
* filing from a no-op (`item.createdAt !== draft.at`, or a `state` that is
|
|
3420
|
+
* no longer `pending`) — and in particular does NOT reopen an item the
|
|
3421
|
+
* operator dismissed.
|
|
3422
|
+
*
|
|
3423
|
+
* Without a `source` the id is random, so every call is a new row.
|
|
3424
|
+
*/
|
|
3122
3425
|
recordIntake(draft: IntakeDraft): IntakeItem;
|
|
3123
3426
|
/** Ideas still `pending`, oldest first — what `intake list` and `status` read. */
|
|
3124
3427
|
pendingIntake(project: string): IntakeItem[];
|
|
3125
3428
|
/** Resolve one idea to `groomed` (recording the issue URL #300 chose) or
|
|
3126
3429
|
* `dismissed`. `false` when the id is unknown. */
|
|
3127
3430
|
resolveIntake(id: string, state: "groomed" | "dismissed", issueUrl?: string): boolean;
|
|
3128
|
-
/**
|
|
3129
|
-
*
|
|
3431
|
+
/**
|
|
3432
|
+
* Record the current grooming verdict for one issue, replacing any prior
|
|
3433
|
+
* row — one row per project + issue, never a history (#735).
|
|
3434
|
+
*
|
|
3435
|
+
* Clears {@link GroomingRecord.promotedAt}/{@link GroomingRecord.promotedBy}
|
|
3436
|
+
* (#1041). A re-recorded verdict is a fresh judgement about the issue, so
|
|
3437
|
+
* carrying the old promotion forward would let the audit attribute a queueing
|
|
3438
|
+
* to a verdict that no longer exists — and, worse, would make a demote-then-
|
|
3439
|
+
* regroom cycle read as though the demoted promotion still stood. A caller
|
|
3440
|
+
* that re-promotes stamps provenance again through
|
|
3441
|
+
* {@link markGroomingPromoted}, which is the only writer of these columns.
|
|
3442
|
+
*/
|
|
3130
3443
|
upsertGrooming(draft: GroomingDraft): void;
|
|
3131
3444
|
/** The current grooming verdict for one issue, or undefined when none. */
|
|
3132
3445
|
grooming(project: string, issue: number): GroomingRecord | undefined;
|
|
@@ -3142,6 +3455,15 @@ export interface Store {
|
|
|
3142
3455
|
* self-heals instead of lingering as a cache someone must invalidate. Runs
|
|
3143
3456
|
* at the end of every admission pass. Never touches `promotable` or
|
|
3144
3457
|
* `considered` rows, which belong to #679's scout loop.
|
|
3458
|
+
*
|
|
3459
|
+
* A promoted row (#1041) is likewise inert here, in both directions: it is
|
|
3460
|
+
* neither overwritten with `blocked` nor swept by the self-clearing delete.
|
|
3461
|
+
* Promotion hands the issue to dispatch, and admission's lane/dependency
|
|
3462
|
+
* holds on a queued issue are flow control of the moment, not a re-grooming.
|
|
3463
|
+
* Without this the fleet would lose the audit line within one 5-minute
|
|
3464
|
+
* admission pass — a promoted issue whose lane overlaps an active run would
|
|
3465
|
+
* be rewritten as `blocked` and then deleted — long before the ~30-minute
|
|
3466
|
+
* tick that is supposed to review the promotion ever read it.
|
|
3145
3467
|
*/
|
|
3146
3468
|
reconcileGrooming(
|
|
3147
3469
|
project: string,
|
|
@@ -3162,6 +3484,48 @@ export interface Store {
|
|
|
3162
3484
|
* snapshot here deletes verdicts nobody re-derived.
|
|
3163
3485
|
*/
|
|
3164
3486
|
retireGroomingNotOpen(project: string, openIssues: readonly number[]): number[];
|
|
3487
|
+
/**
|
|
3488
|
+
* Stamp promotion provenance on one existing verdict (#1041), returning
|
|
3489
|
+
* whether this call is the one that stamped it.
|
|
3490
|
+
*
|
|
3491
|
+
* `false` on both no-op shapes, and the caller must treat them the same way:
|
|
3492
|
+
* there is no such verdict, or it already carries a promotion. That makes the
|
|
3493
|
+
* write the single-flight latch for promotion itself — the daemon enqueues the
|
|
3494
|
+
* queue label and fires the wake only when it wins this call, so a restart
|
|
3495
|
+
* mid-promotion re-reads a stamped row and does not queue the issue twice.
|
|
3496
|
+
* Provenance is therefore stamped BEFORE the label op, never after.
|
|
3497
|
+
*/
|
|
3498
|
+
markGroomingPromoted(project: string, issue: number, at: number, by: PromotedBy): boolean;
|
|
3499
|
+
/**
|
|
3500
|
+
* Verdicts promoted at or after `since`, newest promotion first — the tick's
|
|
3501
|
+
* promotion audit (#1041): what was queued unattended while nobody was
|
|
3502
|
+
* looking, with the verdict and evidence that justified it still attached.
|
|
3503
|
+
*
|
|
3504
|
+
* Bounded by the window rather than by a count, because the audit's contract
|
|
3505
|
+
* is "everything since the last tick" — a truncated list would silently
|
|
3506
|
+
* un-audit a promotion.
|
|
3507
|
+
*/
|
|
3508
|
+
promotionsSince(project: string, since: number): GroomingRecord[];
|
|
3509
|
+
/**
|
|
3510
|
+
* Record the operator's standing approval of one epic's scope (#1041).
|
|
3511
|
+
*
|
|
3512
|
+
* Idempotent and first-write-wins: re-approving keeps the original
|
|
3513
|
+
* `approvedAt` and `approvedBy`, because the durable fact is when consent was
|
|
3514
|
+
* given, not when it was last restated. A caller that must tell "approved
|
|
3515
|
+
* now" from "already approved" reads {@link epicApproval} first. Refuses a
|
|
3516
|
+
* non-integer or non-positive issue number rather than storing a fact about
|
|
3517
|
+
* an issue that cannot exist.
|
|
3518
|
+
*/
|
|
3519
|
+
approveEpic(project: string, issue: number, at: number, by: string): void;
|
|
3520
|
+
/** Withdraw an epic's approval, `false` when there was none to withdraw.
|
|
3521
|
+
* Approval is revocable by design: scope consent an operator regrets must be
|
|
3522
|
+
* removable without deleting the epic or its children (#1041). */
|
|
3523
|
+
revokeEpicApproval(project: string, issue: number): boolean;
|
|
3524
|
+
/** One epic's approval, or `undefined` when it has none — which every gate
|
|
3525
|
+
* must read as "not approved", never as approval whose row is missing. */
|
|
3526
|
+
epicApproval(project: string, issue: number): EpicApproval | undefined;
|
|
3527
|
+
/** Every approved epic in this project, issue-ascending. */
|
|
3528
|
+
epicApprovals(project: string): EpicApproval[];
|
|
3165
3529
|
/** Count and age source for status and digest prompt bounds. */
|
|
3166
3530
|
digestBacklog(project: string): DigestBacklog;
|
|
3167
3531
|
/** Add one bounded observation to the per-day friction rollup. */
|
|
@@ -3211,6 +3575,38 @@ export interface Store {
|
|
|
3211
3575
|
bumpGhCalls?(day: string, source: string): void;
|
|
3212
3576
|
/** The tracked call counts for a UTC day, per source. */
|
|
3213
3577
|
ghCallsToday?(day: string): { source: string; calls: number }[];
|
|
3578
|
+
/**
|
|
3579
|
+
* Record one auto-restart the daemon fired at a wedged orchestrator
|
|
3580
|
+
* (Phase 4). A bare append, like {@link Store.recordGhRefusal}: the cap is
|
|
3581
|
+
* enforced by counting rows in a window, so there is no counter to reset and
|
|
3582
|
+
* a crash cannot leave a high-water mark that either disables the cap or
|
|
3583
|
+
* spends it twice. Rows older than a week are pruned in the same write.
|
|
3584
|
+
*/
|
|
3585
|
+
recordOrchestratorRestart(project: string, at: number, reason: string): void;
|
|
3586
|
+
/**
|
|
3587
|
+
* This project's auto-restarts at or after `since`, NEWEST FIRST — the
|
|
3588
|
+
* bounded-restart gate counts them against its per-window cap, and the page
|
|
3589
|
+
* it sends when the cap is reached names the most recent reasons.
|
|
3590
|
+
*/
|
|
3591
|
+
orchestratorRestartsSince(project: string, since: number): { at: number; reason: string }[];
|
|
3592
|
+
/**
|
|
3593
|
+
* Record what one daemon-owned session (grooming, adjudication) cost
|
|
3594
|
+
* (Phase 4 attribution). Append-only; these sessions have no lifecycle to
|
|
3595
|
+
* update, they either ran or they did not.
|
|
3596
|
+
*
|
|
3597
|
+
* Deliberately not a `runs` row: `computeStats` walks runs as issue journeys
|
|
3598
|
+
* and a role row in there would invent a journey, corrupt the settled counts
|
|
3599
|
+
* and misattribute grooming spend to a merge it did not produce.
|
|
3600
|
+
*
|
|
3601
|
+
* Omit `spendUsd` when the provider reported no cost — see
|
|
3602
|
+
* {@link SessionSpendRow}.
|
|
3603
|
+
*/
|
|
3604
|
+
recordSessionSpend(row: SessionSpendRow): void;
|
|
3605
|
+
/** This project's daemon-owned session spend at or after `since`, newest
|
|
3606
|
+
* first. Never includes worker runs: those live in `runs` and are already
|
|
3607
|
+
* counted by {@link Store.statsRuns}, so summing both surfaces gives the
|
|
3608
|
+
* fleet's whole bill without double-counting either half. */
|
|
3609
|
+
sessionSpendSince(project: string, since: number): SessionSpendRow[];
|
|
3214
3610
|
/**
|
|
3215
3611
|
* Persist a rendered report `pending`, before anything is sent — the whole
|
|
3216
3612
|
* point of #123 is that an undelivered report is a queryable row rather than
|
|
@@ -3665,10 +4061,15 @@ export const VERB_NAMES = [
|
|
|
3665
4061
|
* re-enter normal review/merge without a fresh clone. Idempotent: an
|
|
3666
4062
|
* already-matching PR is returned, not duplicated. */
|
|
3667
4063
|
"conductor_pr_recover",
|
|
3668
|
-
/** The
|
|
4064
|
+
/** The first read verb. It answers with the merge gate's own verdict, so
|
|
3669
4065
|
* "pushed-green" is the dispatcher's reading of the PR rather than a claim the
|
|
3670
4066
|
* worker makes about itself from whatever it happened to run. */
|
|
3671
4067
|
"conductor_pr_status",
|
|
4068
|
+
/** The second read verb (#1043 lane): the decisive log lines of the failing
|
|
4069
|
+
* jobs at one pull request's head, through the tracker's bounded
|
|
4070
|
+
* failed-attempt log read. A worker calls it to diagnose a red check it
|
|
4071
|
+
* must fix, instead of guessing from the check name. */
|
|
4072
|
+
"conductor_ci_logs",
|
|
3672
4073
|
] as const;
|
|
3673
4074
|
|
|
3674
4075
|
export type VerbName = (typeof VERB_NAMES)[number];
|
package/src/verbs/protocol.ts
CHANGED
|
@@ -484,6 +484,34 @@ export const VERB_SPECS: Readonly<Record<VerbName, VerbSpec>> = {
|
|
|
484
484
|
},
|
|
485
485
|
roleRefusalText: (role) => `conductor_pr_status is not open to a ${role} session.`,
|
|
486
486
|
},
|
|
487
|
+
conductor_ci_logs: {
|
|
488
|
+
name: "conductor_ci_logs",
|
|
489
|
+
mutating: false,
|
|
490
|
+
allowedRoles: ["worker", "orchestrator"],
|
|
491
|
+
description:
|
|
492
|
+
"Read the failing CI jobs' own log output at one pull request's head. Call this when " +
|
|
493
|
+
"conductor_pr_status reports a red check you have to fix, INSTEAD of guessing from the check name or " +
|
|
494
|
+
"re-running the suite locally to see what CI saw. It answers with the failed steps of every failed job " +
|
|
495
|
+
"across the head's attempts, bounded to the newest evidence and truncated per job when large — so it is " +
|
|
496
|
+
"cheap in turns but not free in context; read it once and fix from it, do not poll it. Read-only: it " +
|
|
497
|
+
"changes nothing and is not ledgered. If the logs cannot be read it says so — an unreadable log is never " +
|
|
498
|
+
"reported as \"no failures\". A worker may omit prUrl and gets its own run's.",
|
|
499
|
+
args: {
|
|
500
|
+
prUrl: {
|
|
501
|
+
type: "string",
|
|
502
|
+
required: false,
|
|
503
|
+
description: "Full pull request URL. Omit as a worker to read your own run's.",
|
|
504
|
+
},
|
|
505
|
+
headSha: {
|
|
506
|
+
type: "string",
|
|
507
|
+
required: false,
|
|
508
|
+
description:
|
|
509
|
+
"Optional: the head you believe you are diagnosing. If the branch has moved since, the call is " +
|
|
510
|
+
"refused naming both shas rather than handing you logs from a superseded commit.",
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
roleRefusalText: (role) => `conductor_ci_logs is not open to a ${role} session.`,
|
|
514
|
+
},
|
|
487
515
|
};
|
|
488
516
|
|
|
489
517
|
export interface VerbRequest {
|