omp-conductor 0.19.7 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/admission.ts +58 -14
- package/src/arm-challenge.ts +255 -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 +258 -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 +115 -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 +422 -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 +788 -0
- package/src/daemon/settle-pass.ts +606 -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 +135 -9
- package/src/doctor.ts +2 -2
- package/src/failure-class.ts +257 -2
- package/src/fleet.ts +295 -176
- 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 +689 -1670
- package/src/ready-gate.ts +267 -0
- package/src/settlement.ts +107 -11
- 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 +169 -14
- package/src/store.ts +618 -28
- package/src/to-spec.ts +426 -44
- package/src/tracker/github.ts +50 -0
- package/src/types.ts +434 -18
- package/src/verbs/protocol.ts +28 -0
- package/src/verbs/server.ts +330 -39
- package/src/wake.ts +19 -2
- package/src/worker.ts +570 -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
|
|
@@ -1681,6 +1797,14 @@ export const FAILURE_CLASSES = [
|
|
|
1681
1797
|
"dispatch-infra",
|
|
1682
1798
|
"merge-conflict",
|
|
1683
1799
|
"question",
|
|
1800
|
+
/** A run that finished its work and stopped because it is waiting on an
|
|
1801
|
+
* observable condition — its settlement names `blockers:` (a PR whose
|
|
1802
|
+
* checks were still pending, say) and no question (#1068). Distinct from
|
|
1803
|
+
* `question` because the remedy is not a human: the recovery is the later
|
|
1804
|
+
* observation, which the settle sweep performs by re-offering the blocked
|
|
1805
|
+
* row until its PR resolves (this fleet's #1062 sat `blocked`/`question`
|
|
1806
|
+
* after its PR merged and its tick kept listing it for Duty 1 triage). */
|
|
1807
|
+
"awaiting-observation",
|
|
1684
1808
|
"orphan-clean",
|
|
1685
1809
|
"orphan-dirty",
|
|
1686
1810
|
"settlement-stuck",
|
|
@@ -1715,21 +1839,27 @@ export type FailureClass = (typeof FAILURE_CLASSES)[number];
|
|
|
1715
1839
|
/**
|
|
1716
1840
|
* What the daemon does about a class.
|
|
1717
1841
|
*
|
|
1718
|
-
*
|
|
1842
|
+
* Three of these perform nothing, and they are not the same nothing:
|
|
1719
1843
|
*
|
|
1844
|
+
* - `observe` — recorded and deliberately left alone, because the recovery IS
|
|
1845
|
+
* the later observation: `awaiting-observation` rows wait on something the
|
|
1846
|
+
* world will resolve (their PR), and the settle sweep re-offers the blocked
|
|
1847
|
+
* row until it does, settling it the way it settles `settlement-stuck` when
|
|
1848
|
+
* the PR merges. Like `hold` it never stamps `recoveredAt` — nothing is
|
|
1849
|
+
* recovered yet, by design (#1068).
|
|
1720
1850
|
* - `hold` — recorded and deliberately left alone, because acting would destroy
|
|
1721
1851
|
* something. `orphan-dirty` is the case: the tree is the only copy, so
|
|
1722
1852
|
* `unblock` refuses rather than re-claiming over it.
|
|
1723
1853
|
* - `none` — the classifier declining to name a failure at all. Two callers
|
|
1724
1854
|
* reach it, and neither is "unclassifiable": `classifyRun` returns it for a
|
|
1725
|
-
|
|
1855
|
+
* terminal row whose PR is open and fully green, where `classifyAndRecover`
|
|
1726
1856
|
* restores `pushed-green` *without* persisting a class (#766); and the settle
|
|
1727
1857
|
* sweep persists it beside `returned-for-revision` when a reviewer closes
|
|
1728
1858
|
* pushed work without merging, where the remedy is the queue label the sweep
|
|
1729
1859
|
* deliberately leaves on.
|
|
1730
1860
|
*
|
|
1731
1861
|
* An *unclassifiable* run is `unknown` → `escalate`, and never a silent retry.
|
|
1732
|
-
* This comment previously said nothing in the decision
|
|
1862
|
+
* This comment previously said nothing in the decision tree maps to `none`,
|
|
1733
1863
|
* which both callers above contradict; it cost a reading of two modules to
|
|
1734
1864
|
* discover, which is what a wrong comment costs (#132).
|
|
1735
1865
|
*/
|
|
@@ -1739,6 +1869,7 @@ export const RECOVERY_ACTIONS = [
|
|
|
1739
1869
|
"rerun-checks",
|
|
1740
1870
|
"settle",
|
|
1741
1871
|
"escalate",
|
|
1872
|
+
"observe",
|
|
1742
1873
|
"hold",
|
|
1743
1874
|
"none",
|
|
1744
1875
|
] as const;
|
|
@@ -2036,11 +2167,13 @@ export interface RunRecord {
|
|
|
2036
2167
|
*/
|
|
2037
2168
|
outputTokens?: number;
|
|
2038
2169
|
reasoningTokens?: number;
|
|
2039
|
-
/** The run id
|
|
2040
|
-
* (#536, #567)
|
|
2041
|
-
*
|
|
2042
|
-
*
|
|
2043
|
-
*
|
|
2170
|
+
/** The run id whose session this row resumes: the orphan-clean attempt it
|
|
2171
|
+
* inherited (#536, #567), or — since the blocked-continuation resume — the
|
|
2172
|
+
* answered-block run whose transcript this continuation carries on. Set only
|
|
2173
|
+
* when a dispatch-time resume verdict fired; absent means a fresh dispatch,
|
|
2174
|
+
* never "resumed from unknown". Readable without opening the transcript, so
|
|
2175
|
+
* "how often did resume fire and did it save turns" is a query, not a file
|
|
2176
|
+
* walk. */
|
|
2044
2177
|
resumedFromRunId?: string;
|
|
2045
2178
|
prUrl?: string;
|
|
2046
2179
|
/** Pull request head the worker observed after its deterministic CI watcher exited. */
|
|
@@ -2548,6 +2681,14 @@ export interface IntakeItem {
|
|
|
2548
2681
|
issueUrl?: string;
|
|
2549
2682
|
/** When the item was resolved into an issue (#300) or dismissed. */
|
|
2550
2683
|
groomedAt?: number;
|
|
2684
|
+
/**
|
|
2685
|
+
* Provenance of a machine-filed item (Phase 4 signal mining) — the stable
|
|
2686
|
+
* key of the signal that produced it, e.g.
|
|
2687
|
+
* `mined:settlement-weakening:omp/src/foo.ts`. Absent means an operator
|
|
2688
|
+
* typed this idea themselves, which surfaces matching on it must render as
|
|
2689
|
+
* their own idea rather than as an unattributed machine signal.
|
|
2690
|
+
*/
|
|
2691
|
+
source?: string;
|
|
2551
2692
|
}
|
|
2552
2693
|
|
|
2553
2694
|
/** What a caller hands over. The store owns the id, the state and the timestamps. */
|
|
@@ -2555,6 +2696,72 @@ export interface IntakeDraft {
|
|
|
2555
2696
|
project: string;
|
|
2556
2697
|
text: string;
|
|
2557
2698
|
at: number;
|
|
2699
|
+
/**
|
|
2700
|
+
* Provenance and dedupe key in one (Phase 4). When present, the store
|
|
2701
|
+
* derives the item's id DETERMINISTICALLY from `project` + `source` and
|
|
2702
|
+
* inserts only if that id is new, so an hourly mining pass re-filing a
|
|
2703
|
+
* signal it already filed is a no-op rather than an hourly duplicate.
|
|
2704
|
+
*
|
|
2705
|
+
* The consequence is intended: an item the operator dismissed stays
|
|
2706
|
+
* dismissed instead of reappearing every hour, because the second filing
|
|
2707
|
+
* cannot resurrect the row it collides with. The trap is the same fact from
|
|
2708
|
+
* the other side — a signal genuinely worth re-raising later needs a
|
|
2709
|
+
* DIFFERENT key, which is what a source carrying a bucket (a week, a run
|
|
2710
|
+
* count) is for. A bare `mined:<kind>:<path>` is a once-ever claim.
|
|
2711
|
+
*
|
|
2712
|
+
* Absent for an operator's own idea, which keeps a random id and is
|
|
2713
|
+
* therefore always a new row: two identical thoughts on two days are two
|
|
2714
|
+
* thoughts.
|
|
2715
|
+
*/
|
|
2716
|
+
source?: string;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
/**
|
|
2720
|
+
* Which daemon-owned session spent this (Phase 4 attribution). Exactly the two
|
|
2721
|
+
* the daemon runs itself: to-spec grooming scouts and review adjudication.
|
|
2722
|
+
*
|
|
2723
|
+
* The orchestrator's own tick turns are deliberately NOT a member. The tick
|
|
2724
|
+
* extension observes no spend or usage events at all — its `message_start`
|
|
2725
|
+
* adapter reads content text — so there is no number to record, and a `0` row
|
|
2726
|
+
* per tick would report the orchestrator as free. Orchestrator spend is
|
|
2727
|
+
* reported as unmetered instead of invented.
|
|
2728
|
+
*/
|
|
2729
|
+
export const SESSION_SPEND_ROLES = ["groom", "adjudicator"] as const;
|
|
2730
|
+
|
|
2731
|
+
export type SessionSpendRole = (typeof SESSION_SPEND_ROLES)[number];
|
|
2732
|
+
|
|
2733
|
+
/**
|
|
2734
|
+
* One daemon-owned session's turns and cost (Phase 4 attribution).
|
|
2735
|
+
*
|
|
2736
|
+
* The same shape going in and coming out: the store adds nothing but a rowid,
|
|
2737
|
+
* because every field here is something the caller actually observed.
|
|
2738
|
+
*
|
|
2739
|
+
* `spendUsd` is OPTIONAL, and that is the contract rather than convenience.
|
|
2740
|
+
* In the `runs` table an unmetered request and a genuinely free one are both
|
|
2741
|
+
* `0`, which is why `spend-telemetry.ts` has to infer a subscription verdict
|
|
2742
|
+
* from the share of zeros instead of reading a fact. Here the caller
|
|
2743
|
+
* distinguishes them: omit `spendUsd` when the provider reported no cost for
|
|
2744
|
+
* the session (subscription billing, or a harness that emitted no usage
|
|
2745
|
+
* event), pass it when it reported a number — including a real `0`. A reader
|
|
2746
|
+
* MUST render an absent `spendUsd` as unmetered and never as `$0.00`.
|
|
2747
|
+
*/
|
|
2748
|
+
export interface SessionSpendRow {
|
|
2749
|
+
project: string;
|
|
2750
|
+
role: SessionSpendRole;
|
|
2751
|
+
/** The issue the session was about, when it was about one. Absent for a
|
|
2752
|
+
* grooming batch that spanned several. */
|
|
2753
|
+
issue?: number;
|
|
2754
|
+
/** The model the caller asked for. */
|
|
2755
|
+
model?: string;
|
|
2756
|
+
/** The model the harness actually ran, which is the one that was billed and
|
|
2757
|
+
* the one a per-model breakdown groups on. Absent when unobserved — never
|
|
2758
|
+
* filled in from `model`, because a requested model is not evidence of a
|
|
2759
|
+
* resolved one. */
|
|
2760
|
+
resolvedModel?: string;
|
|
2761
|
+
turns: number;
|
|
2762
|
+
/** Absent means the provider reported no cost. See the interface note. */
|
|
2763
|
+
spendUsd?: number;
|
|
2764
|
+
at: number;
|
|
2558
2765
|
}
|
|
2559
2766
|
|
|
2560
2767
|
/**
|
|
@@ -2569,6 +2776,16 @@ export const GROOMING_VERDICTS = ["promotable", "blocked", "considered"] as cons
|
|
|
2569
2776
|
|
|
2570
2777
|
export type GroomingVerdict = (typeof GROOMING_VERDICTS)[number];
|
|
2571
2778
|
|
|
2779
|
+
/**
|
|
2780
|
+
* Who acted on a grooming verdict by queueing the issue (#1041). Three actors,
|
|
2781
|
+
* because the promotion audit's whole question is which of them did it: the
|
|
2782
|
+
* daemon promotes unattended once the ready gate passes, an orchestrator tick
|
|
2783
|
+
* promotes as part of its own reasoning, and an operator promotes by hand. A
|
|
2784
|
+
* fourth spelling would mean a fourth promotion path exists, which is exactly
|
|
2785
|
+
* what this closed union is here to prevent.
|
|
2786
|
+
*/
|
|
2787
|
+
export type PromotedBy = "daemon" | "orchestrator" | "operator";
|
|
2788
|
+
|
|
2572
2789
|
/**
|
|
2573
2790
|
* One issue's current grooming verdict, durable across restarts and keyed by
|
|
2574
2791
|
* project + issue — one row per issue, replaced in place by upsert, never a
|
|
@@ -2587,6 +2804,24 @@ export interface GroomingRecord {
|
|
|
2587
2804
|
* or a scout summary. Free text, bounded at the write site. */
|
|
2588
2805
|
evidence: string;
|
|
2589
2806
|
recordedAt: number;
|
|
2807
|
+
/**
|
|
2808
|
+
* When this verdict was acted on by queueing the issue (#1041). Absent on
|
|
2809
|
+
* every verdict nobody promoted, which is the honest reading of a row from
|
|
2810
|
+
* before mechanical promotion existed: nothing recorded who queued it, so
|
|
2811
|
+
* nothing may claim to know.
|
|
2812
|
+
*
|
|
2813
|
+
* Set exactly once per verdict, by {@link Store.markGroomingPromoted}, and
|
|
2814
|
+
* cleared by the next `upsertGrooming` — a re-recorded verdict is a new
|
|
2815
|
+
* judgement about the issue, and provenance that outlived the judgement it
|
|
2816
|
+
* describes would audit the wrong decision.
|
|
2817
|
+
*/
|
|
2818
|
+
promotedAt?: number;
|
|
2819
|
+
/** Who queued it: the daemon's own ready gate, an orchestrator tick, or an
|
|
2820
|
+
* operator at the CLI. Stored rather than inferred — the tick's promotion
|
|
2821
|
+
* audit exists precisely to review what the daemon did unattended, and a
|
|
2822
|
+
* guess would make that review circular. Always present when
|
|
2823
|
+
* {@link promotedAt} is, and absent when it is not. */
|
|
2824
|
+
promotedBy?: PromotedBy;
|
|
2590
2825
|
}
|
|
2591
2826
|
|
|
2592
2827
|
/** What a caller hands over. The store owns the timestamp and the replace-in-
|
|
@@ -2600,6 +2835,32 @@ export interface GroomingDraft {
|
|
|
2600
2835
|
at: number;
|
|
2601
2836
|
}
|
|
2602
2837
|
|
|
2838
|
+
/**
|
|
2839
|
+
* One operator's standing approval of an epic's scope (#1041).
|
|
2840
|
+
*
|
|
2841
|
+
* Deliberately its own durable fact rather than a resolved decision row, for
|
|
2842
|
+
* two reasons that both bite in production. First, approving an epic is an
|
|
2843
|
+
* explicit, revocable act with exactly one meaning — "the children of this
|
|
2844
|
+
* epic may be queued without asking me again" — while a decision row's
|
|
2845
|
+
* `resolution` is free text a human wrote for a human, and no amount of
|
|
2846
|
+
* parsing turns "yes, but do the migration first" into a machine gate. Second,
|
|
2847
|
+
* decision rows expire ({@link DECISION_TTL_MS}) and an approval must not:
|
|
2848
|
+
* scope consent does not lapse after a week just because nobody looked at it.
|
|
2849
|
+
*
|
|
2850
|
+
* Carries no `project` field because it is always read through a
|
|
2851
|
+
* project-scoped accessor — the scoping is the key, not a payload column an
|
|
2852
|
+
* unwary caller could compare against the wrong project.
|
|
2853
|
+
*/
|
|
2854
|
+
export interface EpicApproval {
|
|
2855
|
+
issue: number;
|
|
2856
|
+
approvedAt: number;
|
|
2857
|
+
/** Who approved it, in the same spelling the report-withdrawal path already
|
|
2858
|
+
* uses: the session role when one is set, else `orchestrator` inside a
|
|
2859
|
+
* Herdr pane, else `operator`. Free text on purpose — it is provenance for
|
|
2860
|
+
* a human reading an audit line, never a gate anything branches on. */
|
|
2861
|
+
approvedBy: string;
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2603
2864
|
/**
|
|
2604
2865
|
* Where one operator decision stands (#136).
|
|
2605
2866
|
*
|
|
@@ -2781,7 +3042,7 @@ export interface Store {
|
|
|
2781
3042
|
* two concurrent same-head findings cannot both create a row or overwrite
|
|
2782
3043
|
* each other (the run row's state is the other half once the revision is
|
|
2783
3044
|
* dispatched). */
|
|
2784
|
-
enqueueReviewRevision(draft:
|
|
3045
|
+
enqueueReviewRevision(draft: ReviewRevisionDraft): ReviewRevisionEnqueue;
|
|
2785
3046
|
/** The pending revision for one run, if any — the header the verb reads to
|
|
2786
3047
|
* tell "this call folds into the in-flight round" from "this call opens a
|
|
2787
3048
|
* new round" before the round-ceiling gate applies. */
|
|
@@ -2793,6 +3054,51 @@ export interface Store {
|
|
|
2793
3054
|
latestReviewRound(project: string, runId: string): number;
|
|
2794
3055
|
/** Record that a revision was handed to a worker. */
|
|
2795
3056
|
markReviewRevisionDispatched(id: string, at: number): void;
|
|
3057
|
+
/** Record, once, how this review round is being launched (#1045).
|
|
3058
|
+
*
|
|
3059
|
+
* Write-once and atomic — one guarded UPDATE, never read-then-write, so two
|
|
3060
|
+
* dispatch passes cannot each decide. The FIRST decision for a round wins:
|
|
3061
|
+
*
|
|
3062
|
+
* - no decision yet ⇒ written, `true`;
|
|
3063
|
+
* - the identical decision again (a dispatch retry, a restart re-deciding
|
|
3064
|
+
* the same way) ⇒ no-op, `true`, and the original `launchDecidedAt`
|
|
3065
|
+
* stands;
|
|
3066
|
+
* - a *contradictory* decision (different mode, requested model or origin
|
|
3067
|
+
* lineage) ⇒ nothing written, `false`. The caller must re-read the
|
|
3068
|
+
* recorded decision and honour it rather than launching its own;
|
|
3069
|
+
* - no such round in this project ⇒ `false`.
|
|
3070
|
+
*
|
|
3071
|
+
* Touches the decision columns only. The issue, run, attempt/continuation
|
|
3072
|
+
* charges, round number, branch, PR URL, reviewed head, findings and
|
|
3073
|
+
* settlement history are the round's identity and are never written here. */
|
|
3074
|
+
recordReviewRevisionLaunch(
|
|
3075
|
+
project: string,
|
|
3076
|
+
id: string,
|
|
3077
|
+
decision: {
|
|
3078
|
+
launchMode: ReviewLaunchMode;
|
|
3079
|
+
requestedModel?: string;
|
|
3080
|
+
originSessionRef?: string;
|
|
3081
|
+
at: number;
|
|
3082
|
+
},
|
|
3083
|
+
): boolean;
|
|
3084
|
+
/** Attach the fresh correction's own session lineage — and the model the
|
|
3085
|
+
* harness actually resolved — to a round already decided
|
|
3086
|
+
* `fresh-correction` (#1045). `false`, writing nothing, on a round decided
|
|
3087
|
+
* `resume-original`, on an undecided round, or on no such round: a resumed
|
|
3088
|
+
* round *is* its origin session, so recording a separate correction
|
|
3089
|
+
* lineage for one would invent a session that never existed.
|
|
3090
|
+
*
|
|
3091
|
+
* Not write-once, and deliberately so: an infra-killed round relaunches on
|
|
3092
|
+
* the same decision, and the newest fresh session is then the truth about
|
|
3093
|
+
* what is running. `resolvedModel` is only ever added, never cleared — a
|
|
3094
|
+
* later call without one leaves a known resolution in place. */
|
|
3095
|
+
recordReviewCorrectionSession(project: string, id: string, ref: string, resolvedModel?: string): boolean;
|
|
3096
|
+
/** The recorded launch decision and provenance for one round, or `undefined`
|
|
3097
|
+
* when no such round exists (#1045). Every field is absent on a round
|
|
3098
|
+
* nobody has decided and on every pre-#1045 row — the explicit unknown a
|
|
3099
|
+
* claim/retry/restart path must read *before* deciding, and the honest
|
|
3100
|
+
* projection a renderer must print instead of guessing. */
|
|
3101
|
+
reviewRevisionLaunch(project: string, id: string): ReviewCorrectionProvenance | undefined;
|
|
2796
3102
|
/** Record how a revision ended. */
|
|
2797
3103
|
settleReviewRevision(id: string, outcome: ReviewRevisionOutcome, at: number): void;
|
|
2798
3104
|
/** Every revision row not yet settled — queued (`dispatchedAt` unset) and
|
|
@@ -3118,15 +3424,37 @@ export interface Store {
|
|
|
3118
3424
|
getMaterialEvent(id: string): MaterialEvent | undefined;
|
|
3119
3425
|
/** Ordinary material outcomes still owed, oldest first and bounded. */
|
|
3120
3426
|
undigestedMaterialEvents(project: string, limit?: number): MaterialEvent[];
|
|
3121
|
-
/**
|
|
3427
|
+
/**
|
|
3428
|
+
* Persist one raw idea, `pending`, for later grooming (#299).
|
|
3429
|
+
*
|
|
3430
|
+
* Idempotent when {@link IntakeDraft.source} is present (Phase 4): the id is
|
|
3431
|
+
* derived from `project` + `source` and the insert is ignored if that id
|
|
3432
|
+
* already exists, so the hourly mining pass is safe to re-run. A collision
|
|
3433
|
+
* returns the EXISTING row untouched — which is how a caller tells a new
|
|
3434
|
+
* filing from a no-op (`item.createdAt !== draft.at`, or a `state` that is
|
|
3435
|
+
* no longer `pending`) — and in particular does NOT reopen an item the
|
|
3436
|
+
* operator dismissed.
|
|
3437
|
+
*
|
|
3438
|
+
* Without a `source` the id is random, so every call is a new row.
|
|
3439
|
+
*/
|
|
3122
3440
|
recordIntake(draft: IntakeDraft): IntakeItem;
|
|
3123
3441
|
/** Ideas still `pending`, oldest first — what `intake list` and `status` read. */
|
|
3124
3442
|
pendingIntake(project: string): IntakeItem[];
|
|
3125
3443
|
/** Resolve one idea to `groomed` (recording the issue URL #300 chose) or
|
|
3126
3444
|
* `dismissed`. `false` when the id is unknown. */
|
|
3127
3445
|
resolveIntake(id: string, state: "groomed" | "dismissed", issueUrl?: string): boolean;
|
|
3128
|
-
/**
|
|
3129
|
-
*
|
|
3446
|
+
/**
|
|
3447
|
+
* Record the current grooming verdict for one issue, replacing any prior
|
|
3448
|
+
* row — one row per project + issue, never a history (#735).
|
|
3449
|
+
*
|
|
3450
|
+
* Clears {@link GroomingRecord.promotedAt}/{@link GroomingRecord.promotedBy}
|
|
3451
|
+
* (#1041). A re-recorded verdict is a fresh judgement about the issue, so
|
|
3452
|
+
* carrying the old promotion forward would let the audit attribute a queueing
|
|
3453
|
+
* to a verdict that no longer exists — and, worse, would make a demote-then-
|
|
3454
|
+
* regroom cycle read as though the demoted promotion still stood. A caller
|
|
3455
|
+
* that re-promotes stamps provenance again through
|
|
3456
|
+
* {@link markGroomingPromoted}, which is the only writer of these columns.
|
|
3457
|
+
*/
|
|
3130
3458
|
upsertGrooming(draft: GroomingDraft): void;
|
|
3131
3459
|
/** The current grooming verdict for one issue, or undefined when none. */
|
|
3132
3460
|
grooming(project: string, issue: number): GroomingRecord | undefined;
|
|
@@ -3142,6 +3470,15 @@ export interface Store {
|
|
|
3142
3470
|
* self-heals instead of lingering as a cache someone must invalidate. Runs
|
|
3143
3471
|
* at the end of every admission pass. Never touches `promotable` or
|
|
3144
3472
|
* `considered` rows, which belong to #679's scout loop.
|
|
3473
|
+
*
|
|
3474
|
+
* A promoted row (#1041) is likewise inert here, in both directions: it is
|
|
3475
|
+
* neither overwritten with `blocked` nor swept by the self-clearing delete.
|
|
3476
|
+
* Promotion hands the issue to dispatch, and admission's lane/dependency
|
|
3477
|
+
* holds on a queued issue are flow control of the moment, not a re-grooming.
|
|
3478
|
+
* Without this the fleet would lose the audit line within one 5-minute
|
|
3479
|
+
* admission pass — a promoted issue whose lane overlaps an active run would
|
|
3480
|
+
* be rewritten as `blocked` and then deleted — long before the ~30-minute
|
|
3481
|
+
* tick that is supposed to review the promotion ever read it.
|
|
3145
3482
|
*/
|
|
3146
3483
|
reconcileGrooming(
|
|
3147
3484
|
project: string,
|
|
@@ -3162,6 +3499,48 @@ export interface Store {
|
|
|
3162
3499
|
* snapshot here deletes verdicts nobody re-derived.
|
|
3163
3500
|
*/
|
|
3164
3501
|
retireGroomingNotOpen(project: string, openIssues: readonly number[]): number[];
|
|
3502
|
+
/**
|
|
3503
|
+
* Stamp promotion provenance on one existing verdict (#1041), returning
|
|
3504
|
+
* whether this call is the one that stamped it.
|
|
3505
|
+
*
|
|
3506
|
+
* `false` on both no-op shapes, and the caller must treat them the same way:
|
|
3507
|
+
* there is no such verdict, or it already carries a promotion. That makes the
|
|
3508
|
+
* write the single-flight latch for promotion itself — the daemon enqueues the
|
|
3509
|
+
* queue label and fires the wake only when it wins this call, so a restart
|
|
3510
|
+
* mid-promotion re-reads a stamped row and does not queue the issue twice.
|
|
3511
|
+
* Provenance is therefore stamped BEFORE the label op, never after.
|
|
3512
|
+
*/
|
|
3513
|
+
markGroomingPromoted(project: string, issue: number, at: number, by: PromotedBy): boolean;
|
|
3514
|
+
/**
|
|
3515
|
+
* Verdicts promoted at or after `since`, newest promotion first — the tick's
|
|
3516
|
+
* promotion audit (#1041): what was queued unattended while nobody was
|
|
3517
|
+
* looking, with the verdict and evidence that justified it still attached.
|
|
3518
|
+
*
|
|
3519
|
+
* Bounded by the window rather than by a count, because the audit's contract
|
|
3520
|
+
* is "everything since the last tick" — a truncated list would silently
|
|
3521
|
+
* un-audit a promotion.
|
|
3522
|
+
*/
|
|
3523
|
+
promotionsSince(project: string, since: number): GroomingRecord[];
|
|
3524
|
+
/**
|
|
3525
|
+
* Record the operator's standing approval of one epic's scope (#1041).
|
|
3526
|
+
*
|
|
3527
|
+
* Idempotent and first-write-wins: re-approving keeps the original
|
|
3528
|
+
* `approvedAt` and `approvedBy`, because the durable fact is when consent was
|
|
3529
|
+
* given, not when it was last restated. A caller that must tell "approved
|
|
3530
|
+
* now" from "already approved" reads {@link epicApproval} first. Refuses a
|
|
3531
|
+
* non-integer or non-positive issue number rather than storing a fact about
|
|
3532
|
+
* an issue that cannot exist.
|
|
3533
|
+
*/
|
|
3534
|
+
approveEpic(project: string, issue: number, at: number, by: string): void;
|
|
3535
|
+
/** Withdraw an epic's approval, `false` when there was none to withdraw.
|
|
3536
|
+
* Approval is revocable by design: scope consent an operator regrets must be
|
|
3537
|
+
* removable without deleting the epic or its children (#1041). */
|
|
3538
|
+
revokeEpicApproval(project: string, issue: number): boolean;
|
|
3539
|
+
/** One epic's approval, or `undefined` when it has none — which every gate
|
|
3540
|
+
* must read as "not approved", never as approval whose row is missing. */
|
|
3541
|
+
epicApproval(project: string, issue: number): EpicApproval | undefined;
|
|
3542
|
+
/** Every approved epic in this project, issue-ascending. */
|
|
3543
|
+
epicApprovals(project: string): EpicApproval[];
|
|
3165
3544
|
/** Count and age source for status and digest prompt bounds. */
|
|
3166
3545
|
digestBacklog(project: string): DigestBacklog;
|
|
3167
3546
|
/** Add one bounded observation to the per-day friction rollup. */
|
|
@@ -3211,6 +3590,38 @@ export interface Store {
|
|
|
3211
3590
|
bumpGhCalls?(day: string, source: string): void;
|
|
3212
3591
|
/** The tracked call counts for a UTC day, per source. */
|
|
3213
3592
|
ghCallsToday?(day: string): { source: string; calls: number }[];
|
|
3593
|
+
/**
|
|
3594
|
+
* Record one auto-restart the daemon fired at a wedged orchestrator
|
|
3595
|
+
* (Phase 4). A bare append, like {@link Store.recordGhRefusal}: the cap is
|
|
3596
|
+
* enforced by counting rows in a window, so there is no counter to reset and
|
|
3597
|
+
* a crash cannot leave a high-water mark that either disables the cap or
|
|
3598
|
+
* spends it twice. Rows older than a week are pruned in the same write.
|
|
3599
|
+
*/
|
|
3600
|
+
recordOrchestratorRestart(project: string, at: number, reason: string): void;
|
|
3601
|
+
/**
|
|
3602
|
+
* This project's auto-restarts at or after `since`, NEWEST FIRST — the
|
|
3603
|
+
* bounded-restart gate counts them against its per-window cap, and the page
|
|
3604
|
+
* it sends when the cap is reached names the most recent reasons.
|
|
3605
|
+
*/
|
|
3606
|
+
orchestratorRestartsSince(project: string, since: number): { at: number; reason: string }[];
|
|
3607
|
+
/**
|
|
3608
|
+
* Record what one daemon-owned session (grooming, adjudication) cost
|
|
3609
|
+
* (Phase 4 attribution). Append-only; these sessions have no lifecycle to
|
|
3610
|
+
* update, they either ran or they did not.
|
|
3611
|
+
*
|
|
3612
|
+
* Deliberately not a `runs` row: `computeStats` walks runs as issue journeys
|
|
3613
|
+
* and a role row in there would invent a journey, corrupt the settled counts
|
|
3614
|
+
* and misattribute grooming spend to a merge it did not produce.
|
|
3615
|
+
*
|
|
3616
|
+
* Omit `spendUsd` when the provider reported no cost — see
|
|
3617
|
+
* {@link SessionSpendRow}.
|
|
3618
|
+
*/
|
|
3619
|
+
recordSessionSpend(row: SessionSpendRow): void;
|
|
3620
|
+
/** This project's daemon-owned session spend at or after `since`, newest
|
|
3621
|
+
* first. Never includes worker runs: those live in `runs` and are already
|
|
3622
|
+
* counted by {@link Store.statsRuns}, so summing both surfaces gives the
|
|
3623
|
+
* fleet's whole bill without double-counting either half. */
|
|
3624
|
+
sessionSpendSince(project: string, since: number): SessionSpendRow[];
|
|
3214
3625
|
/**
|
|
3215
3626
|
* Persist a rendered report `pending`, before anything is sent — the whole
|
|
3216
3627
|
* point of #123 is that an undelivered report is a queryable row rather than
|
|
@@ -3665,10 +4076,15 @@ export const VERB_NAMES = [
|
|
|
3665
4076
|
* re-enter normal review/merge without a fresh clone. Idempotent: an
|
|
3666
4077
|
* already-matching PR is returned, not duplicated. */
|
|
3667
4078
|
"conductor_pr_recover",
|
|
3668
|
-
/** The
|
|
4079
|
+
/** The first read verb. It answers with the merge gate's own verdict, so
|
|
3669
4080
|
* "pushed-green" is the dispatcher's reading of the PR rather than a claim the
|
|
3670
4081
|
* worker makes about itself from whatever it happened to run. */
|
|
3671
4082
|
"conductor_pr_status",
|
|
4083
|
+
/** The second read verb (#1043 lane): the decisive log lines of the failing
|
|
4084
|
+
* jobs at one pull request's head, through the tracker's bounded
|
|
4085
|
+
* failed-attempt log read. A worker calls it to diagnose a red check it
|
|
4086
|
+
* must fix, instead of guessing from the check name. */
|
|
4087
|
+
"conductor_ci_logs",
|
|
3672
4088
|
] as const;
|
|
3673
4089
|
|
|
3674
4090
|
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 {
|