@coreplane/switchboard 1.260.0 → 1.260.2

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 (49) hide show
  1. package/bin/switchboard.js +15 -11
  2. package/dist/assets/config/config.example.yaml +1 -0
  3. package/dist/assets/deploy/cloudflare/worker.ts +17 -56
  4. package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +6 -0
  5. package/dist/assets/deploy/cloudflare-memory/backgroundTasks.ts +25 -0
  6. package/dist/assets/deploy/cloudflare-memory/worker.ts +36 -16
  7. package/dist/assets/deploy/cloudflare-resident/worker.ts +39 -18
  8. package/dist/assets/deploy/profile.example.json +2 -1
  9. package/dist/assets/package-lock.json +3 -3
  10. package/dist/assets/package.json +1 -1
  11. package/dist/assets/source.json +3 -3
  12. package/dist/assets/src/agents/registry.ts +2 -2
  13. package/dist/assets/src/core/budgets.ts +19 -6
  14. package/dist/assets/src/core/coordinator/driver.ts +47 -9
  15. package/dist/assets/src/core/pipelineStanding.ts +2 -0
  16. package/dist/assets/src/core/reviewedHead.ts +16 -2
  17. package/dist/assets/src/core/runEvents.ts +9 -1
  18. package/dist/assets/src/core/runLedger/types.ts +3 -3
  19. package/dist/assets/src/core/runRecord.ts +28 -8
  20. package/dist/assets/src/core/ship/coordinator.ts +248 -44
  21. package/dist/assets/src/core/ship/renewal.ts +2 -0
  22. package/dist/assets/src/deploy/liveGate.ts +8 -0
  23. package/dist/assets/src/deploy/profile.ts +8 -0
  24. package/dist/assets/src/deploy/restart.ts +49 -35
  25. package/dist/assets/web/dist/.vite/manifest.json +67 -67
  26. package/dist/assets/web/dist/assets/{DeliveryPage-DvMrWUg7.js → DeliveryPage-62qCSLvk.js} +1 -1
  27. package/dist/assets/web/dist/assets/{HomePage-CcGEJ4w0.js → HomePage-Dk3HRBc3.js} +1 -1
  28. package/dist/assets/web/dist/assets/{PendingTurnRow-BzGVxDYs.js → PendingTurnRow-CeTGOXUy.js} +1 -1
  29. package/dist/assets/web/dist/assets/{PlanePage-Arj9cyd5.js → PlanePage-RD0M6O6W.js} +1 -1
  30. package/dist/assets/web/dist/assets/{ResidentDetailPage-CBPPe4Sj.js → ResidentDetailPage-Dl2KId95.js} +1 -1
  31. package/dist/assets/web/dist/assets/{ResidentsIndexPage-CEpXgGo7.js → ResidentsIndexPage-DBy43I1R.js} +1 -1
  32. package/dist/assets/web/dist/assets/{RunFoldRow-BJq7tnLr.js → RunFoldRow-CwHDL-Mo.js} +1 -1
  33. package/dist/assets/web/dist/assets/{RunRoutePage-DWiny7LN.js → RunRoutePage-Du1HqLZs.js} +3 -3
  34. package/dist/assets/web/dist/assets/{RunsIndexPage-Cxufw5sV.js → RunsIndexPage-BZphy3Hr.js} +1 -1
  35. package/dist/assets/web/dist/assets/{ScheduledPage-SBcuDdVg.js → ScheduledPage-B7jh898t.js} +1 -1
  36. package/dist/assets/web/dist/assets/{SettingsPage-DrD_vWdp.js → SettingsPage-3uLEsiiE.js} +1 -1
  37. package/dist/assets/web/dist/assets/{SilentTurn-Bjl7EPEN.js → SilentTurn-CKxSxnQC.js} +1 -1
  38. package/dist/assets/web/dist/assets/{StatusDot-BqteQTav.js → StatusDot-x7vcK0iE.js} +1 -1
  39. package/dist/assets/web/dist/assets/{Tooltip-KRqJXEPN.js → Tooltip-wVXCWhMp.js} +1 -1
  40. package/dist/assets/web/dist/assets/{UnitRoutePage-Cv28FV4I.js → UnitRoutePage-V5BXK7FH.js} +1 -1
  41. package/dist/assets/web/dist/assets/budgets-KNNkT4PZ.js +1 -0
  42. package/dist/assets/web/dist/assets/{dist-CTASUSno.js → dist-C-XGnvM4.js} +1 -1
  43. package/dist/assets/web/dist/assets/{indexRow-B1YBy_IL.js → indexRow-Dxg7C8s9.js} +1 -1
  44. package/dist/assets/web/dist/assets/{main-eTAe9-hk.js → main-DAL--hyj.js} +2 -2
  45. package/dist/assets/web/dist/assets/sseReplay-BqgOggyQ.js +11 -0
  46. package/dist/cli.js +1405 -436
  47. package/package.json +1 -1
  48. package/dist/assets/web/dist/assets/budgets-DQXVllsm.js +0 -1
  49. package/dist/assets/web/dist/assets/sseReplay-BcsNbn2j.js +0 -11
@@ -51,6 +51,7 @@ import {
51
51
  type PlanCursor,
52
52
  type UnitStatus,
53
53
  renderUnitReport,
54
+ reenterApprovedRebase,
54
55
  settleUnit,
55
56
  startUnit,
56
57
  type ChildFacts,
@@ -114,6 +115,7 @@ export type CoordinatorStepRoute =
114
115
  | "unit-wake"
115
116
  | "checks"
116
117
  | "merge"
118
+ | "rebase"
117
119
  | "finish";
118
120
 
119
121
  /** What a step stores: the bot's reply as the wire carried it — its status and
@@ -469,7 +471,7 @@ function prCheckReturn(step: string, a: BotAnswer): StepReturn {
469
471
  * ask's answer carries `retried` instead: whether the re-run was dispatched,
470
472
  * so the machine never waits on a head an undispatched re-run left unchanged. */
471
473
  function checksReturn(step: string, a: BotAnswer): StepReturn {
472
- const { ok, checks, draft, retried, at } = a.body;
474
+ const { ok, checks, draft, retried, refired, at } = a.body;
473
475
  if (ok !== true) throw new UnreadableAnswer("checks", a, "ok");
474
476
  return {
475
477
  type: "checks",
@@ -479,6 +481,7 @@ function checksReturn(step: string, a: BotAnswer): StepReturn {
479
481
  // unit for the ready event instead of merging or ending without a cause.
480
482
  ...(draft === true ? { draft: true } : {}),
481
483
  ...(typeof retried === "boolean" ? { retried } : {}),
484
+ ...(typeof refired === "boolean" ? { refired } : {}),
482
485
  at,
483
486
  };
484
487
  }
@@ -489,7 +492,10 @@ const isRoundChecks = (v: unknown): v is RoundChecks =>
489
492
  Array.isArray(v.pending) &&
490
493
  v.pending.every((n: unknown) => typeof n === "string") &&
491
494
  Array.isArray(v.failed) &&
492
- v.failed.every((f: unknown) => isRecord(f) && typeof f.name === "string" && typeof f.conclusion === "string");
495
+ v.failed.every((f: unknown) => isRecord(f) && typeof f.name === "string" && typeof f.conclusion === "string") &&
496
+ (v.required === undefined ||
497
+ (Array.isArray(v.required) && v.required.every((n: unknown) => typeof n === "string"))) &&
498
+ (v.expected === undefined || (Array.isArray(v.expected) && v.expected.every((n: unknown) => typeof n === "string")));
493
499
 
494
500
  function mergeReturn(step: string, a: BotAnswer): StepReturn {
495
501
  const { ok, outcome, by, sha, mergedAt, reason, at } = a.body;
@@ -501,13 +507,26 @@ function mergeReturn(step: string, a: BotAnswer): StepReturn {
501
507
  return { type: "merge", step, outcome: "merged", sha, at };
502
508
  if (
503
509
  ok === true &&
504
- (outcome === "pending" || outcome === "refused" || outcome === "enqueued" || outcome === "removed") &&
510
+ (outcome === "pending" ||
511
+ outcome === "refused" ||
512
+ outcome === "conflict" ||
513
+ outcome === "enqueued" ||
514
+ outcome === "removed") &&
505
515
  typeof reason === "string"
506
516
  )
507
517
  return { type: "merge", step, outcome, reason, at };
508
518
  throw new UnreadableAnswer("merge", a, "outcome");
509
519
  }
510
520
 
521
+ function rebaseReturn(step: string, a: BotAnswer): StepReturn {
522
+ const { ok, outcome, headSha, reason, at } = a.body;
523
+ if (ok === true && (outcome === "carried" || outcome === "changed") && typeof headSha === "string")
524
+ return { type: "rebase", step, outcome, headSha, at };
525
+ if (ok === true && (outcome === "conflict" || outcome === "refused") && typeof reason === "string")
526
+ return { type: "rebase", step, outcome, reason, at };
527
+ throw new UnreadableAnswer("rebase", a, "outcome");
528
+ }
529
+
511
530
  // ---- the steps ----------------------------------------------------------------------------------------
512
531
 
513
532
  /** One bot call inside a step: a reply that is not the bot's answer and a
@@ -670,8 +689,8 @@ async function perform(
670
689
  }
671
690
  case "checks":
672
691
  // The round's checks step (record 0055): the bot reads the check runs at
673
- // the reviewed head with the merge door's own reading — or, on a retry
674
- // ask, re-runs the named failed checks' jobs first.
692
+ // the reviewed head with the merge door's own reading — or performs one
693
+ // of the step's bounded recovery effects first.
675
694
  return checksReturn(
676
695
  action.step,
677
696
  answerOf(
@@ -682,6 +701,7 @@ async function perform(
682
701
  prNumber: action.prNumber,
683
702
  headSha: action.headSha,
684
703
  ...(action.retry !== undefined ? { retry: action.retry } : {}),
704
+ ...(action.refire === true ? { refire: true } : {}),
685
705
  }),
686
706
  ),
687
707
  ),
@@ -701,6 +721,16 @@ async function perform(
701
721
  ),
702
722
  ),
703
723
  );
724
+ case "rebase":
725
+ return rebaseReturn(
726
+ action.step,
727
+ answerOf(
728
+ "rebase",
729
+ await step.do(action.step, STEP_CONFIG, () =>
730
+ call(bot, "rebase", { ...tag, prNumber: action.prNumber, headSha: action.headSha }),
731
+ ),
732
+ ),
733
+ );
704
734
  }
705
735
  }
706
736
 
@@ -833,7 +863,7 @@ async function runUnit(
833
863
  let notes = 0;
834
864
  let endedAt: number | undefined;
835
865
  try {
836
- for (;;) {
866
+ pipeline: for (;;) {
837
867
  const action = nextAction(state);
838
868
  if (action.type === "end") return { ...action.ending, ...(endedAt !== undefined ? { endedAt } : {}) };
839
869
  // Keep the current round across its non-round phases (for example merge)
@@ -887,9 +917,9 @@ async function runUnit(
887
917
  // The checks at the approved head (record 0055): the report's
888
918
  // headline is a claim about them, never "merge-ready" over a red one.
889
919
  ...(check.pr.checks !== undefined ? { checks: check.pr.checks } : {}),
890
- // The ready state beside them (agent-ship item 9): a conflicting
891
- // head, or one carrying an unsquashed fix-up commit, is reported
892
- // approved-but-not-merge-ready, never "merge-ready".
920
+ // The ready state beside them (agent-ship item 9): a conflict
921
+ // feeds the runner re-entry below, while an unsquashed fix-up
922
+ // commit remains an approved-but-not-merge-ready report.
893
923
  ...(check.pr.mergeableState !== undefined ? { mergeableState: check.pr.mergeableState } : {}),
894
924
  ...(check.pr.fixupCommits !== undefined ? { fixupCommits: check.pr.fixupCommits } : {}),
895
925
  // The base's merge-queue rule (issue 2011): the merge:person
@@ -902,6 +932,14 @@ async function runUnit(
902
932
  // the report simply omits the fact
903
933
  }
904
934
  }
935
+ // A person-merge pipeline used to publish this dirty state as its
936
+ // ending and hand the pull request to the sweep. It remains the
937
+ // owner instead: rung one runs now, and only a conflict buys a coding
938
+ // child under this run's remaining lease.
939
+ if (note.ending.kind === "merge_ready" && endFacts?.mergeableState === "dirty") {
940
+ state = reenterApprovedRebase(state);
941
+ continue pipeline;
942
+ }
905
943
  // The last coding child's run is named so the bot can put its handoff
906
944
  // — the deviations it recorded — on the unit's board issue beside the
907
945
  // ending (agent-ship item 14).
@@ -94,6 +94,7 @@ export const ROUND_STAGE = {
94
94
  request_changes: "fix",
95
95
  no_verdict: "hold",
96
96
  checks_failed: "fix",
97
+ checks_restarted: "approved",
97
98
  transient: "hold",
98
99
  enqueued: "approved",
99
100
  dequeued: "fix",
@@ -174,6 +175,7 @@ export const ROUND_OUTCOME_WORDS = {
174
175
  request_changes: "changes requested",
175
176
  no_verdict: "no verdict",
176
177
  checks_failed: "checks failed",
178
+ checks_restarted: "checks restarted",
177
179
  transient: "retried",
178
180
  enqueued: "queued to merge",
179
181
  dequeued: "removed from the merge queue",
@@ -51,6 +51,20 @@ export function sameCommit(a: string, b: string): boolean {
51
51
  return n >= 7 && a.slice(0, n) === b.slice(0, n);
52
52
  }
53
53
 
54
+ /** A mismatch sentence that cannot print two identical accepted-looking
55
+ * prefixes: it names the authoritative source, both values and their first
56
+ * differing hex position. */
57
+ function mismatchReason(source: "workspace-observed" | "model-reported", reviewed: string, expected: string): string {
58
+ const compared = Math.min(reviewed.length, expected.length);
59
+ let at = 0;
60
+ while (at < compared && reviewed[at] === expected[at]) at += 1;
61
+ const divergence =
62
+ at < compared
63
+ ? `they first differ at hex ${at + 1} (${reviewed[at]} ≠ ${expected[at]})`
64
+ : `the ${reviewed.length}-hex reviewed value ends before the ${expected.length}-hex PR head`;
65
+ return `${source} reviewed head ${reviewed} is not the PR head ${expected}; ${divergence}`;
66
+ }
67
+
54
68
  /**
55
69
  * Decide whether the reviewed head is the PR head. `expected` is the PR head
56
70
  * from repo resolution; `observed` the dispatcher's own `git rev-parse HEAD`
@@ -63,7 +77,7 @@ export function checkReviewedHead(input: { expected?: string; observed?: string;
63
77
  if (observed) {
64
78
  return sameCommit(observed, expected)
65
79
  ? { ok: true, head: observed, source: "observed" }
66
- : { ok: false, reason: `reviewed head ${observed.slice(0, 7)} is not the PR head ${expected.slice(0, 7)}` };
80
+ : { ok: false, reason: mismatchReason("workspace-observed", observed, expected) };
67
81
  }
68
82
  const reported = normalizeHead(input.reported);
69
83
  if (!reported)
@@ -73,5 +87,5 @@ export function checkReviewedHead(input: { expected?: string; observed?: string;
73
87
  };
74
88
  return sameCommit(reported, expected)
75
89
  ? { ok: true, head: reported, source: "reported" }
76
- : { ok: false, reason: `reviewed head ${reported.slice(0, 7)} is not the PR head ${expected.slice(0, 7)}` };
90
+ : { ok: false, reason: mismatchReason("model-reported", reported, expected) };
77
91
  }
@@ -294,6 +294,10 @@ export type RunNoteKind =
294
294
  * there was nothing to push), so a re-issue starts from the partial work
295
295
  * (docs/reference/specs/agent-ship.md item 8). */
296
296
  | "budget_salvage"
297
+ /** A coordinator's coding child ended failed, stopped or interrupted with
298
+ * work in its tree: before teardown the run loop made a WIP commit and
299
+ * pushed the unit branch, or recorded why no push was possible. */
300
+ | "work_salvage"
297
301
  /** A pi run's compaction failed for good — the provider refused the summary
298
302
  * (harness-pi.md item 7) — and the run loop treated it as a checkpoint
299
303
  * signal: the tracked changes were committed and pushed to the run's own
@@ -363,6 +367,7 @@ export const RUN_NOTE_KINDS = [
363
367
  "tool_unnamed",
364
368
  "directory_reached",
365
369
  "budget_salvage",
370
+ "work_salvage",
366
371
  "compaction_salvage",
367
372
  "stuck_loop",
368
373
  "decline_cascade",
@@ -432,6 +437,9 @@ export type ShipRoundOutcome =
432
437
  * 0055): the failures become check findings and the findings step runs as
433
438
  * for any changes-requested round. */
434
439
  | "checks_failed"
440
+ /** No required check appeared during the checks step's grace chunk, so the
441
+ * runner re-fired the pull_request event once by closing and reopening it. */
442
+ | "checks_restarted"
435
443
  /** The round's coding child died on a provider transient with nothing
436
444
  * pushed (issue 1932): the first such boundary marks the round's one
437
445
  * re-run, a second the `transient` ending. */
@@ -1096,7 +1104,7 @@ export type RunEvent =
1096
1104
  * `model` is the ref the run uses, resolved from a model the person
1097
1105
  * named in plain words (the plain-words model unit) — applied at
1098
1106
  * directive precedence. Additive: unknown → no model. */
1099
- binds?: ReadonlyArray<{ line: string; reason: string; model?: string; confirmed?: true }>;
1107
+ binds?: ReadonlyArray<{ line: string; reason: string; model?: string; repo?: string; confirmed?: true }>;
1100
1108
  question?: string;
1101
1109
  /** A question's proposed line, redacted and cut like the receipt — what
1102
1110
  * the next turn's "yes" binds (`bindFromAnswer`). */
@@ -128,9 +128,9 @@ export interface LiveRunMeta {
128
128
  export type RunState = Record<string, unknown>;
129
129
 
130
130
  /** A hosted ship parent's hosting fact on the row's state (record 0060): the
131
- * runner instance the run hosts and the deadline past which a reclaim closes
132
- * the row `interrupted` instead of re-hosting it. Set by the ship branch once
133
- * the instance exists; read by the boot reclaim. */
131
+ * runner instance the run hosts and its original wall-clock estimate. Reclaim
132
+ * may outlive that estimate while the Workflow or one of its child rows is
133
+ * live; only no durable owner beyond it closes the row. */
134
134
  export interface HostingState {
135
135
  instanceId: string;
136
136
  /** Epoch ms: the hand-off time plus the instance's `caps.maxMinutes` plus
@@ -289,19 +289,23 @@ export interface RunLease {
289
289
  loopEndsAt: number;
290
290
  }
291
291
 
292
- /** A head a run pushed, as the record names it (item 2): the branch and the sha. */
292
+ /** A head a run pushed, as the record names it (item 2): the branch, sha,
293
+ * and whether the agent pushed it or the run's mechanical WIP salvage did.
294
+ * `by` is optional for records written before that distinction survived the
295
+ * event fold. */
293
296
  export interface PushedHead {
294
297
  ref: string;
295
298
  sha: string;
299
+ by?: "push" | "salvage";
296
300
  }
297
301
 
298
302
  /** The heads a run's events say it pushed — one per branch, the last event's
299
303
  * sha winning, in first-seen order — or nothing when it pushed none. */
300
304
  export function pushedHeadsOf(events: readonly RunEvent[]): PushedHead[] | undefined {
301
- const byRef = new Map<string, string>();
302
- for (const e of events) if (e.type === "pushed_head") byRef.set(e.ref, e.sha);
305
+ const byRef = new Map<string, { sha: string; by: "push" | "salvage" }>();
306
+ for (const e of events) if (e.type === "pushed_head") byRef.set(e.ref, { sha: e.sha, by: e.by });
303
307
  if (byRef.size === 0) return undefined;
304
- return [...byRef].map(([ref, sha]) => ({ ref, sha }));
308
+ return [...byRef].map(([ref, pushed]) => ({ ref, ...pushed }));
305
309
  }
306
310
 
307
311
  /** The lease a run's events say its harness started — the first `lease`
@@ -440,7 +444,7 @@ export interface RunOperatorDecision {
440
444
  mode: "shadow" | "on";
441
445
  outcome: "binds" | "question" | "refusal" | "non_decision";
442
446
  reason: string;
443
- binds?: { line: string; reason: string }[];
447
+ binds?: { line: string; reason: string; model?: string; repo?: string; confirmed?: true }[];
444
448
  question?: string;
445
449
  /** A question's proposed line — what the next turn's "yes" binds. */
446
450
  proposal?: string;
@@ -468,7 +472,17 @@ export function operatorOfEvents(events: readonly RunEvent[]): RunOperatorDecisi
468
472
  mode: e.mode,
469
473
  outcome: e.outcome,
470
474
  reason: e.reason,
471
- ...(e.binds ? { binds: e.binds.map((b) => ({ line: b.line, reason: b.reason })) } : {}),
475
+ ...(e.binds
476
+ ? {
477
+ binds: e.binds.map((b) => ({
478
+ line: b.line,
479
+ reason: b.reason,
480
+ ...(b.model !== undefined ? { model: b.model } : {}),
481
+ ...(b.repo !== undefined ? { repo: b.repo } : {}),
482
+ ...(b.confirmed ? { confirmed: true as const } : {}),
483
+ })),
484
+ }
485
+ : {}),
472
486
  ...(e.question !== undefined ? { question: e.question } : {}),
473
487
  ...(e.proposal !== undefined ? { proposal: e.proposal } : {}),
474
488
  ...(e.request !== undefined ? { request: e.request } : {}),
@@ -1019,8 +1033,14 @@ export function isRunRecord(v: unknown): v is RunRecord {
1019
1033
  Array.isArray(r.pushed) &&
1020
1034
  r.pushed.every((h) => {
1021
1035
  if (typeof h !== "object" || h === null) return false;
1022
- const { ref, sha } = h as Record<string, unknown>;
1023
- return typeof ref === "string" && ref.length > 0 && typeof sha === "string" && /^[0-9a-f]{7,40}$/.test(sha);
1036
+ const { ref, sha, by } = h as Record<string, unknown>;
1037
+ return (
1038
+ typeof ref === "string" &&
1039
+ ref.length > 0 &&
1040
+ typeof sha === "string" &&
1041
+ /^[0-9a-f]{7,40}$/.test(sha) &&
1042
+ (by === undefined || by === "push" || by === "salvage")
1043
+ );
1024
1044
  })
1025
1045
  )
1026
1046
  )