@coreplane/switchboard 0.0.0 → 1.18.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 (131) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +18 -1
  3. package/dist/assets/.dockerignore +27 -0
  4. package/dist/assets/.env.example +33 -0
  5. package/dist/assets/Dockerfile +111 -0
  6. package/dist/assets/config/config.example.yaml +359 -0
  7. package/dist/assets/deploy/bin/build-stamp.d.mts +15 -0
  8. package/dist/assets/deploy/bin/build-stamp.mjs +98 -0
  9. package/dist/assets/deploy/bin/cf-logs +32 -0
  10. package/dist/assets/deploy/cloudflare/package.json +29 -0
  11. package/dist/assets/deploy/cloudflare/preflight.mjs +243 -0
  12. package/dist/assets/deploy/cloudflare/tsconfig.json +18 -0
  13. package/dist/assets/deploy/cloudflare/worker.ts +382 -0
  14. package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +67 -0
  15. package/dist/assets/deploy/cloudflare/write-build.d.mts +7 -0
  16. package/dist/assets/deploy/cloudflare/write-build.mjs +53 -0
  17. package/dist/assets/deploy/cloudflare-docs/package.json +18 -0
  18. package/dist/assets/deploy/cloudflare-docs/wrangler.template.jsonc +30 -0
  19. package/dist/assets/deploy/cloudflare-memory/package.json +25 -0
  20. package/dist/assets/deploy/cloudflare-memory/tsconfig.json +17 -0
  21. package/dist/assets/deploy/cloudflare-memory/worker.ts +2635 -0
  22. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +50 -0
  23. package/dist/assets/deploy/cloudflare-resident/Dockerfile +91 -0
  24. package/dist/assets/deploy/cloudflare-resident/gc.ts +287 -0
  25. package/dist/assets/deploy/cloudflare-resident/node-async-hooks.d.ts +11 -0
  26. package/dist/assets/deploy/cloudflare-resident/package.json +29 -0
  27. package/dist/assets/deploy/cloudflare-resident/preflight.mjs +224 -0
  28. package/dist/assets/deploy/cloudflare-resident/tsconfig.json +19 -0
  29. package/dist/assets/deploy/cloudflare-resident/worker.ts +6637 -0
  30. package/dist/assets/deploy/cloudflare-resident/wrangler.template.jsonc +120 -0
  31. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +67 -0
  32. package/dist/assets/deploy/cloudflare-sandbox/docker-wrapper.sh +37 -0
  33. package/dist/assets/deploy/cloudflare-sandbox/package.json +26 -0
  34. package/dist/assets/deploy/cloudflare-sandbox/tsconfig.json +20 -0
  35. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +410 -0
  36. package/dist/assets/deploy/cloudflare-sandbox/wrangler.template.jsonc +67 -0
  37. package/dist/assets/deploy/profile.example.json +13 -0
  38. package/dist/assets/deploy/secrets.manifest.json +108 -0
  39. package/dist/assets/docker-entrypoint.sh +15 -0
  40. package/dist/assets/package-lock.json +18407 -0
  41. package/dist/assets/package.json +104 -0
  42. package/dist/assets/project.json +219 -0
  43. package/dist/assets/source.json +5 -0
  44. package/dist/assets/src/core/authz/actor.ts +100 -0
  45. package/dist/assets/src/core/authz/authorize.ts +169 -0
  46. package/dist/assets/src/core/authz/grants.ts +347 -0
  47. package/dist/assets/src/core/authz/policy.ts +281 -0
  48. package/dist/assets/src/core/authz/resource.ts +147 -0
  49. package/dist/assets/src/core/authz/types.ts +164 -0
  50. package/dist/assets/src/core/drain.ts +54 -0
  51. package/dist/assets/src/core/ingressTokens.ts +64 -0
  52. package/dist/assets/src/core/memory/engine.ts +115 -0
  53. package/dist/assets/src/core/memory/scorer.ts +147 -0
  54. package/dist/assets/src/core/memory/types.ts +120 -0
  55. package/dist/assets/src/core/normalizeSpans.ts +299 -0
  56. package/dist/assets/src/core/prDescriptionTypes.ts +54 -0
  57. package/dist/assets/src/core/redact.ts +113 -0
  58. package/dist/assets/src/core/runEvents.ts +537 -0
  59. package/dist/assets/src/core/runFriction.ts +665 -0
  60. package/dist/assets/src/core/runLedger/decisions.ts +126 -0
  61. package/dist/assets/src/core/runLedger/types.ts +177 -0
  62. package/dist/assets/src/core/runRecord.ts +627 -0
  63. package/dist/assets/src/core/runShape.ts +61 -0
  64. package/dist/assets/src/core/schedules.ts +452 -0
  65. package/dist/assets/src/core/time/formatDuration.ts +61 -0
  66. package/dist/assets/src/core/trace/attrs.ts +203 -0
  67. package/dist/assets/src/core/trace/classify.ts +49 -0
  68. package/dist/assets/src/core/trace/clock.ts +6 -0
  69. package/dist/assets/src/core/trace/context.ts +9 -0
  70. package/dist/assets/src/core/trace/ids.ts +23 -0
  71. package/dist/assets/src/core/trace/partition.ts +235 -0
  72. package/dist/assets/src/core/trace/sinks.ts +68 -0
  73. package/dist/assets/src/core/trace/streamSpans.ts +163 -0
  74. package/dist/assets/src/core/trace/traceparent.ts +29 -0
  75. package/dist/assets/src/core/trace/tracer.ts +247 -0
  76. package/dist/assets/src/core/trace/types.ts +125 -0
  77. package/dist/assets/src/core/trace/workerTrace.ts +97 -0
  78. package/dist/assets/src/deploy/buildStamp.ts +93 -0
  79. package/dist/assets/src/deploy/liveGate.ts +203 -0
  80. package/dist/assets/src/deploy/profile.ts +162 -0
  81. package/dist/assets/src/deploy/restart.ts +393 -0
  82. package/dist/assets/src/effort.ts +17 -0
  83. package/dist/assets/src/execution/bashTimeout.ts +78 -0
  84. package/dist/assets/src/execution/bindingPurge.ts +43 -0
  85. package/dist/assets/src/execution/residentBackupTransfer.ts +50 -0
  86. package/dist/assets/src/execution/residentCleanliness.ts +95 -0
  87. package/dist/assets/src/execution/residentCredentials.ts +81 -0
  88. package/dist/assets/src/execution/residentDepCache.ts +321 -0
  89. package/dist/assets/src/execution/residentDepsStore.ts +326 -0
  90. package/dist/assets/src/execution/residentDetach.ts +48 -0
  91. package/dist/assets/src/execution/residentDisk.ts +107 -0
  92. package/dist/assets/src/execution/residentDiskBudget.ts +448 -0
  93. package/dist/assets/src/execution/residentExecWrap.ts +100 -0
  94. package/dist/assets/src/execution/residentHead.ts +85 -0
  95. package/dist/assets/src/execution/residentReadonly.ts +72 -0
  96. package/dist/assets/src/execution/residentRefresh.ts +429 -0
  97. package/dist/assets/src/execution/residentRestoreExtract.ts +130 -0
  98. package/dist/assets/src/execution/residentState.ts +47 -0
  99. package/dist/assets/src/execution/residentStepReport.ts +98 -0
  100. package/dist/assets/src/execution/residentStepTrace.ts +97 -0
  101. package/dist/assets/src/execution/residentSteps.ts +99 -0
  102. package/dist/assets/src/execution/residentText.ts +83 -0
  103. package/dist/assets/src/execution/residentTrace.ts +119 -0
  104. package/dist/assets/src/execution/sandboxEnv.ts +42 -0
  105. package/dist/assets/src/execution/sandboxErrors.ts +159 -0
  106. package/dist/assets/src/execution/sandboxKeepalive.ts +118 -0
  107. package/dist/assets/src/execution/shellQuote.ts +8 -0
  108. package/dist/assets/src/mcp/registry.ts +242 -0
  109. package/dist/assets/src/providers/types.ts +152 -0
  110. package/dist/assets/web/dist/.vite/manifest.json +176 -0
  111. package/dist/assets/web/dist/assets/AppShell-Bk2gbvet.js +1 -0
  112. package/dist/assets/web/dist/assets/CostsPage-CTZcMYYx.js +1 -0
  113. package/dist/assets/web/dist/assets/NotFoundPage-C-BuaSm8.js +1 -0
  114. package/dist/assets/web/dist/assets/ResidentDetailPage-D3shEnzl.js +1 -0
  115. package/dist/assets/web/dist/assets/ResidentsIndexPage-DWIubQ05.js +1 -0
  116. package/dist/assets/web/dist/assets/RunRoutePage-BMjuE-oX.js +126 -0
  117. package/dist/assets/web/dist/assets/RunRoutePage-XVFj0XDc.css +1 -0
  118. package/dist/assets/web/dist/assets/RunsIndexPage-C3_jYIo0.js +1 -0
  119. package/dist/assets/web/dist/assets/RunsTabs-C4krAL9o.js +1 -0
  120. package/dist/assets/web/dist/assets/ScheduledPage-g1W58mtN.js +1 -0
  121. package/dist/assets/web/dist/assets/StatusDot-DcPRw3zu.js +1 -0
  122. package/dist/assets/web/dist/assets/Tooltip-DJUkMYjo.js +1 -0
  123. package/dist/assets/web/dist/assets/favicon-DL1rdWJt.js +1 -0
  124. package/dist/assets/web/dist/assets/localIso-L06jV29p.js +1 -0
  125. package/dist/assets/web/dist/assets/main-BsBGUyMH.css +2 -0
  126. package/dist/assets/web/dist/assets/main-CyM5f4JC.js +28 -0
  127. package/dist/assets/web/dist/assets/residentDiskBudget-BMBKlYRH.js +1 -0
  128. package/dist/assets/web/dist/assets/seed-BglCRKLA.js +6 -0
  129. package/dist/assets/web/dist/assets/wallClock-Ckv3sKoR.js +1 -0
  130. package/dist/cli.js +34494 -0
  131. package/package.json +43 -10
@@ -0,0 +1,113 @@
1
+ /** Redaction and display-safety helpers shared by every surface that shows text
2
+ * it did not write: run-visibility streams, Slack cards and replies, resident
3
+ * state and error bodies, GitHub error messages. Node-free and import-free so
4
+ * the Workers can import it by relative path. */
5
+
6
+ // Credential shapes we must never surface in a run-visibility stream (which may
7
+ // be shown in-channel or on a shared page). Two layers: (1) specific known
8
+ // formats (below), and (2) a name-gated assignment pass (redactNamedAssignments)
9
+ // that hides the VALUE of any `…SECRET`/`…KEY`/`…TOKEN`-style identifier. We
10
+ // redact recognized shapes rather than any long string, to avoid mangling
11
+ // legitimate output (SHAs, UUIDs, digests, version numbers all pass through).
12
+ const REDACT: Array<{ re: RegExp; replace: string }> = [
13
+ // PEM private keys — full block (incl. \n-escaped inside JSON) and a bare header.
14
+ {
15
+ re: /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g,
16
+ replace: "«redacted-private-key»",
17
+ },
18
+ { re: /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g, replace: "«redacted-private-key»" },
19
+ // URL / connection-string basic-auth: scheme://user:password@host
20
+ // Anchored to the start of a scheme-character run (not `\b`): one attempt per
21
+ // run keeps a long pasted token linear, and `1https://u:p@h` still redacts.
22
+ { re: /(?<![a-z0-9+.-])([a-z0-9+.-]+:\/\/)([^\s:/@]+):([^\s:/@]+)@/gi, replace: "$1$2:«redacted»@" },
23
+ // curl -u user:pass
24
+ { re: /(^|\s)(-u|--user)(\s+|=)\S+:\S+/g, replace: "$1$2$3«redacted»" },
25
+ // HTTP auth headers (Bearer / Basic / token) and bare Bearer tokens
26
+ { re: /\b(Authorization\s*:\s*)(Bearer|Basic|token)\s+[A-Za-z0-9._~+/=-]{8,}/gi, replace: "$1$2 «redacted»" },
27
+ { re: /\b[Bb]earer\s+[A-Za-z0-9._~+/-]{12,}=*/g, replace: "Bearer «redacted»" },
28
+ // Cookies (whole header value)
29
+ { re: /\b((?:Set-)?Cookie\s*:\s*)[^\r\n]+/gi, replace: "$1«redacted»" },
30
+ // Provider / cloud token formats
31
+ { re: /xox[baprs]-[A-Za-z0-9-]{8,}/g, replace: "«redacted-slack-token»" },
32
+ { re: /gh[pousr]_[A-Za-z0-9]{20,}/g, replace: "«redacted-github-token»" },
33
+ { re: /github_pat_[A-Za-z0-9_]{20,}/g, replace: "«redacted-github-pat»" },
34
+ { re: /x-access-token:[^@\s/'"]+/gi, replace: "x-access-token:«redacted»" },
35
+ { re: /sk-ant-[A-Za-z0-9_-]{16,}/g, replace: "«redacted-anthropic-key»" },
36
+ { re: /sk-(?:proj-)?[A-Za-z0-9_-]{16,}/g, replace: "«redacted-api-key»" },
37
+ { re: /AKIA[0-9A-Z]{16}/g, replace: "«redacted-aws-key»" },
38
+ { re: /AIza[0-9A-Za-z_-]{35}/g, replace: "«redacted-gcp-key»" },
39
+ { re: /\b(?:whsec|sk_live|sk_test|rk_live|pk_live)_[A-Za-z0-9]{16,}/g, replace: "«redacted-stripe-key»" },
40
+ ];
41
+
42
+ // An identifier component (split on _ or -) that marks its assignment's value as
43
+ // secret. Matched case-insensitively against each component, so `AWS_SECRET_
44
+ // ACCESS_KEY` (…SECRET, …KEY) and `STRIPE_WEBHOOK_SECRET` are caught while
45
+ // `PORT`, `REACT_VERSION`, `DATABASE_URL`, `MONKEY_BARS` are not.
46
+ const SECRET_COMPONENT =
47
+ /^(secret|token|password|passwd|pwd|credential|credentials|key|apikey|auth|session|sessionid|cookie)$/i;
48
+
49
+ /** Redact the VALUE of any `<name> = value` / `<name>: value` where the name has
50
+ * a secret-marking component. Handles quoted values (with spaces) and unquoted,
51
+ * and a quoted NAME (`"password": "…"` in pasted JSON — the closing quote sits
52
+ * between the name and the separator). Name-gated so ordinary config
53
+ * assignments are untouched. */
54
+ function redactNamedAssignments(text: string): string {
55
+ return text.replace(
56
+ // The identifier is the WHOLE `[A-Za-z0-9_-]` run, anchored to its start by
57
+ // the lookbehind: without an anchor a long unbroken token (pasted base64, a
58
+ // minified line) is retried from every offset and each attempt backtracks
59
+ // the whole tail — O(n²), ~0.5 s per 20 KB. A `\b` anchor is not enough:
60
+ // it skips `_SECRET=`, `self._password =`, `2fa_token=` (a letter run
61
+ // preceded by `_`/digit), and a letter-start id (`[A-Za-z][A-Za-z0-9]*`)
62
+ // is still retried at every letter of a mixed alphanumeric run. Taking the
63
+ // maximal run means one attempt per run; the component check below still
64
+ // decides whether it names a secret (`_SECRET` → ["", "SECRET"]).
65
+ /(?<![A-Za-z0-9_-])([A-Za-z0-9_-]+)("?)(\s*[=:]\s*)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s]{4,})/g,
66
+ (whole, id: string, close: string, sep: string, val: string) => {
67
+ if (!id.split(/[_-]/).some((p) => SECRET_COMPONENT.test(p))) return whole;
68
+ const quote = val[0] === '"' || val[0] === "'" ? val[0] : "";
69
+ return `${id}${close}${sep}${quote}«redacted»${quote}`;
70
+ },
71
+ );
72
+ }
73
+
74
+ /** Strip known credential formats from text before it enters a run-visibility
75
+ * stream: specific shapes first, then the name-gated assignment pass. */
76
+ export function redactSecrets(text: string): string {
77
+ let out = text;
78
+ for (const { re, replace } of REDACT) out = out.replace(re, replace);
79
+ return redactNamedAssignments(out);
80
+ }
81
+
82
+ // Terminal control sequences: CSI (`ESC [ … final`, covers SGR colors, cursor
83
+ // moves, erase), OSC (`ESC ] … BEL|ST`, covers hyperlinks/titles; an OSC cut
84
+ // off by truncation is stripped to end of line so its payload never shows),
85
+ // two-byte ESC sequences, plus C0 controls other than \n and \t (so \r is
86
+ // dropped too: CRLF becomes \n and progress-bar rewrites collapse). Tool
87
+ // output from vitest/git/npm carries these; a browser drops the ESC byte and
88
+ // shows the bare `[32m` remainder, so strip the whole sequence before display.
89
+ const ANSI_RE =
90
+ // eslint-disable-next-line no-control-regex
91
+ /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b\n]*(?:\x07|\x1b\\)?|\x1b[@-Z\\-_]|[\x00-\x08\x0b-\x1f\x7f]/g;
92
+
93
+ /** Remove terminal escape/control sequences, leaving printable text, `\n`, `\t`.
94
+ * Callers strip BEFORE redactSecrets: an escape embedded mid-token would
95
+ * otherwise split a secret across the redaction regex and let it leak. */
96
+ export function stripAnsi(text: string): string {
97
+ return text.replace(ANSI_RE, "");
98
+ }
99
+
100
+ /** Redact THEN cap — the correct order for a length-limited display string, so a
101
+ * secret near a truncation boundary can never be emitted as a raw fragment. */
102
+ export function redactAndCap(text: string, cap = 200): string {
103
+ const redacted = redactSecrets(stripAnsi(text));
104
+ return redacted.length > cap ? `${redacted.slice(0, cap)}…` : redacted;
105
+ }
106
+
107
+ /** The first non-empty line of `text`, with runs of whitespace collapsed to one
108
+ * space — for titles and labels, which are one line by construction and must
109
+ * never carry a second line of remote text. */
110
+ export function oneLine(text: string): string {
111
+ const first = text.split("\n").find((l) => l.trim().length > 0) ?? "";
112
+ return first.replace(/\s+/g, " ").trim();
113
+ }
@@ -0,0 +1,537 @@
1
+ // Types only, and from the zod-free module deliberately: this file is part of
2
+ // the node-free contract the memory Worker and web app compile with their own
3
+ // tsconfigs — importing prDescription.ts would drag zod into those graphs.
4
+ import type { PrDescription, RenderedTourStep } from "./prDescriptionTypes.js";
5
+
6
+ /** The `pr_description` review artifact minus the event envelope
7
+ * (docs/reference/specs/reading-diff.md item 7). */
8
+ export interface PrDescriptionArtifact {
9
+ artifact: "pr_description";
10
+ /** `submitted`: the typed object a coding run submitted — exact and complete;
11
+ * `parsed`: read back from the PR body GitHub holds, with `problems` naming
12
+ * what the body did not carry in the renderer's shape. (Not named `source`:
13
+ * the `input` event's `source` is an object, and a literal-typed twin here
14
+ * would make `source` a discriminant of the whole union.) */
15
+ origin: "submitted" | "parsed";
16
+ repo: string;
17
+ pr: number;
18
+ /** The PR head the artifact was produced at: the render head for `submitted`,
19
+ * the reviewed head for `parsed`. Compare with each anchor's `sha` to know
20
+ * whether a step points into the head being looked at. */
21
+ headSha?: string;
22
+ /** The coding run a review run copied a `submitted` artifact from. */
23
+ fromRunId?: string;
24
+ title: string;
25
+ /** The body as rendered (submitted) or as GitHub holds it (parsed), capped. */
26
+ body: string;
27
+ tldr?: string;
28
+ tour: RenderedTourStep[];
29
+ remaining: { path: string; note: string }[];
30
+ decisions: { title: string; rationale: string }[];
31
+ /** Nothing missing or malformed — always true for `submitted`. */
32
+ complete: boolean;
33
+ problems: string[];
34
+ /** The body was cut at the cap before parsing. */
35
+ truncated: boolean;
36
+ }
37
+
38
+ /** The artifact on the stream. An interface, not an intersection, so the
39
+ * union below stays discriminable on `type` for every object literal. */
40
+ export interface PrDescriptionArtifactEvent extends PrDescriptionArtifact {
41
+ type: "review_artifact";
42
+ seq?: number;
43
+ at?: number;
44
+ }
45
+
46
+ // Run visibility (docs/reference/specs/run-visibility.md): a typed stream of what an agent is doing —
47
+ // tool calls and their (redacted, summarized) results — emitted by the runner.
48
+ // Today the in-channel status card consumes it live; the external live-view
49
+ // page (a follow-up) will consume the same stream. Keeping it a small typed
50
+ // seam here means neither consumer reaches into the runner's internals.
51
+
52
+ //
53
+ // Timing + lifecycle: every event carries an optional `at`
54
+ // (epoch ms, stamped by the runner's injectable clock) so the run-friction
55
+ // analyzer (`runFriction.ts`) can attribute delay; a `tool_result` marks exec-
56
+ // INFRASTRUCTURE failures (`infra: true`, an `ExecInfraError` — the sandbox, not
57
+ // the command) so they are never confused with an ordinary nonzero exit; and
58
+ // `run_note` events carry the runner's lifecycle notices (wrap-up warning, budget
59
+ // exhaustion, dead sandbox) as typed kinds instead of only free-text progress.
60
+ // All additive: consumers that only know tool_call/tool_result keep working.
61
+
62
+ /** Typed lifecycle notices the runner emits alongside its `onProgress` text.
63
+ * `stop_requested` is published by the registry when an operator asks the run
64
+ * to stop from /runs; `stopped` by the runner when it honors it. */
65
+ export type RunNoteKind =
66
+ | "wrap_up"
67
+ | "time_budget_exhausted"
68
+ | "turn_budget_exhausted"
69
+ | "sandbox_dead"
70
+ /** The sandbox fleet had no free instance for this thread within the
71
+ * executor's bounded wait (docs/reference/specs/execution.md item 14). Capacity, not a
72
+ * dead sandbox: the run goes on and the model is told to retry or finish. */
73
+ | "fleet_busy"
74
+ | "stop_requested"
75
+ | "stopped"
76
+ /** Setup spans the request's stream sink had to drop before this run was
77
+ * bound (docs/reference/specs/tracing.md): `summary` says how many, `from`/`to` the
78
+ * interval, which the partition reports as not recorded. */
79
+ | "spans_dropped"
80
+ /** The PR head moved while a review ran and the same run is re-reviewing at
81
+ * the new head (agent-review.md item 12). Published by the dispatcher. */
82
+ | "head_moved"
83
+ /** An MCP server configured for this agent did not answer discovery
84
+ * (docs/reference/specs/mcp-tools.md item 8); the run proceeds without its tools. One
85
+ * note per server, published by the dispatcher before the first turn. */
86
+ | "mcp_unavailable"
87
+ /** A thread follow-up steered into this run was folded into its next step
88
+ * (docs/reference/specs/thread-admission.md item 2). Published by the runner as it
89
+ * drains the inbox, beside an `input` event carrying the follow-up itself. */
90
+ | "follow_up"
91
+ /** The run was resumed by a new bot generation from its ledger transcript
92
+ * (docs/reference/specs/run-history.md item 37); the summary says how many calls were
93
+ * in flight at the kill and how each was settled. Published by the runner. */
94
+ | "resumed"
95
+ /** A coding run pushed onto a branch that already heads an open PR without
96
+ * resubmitting the PR description, and the same run is being given one
97
+ * bounded extra model turn to submit it (docs/reference/specs/pr-description.md
98
+ * item 5). Published by the dispatcher before that turn. */
99
+ | "description_turn"
100
+ /** The run is on a cold per-thread sandbox instead of a warm resident, and
101
+ * the summary says why — the resident attach failed (its steps so far are
102
+ * grafted under the attach span), the resident was unreachable or not
103
+ * serviceable, or the repo is not onboarded (docs/reference/specs/resident-repos.md
104
+ * item 24). The same text the card carries; published by the dispatcher
105
+ * after the attach, before the first turn, so the run page explains a
106
+ * sandbox run that shows resident steps. */
107
+ | "cold_sandbox";
108
+
109
+ /** Every `RunNoteKind`, as a value (a reader that filters notes by kind uses
110
+ * this; adding a kind to the union without adding it here is a type error). */
111
+ export const RUN_NOTE_KINDS = [
112
+ "wrap_up",
113
+ "time_budget_exhausted",
114
+ "turn_budget_exhausted",
115
+ "sandbox_dead",
116
+ "fleet_busy",
117
+ "stop_requested",
118
+ "stopped",
119
+ "spans_dropped",
120
+ "head_moved",
121
+ "mcp_unavailable",
122
+ "follow_up",
123
+ "resumed",
124
+ "description_turn",
125
+ "cold_sandbox",
126
+ ] as const satisfies readonly RunNoteKind[];
127
+ type _EveryKindListed = [RunNoteKind] extends [(typeof RUN_NOTE_KINDS)[number]] ? true : never;
128
+ const _everyKindListed: _EveryKindListed = true;
129
+ void _everyKindListed;
130
+
131
+ /** How an operator asked a run to stop: `soft` — take no new steps and
132
+ * wrap up through the normal finale; `hard` — abort the in-flight call now, no
133
+ * finale, tear the workspace down. */
134
+ export type StopMode = "soft" | "hard";
135
+
136
+ /** Who asked a run to stop: the caller's surface and its platform-
137
+ * namespaced identity (`slack:U…`, `access:<sub>`, `mcp:<subject>`,
138
+ * `cli:local`). Recorded on the `stop_requested` note so the stream itself says
139
+ * who stopped the run; `id` is charset-restricted to `ACTOR_ID_PATTERN` and
140
+ * capped by `sanitizeActor` before it is published. Absent on notes published
141
+ * through the token-gated HTML path (the capability, not a person, is the actor). */
142
+ export interface RunActor {
143
+ kind: "access" | "mcp" | "cli" | "chat";
144
+ id: string;
145
+ }
146
+
147
+ /** The character class an `actor.id` may carry (regex body, no brackets) — the
148
+ * ONE source both the validating pattern and the sanitizer are built from. */
149
+ const ACTOR_ID_CHARS = "A-Za-z0-9:@._-";
150
+ const ACTOR_ID_MAX = 128;
151
+ /** The characters an `actor.id` may carry; anything else is dropped. */
152
+ export const ACTOR_ID_PATTERN = new RegExp(`^[${ACTOR_ID_CHARS}]{1,${ACTOR_ID_MAX}}$`);
153
+ const ACTOR_ID_FORBIDDEN = new RegExp(`[^${ACTOR_ID_CHARS}]`, "g");
154
+
155
+ /** Coerce an actor into the published shape: strip every character outside the
156
+ * allowed set, cap at 128, and fall back to `unknown` when nothing survives —
157
+ * a hostile id is neutered, never a reason to refuse the stop. */
158
+ export function sanitizeActor(actor: RunActor): RunActor {
159
+ const id = actor.id.replace(ACTOR_ID_FORBIDDEN, "").slice(0, ACTOR_ID_MAX);
160
+ return { kind: actor.kind, id: id.length > 0 ? id : "unknown" };
161
+ }
162
+
163
+ /** How one `agent:ship` round boundary reads (docs/reference/specs/agent-ship.md item 12).
164
+ * `started` marks the round's child being dispatched; the rest settle it:
165
+ * `pr_opened` — a coding round's post-step opened or edited the PR;
166
+ * `completed` — a fix round finished without a PR write (e.g. it declined
167
+ * everything and never resubmitted the description); `approve` /
168
+ * `request_changes` — a review round's verdict; `no_verdict` — the review
169
+ * child ended without `submit_verdict`, aborting the pipeline; `aborted` — the
170
+ * round ended the pipeline (a refusal, no resident worktree, a round-0
171
+ * terminal); `stopped` — an operator stop settled the round. A cap never
172
+ * settles a round: caps end the pipeline BETWEEN rounds, visible as the
173
+ * absence of a next `started` boundary plus the answer's cap report. */
174
+ export type ShipRoundOutcome =
175
+ "started" | "pr_opened" | "completed" | "approve" | "request_changes" | "no_verdict" | "aborted" | "stopped";
176
+
177
+ /**
178
+ * One event in a run's stream. `seq` is stamped by `RunRegistry.publish` — a
179
+ * monotonic, per-run 1-based position (optional on the way in, present on every
180
+ * event read back from the registry) so replays and history pages can resume
181
+ * from a point without comparing payloads.
182
+ */
183
+ /** A span record (docs/reference/specs/tracing.md): timing, not content. The registry
184
+ * accepts them between a run's finish and its seal, they never repaint the
185
+ * index, and every reader's counts and clocks skip them. */
186
+ export function isSpanRecord(e: { type: string }): e is SpanStartEvent | SpanEndEvent {
187
+ return e.type === "span_start" || e.type === "span_end";
188
+ }
189
+
190
+ /** The protected head (docs/reference/specs/tracing.md; live-view item 2) — is this event head material? The root's start, `slack.receive` and the
191
+ * `dispatch.*` span pairs, `input`, `context`, `run_meta`, and the
192
+ * `mcp_unavailable` / `spans_dropped` notes. */
193
+ export function isHeadMaterial(event: RunEvent): boolean {
194
+ switch (event.type) {
195
+ case "input":
196
+ case "context":
197
+ case "run_meta":
198
+ return true;
199
+ case "run_note":
200
+ return event.kind === "mcp_unavailable" || event.kind === "spans_dropped" || event.kind === "cold_sandbox";
201
+ case "span_start":
202
+ return event.name === "request" || event.name === "slack.receive" || event.name.startsWith("dispatch.");
203
+ case "span_end":
204
+ return event.name === "slack.receive" || event.name.startsWith("dispatch.");
205
+ default:
206
+ return false;
207
+ }
208
+ }
209
+
210
+ /** A span's attributes on the wire: the closed-table values `attrs.ts` admits
211
+ * (literal unions, numbers, booleans; a few sanitized strings). Free text
212
+ * rides only in `span_end.error`, redacted and capped. */
213
+ export type SpanEventAttrs = Readonly<Record<string, string | number | boolean>>;
214
+
215
+ /** A span opened (docs/reference/specs/tracing.md): the run-stream sink publishes one per
216
+ * streamed span so a live page can show the step as it runs. `at` is the
217
+ * span's start on the runner clock. */
218
+ export interface SpanStartEvent {
219
+ type: "span_start";
220
+ spanId: string;
221
+ parentSpanId?: string;
222
+ name: string;
223
+ attrs?: SpanEventAttrs;
224
+ seq?: number;
225
+ at?: number;
226
+ }
227
+
228
+ /** A span closed: its measured interval, its outcome and — for a span about
229
+ * this run's own work whose message we produce — the redacted, capped error
230
+ * text; a span whose failure came from a remote body carries the
231
+ * classification (`errorKind`/`errorCode`) in `attrs` and no message. `at` is
232
+ * the end on the runner clock (`startedAt + durationMs`). */
233
+ export interface SpanEndEvent {
234
+ type: "span_end";
235
+ spanId: string;
236
+ parentSpanId?: string;
237
+ name: string;
238
+ startedAt: number;
239
+ durationMs: number;
240
+ status: "ok" | "error";
241
+ error?: string;
242
+ attrs?: SpanEventAttrs;
243
+ seq?: number;
244
+ at?: number;
245
+ }
246
+
247
+ export type RunEvent =
248
+ /** `callId` is the provider's tool_use id — the explicit pair key between a
249
+ * call and its result (live-view item 13); the runner stamps it on both. */
250
+ /** `command` rides on bash calls only: the FULL command (redacted, capped at
251
+ * `COMMAND_CAP`, far above the 200-char `summary`), for consumers that must
252
+ * judge what the command did — the pushed-branch tracker
253
+ * (docs/reference/specs/pr-description.md item 5) reads it, because an agent's chained
254
+ * `git add … && git commit … && git push …` routinely carries its `push`
255
+ * past the summary cap. The status card and friction analyzer keep reading
256
+ * `summary`. */
257
+ /** `spanId` (docs/reference/specs/tracing.md): the `tool.*` span this call ran under, once the runner emits spans. */
258
+ | {
259
+ type: "tool_call";
260
+ tool: string;
261
+ summary: string;
262
+ command?: string;
263
+ callId?: string;
264
+ spanId?: string;
265
+ seq?: number;
266
+ at?: number;
267
+ }
268
+ /** `ok` is "the tool succeeded": false when it threw AND (bash) when the
269
+ * command exited nonzero. `exitCode` rides on bash results (parsed from the
270
+ * executors' shared `exit N:` prefix, 0 for a clean run; absent when the code
271
+ * was not numeric). `output` is the tool's text — control-stripped, redacted,
272
+ * capped at TOOL_OUTPUT_CAP — for the run page's expandable card; the status
273
+ * card and the friction analyzer keep reading `summary`. */
274
+ | {
275
+ type: "tool_result";
276
+ tool: string;
277
+ ok: boolean;
278
+ summary: string;
279
+ callId?: string;
280
+ exitCode?: number;
281
+ output?: string;
282
+ infra?: true;
283
+ spanId?: string;
284
+ seq?: number;
285
+ at?: number;
286
+ }
287
+ /** `mode` rides only on the stop notes (`stop_requested` / `stopped`); `actor`
288
+ * only on a `stop_requested` published through `RunsService.stopRun`. */
289
+ | {
290
+ type: "run_note";
291
+ kind: RunNoteKind;
292
+ summary: string;
293
+ mode?: StopMode;
294
+ actor?: RunActor;
295
+ /** On a `spans_dropped` note only (docs/reference/specs/tracing.md): the runner-clock
296
+ * interval the dropped setup records covered — a `not recorded` loss. */
297
+ from?: number;
298
+ to?: number;
299
+ spanId?: string;
300
+ seq?: number;
301
+ at?: number;
302
+ }
303
+ /** The run's final answer — the same text the channel reply/PR post is
304
+ * projected from, redacted like every event (NOT capped: the run record is
305
+ * the source of truth, the summaries are). Published by the dispatcher once
306
+ * per run, before `finish()` and before the reply goes out; absent when an
307
+ * AGENT run threw (the card shows ❌). An inline command run that throws
308
+ * still publishes one — the `⚠️ <error>` reply — so its record explains the
309
+ * `failed` status. `text` is the CANONICAL Markdown (docs/reference/specs/llm-output.md
310
+ * item 5); `raw` is the model's own text, present only when normalization
311
+ * changed it (redacted too, dropped if it would blow the per-event budget). */
312
+ | { type: "answer"; text: string; raw?: string; seq?: number; at?: number }
313
+ /** The request as received (directives stripped, attachments noted as a
314
+ * one-line count suffix — never bytes or file bodies), redacted, uncapped. Published by the dispatcher once
315
+ * per run, right after the run is registered — the first event of the record,
316
+ * so the run page can lead with what was asked (docs/reference/specs/live-view.md item 12). */
317
+ | {
318
+ type: "input";
319
+ text: string;
320
+ /** Where the request came from, for the Request block: the channel and
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 };
324
+ seq?: number;
325
+ at?: number;
326
+ }
327
+ /** One prior thread turn fed to the model, prefixed with its role
328
+ * (`user: …` / `assistant: …`), humanized and redacted like `input`, with
329
+ * attachments as metadata lines. Published by the dispatcher right after
330
+ * `input`, bounded (newest 20 turns / 256 KiB) and gated by
331
+ * `runHistory.includeContext`. */
332
+ | { type: "context"; text: string; seq?: number; at?: number }
333
+ /** The model's prose BETWEEN tool calls — text content that rode alongside
334
+ * tool_use in one completion. Emitted by the runner, redacted, uncapped. The
335
+ * final text-only completion is NOT one of these (that is the `answer`). */
336
+ | { type: "assistant"; text: string; spanId?: string; seq?: number; at?: number }
337
+ /** What the run is about (live-view item 19): the resolved agent and model,
338
+ * and — for a repo run — the repo, ref, PR number and PR head as resolved
339
+ * BEFORE the first model turn (`RepoContext`). Published by the dispatcher
340
+ * right after `input`, once per run, so the run page can head its Request
341
+ * block with linked `owner/repo · ref · #PR · sha`. Additive: every
342
+ * consumer that only knows the other types keeps working. */
343
+ | {
344
+ type: "run_meta";
345
+ agent: string;
346
+ /** Absent on a command run, which resolves no model. */
347
+ model?: string;
348
+ /** The request's trace id (docs/reference/specs/tracing.md), once the root exists. */
349
+ traceId?: string;
350
+ effort?: string;
351
+ repo?: string;
352
+ ref?: string;
353
+ pr?: number;
354
+ headSha?: string;
355
+ seq?: number;
356
+ at?: number;
357
+ }
358
+ /** A skill was loaded into the model's context (docs/reference/specs/skills.md). Emitted
359
+ * by the `use_skill` tool on a successful load — alongside, not instead of,
360
+ * its `tool_call`/`tool_result` pair — so skill use is a first-class fact in
361
+ * the run data with its own metadata: which skill, for which agent, from
362
+ * where (`source`, the pinned upstream URL when vendored), and how much
363
+ * context it cost (`bodyBytes`). Additive: consumers that only know the
364
+ * other types keep working; the friction analyzer ignores it. */
365
+ | {
366
+ type: "skill_use";
367
+ skill: string;
368
+ description: string;
369
+ agent: string;
370
+ /** The pinned upstream file URL for a vendored skill (docs/reference/specs/skills.md item 9). */
371
+ source?: string;
372
+ /** Structured vendoring provenance (`Skill.upstream`, recorded by skills:sync). */
373
+ upstream?: { repo: string; commit: string };
374
+ bodyBytes: number;
375
+ spanId?: string;
376
+ seq?: number;
377
+ at?: number;
378
+ }
379
+ /** A review run's reading diff (docs/reference/specs/reading-diff.md): the change as a
380
+ * reviewer reads it. The baseline artifact is the full `git diff`
381
+ * (`poweredBy: "git"`, guaranteed on every PR review); with the meat
382
+ * provider a SECOND artifact may follow — meat.dev's abridged reading diff
383
+ * (`poweredBy: "meat"`, with its one-line `summary` and the abridging
384
+ * call's own token usage) — iff meat finishes within the review. Published
385
+ * by the dispatcher straight to the registry (like `input`/`run_meta`),
386
+ * produced concurrently with the review by the run's own executor; readers
387
+ * prefer the meat artifact when both exist. Additive: unknown → ignored. */
388
+ | {
389
+ type: "review_artifact";
390
+ artifact: "reading_diff";
391
+ poweredBy: "git" | "meat";
392
+ baseRef: string;
393
+ diff: string;
394
+ truncated: boolean;
395
+ summary?: string;
396
+ meatTokens?: { input: number; output: number };
397
+ /** meat only (docs/reference/specs/reading-diff.md item 6): which diff meat
398
+ * read — GitHub's compare of base...head, or the recorded git artifact
399
+ * (whole) — and how many bytes it was; the `-model` it ran with. */
400
+ input?: "github-compare" | "recorded";
401
+ inputBytes?: number;
402
+ model?: string;
403
+ seq?: number;
404
+ at?: number;
405
+ }
406
+ /** The PR's description as data (docs/reference/specs/reading-diff.md item 7): the TL;DR,
407
+ * the Tour's steps with their anchors (each carrying the sha its permalink
408
+ * was rendered at), the Remaining-changes list and the decisions, so the run
409
+ * page's panel can render a collapsed description and a Tour that jumps to
410
+ * files and lines in the diff. Two sources, one shape: a coding run publishes
411
+ * its `submitted` object when the post-step opens or edits the PR (beside
412
+ * `pr_opened`); a review run copies that object when one exists for the head
413
+ * it reviews, else `parsed` reads the body GitHub holds back through the
414
+ * inverse parser. Every string is control-stripped and redacted like the
415
+ * reading diff. Additive: unknown → ignored. */
416
+ | PrDescriptionArtifactEvent
417
+ /** A coding run's accepted `PrDescription` (docs/reference/specs/pr-description.md): the
418
+ * typed object the run submitted through `submit_pr_description`, as
419
+ * validated — the same object the dispatcher renders the GitHub body from,
420
+ * so the run page's review panel can render it without a second authoring
421
+ * path. Published by the dispatcher once per run (the last valid submission
422
+ * wins), string fields redacted like every event payload. Additive: unknown
423
+ * → ignored. */
424
+ | { type: "pr_description"; description: PrDescription; seq?: number; at?: number }
425
+ /** The coding PR post-step's outcome (docs/reference/specs/pr-description.md item 5):
426
+ * the PR opened for the run's pushed branch — or, open-or-edit, the
427
+ * existing open PR that was edited (`created: false`). Published by the
428
+ * dispatcher straight to the registry BEFORE the stream finishes, so the
429
+ * run record carries the PR URL as a fact of the run rather than only the
430
+ * channel reply's projection of it. Additive: unknown → ignored. */
431
+ | { type: "pr_opened"; url: string; number: number; created: boolean; seq?: number; at?: number }
432
+ /** One `agent:ship` round boundary (docs/reference/specs/agent-ship.md item 12): the
433
+ * pipeline publishes a `started` event when a round's child is dispatched
434
+ * and one settle event when its outcome is known (`ShipRoundOutcome`), so
435
+ * rounds are legible on the one stream and per-round cost is derivable by
436
+ * slicing `model.turn` spans between boundaries. `index` is 0-based in the
437
+ * spec's round vocabulary — round 0 is the initial coding round; a review
438
+ * round and its fix round share an index. Published by the ship pipeline
439
+ * straight to the registry (like `pr_opened`), never through the runner.
440
+ * Additive: unknown → ignored. */
441
+ | { type: "ship_round"; index: number; agent: string; outcome: ShipRoundOutcome; seq?: number; at?: number }
442
+ /** The span records (docs/reference/specs/tracing.md): published, counted and stored like
443
+ * every other event, read as timing and never as content. */
444
+ | SpanStartEvent
445
+ | SpanEndEvent;
446
+
447
+ // Redaction helpers live in ./redact.ts and are re-exported here so every
448
+ // existing import site keeps working.
449
+ export { redactAndCap, redactSecrets, stripAnsi } from "./redact.js";
450
+ import { redactSecrets, stripAnsi } from "./redact.js";
451
+
452
+ const SUMMARY_CAP = 200;
453
+
454
+ /** One-line, control-stripped, redacted, length-capped summary of a tool's output for the run
455
+ * stream — the first non-empty line plus a size note. */
456
+ export function summarizeToolResult(output: string): string {
457
+ return summarizeClean(redactSecrets(stripAnsi(output)));
458
+ }
459
+
460
+ /** `summarizeToolResult` for text that is ALREADY control-stripped and redacted. */
461
+ function summarizeClean(clean: string): string {
462
+ const trimmed = clean.trim();
463
+ if (trimmed === "") return "(no output)";
464
+ const firstLine =
465
+ trimmed
466
+ .split("\n")
467
+ .find((l) => l.trim().length > 0)
468
+ ?.trim() ?? "";
469
+ const head = firstLine.length > SUMMARY_CAP ? `${firstLine.slice(0, SUMMARY_CAP)}…` : firstLine;
470
+ const lineCount = trimmed.split("\n").length;
471
+ const more =
472
+ trimmed.length > head.length ? ` (${trimmed.length} chars${lineCount > 1 ? `, ${lineCount} lines` : ""})` : "";
473
+ return head + more;
474
+ }
475
+
476
+ /** Every executor renders a nonzero command exit as an `exit <code>:` first line
477
+ * (`LocalExecutor.exec`, `ResidentExecutor.exec`, `CloudflareSandboxExecutor.exec`)
478
+ * so the model sees the status. This is the single reader of that contract.
479
+ * `failed` is true for any such prefix; `exitCode` is the code when numeric
480
+ * (execFile can report an errno string instead). Ordinary output — including
481
+ * text that merely mentions "exit 1:" later on — is a clean 0. */
482
+ export function parseExitPrefix(output: string): { failed: boolean; exitCode?: number } {
483
+ const m = /^\s*exit (\S+?):/.exec(stripAnsi(output));
484
+ if (!m) return { failed: false, exitCode: 0 };
485
+ return /^\d+$/.test(m[1]) ? { failed: true, exitCode: Number(m[1]) } : { failed: true };
486
+ }
487
+
488
+ /** Upper bound on a `tool_result.output` — large enough for a test run or a
489
+ * diff to read in full on the run page, small enough that a run of ordinary
490
+ * length fits the registry's backlog whole (the backlog is bounded by count AND
491
+ * bytes — `RunRegistry`, 5000 events / 4 MiB — so an output-heavy run trims its
492
+ * oldest events rather than growing without bound). */
493
+ export const TOOL_OUTPUT_CAP = 8_000;
494
+
495
+ /** Cap on a bash `tool_call.command` (the full command beside the 200-char
496
+ * `summary`). Generous — an agent's chained `checkout && add && commit && push`
497
+ * is a few hundred chars; a heredoc-fed script can be a few thousand — and
498
+ * still a small fraction of `MAX_EVENT_BYTES`. */
499
+ export const COMMAND_CAP = 4_000;
500
+
501
+ /** The full tool output as it may leave the process: control-stripped, then
502
+ * redacted, then capped (that order — see redactAndCap). Empty output → "". */
503
+ export function prepareToolOutput(output: string): string {
504
+ return capClean(redactSecrets(stripAnsi(output)));
505
+ }
506
+
507
+ /** `prepareToolOutput` for text that is ALREADY control-stripped and redacted. */
508
+ function capClean(clean: string): string {
509
+ const text = clean.trim();
510
+ if (text.length <= TOOL_OUTPUT_CAP) return text;
511
+ return `${text.slice(0, TOOL_OUTPUT_CAP)}…[${text.length - TOOL_OUTPUT_CAP} more chars]`;
512
+ }
513
+
514
+ /** Both `tool_result` display fields from ONE strip+redact pass. The redaction
515
+ * battery is ~20 regexes over up to 120k chars of raw tool output; paying it
516
+ * once per result instead of twice (summary, then output) halves the
517
+ * synchronous CPU the runner spends per tool call. Byte-identical to calling
518
+ * `summarizeToolResult` and `prepareToolOutput` separately. */
519
+ export function prepareToolResult(output: string): { summary: string; output: string } {
520
+ const clean = redactSecrets(stripAnsi(output));
521
+ return { summary: summarizeClean(clean), output: capClean(clean) };
522
+ }
523
+
524
+ /** `JSON.stringify(event)`, computed once per event object however many readers
525
+ * it has: the registry measures each event's byte size at publish, then hands
526
+ * the SAME object to every subscriber (and replays the same backlog entries),
527
+ * so with k open tabs on one run each tool_result (up to 8 KB of output) would
528
+ * otherwise be serialized k+1 times. */
529
+ const serialized = new WeakMap<object, string>();
530
+ export function serializedOnce(event: object): string {
531
+ let s = serialized.get(event);
532
+ if (s === undefined) {
533
+ s = JSON.stringify(event);
534
+ serialized.set(event, s);
535
+ }
536
+ return s;
537
+ }