@coreplane/switchboard 1.256.0 → 1.258.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 +14 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +340 -7
- package/dist/assets/deploy/cloudflare-resident/drain.ts +49 -4
- package/dist/assets/deploy/cloudflare-resident/worker.ts +52 -4
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +5 -3
- package/dist/assets/project.json +17 -9
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +65 -2
- package/dist/assets/src/core/authz/grants.ts +12 -0
- package/dist/assets/src/core/authz/policy.ts +15 -0
- package/dist/assets/src/core/budgets.ts +66 -18
- package/dist/assets/src/core/coordinator/contract.ts +45 -5
- package/dist/assets/src/core/coordinator/driver.ts +6 -3
- package/dist/assets/src/core/delivery.ts +8 -5
- package/dist/assets/src/core/pipelineStanding.ts +2 -0
- package/dist/assets/src/core/plane/decide.ts +477 -12
- package/dist/assets/src/core/plane/findings.ts +120 -0
- package/dist/assets/src/core/runEvents.ts +54 -14
- package/dist/assets/src/core/runFriction.ts +16 -0
- package/dist/assets/src/core/runLedger/types.ts +16 -3
- package/dist/assets/src/core/runRecord.ts +12 -0
- package/dist/assets/src/core/ship/contract.ts +14 -2
- package/dist/assets/src/core/ship/coordinator.ts +179 -51
- package/dist/assets/src/core/ship/renewal.ts +23 -20
- package/dist/assets/src/core/trace/attrs.ts +1 -1
- package/dist/assets/src/core/untrusted.ts +35 -0
- package/dist/assets/src/execution/residentCredentials.ts +7 -4
- package/dist/assets/web/dist/.vite/manifest.json +97 -72
- package/dist/assets/web/dist/assets/{DeliveryPage-BBAyLwPq.js → DeliveryPage-DadntjSp.js} +1 -1
- package/dist/assets/web/dist/assets/HomePage-BEEpTnVT.js +1 -0
- package/dist/assets/web/dist/assets/{PendingTurnRow-DGINv9XT.js → PendingTurnRow-DBPMLVsm.js} +1 -1
- package/dist/assets/web/dist/assets/PlanePage-DLusMGQ0.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-jYhEeeyu.js → ResidentDetailPage-CPnHnrxy.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-DvFmlV4U.js → ResidentsIndexPage-C9hASwHL.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-OPe7Z0Cz.js +1 -0
- package/dist/assets/web/dist/assets/{RunRoutePage-CvZ-TOT3.js → RunRoutePage-o1xzkTGZ.js} +4 -4
- package/dist/assets/web/dist/assets/RunsIndexPage-0YAClYQh.js +1 -0
- package/dist/assets/web/dist/assets/{ScheduledPage-Viim-bus.js → ScheduledPage-BIdfC0V3.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-CYUy8McC.js → SettingsPage-CnqWAjl3.js} +1 -1
- package/dist/assets/web/dist/assets/SilentTurn-DGNg0sIZ.js +2 -0
- package/dist/assets/web/dist/assets/SlackMark-CtiHrNjr.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-Dv6UMaPy.js → StatusDot-D_D_iJS8.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-Bd-5Rypv.js → Tooltip-DVFmM7on.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-7fAXFoB_.js +1 -0
- package/dist/assets/web/dist/assets/budgets-BRLm6oID.js +1 -0
- package/dist/assets/web/dist/assets/{dist-Nl3uaxrP.js → dist-C1VX-_mm.js} +1 -1
- package/dist/assets/web/dist/assets/durationTone-CVpX_yIk.js +1 -0
- package/dist/assets/web/dist/assets/{indexRow-DborJPFp.js → indexRow-BO_GIdF6.js} +1 -1
- package/dist/assets/web/dist/assets/main-Cl7xHG34.js +28 -0
- package/dist/assets/web/dist/assets/main-DGC6WFqS.css +1 -0
- package/dist/assets/web/dist/assets/{sseReplay-DE6wv1Ua.js → sseReplay-BjlKmQsY.js} +1 -1
- package/dist/cli.js +3096 -1136
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/HomePage-BEiStFwW.js +0 -3
- package/dist/assets/web/dist/assets/PlanePage-JEj-lqgz.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-DvWzR1JQ.js +0 -1
- package/dist/assets/web/dist/assets/RunsIndexPage-JqVSDOok.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-B81wEnKN.js +0 -1
- package/dist/assets/web/dist/assets/budgets-c1eumrqD.js +0 -1
- package/dist/assets/web/dist/assets/durationTone-U_rOqo6r.js +0 -1
- package/dist/assets/web/dist/assets/main-C4QfwybO.css +0 -1
- package/dist/assets/web/dist/assets/main-DRxWlffc.js +0 -28
|
@@ -350,6 +350,7 @@ import {
|
|
|
350
350
|
liveDrain,
|
|
351
351
|
parseDrainRequest,
|
|
352
352
|
reportImageCurrent,
|
|
353
|
+
staleHolds,
|
|
353
354
|
type DrainRecord,
|
|
354
355
|
} from "./drain";
|
|
355
356
|
import {
|
|
@@ -1604,9 +1605,15 @@ export class ResidentRegistryDO extends DurableObject<Env> {
|
|
|
1604
1605
|
* the new image (issue 1931): hold the drain for each — the fleet must not
|
|
1605
1606
|
* reopen onto them until they report. No live drain, nothing to hold. */
|
|
1606
1607
|
async holdDrainFor(resources: string[]): Promise<void> {
|
|
1607
|
-
const
|
|
1608
|
+
const now = systemClock();
|
|
1609
|
+
const record = liveDrain(await this.ctx.storage.get(DRAIN_KEY), now);
|
|
1608
1610
|
if (record === null || resources.length === 0) return;
|
|
1609
|
-
|
|
1611
|
+
const held = holdDrain(record, resources, now);
|
|
1612
|
+
await this.ctx.storage.put(DRAIN_KEY, held);
|
|
1613
|
+
// The hold's liveness alarm (issue 2044): the cycle bound is the earlier
|
|
1614
|
+
// end, so the alarm fires there — reopening the fleet with the stale
|
|
1615
|
+
// containers named — instead of at `until`, the last resort.
|
|
1616
|
+
if (held.holdsUntil !== undefined) await this.ctx.storage.setAlarm(Date.parse(held.holdsUntil));
|
|
1610
1617
|
}
|
|
1611
1618
|
|
|
1612
1619
|
/** One resident's word that its running container is on the deploy's image:
|
|
@@ -1635,11 +1642,25 @@ export class ResidentRegistryDO extends DurableObject<Env> {
|
|
|
1635
1642
|
const stored = await this.ctx.storage.get(DRAIN_KEY);
|
|
1636
1643
|
if (stored === undefined) return;
|
|
1637
1644
|
if (liveDrain(stored, now) === null) {
|
|
1645
|
+
// The hold's liveness (issue 2044): a reopen past the cycle bound names
|
|
1646
|
+
// the containers whose post-deploy cycle never landed — a warning, never
|
|
1647
|
+
// a silence; each restarts on its own next quiet attach or refresh.
|
|
1648
|
+
const stale = staleHolds(stored, now);
|
|
1649
|
+
if (stale !== null)
|
|
1650
|
+
console.log(
|
|
1651
|
+
`[drain] fleet reopened with ${stale.join(", ")} still on the pre-deploy image — the post-deploy cycle did not land within its bound; a stale container restarts on its next quiet attach or refresh`,
|
|
1652
|
+
);
|
|
1638
1653
|
await this.ctx.storage.delete(DRAIN_KEY);
|
|
1639
1654
|
await this.pushDrainPost("below");
|
|
1640
1655
|
} else {
|
|
1641
|
-
// Replaced with a later end under an already-armed alarm: re-arm at
|
|
1642
|
-
|
|
1656
|
+
// Replaced with a later end under an already-armed alarm: re-arm at the
|
|
1657
|
+
// record's own earlier end — the hold's cycle bound when one stands.
|
|
1658
|
+
const record = stored as DrainRecord;
|
|
1659
|
+
const ends = [
|
|
1660
|
+
Date.parse(record.until),
|
|
1661
|
+
...(record.holdsUntil !== undefined ? [Date.parse(record.holdsUntil)] : []),
|
|
1662
|
+
];
|
|
1663
|
+
await this.ctx.storage.setAlarm(Math.min(...ends.filter(Number.isFinite)));
|
|
1643
1664
|
}
|
|
1644
1665
|
}
|
|
1645
1666
|
|
|
@@ -3261,6 +3282,14 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3261
3282
|
await this.writeDiskMarkers({ ready: sha, depsKey: lockfileHash, builtSha: sha });
|
|
3262
3283
|
this.deleteSchedules(PROVISIONING_CALLBACK);
|
|
3263
3284
|
await this.setResidentState("warm");
|
|
3285
|
+
// A fresh provision runs on the image the deployed Worker pins, so it IS
|
|
3286
|
+
// the cycle a held drain waits for (issue 2044): report it — a rebuild
|
|
3287
|
+
// mid-drain reopens the fleet instead of leaving the hold standing on a
|
|
3288
|
+
// report `reconcileImage` would never send for a container it never saw
|
|
3289
|
+
// as stale. The marker is cleared with the report; a failure keeps both
|
|
3290
|
+
// for the next reconcile, and the hold's cycle bound is the backstop.
|
|
3291
|
+
await this.ctx.storage.put(IMAGE_REPORT_PENDING_KEY, { resource });
|
|
3292
|
+
await this.reportPendingImageCurrent("provision");
|
|
3264
3293
|
// The first refresh instance is the cron's: the row now reads `warm`
|
|
3265
3294
|
// with no instance recorded, so the next firing creates it (item 9).
|
|
3266
3295
|
} catch (err) {
|
|
@@ -7597,6 +7626,19 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7597
7626
|
}
|
|
7598
7627
|
|
|
7599
7628
|
private async runOpTraced(op: "test" | "build", refArg: string | null, t0: number): Promise<OpRunOk | ThreadErr> {
|
|
7629
|
+
// The fleet drain (item 69; issue 2044): a typed op is a new piece of work
|
|
7630
|
+
// like a new run's attach, and during the incident one waited silently at
|
|
7631
|
+
// the drain — so it is answered with the drain's own record at once, a
|
|
7632
|
+
// deterministic refusal the command surfaces as its reason, never a wait.
|
|
7633
|
+
const drain = await this.fleetDrain();
|
|
7634
|
+
if (drain !== null) {
|
|
7635
|
+
return {
|
|
7636
|
+
error: `op-refused: the resident fleet is drained for ${drain.reason} (asked by ${drain.by}, ends by ${drain.until}) — re-run the command when the fleet reopens`,
|
|
7637
|
+
status: 503,
|
|
7638
|
+
reason: "draining",
|
|
7639
|
+
cause: "system",
|
|
7640
|
+
};
|
|
7641
|
+
}
|
|
7600
7642
|
try {
|
|
7601
7643
|
await this.ensureHydrated();
|
|
7602
7644
|
} catch (err) {
|
|
@@ -8070,6 +8112,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
8070
8112
|
AUTO_REBUILDS_KEY,
|
|
8071
8113
|
INFRA_STREAK_KEY,
|
|
8072
8114
|
MEMORY_KEY,
|
|
8115
|
+
IMAGE_REPORT_PENDING_KEY,
|
|
8073
8116
|
]);
|
|
8074
8117
|
const facts = map.get(FACTS_KEY) as RepoFacts | undefined;
|
|
8075
8118
|
const snap = map.get(SNAPSHOT_KEY) as SnapshotRecord | undefined;
|
|
@@ -8147,6 +8190,11 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
8147
8190
|
inFlight: this.inFlightCount() + registeredRuns,
|
|
8148
8191
|
// The runs alone (no refresh cycle): what the deploy preflight refuses on.
|
|
8149
8192
|
runsInFlight: this.runsInFlightCount() + registeredRuns,
|
|
8193
|
+
// Issue 2044: whether this container's new-image report is still owed —
|
|
8194
|
+
// `pending` names a resident a held drain waits on, so `repo list` and
|
|
8195
|
+
// the deploy's readers never print a bare "warm" for a repository whose
|
|
8196
|
+
// running container the last deploy could not verify.
|
|
8197
|
+
imageReport: map.get(IMAGE_REPORT_PENDING_KEY) !== undefined ? "pending" : "current",
|
|
8150
8198
|
// Item 22: who holds what, as the rows say — the mirror mutex and the
|
|
8151
8199
|
// cycle/hydration leases, each judged against this incarnation.
|
|
8152
8200
|
incarnation: this.incarnation,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.258.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.258.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.258.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.258.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",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"cli": "tsx src/cli.ts",
|
|
40
40
|
"verify": "npm run verify:root && npm run verify --workspaces --if-present && npm run check:site",
|
|
41
41
|
"verify:root": "npm run check:consistency && npm run typecheck && npm run lint && npm run format:check && npm test && npm run check:dist",
|
|
42
|
-
"check:consistency": "npm run check:lockfile && npm run check:sandbox-pair && npm run skills:check && npm run check:deps-drift && npm run licenses:check && npm run docs:check && npm run pr-title:check && npm run specs:check && npm run decisions:check && npm run hygiene:check && npm run check:project-facts && npm run check:registry-drift && npm run agents:check && npm run clock:check && npm run screenshots:check",
|
|
42
|
+
"check:consistency": "npm run check:lockfile && npm run check:sandbox-pair && npm run skills:check && npm run check:deps-drift && npm run licenses:check && npm run docs:check && npm run pr-title:check && npm run specs:check && npm run decisions:check && npm run hygiene:check && npm run vocabulary:check && npm run check:project-facts && npm run check:registry-drift && npm run agents:check && npm run clock:check && npm run screenshots:check",
|
|
43
43
|
"ci:gate": "node scripts/ci-gate.mjs",
|
|
44
|
-
"fix": "npm run docs:gen && npm run pr-title:gen && npm run agents:gen && npm run clock:gen && npm run hygiene:gen && npm run deploy:gen && npm run skills:sync && npm run lint:fix && npm run format",
|
|
44
|
+
"fix": "npm run docs:gen && npm run pr-title:gen && npm run agents:gen && npm run clock:gen && npm run hygiene:gen && npm run vocabulary:gen && npm run deploy:gen && npm run skills:sync && npm run lint:fix && npm run format",
|
|
45
45
|
"deploy:gen": "npm run --silent cli -- deploy init",
|
|
46
46
|
"deploy:check": "npm run --silent cli -- deploy init --check",
|
|
47
47
|
"check:lockfile": "node scripts/check-lockfile.mjs",
|
|
@@ -73,6 +73,8 @@
|
|
|
73
73
|
"decisions:check": "tsx scripts/decisions-check.ts",
|
|
74
74
|
"hygiene:check": "node scripts/public-hygiene.mjs",
|
|
75
75
|
"hygiene:gen": "node scripts/public-hygiene.mjs --write",
|
|
76
|
+
"vocabulary:check": "node scripts/vocabulary-check.mjs",
|
|
77
|
+
"vocabulary:gen": "node scripts/vocabulary-check.mjs --write",
|
|
76
78
|
"docs:changed": "node scripts/docs-changed.mjs",
|
|
77
79
|
"deploy:targets": "node scripts/deploy-targets.mjs",
|
|
78
80
|
"docs:dev": "npm run dev -w docs --",
|
package/dist/assets/project.json
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"commands": {
|
|
33
33
|
"$comment": "What each root npm script does and when to run it — the source of the Commands table in AGENTS.md (npm run agents:gen). A script without an entry fails agents:check, so a new script cannot ship undocumented.",
|
|
34
34
|
"verify": {
|
|
35
|
-
"does": "The whole gate: every
|
|
35
|
+
"does": "The whole gate: every check, every workspace — exactly what CI runs.",
|
|
36
36
|
"when": "Local sessions, before requesting review. ~4 min. A pipeline agent leaves it to CI."
|
|
37
37
|
},
|
|
38
38
|
"verify:root": {
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"when": "When only the bot changed."
|
|
41
41
|
},
|
|
42
42
|
"check:consistency": {
|
|
43
|
-
"does": "The sub-second checks that generated and declared things equal the code
|
|
43
|
+
"does": "The sub-second checks that generated and declared things equal the code, this table included.",
|
|
44
44
|
"when": "After touching a generated or declared artifact; one CI leg."
|
|
45
45
|
},
|
|
46
46
|
"test": {
|
|
47
|
-
"does": "The whole vitest suite
|
|
47
|
+
"does": "The whole vitest suite from one entry, after `deploy:gen`.",
|
|
48
48
|
"when": "Local sessions, before pushing; a pipeline agent runs the changed set — the full suite is CI's."
|
|
49
49
|
},
|
|
50
50
|
"typecheck": {
|
|
@@ -160,21 +160,29 @@
|
|
|
160
160
|
"when": "After renaming a test or editing a spec; `-- --fix` makes truncated titles explicit."
|
|
161
161
|
},
|
|
162
162
|
"specs:coverage": {
|
|
163
|
-
"does": "Maps a change's paths to the specs whose
|
|
163
|
+
"does": "Maps a change's paths to the specs whose headers cover them, then lists paths no spec covers.",
|
|
164
164
|
"when": "`-- --changed origin/main...HEAD [--test-guard]` before review; `-- --require` fails on an uncovered path; `-- --json` for machines."
|
|
165
165
|
},
|
|
166
166
|
"decisions:check": {
|
|
167
|
-
"does": "Every record under `docs/decisions/` and `docs/plans/` has a valid `status`, a superseded one names its successor, an accepted body only gains
|
|
168
|
-
"when": "Part of `check:consistency`; a failing record is superseded or amended
|
|
167
|
+
"does": "Every record under `docs/decisions/` and `docs/plans/` has a valid `status`, a superseded one names its successor, an accepted body only gains `## Amended`.",
|
|
168
|
+
"when": "Part of `check:consistency`; a failing record is superseded or amended, never edited."
|
|
169
169
|
},
|
|
170
170
|
"hygiene:check": {
|
|
171
171
|
"does": "The public tree's imprint (company, people, trackers, plan ids, ids, dates) equals the recorded list, which only shrinks.",
|
|
172
|
-
"when": "Part of `check:consistency`. New hit: rewrite the line or allow it
|
|
172
|
+
"when": "Part of `check:consistency`. New hit: rewrite the line or allow it by name."
|
|
173
173
|
},
|
|
174
174
|
"hygiene:gen": {
|
|
175
175
|
"does": "Records the tree's remaining imprint after a scrub; refuses growth unless `-- --force`.",
|
|
176
176
|
"when": "Part of `fix`; new imprint fails it like `hygiene:check`."
|
|
177
177
|
},
|
|
178
|
+
"vocabulary:check": {
|
|
179
|
+
"does": "No internal word prints on a user surface; the baseline only shrinks.",
|
|
180
|
+
"when": "Part of `check:consistency`; a hit is rewritten in the user's nouns."
|
|
181
|
+
},
|
|
182
|
+
"vocabulary:gen": {
|
|
183
|
+
"does": "Records the remaining internal words; refuses growth unless `-- --force`.",
|
|
184
|
+
"when": "Part of `fix`."
|
|
185
|
+
},
|
|
178
186
|
"agents:gen": {
|
|
179
187
|
"does": "Writes the Commands table in AGENTS.md from `package.json` and this file.",
|
|
180
188
|
"when": "After adding or changing a script; part of `fix`."
|
|
@@ -220,8 +228,8 @@
|
|
|
220
228
|
"when": "After a `web/` change."
|
|
221
229
|
},
|
|
222
230
|
"screenshots:gen": {
|
|
223
|
-
"does": "Renders the dashboard screenshots whose inputs changed, both themes, recording
|
|
224
|
-
"when": "After a `web/` or fixture change
|
|
231
|
+
"does": "Renders the dashboard screenshots whose inputs changed, both themes, recording input hashes in the manifest (`--force`: all).",
|
|
232
|
+
"when": "After a `web/` or fixture change; needs `npx playwright-core install chromium`, so not in `fix`."
|
|
225
233
|
},
|
|
226
234
|
"screenshots:check": {
|
|
227
235
|
"does": "Each surface's inputs still hash to what its screenshots were rendered from — no browser.",
|
package/dist/assets/source.json
CHANGED
|
@@ -74,7 +74,7 @@ export interface AgentDef {
|
|
|
74
74
|
system: string;
|
|
75
75
|
/** key into TOOLSETS (src/tools/toolsets.ts): the tools the bot relays to
|
|
76
76
|
* the preset's pi; pi's own workspace tools follow `identity`. */
|
|
77
|
-
toolset: "full" | "readonly" | "web" | "assistant" | "explore" | "conductor" | "none";
|
|
77
|
+
toolset: "full" | "readonly" | "web" | "assistant" | "explore" | "conductor" | "orchestrator" | "none";
|
|
78
78
|
/** The runaway guard, not a budget: `runawayTurnCap(maxMinutes)` for every
|
|
79
79
|
* preset that runs the loop (`loopBudget`). The wall clock below is the
|
|
80
80
|
* budget; a run that reaches this cap first was pacing like a loop, and its
|
|
@@ -279,6 +279,23 @@ export const FAST_GATES_BEFORE_PUSH =
|
|
|
279
279
|
"CI's gate — `npm run verify` runs there on your push, never here: push a head early and let CI judge it, " +
|
|
280
280
|
"fixing forward with further commits and pushes.";
|
|
281
281
|
|
|
282
|
+
// Every coding prompt carries this verbatim, right after the fast gates
|
|
283
|
+
// (agent-coding item 13; record 0071 mechanism one, issue 1747): the rebase
|
|
284
|
+
// before every push, always and never configurable. Approved pull requests
|
|
285
|
+
// went stale behind sibling merges and each needed a hand-posted rebase; a
|
|
286
|
+
// rebase against a base the run just fetched, with the change's context still
|
|
287
|
+
// in its window, is the cheapest rebase the system will ever run, so it is
|
|
288
|
+
// unconditional. One constant, one text: the three coding variants carry the
|
|
289
|
+
// same bytes — a ship fix round's child runs these same prompts — and the ship
|
|
290
|
+
// contract's first instruction points here instead of re-stating it
|
|
291
|
+
// (src/core/ship/contract.ts).
|
|
292
|
+
export const REBASE_BEFORE_PUSH =
|
|
293
|
+
"REBASE BEFORE EVERY PUSH — always, not configurable. Immediately before each push: fetch your base branch, " +
|
|
294
|
+
"rebase your branch onto it, resolve any conflict with the context you already have (the repository's " +
|
|
295
|
+
"AGENTS.md says how a generated file is regenerated — regenerate it, never hand-merge it), re-run THE FAST " +
|
|
296
|
+
"GATES on the rebased tree, and only then push. Every head that reaches review is then current with its " +
|
|
297
|
+
"base when it lands, and no unit ends merge-ready behind a sibling that merged first.";
|
|
298
|
+
|
|
282
299
|
const CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
|
|
283
300
|
|
|
284
301
|
You work inside a dedicated workspace directory with bash, read_file, and write_file tools. ${SANDBOX_TOOLCHAIN}
|
|
@@ -303,6 +320,8 @@ ${CHECKS_BY_COST}
|
|
|
303
320
|
|
|
304
321
|
${FAST_GATES_BEFORE_PUSH}
|
|
305
322
|
|
|
323
|
+
${REBASE_BEFORE_PUSH}
|
|
324
|
+
|
|
306
325
|
${NEVER_MERGE}
|
|
307
326
|
|
|
308
327
|
${UNIT_CONTRACT}
|
|
@@ -352,6 +371,8 @@ ${CHECKS_BY_COST}
|
|
|
352
371
|
|
|
353
372
|
${FAST_GATES_BEFORE_PUSH}
|
|
354
373
|
|
|
374
|
+
${REBASE_BEFORE_PUSH}
|
|
375
|
+
|
|
355
376
|
${NEVER_MERGE}
|
|
356
377
|
|
|
357
378
|
${UNIT_CONTRACT}
|
|
@@ -397,6 +418,8 @@ ${CHECKS_BY_COST}
|
|
|
397
418
|
|
|
398
419
|
${FAST_GATES_BEFORE_PUSH}
|
|
399
420
|
|
|
421
|
+
${REBASE_BEFORE_PUSH}
|
|
422
|
+
|
|
400
423
|
${NEVER_MERGE}
|
|
401
424
|
|
|
402
425
|
${UNIT_CONTRACT}
|
|
@@ -589,7 +612,7 @@ Your tools work without a workspace: the GitHub tools — \`github_repos\` (the
|
|
|
589
612
|
|
|
590
613
|
${statusCardRule('"Read the issue and its thread", "Post the comment"')} A one-step answer needs no checklist; post one when the request has steps the person would wait on.
|
|
591
614
|
|
|
592
|
-
You cannot run commands, clone repositories, edit code, or review pull requests, and you cannot search the web — and you cannot hand off, route, forward or start another run: never say you will hand off, route, forward or start anything, because you cannot start another agent's run. Other Switchboard agents can do those things, and a plain message reaches them by itself
|
|
615
|
+
You cannot run commands, clone repositories, edit code, or review pull requests, and you cannot search the web — and you cannot hand off, route, forward or start another run: never say you will hand off, route, forward or start anything, because you cannot start another agent's run. Other Switchboard agents can do those things, and a plain message reaches them by itself: Switchboard's door reads every message — a reply in this thread included — and starts the right agent from the person's own words. For a code change or a pull request, say what you found and that the change is not yours to make, and that asking for it in plain words — right here in the thread or anywhere in the channel, like "in acme/api: fix the failing login test" — starts the agent that makes the change, opens the PR and loops review; the same for a PR review ("review <PR URL>") and a web-research question ("compare X and Y on the web"). NEVER tell the person to post a new top-level message, re-post, re-ask elsewhere or retype their request — their words have already been said and the door reads thread replies; and never hand back a command or an \`agent:…\` line to type: describe the ask in their words. Delete an issue only when the user explicitly asked to delete it (closing is an update).`;
|
|
593
616
|
|
|
594
617
|
// The explore agent (docs/reference/specs/agent-explore.md): a long, read-only
|
|
595
618
|
// investigation — "run our CI locally and validate the claims", "how long does
|
|
@@ -624,6 +647,32 @@ ${BREVITY_RULE}
|
|
|
624
647
|
${FENCED_CONTENT_RULE}
|
|
625
648
|
Report outcomes faithfully: a check you could not run is "could not check", never a guess. Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks — render the claim table as aligned rows inside a code block). Your final message is posted to Slack: lead with the overall verdict in one line, then the claim table, then what a follow-up should do.`;
|
|
626
649
|
|
|
650
|
+
// The orchestrator (record 0070; docs/reference/specs/orchestration-plane.md
|
|
651
|
+
// item 11): the plane's chat preset — one long-lived thread per person beside
|
|
652
|
+
// the panels, and `agent:orchestrator` from any channel a grant admits. It
|
|
653
|
+
// reads the fleet from the plane's projections (the `plane_show` tool answers
|
|
654
|
+
// the SAME rows the panels paint) and never from its own context: every fleet
|
|
655
|
+
// fact cites the row it read, and a question the tables cannot answer says so
|
|
656
|
+
// instead of recalling. Machine `none`, identity `none`: no workspace, no
|
|
657
|
+
// shell, no credential — the read tool set is its whole reach, and its writes
|
|
658
|
+
// are the registry's fenced commands through the door (record 0070's later
|
|
659
|
+
// units), never a tool here. The prose speaks record 0066's twelve nouns:
|
|
660
|
+
// thread, run, agent, pipeline, unit, round, budget, follow-up, verdict,
|
|
661
|
+
// outcome, card, pull request — no thirteenth noun rides in.
|
|
662
|
+
const ORCHESTRATOR_SYSTEM = `You are Switchboard's orchestrator: the control plane's chat, answering a person about their fleet — every thread, run, agent, pipeline, unit, round, budget, follow-up, verdict, outcome, card and pull request the plane's tables carry.
|
|
663
|
+
|
|
664
|
+
You have no workspace and no shell. Your tools: \`plane_show\` (the plane's live tables — every live and recently ended run, every unit and every tracked pull request, each with its owner and its health: the same rows the /plane panels paint, seen as the person who asked may see them), \`recall\` and \`notes\` (this thread's log and your notepad), and \`update_status\`.
|
|
665
|
+
|
|
666
|
+
EVERY FLEET FACT COMES FROM THE TABLES, NEVER FROM MEMORY. Before answering any question about the fleet — what is running, which unit is idle, which pull request is merge-ready and nobody's — read the tables with \`plane_show\` and cite the row you read: name the run id, the unit or the pull request the answer stands on, so the person can check it against the panel beside you. Never answer a standing question from this conversation's earlier turns or your own context: an earlier turn is history, the table is now. A question the tables cannot answer — a why the rows do not carry, a fact outside the fleet — is answered "the tables do not say", plainly, instead of recalling or guessing; say what the rows do show and where the answer would live.
|
|
667
|
+
|
|
668
|
+
You cannot run commands, edit code, merge or stop anything from here: acting on the fleet goes through Switchboard's own commands, which the person's own words reach through the door under their own grants — say what you found and let their next sentence act.
|
|
669
|
+
|
|
670
|
+
${statusCardRule('"Read the plane tables", "Check the run\'s standing"')} A one-read answer needs no checklist.
|
|
671
|
+
|
|
672
|
+
${BREVITY_RULE}
|
|
673
|
+
${FENCED_CONTENT_RULE}
|
|
674
|
+
Lead with the answer, then the cited rows — short lines, the row's own words. Your reply renders in the plane's chat column or the channel the question came from.`;
|
|
675
|
+
|
|
627
676
|
// The conductor (docs/reference/specs/agent-conductor.md): a run that starts
|
|
628
677
|
// other runs instead of doing the work — the spawn/await substrate's first
|
|
629
678
|
// preset. A child is a `dispatch()` run as the requesting user, in a thread of
|
|
@@ -802,6 +851,20 @@ const WORK_PRESETS = {
|
|
|
802
851
|
...loopBudget("explore"),
|
|
803
852
|
// No built-in effort: the deployment decides, as for coding.
|
|
804
853
|
},
|
|
854
|
+
orchestrator: {
|
|
855
|
+
name: "orchestrator",
|
|
856
|
+
description:
|
|
857
|
+
"Answers fleet questions from the plane's live tables — runs, units, pull requests — with the row cited. No workspace or shell.",
|
|
858
|
+
system: ORCHESTRATOR_SYSTEM,
|
|
859
|
+
toolset: "orchestrator",
|
|
860
|
+
// The plane read is a call in the bot process: nothing is provisioned
|
|
861
|
+
// and no credential is minted (record 0070, criterion 3).
|
|
862
|
+
machine: "none",
|
|
863
|
+
identity: "none",
|
|
864
|
+
tiers: ["fast", "strong"], // reads and cites rows: may run fast
|
|
865
|
+
maxTokens: 16000,
|
|
866
|
+
...loopBudget("orchestrator"),
|
|
867
|
+
},
|
|
805
868
|
} satisfies Record<string, AgentDef>;
|
|
806
869
|
|
|
807
870
|
export const AGENTS: Record<string, AgentDef> = {
|
|
@@ -13,6 +13,18 @@ import { NO_GRANTS, type Grants, type GrantSet } from "./types.js";
|
|
|
13
13
|
/** Every grant, on every axis. What admins and the local CLI hold. */
|
|
14
14
|
export const ALL_GRANTS: Grants = Object.freeze({ actions: "all", channels: "all", repos: "all" });
|
|
15
15
|
|
|
16
|
+
/** The plane's system actor (record 0064; record 0057 as amended): the run
|
|
17
|
+
* ledger object steers write-preset runs itself — the checkpoint steer and the
|
|
18
|
+
* held turn's reissue land as `run_inbox` rows with sender `plane` — under
|
|
19
|
+
* this one standing grant. Recorded here so the table stays the whole story:
|
|
20
|
+
* the write happens on the object, which holds no other authority. */
|
|
21
|
+
export const PLANE_ACTOR_ID = "plane";
|
|
22
|
+
export const PLANE_GRANTS: Grants = Object.freeze({
|
|
23
|
+
actions: new Set(["steer:write"]),
|
|
24
|
+
channels: "all",
|
|
25
|
+
repos: "all",
|
|
26
|
+
});
|
|
27
|
+
|
|
16
28
|
/** The namespaces a native `grants` key may use (invariant 4). `cli:` is not
|
|
17
29
|
* configurable (the local CLI always holds everything) and `agent:` actors
|
|
18
30
|
* derive their grants from their principal, so neither is listed. */
|
|
@@ -63,6 +63,12 @@ export const POLICY: readonly Rule[] = [
|
|
|
63
63
|
// NAMED run (`authorizeSteerOwner`, authorization item 16a) — the requester,
|
|
64
64
|
// a linked id, or `runs:write` — which no command-door row can see.
|
|
65
65
|
{ action: "steer:write", resource: "command", when: [grant("steer:write")] },
|
|
66
|
+
// The plane's checkpoint steer (record 0064): the system actor `plane`
|
|
67
|
+
// (`PLANE_ACTOR_ID`, a `service` actor holding `PLANE_GRANTS`' one standing
|
|
68
|
+
// `steer:write`) writes the steer into a write-preset run's inbox on the
|
|
69
|
+
// ledger object itself, sender `plane`; this row records that authority in
|
|
70
|
+
// the one table even though the object, not the bot, executes the write.
|
|
71
|
+
{ action: "steer:write", resource: "run", actorKinds: ["service"], when: [grant("steer:write")] },
|
|
66
72
|
|
|
67
73
|
// ── conversations ────────────────────────────────────────────────────────
|
|
68
74
|
// Who may point the bot at another thread (record 0037). Asked for a
|
|
@@ -108,6 +114,11 @@ export const POLICY: readonly Rule[] = [
|
|
|
108
114
|
// read is never a chat baseline, like the costs reads.
|
|
109
115
|
{ action: "providers:read", resource: "command", when: [grant("providers:read")] },
|
|
110
116
|
|
|
117
|
+
// ── pull requests ──────────────────────────────────────────────────
|
|
118
|
+
// `pulls rebase` force-pushes the pipeline's branches and can start a paid
|
|
119
|
+
// fix round (record 0071): a grant, never a chat baseline.
|
|
120
|
+
{ action: "pulls:write", resource: "command", when: [grant("pulls:write")] },
|
|
121
|
+
|
|
111
122
|
// ── repos ────────────────────────────────────────────────────────────────
|
|
112
123
|
{ action: "repo:read", resource: "command", when: [grant("repo:read")] },
|
|
113
124
|
{ action: "repo:write", resource: "repo", when: [grant("repo:write")] },
|
|
@@ -150,6 +161,10 @@ export const POLICY: readonly Rule[] = [
|
|
|
150
161
|
// channel-config right — whoever may set the channel may set a thread in it;
|
|
151
162
|
// never a baseline, so membership alone admits nobody.
|
|
152
163
|
{ action: "config:write", resource: "config-scope", resourceKind: "thread", when: [grant("config:write")] },
|
|
164
|
+
// The ORG scope (`config set org --pulls.…`, record 0071) — and a repository
|
|
165
|
+
// scope, the org setting's per-repository slice — affects every requester:
|
|
166
|
+
// the same `config:write` grant, never a baseline.
|
|
167
|
+
{ action: "config:write", resource: "config-scope", resourceKind: "org", when: [grant("config:write")] },
|
|
153
168
|
// A user edits only their own scope.
|
|
154
169
|
{ action: "config:write", resource: "config-scope", resourceKind: "user", when: [IS_SELF] },
|
|
155
170
|
// The author binding's one trusted write (record 0062, authorization.md item
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
// Workflow-driven coordinator and the deploy Workers can bundle it — the
|
|
17
17
|
// dependency runs registry → budgets, never the reverse.
|
|
18
18
|
|
|
19
|
+
export const SECOND_MS = 1_000;
|
|
20
|
+
|
|
21
|
+
export const secondsToMs = (seconds: number): number => seconds * SECOND_MS;
|
|
22
|
+
|
|
19
23
|
export const MINUTE_MS = 60_000;
|
|
20
24
|
|
|
21
25
|
/** Minutes → milliseconds, for a duration a request names in minutes (a drain's
|
|
@@ -109,6 +113,14 @@ export const DRAIN = {
|
|
|
109
113
|
marginMinutes: 5,
|
|
110
114
|
maxMinutes: 90,
|
|
111
115
|
defaultMinutes: 60,
|
|
116
|
+
/** How long a held drain waits for a container's post-deploy cycle before
|
|
117
|
+
* reopening the fleet anyway with the stale container named (issue 2044):
|
|
118
|
+
* the platform replaces container processes three to ten minutes after a
|
|
119
|
+
* deploy (docs/reference/specs/resident-repos.md item 65), so a cycle that
|
|
120
|
+
* has not landed within this bound is not coming on its own — the gate
|
|
121
|
+
* asked for the cycle, so it owns the outcome and never holds the fleet
|
|
122
|
+
* closed to the drain's whole `until` on a report nothing will send. */
|
|
123
|
+
cycleBoundMinutes: 10,
|
|
112
124
|
} as const;
|
|
113
125
|
|
|
114
126
|
/** How long an intake receipt row is kept on the run history object
|
|
@@ -132,7 +144,15 @@ export function intakeReceiptRetentionMs(catchUpWindowMs: number): number {
|
|
|
132
144
|
export const INTAKE_RECOVERY_WINDOW_MS = 10 * MINUTE_MS;
|
|
133
145
|
|
|
134
146
|
/** The presets that run the tool loop, and the one pipeline preset. */
|
|
135
|
-
export const LOOP_PRESETS = [
|
|
147
|
+
export const LOOP_PRESETS = [
|
|
148
|
+
"general",
|
|
149
|
+
"coding",
|
|
150
|
+
"review",
|
|
151
|
+
"research",
|
|
152
|
+
"explore",
|
|
153
|
+
"conductor",
|
|
154
|
+
"orchestrator",
|
|
155
|
+
] as const;
|
|
136
156
|
export type LoopPreset = (typeof LOOP_PRESETS)[number];
|
|
137
157
|
export type Preset = LoopPreset | "ship";
|
|
138
158
|
|
|
@@ -143,7 +163,7 @@ export type Preset = LoopPreset | "ship";
|
|
|
143
163
|
* that pushes at minute 29 and then runs the repo's whole gate (seven to ten
|
|
144
164
|
* minutes on a resident) was cut at 45 one run in twelve, so the ask is
|
|
145
165
|
* double the 90th percentile (31.5) with the gate inside it. Ship's holds
|
|
146
|
-
* its own loop at the default rounds with a
|
|
166
|
+
* its own loop at the default rounds with a findings round at that ask, not at
|
|
147
167
|
* its floor (`fit` proves the floor case; the ask leaves room above it). */
|
|
148
168
|
export const ASKS: Readonly<Record<Preset, number>> = {
|
|
149
169
|
general: 5,
|
|
@@ -153,19 +173,24 @@ export const ASKS: Readonly<Record<Preset, number>> = {
|
|
|
153
173
|
research: 8,
|
|
154
174
|
explore: 120,
|
|
155
175
|
conductor: 120,
|
|
176
|
+
// The plane's chat preset (record 0070): reads the fleet's tables and
|
|
177
|
+
// answers; a turn is a projection read plus a write-up, so its ask sits
|
|
178
|
+
// between general's and research's kind of work, with room for a few reads.
|
|
179
|
+
orchestrator: 10,
|
|
156
180
|
};
|
|
157
181
|
|
|
158
|
-
/** The rounds a ship loop is made of. `
|
|
182
|
+
/** The rounds a ship loop is made of. `findings` is a coding child handed the
|
|
159
183
|
* review's findings; `merge` is the runner's wait on the guards. */
|
|
160
|
-
export type RoundKind = "coding" | "review" | "
|
|
184
|
+
export type RoundKind = "coding" | "review" | "findings" | "merge";
|
|
161
185
|
|
|
162
186
|
/** The least lease in which a round does useful work, in minutes. A carve that
|
|
163
187
|
* falls under the floor is refused rather than dispatched: a two-minute
|
|
164
|
-
* review or
|
|
165
|
-
* Review's is the ledger's 90th percentile of completed reviews (5.1
|
|
166
|
-
* 181); coding's is the least a
|
|
167
|
-
* gate alone takes seven to ten minutes on a resident, and a
|
|
168
|
-
* run it finishes nothing its contract asks; the merge
|
|
188
|
+
* review or findings round costs an attach and a model turn and finishes
|
|
189
|
+
* nothing. Review's is the ledger's 90th percentile of completed reviews (5.1
|
|
190
|
+
* min over 181); coding's is the least a findings round can run the repo's
|
|
191
|
+
* gate in — the gate alone takes seven to ten minutes on a resident, and a
|
|
192
|
+
* round that cannot run it finishes nothing its contract asks; the merge
|
|
193
|
+
* wait's is a guess
|
|
169
194
|
* until the ledger says. */
|
|
170
195
|
export const PRESET_FLOORS: Readonly<Record<LoopPreset, number>> = {
|
|
171
196
|
general: 2,
|
|
@@ -174,10 +199,11 @@ export const PRESET_FLOORS: Readonly<Record<LoopPreset, number>> = {
|
|
|
174
199
|
research: 3,
|
|
175
200
|
explore: 15,
|
|
176
201
|
conductor: 15,
|
|
202
|
+
orchestrator: 2,
|
|
177
203
|
};
|
|
178
204
|
export const FLOORS: Readonly<Record<RoundKind, number>> = {
|
|
179
205
|
coding: PRESET_FLOORS.coding,
|
|
180
|
-
|
|
206
|
+
findings: PRESET_FLOORS.coding,
|
|
181
207
|
review: PRESET_FLOORS.review,
|
|
182
208
|
merge: 10,
|
|
183
209
|
};
|
|
@@ -186,6 +212,12 @@ export const FLOORS: Readonly<Record<RoundKind, number>> = {
|
|
|
186
212
|
* when the remainder allows it. */
|
|
187
213
|
export const MERGE_WAIT_ASK_MINUTES = 60;
|
|
188
214
|
|
|
215
|
+
/** The sweep's bounds (record 0071, mechanism two): the short lease of the one
|
|
216
|
+
* model round a conflicting pull request may buy, and the spend that round is
|
|
217
|
+
* capped at per pull request — a round refused at either bound ends the pull
|
|
218
|
+
* request's line with the conflict named, never a retry loop. */
|
|
219
|
+
export const PULL_SWEEP = { leaseMinutes: 15, spendCapUsd: 5 } as const;
|
|
220
|
+
|
|
189
221
|
/** The provider retry ladder (issue 1932): the backoff before each retry of a
|
|
190
222
|
* transient model-call failure — a gateway 5xx, a stream cut before
|
|
191
223
|
* `message_stop`, a gateway timeout. Three attempts with growing waits,
|
|
@@ -208,7 +240,15 @@ export const SHIP_WAIT = { marginMinutes: 5, chunkMinutes: 5, mergeChunkMinutes:
|
|
|
208
240
|
* it, a reservation's window, and the default re-ask cadence — how often a
|
|
209
241
|
* silent resident something waits on is probed (`plane.reaskMinutes`
|
|
210
242
|
* overrides it per deployment). */
|
|
211
|
-
export const PLANE = {
|
|
243
|
+
export const PLANE = {
|
|
244
|
+
recentMinutes: 60,
|
|
245
|
+
reservationMinutes: 2,
|
|
246
|
+
reaskMinutes: 2,
|
|
247
|
+
/** A coding round with no pushed head past this is steered once (record 0064, "The backpressure contract"). */
|
|
248
|
+
noPushMinutes: 15,
|
|
249
|
+
/** The line an in-flight call with NO declared bound is judged against for the same steer. */
|
|
250
|
+
noBoundMinutes: 20,
|
|
251
|
+
} as const;
|
|
212
252
|
|
|
213
253
|
/** The named amounts a lease holds back, in minutes. Each stands for a step
|
|
214
254
|
* every run or round pays: `provision` is attach and restore before the
|
|
@@ -238,6 +278,7 @@ export const POST_STEP_MINUTES: Readonly<Record<Preset, number>> = {
|
|
|
238
278
|
research: 0,
|
|
239
279
|
explore: 0,
|
|
240
280
|
conductor: 0,
|
|
281
|
+
orchestrator: 0,
|
|
241
282
|
ship: 0,
|
|
242
283
|
};
|
|
243
284
|
|
|
@@ -328,6 +369,12 @@ export function provisionalBearerExpiresAt(now: number, leaseMinutes: number): n
|
|
|
328
369
|
return now + (ALLOWANCES.provision + leaseMinutes + ALLOWANCES.bearerGrace) * MINUTE_MS;
|
|
329
370
|
}
|
|
330
371
|
|
|
372
|
+
/** The e2b credential-write exec's own timeout: the store-file write is one
|
|
373
|
+
* `printf` plus a `git config` reset, so a minute is generous headroom while
|
|
374
|
+
* still failing fast when the micro-VM is wedged
|
|
375
|
+
* (docs/reference/specs/execution.md item 5). */
|
|
376
|
+
export const SANDBOX_CREDENTIAL_WRITE_TIMEOUT_MS = MINUTE_MS;
|
|
377
|
+
|
|
331
378
|
/** The per-command bash budget's rows: the default when a call names none, the
|
|
332
379
|
* ceiling a call may raise it to, and the floor under which a number is a
|
|
333
380
|
* typo (docs/reference/specs/execution.md item 11). */
|
|
@@ -361,19 +408,20 @@ export interface RoundPosition {
|
|
|
361
408
|
}
|
|
362
409
|
|
|
363
410
|
/** The rounds of a loop in order: the coding round, then a review and, after
|
|
364
|
-
* every review but the last, a
|
|
411
|
+
* every review but the last, a findings round, then the merge wait. */
|
|
365
412
|
export function loopRounds(loop: Loop): RoundKind[] {
|
|
366
413
|
const rounds: RoundKind[] = ["coding"];
|
|
367
414
|
for (let i = 0; i < loop.maxRounds; i++) {
|
|
368
415
|
rounds.push("review");
|
|
369
|
-
if (i < loop.maxRounds - 1) rounds.push("
|
|
416
|
+
if (i < loop.maxRounds - 1) rounds.push("findings");
|
|
370
417
|
}
|
|
371
418
|
rounds.push("merge");
|
|
372
419
|
return rounds;
|
|
373
420
|
}
|
|
374
421
|
|
|
375
422
|
/** Where a round sits in `loopRounds`: the coding round first; review round
|
|
376
|
-
* `n` (counted from 1) and the
|
|
423
|
+
* `n` (counted from 1) and the findings round that follows it at `2n − 1`
|
|
424
|
+
* and `2n`; the
|
|
377
425
|
* merge wait last. The ship coordinator numbers its rounds this way, so the
|
|
378
426
|
* reserve it carves with is the one the fit assumed. */
|
|
379
427
|
export function loopPosition(loop: Loop, kind: RoundKind, n = 0): number {
|
|
@@ -382,7 +430,7 @@ export function loopPosition(loop: Loop, kind: RoundKind, n = 0): number {
|
|
|
382
430
|
return 0;
|
|
383
431
|
case "review":
|
|
384
432
|
return 2 * n - 1;
|
|
385
|
-
case "
|
|
433
|
+
case "findings":
|
|
386
434
|
return 2 * n;
|
|
387
435
|
case "merge":
|
|
388
436
|
return 2 * loop.maxRounds;
|
|
@@ -403,7 +451,7 @@ export function carveChildOfParent(
|
|
|
403
451
|
}
|
|
404
452
|
|
|
405
453
|
/** The minutes a round holds back for what must follow it in the loop: the
|
|
406
|
-
* floor plus provisioning of every later review and
|
|
454
|
+
* floor plus provisioning of every later review and findings round, and the merge
|
|
407
455
|
* wait's floor. A merge holds nothing back; a round with no loop (a
|
|
408
456
|
* conductor's child) holds nothing back either. */
|
|
409
457
|
export function reserveMinutes(
|
|
@@ -419,12 +467,12 @@ export function reserveMinutes(
|
|
|
419
467
|
return reserve;
|
|
420
468
|
}
|
|
421
469
|
|
|
422
|
-
/** What a round asks for: the preset's ask for a coding or
|
|
470
|
+
/** What a round asks for: the preset's ask for a coding or findings round, the
|
|
423
471
|
* review's for a review, the merge wait's own for the merge. */
|
|
424
472
|
export function roundAskMinutes(kind: RoundKind): number {
|
|
425
473
|
switch (kind) {
|
|
426
474
|
case "coding":
|
|
427
|
-
case "
|
|
475
|
+
case "findings":
|
|
428
476
|
return ASKS.coding;
|
|
429
477
|
case "review":
|
|
430
478
|
return ASKS.review;
|