@coreplane/switchboard 1.254.1 → 1.256.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/.dockerignore +3 -0
- package/dist/assets/Dockerfile +12 -1
- package/dist/assets/config/config.example.yaml +6 -1
- package/dist/assets/deploy/cloudflare/worker.ts +39 -23
- package/dist/assets/deploy/cloudflare-memory/worker.ts +552 -5
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +5 -0
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +13 -1
- package/dist/assets/deploy/cloudflare-resident/drain.ts +55 -1
- package/dist/assets/deploy/cloudflare-resident/levels.ts +84 -0
- package/dist/assets/deploy/cloudflare-resident/prepare-commit-msg +17 -0
- package/dist/assets/deploy/cloudflare-resident/worker.ts +358 -42
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -1
- package/dist/assets/deploy/cloudflare-sandbox/prepare-commit-msg +17 -0
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +21 -4
- package/dist/assets/deploy/hooks/prepare-commit-msg +17 -0
- package/dist/assets/deploy/secrets.manifest.json +12 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +52 -0
- package/dist/assets/src/core/budgets.ts +35 -2
- package/dist/assets/src/core/coordinator/contract.ts +6 -0
- package/dist/assets/src/core/coordinator/driver.ts +59 -6
- package/dist/assets/src/core/costs.ts +39 -16
- package/dist/assets/src/core/pipelineStanding.ts +62 -0
- package/dist/assets/src/core/plane/decide.ts +389 -22
- package/dist/assets/src/core/refusal.ts +3 -0
- package/dist/assets/src/core/reviewVerdict.ts +64 -2
- package/dist/assets/src/core/runEvents.ts +31 -12
- package/dist/assets/src/core/runLedger/sessionLog.ts +128 -0
- package/dist/assets/src/core/runLedger/types.ts +3 -0
- package/dist/assets/src/core/runRecord.ts +24 -7
- package/dist/assets/src/core/ship/contract.ts +20 -30
- package/dist/assets/src/core/ship/coordinator.ts +519 -126
- package/dist/assets/src/core/trace/workerTrace.ts +3 -0
- package/dist/assets/src/execution/sandboxErrors.ts +77 -6
- package/dist/assets/web/dist/.vite/manifest.json +61 -60
- package/dist/assets/web/dist/assets/CostsPage-BaeWnm-o.js +1 -0
- package/dist/assets/web/dist/assets/{DeliveryPage-ngPsO2to.js → DeliveryPage-BBAyLwPq.js} +1 -1
- package/dist/assets/web/dist/assets/{HomePage-DvxTHzPx.js → HomePage-BEiStFwW.js} +1 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-DGINv9XT.js +1 -0
- package/dist/assets/web/dist/assets/{PlanePage-DpWfiX4C.js → PlanePage-JEj-lqgz.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DG86v39Y.js → ResidentDetailPage-jYhEeeyu.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-x6p689VH.js → ResidentsIndexPage-DvFmlV4U.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-DvWzR1JQ.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-CvZ-TOT3.js +9 -0
- package/dist/assets/web/dist/assets/{RunsIndexPage-CuzFchcn.js → RunsIndexPage-JqVSDOok.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-BuLmfcbG.js → ScheduledPage-Viim-bus.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-BujWkdU_.js → SettingsPage-CYUy8McC.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-C8Bc0pTX.js → StatusDot-Dv6UMaPy.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-BWwJx27K.js → Tooltip-Bd-5Rypv.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-B81wEnKN.js +1 -0
- package/dist/assets/web/dist/assets/budgets-c1eumrqD.js +1 -0
- package/dist/assets/web/dist/assets/{dist-CpnyQGOb.js → dist-Nl3uaxrP.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-DborJPFp.js +1 -0
- package/dist/assets/web/dist/assets/{main-Comxmwi4.js → main-DRxWlffc.js} +2 -2
- package/dist/assets/web/dist/assets/{sseReplay-IzTdD4-3.js → sseReplay-DE6wv1Ua.js} +6 -6
- package/dist/cli.js +4400 -3079
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/CostsPage-BuKjw3nv.js +0 -1
- package/dist/assets/web/dist/assets/PendingTurnRow-ZYIRCCZ2.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-DG29LOTs.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-ysJBY8xQ.js +0 -9
- package/dist/assets/web/dist/assets/UnitRoutePage-B9kjA1AT.js +0 -1
- package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-DABQtONT.js +0 -1
|
@@ -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
|
|
|
@@ -108,6 +109,14 @@ export interface Finding {
|
|
|
108
109
|
line?: number;
|
|
109
110
|
/** One line naming the issue; the full explanation lives in the prose. */
|
|
110
111
|
title: string;
|
|
112
|
+
/** The finding's remedy is a receipt only a person can produce — a replay
|
|
113
|
+
* that needs a provider credential no sandbox holds, a procedure a person
|
|
114
|
+
* runs live — so no fix round can address it. Set by the reviewer beside the
|
|
115
|
+
* severity through `submit_verdict`; ship's coordinator reads this flag,
|
|
116
|
+
* never prose, and a round whose actionable findings all carry it ends
|
|
117
|
+
* `held` (docs/reference/specs/agent-ship.md item 9). Anything but the
|
|
118
|
+
* literal `true` is dropped and the finding stands as actionable. */
|
|
119
|
+
humanGated?: true;
|
|
111
120
|
/** Machine provenance: true only on a check finding the ship round's checks
|
|
112
121
|
* step itself appended (ship/coordinator.ts `checkFinding`) — never set from
|
|
113
122
|
* a reviewer's input, whatever id the reviewer chose. */
|
|
@@ -222,6 +231,9 @@ function parseFinding(
|
|
|
222
231
|
// dropping the whole finding over a bad line would also drop the severity
|
|
223
232
|
// that the approve→request_changes downgrade keys on.
|
|
224
233
|
if (typeof r.line === "number" && Number.isInteger(r.line) && r.line >= 1) finding.line = r.line;
|
|
234
|
+
// Fail-open on the flag alone: a malformed humanGated never drops the
|
|
235
|
+
// finding — it stands as actionable, which is the conservative reading.
|
|
236
|
+
if (r.humanGated === true) finding.humanGated = true;
|
|
225
237
|
return { finding };
|
|
226
238
|
}
|
|
227
239
|
|
|
@@ -237,11 +249,42 @@ export function verdictLine(verdict: ReviewVerdict | undefined): string {
|
|
|
237
249
|
return summary ? `${token} ${summary}` : token;
|
|
238
250
|
}
|
|
239
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
|
+
|
|
240
283
|
/** One compact finding line — `[severity] id file[:line] — title` — shared by
|
|
241
284
|
* the posted body's list (bulleted below) and ship's synthesized child turns. */
|
|
242
285
|
export function formatFinding(f: Finding): string {
|
|
243
286
|
const location = f.line !== undefined ? `${f.file}:${f.line}` : f.file;
|
|
244
|
-
return `[${f.severity}] ${f.id} ${location} — ${f.title}`;
|
|
287
|
+
return `[${f.severity}] ${f.id} ${location} — ${f.title}${f.humanGated ? " (human-gated)" : ""}`;
|
|
245
288
|
}
|
|
246
289
|
|
|
247
290
|
/** One compact disposition line — `id: fixed|declined[ — note]` — the coding
|
|
@@ -318,6 +361,7 @@ function verdictMarker(verdict: ReviewVerdict | undefined, target: ReviewBodyTar
|
|
|
318
361
|
severity: f.severity,
|
|
319
362
|
file: f.file,
|
|
320
363
|
...(f.line !== undefined ? { line: f.line } : {}),
|
|
364
|
+
...(f.humanGated ? { humanGated: true } : {}),
|
|
321
365
|
})),
|
|
322
366
|
}
|
|
323
367
|
: {}),
|
|
@@ -369,6 +413,13 @@ export function buildReviewPostBody(
|
|
|
369
413
|
* opt-out, a guard refusal) or findings not itemized (a list that says
|
|
370
414
|
* nothing is no substitute) — so the review's text is always somewhere a
|
|
371
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.
|
|
372
423
|
*/
|
|
373
424
|
export function buildReviewChannelReply(input: {
|
|
374
425
|
answer: string;
|
|
@@ -376,8 +427,18 @@ export function buildReviewChannelReply(input: {
|
|
|
376
427
|
/** The PR the post landed on, or undefined when nothing was posted. */
|
|
377
428
|
posted: { repo: string; number: number } | undefined;
|
|
378
429
|
liveUrl: string | undefined;
|
|
430
|
+
/** The request's level (default `verbose`: the full render, the row's shape). */
|
|
431
|
+
verbosity?: Verbosity;
|
|
379
432
|
}): string {
|
|
380
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
|
+
}
|
|
381
442
|
const tail = [
|
|
382
443
|
...(posted && verdict ? [`Posted to ${posted.repo}#${posted.number}`] : []),
|
|
383
444
|
...(liveUrl ? [`[Live run](${liveUrl})`] : []),
|
|
@@ -463,7 +524,8 @@ function isFindingShape(v: unknown): v is Finding {
|
|
|
463
524
|
(FINDING_SEVERITIES as readonly string[]).includes(v.severity as string) &&
|
|
464
525
|
typeof v.file === "string" &&
|
|
465
526
|
typeof v.title === "string" &&
|
|
466
|
-
(v.line === undefined || typeof v.line === "number")
|
|
527
|
+
(v.line === undefined || typeof v.line === "number") &&
|
|
528
|
+
(v.humanGated === undefined || v.humanGated === true)
|
|
467
529
|
);
|
|
468
530
|
}
|
|
469
531
|
|
|
@@ -418,6 +418,16 @@ export type ShipRoundOutcome =
|
|
|
418
418
|
* 0055): the failures become check findings and the findings step runs as
|
|
419
419
|
* for any changes-requested round. */
|
|
420
420
|
| "checks_failed"
|
|
421
|
+
/** The round's coding child died on a provider transient with nothing
|
|
422
|
+
* pushed (issue 1932): the first such boundary marks the round's one
|
|
423
|
+
* re-run, a second the `transient` ending. */
|
|
424
|
+
| "transient"
|
|
425
|
+
/** The merge door enqueued the pull request — the base takes changes only
|
|
426
|
+
* through a merge queue (issue 2011): the unit waits for the queue's outcome. */
|
|
427
|
+
| "enqueued"
|
|
428
|
+
/** The merge queue removed the pull request: the removal reason becomes a
|
|
429
|
+
* finding of the round, like a red check, and a fix round follows. */
|
|
430
|
+
| "dequeued"
|
|
421
431
|
| "aborted"
|
|
422
432
|
| "stopped"
|
|
423
433
|
/** The coding round ended at its lease with the unit unfinished and the row
|
|
@@ -1019,6 +1029,10 @@ export type RunEvent =
|
|
|
1019
1029
|
command?: string;
|
|
1020
1030
|
input?: { readonly [key: string]: RouteInputValue };
|
|
1021
1031
|
receipt?: string;
|
|
1032
|
+
/** The structured seam's attempts ([record 0067](../../docs/decisions/0067-one-seam-for-a-structured-answer-a-violation-is-re-asked-with-the-violation-named-and-the-callers-declared-floor-holds-never-a-refusal-shown-to-the-person.md)):
|
|
1033
|
+
* what each answer violated, or that it was accepted, so a flaky model
|
|
1034
|
+
* is legible on the record as re-asks, not as silent floors. */
|
|
1035
|
+
attempts?: ReadonlyArray<{ outcome: "accepted" | "violation"; violation?: string }>;
|
|
1022
1036
|
outcome?: RouteOutcome;
|
|
1023
1037
|
/** The refusal's code (src/core/refusal.ts) when `outcome` is `refused`
|
|
1024
1038
|
* (record 0054): a refusal after a command was bound is a run
|
|
@@ -1031,28 +1045,33 @@ export type RunEvent =
|
|
|
1031
1045
|
/** The operator's decision beside the routed request ([record 0057](../../docs/decisions/0057-the-operator-is-the-one-door-a-model-binds-every-chat-input-and-deterministic-code-authorizes-fences-and-executes.md);
|
|
1032
1046
|
* the one-door plan's operator unit; run-history item 60): one per admitted chat
|
|
1033
1047
|
* event under `routing.operator: shadow` or `on`, published beside the
|
|
1034
|
-
* `route` event. The decision is binds, a question
|
|
1035
|
-
*
|
|
1036
|
-
* the
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1048
|
+
* `route` event. The decision is binds, a question, a refusal or — the
|
|
1049
|
+
* structured seam's floor ([record 0067](../../docs/decisions/0067-one-seam-for-a-structured-answer-a-violation-is-re-asked-with-the-violation-named-and-the-callers-declared-floor-holds-never-a-refusal-shown-to-the-person.md)),
|
|
1050
|
+
* never the model's decision — `non_decision`: under `on` the dispatcher
|
|
1051
|
+
* falls back to the readers' route for that event, this event recorded on
|
|
1052
|
+
* the run that then runs; `attempts` lists what each answer violated or
|
|
1053
|
+
* that it was accepted. A bind's `line` is redacted and cut like the
|
|
1054
|
+
* receipt (`ROUTE_RECEIPT_CAP`), never the message text; `intake` carries
|
|
1055
|
+
* the intake gate's verdict when the gate is present; `latencyMs` and
|
|
1056
|
+
* `outputTokens` feed the replay's median rows. Under `shadow` nothing
|
|
1057
|
+
* runs from it. Additive: unknown → ignored. */
|
|
1039
1058
|
| {
|
|
1040
1059
|
type: "operator";
|
|
1041
1060
|
mode: "shadow" | "on";
|
|
1042
|
-
outcome: "binds" | "question" | "refusal";
|
|
1061
|
+
outcome: "binds" | "question" | "refusal" | "non_decision";
|
|
1043
1062
|
reason: string;
|
|
1044
|
-
|
|
1063
|
+
/** A bind marked `confirmed` is a pending question's confirmed proposal
|
|
1064
|
+
* (`bindFromAnswer`): the line itself carries the task — the person's
|
|
1065
|
+
* message was the word "yes" — so a confirmed preset line routes its
|
|
1066
|
+
* own tail as the request. Additive: unknown → a fresh bind. */
|
|
1067
|
+
binds?: ReadonlyArray<{ line: string; reason: string; confirmed?: true }>;
|
|
1045
1068
|
question?: string;
|
|
1046
1069
|
/** A question's proposed line, redacted and cut like the receipt — what
|
|
1047
1070
|
* the next turn's "yes" binds (`bindFromAnswer`). */
|
|
1048
1071
|
proposal?: string;
|
|
1049
1072
|
refusalCause?: string;
|
|
1050
1073
|
refusalText?: string;
|
|
1051
|
-
|
|
1052
|
-
* tool, a transport failure) — never the model's decision: under `on`
|
|
1053
|
-
* the dispatcher falls back to the readers' route for that event, this
|
|
1054
|
-
* event recorded on the run that then runs. */
|
|
1055
|
-
fallback?: true;
|
|
1074
|
+
attempts?: ReadonlyArray<{ outcome: "accepted" | "violation"; violation?: string }>;
|
|
1056
1075
|
intake?: { verdict: string; reason: string };
|
|
1057
1076
|
latencyMs?: number;
|
|
1058
1077
|
outputTokens?: number;
|
|
@@ -35,6 +35,134 @@ export function sessionKey(threadKey: string, agent: string | undefined): string
|
|
|
35
35
|
return `${threadKey}:${agent ?? "-"}`;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** The thread session's key (record 0057; the one-door plan's memory unit,
|
|
39
|
+
* item 13): one log per thread, read and written by the operator — every
|
|
40
|
+
* connector event the intake gate admits, every child's report, every
|
|
41
|
+
* question and answer is a turn in it. The `@thread` half can never collide
|
|
42
|
+
* with `sessionKey`'s agent half: no agent id starts with `@`. */
|
|
43
|
+
export function threadSessionKey(threadKey: string): string {
|
|
44
|
+
return `${threadKey}:@thread`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A working session's lane: a unit's coding rounds continue one log, its
|
|
48
|
+
* review rounds another (item 13). */
|
|
49
|
+
export type WorkingLane = "coding" | "review";
|
|
50
|
+
|
|
51
|
+
/** A working session's key `<instance>:<unit>:<lane>` (item 13). A re-issue of the
|
|
52
|
+
* same plan carries an attempt suffix on its instance id (`planInstanceId`:
|
|
53
|
+
* `plan-<id>-<attempt>`, attempt ≥ 2); the key strips it, so a re-issue
|
|
54
|
+
* continues the prior instance's lanes rather than starting cold. */
|
|
55
|
+
export function workingSessionKey(instance: { id: string; attempt?: number }, unit: string, lane: WorkingLane): string {
|
|
56
|
+
const suffix = instance.attempt !== undefined ? `-${instance.attempt}` : "";
|
|
57
|
+
const base =
|
|
58
|
+
suffix !== "" && instance.id.endsWith(suffix)
|
|
59
|
+
? instance.id.slice(0, instance.id.length - suffix.length)
|
|
60
|
+
: instance.id;
|
|
61
|
+
return `${base}:${unit}:${lane}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A connector turn's row id (item 13): the message id, with its edit timestamp
|
|
65
|
+
* when the message was edited — an edited message appends a second row (the
|
|
66
|
+
* first said what the person first said), a re-delivered unedited one
|
|
67
|
+
* appends nothing. */
|
|
68
|
+
export function connectorRowId(messageId: string, editedAt?: string | number): string {
|
|
69
|
+
return editedAt === undefined ? messageId : `${messageId}:edit-${editedAt}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The fold's row id for a hosted parent's `ship_unit` event (item 13): the
|
|
73
|
+
* event's own identity — its unit, its state and the registry seq it was
|
|
74
|
+
* published under — so the same event read twice folds one row. The seq is
|
|
75
|
+
* required (the registry stamps every published event with one): without it,
|
|
76
|
+
* a unit reopened at a second segment would repeat a state — two `started`
|
|
77
|
+
* events — and the second fold would be dropped as a duplicate. */
|
|
78
|
+
export function shipUnitRowId(event: { unit: string; state: string; seq: number }): string {
|
|
79
|
+
return `ship-unit:${event.unit}:${event.state}:${event.seq}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One thread-session row as its JSON is stored (item 13): a text turn with
|
|
83
|
+
* its author when a person wrote it, `silent` when the intake gate withheld
|
|
84
|
+
* the reply (the person's words are context all the same), `folded` when the
|
|
85
|
+
* row is a child's report folded whole — what `operatorTail` keeps ahead of
|
|
86
|
+
* older turns. */
|
|
87
|
+
export function storedTurnRow(turn: {
|
|
88
|
+
role: "user" | "assistant";
|
|
89
|
+
text: string;
|
|
90
|
+
actor?: string;
|
|
91
|
+
silent?: boolean;
|
|
92
|
+
folded?: boolean;
|
|
93
|
+
}): string {
|
|
94
|
+
return JSON.stringify({
|
|
95
|
+
role: turn.role,
|
|
96
|
+
part: { type: "text", text: turn.text },
|
|
97
|
+
...(turn.actor !== undefined ? { actor: turn.actor } : {}),
|
|
98
|
+
...(turn.silent === true ? { silent: true } : {}),
|
|
99
|
+
...(turn.folded === true ? { folded: true } : {}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Whether a stored row is a reply the intake gate withheld as silent (item 13). */
|
|
104
|
+
export function silentOfStoredRow(json: string): boolean {
|
|
105
|
+
const stored = parseStored(json);
|
|
106
|
+
return stored !== undefined && !("compaction" in stored) && (stored as { silent?: unknown }).silent === true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Whether a stored row is a folded report (item 13) — kept whole by the
|
|
110
|
+
* operator's cap ahead of older turns. */
|
|
111
|
+
export function foldedOfStoredRow(json: string): boolean {
|
|
112
|
+
const stored = parseStored(json);
|
|
113
|
+
return stored !== undefined && !("compaction" in stored) && (stored as { folded?: unknown }).folded === true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A run of an old `<thread>:<agent>` log, as the cutover migration reads it
|
|
117
|
+
* (item 13): its log's key, when it started and the row range its record closed. */
|
|
118
|
+
export interface MigrationRun {
|
|
119
|
+
key: string;
|
|
120
|
+
startedAt: number;
|
|
121
|
+
range: { from: number; to?: number };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The order the cutover migration reads an old thread's per-agent logs into
|
|
125
|
+
* the thread session (item 13): the runs by their start times, each run's rows in
|
|
126
|
+
* its session range in index order, and rows outside any run's range after
|
|
127
|
+
* the runs that precede them in their own log (before every run of that log
|
|
128
|
+
* when none does). The read is once: each row lands under the row id
|
|
129
|
+
* `migrationRowId(key, idx)`, so a replay appends nothing twice, and the old
|
|
130
|
+
* keys stay read-only for recall. */
|
|
131
|
+
export function migrationOrder(
|
|
132
|
+
runs: readonly MigrationRun[],
|
|
133
|
+
logs: readonly { key: string; rows: readonly number[] }[],
|
|
134
|
+
): Array<{ key: string; idx: number }> {
|
|
135
|
+
const byStart = [...runs].sort((a, b) => a.startedAt - b.startedAt);
|
|
136
|
+
// A row's place: inside a run's range it rides at that run's start (phase 0);
|
|
137
|
+
// past a run's closed range it rides after that run's rows (phase 1); before
|
|
138
|
+
// every run of its log it comes first of all (start -Infinity).
|
|
139
|
+
const placed = logs.flatMap((log, logOrder) =>
|
|
140
|
+
log.rows.map((idx) => {
|
|
141
|
+
let at = Number.NEGATIVE_INFINITY;
|
|
142
|
+
let phase = 1;
|
|
143
|
+
for (const r of byStart) {
|
|
144
|
+
if (r.key !== log.key) continue;
|
|
145
|
+
const to = r.range.to;
|
|
146
|
+
if (idx >= r.range.from && (to === undefined || idx <= to)) {
|
|
147
|
+
at = r.startedAt;
|
|
148
|
+
phase = 0;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
if (to !== undefined && to < idx && r.startedAt > at) at = r.startedAt;
|
|
152
|
+
}
|
|
153
|
+
return { key: log.key, idx, at, phase, logOrder };
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
placed.sort((a, b) => a.at - b.at || a.phase - b.phase || a.logOrder - b.logOrder || a.idx - b.idx);
|
|
157
|
+
return placed.map(({ key, idx }) => ({ key, idx }));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The row id a migrated row lands under (item 13): the old log's key and the
|
|
161
|
+
* row's index there — stable, so the read-once migration is idempotent. */
|
|
162
|
+
export function migrationRowId(key: string, idx: number): string {
|
|
163
|
+
return `migrated:${key}#${idx}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
38
166
|
/** The request is the seed's last user turn (`splitSeed` reads the seed the
|
|
39
167
|
* same way); a seed with no user turn — or no turns — puts its first row there. */
|
|
40
168
|
export function requestIndex(seed: readonly ChatMessage[]): number {
|
|
@@ -287,6 +287,9 @@ export interface IntakeReceipt {
|
|
|
287
287
|
verdict: "addressed" | "silent";
|
|
288
288
|
reason: string;
|
|
289
289
|
source: "model" | "mode" | "error" | "timeout";
|
|
290
|
+
/** The structured seam's attempts (docs/decisions/0067): what each answer
|
|
291
|
+
* violated, or that it was accepted; absent when no model was asked. */
|
|
292
|
+
attempts?: ReadonlyArray<{ outcome: "accepted" | "violation"; violation?: string }>;
|
|
290
293
|
mode: "mention" | "classify";
|
|
291
294
|
/** The `<provider>/<model>` ref the verdict ran on. */
|
|
292
295
|
model: string;
|
|
@@ -392,9 +392,10 @@ export function callsInFlight(events: readonly RunEvent[], status: RunStatus): C
|
|
|
392
392
|
return [...calls.values()].filter((c) => c.state === "cut" || (c.state === "open" && !orderly)).map((c) => c.call);
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
/** The router's decision as a record carries it — the
|
|
396
|
-
*
|
|
397
|
-
*
|
|
395
|
+
/** The router's decision as a record carries it — the fields the `route`
|
|
396
|
+
* event and the ledger row's `meta.route` carry (routing-and-config item 21),
|
|
397
|
+
* minus the event's `attempts` (record 0067): the record keeps the decision,
|
|
398
|
+
* the re-asks stay on the event. */
|
|
398
399
|
export interface RunRouteDecision {
|
|
399
400
|
preset: string;
|
|
400
401
|
reason: string;
|
|
@@ -425,7 +426,7 @@ export function routeOfEvents(events: readonly RunEvent[]): RunRouteDecision | u
|
|
|
425
426
|
* fields less the stream bookkeeping (record 0057; run-history item 60). */
|
|
426
427
|
export interface RunOperatorDecision {
|
|
427
428
|
mode: "shadow" | "on";
|
|
428
|
-
outcome: "binds" | "question" | "refusal";
|
|
429
|
+
outcome: "binds" | "question" | "refusal" | "non_decision";
|
|
429
430
|
reason: string;
|
|
430
431
|
binds?: { line: string; reason: string }[];
|
|
431
432
|
question?: string;
|
|
@@ -433,6 +434,9 @@ export interface RunOperatorDecision {
|
|
|
433
434
|
proposal?: string;
|
|
434
435
|
refusalCause?: string;
|
|
435
436
|
refusalText?: string;
|
|
437
|
+
/** The structured seam's attempts (record 0067): what each answer violated,
|
|
438
|
+
* or that it was accepted. */
|
|
439
|
+
attempts?: { outcome: "accepted" | "violation"; violation?: string }[];
|
|
436
440
|
intake?: { verdict: string; reason: string };
|
|
437
441
|
latencyMs?: number;
|
|
438
442
|
outputTokens?: number;
|
|
@@ -454,6 +458,14 @@ export function operatorOfEvents(events: readonly RunEvent[]): RunOperatorDecisi
|
|
|
454
458
|
...(e.proposal !== undefined ? { proposal: e.proposal } : {}),
|
|
455
459
|
...(e.refusalCause !== undefined ? { refusalCause: e.refusalCause } : {}),
|
|
456
460
|
...(e.refusalText !== undefined ? { refusalText: e.refusalText } : {}),
|
|
461
|
+
...(e.attempts
|
|
462
|
+
? {
|
|
463
|
+
attempts: e.attempts.map((a) => ({
|
|
464
|
+
outcome: a.outcome,
|
|
465
|
+
...(a.violation !== undefined ? { violation: a.violation } : {}),
|
|
466
|
+
})),
|
|
467
|
+
}
|
|
468
|
+
: {}),
|
|
457
469
|
...(e.intake ? { intake: { verdict: e.intake.verdict, reason: e.intake.reason } } : {}),
|
|
458
470
|
...(e.latencyMs !== undefined ? { latencyMs: e.latencyMs } : {}),
|
|
459
471
|
...(e.outputTokens !== undefined ? { outputTokens: e.outputTokens } : {}),
|
|
@@ -497,9 +509,14 @@ function isRunPullRequestShape(v: unknown): v is RunPullRequest {
|
|
|
497
509
|
* (item 57). `policy_refusal`: the model provider refused the run's call
|
|
498
510
|
* under its usage policy — the stop reason its wire names, never the
|
|
499
511
|
* explanation's words — so the session's next seed leaves the refused
|
|
500
|
-
* request out of its tail (docs/reference/specs/session-log.md item 9).
|
|
501
|
-
*
|
|
502
|
-
|
|
512
|
+
* request out of its tail (docs/reference/specs/session-log.md item 9).
|
|
513
|
+
* `provider_transient`: the run's model call failed on a provider transient
|
|
514
|
+
* (a gateway 5xx, a cut stream, a gateway timeout) with the harness's retry
|
|
515
|
+
* ladder spent — the ship runner reads it off the child's record to re-run a
|
|
516
|
+
* round-0 child that pushed nothing instead of aborting the unit
|
|
517
|
+
* (docs/reference/specs/agent-ship.md item 9, issue 1932). A failure without
|
|
518
|
+
* a name here leaves the record without the field. */
|
|
519
|
+
export const RUN_FAILURE_KINDS = ["policy_refusal", "provider_transient"] as const;
|
|
503
520
|
export type RunFailureKind = (typeof RUN_FAILURE_KINDS)[number];
|
|
504
521
|
export interface RunFailure {
|
|
505
522
|
kind: RunFailureKind;
|
|
@@ -439,42 +439,32 @@ 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
|
|
|
474
464
|
function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
|
|
475
465
|
const branch = rebase.branch ? `\`${rebase.branch}\`` : "the unit's branch";
|
|
476
466
|
const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
|
|
477
|
-
const gates =
|
|
467
|
+
const gates = `${FAST_GATES_POINTER} ${GATE_RECEIPTS}`;
|
|
478
468
|
return (
|
|
479
469
|
`Rebase ${branch} onto ${onto} before any other work — the parent unit has merged and the base has moved; ` +
|
|
480
470
|
`the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop. ` +
|