@coreplane/switchboard 1.255.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.
- package/dist/assets/config/config.example.yaml +14 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +340 -7
- package/dist/assets/deploy/cloudflare-resident/drain.ts +100 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +227 -34
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +5 -3
- package/dist/assets/project.json +17 -9
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +55 -1
- package/dist/assets/src/core/authz/grants.ts +12 -0
- package/dist/assets/src/core/authz/policy.ts +15 -0
- package/dist/assets/src/core/budgets.ts +51 -17
- package/dist/assets/src/core/coordinator/contract.ts +45 -5
- package/dist/assets/src/core/coordinator/driver.ts +17 -4
- package/dist/assets/src/core/delivery.ts +8 -5
- package/dist/assets/src/core/pipelineStanding.ts +57 -0
- package/dist/assets/src/core/plane/decide.ts +468 -12
- package/dist/assets/src/core/plane/findings.ts +120 -0
- package/dist/assets/src/core/reviewVerdict.ts +49 -0
- package/dist/assets/src/core/runEvents.ts +41 -14
- package/dist/assets/src/core/runFriction.ts +16 -0
- package/dist/assets/src/core/runLedger/types.ts +16 -3
- package/dist/assets/src/core/runRecord.ts +12 -0
- package/dist/assets/src/core/ship/contract.ts +34 -32
- package/dist/assets/src/core/ship/coordinator.ts +291 -95
- package/dist/assets/src/core/ship/renewal.ts +19 -17
- package/dist/assets/src/core/trace/attrs.ts +1 -1
- package/dist/assets/src/core/untrusted.ts +35 -0
- package/dist/assets/src/execution/residentCredentials.ts +7 -4
- package/dist/assets/web/dist/.vite/manifest.json +82 -57
- package/dist/assets/web/dist/assets/HomePage-DJqJQzrL.js +1 -0
- package/dist/assets/web/dist/assets/{PendingTurnRow-DGINv9XT.js → PendingTurnRow-fhKqKUVv.js} +1 -1
- package/dist/assets/web/dist/assets/PlanePage-Bood0hNO.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-D_RD6wLo.js → ResidentDetailPage-DOe1HtUC.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-BLkSuCxo.js → ResidentsIndexPage-TyCGjXM4.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-C_4t-sit.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-BNepPMRX.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-jZnMw1mM.js +1 -0
- package/dist/assets/web/dist/assets/{ScheduledPage-3aYsDf-q.js → ScheduledPage-BkNoudDP.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-DG-p5Xy1.js → SettingsPage-COvWGXl-.js} +1 -1
- package/dist/assets/web/dist/assets/SilentTurn-DNAANKgN.js +2 -0
- package/dist/assets/web/dist/assets/SlackMark-CtiHrNjr.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-CEnGlyAL.js → StatusDot-BgmkSV0S.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-CiunVowT.js → Tooltip-DEUPuCPW.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DNhdzxvY.js +1 -0
- package/dist/assets/web/dist/assets/{dist-luhv3YSo.js → dist-CF3jz9LM.js} +1 -1
- package/dist/assets/web/dist/assets/durationTone-CVpX_yIk.js +1 -0
- package/dist/assets/web/dist/assets/main-7SAujY_s.js +28 -0
- package/dist/assets/web/dist/assets/main-DGC6WFqS.css +1 -0
- package/dist/assets/web/dist/assets/{sseReplay-DE6wv1Ua.js → sseReplay-BRJEIh83.js} +1 -1
- package/dist/cli.js +2941 -1116
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/HomePage-Be7jLLnU.js +0 -3
- package/dist/assets/web/dist/assets/PlanePage-JEj-lqgz.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CfuZqf_O.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-CmWYGR36.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-DW-HHuZa.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-iOYnmTcQ.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-U_rOqo6r.js +0 -1
- package/dist/assets/web/dist/assets/main-C4QfwybO.css +0 -1
- package/dist/assets/web/dist/assets/main-mAKx_zo9.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
|
+
}
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
import { normalizeHead } from "./reviewedHead.js";
|
|
35
35
|
import { redactSecrets } from "./redact.js";
|
|
36
|
+
import { shows, type Verbosity } from "./verbosity.js";
|
|
36
37
|
|
|
37
38
|
export type ReviewVerdictKind = "approve" | "request_changes";
|
|
38
39
|
|
|
@@ -248,6 +249,37 @@ export function verdictLine(verdict: ReviewVerdict | undefined): string {
|
|
|
248
249
|
return summary ? `${token} ${summary}` : token;
|
|
249
250
|
}
|
|
250
251
|
|
|
252
|
+
/** The counted plural for a severity: `blocker` and `nit` inflect, `major`
|
|
253
|
+
* and `minor` read as adjectives and stay uninflected. */
|
|
254
|
+
function severityCount(sev: FindingSeverity, n: number): string {
|
|
255
|
+
if (sev === "blocking") return `${n} blocker${n === 1 ? "" : "s"}`;
|
|
256
|
+
if (sev === "nit") return `${n} nit${n === 1 ? "" : "s"}`;
|
|
257
|
+
return `${n} ${sev}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The verdict as ONE line in the user's words (record 0066): `LGTM` for an
|
|
261
|
+
* approve, `Changes requested: 2 blockers, 1 major, 2 minor, 3 nits` — only
|
|
262
|
+
* the non-zero counts, most severe first — for a request for changes, the
|
|
263
|
+
* bare token word when the findings were not itemized or none were filed.
|
|
264
|
+
* The quiet thread reply prints this and nothing more; the full verdict line
|
|
265
|
+
* and the finding bullets are `verbose` material and stay on the pull
|
|
266
|
+
* request, where the post-step put them. */
|
|
267
|
+
export function verdictCountsLine(verdict: ReviewVerdict): string {
|
|
268
|
+
if (verdict.verdict === "approve") return "LGTM";
|
|
269
|
+
const counts = severityCounts(verdict.findings ?? []);
|
|
270
|
+
return counts ? `Changes requested: ${counts}` : "Changes requested";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The non-zero severity counts of a finding list, most severe first —
|
|
274
|
+
* `2 blockers, 1 major, 2 minor, 3 nits` — or the empty string for none.
|
|
275
|
+
* Shared by the quiet verdict line and the quiet round-cap report. */
|
|
276
|
+
export function severityCounts(findings: readonly Finding[]): string {
|
|
277
|
+
return FINDING_SEVERITIES.map((sev) => [sev, findings.filter((f) => f.severity === sev).length] as const)
|
|
278
|
+
.filter(([, n]) => n > 0)
|
|
279
|
+
.map(([sev, n]) => severityCount(sev, n))
|
|
280
|
+
.join(", ");
|
|
281
|
+
}
|
|
282
|
+
|
|
251
283
|
/** One compact finding line — `[severity] id file[:line] — title` — shared by
|
|
252
284
|
* the posted body's list (bulleted below) and ship's synthesized child turns. */
|
|
253
285
|
export function formatFinding(f: Finding): string {
|
|
@@ -381,6 +413,13 @@ export function buildReviewPostBody(
|
|
|
381
413
|
* opt-out, a guard refusal) or findings not itemized (a list that says
|
|
382
414
|
* nothing is no substitute) — so the review's text is always somewhere a
|
|
383
415
|
* person reads it. No verdict → the bare answer with the link, as before.
|
|
416
|
+
*
|
|
417
|
+
* The request's verbosity decides how much of the verdict the thread hears
|
|
418
|
+
* (routing-and-config item 28, record 0066): below `verbose`, a verdict whose
|
|
419
|
+
* findings are itemized and posted to GitHub is ONE line — `verdictCountsLine`
|
|
420
|
+
* with the pull request link — because the full verdict, the finding lines and
|
|
421
|
+
* the prose already stand on the pull request; at `verbose`, and whenever the
|
|
422
|
+
* text would otherwise land nowhere a person reads it, the full render above.
|
|
384
423
|
*/
|
|
385
424
|
export function buildReviewChannelReply(input: {
|
|
386
425
|
answer: string;
|
|
@@ -388,8 +427,18 @@ export function buildReviewChannelReply(input: {
|
|
|
388
427
|
/** The PR the post landed on, or undefined when nothing was posted. */
|
|
389
428
|
posted: { repo: string; number: number } | undefined;
|
|
390
429
|
liveUrl: string | undefined;
|
|
430
|
+
/** The request's level (default `verbose`: the full render, the row's shape). */
|
|
431
|
+
verbosity?: Verbosity;
|
|
391
432
|
}): string {
|
|
392
433
|
const { answer, verdict, posted, liveUrl } = input;
|
|
434
|
+
if (
|
|
435
|
+
!shows(input.verbosity ?? "verbose", "verbose") &&
|
|
436
|
+
verdict !== undefined &&
|
|
437
|
+
verdict.findings !== undefined &&
|
|
438
|
+
posted !== undefined
|
|
439
|
+
) {
|
|
440
|
+
return `${verdictCountsLine(verdict)} — https://github.com/${posted.repo}/pull/${posted.number}`;
|
|
441
|
+
}
|
|
393
442
|
const tail = [
|
|
394
443
|
...(posted && verdict ? [`Posted to ${posted.repo}#${posted.number}`] : []),
|
|
395
444
|
...(liveUrl ? [`[Live run](${liveUrl})`] : []),
|
|
@@ -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
|
|
541
|
-
*
|
|
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
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
export type RouteOutcome = "hand_back" | "offered" | "confirmed" | "
|
|
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
|
-
| {
|
|
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
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1020
|
-
* the
|
|
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" ||
|
|
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;
|
|
@@ -439,49 +439,51 @@ export const TIMEOUT_ON_LONG_COMMANDS =
|
|
|
439
439
|
"State a timeout on any command you expect to run longer than a minute: a timeout that reaches past the loop's " +
|
|
440
440
|
"end is refused before the command runs, never cut midway.";
|
|
441
441
|
|
|
442
|
-
/** The fast gates a plan child runs before every push
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
export const FAST_GATES_BEFORE_PUSH =
|
|
462
|
-
"The fast gates, before every push — each scoped to the changed set, never the whole project: " +
|
|
463
|
-
`${TOUCHED_TESTS_COMMAND} once (never \`--changed\`, never a directory: on a moving base that is most of the suite), ` +
|
|
464
|
-
"`tsc --noEmit -p` the touched tsconfig under `NODE_OPTIONS=--max-old-space-size=6144`, " +
|
|
465
|
-
"`npx prettier --check` on the changed files, `npm run hygiene:check` and `npm run specs:check` — " +
|
|
466
|
-
"then your judgement on what else this change needs, not a longer checklist. Every CI pipeline runs the " +
|
|
467
|
-
"tests, the types, the formatting and the full verification on your push, so you never run them again: " +
|
|
468
|
-
"you validate and fix your own change before pushing, at the changed-set scope. Passing the full test suite " +
|
|
469
|
-
"and the full typecheck is NOT part of your criteria: CI is that gate and the only place they run — on a " +
|
|
470
|
-
"shared resident they cost minutes that every other run pays for. " +
|
|
442
|
+
/** The fast gates a plan child runs before every push live in the coding
|
|
443
|
+
* preset's own instructions (`FAST_GATES_BEFORE_PUSH`,
|
|
444
|
+
* src/agents/registry.ts — agent-coding item 13; issue 1796): the changed-set
|
|
445
|
+
* forms with their commands named, the full verification named as CI's gate.
|
|
446
|
+
* The contract used to restate the whole paragraph, so every ask that was not
|
|
447
|
+
* a plan unit had to repeat it by hand; now the first instruction points at
|
|
448
|
+
* the preset's paragraph — the child reads the sentence once — and keeps only
|
|
449
|
+
* what is the contract's own: the receipts. Each gate is a receipt — the exit
|
|
450
|
+
* line goes into the PR description's validation table as the row's proof
|
|
451
|
+
* (the handoff has no verified list; parseHandoff carries deviations,
|
|
452
|
+
* followUps, unproven and landed), and a gate the child could not run goes
|
|
453
|
+
* under the handoff's unproven list, never claimed clean — because only a
|
|
454
|
+
* plan child has a handoff to route them to. */
|
|
455
|
+
export const FAST_GATES_POINTER =
|
|
456
|
+
"The fast gates are the ones your preset instructions name (THE FAST GATES): the changed-set forms, " +
|
|
457
|
+
"never the whole project — the full suite, the full typecheck and the full verification are CI's, " +
|
|
458
|
+
"never yours to run.";
|
|
459
|
+
|
|
460
|
+
export const GATE_RECEIPTS =
|
|
471
461
|
"Paste each command's exit line into the PR description's validation table as the row's proof; a gate you " +
|
|
472
462
|
"could not run goes under the handoff's unproven list and is never claimed clean.";
|
|
473
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
|
+
|
|
474
477
|
function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
|
|
475
478
|
const branch = rebase.branch ? `\`${rebase.branch}\`` : "the unit's branch";
|
|
476
479
|
const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
|
|
477
|
-
const gates =
|
|
480
|
+
const gates = `${FAST_GATES_POINTER} ${GATE_RECEIPTS}`;
|
|
478
481
|
return (
|
|
479
482
|
`Rebase ${branch} onto ${onto} before any other work — the parent unit has merged and the base has moved; ` +
|
|
480
483
|
`the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop. ` +
|
|
481
484
|
`Push the branch as soon as the change exists and the fast gates pass — the project's full verification ` +
|
|
482
485
|
`is CI's gate, run there after the push with any fix as a further commit; an unpushed tree does not ` +
|
|
483
|
-
`survive the run's end. ${gates}
|
|
484
|
-
`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, ` +
|
|
485
487
|
`commit and push what compiles, say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS}`
|
|
486
488
|
);
|
|
487
489
|
}
|