@coreplane/switchboard 1.256.0 → 1.257.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 (58) hide show
  1. package/dist/assets/config/config.example.yaml +14 -0
  2. package/dist/assets/deploy/cloudflare-memory/worker.ts +340 -7
  3. package/dist/assets/deploy/cloudflare-resident/drain.ts +49 -4
  4. package/dist/assets/deploy/cloudflare-resident/worker.ts +52 -4
  5. package/dist/assets/package-lock.json +3 -3
  6. package/dist/assets/package.json +5 -3
  7. package/dist/assets/project.json +17 -9
  8. package/dist/assets/source.json +3 -3
  9. package/dist/assets/src/agents/registry.ts +24 -1
  10. package/dist/assets/src/core/authz/grants.ts +12 -0
  11. package/dist/assets/src/core/authz/policy.ts +15 -0
  12. package/dist/assets/src/core/budgets.ts +51 -17
  13. package/dist/assets/src/core/coordinator/contract.ts +45 -5
  14. package/dist/assets/src/core/coordinator/driver.ts +6 -3
  15. package/dist/assets/src/core/delivery.ts +8 -5
  16. package/dist/assets/src/core/plane/decide.ts +468 -12
  17. package/dist/assets/src/core/plane/findings.ts +120 -0
  18. package/dist/assets/src/core/runEvents.ts +41 -14
  19. package/dist/assets/src/core/runFriction.ts +16 -0
  20. package/dist/assets/src/core/runLedger/types.ts +16 -3
  21. package/dist/assets/src/core/runRecord.ts +12 -0
  22. package/dist/assets/src/core/ship/contract.ts +14 -2
  23. package/dist/assets/src/core/ship/coordinator.ts +98 -29
  24. package/dist/assets/src/core/ship/renewal.ts +19 -17
  25. package/dist/assets/src/core/trace/attrs.ts +1 -1
  26. package/dist/assets/src/core/untrusted.ts +35 -0
  27. package/dist/assets/src/execution/residentCredentials.ts +7 -4
  28. package/dist/assets/web/dist/.vite/manifest.json +80 -55
  29. package/dist/assets/web/dist/assets/HomePage-DJqJQzrL.js +1 -0
  30. package/dist/assets/web/dist/assets/{PendingTurnRow-DGINv9XT.js → PendingTurnRow-fhKqKUVv.js} +1 -1
  31. package/dist/assets/web/dist/assets/PlanePage-Bood0hNO.js +1 -0
  32. package/dist/assets/web/dist/assets/{ResidentDetailPage-jYhEeeyu.js → ResidentDetailPage-DOe1HtUC.js} +1 -1
  33. package/dist/assets/web/dist/assets/{ResidentsIndexPage-DvFmlV4U.js → ResidentsIndexPage-TyCGjXM4.js} +1 -1
  34. package/dist/assets/web/dist/assets/RunFoldRow-C_4t-sit.js +1 -0
  35. package/dist/assets/web/dist/assets/{RunRoutePage-CvZ-TOT3.js → RunRoutePage-BNepPMRX.js} +4 -4
  36. package/dist/assets/web/dist/assets/RunsIndexPage-jZnMw1mM.js +1 -0
  37. package/dist/assets/web/dist/assets/{ScheduledPage-Viim-bus.js → ScheduledPage-BkNoudDP.js} +1 -1
  38. package/dist/assets/web/dist/assets/{SettingsPage-CYUy8McC.js → SettingsPage-COvWGXl-.js} +1 -1
  39. package/dist/assets/web/dist/assets/SilentTurn-DNAANKgN.js +2 -0
  40. package/dist/assets/web/dist/assets/SlackMark-CtiHrNjr.js +1 -0
  41. package/dist/assets/web/dist/assets/{StatusDot-Dv6UMaPy.js → StatusDot-BgmkSV0S.js} +1 -1
  42. package/dist/assets/web/dist/assets/{Tooltip-Bd-5Rypv.js → Tooltip-DEUPuCPW.js} +1 -1
  43. package/dist/assets/web/dist/assets/UnitRoutePage-DNhdzxvY.js +1 -0
  44. package/dist/assets/web/dist/assets/{dist-Nl3uaxrP.js → dist-CF3jz9LM.js} +1 -1
  45. package/dist/assets/web/dist/assets/durationTone-CVpX_yIk.js +1 -0
  46. package/dist/assets/web/dist/assets/main-7SAujY_s.js +28 -0
  47. package/dist/assets/web/dist/assets/main-DGC6WFqS.css +1 -0
  48. package/dist/assets/web/dist/assets/{sseReplay-DE6wv1Ua.js → sseReplay-BRJEIh83.js} +1 -1
  49. package/dist/cli.js +2672 -1102
  50. package/package.json +1 -1
  51. package/dist/assets/web/dist/assets/HomePage-BEiStFwW.js +0 -3
  52. package/dist/assets/web/dist/assets/PlanePage-JEj-lqgz.js +0 -1
  53. package/dist/assets/web/dist/assets/RunFoldRow-DvWzR1JQ.js +0 -1
  54. package/dist/assets/web/dist/assets/RunsIndexPage-JqVSDOok.js +0 -1
  55. package/dist/assets/web/dist/assets/UnitRoutePage-B81wEnKN.js +0 -1
  56. package/dist/assets/web/dist/assets/durationTone-U_rOqo6r.js +0 -1
  57. package/dist/assets/web/dist/assets/main-C4QfwybO.css +0 -1
  58. package/dist/assets/web/dist/assets/main-DRxWlffc.js +0 -28
@@ -0,0 +1,120 @@
1
+ // The plane's findings (docs/decisions/0064, "Endings and the watches"): a
2
+ // finding is filed when no move applies
3
+ // — it carries the watch's name and the timeline of events that fired it, and
4
+ // two findings on one watch and subject are ONE, their timelines merged. The
5
+ // plane itself calls no model: the bot dispatches a research-tier run over the
6
+ // timeline inside the untrusted fence (`findingResearchRequest`) and files the
7
+ // answer through the path `friction propose` uses (`filePlaneFinding`, the
8
+ // same `IssueTracker` seam). The dedupe half is node-free, like `decide.ts`.
9
+
10
+ import { wrapUntrusted } from "../untrusted.js";
11
+
12
+ /** The tracker seam, structurally the `IssueTracker` of
13
+ * `src/execution/githubIssues.ts` (the friction proposer's) — declared here
14
+ * so this module stays node-free like `decide.ts` and imports nothing the
15
+ * Worker's build cannot carry. */
16
+ export interface PlaneIssueRef {
17
+ number: number;
18
+ url: string;
19
+ title: string;
20
+ body: string;
21
+ }
22
+
23
+ export interface PlaneIssueTracker {
24
+ listOpen(repo: string, label: string): Promise<PlaneIssueRef[]>;
25
+ create(repo: string, issue: { title: string; body: string; labels: string[] }): Promise<PlaneIssueRef>;
26
+ }
27
+
28
+ /** One event on a finding's timeline: when, and what the watch saw — words
29
+ * the bot recorded, never a model's. */
30
+ export interface PlaneFindingEvent {
31
+ at: number;
32
+ what: string;
33
+ }
34
+
35
+ /** A finding: the watch, its subject (the run, pull request or instance the
36
+ * watch fired about) and the timeline. Deduplicated by watch and subject —
37
+ * `planeFindingKey` — so the same incident observed twice is one finding. */
38
+ export interface PlaneFinding {
39
+ watch: string;
40
+ subject: string;
41
+ timeline: PlaneFindingEvent[];
42
+ firstAt: number;
43
+ lastAt: number;
44
+ }
45
+
46
+ export function planeFindingKey(f: Pick<PlaneFinding, "watch" | "subject">): string {
47
+ return `${f.watch}#${f.subject}`;
48
+ }
49
+
50
+ /** A timeline is bounded: a watch that fires forever grows one finding, not
51
+ * unbounded storage — the newest events are kept. */
52
+ export const PLANE_FINDING_TIMELINE_CAP = 50;
53
+
54
+ /** Fold one observation into the set: a finding already standing for the same
55
+ * watch and subject absorbs the timeline (two findings on one subject are
56
+ * one); a new pair appends. Pure — the object commits the returned set. */
57
+ export function mergePlaneFindings(existing: readonly PlaneFinding[], incoming: PlaneFinding): PlaneFinding[] {
58
+ const key = planeFindingKey(incoming);
59
+ const standing = existing.find((f) => planeFindingKey(f) === key);
60
+ if (!standing) return [...existing, { ...incoming, timeline: incoming.timeline.slice(-PLANE_FINDING_TIMELINE_CAP) }];
61
+ const timeline = [...standing.timeline, ...incoming.timeline]
62
+ .sort((a, b) => a.at - b.at)
63
+ .slice(-PLANE_FINDING_TIMELINE_CAP);
64
+ const merged: PlaneFinding = {
65
+ ...standing,
66
+ timeline,
67
+ firstAt: Math.min(standing.firstAt, incoming.firstAt),
68
+ lastAt: Math.max(standing.lastAt, incoming.lastAt),
69
+ };
70
+ return existing.map((f) => (planeFindingKey(f) === key ? merged : f));
71
+ }
72
+
73
+ /** The request text for the research-tier run the bot dispatches over a
74
+ * finding (record 0064): the watch and subject are the bot's own words; the
75
+ * timeline — recorded run output, GitHub words, whatever the watch saw — is
76
+ * data inside the untrusted fence, never instructions. */
77
+ export function findingResearchRequest(finding: PlaneFinding): string {
78
+ const timeline = finding.timeline.map((e) => `${new Date(e.at).toISOString()} — ${e.what}`).join("\n");
79
+ return [
80
+ `The orchestration plane's \`${finding.watch}\` watch fired for \`${finding.subject}\` and no mechanical move applied.`,
81
+ `Investigate the timeline below and write up what happened, why the watch fired, and what change would prevent it — a report to file as an issue, nothing executed.`,
82
+ wrapUntrusted(timeline),
83
+ ].join("\n\n");
84
+ }
85
+
86
+ /** The issue title a finding files under — also the dedupe key on the tracker:
87
+ * an open issue with this exact title is the same finding, not filed twice. */
88
+ export function planeFindingIssueTitle(finding: Pick<PlaneFinding, "watch" | "subject">): string {
89
+ return `plane finding: ${finding.watch} — ${finding.subject}`;
90
+ }
91
+
92
+ export const PLANE_FINDING_LABEL = "switchboard-plane";
93
+
94
+ export type FiledPlaneFinding = { kind: "filed"; issue: PlaneIssueRef } | { kind: "duplicate"; issue: PlaneIssueRef };
95
+
96
+ /** File one finding's write-up through the path `friction propose` uses — the
97
+ * `IssueTracker` seam over the repository's open issues: an open issue with
98
+ * the finding's title is a duplicate (the tracker-side half of the dedupe),
99
+ * else the report is created under the plane's label. The body is the
100
+ * research run's answer with the timeline appended as data. */
101
+ export async function filePlaneFinding(
102
+ finding: PlaneFinding,
103
+ report: string,
104
+ opts: { tracker: PlaneIssueTracker; repo: string; label?: string },
105
+ ): Promise<FiledPlaneFinding> {
106
+ const label = opts.label ?? PLANE_FINDING_LABEL;
107
+ const title = planeFindingIssueTitle(finding);
108
+ const open = await opts.tracker.listOpen(opts.repo, label);
109
+ const existing = open.find((i) => i.title === title);
110
+ if (existing) return { kind: "duplicate", issue: existing };
111
+ const timeline = finding.timeline.map((e) => `- ${new Date(e.at).toISOString()} — ${e.what}`).join("\n");
112
+ return {
113
+ kind: "filed",
114
+ issue: await opts.tracker.create(opts.repo, {
115
+ title,
116
+ body: `${report}\n\n## Timeline\n\n${timeline}`,
117
+ labels: [label],
118
+ }),
119
+ };
120
+ }
@@ -93,6 +93,11 @@ export type RunNoteKind =
93
93
  * note says that loop's settlement instead: the executor waited for the
94
94
  * wake and the run went on. */
95
95
  | "sandbox_restarted"
96
+ /** The run was admitted onto a drained fleet (docs/reference/specs/resident-repos.md
97
+ * item 69) and waited at its attach for the deploy to finish: the summary
98
+ * names the wait. Published by the dispatcher after the attach, so `runs
99
+ * friction` reads the drain as the wait's category instead of "none". */
100
+ | "drain_wait"
96
101
  | "stop_requested"
97
102
  | "stopped"
98
103
  /** A stop the loop or turn had to ask pi for again reached it: the series a
@@ -317,6 +322,7 @@ export const RUN_NOTE_KINDS = [
317
322
  "sandbox_dead",
318
323
  "fleet_busy",
319
324
  "sandbox_restarted",
325
+ "drain_wait",
320
326
  "stop_requested",
321
327
  "stopped",
322
328
  "stop_landed",
@@ -537,15 +543,16 @@ export type RouteInputObject3 = { readonly [key: string]: RouteInputLeafOrList |
537
543
  export type RouteInputValue = RouteInputLeafOrList | RouteInputObject3;
538
544
 
539
545
  /** How a door decision about a state change ended (docs/decisions/0044-a-routed-write-is-confirmed-in-proportion-to-its-blast-radius.md):
540
- * `hand_back` — the router bound a state-changing command and the door answered
541
- * the line to paste, nothing invoked; `offered` — the same decision on a
546
+ * `hand_back` — the door held a state-changing command and ran nothing: a
547
+ * typed surface's refusal naming the typed form, or a chat surface's refusal
548
+ * naming why the click could not mint; `offered` — the same decision on a
542
549
  * channel that can show a confirmation: the line, its risk and a button, a
543
550
  * row stored in the config object, nothing invoked; `confirmed` — the stored
544
- * input ran at the click, as the requester, through the typed line's path;
545
- * `pasted` — the typed line that followed a hand-back in its thread, with the
546
- * same receipt, ran. A routed read carries no outcome: it is not a decision
547
- * about a state change. */
548
- export type RouteOutcome = "hand_back" | "offered" | "confirmed" | "pasted" | "refused";
551
+ * input ran at the click, as the requester, through the typed line's path.
552
+ * A routed read carries no outcome: it is not a decision about a state
553
+ * change. (A record from before the hand-back retired from chat may carry
554
+ * `pasted`; nothing writes or reads it now.) */
555
+ export type RouteOutcome = "hand_back" | "offered" | "confirmed" | "refused";
549
556
 
550
557
  export type RunEvent =
551
558
  /** `callId` is the provider's tool_use id — the explicit pair key between a
@@ -895,7 +902,17 @@ export type RunEvent =
895
902
  * (`by: "push"`), or the budget-end salvage pushed (`by: "salvage"`),
896
903
  * whether or not a pull request follows — the fact renewal reads. Published
897
904
  * straight to the registry like `pr_opened`. Additive: unknown → ignored. */
898
- | { type: "pushed_head"; ref: string; sha: string; by: "push" | "salvage"; seq?: number; at?: number }
905
+ | {
906
+ type: "pushed_head";
907
+ ref: string;
908
+ sha: string;
909
+ by: "push" | "salvage";
910
+ /** No uncommitted or unpushed work at the push (record 0064): the fact
911
+ * the plane's soft stop reads. Absent where the measure was missing. */
912
+ clean?: boolean;
913
+ seq?: number;
914
+ at?: number;
915
+ }
899
916
  /** The coordinator tag as a fact of the run (docs/reference/specs/run-history.md
900
917
  * item 48a): the instance the run is a child of, the unit its idempotency
901
918
  * key named, and the base branch its pull request targets — published by
@@ -1014,11 +1031,10 @@ export type RunEvent =
1014
1031
  * how the invoke ended is the run's own status and `answer`. A door
1015
1032
  * decision about a state change (record 0044) is a command run too, with
1016
1033
  * `outcome` saying which: a hand-back invoked nothing and its `answer` is
1017
- * the line to paste; an offer invoked nothing and its `answer` is the offer
1018
- * as the channel shows it; a confirmation is the stored input run at the
1019
- * click, its `answer` the command's own text as a routed read's is; a paste is
1020
- * the typed line that followed a hand-back, whatever its command,
1021
- * `handBackRunId` naming the hand-back's record. */
1034
+ * the refusal the surface was shown (the typed form on a typed surface);
1035
+ * an offer invoked nothing and its `answer` is the offer as the channel
1036
+ * shows it; a confirmation is the stored input run at the click, its
1037
+ * `answer` the command's own text as a routed read's is. */
1022
1038
  | {
1023
1039
  type: "route";
1024
1040
  preset: string;
@@ -1038,7 +1054,6 @@ export type RunEvent =
1038
1054
  * (record 0054): a refusal after a command was bound is a run
1039
1055
  * record, and the door report counts it by cause and code. */
1040
1056
  refusalCode?: string;
1041
- handBackRunId?: string;
1042
1057
  seq?: number;
1043
1058
  at?: number;
1044
1059
  }
@@ -1060,6 +1075,11 @@ export type RunEvent =
1060
1075
  mode: "shadow" | "on";
1061
1076
  outcome: "binds" | "question" | "refusal" | "non_decision";
1062
1077
  reason: string;
1078
+ /** The decision was the loop's floor (record 0069, as amended): a turn
1079
+ * that ended with no tool call, or the bounded re-asks ran out — the
1080
+ * readers' route ran the person's own request, and the event never
1081
+ * re-enters the loop. Additive: unknown → ignored. */
1082
+ floored?: true;
1063
1083
  /** A bind marked `confirmed` is a pending question's confirmed proposal
1064
1084
  * (`bindFromAnswer`): the line itself carries the task — the person's
1065
1085
  * message was the word "yes" — so a confirmed preset line routes its
@@ -1069,6 +1089,13 @@ export type RunEvent =
1069
1089
  /** A question's proposed line, redacted and cut like the receipt — what
1070
1090
  * the next turn's "yes" binds (`bindFromAnswer`). */
1071
1091
  proposal?: string;
1092
+ /** A question's original ask, redacted and capped: the request the
1093
+ * question interrupted, kept so the person's next words in the thread
1094
+ * join back onto it (`joinedAnswerRequest` —
1095
+ * `<request> — <question>: <answer>`) and bind as the request would
1096
+ * have been, never routed as a bare fragment. Set on `question`
1097
+ * outcomes alone. Additive: unknown → ignored. */
1098
+ request?: string;
1072
1099
  refusalCause?: string;
1073
1100
  refusalText?: string;
1074
1101
  attempts?: ReadonlyArray<{ outcome: "accepted" | "violation"; violation?: string }>;
@@ -39,6 +39,7 @@ export type FrictionCategory =
39
39
  | "wrap_up"
40
40
  | "budget_hit"
41
41
  | "infra_failure"
42
+ | "drain_wait"
42
43
  | "unkept_promise";
43
44
 
44
45
  export const FRICTION_CATEGORIES: readonly FrictionCategory[] = [
@@ -50,6 +51,7 @@ export const FRICTION_CATEGORIES: readonly FrictionCategory[] = [
50
51
  "wrap_up",
51
52
  "budget_hit",
52
53
  "infra_failure",
54
+ "drain_wait",
53
55
  "unkept_promise",
54
56
  ];
55
57
 
@@ -63,6 +65,7 @@ export const CATEGORY_LABEL: Record<FrictionCategory, string> = {
63
65
  wrap_up: "agent wind-down",
64
66
  budget_hit: "budget hits",
65
67
  infra_failure: "infra failures",
68
+ drain_wait: "the fleet drain",
66
69
  unkept_promise: "unkept promises",
67
70
  };
68
71
 
@@ -78,6 +81,7 @@ export const DENOMINATOR_OF: Record<FrictionCategory, "tool" | "run"> = {
78
81
  wrap_up: "run",
79
82
  budget_hit: "run",
80
83
  infra_failure: "run",
84
+ drain_wait: "run",
81
85
  unkept_promise: "run",
82
86
  };
83
87
 
@@ -574,6 +578,18 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
574
578
  eventIndex: index,
575
579
  });
576
580
  return;
581
+ case "drain_wait":
582
+ // The run was admitted onto a drained fleet (resident-repos item 69;
583
+ // issue 2044): the wait is the deploy's, its own category — a verdict
584
+ // of "none" over a run that sat half an hour at the drain was the
585
+ // incident's shape. No extent: the wait precedes the loop's spans.
586
+ findings.push({
587
+ category: "drain_wait",
588
+ severity: "medium",
589
+ summary: `fleet drained: ${ev.summary}`,
590
+ eventIndex: index,
591
+ });
592
+ return;
577
593
  case "sandbox_restarted":
578
594
  // The container rolled under the run: on pi the run ends here and its
579
595
  // request starts over (harness-pi item 16); on the deleted native loop
@@ -79,6 +79,12 @@ export interface LiveRunMeta {
79
79
  /** The run that spawned this one (item 46), so a reclaimed child's record
80
80
  * still names its parent. Absent on every run a person or a schedule started. */
81
81
  parentRunId?: string;
82
+ /** The run this claim restarts (record 0064; run-history item 54): set by
83
+ * the restart-from-request dispatch, so the plane knows the claim continues
84
+ * a run it saw end `restarting` — the object sends the waiting parent
85
+ * `child-resumed-<runId>` from it, and a `restartOf` ask passes the windows
86
+ * and the memory line. Absent on every fresh request. */
87
+ restartOf?: string;
82
88
  /** The coordinator instance this run is a child of, and the key its spawn
83
89
  * carried (item 48) — stored at the claim, so a reclaimed child's record
84
90
  * still sends the parent its event and a retried spawn finds its run. */
@@ -150,10 +156,13 @@ export interface LiveRunRow {
150
156
  state: RunState;
151
157
  }
152
158
 
153
- /** One tool call the step dispatched; `tool` decides how a resume settles it. */
159
+ /** One tool call the step dispatched; `tool` decides how a resume settles it.
160
+ * `boundMs` is the bound the call declared (a bash `timeout`), when it stated
161
+ * one — what the plane judges a `long_call` steer against (record 0064). */
154
162
  export interface InFlightCall {
155
163
  callId: string;
156
164
  tool: string;
165
+ boundMs?: number;
157
166
  }
158
167
 
159
168
  /** Written BEFORE a step's tools run (after its transcript turns landed). */
@@ -286,7 +295,7 @@ export type AppendableEvent = RunEvent & { seq: number };
286
295
  export interface IntakeReceipt {
287
296
  verdict: "addressed" | "silent";
288
297
  reason: string;
289
- source: "model" | "mode" | "error" | "timeout";
298
+ source: "model" | "mode" | "question" | "error" | "timeout";
290
299
  /** The structured seam's attempts (docs/decisions/0067): what each answer
291
300
  * violated, or that it was accepted; absent when no model was asked. */
292
301
  attempts?: ReadonlyArray<{ outcome: "accepted" | "violation"; violation?: string }>;
@@ -322,7 +331,11 @@ export function isIntakeReceipt(v: unknown): v is IntakeReceipt {
322
331
  return (
323
332
  (r.verdict === "addressed" || r.verdict === "silent") &&
324
333
  typeof r.reason === "string" &&
325
- (r.source === "model" || r.source === "mode" || r.source === "error" || r.source === "timeout") &&
334
+ (r.source === "model" ||
335
+ r.source === "mode" ||
336
+ r.source === "question" ||
337
+ r.source === "error" ||
338
+ r.source === "timeout") &&
326
339
  (r.mode === "mention" || r.mode === "classify") &&
327
340
  typeof r.model === "string" &&
328
341
  typeof r.gen === "number" &&
@@ -225,6 +225,12 @@ export interface RunRecord {
225
225
  * carried onto the record at the seal, so a history reader draws the run as
226
226
  * a pipeline. Absent on every other run and on older records. */
227
227
  hosted?: true;
228
+ /** The reattach path restarted this run from its request when its workspace
229
+ * could not be re-attached (record 0064; item 47a): the `interrupted` close
230
+ * is not the run's end — the same id carries on — so a waiting parent keeps
231
+ * waiting for `child_resumed` instead of ending its unit on this record.
232
+ * Absent on every ending that is final. */
233
+ restarting?: true;
228
234
  /** Where the run's conversation started (item 52): `channel` — its own
229
235
  * thread's history, as for every run a person, a schedule or a coordinator
230
236
  * started — or `parent` — a spawned child seeded from its parent's text
@@ -432,6 +438,9 @@ export interface RunOperatorDecision {
432
438
  question?: string;
433
439
  /** A question's proposed line — what the next turn's "yes" binds. */
434
440
  proposal?: string;
441
+ /** A question's original ask — what the person's next words join back onto
442
+ * (`joinedAnswerRequest`). */
443
+ request?: string;
435
444
  refusalCause?: string;
436
445
  refusalText?: string;
437
446
  /** The structured seam's attempts (record 0067): what each answer violated,
@@ -456,6 +465,7 @@ export function operatorOfEvents(events: readonly RunEvent[]): RunOperatorDecisi
456
465
  ...(e.binds ? { binds: e.binds.map((b) => ({ line: b.line, reason: b.reason })) } : {}),
457
466
  ...(e.question !== undefined ? { question: e.question } : {}),
458
467
  ...(e.proposal !== undefined ? { proposal: e.proposal } : {}),
468
+ ...(e.request !== undefined ? { request: e.request } : {}),
459
469
  ...(e.refusalCause !== undefined ? { refusalCause: e.refusalCause } : {}),
460
470
  ...(e.refusalText !== undefined ? { refusalText: e.refusalText } : {}),
461
471
  ...(e.attempts
@@ -1017,6 +1027,8 @@ export function isRunRecord(v: unknown): v is RunRecord {
1017
1027
  // marker, like `provisional`, is the literal `true` or absent.
1018
1028
  if (r.pipeline !== undefined && !isPipelineSummaryShape(r.pipeline)) return false;
1019
1029
  if (r.hosted !== undefined && r.hosted !== true) return false;
1030
+ // A restarting close carries the literal `true` or nothing (record 0064).
1031
+ if (r.restarting !== undefined && r.restarting !== true) return false;
1020
1032
  if (typeof r.channelId !== "string" || typeof r.userId !== "string" || typeof r.threadKey !== "string") return false;
1021
1033
  if (r.relayedBy !== undefined && typeof r.relayedBy !== "string") return false;
1022
1034
  if (r.authenticatedAs !== undefined && typeof r.authenticatedAs !== "string") return false;
@@ -461,6 +461,19 @@ export const GATE_RECEIPTS =
461
461
  "Paste each command's exit line into the PR description's validation table as the row's proof; a gate you " +
462
462
  "could not run goes under the handoff's unproven list and is never claimed clean.";
463
463
 
464
+ /** The rebase before every push lives in the coding preset's own instructions
465
+ * (`REBASE_BEFORE_PUSH`, src/agents/registry.ts — agent-coding item 13;
466
+ * record 0071 mechanism one): the base fetched, the branch rebased onto it,
467
+ * the fast gates re-run on the rebased tree, then the push — always, never
468
+ * configurable. The contract used to carry the conditional form ("rebase once
469
+ * more if it moved while you worked"); now the first instruction points at
470
+ * the preset's paragraph, the same shape as the fast-gates pointer above, so
471
+ * the child reads the rule once and cannot read a softer one here. */
472
+ export const REBASE_POINTER =
473
+ "The rebase before every push is your preset instructions' rule (REBASE BEFORE EVERY PUSH): it holds before " +
474
+ "each push here — always, not configurable — so every head you push is current with its base and the pull " +
475
+ "request is never born conflicting.";
476
+
464
477
  function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
465
478
  const branch = rebase.branch ? `\`${rebase.branch}\`` : "the unit's branch";
466
479
  const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
@@ -470,8 +483,7 @@ function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
470
483
  `the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop. ` +
471
484
  `Push the branch as soon as the change exists and the fast gates pass — the project's full verification ` +
472
485
  `is CI's gate, run there after the push with any fix as a further commit; an unpushed tree does not ` +
473
- `survive the run's end. ${gates} Right before each push, fetch ${onto} again and rebase ` +
474
- `once more if it moved while you worked, so the pull request is not born conflicting. At the wind-down note, ` +
486
+ `survive the run's end. ${gates} ${REBASE_POINTER} At the wind-down note, ` +
475
487
  `commit and push what compiles, say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS}`
476
488
  );
477
489
  }