@coreplane/switchboard 1.256.0 → 1.258.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 (63) 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 +65 -2
  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 +66 -18
  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/pipelineStanding.ts +2 -0
  17. package/dist/assets/src/core/plane/decide.ts +477 -12
  18. package/dist/assets/src/core/plane/findings.ts +120 -0
  19. package/dist/assets/src/core/runEvents.ts +54 -14
  20. package/dist/assets/src/core/runFriction.ts +16 -0
  21. package/dist/assets/src/core/runLedger/types.ts +16 -3
  22. package/dist/assets/src/core/runRecord.ts +12 -0
  23. package/dist/assets/src/core/ship/contract.ts +14 -2
  24. package/dist/assets/src/core/ship/coordinator.ts +179 -51
  25. package/dist/assets/src/core/ship/renewal.ts +23 -20
  26. package/dist/assets/src/core/trace/attrs.ts +1 -1
  27. package/dist/assets/src/core/untrusted.ts +35 -0
  28. package/dist/assets/src/execution/residentCredentials.ts +7 -4
  29. package/dist/assets/web/dist/.vite/manifest.json +97 -72
  30. package/dist/assets/web/dist/assets/{DeliveryPage-BBAyLwPq.js → DeliveryPage-DadntjSp.js} +1 -1
  31. package/dist/assets/web/dist/assets/HomePage-BEEpTnVT.js +1 -0
  32. package/dist/assets/web/dist/assets/{PendingTurnRow-DGINv9XT.js → PendingTurnRow-DBPMLVsm.js} +1 -1
  33. package/dist/assets/web/dist/assets/PlanePage-DLusMGQ0.js +1 -0
  34. package/dist/assets/web/dist/assets/{ResidentDetailPage-jYhEeeyu.js → ResidentDetailPage-CPnHnrxy.js} +1 -1
  35. package/dist/assets/web/dist/assets/{ResidentsIndexPage-DvFmlV4U.js → ResidentsIndexPage-C9hASwHL.js} +1 -1
  36. package/dist/assets/web/dist/assets/RunFoldRow-OPe7Z0Cz.js +1 -0
  37. package/dist/assets/web/dist/assets/{RunRoutePage-CvZ-TOT3.js → RunRoutePage-o1xzkTGZ.js} +4 -4
  38. package/dist/assets/web/dist/assets/RunsIndexPage-0YAClYQh.js +1 -0
  39. package/dist/assets/web/dist/assets/{ScheduledPage-Viim-bus.js → ScheduledPage-BIdfC0V3.js} +1 -1
  40. package/dist/assets/web/dist/assets/{SettingsPage-CYUy8McC.js → SettingsPage-CnqWAjl3.js} +1 -1
  41. package/dist/assets/web/dist/assets/SilentTurn-DGNg0sIZ.js +2 -0
  42. package/dist/assets/web/dist/assets/SlackMark-CtiHrNjr.js +1 -0
  43. package/dist/assets/web/dist/assets/{StatusDot-Dv6UMaPy.js → StatusDot-D_D_iJS8.js} +1 -1
  44. package/dist/assets/web/dist/assets/{Tooltip-Bd-5Rypv.js → Tooltip-DVFmM7on.js} +1 -1
  45. package/dist/assets/web/dist/assets/UnitRoutePage-7fAXFoB_.js +1 -0
  46. package/dist/assets/web/dist/assets/budgets-BRLm6oID.js +1 -0
  47. package/dist/assets/web/dist/assets/{dist-Nl3uaxrP.js → dist-C1VX-_mm.js} +1 -1
  48. package/dist/assets/web/dist/assets/durationTone-CVpX_yIk.js +1 -0
  49. package/dist/assets/web/dist/assets/{indexRow-DborJPFp.js → indexRow-BO_GIdF6.js} +1 -1
  50. package/dist/assets/web/dist/assets/main-Cl7xHG34.js +28 -0
  51. package/dist/assets/web/dist/assets/main-DGC6WFqS.css +1 -0
  52. package/dist/assets/web/dist/assets/{sseReplay-DE6wv1Ua.js → sseReplay-BjlKmQsY.js} +1 -1
  53. package/dist/cli.js +3096 -1136
  54. package/package.json +1 -1
  55. package/dist/assets/web/dist/assets/HomePage-BEiStFwW.js +0 -3
  56. package/dist/assets/web/dist/assets/PlanePage-JEj-lqgz.js +0 -1
  57. package/dist/assets/web/dist/assets/RunFoldRow-DvWzR1JQ.js +0 -1
  58. package/dist/assets/web/dist/assets/RunsIndexPage-JqVSDOok.js +0 -1
  59. package/dist/assets/web/dist/assets/UnitRoutePage-B81wEnKN.js +0 -1
  60. package/dist/assets/web/dist/assets/budgets-c1eumrqD.js +0 -1
  61. package/dist/assets/web/dist/assets/durationTone-U_rOqo6r.js +0 -1
  62. package/dist/assets/web/dist/assets/main-C4QfwybO.css +0 -1
  63. 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
@@ -123,6 +128,13 @@ export type RunNoteKind =
123
128
  * (docs/reference/specs/run-history.md item 37); the summary says how many calls were
124
129
  * in flight at the kill and how each was settled. Published by the runner. */
125
130
  | "resumed"
131
+ /** The restart the run's `restarting` close promised never claimed the run
132
+ * (issue 2081): its dispatch died between the close and the successor's
133
+ * claim, so this interrupted close is the run's end, not a restart. The
134
+ * summary names how the dispatch ended. Published by the dispatcher over
135
+ * the closed record — a kind of its own, so the interruption's cause is
136
+ * still read off the roll's earlier words, never off the dispatch error. */
137
+ | "restart_died"
126
138
  /** A control was decided against the model card and is not native (record
127
139
  * 0052): a fallback (`applied` differs from `asked`, `vouched` true) or
128
140
  * an unvouched send (`vouched` false). One note per degraded control,
@@ -317,6 +329,7 @@ export const RUN_NOTE_KINDS = [
317
329
  "sandbox_dead",
318
330
  "fleet_busy",
319
331
  "sandbox_restarted",
332
+ "drain_wait",
320
333
  "stop_requested",
321
334
  "stopped",
322
335
  "stop_landed",
@@ -326,6 +339,7 @@ export const RUN_NOTE_KINDS = [
326
339
  "mcp_unavailable",
327
340
  "follow_up",
328
341
  "resumed",
342
+ "restart_died",
329
343
  "control_degraded",
330
344
  "seed",
331
345
  "redispatch",
@@ -430,6 +444,11 @@ export type ShipRoundOutcome =
430
444
  | "dequeued"
431
445
  | "aborted"
432
446
  | "stopped"
447
+ /** The round-0 coding child concluded its round with an answer instead of a
448
+ * pull request (issue 2086): its handoff records a deviation — a stop
449
+ * condition, a blocked precondition — so the unit ends held for the
450
+ * person's word and no renewal is judged. */
451
+ | "held"
433
452
  /** The coding round ended at its lease with the unit unfinished and the row
434
453
  * showing progress, and the grant renewed: the next segment opens (decision 0046). */
435
454
  | "continued"
@@ -537,15 +556,16 @@ export type RouteInputObject3 = { readonly [key: string]: RouteInputLeafOrList |
537
556
  export type RouteInputValue = RouteInputLeafOrList | RouteInputObject3;
538
557
 
539
558
  /** 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
559
+ * `hand_back` — the door held a state-changing command and ran nothing: a
560
+ * typed surface's refusal naming the typed form, or a chat surface's refusal
561
+ * naming why the click could not mint; `offered` — the same decision on a
542
562
  * channel that can show a confirmation: the line, its risk and a button, a
543
563
  * 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";
564
+ * input ran at the click, as the requester, through the typed line's path.
565
+ * A routed read carries no outcome: it is not a decision about a state
566
+ * change. (A record from before the hand-back retired from chat may carry
567
+ * `pasted`; nothing writes or reads it now.) */
568
+ export type RouteOutcome = "hand_back" | "offered" | "confirmed" | "refused";
549
569
 
550
570
  export type RunEvent =
551
571
  /** `callId` is the provider's tool_use id — the explicit pair key between a
@@ -895,7 +915,17 @@ export type RunEvent =
895
915
  * (`by: "push"`), or the budget-end salvage pushed (`by: "salvage"`),
896
916
  * whether or not a pull request follows — the fact renewal reads. Published
897
917
  * 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 }
918
+ | {
919
+ type: "pushed_head";
920
+ ref: string;
921
+ sha: string;
922
+ by: "push" | "salvage";
923
+ /** No uncommitted or unpushed work at the push (record 0064): the fact
924
+ * the plane's soft stop reads. Absent where the measure was missing. */
925
+ clean?: boolean;
926
+ seq?: number;
927
+ at?: number;
928
+ }
899
929
  /** The coordinator tag as a fact of the run (docs/reference/specs/run-history.md
900
930
  * item 48a): the instance the run is a child of, the unit its idempotency
901
931
  * key named, and the base branch its pull request targets — published by
@@ -1014,11 +1044,10 @@ export type RunEvent =
1014
1044
  * how the invoke ended is the run's own status and `answer`. A door
1015
1045
  * decision about a state change (record 0044) is a command run too, with
1016
1046
  * `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. */
1047
+ * the refusal the surface was shown (the typed form on a typed surface);
1048
+ * an offer invoked nothing and its `answer` is the offer as the channel
1049
+ * shows it; a confirmation is the stored input run at the click, its
1050
+ * `answer` the command's own text as a routed read's is. */
1022
1051
  | {
1023
1052
  type: "route";
1024
1053
  preset: string;
@@ -1038,7 +1067,6 @@ export type RunEvent =
1038
1067
  * (record 0054): a refusal after a command was bound is a run
1039
1068
  * record, and the door report counts it by cause and code. */
1040
1069
  refusalCode?: string;
1041
- handBackRunId?: string;
1042
1070
  seq?: number;
1043
1071
  at?: number;
1044
1072
  }
@@ -1060,6 +1088,11 @@ export type RunEvent =
1060
1088
  mode: "shadow" | "on";
1061
1089
  outcome: "binds" | "question" | "refusal" | "non_decision";
1062
1090
  reason: string;
1091
+ /** The decision was the loop's floor (record 0069, as amended): a turn
1092
+ * that ended with no tool call, or the bounded re-asks ran out — the
1093
+ * readers' route ran the person's own request, and the event never
1094
+ * re-enters the loop. Additive: unknown → ignored. */
1095
+ floored?: true;
1063
1096
  /** A bind marked `confirmed` is a pending question's confirmed proposal
1064
1097
  * (`bindFromAnswer`): the line itself carries the task — the person's
1065
1098
  * message was the word "yes" — so a confirmed preset line routes its
@@ -1069,6 +1102,13 @@ export type RunEvent =
1069
1102
  /** A question's proposed line, redacted and cut like the receipt — what
1070
1103
  * the next turn's "yes" binds (`bindFromAnswer`). */
1071
1104
  proposal?: string;
1105
+ /** A question's original ask, redacted and capped: the request the
1106
+ * question interrupted, kept so the person's next words in the thread
1107
+ * join back onto it (`joinedAnswerRequest` —
1108
+ * `<request> — <question>: <answer>`) and bind as the request would
1109
+ * have been, never routed as a bare fragment. Set on `question`
1110
+ * outcomes alone. Additive: unknown → ignored. */
1111
+ request?: string;
1072
1112
  refusalCause?: string;
1073
1113
  refusalText?: string;
1074
1114
  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
  }