@coreplane/switchboard 1.250.0 → 1.251.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/config/config.example.yaml +9 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +31 -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/core/authz/policy.ts +4 -0
- package/dist/assets/src/core/authz/resource.ts +6 -2
- package/dist/assets/src/core/authz/types.ts +2 -0
- package/dist/assets/src/core/coordinator/driver.ts +16 -2
- package/dist/assets/src/core/modelCard.ts +19 -3
- package/dist/assets/src/core/provider.ts +49 -0
- package/dist/assets/src/core/refusal.ts +3 -0
- package/dist/assets/src/core/runLedger/types.ts +12 -4
- package/dist/assets/src/core/runRecord.ts +16 -0
- package/dist/assets/src/core/ship/contract.ts +17 -4
- package/dist/assets/src/core/ship/coordinator.ts +70 -4
- package/dist/assets/web/dist/.vite/manifest.json +58 -52
- package/dist/assets/web/dist/assets/DeliveryPage-DUXd-Sl-.js +1 -0
- package/dist/assets/web/dist/assets/HomePage-mSiqEEcN.js +2 -0
- package/dist/assets/web/dist/assets/{PendingTurnRow-BuRre8it.js → PendingTurnRow-DDhMhrI7.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-Cb3sFkfj.js → ResidentDetailPage-BnEoOnGQ.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-BZ6n6UxF.js → ResidentsIndexPage-Dxpgf-l-.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CSo4-vld.js +1 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-psSMI3fN.js → RunRoutePage-9klVWhSF.js} +6 -6
- package/dist/assets/web/dist/assets/{RunsIndexPage-68YT_RWt.js → RunsIndexPage-BplMIgaw.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-CBUxbeqN.js → ScheduledPage-B_GgeJrb.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-CBTnZ9Qv.js → SettingsPage-BXX4R113.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-BnRjWzFN.js → StatusDot-BOaw8le9.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-Brge0wnd.js → Tooltip-DYZZ4l4V.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-o6sLju16.js → UnitRoutePage-BaSW5Odq.js} +1 -1
- package/dist/assets/web/dist/assets/budgets-BvWYKPsY.js +1 -0
- package/dist/assets/web/dist/assets/{dist-rgAhsmE-.js → dist-BCVXeBJ9.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-Bde9OZxG.js +1 -0
- package/dist/assets/web/dist/assets/{main-CeRuGONy.js → main-Dkcbtu3u.js} +2 -2
- package/dist/assets/web/dist/assets/sseReplay-DPwdsaok.js +9 -0
- package/dist/cli.js +975 -239
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/DeliveryPage-CIfBiINK.js +0 -1
- package/dist/assets/web/dist/assets/HomePage-AnycA57D.js +0 -2
- package/dist/assets/web/dist/assets/RunFoldRow-BRXkjkgO.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-BmK74Vp1.js +0 -1
- package/dist/assets/web/dist/assets/sseReplay-DXC7kGbN.js +0 -9
|
@@ -87,6 +87,14 @@ defaults:
|
|
|
87
87
|
# provider default. Skipped for models without effort support.
|
|
88
88
|
# efforts:
|
|
89
89
|
# coding: medium
|
|
90
|
+
# How much the bot says about its own doing (docs/reference/specs/routing-and-config.md
|
|
91
|
+
# item 28): quiet (default) — only what needs the person: answers, verdicts,
|
|
92
|
+
# refusals, questions, results, the card's progress; verbose — plus every
|
|
93
|
+
# acknowledgement (a follow-up folded into a live run, a plan handed to the
|
|
94
|
+
# runner, the workspace and budget on the card); debug — plus the router's
|
|
95
|
+
# reason and the ledger's word. Same layering as effort: per-channel/user
|
|
96
|
+
# `verbosity` and a per-request `verbosity:<level>` directive override it.
|
|
97
|
+
# verbosity: quiet
|
|
90
98
|
# A boundary caps what ANY run in a scope may have — never grants — on the
|
|
91
99
|
# three axes of a run's profile (docs/reference/specs/routing-and-config.md item 2):
|
|
92
100
|
# maxMinutes (the wall-clock budget; at least 2), maxIdentity (the credential
|
|
@@ -156,6 +164,7 @@ defaults:
|
|
|
156
164
|
# users:
|
|
157
165
|
# slack:U012345:
|
|
158
166
|
# model: openai/gpt-5
|
|
167
|
+
# verbosity: verbose # hear every acknowledgement of what the bot does for you
|
|
159
168
|
# harness:
|
|
160
169
|
# coding: opencode
|
|
161
170
|
|
|
@@ -779,6 +779,27 @@ export class ConfigDO extends DurableObject<Env> {
|
|
|
779
779
|
});
|
|
780
780
|
}
|
|
781
781
|
|
|
782
|
+
/** The thread's pending row when one exists and is inside its ttl, else
|
|
783
|
+
* null. A pure read: expiry is checked here on this object's clock and
|
|
784
|
+
* nothing is deleted — nothing sweeps, and a consume still finds the
|
|
785
|
+
* expired row to name `expired`. */
|
|
786
|
+
async pendingConfirmationByThread(threadKey: string, now: number): Promise<ConfirmationRow | null> {
|
|
787
|
+
const row = this.sql
|
|
788
|
+
.exec<{ id: string; requester: string; expires_at: number; body: string }>(
|
|
789
|
+
`SELECT id, requester, expires_at, body FROM confirmations WHERE thread_key = ?`,
|
|
790
|
+
threadKey,
|
|
791
|
+
)
|
|
792
|
+
.toArray()[0];
|
|
793
|
+
if (!row || row.expires_at <= now) return null;
|
|
794
|
+
return {
|
|
795
|
+
id: row.id,
|
|
796
|
+
threadKey,
|
|
797
|
+
requester: row.requester,
|
|
798
|
+
expiresAt: row.expires_at,
|
|
799
|
+
body: parseStored(row.body, isJsonObject) ?? {},
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
782
803
|
private readConfirmation(id: string): ConfirmationRow | undefined {
|
|
783
804
|
const row = this.sql
|
|
784
805
|
.exec<{ thread_key: string; requester: string; expires_at: number; body: string }>(
|
|
@@ -1199,6 +1220,7 @@ const CONFIG_ROUTES = new Set([
|
|
|
1199
1220
|
"/config/confirmations/consume",
|
|
1200
1221
|
"/config/confirmations/cancel",
|
|
1201
1222
|
"/config/confirmations/cancel-by-thread",
|
|
1223
|
+
"/config/confirmations/pending-by-thread",
|
|
1202
1224
|
]);
|
|
1203
1225
|
const TICKET_STATES: ReadonlySet<string> = new Set<McpTicketState>(MCP_TICKET_STATES);
|
|
1204
1226
|
/** A confirmation id as the bot mints it (a UUID) — one token, no whitespace, bounded. */
|
|
@@ -1284,6 +1306,15 @@ async function handleConfig(pathname: string, body: unknown, env: Env): Promise<
|
|
|
1284
1306
|
console.log(`[config/confirmations/cancel] ${click.id} ${"ok" in outcome ? "cancelled" : outcome.refused}`);
|
|
1285
1307
|
return json(outcome);
|
|
1286
1308
|
}
|
|
1309
|
+
case "/config/confirmations/pending-by-thread": {
|
|
1310
|
+
if (typeof b.threadKey !== "string" || !b.threadKey) return json({ error: "threadKey required" }, 400);
|
|
1311
|
+
// The stub types this result `never`: workers-types' Serializable rejects
|
|
1312
|
+
// the row's opaque `Record<string, unknown>` body. What arrives is the
|
|
1313
|
+
// object's declared result, so the boundary restates it.
|
|
1314
|
+
const row = (await dO.pendingConfirmationByThread(b.threadKey, systemClock())) as ConfirmationRow | null;
|
|
1315
|
+
console.log(`[config/confirmations/pending-by-thread] ${b.threadKey} ${row === null ? "none" : row.id}`);
|
|
1316
|
+
return json({ row });
|
|
1317
|
+
}
|
|
1287
1318
|
case "/config/confirmations/cancel-by-thread": {
|
|
1288
1319
|
if (typeof b.threadKey !== "string" || !b.threadKey) return json({ error: "threadKey required" }, 400);
|
|
1289
1320
|
if (!Array.isArray(b.actorIds) || !b.actorIds.every((a): a is string => typeof a === "string"))
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.251.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.251.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -20445,7 +20445,7 @@
|
|
|
20445
20445
|
},
|
|
20446
20446
|
"packages/switchboard": {
|
|
20447
20447
|
"name": "@coreplane/switchboard",
|
|
20448
|
-
"version": "1.
|
|
20448
|
+
"version": "1.251.0",
|
|
20449
20449
|
"license": "Apache-2.0",
|
|
20450
20450
|
"dependencies": {
|
|
20451
20451
|
"@earendil-works/pi-ai": "0.85.1",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.251.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/dist/assets/source.json
CHANGED
|
@@ -135,6 +135,10 @@ export const POLICY: readonly Rule[] = [
|
|
|
135
135
|
// scope from anywhere, a private one only from inside it, `unknown` never.
|
|
136
136
|
{ action: "config:read", resource: "config-scope", resourceKind: "channel", when: [grant("config:write")] },
|
|
137
137
|
{ action: "config:read", resource: "config-scope", resourceKind: "channel", when: [MEMBER_OF] },
|
|
138
|
+
// A THREAD's scope (`config set thread`, routing-and-config item 27): the
|
|
139
|
+
// channel-config right — whoever may set the channel may set a thread in it;
|
|
140
|
+
// never a baseline, so membership alone admits nobody.
|
|
141
|
+
{ action: "config:write", resource: "config-scope", resourceKind: "thread", when: [grant("config:write")] },
|
|
138
142
|
// A user edits only their own scope.
|
|
139
143
|
{ action: "config:write", resource: "config-scope", resourceKind: "user", when: [IS_SELF] },
|
|
140
144
|
|
|
@@ -31,14 +31,14 @@ export type AttributeName = Exclude<keyof ResourceAttributes, "visibility">;
|
|
|
31
31
|
/** The kinds each kinded resource type takes. Types absent here are not kinded. */
|
|
32
32
|
export const RESOURCE_KINDS: { readonly [T in ResourceType]?: readonly KindOf<T>[] } = {
|
|
33
33
|
"memory-scope": ["org", "user", "repo", "channel"],
|
|
34
|
-
"config-scope": ["channel", "user", "org"],
|
|
34
|
+
"config-scope": ["channel", "user", "thread", "org"],
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
/** A resource type, or `type/kind` for the kinded ones — the unit a rule row targets. */
|
|
38
38
|
export type Target =
|
|
39
39
|
| Exclude<ResourceType, "memory-scope" | "config-scope">
|
|
40
40
|
| `memory-scope/${"org" | "user" | "repo" | "channel"}`
|
|
41
|
-
| `config-scope/${"channel" | "user" | "org"}`;
|
|
41
|
+
| `config-scope/${"channel" | "user" | "thread" | "org"}`;
|
|
42
42
|
|
|
43
43
|
export const TARGET_ATTRIBUTES: Readonly<Record<Target, readonly AttributeName[]>> = {
|
|
44
44
|
run: ["channelId", "userId", "repo"],
|
|
@@ -50,6 +50,9 @@ export const TARGET_ATTRIBUTES: Readonly<Record<Target, readonly AttributeName[]
|
|
|
50
50
|
repo: ["repo"],
|
|
51
51
|
"config-scope/channel": ["channelId"],
|
|
52
52
|
"config-scope/user": ["userId"],
|
|
53
|
+
// A thread key carries no attribute a condition reads: the one row on it is
|
|
54
|
+
// a bare grant check (the channel-config right, `config set thread`).
|
|
55
|
+
"config-scope/thread": [],
|
|
53
56
|
"config-scope/org": [],
|
|
54
57
|
agent: ["name"],
|
|
55
58
|
command: [],
|
|
@@ -136,6 +139,7 @@ export function attributesOf(resource: Resource): ResourceAttributes {
|
|
|
136
139
|
return { channelId: resource.id, visibility: "unknown", channelVisibility: resource.visibility ?? "unknown" };
|
|
137
140
|
case "user":
|
|
138
141
|
return { userId: resource.id, visibility: "unknown" };
|
|
142
|
+
case "thread":
|
|
139
143
|
case "org":
|
|
140
144
|
return { visibility: "unknown" };
|
|
141
145
|
}
|
|
@@ -114,6 +114,8 @@ export type Resource =
|
|
|
114
114
|
readonly visibility?: ChannelVisibility;
|
|
115
115
|
}
|
|
116
116
|
| { readonly type: "config-scope"; readonly kind: "user"; readonly id: string }
|
|
117
|
+
/** A thread's runtime scope (`config set thread`, routing-and-config item 27); `id` is the thread key. */
|
|
118
|
+
| { readonly type: "config-scope"; readonly kind: "thread"; readonly id: string }
|
|
117
119
|
| { readonly type: "config-scope"; readonly kind: "org" }
|
|
118
120
|
| { readonly type: "agent"; readonly name: string }
|
|
119
121
|
/** List-shaped actions with no single resource (`runs.list`, `friction.report`). */
|
|
@@ -172,6 +172,8 @@ interface PlanFacts {
|
|
|
172
172
|
grantSource: GrantSource;
|
|
173
173
|
/** The instance's mark as the plan route answers it: a generated one-unit plan (a `plan` with no `path`). */
|
|
174
174
|
generated: boolean;
|
|
175
|
+
/** The runs page base the bot answered: the report links a child's write-up to its run page with it. */
|
|
176
|
+
runPageBase?: string;
|
|
175
177
|
repo: string;
|
|
176
178
|
base: string;
|
|
177
179
|
caps: ShipCaps;
|
|
@@ -217,6 +219,7 @@ function readPlan(a: BotAnswer): PlanFacts {
|
|
|
217
219
|
grantSource:
|
|
218
220
|
b.grantSource === "run" || b.grantSource === "user" || b.grantSource === "channel" ? b.grantSource : "org",
|
|
219
221
|
generated: b.generated === true,
|
|
222
|
+
...(typeof b.runPageBase === "string" && b.runPageBase.length > 0 ? { runPageBase: b.runPageBase } : {}),
|
|
220
223
|
repo: b.repo,
|
|
221
224
|
base: b.base,
|
|
222
225
|
caps: { maxRounds: b.caps.maxRounds, maxMinutes: b.caps.maxMinutes },
|
|
@@ -318,7 +321,7 @@ function isCommitChecks(v: unknown): v is { total: number; pending: string[]; fa
|
|
|
318
321
|
function prCheckReturn(step: string, a: BotAnswer): StepReturn {
|
|
319
322
|
const { ok, state, prNumber, url, headSha, sha, mergedAt, at } = a.body;
|
|
320
323
|
if (ok === true && state === "none") {
|
|
321
|
-
const { unrecovered, aheadOfBase } = a.body;
|
|
324
|
+
const { unrecovered, aheadOfBase, prClosed } = a.body;
|
|
322
325
|
return {
|
|
323
326
|
type: "pr-check",
|
|
324
327
|
step,
|
|
@@ -328,6 +331,9 @@ function prCheckReturn(step: string, a: BotAnswer): StepReturn {
|
|
|
328
331
|
// The branch's commits over the base, when the bot could read them
|
|
329
332
|
// (agent-ship item 12): zero is the `already_landed` ending's fact.
|
|
330
333
|
...(typeof aheadOfBase === "number" ? { aheadOfBase } : {}),
|
|
334
|
+
// The followed pull request verified closed unmerged (issue 1799):
|
|
335
|
+
// the machine must not brief a review round on it.
|
|
336
|
+
...(prClosed === true ? { prClosed: true } : {}),
|
|
331
337
|
},
|
|
332
338
|
at,
|
|
333
339
|
};
|
|
@@ -453,12 +459,19 @@ async function perform(
|
|
|
453
459
|
case "pr-check":
|
|
454
460
|
// `recover` rides only after a dead coding child: the bot opens the pull
|
|
455
461
|
// request from the pushed branch itself instead of answering `none`.
|
|
462
|
+
// `pr` is the machine's adopted pull request (issue 1799): the bot
|
|
463
|
+
// follows it when nothing heads the unit's branch and answers its live
|
|
464
|
+
// state instead of `none` over a minutes-old record fact.
|
|
456
465
|
return prCheckReturn(
|
|
457
466
|
action.step,
|
|
458
467
|
answerOf(
|
|
459
468
|
"pr-check",
|
|
460
469
|
await step.do(action.step, STEP_CONFIG, () =>
|
|
461
|
-
call(bot, "pr-check", {
|
|
470
|
+
call(bot, "pr-check", {
|
|
471
|
+
...tag,
|
|
472
|
+
...(action.recover !== undefined ? { recover: action.recover } : {}),
|
|
473
|
+
...(action.pr !== undefined ? { pr: action.pr } : {}),
|
|
474
|
+
}),
|
|
462
475
|
),
|
|
463
476
|
),
|
|
464
477
|
);
|
|
@@ -537,6 +550,7 @@ async function runUnit(
|
|
|
537
550
|
grant: plan.grant,
|
|
538
551
|
grantSource: plan.grantSource,
|
|
539
552
|
generated: plan.generated,
|
|
553
|
+
...(plan.runPageBase !== undefined ? { runPageBase: plan.runPageBase } : {}),
|
|
540
554
|
...(resume !== undefined ? { resume } : {}),
|
|
541
555
|
...(lastPush !== undefined ? { lastPush } : {}),
|
|
542
556
|
...(session !== undefined ? { session } : {}),
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// catalog lives in ./modelRegistry.ts.
|
|
11
11
|
|
|
12
12
|
import { EFFORT_LEVELS, type Effort } from "../effort.js";
|
|
13
|
-
import { vendorOf, wireOf, type ProviderConfig, type Wire } from "./provider.js";
|
|
13
|
+
import { billerHarnessProvider, vendorOf, wireOf, type ProviderConfig, type Wire } from "./provider.js";
|
|
14
14
|
import type { RegistryCard } from "./modelRegistry.js";
|
|
15
15
|
|
|
16
16
|
/** Which layer named a field: the operator's block, the registry card, or the
|
|
@@ -335,13 +335,29 @@ export function decideControls(card: ModelCard, asked: AskedControls): ControlDe
|
|
|
335
335
|
why: windowVouched ? "" : `no layer names the window; compacting at ${card.window}`,
|
|
336
336
|
});
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
// A `markers` rule needs a harness-side write that places the markers
|
|
339
|
+
// (record 0052's amendment: the harness write names the biller's own
|
|
340
|
+
// provider). The Anthropic wire's own packages place per-block breakpoints;
|
|
341
|
+
// on the chat wire a biller the table names caches the aggregator's own way
|
|
342
|
+
// — the pinned OpenCode binary exempts the openrouter route from per-block
|
|
343
|
+
// placement, so the write carries OpenRouter's top-level
|
|
344
|
+
// `cache_control: { type: "ephemeral" }` via `settings.extraBody` (measured
|
|
345
|
+
// in `opencode/testing/realDriver.test.ts`), and pi's compat sends the
|
|
346
|
+
// per-block markers. A biller served generically degrades, never a silent
|
|
347
|
+
// `native`.
|
|
348
|
+
const markersUnplaced =
|
|
349
|
+
card.cache === "markers" && card.wire === "openai-chat" && billerHarnessProvider(card.block) === undefined;
|
|
350
|
+
const cacheNative = card.cache !== "unknown" && !markersUnplaced;
|
|
339
351
|
decisions.push({
|
|
340
352
|
control: "cache",
|
|
341
353
|
outcome: cacheNative ? "native" : "degraded",
|
|
342
354
|
applied: card.cache,
|
|
343
355
|
vouched: cacheNative,
|
|
344
|
-
why: cacheNative
|
|
356
|
+
why: cacheNative
|
|
357
|
+
? ""
|
|
358
|
+
: markersUnplaced
|
|
359
|
+
? `no harness-side provider vouches for the "${card.block}" biller's cache markers; the rule goes out unvouched`
|
|
360
|
+
: `no layer names ${card.model}'s cache rule`,
|
|
345
361
|
});
|
|
346
362
|
|
|
347
363
|
return decisions;
|
|
@@ -209,6 +209,55 @@ export function vendorOf(
|
|
|
209
209
|
return { block, model, vendor: block, vendorId: model, vendorSource: "block" };
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/** The harness-side provider a block's biller implies (record 0052's
|
|
213
|
+
* amendment: the harness write names the biller's own provider, never a
|
|
214
|
+
* generic alias). Keyed by the biller — the block's name — for the billers
|
|
215
|
+
* whose protocol a harness bundles a provider for. The wires with a package
|
|
216
|
+
* of their own (`anthropic-messages` → `@ai-sdk/anthropic`,
|
|
217
|
+
* `openai-responses` → `@ai-sdk/openai`) need no entry: the wire names the
|
|
218
|
+
* package. A chat-wire biller not here is served generically
|
|
219
|
+
* (`@ai-sdk/openai-compatible`), under which a `markers` cache rule cannot be
|
|
220
|
+
* vouched for (`decideControls`): the generic provider places no cache
|
|
221
|
+
* breakpoints. */
|
|
222
|
+
export interface BillerHarnessProvider {
|
|
223
|
+
/** The AI SDK package OpenCode's configuration names for the biller
|
|
224
|
+
* (`openCodeProviderPackage` adds the `aisdk:` prefix). */
|
|
225
|
+
openCodePackage: string;
|
|
226
|
+
/** The compat words pi keys on the biller's identity, copied once from pi's
|
|
227
|
+
* own completions detection of that biller and never inferred from a URL at
|
|
228
|
+
* run time: through the proxy pi sees the bot's URL, so `piModelsJson` must
|
|
229
|
+
* say the words the biller's own base URL would have made pi detect. */
|
|
230
|
+
piCompat: {
|
|
231
|
+
/** How the wire spells reasoning: `reasoning: { effort }` under
|
|
232
|
+
* `"openrouter"`, never the completions shape's flat `reasoning_effort`. */
|
|
233
|
+
thinkingFormat: string;
|
|
234
|
+
/** How a session id would ride the headers, were affinity ever turned on. */
|
|
235
|
+
sessionAffinityFormat: string;
|
|
236
|
+
/** The vendor-qualified id prefixes the biller grants the developer role:
|
|
237
|
+
* any other id is told `supportsDeveloperRole: false`, as pi's own
|
|
238
|
+
* detection would say against the biller directly. */
|
|
239
|
+
developerRoleIdPrefixes: readonly string[];
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** The biller-to-provider table: one row per biller a harness speaks natively
|
|
244
|
+
* — OpenCode's package (U44) and pi's compat words (U45) side by side. */
|
|
245
|
+
export const BILLER_HARNESS_PROVIDERS: Readonly<Record<string, BillerHarnessProvider>> = {
|
|
246
|
+
openrouter: {
|
|
247
|
+
openCodePackage: "@openrouter/ai-sdk-provider",
|
|
248
|
+
piCompat: {
|
|
249
|
+
thinkingFormat: "openrouter",
|
|
250
|
+
sessionAffinityFormat: "openrouter",
|
|
251
|
+
developerRoleIdPrefixes: ["anthropic/", "openai/"],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/** The biller's harness-side provider, or undefined when it is served generically. */
|
|
257
|
+
export function billerHarnessProvider(biller: string | undefined): BillerHarnessProvider | undefined {
|
|
258
|
+
return biller === undefined ? undefined : BILLER_HARNESS_PROVIDERS[biller];
|
|
259
|
+
}
|
|
260
|
+
|
|
212
261
|
/** The env var Anthropic's own SDK reads when an `anthropic` provider block names none. */
|
|
213
262
|
export const ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY";
|
|
214
263
|
|
|
@@ -56,6 +56,8 @@ const CAUSE_OF = {
|
|
|
56
56
|
which_branch: "request",
|
|
57
57
|
workspace_lost: "system",
|
|
58
58
|
ship_budget: "request",
|
|
59
|
+
// one pipeline per thread (record 0060): the host key's claim answered thread-live
|
|
60
|
+
ship_thread_live: "request",
|
|
59
61
|
setup_failed: "system",
|
|
60
62
|
// the click on a confirmation (confirm.ts)
|
|
61
63
|
confirmation_used: "request",
|
|
@@ -106,6 +108,7 @@ const CAUSE_OF = {
|
|
|
106
108
|
directive_budget: "request",
|
|
107
109
|
directive_severity: "request",
|
|
108
110
|
directive_renewals: "request",
|
|
111
|
+
directive_verbosity: "request",
|
|
109
112
|
provider_unknown: "request",
|
|
110
113
|
// the model card's refusal (record 0052, model-proxy item 11): a control the
|
|
111
114
|
// resolved card does not take, named before any card or model call
|
|
@@ -59,6 +59,14 @@ export interface LiveRunMeta {
|
|
|
59
59
|
/** The app that relayed the request for the person (authorization.md item 14): a resume or restart keeps app ∩ person at the gates. */
|
|
60
60
|
postedBy?: string;
|
|
61
61
|
effort?: string;
|
|
62
|
+
/** A ship pipeline's parent (record 0060): claimed under the host key
|
|
63
|
+
* (`hostKey.ts`) while `threadKey` here names the thread itself, so every
|
|
64
|
+
* record, notice and rebuilt handle files by the metadata's thread and the
|
|
65
|
+
* occupancy readers that key on the ledger's key column never see it. */
|
|
66
|
+
hosted?: true;
|
|
67
|
+
/** The run's index label (the registry sets it at create): on the row so a
|
|
68
|
+
* hosted run listed from the ledger reads as its registry view does. */
|
|
69
|
+
label?: string;
|
|
62
70
|
ref?: string;
|
|
63
71
|
headSha?: string;
|
|
64
72
|
pr?: number;
|
|
@@ -96,10 +104,10 @@ export interface LiveRunMeta {
|
|
|
96
104
|
request?: Record<string, unknown>;
|
|
97
105
|
/** The router's decision when it chose the run's preset (routing-and-config
|
|
98
106
|
* item 21) — the same fields the record's `route` event carries. On the row
|
|
99
|
-
* so a resume repaints the card as it was (`
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
107
|
+
* so a resume repaints the card as it was (the `route reason:` note at
|
|
108
|
+
* debug, the parts) and a record built from the row knows it was routed
|
|
109
|
+
* without reading the events. Absent for a preset a person, a scope or the
|
|
110
|
+
* default chose. */
|
|
103
111
|
route?: {
|
|
104
112
|
preset: string;
|
|
105
113
|
reason: string;
|
|
@@ -39,6 +39,12 @@ import {
|
|
|
39
39
|
// reached `finish`.
|
|
40
40
|
export type RunStatus = "completed" | "stopped_soft" | "stopped_hard" | "failed" | "interrupted";
|
|
41
41
|
|
|
42
|
+
/** How every store-only reader renders a record still in its provisional
|
|
43
|
+
* window (run-history.md item 27's third state): not `interrupted` (a real
|
|
44
|
+
* terminal state) and not `running` (the store cannot know). One string for
|
|
45
|
+
* the CLI's `runs list`/`runs get` and the web's index row and run page. */
|
|
46
|
+
export const PROVISIONAL_LABEL = "unfinished — no finish recorded";
|
|
47
|
+
|
|
42
48
|
const RUN_STATUSES: readonly RunStatus[] = ["completed", "stopped_soft", "stopped_hard", "failed", "interrupted"];
|
|
43
49
|
|
|
44
50
|
/** Every `runs.*` id: checked before any store call. */
|
|
@@ -115,6 +121,13 @@ export interface RunRecord {
|
|
|
115
121
|
stepCount?: number;
|
|
116
122
|
schema?: number;
|
|
117
123
|
status: RunStatus;
|
|
124
|
+
/** Present on a tombstone record still in its provisional window — the
|
|
125
|
+
* start-of-run `interrupted` or the drain-deadline upgrade — before the
|
|
126
|
+
* run's final write lands. Absent on every final (finished) record. A
|
|
127
|
+
* store-only reader must render a provisional record as "unfinished — no
|
|
128
|
+
* finish recorded" rather than as `interrupted`, because the run may still
|
|
129
|
+
* be live in a registry the reader cannot see (run-history.md item 27). */
|
|
130
|
+
provisional?: true;
|
|
118
131
|
/** The failure by name, when a `failed` run has one (item 57):
|
|
119
132
|
* `policy_refusal`, the provider refused the run's model call under its
|
|
120
133
|
* usage policy. Absent on a run that did not fail, on one that failed for
|
|
@@ -944,6 +957,9 @@ export function isRunRecord(v: unknown): v is RunRecord {
|
|
|
944
957
|
return false;
|
|
945
958
|
}
|
|
946
959
|
if (!RUN_STATUSES.includes(r.status as RunStatus)) return false;
|
|
960
|
+
// A provisional tombstone carries `provisional: true`; any other value (false,
|
|
961
|
+
// a string, etc.) is a malformed record — only the presence of the flag matters.
|
|
962
|
+
if (r.provisional !== undefined && r.provisional !== true) return false;
|
|
947
963
|
if (!isFiniteNumber(r.eventCount) || !isFiniteNumber(r.storedEventCount)) return false;
|
|
948
964
|
if (typeof r.truncated !== "boolean") return false;
|
|
949
965
|
if (!Array.isArray(r.events)) return false;
|
|
@@ -439,17 +439,30 @@ 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, named one by one
|
|
443
|
+
* (agent-ship item 13; agent-coding item 9). "Its cheapest proving checks"
|
|
444
|
+
* left the choice to the child, and children chose wrong: prettier was
|
|
445
|
+
* reported clean while `format:check` was red, and hygiene imprints reached
|
|
446
|
+
* CI that `hygiene:check` would have caught locally. Naming the commands
|
|
447
|
+
* makes each gate a receipt — the exit line goes into the handoff's verified
|
|
448
|
+
* list, and a gate the child could not run is unproven, never claimed clean. */
|
|
449
|
+
export const FAST_GATES_BEFORE_PUSH =
|
|
450
|
+
"The fast gates, before every push: `npx prettier --check` on the changed files, `npm run hygiene:check`, " +
|
|
451
|
+
"`npm run specs:check`, and `tsc --noEmit` on the touched project under `NODE_OPTIONS=--max-old-space-size=6144`. " +
|
|
452
|
+
"Paste each command's exit line into the handoff's verified list; a gate you could not run goes under unproven " +
|
|
453
|
+
"and is never claimed clean.";
|
|
454
|
+
|
|
442
455
|
function renderFirstInstruction(rebase: ChildContract["rebase"]): string {
|
|
443
456
|
const branch = rebase.branch ? `\`${rebase.branch}\`` : "the unit's branch";
|
|
444
457
|
const onto = rebase.onto ? `\`${rebase.onto}\`` : "the merged parent";
|
|
445
458
|
return (
|
|
446
459
|
`Rebase ${branch} onto ${onto} before any other work — the parent unit has merged and the base has moved; ` +
|
|
447
460
|
`the only writes are your own on that branch. A conflict ends the unit: report it as the handoff and stop. ` +
|
|
448
|
-
`Push the branch as soon as the change exists and
|
|
461
|
+
`Push the branch as soon as the change exists and the fast gates pass — before the project's ` +
|
|
449
462
|
`full verification, which runs after that push with any fix as a further commit; an unpushed tree does not ` +
|
|
450
|
-
`survive the run's end. Right before each push, fetch ${onto} again and rebase
|
|
451
|
-
`you worked, so the pull request is not born conflicting. At the wind-down note,
|
|
452
|
-
`say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS}`
|
|
463
|
+
`survive the run's end. ${FAST_GATES_BEFORE_PUSH} Right before each push, fetch ${onto} again and rebase ` +
|
|
464
|
+
`once more if it moved while you worked, so the pull request is not born conflicting. At the wind-down note, ` +
|
|
465
|
+
`commit and push what compiles, say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS}`
|
|
453
466
|
);
|
|
454
467
|
}
|
|
455
468
|
|
|
@@ -453,6 +453,13 @@ export type CoordinatorAction =
|
|
|
453
453
|
* submitted description when the record holds one) instead of answering
|
|
454
454
|
* `none` over stranded work. */
|
|
455
455
|
recover?: { runId: string };
|
|
456
|
+
/** The pull request the machine has adopted (`state.pr`), when it holds
|
|
457
|
+
* one: the child may have worked that pull request's own head branch,
|
|
458
|
+
* not the unit's (issue 1799), so when nothing heads the unit's branch
|
|
459
|
+
* the bot follows this number and answers the pull request's LIVE state
|
|
460
|
+
* — open at a fresh head, merged, or closed (`prClosed`) — never `none`
|
|
461
|
+
* over a record fact written minutes earlier. */
|
|
462
|
+
pr?: number;
|
|
456
463
|
}
|
|
457
464
|
| { type: "merge"; step: string; prNumber: number; headSha: string }
|
|
458
465
|
/** Wait for the intake's checks-settled event at the approved head, bounded as the fallback. */
|
|
@@ -509,6 +516,10 @@ export type PrCheck =
|
|
|
509
516
|
* naming where the scope landed is the `already_landed` ending
|
|
510
517
|
* (agent-ship item 12); absent, the fact is unknown and never claimed. */
|
|
511
518
|
aheadOfBase?: number;
|
|
519
|
+
/** The action's followed pull request (`pr`) was verified CLOSED
|
|
520
|
+
* unmerged: the machine must not brief a review round on it. Absent when
|
|
521
|
+
* nothing was followed or the follow could not read the pull request. */
|
|
522
|
+
prClosed?: boolean;
|
|
512
523
|
}
|
|
513
524
|
| {
|
|
514
525
|
state: "open";
|
|
@@ -704,6 +715,10 @@ export interface UnitPipelineInput {
|
|
|
704
715
|
* pre-check finds the open pull request still at exactly this head, there is
|
|
705
716
|
* nothing to code and the attempt starts at the review round. */
|
|
706
717
|
lastPush?: string;
|
|
718
|
+
/** The runs page base (`<PUBLIC_BASE_URL>/runs`), the plan route's answer:
|
|
719
|
+
* the report's pointer at a child's write-up links its run page with it and
|
|
720
|
+
* names the run id without it — the machine never reads an environment. */
|
|
721
|
+
runPageBase?: string;
|
|
707
722
|
}
|
|
708
723
|
|
|
709
724
|
type Phase =
|
|
@@ -935,6 +950,9 @@ export function nextAction(s: UnitPipelineState): CoordinatorAction {
|
|
|
935
950
|
type: "pr-check",
|
|
936
951
|
step: `${roundStep(s, p.round)}/pr-check`,
|
|
937
952
|
...(p.dead !== undefined ? { recover: { runId: p.runId } } : {}),
|
|
953
|
+
// The adopted pull request rides the check so the bot can follow it
|
|
954
|
+
// when nothing heads the unit's branch (issue 1799).
|
|
955
|
+
...(s.pr !== undefined ? { pr: s.pr.number } : {}),
|
|
938
956
|
};
|
|
939
957
|
case "merge":
|
|
940
958
|
return { type: "merge", step: `${unit}/merge/${p.n}`, prNumber: p.pr.number, headSha: p.headSha };
|
|
@@ -1286,6 +1304,22 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
1286
1304
|
[roundNote(round, "aborted")],
|
|
1287
1305
|
);
|
|
1288
1306
|
}
|
|
1307
|
+
// Nothing heads the unit's branch, but the machine holds the round's pull
|
|
1308
|
+
// request (`pr_opened` off the child's record, or an earlier round's
|
|
1309
|
+
// adoption): the child worked on that pull request's own head branch, not
|
|
1310
|
+
// the unit's — a re-issued task in the thread of an existing pull request
|
|
1311
|
+
// (issue 1799). The round HAS its pull request — at round 0 and after a
|
|
1312
|
+
// findings step alike, since the same child keeps repushing that branch
|
|
1313
|
+
// through every later round — so it carries on to the (re-)review at the
|
|
1314
|
+
// head the child pushed through the shared open settle (a findings step
|
|
1315
|
+
// that repushed nothing keeps its abort, item 7), instead of the unit
|
|
1316
|
+
// ending "no pull request" or "closed out from under" over work that
|
|
1317
|
+
// stands. Never over a followed answer that VERIFIED the pull request
|
|
1318
|
+
// closed (`prClosed`): a review briefed on a closed pull request reviews
|
|
1319
|
+
// nothing, so the endings below stand — and are then truthful. Dead
|
|
1320
|
+
// children never reach here: their endings are above.
|
|
1321
|
+
if (s.pr !== undefined && pr.prClosed !== true)
|
|
1322
|
+
return roundOnOpenPr(s, phase, { prNumber: s.pr.number, url: s.pr.url });
|
|
1289
1323
|
if (round.index === 0) {
|
|
1290
1324
|
// The scope already landed (agent-ship item 12): the child's handoff
|
|
1291
1325
|
// names where, and the branch carries no commits over the base — two
|
|
@@ -1370,6 +1404,18 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
1370
1404
|
[roundNote(round, "aborted")],
|
|
1371
1405
|
);
|
|
1372
1406
|
}
|
|
1407
|
+
return roundOnOpenPr(s, phase, pr);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/** The round carried on its open pull request: the pr-check's `open` answer,
|
|
1411
|
+
* or — when nothing heads the unit's branch — the pull request the machine
|
|
1412
|
+
* already holds (issue 1799), at the head the child pushed. */
|
|
1413
|
+
function roundOnOpenPr(
|
|
1414
|
+
s: UnitPipelineState,
|
|
1415
|
+
phase: Extract<Phase, { at: "pr-check" }>,
|
|
1416
|
+
pr: { prNumber: number; url: string; headSha?: string },
|
|
1417
|
+
): Transition {
|
|
1418
|
+
const { round } = phase;
|
|
1373
1419
|
const head = pr.headSha ?? phase.childHead;
|
|
1374
1420
|
const next: UnitPipelineState = { ...s, pr: { number: pr.prNumber, url: pr.url } };
|
|
1375
1421
|
if (round.kind === "findings") {
|
|
@@ -1647,6 +1693,18 @@ function dispositionFor(s: UnitPipelineState, finding: Finding, round: number):
|
|
|
1647
1693
|
return undefined;
|
|
1648
1694
|
}
|
|
1649
1695
|
|
|
1696
|
+
/** Where a child's write-up lives, for the report's pointer (agent-ship item
|
|
1697
|
+
* 12): the child's own message in the thread is the single copy of the detail
|
|
1698
|
+
* — posted moments before the unit-end — so the report points at it instead
|
|
1699
|
+
* of repeating it: the run page when the plan named the base, the run id
|
|
1700
|
+
* otherwise. Undefined when no run is known to point at. */
|
|
1701
|
+
function writeUpPointer(s: UnitPipelineState, kind: RoundKind, runId: string | undefined): string | undefined {
|
|
1702
|
+
if (runId === undefined) return undefined;
|
|
1703
|
+
const base = s.input.runPageBase;
|
|
1704
|
+
const at = base !== undefined ? `${base.replace(/\/+$/, "")}/${encodeURIComponent(runId)}` : `run ${runId}`;
|
|
1705
|
+
return `The ${presetOf(kind)} child's write-up is its own message above in this thread — the single copy of the detail (${at}).`;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1650
1708
|
/** How the budget went, in the card's words: coding, review, waiting minutes. */
|
|
1651
1709
|
function budgetSplitLine(spent: ShipBudgetSpent, maxMinutes: number): string {
|
|
1652
1710
|
const min = (ms: number) => Math.round(ms / MIN);
|
|
@@ -1815,7 +1873,11 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1815
1873
|
case "stopped":
|
|
1816
1874
|
return join([
|
|
1817
1875
|
`${e.mode === "hard" ? "⛔" : "⏹"} Ship stopped by operator (${e.mode} stop) after ${rounds}.${prLine}`,
|
|
1818
|
-
|
|
1876
|
+
writeUpPointer(
|
|
1877
|
+
s,
|
|
1878
|
+
e.round.kind,
|
|
1879
|
+
e.round.kind === "review" ? s.reviewRunByRound[e.round.index] : s.lastCodingRunId,
|
|
1880
|
+
),
|
|
1819
1881
|
e.postedReview
|
|
1820
1882
|
? "ℹ️ A changes-requested review was posted this round before the stop — its findings stand on the PR."
|
|
1821
1883
|
: undefined,
|
|
@@ -1823,22 +1885,26 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1823
1885
|
]);
|
|
1824
1886
|
case "aborted":
|
|
1825
1887
|
return join([
|
|
1826
|
-
e.finalReply,
|
|
1827
1888
|
e.reason,
|
|
1889
|
+
writeUpPointer(
|
|
1890
|
+
s,
|
|
1891
|
+
e.round?.kind ?? "coding",
|
|
1892
|
+
e.round?.kind === "review" ? s.reviewRunByRound[e.round.index] : s.lastCodingRunId,
|
|
1893
|
+
),
|
|
1828
1894
|
e.renewal !== undefined ? `🔁 Not renewed: ${e.renewal.line}.` : undefined,
|
|
1829
1895
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
1830
1896
|
reissue,
|
|
1831
1897
|
]);
|
|
1832
1898
|
case "continued":
|
|
1833
1899
|
return join([
|
|
1834
|
-
e.
|
|
1900
|
+
writeUpPointer(s, e.round.kind, e.runId),
|
|
1835
1901
|
`🔁 Segment ${e.segment - 1} ended at its lease with the unit unfinished — ${e.line}. Segment ${e.segment} opens in this thread${e.from !== undefined ? ` from \`${e.from.slice(0, 7)}\`` : ""} under a fresh ${s.input.caps.maxMinutes}-minute lease, with this segment's write-up as its request; ${e.renewalsLeft} renewal${e.renewalsLeft === 1 ? "" : "s"} remain${e.spendUsd !== null ? `, $${e.spendUsd.toFixed(2)} spent so far` : ""}.`,
|
|
1836
1902
|
budgetSplitLine(e.spent, s.input.caps.maxMinutes),
|
|
1837
1903
|
]);
|
|
1838
1904
|
case "no_verdict":
|
|
1839
1905
|
return join([
|
|
1840
1906
|
`⚠️ Review round ${e.round.index} ended without a submitted verdict (budget, refusal, or stop) — ship never converts that into a request for changes, so no findings step ran.`,
|
|
1841
|
-
e.
|
|
1907
|
+
writeUpPointer(s, e.round.kind, s.reviewRunByRound[e.round.index]),
|
|
1842
1908
|
`⚠️ Ship aborted after ${rounds}.`,
|
|
1843
1909
|
reissue,
|
|
1844
1910
|
]);
|