@coreplane/switchboard 1.205.0 → 1.206.1
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/Dockerfile +6 -3
- package/dist/assets/config/config.example.yaml +24 -1
- package/dist/assets/deploy/cloudflare/coordinator.ts +86 -0
- package/dist/assets/deploy/cloudflare/shared.ts +9 -0
- package/dist/assets/deploy/cloudflare/worker.ts +142 -6
- package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +9 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +266 -18
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +15 -0
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +89 -3
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +81 -10
- package/dist/assets/deploy/secrets.manifest.json +1 -1
- 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 +58 -7
- package/dist/assets/src/config/profile.ts +34 -5
- package/dist/assets/src/core/authz/policy.ts +15 -0
- package/dist/assets/src/core/coordinator/contract.ts +254 -0
- package/dist/assets/src/core/coordinator/driver.ts +504 -0
- package/dist/assets/src/core/coordinator/instancesRoute.ts +181 -0
- package/dist/assets/src/core/delivery.ts +69 -15
- package/dist/assets/src/core/deliverySnapshotStore.ts +84 -24
- package/dist/assets/src/core/reviewVerdict.ts +296 -0
- package/dist/assets/src/core/reviewedHead.ts +77 -0
- package/dist/assets/src/core/runEvents.ts +12 -3
- package/dist/assets/src/core/runLedger/decisions.ts +2 -1
- package/dist/assets/src/core/runLedger/types.ts +17 -1
- package/dist/assets/src/core/runRecord.ts +56 -0
- package/dist/assets/src/core/ship/coordinator.ts +1058 -0
- package/dist/assets/web/dist/.vite/manifest.json +20 -20
- package/dist/assets/web/dist/assets/DeliveryPage-CvlWP7Eq.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-Bcasasjb.js → ResidentDetailPage-D3P21yeI.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-BJwyrphn.js → ResidentsIndexPage-DOYqnZ1q.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-6eStApqP.js → RunRoutePage-OmvrvPXY.js} +4 -4
- package/dist/assets/web/dist/assets/{RunsIndexPage-CWrkv7v8.js → RunsIndexPage-DWbSQtL4.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-BX2py1X3.js → ScheduledPage-CPKfJ4mR.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-CwCK84JN.js → StatusDot-COr8jTyM.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-CFC88_-Z.js → Tooltip-fOqTZkNT.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-BoLiHpua.js → dist-BVjAWgkb.js} +1 -1
- package/dist/assets/web/dist/assets/main-CuENKPdD.css +1 -0
- package/dist/assets/web/dist/assets/{main-tYcFk9Dc.js → main-DZbJaqUb.js} +2 -2
- package/dist/cli.js +6056 -3225
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/DeliveryPage-Cx7kQC_e.js +0 -1
- package/dist/assets/web/dist/assets/main-i3ZNDRLK.css +0 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Reviewed-head guard for the review post-step (docs/reference/specs/agent-review.md item 8).
|
|
2
|
+
//
|
|
3
|
+
// A review may only be posted to a PR when the commit the agent ACTUALLY
|
|
4
|
+
// reviewed is the PR head resolved for the run (`RepoContext.headSha`). The
|
|
5
|
+
// failure this guards against: the agent's own worktree is on the right
|
|
6
|
+
// branch, but it fetches and checks out another PR's branch because the PR
|
|
7
|
+
// body references it, reviews that, calls `submit_verdict approve`, and the
|
|
8
|
+
// deterministic post-step puts `LGTM:` on the PR under review — which an
|
|
9
|
+
// auto-approve workflow then approves. Every layer has done its job except the
|
|
10
|
+
// one that asks "is this review OF this PR?".
|
|
11
|
+
//
|
|
12
|
+
// Two sources for the reviewed head, in authority order:
|
|
13
|
+
// observed — `git rev-parse HEAD` run by the dispatcher in the run's
|
|
14
|
+
// workspace AFTER the model finished and BEFORE the workspace is
|
|
15
|
+
// released (a resident worktree; authoritative). Absent when the
|
|
16
|
+
// cwd is not a git repo (the cold sandbox's workspace root — the
|
|
17
|
+
// clone lives in a subdirectory the agent chose).
|
|
18
|
+
// reported — the `head` the agent passed to `submit_verdict` (it is told to
|
|
19
|
+
// run `git rev-parse HEAD` in the checkout it reviewed). Consulted
|
|
20
|
+
// only when nothing was observed; an observed mismatch is never
|
|
21
|
+
// rescued by a matching report.
|
|
22
|
+
//
|
|
23
|
+
// Fail-closed: unknown PR head, or no head from either source → no post.
|
|
24
|
+
|
|
25
|
+
export type HeadCheck = { ok: true; head: string; source: "observed" | "reported" } | { ok: false; reason: string };
|
|
26
|
+
|
|
27
|
+
const FULL_SHA = /^[0-9a-f]{40}$/;
|
|
28
|
+
const SHA_PREFIX = /^[0-9a-f]{7,40}$/;
|
|
29
|
+
|
|
30
|
+
/** Normalize a reported/observed head: trimmed, lowercased, 7–40 hex; else undefined. */
|
|
31
|
+
export function normalizeHead(value: unknown): string | undefined {
|
|
32
|
+
if (typeof value !== "string") return undefined;
|
|
33
|
+
const h = value.trim().toLowerCase();
|
|
34
|
+
return SHA_PREFIX.test(h) ? h : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The 40-hex commit from `git rev-parse HEAD` output, or undefined when the
|
|
38
|
+
* command failed (not a git repo, git missing) or printed nothing usable. */
|
|
39
|
+
export function parseRevParseOutput(output: string): string | undefined {
|
|
40
|
+
for (const line of output.split(/\r?\n/)) {
|
|
41
|
+
const t = line.trim().toLowerCase();
|
|
42
|
+
if (FULL_SHA.test(t)) return t;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Two normalized heads name the same commit when one is a ≥7-hex prefix of
|
|
48
|
+
* the other (the dispatcher's pre-run attach check reuses this). */
|
|
49
|
+
export function sameCommit(a: string, b: string): boolean {
|
|
50
|
+
const n = Math.min(a.length, b.length);
|
|
51
|
+
return n >= 7 && a.slice(0, n) === b.slice(0, n);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Decide whether the reviewed head is the PR head. `expected` is the PR head
|
|
56
|
+
* from repo resolution; `observed` the dispatcher's own `git rev-parse HEAD`
|
|
57
|
+
* (40-hex or undefined); `reported` whatever the agent passed to submit_verdict.
|
|
58
|
+
*/
|
|
59
|
+
export function checkReviewedHead(input: { expected?: string; observed?: string; reported?: string }): HeadCheck {
|
|
60
|
+
const expected = normalizeHead(input.expected);
|
|
61
|
+
if (!expected) return { ok: false, reason: "PR head unknown at resolution time — cannot verify what was reviewed" };
|
|
62
|
+
const observed = normalizeHead(input.observed);
|
|
63
|
+
if (observed) {
|
|
64
|
+
return sameCommit(observed, expected)
|
|
65
|
+
? { ok: true, head: observed, source: "observed" }
|
|
66
|
+
: { ok: false, reason: `reviewed head ${observed.slice(0, 7)} is not the PR head ${expected.slice(0, 7)}` };
|
|
67
|
+
}
|
|
68
|
+
const reported = normalizeHead(input.reported);
|
|
69
|
+
if (!reported)
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
reason: "reviewed head unknown — the workspace HEAD could not be read and no head was reported with the verdict",
|
|
73
|
+
};
|
|
74
|
+
return sameCommit(reported, expected)
|
|
75
|
+
? { ok: true, head: reported, source: "reported" }
|
|
76
|
+
: { ok: false, reason: `reviewed head ${reported.slice(0, 7)} is not the PR head ${expected.slice(0, 7)}` };
|
|
77
|
+
}
|
|
@@ -104,7 +104,13 @@ export type RunNoteKind =
|
|
|
104
104
|
* item 24). The same text the card carries; published by the dispatcher
|
|
105
105
|
* after the attach, before the first turn, so the run page explains a
|
|
106
106
|
* sandbox run that shows resident steps. */
|
|
107
|
-
| "cold_sandbox"
|
|
107
|
+
| "cold_sandbox"
|
|
108
|
+
/** A coding run submitted a PR description but the post-step opened no
|
|
109
|
+
* pull request because the branch it observed IS the base the pull
|
|
110
|
+
* request would target (docs/reference/specs/pr-description.md item 5) —
|
|
111
|
+
* the summary names the branch. Published by the post-step, so a unit
|
|
112
|
+
* that ends without a pull request says why on the record and the card. */
|
|
113
|
+
| "pr_not_opened";
|
|
108
114
|
|
|
109
115
|
/** Every `RunNoteKind`, as a value (a reader that filters notes by kind uses
|
|
110
116
|
* this; adding a kind to the union without adding it here is a type error). */
|
|
@@ -123,6 +129,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
123
129
|
"resumed",
|
|
124
130
|
"description_turn",
|
|
125
131
|
"cold_sandbox",
|
|
132
|
+
"pr_not_opened",
|
|
126
133
|
] as const satisfies readonly RunNoteKind[];
|
|
127
134
|
type _EveryKindListed = [RunNoteKind] extends [(typeof RUN_NOTE_KINDS)[number]] ? true : never;
|
|
128
135
|
const _everyKindListed: _EveryKindListed = true;
|
|
@@ -319,8 +326,10 @@ export type RunEvent =
|
|
|
319
326
|
text: string;
|
|
320
327
|
/** Where the request came from, for the Request block: the channel and
|
|
321
328
|
* user display names and a link back to the triggering message —
|
|
322
|
-
* whatever the adapter supplied (all optional)
|
|
323
|
-
|
|
329
|
+
* whatever the adapter supplied (all optional) — and, for a follow-up a
|
|
330
|
+
* run sent rather than a person (a parent's `send_to_run`), that run's
|
|
331
|
+
* id (docs/reference/specs/agent-conductor.md item 8). */
|
|
332
|
+
source?: { url?: string; channel?: string; user?: string; run?: string };
|
|
324
333
|
seq?: number;
|
|
325
334
|
at?: number;
|
|
326
335
|
}
|
|
@@ -9,7 +9,7 @@ import type { ClaimResult, FenceResult, LivePhase } from "./types.js";
|
|
|
9
9
|
* after a lost response), anything else is refused with what the steer
|
|
10
10
|
* message needs. */
|
|
11
11
|
export function decideClaim(
|
|
12
|
-
existing: { runId: string; agent?: string; startedAt: number; ownerGen: string } | undefined,
|
|
12
|
+
existing: { runId: string; agent?: string; startedAt: number; ownerGen: string; idempotencyKey?: string } | undefined,
|
|
13
13
|
req: { runId: string; gen: string },
|
|
14
14
|
): ClaimResult {
|
|
15
15
|
if (!existing) return { ok: true };
|
|
@@ -21,6 +21,7 @@ export function decideClaim(
|
|
|
21
21
|
runId: existing.runId,
|
|
22
22
|
...(existing.agent !== undefined ? { agent: existing.agent } : {}),
|
|
23
23
|
startedAt: existing.startedAt,
|
|
24
|
+
...(existing.idempotencyKey !== undefined ? { idempotencyKey: existing.idempotencyKey } : {}),
|
|
24
25
|
},
|
|
25
26
|
};
|
|
26
27
|
}
|
|
@@ -61,6 +61,14 @@ export interface LiveRunMeta {
|
|
|
61
61
|
* budget instead of re-reading the preset's. Absent on rows written before
|
|
62
62
|
* profiles existed. */
|
|
63
63
|
profile?: RunProfile;
|
|
64
|
+
/** The run that spawned this one (item 46), so a reclaimed child's record
|
|
65
|
+
* still names its parent. Absent on every run a person or a schedule started. */
|
|
66
|
+
parentRunId?: string;
|
|
67
|
+
/** The coordinator instance this run is a child of, and the key its spawn
|
|
68
|
+
* carried (item 48) — stored at the claim, so a reclaimed child's record
|
|
69
|
+
* still sends the parent its event and a retried spawn finds its run. */
|
|
70
|
+
parentInstanceId?: string;
|
|
71
|
+
idempotencyKey?: string;
|
|
64
72
|
/** Which executor the run attached: what `makeExecutor` chose. */
|
|
65
73
|
selection?: "resident" | "sandbox" | "local" | "none";
|
|
66
74
|
/** The worktree path the system prompt names. */
|
|
@@ -143,8 +151,16 @@ export interface ClaimRequest {
|
|
|
143
151
|
phase?: "attaching" | "live";
|
|
144
152
|
}
|
|
145
153
|
|
|
154
|
+
/** A refused claim names the run holding the thread — and the coordinator key
|
|
155
|
+
* its row carries (item 48), so a retried spawn can tell its own child from
|
|
156
|
+
* a busy thread. */
|
|
146
157
|
export type ClaimResult =
|
|
147
|
-
|
|
158
|
+
| { ok: true }
|
|
159
|
+
| {
|
|
160
|
+
ok: false;
|
|
161
|
+
reason: "thread-live";
|
|
162
|
+
live: { runId: string; agent?: string; startedAt: number; idempotencyKey?: string };
|
|
163
|
+
};
|
|
148
164
|
|
|
149
165
|
export type FenceResult = { ok: true } | { ok: false; reason: "fenced" | "unknown-run" };
|
|
150
166
|
|
|
@@ -3,6 +3,13 @@ import type { BoundaryScope, Identity, MachineClass, RunProfile } from "../confi
|
|
|
3
3
|
import type { RunEvent } from "./runEvents.js";
|
|
4
4
|
import { isHeadMaterial, isSpanRecord } from "./runEvents.js";
|
|
5
5
|
import { isHandoffShape, type Handoff } from "./ship/handoff.js";
|
|
6
|
+
import {
|
|
7
|
+
isFindingDispositionsShape,
|
|
8
|
+
isReviewVerdictShape,
|
|
9
|
+
type FindingDisposition,
|
|
10
|
+
type ReviewVerdict,
|
|
11
|
+
} from "./reviewVerdict.js";
|
|
12
|
+
import { IDEMPOTENCY_KEY_PATTERN, INSTANCE_ID_PATTERN } from "./coordinator/contract.js";
|
|
6
13
|
import {
|
|
7
14
|
FRICTION_CATEGORIES,
|
|
8
15
|
type CategoryTotals,
|
|
@@ -32,6 +39,8 @@ const RUN_STATUSES: readonly RunStatus[] = ["completed", "stopped_soft", "stoppe
|
|
|
32
39
|
|
|
33
40
|
/** Every `runs.*` id: checked before any store call. */
|
|
34
41
|
export const RUN_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
42
|
+
/** A reviewed head as the record stores it: the normalized sha (`normalizeHead`), 7 to 40 lowercase hex. */
|
|
43
|
+
const REVIEW_HEAD_PATTERN = /^[0-9a-f]{7,40}$/;
|
|
35
44
|
|
|
36
45
|
/** One finished run as the store keeps it. Events are already redacted and
|
|
37
46
|
* capped upstream (`runEvents.ts`); this layer adds no data. */
|
|
@@ -98,12 +107,36 @@ export interface RunRecord {
|
|
|
98
107
|
* three empty lists, distinguishable from none); absent on every other run
|
|
99
108
|
* and on records written before it existed. */
|
|
100
109
|
handoff?: Handoff;
|
|
110
|
+
/** The verdict a review run submitted through `submit_verdict` (docs/reference/specs/agent-review.md),
|
|
111
|
+
* redacted like every stored string — what a coordinator's `read-record`
|
|
112
|
+
* answers for a review child. Present only on a run that submitted one. */
|
|
113
|
+
verdict?: ReviewVerdict;
|
|
114
|
+
/** The head a review run reviewed and posted against (7 to 40 lowercase hex),
|
|
115
|
+
* after the settle; present only on a review run that pinned one. */
|
|
116
|
+
reviewHead?: string;
|
|
117
|
+
/** The dispositions a fix round submitted through `submit_dispositions`
|
|
118
|
+
* (docs/reference/specs/agent-ship.md item 6), the last call's set, redacted;
|
|
119
|
+
* present only on a coding run dispatched as a fix round that submitted one. */
|
|
120
|
+
dispositions?: FindingDisposition[];
|
|
101
121
|
/** The effective profile the run was admitted with (docs/decisions/0026-capability-profiles-and-request-routing.md):
|
|
102
122
|
* the preset, its machine class and identity, the minutes it ran on and —
|
|
103
123
|
* when a boundary clipped the budget — the scope that did, so a reader can
|
|
104
124
|
* tell a clipped budget from a declared one. Absent on records written
|
|
105
125
|
* before profiles existed. */
|
|
106
126
|
profile?: RunProfileRecord;
|
|
127
|
+
/** The run that spawned this one (item 46): a child started through
|
|
128
|
+
* `spawnChild()` names its parent, so a listing can draw the tree. Absent
|
|
129
|
+
* on every run a person or a schedule started. */
|
|
130
|
+
parentRunId?: string;
|
|
131
|
+
/** The coordinator instance this run is a child of (item 48): the state
|
|
132
|
+
* Worker's finish sends that instance `run-finished-<id>` for a record
|
|
133
|
+
* carrying it, and `read-record` answers only for a matching instance.
|
|
134
|
+
* Absent on every run no coordinator spawned. */
|
|
135
|
+
parentInstanceId?: string;
|
|
136
|
+
/** The key the coordinator's spawn carried (`<parentInstanceId>:<step>`,
|
|
137
|
+
* item 48), stored at the claim so a retried spawn finds its run. Present
|
|
138
|
+
* exactly when `parentInstanceId` is — both or neither, never one alone. */
|
|
139
|
+
idempotencyKey?: string;
|
|
107
140
|
}
|
|
108
141
|
|
|
109
142
|
/** The profile as the record stores it: the run's effective profile plus the preset it came from. */
|
|
@@ -124,6 +157,7 @@ const BOUNDARY_SCOPES_IN_RECORD: Record<BoundaryScope, true> = {
|
|
|
124
157
|
channel: true,
|
|
125
158
|
user: true,
|
|
126
159
|
directive: true,
|
|
160
|
+
parent: true,
|
|
127
161
|
};
|
|
128
162
|
|
|
129
163
|
/** Structural check on a stored profile (item 3's rule for the field). */
|
|
@@ -462,7 +496,29 @@ export function isRunRecord(v: unknown): v is RunRecord {
|
|
|
462
496
|
// The handoff is checked for shape, not bounds (docs/reference/specs/agent-ship.md
|
|
463
497
|
// item 14): redaction may lengthen a stored string past the tool's limit.
|
|
464
498
|
if (r.handoff !== undefined && !isHandoffShape(r.handoff)) return false;
|
|
499
|
+
// The review's verdict and head and the fix round's dispositions (item 2):
|
|
500
|
+
// shape only, like the handoff — redaction may lengthen a stored string.
|
|
501
|
+
if (r.verdict !== undefined && !isReviewVerdictShape(r.verdict)) return false;
|
|
502
|
+
if (r.reviewHead !== undefined && (typeof r.reviewHead !== "string" || !REVIEW_HEAD_PATTERN.test(r.reviewHead)))
|
|
503
|
+
return false;
|
|
504
|
+
if (r.dispositions !== undefined && !isFindingDispositionsShape(r.dispositions)) return false;
|
|
465
505
|
if (r.profile !== undefined && !isRunProfileRecord(r.profile)) return false;
|
|
506
|
+
// A parent is named by a run id (item 46): the same shape as the record's own.
|
|
507
|
+
if (r.parentRunId !== undefined && (typeof r.parentRunId !== "string" || !RUN_ID_PATTERN.test(r.parentRunId)))
|
|
508
|
+
return false;
|
|
509
|
+
// A coordinator's child (item 48): the instance id in the platform's alphabet
|
|
510
|
+
// and the key `<instance>:<step>` — both or neither; one alone is no tag.
|
|
511
|
+
if ((r.parentInstanceId === undefined) !== (r.idempotencyKey === undefined)) return false;
|
|
512
|
+
if (
|
|
513
|
+
r.parentInstanceId !== undefined &&
|
|
514
|
+
(typeof r.parentInstanceId !== "string" || !INSTANCE_ID_PATTERN.test(r.parentInstanceId))
|
|
515
|
+
)
|
|
516
|
+
return false;
|
|
517
|
+
if (
|
|
518
|
+
r.idempotencyKey !== undefined &&
|
|
519
|
+
(typeof r.idempotencyKey !== "string" || !IDEMPOTENCY_KEY_PATTERN.test(r.idempotencyKey))
|
|
520
|
+
)
|
|
521
|
+
return false;
|
|
466
522
|
if (typeof r.channelId !== "string" || typeof r.userId !== "string" || typeof r.threadKey !== "string") return false;
|
|
467
523
|
// Absent on records written before the stamp existed (read as `unknown`); present → a known value.
|
|
468
524
|
if (r.channelVisibility !== undefined && !CHANNEL_VISIBILITIES.includes(r.channelVisibility as ChannelVisibility))
|