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.
Files changed (69) hide show
  1. package/REFERENCE.md +10 -1
  2. package/agents/to-spec.md +76 -9
  3. package/package.json +1 -1
  4. package/schema/config.schema.json +4 -0
  5. package/src/arm-challenge.ts +204 -85
  6. package/src/ask.ts +130 -615
  7. package/src/board.ts +7 -1
  8. package/src/brief-upgrade.ts +24 -0
  9. package/src/briefs/console.md +253 -0
  10. package/src/briefs/correction.md +203 -0
  11. package/src/briefs/orchestrator.md +167 -97
  12. package/src/briefs/policy.md +19 -16
  13. package/src/briefs/to-spec.md +76 -9
  14. package/src/briefs/worker.md +50 -16
  15. package/src/cli.ts +4 -0
  16. package/src/command-manifest.ts +54 -8
  17. package/src/commands/arm.ts +113 -49
  18. package/src/commands/console.ts +70 -0
  19. package/src/commands/context.ts +2 -0
  20. package/src/commands/epic.ts +132 -0
  21. package/src/commands/extend.ts +9 -1
  22. package/src/commands/intake.ts +44 -14
  23. package/src/commands/stats.ts +19 -4
  24. package/src/commands/worker.ts +9 -1
  25. package/src/config-schema.ts +13 -0
  26. package/src/config.ts +27 -0
  27. package/src/daemon/ack.ts +159 -0
  28. package/src/daemon/admission-pass.ts +135 -0
  29. package/src/daemon/brief.ts +461 -0
  30. package/src/daemon/deps.ts +539 -0
  31. package/src/daemon/dispatch.ts +1779 -0
  32. package/src/daemon/drain.ts +185 -0
  33. package/src/daemon/groom-pass.ts +412 -0
  34. package/src/daemon/http.ts +417 -0
  35. package/src/daemon/integrity.ts +108 -0
  36. package/src/daemon/panes.ts +180 -0
  37. package/src/daemon/review.ts +1888 -0
  38. package/src/daemon/runtime.ts +736 -0
  39. package/src/daemon/settle-pass.ts +589 -0
  40. package/src/daemon/supervision.ts +438 -0
  41. package/src/daemon/tick.ts +968 -0
  42. package/src/daemon/views.ts +751 -0
  43. package/src/daemon.ts +105 -7923
  44. package/src/dashboard/app.js +58 -0
  45. package/src/dashboard/controls.ts +22 -3
  46. package/src/dashboard/server.ts +4 -0
  47. package/src/diff-flags.ts +24 -3
  48. package/src/failure-class.ts +75 -1
  49. package/src/fleet.ts +290 -164
  50. package/src/groom.ts +461 -0
  51. package/src/http-token.ts +142 -0
  52. package/src/knowledge.ts +229 -0
  53. package/src/mining.ts +316 -0
  54. package/src/orchestrator-tick.ts +428 -1681
  55. package/src/ready-gate.ts +267 -0
  56. package/src/settlement.ts +72 -6
  57. package/src/setup-host.ts +32 -9
  58. package/src/setup-wizard.ts +55 -7
  59. package/src/setup.ts +229 -3
  60. package/src/stats.ts +257 -2
  61. package/src/status-render.ts +158 -7
  62. package/src/store.ts +604 -26
  63. package/src/to-spec.ts +194 -21
  64. package/src/tracker/github.ts +50 -0
  65. package/src/types.ts +416 -15
  66. package/src/verbs/protocol.ts +28 -0
  67. package/src/verbs/server.ts +330 -39
  68. package/src/wake.ts +19 -2
  69. package/src/worker.ts +456 -1
@@ -19,7 +19,13 @@
19
19
 
20
20
  import { formatZonedMinute } from "./availability.ts";
21
21
  import { spendTelemetryDetail, type SpendTelemetryVerdict } from "./spend-telemetry.ts";
22
- import type { DaemonStop, GroomingRecord, InstallSurfaceObservation, RunRecord } from "./types.ts";
22
+ import type {
23
+ DaemonStop,
24
+ GroomingRecord,
25
+ InstallSurfaceObservation,
26
+ ReviewCorrectionRound,
27
+ RunRecord,
28
+ } from "./types.ts";
23
29
  import { settlementFlagSummary } from "./diff-flags.ts";
24
30
  import type { CodeGraphHealth } from "./graph-health.ts";
25
31
  import { formatDigestBacklog, formatOpenReports } from "./reports.ts";
@@ -28,6 +34,12 @@ import { planUsageLine } from "./usage.ts";
28
34
  import { HEALTH_TIMEOUT_MS, SYSTEMD_UNIT, type UnitOwnership } from "./lifecycle.ts";
29
35
  import type { WorkerPauseView } from "./fleet.ts";
30
36
  import { formatRss, rssBytesFromHealthz } from "./host.ts";
37
+ // One constant, two readers: the durable in-flight to-spec launch marker
38
+ // (#777) is written by the daemon's grooming launcher and read here, so the
39
+ // reason string is imported rather than hand-copied — the copy that used to
40
+ // live in this file was a second source of truth for a row shape nobody
41
+ // audits.
42
+ import { TO_SPEC_IN_FLIGHT_REASON } from "./groom.ts";
31
43
  import {
32
44
  formatBaseHealth,
33
45
  formatDispatchSummary,
@@ -252,10 +264,6 @@ const REFUSED_GROOMING_REASONS: Record<string, true> = {
252
264
  "stale-source": true,
253
265
  };
254
266
 
255
- /** The durable in-flight launch marker (#777) — a batch is running right now.
256
- * Mirrors `TO_SPEC_IN_FLIGHT_REASON` in `orchestrator-tick.ts`. */
257
- const GROOMING_IN_FLIGHT_REASON = "in-flight";
258
-
259
267
  /**
260
268
  * One project's durable grooming state as status lines, or nothing when there
261
269
  * is nothing to report (#809).
@@ -332,7 +340,7 @@ export function formatGroomingStatus(input: GroomingStatusInput): string | undef
332
340
  row.verdict === "blocked" &&
333
341
  MECHANICAL_GROOMING_REASONS[row.reason] !== true &&
334
342
  REFUSED_GROOMING_REASONS[row.reason] !== true &&
335
- row.reason !== GROOMING_IN_FLIGHT_REASON,
343
+ row.reason !== TO_SPEC_IN_FLIGHT_REASON,
336
344
  );
337
345
  const mechanical = records.filter(
338
346
  (row) => row.verdict === "blocked" && MECHANICAL_GROOMING_REASONS[row.reason] === true,
@@ -341,7 +349,7 @@ export function formatGroomingStatus(input: GroomingStatusInput): string | undef
341
349
  (row) => row.verdict === "blocked" && REFUSED_GROOMING_REASONS[row.reason] === true,
342
350
  );
343
351
  const inFlight = records.filter(
344
- (row) => row.verdict === "blocked" && row.reason === GROOMING_IN_FLIGHT_REASON,
352
+ (row) => row.verdict === "blocked" && row.reason === TO_SPEC_IN_FLIGHT_REASON,
345
353
  );
346
354
  const awaiting = Math.max(0, routed - records.length);
347
355
  const lines: string[] = [];
@@ -662,6 +670,143 @@ function formatSiblingLive(siblings: { project: string; live: number }[]): strin
662
670
  return `shared daemon also serves ${siblings.length} other project(s): ${counts}`;
663
671
  }
664
672
 
673
+ // review-correction provenance (#1048)
674
+ // ---------------------------------------------------------------------------
675
+
676
+ /**
677
+ * How many rounds of one run's correction chain print. A review ceiling is low
678
+ * single digits, so this bounds a pathological row rather than routinely
679
+ * truncating — and the last line printed says how many rounds it stands for,
680
+ * because a silently shortened chain is how a repeating pattern hides.
681
+ */
682
+ const CORRECTION_CHAIN_LINES = 3;
683
+
684
+ /**
685
+ * A session lineage as a status row carries it. A ref is a session file path,
686
+ * and the leading directories are the same for every session in a fleet: the
687
+ * identifying half is the run and the file, so the line prints those and marks
688
+ * what it dropped with `…/` — the same honesty `firstLine` uses above, and the
689
+ * full ref stays in the durable row this line is projected from. Two lineages
690
+ * plus two models on one row is already at the width `omp-conductor status`
691
+ * can be read at; a full 60-character path twice is not.
692
+ *
693
+ * An absent ref reads `unrecorded`: rows written before #1045 know nothing
694
+ * about their own launch, and saying so is the only honest projection of one.
695
+ */
696
+ function correctionRef(ref: string | undefined): string {
697
+ const value = ref?.trim() ?? "";
698
+ if (value === "") return "unrecorded";
699
+ const segments = value.split("/").filter((segment) => segment !== "");
700
+ const tail = segments.slice(-2).join("/");
701
+ return firstLine(segments.length > 2 ? `…/${tail}` : value, 60);
702
+ }
703
+
704
+ /**
705
+ * The models of one correction round: what the daemon asked for and what the
706
+ * harness actually resolved. Those two differing is the whole finding — in
707
+ * #1035 the fleet had already switched worker models while every correction
708
+ * round replayed the exhausted session under the old one — so the arrow form is
709
+ * kept whenever they differ and collapsed only when they agree.
710
+ *
711
+ * A resumed round is worded differently on purpose. `resolvedModel` is written
712
+ * only by `Store.recordReviewCorrectionSession`, which refuses a round decided
713
+ * `resume-original` (a resumed round *is* its origin session), so a resume
714
+ * durably has a request and no resolution. Rendering that through the fresh
715
+ * wording — `requested X → ran unrecorded` — would read as a launch that lost
716
+ * its model, when what actually happened is that the daemon asked the harness
717
+ * to continue an existing transcript on X and nothing re-recorded the
718
+ * resolution.
719
+ *
720
+ * Every branch reads the round's OWN recorded values. The project config is
721
+ * deliberately not a parameter: rendering it would make a round dispatched
722
+ * under a since-replaced model report the replacement, which is exactly the
723
+ * silent fake #1048 names.
724
+ */
725
+ function correctionModels(round: ReviewCorrectionRound): string {
726
+ const { requestedModel: requested, resolvedModel: resolved } = round;
727
+ if (round.launchMode === "resume-original") {
728
+ if (resolved !== undefined)
729
+ return requested === undefined || requested === resolved
730
+ ? `ran ${resolved}`
731
+ : `ran ${resolved} (asked ${requested})`;
732
+ if (requested !== undefined)
733
+ return round.state === "pending"
734
+ ? `to continue on ${requested}, not launched yet`
735
+ : `asked to continue on ${requested}`;
736
+ return "model unrecorded";
737
+ }
738
+ if (requested !== undefined && resolved !== undefined)
739
+ return requested === resolved ? `model ${resolved} as requested` : `requested ${requested} → ran ${resolved}`;
740
+ if (requested !== undefined)
741
+ return round.state === "pending"
742
+ ? `requested ${requested}, not launched yet`
743
+ : `requested ${requested} → ran unrecorded`;
744
+ // A resolution with no request: the harness named what it ran, the decision
745
+ // row that asked for it predates #1045. Still not a guess in either direction.
746
+ if (resolved !== undefined) return `ran ${resolved} (request unrecorded)`;
747
+ return "model unrecorded";
748
+ }
749
+
750
+ /**
751
+ * One run's review-correction chain as status lines (#1048), newest round
752
+ * first, six-space indented like every other run annotation.
753
+ *
754
+ * Before this, a correction round rendered as `review-revision N` and a
755
+ * dispatch age: an operator could not tell whether conductor had resumed the
756
+ * diagnosed implementation transcript or launched a fresh correction, nor under
757
+ * which model — which is why #1035 read as idle while an exhausted session
758
+ * consumed every round the fleet's new model was never asked to run.
759
+ *
760
+ * The per-line `current`/`prior` marker is where the runtime/history split
761
+ * lives: `current` is the newest round that has not settled, the one whose
762
+ * elapsed time and cumulative turns the run line above reports; every other
763
+ * line is chain history and carries no runtime at all. That is deliberately a
764
+ * property of each line rather than a header, because a header explaining how
765
+ * to read the following lines is a line nobody reads.
766
+ *
767
+ * Exported so the projection can be asserted directly on hand-built rounds:
768
+ * the interesting cases (a differing requested/resolved pair, a legacy row with
769
+ * no provenance) are properties of one row, not of a whole fleet.
770
+ */
771
+ export function formatReviewCorrections(rounds: readonly ReviewCorrectionRound[]): string[] {
772
+ if (rounds.length === 0) return [];
773
+ const newestFirst = [...rounds].sort((a, b) => b.round - a.round);
774
+ const shown = newestFirst.slice(0, CORRECTION_CHAIN_LINES);
775
+ const hidden = newestFirst.length - shown.length;
776
+ return shown.map((round, index) => {
777
+ const current = index === 0 && round.state !== "settled";
778
+ // The round's lifecycle as one phrase. `queued` covers both a round nobody
779
+ // has dispatched yet and one a restart returned to the queue — durably they
780
+ // are the same row, and status must not invent a distinction the store does
781
+ // not keep.
782
+ const state =
783
+ round.state === "settled"
784
+ ? `settled ${round.outcome ?? "outcome unrecorded"}`
785
+ : round.state === "pending"
786
+ ? "queued"
787
+ : "dispatched";
788
+ const provenance =
789
+ round.launchMode === undefined
790
+ ? // A row written before #1045 recorded no launch decision. Status says
791
+ // so and stops: the alternative — reading today's configuration back
792
+ // as though it had been this round's — is a fabricated provenance,
793
+ // and an operator auditing #1035 would have believed it.
794
+ "provenance unknown (pre-#1045 round)"
795
+ : round.launchMode === "resume-original"
796
+ ? // A resume continues one lineage, so there is one session to name:
797
+ // the implementation transcript the review diagnosed.
798
+ `resume-original resumed session ${correctionRef(round.originSessionRef)} ${correctionModels(round)}`
799
+ : // A fresh correction has two: the transcript that was reviewed, and
800
+ // the new session doing the correcting. Naming only one of them is
801
+ // how "fresh" became indistinguishable from "resumed".
802
+ `fresh-correction new session ${correctionRef(round.correctionSessionRef)} (diagnosed ${correctionRef(
803
+ round.originSessionRef,
804
+ )}) ${correctionModels(round)}`;
805
+ const tail = index === shown.length - 1 && hidden > 0 ? ` (+${hidden} earlier round(s))` : "";
806
+ return ` correction round ${round.round} ${current ? "current" : "prior"} (${state}) ${provenance}${tail}`;
807
+ });
808
+ }
809
+
665
810
 
666
811
  function formatProjectBody(
667
812
  s: StatusSnapshot,
@@ -867,6 +1012,12 @@ function formatProjectBody(
867
1012
  : "failed at this head"
868
1013
  } — unresolved findings; push a corrected head, or record a conductor_pr_review_clear for this exact head, before merge`,
869
1014
  );
1015
+ // The correction chain (#1048), rendered from the same annotation helper as
1016
+ // the merge blocker above so it reaches BOTH surfaces that show a run: the
1017
+ // live lease and the preserved artifact. A round whose worker has finished
1018
+ // leaves the lease list, and that is precisely when an operator asks what
1019
+ // the last correction actually did.
1020
+ out.push(...formatReviewCorrections(s.reviewCorrections?.[r.id] ?? []));
870
1021
  return out;
871
1022
  };
872
1023
  if (s.activeRuns.length === 0) {