@coreplane/switchboard 1.205.0 → 1.206.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 (45) hide show
  1. package/dist/assets/Dockerfile +6 -3
  2. package/dist/assets/config/config.example.yaml +24 -1
  3. package/dist/assets/deploy/cloudflare/coordinator.ts +86 -0
  4. package/dist/assets/deploy/cloudflare/shared.ts +9 -0
  5. package/dist/assets/deploy/cloudflare/worker.ts +142 -6
  6. package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +9 -0
  7. package/dist/assets/deploy/cloudflare-memory/worker.ts +266 -18
  8. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +15 -0
  9. package/dist/assets/deploy/cloudflare-resident/Dockerfile +89 -3
  10. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +81 -10
  11. package/dist/assets/deploy/secrets.manifest.json +1 -1
  12. package/dist/assets/package-lock.json +3 -3
  13. package/dist/assets/package.json +1 -1
  14. package/dist/assets/source.json +3 -3
  15. package/dist/assets/src/agents/registry.ts +58 -7
  16. package/dist/assets/src/config/profile.ts +34 -5
  17. package/dist/assets/src/core/authz/policy.ts +15 -0
  18. package/dist/assets/src/core/coordinator/contract.ts +239 -0
  19. package/dist/assets/src/core/coordinator/driver.ts +501 -0
  20. package/dist/assets/src/core/coordinator/instancesRoute.ts +181 -0
  21. package/dist/assets/src/core/delivery.ts +69 -15
  22. package/dist/assets/src/core/deliverySnapshotStore.ts +84 -24
  23. package/dist/assets/src/core/reviewVerdict.ts +296 -0
  24. package/dist/assets/src/core/reviewedHead.ts +77 -0
  25. package/dist/assets/src/core/runEvents.ts +4 -2
  26. package/dist/assets/src/core/runLedger/decisions.ts +2 -1
  27. package/dist/assets/src/core/runLedger/types.ts +17 -1
  28. package/dist/assets/src/core/runRecord.ts +56 -0
  29. package/dist/assets/src/core/ship/coordinator.ts +1039 -0
  30. package/dist/assets/web/dist/.vite/manifest.json +20 -20
  31. package/dist/assets/web/dist/assets/DeliveryPage-CvlWP7Eq.js +1 -0
  32. package/dist/assets/web/dist/assets/{ResidentDetailPage-Bcasasjb.js → ResidentDetailPage-D3P21yeI.js} +1 -1
  33. package/dist/assets/web/dist/assets/{ResidentsIndexPage-BJwyrphn.js → ResidentsIndexPage-DOYqnZ1q.js} +1 -1
  34. package/dist/assets/web/dist/assets/{RunRoutePage-6eStApqP.js → RunRoutePage-OmvrvPXY.js} +4 -4
  35. package/dist/assets/web/dist/assets/{RunsIndexPage-CWrkv7v8.js → RunsIndexPage-DWbSQtL4.js} +1 -1
  36. package/dist/assets/web/dist/assets/{ScheduledPage-BX2py1X3.js → ScheduledPage-CPKfJ4mR.js} +1 -1
  37. package/dist/assets/web/dist/assets/{StatusDot-CwCK84JN.js → StatusDot-COr8jTyM.js} +1 -1
  38. package/dist/assets/web/dist/assets/{Tooltip-CFC88_-Z.js → Tooltip-fOqTZkNT.js} +1 -1
  39. package/dist/assets/web/dist/assets/{dist-BoLiHpua.js → dist-BVjAWgkb.js} +1 -1
  40. package/dist/assets/web/dist/assets/main-CuENKPdD.css +1 -0
  41. package/dist/assets/web/dist/assets/{main-tYcFk9Dc.js → main-DZbJaqUb.js} +2 -2
  42. package/dist/cli.js +4621 -1804
  43. package/package.json +1 -1
  44. package/dist/assets/web/dist/assets/DeliveryPage-Cx7kQC_e.js +0 -1
  45. 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
+ }
@@ -319,8 +319,10 @@ export type RunEvent =
319
319
  text: string;
320
320
  /** Where the request came from, for the Request block: the channel and
321
321
  * user display names and a link back to the triggering message —
322
- * whatever the adapter supplied (all optional). */
323
- source?: { url?: string; channel?: string; user?: string };
322
+ * whatever the adapter supplied (all optional) — and, for a follow-up a
323
+ * run sent rather than a person (a parent's `send_to_run`), that run's
324
+ * id (docs/reference/specs/agent-conductor.md item 8). */
325
+ source?: { url?: string; channel?: string; user?: string; run?: string };
324
326
  seq?: number;
325
327
  at?: number;
326
328
  }
@@ -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
- { ok: true } | { ok: false; reason: "thread-live"; live: { runId: string; agent?: string; startedAt: number } };
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))