@coreplane/switchboard 1.255.0 → 1.257.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 +100 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +227 -34
- 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 +55 -1
- 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 +51 -17
- package/dist/assets/src/core/coordinator/contract.ts +45 -5
- package/dist/assets/src/core/coordinator/driver.ts +17 -4
- package/dist/assets/src/core/delivery.ts +8 -5
- package/dist/assets/src/core/pipelineStanding.ts +57 -0
- package/dist/assets/src/core/plane/decide.ts +468 -12
- package/dist/assets/src/core/plane/findings.ts +120 -0
- package/dist/assets/src/core/reviewVerdict.ts +49 -0
- package/dist/assets/src/core/runEvents.ts +41 -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 +34 -32
- package/dist/assets/src/core/ship/coordinator.ts +291 -95
- package/dist/assets/src/core/ship/renewal.ts +19 -17
- 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 +82 -57
- package/dist/assets/web/dist/assets/HomePage-DJqJQzrL.js +1 -0
- package/dist/assets/web/dist/assets/{PendingTurnRow-DGINv9XT.js → PendingTurnRow-fhKqKUVv.js} +1 -1
- package/dist/assets/web/dist/assets/PlanePage-Bood0hNO.js +1 -0
- package/dist/assets/web/dist/assets/{ResidentDetailPage-D_RD6wLo.js → ResidentDetailPage-DOe1HtUC.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-BLkSuCxo.js → ResidentsIndexPage-TyCGjXM4.js} +1 -1
- package/dist/assets/web/dist/assets/RunFoldRow-C_4t-sit.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-BNepPMRX.js +9 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-jZnMw1mM.js +1 -0
- package/dist/assets/web/dist/assets/{ScheduledPage-3aYsDf-q.js → ScheduledPage-BkNoudDP.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-DG-p5Xy1.js → SettingsPage-COvWGXl-.js} +1 -1
- package/dist/assets/web/dist/assets/SilentTurn-DNAANKgN.js +2 -0
- package/dist/assets/web/dist/assets/SlackMark-CtiHrNjr.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-CEnGlyAL.js → StatusDot-BgmkSV0S.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-CiunVowT.js → Tooltip-DEUPuCPW.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DNhdzxvY.js +1 -0
- package/dist/assets/web/dist/assets/{dist-luhv3YSo.js → dist-CF3jz9LM.js} +1 -1
- package/dist/assets/web/dist/assets/durationTone-CVpX_yIk.js +1 -0
- package/dist/assets/web/dist/assets/main-7SAujY_s.js +28 -0
- package/dist/assets/web/dist/assets/main-DGC6WFqS.css +1 -0
- package/dist/assets/web/dist/assets/{sseReplay-DE6wv1Ua.js → sseReplay-BRJEIh83.js} +1 -1
- package/dist/cli.js +2941 -1116
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/HomePage-Be7jLLnU.js +0 -3
- package/dist/assets/web/dist/assets/PlanePage-JEj-lqgz.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-CfuZqf_O.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-CmWYGR36.js +0 -9
- package/dist/assets/web/dist/assets/RunsIndexPage-DW-HHuZa.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-iOYnmTcQ.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-mAKx_zo9.js +0 -28
|
@@ -343,7 +343,16 @@ import {
|
|
|
343
343
|
} from "../../src/execution/residentDepsStore.js";
|
|
344
344
|
import { buildId, injectedBuildStamp } from "../../src/deploy/buildStamp.js";
|
|
345
345
|
import { createRefreshInstance, createRefreshInstanceNow, type RefreshInstanceParams } from "./refresh";
|
|
346
|
-
import {
|
|
346
|
+
import {
|
|
347
|
+
drainRefusal,
|
|
348
|
+
holdDrain,
|
|
349
|
+
liftDrain,
|
|
350
|
+
liveDrain,
|
|
351
|
+
parseDrainRequest,
|
|
352
|
+
reportImageCurrent,
|
|
353
|
+
staleHolds,
|
|
354
|
+
type DrainRecord,
|
|
355
|
+
} from "./drain";
|
|
347
356
|
import {
|
|
348
357
|
CGROUP_READ_ARGV,
|
|
349
358
|
MEMORY_PRESSURE_REASON,
|
|
@@ -1434,6 +1443,10 @@ const TEST_OVERRIDES_KEY = "testOverrides";
|
|
|
1434
1443
|
* counts as a slot; it survives the isolate swap a deploy performs, which is
|
|
1435
1444
|
* why the record carries its own end. */
|
|
1436
1445
|
const DRAIN_KEY = "drain";
|
|
1446
|
+
/** Resident-DO key (issue 1931): set when a deploy's reconcile could not
|
|
1447
|
+
* verify this resident's container on the new image; the next reconcile that
|
|
1448
|
+
* finds it current reports to the registry and clears it. */
|
|
1449
|
+
const IMAGE_REPORT_PENDING_KEY = "imageReportPending";
|
|
1437
1450
|
|
|
1438
1451
|
type OnboardResult = { ok: true; record: ResidentRecord } | { ok: false; status: number; error: string };
|
|
1439
1452
|
|
|
@@ -1568,12 +1581,56 @@ export class ResidentRegistryDO extends DurableObject<Env> {
|
|
|
1568
1581
|
return record;
|
|
1569
1582
|
}
|
|
1570
1583
|
|
|
1571
|
-
/** Admin-only by construction (POST /undrain):
|
|
1572
|
-
*
|
|
1573
|
-
|
|
1584
|
+
/** Admin-only by construction (POST /undrain): `cleared` when a record was
|
|
1585
|
+
* there and was lifted — a cleared drain posts `below`, the plane's
|
|
1586
|
+
* resident-drain window lifts. With holds outstanding (issue 1931: a
|
|
1587
|
+
* container still to report the deploy's image) the fleet STAYS closed:
|
|
1588
|
+
* the record stands with `liftAsked` and the last container's report lifts
|
|
1589
|
+
* it (`reportContainerImageCurrent`); `until` remains the backstop. */
|
|
1590
|
+
async clearDrain(): Promise<{ cleared: boolean; held: string[] }> {
|
|
1591
|
+
const record = liveDrain(await this.ctx.storage.get(DRAIN_KEY), systemClock());
|
|
1592
|
+
if (record !== null) {
|
|
1593
|
+
const lift = liftDrain(record);
|
|
1594
|
+
if (!lift.cleared) {
|
|
1595
|
+
await this.ctx.storage.put(DRAIN_KEY, lift.record);
|
|
1596
|
+
return { cleared: false, held: lift.record.holds ?? [] };
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1574
1599
|
const had = await this.ctx.storage.delete(DRAIN_KEY);
|
|
1575
1600
|
if (had) await this.pushDrainPost("below");
|
|
1576
|
-
return had;
|
|
1601
|
+
return { cleared: had, held: [] };
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/** The deploy's reconcile could not verify these residents' containers on
|
|
1605
|
+
* the new image (issue 1931): hold the drain for each — the fleet must not
|
|
1606
|
+
* reopen onto them until they report. No live drain, nothing to hold. */
|
|
1607
|
+
async holdDrainFor(resources: string[]): Promise<void> {
|
|
1608
|
+
const now = systemClock();
|
|
1609
|
+
const record = liveDrain(await this.ctx.storage.get(DRAIN_KEY), now);
|
|
1610
|
+
if (record === null || resources.length === 0) return;
|
|
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));
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/** One resident's word that its running container is on the deploy's image:
|
|
1620
|
+
* its hold drops, and when it was the last hold of a lift already asked the
|
|
1621
|
+
* drain lifts here — the reopen fires on the last container's report. */
|
|
1622
|
+
async reportContainerImageCurrent(resource: string): Promise<{ lifted: boolean }> {
|
|
1623
|
+
const record = liveDrain(await this.ctx.storage.get(DRAIN_KEY), systemClock());
|
|
1624
|
+
if (record === null) return { lifted: false };
|
|
1625
|
+
const report = reportImageCurrent(record, resource);
|
|
1626
|
+
if (report.record === null) {
|
|
1627
|
+
await this.ctx.storage.delete(DRAIN_KEY);
|
|
1628
|
+
await this.pushDrainPost("below");
|
|
1629
|
+
console.log(`[drain] fleet reopened — ${resource} was the last container to report the deploy's image`);
|
|
1630
|
+
return { lifted: true };
|
|
1631
|
+
}
|
|
1632
|
+
await this.ctx.storage.put(DRAIN_KEY, report.record);
|
|
1633
|
+
return { lifted: false };
|
|
1577
1634
|
}
|
|
1578
1635
|
|
|
1579
1636
|
/** The one alarm, at the drain's `until` (record 0064): a drain past
|
|
@@ -1585,11 +1642,25 @@ export class ResidentRegistryDO extends DurableObject<Env> {
|
|
|
1585
1642
|
const stored = await this.ctx.storage.get(DRAIN_KEY);
|
|
1586
1643
|
if (stored === undefined) return;
|
|
1587
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
|
+
);
|
|
1588
1653
|
await this.ctx.storage.delete(DRAIN_KEY);
|
|
1589
1654
|
await this.pushDrainPost("below");
|
|
1590
1655
|
} else {
|
|
1591
|
-
// Replaced with a later end under an already-armed alarm: re-arm at
|
|
1592
|
-
|
|
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)));
|
|
1593
1664
|
}
|
|
1594
1665
|
}
|
|
1595
1666
|
|
|
@@ -3211,6 +3282,14 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
3211
3282
|
await this.writeDiskMarkers({ ready: sha, depsKey: lockfileHash, builtSha: sha });
|
|
3212
3283
|
this.deleteSchedules(PROVISIONING_CALLBACK);
|
|
3213
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");
|
|
3214
3293
|
// The first refresh instance is the cron's: the row now reads `warm`
|
|
3215
3294
|
// with no instance recorded, so the next firing creates it (item 9).
|
|
3216
3295
|
} catch (err) {
|
|
@@ -4936,41 +5015,121 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
4936
5015
|
* registration whose release never came defers it only until the clean-idle
|
|
4937
5016
|
* sweep drains that registration. Answers `restarted` when a stop was
|
|
4938
5017
|
* issued, else why not. */
|
|
4939
|
-
private async reconcileImage(where: string): Promise<ImageReconcileResult> {
|
|
4940
|
-
if (!(await this.isRuntimeActive().catch(() => false)))
|
|
5018
|
+
private async reconcileImage(where: string, force = false): Promise<ImageReconcileResult> {
|
|
5019
|
+
if (!(await this.isRuntimeActive().catch(() => false))) {
|
|
5020
|
+
// An inactive container's next start is on the deployed image by
|
|
5021
|
+
// construction — and a held resident whose container idled out has no
|
|
5022
|
+
// other reporter left: new attaches are refused by the very drain the
|
|
5023
|
+
// hold keeps standing, so without this report the fleet stays closed
|
|
5024
|
+
// until the drain's `until` backstop (issue 1931).
|
|
5025
|
+
await this.reportPendingImageCurrent(where);
|
|
5026
|
+
return "inactive";
|
|
5027
|
+
}
|
|
4941
5028
|
const last = THREAD_USERS[THREAD_USERS.length - 1];
|
|
4942
|
-
|
|
4943
|
-
|
|
5029
|
+
// A pending report marker (issue 1931) means a deploy could not verify this
|
|
5030
|
+
// container on its image: the probe shortcut below is a POOL-USER check the
|
|
5031
|
+
// pre-deploy image passes when the pool did not change, so it can never
|
|
5032
|
+
// satisfy the marker — the container is treated as stale until it is cycled
|
|
5033
|
+
// post-deploy and the fresh start reported.
|
|
5034
|
+
const pending = await this.ctx.storage.get<{ resource: string }>(IMAGE_REPORT_PENDING_KEY);
|
|
5035
|
+
if (!force && pending === undefined) {
|
|
5036
|
+
const probe = await this.run(["id", "-u", last]);
|
|
5037
|
+
if (probe.exitCode === 0) return "current";
|
|
5038
|
+
}
|
|
5039
|
+
const stale = force
|
|
5040
|
+
? "the container predates the deploy"
|
|
5041
|
+
: pending !== undefined
|
|
5042
|
+
? "a deploy's new-image report is pending, so the running container cannot be trusted current"
|
|
5043
|
+
: `${last} missing in the running container`;
|
|
4944
5044
|
const busy = this.inFlightCount();
|
|
4945
5045
|
if (busy > 0) {
|
|
4946
|
-
console.log(
|
|
4947
|
-
`image-stale (${where}): ${last} missing but ${busy} operation(s)/attach(es) in flight — deferring restart`,
|
|
4948
|
-
);
|
|
5046
|
+
console.log(`image-stale (${where}): ${stale} but ${busy} operation(s)/attach(es) in flight — deferring restart`);
|
|
4949
5047
|
return "deferred";
|
|
4950
5048
|
}
|
|
4951
5049
|
const registered = await this.registeredRunsBeyondOps();
|
|
4952
5050
|
if (registered > 0) {
|
|
4953
5051
|
console.log(
|
|
4954
|
-
`image-stale (${where}): ${
|
|
5052
|
+
`image-stale (${where}): ${stale} but ${registered} run registration(s) live — deferring restart until the resident is quiet`,
|
|
4955
5053
|
);
|
|
4956
5054
|
return "deferred";
|
|
4957
5055
|
}
|
|
4958
|
-
console.log(
|
|
4959
|
-
`image-stale (${where}): ${last} missing in the running container — stopping so it restarts on the current image`,
|
|
4960
|
-
);
|
|
5056
|
+
console.log(`image-stale (${where}): ${stale} — stopping so it restarts on the current image`);
|
|
4961
5057
|
this.swapIncarnation(); // deliberate incarnation swap
|
|
4962
5058
|
await this.stop().catch((err) => console.log(`image-stale: stop failed: ${errMsg(err)}`));
|
|
5059
|
+
if (pending !== undefined) {
|
|
5060
|
+
// The post-deploy cycle just happened: probe the FRESH container — the
|
|
5061
|
+
// SDK boots it for this command on the image the deployed Worker pins,
|
|
5062
|
+
// so a fresh start answering after the deploy is the verified fact
|
|
5063
|
+
// (issue 1931), never the pool-user probe on the old process. A failed
|
|
5064
|
+
// probe keeps the marker: the next reconcile tries the cycle again.
|
|
5065
|
+
const fresh = await this.run(["id", "-u", last])
|
|
5066
|
+
.then((p) => p.exitCode === 0)
|
|
5067
|
+
.catch(() => false);
|
|
5068
|
+
if (fresh) await this.reportPendingImageCurrent(where);
|
|
5069
|
+
else
|
|
5070
|
+
console.log(`image-stale (${where}): the fresh container's probe failed — the new-image report stays pending`);
|
|
5071
|
+
}
|
|
4963
5072
|
return "restarted";
|
|
4964
5073
|
}
|
|
4965
5074
|
|
|
5075
|
+
/** The report a held drain waits for (issue 1931): when the deploy's
|
|
5076
|
+
* reconcile could not verify this resident's fresh container on the new
|
|
5077
|
+
* image, a marker stays in storage and the registry holds the drain. The
|
|
5078
|
+
* fact the report stands on is a fresh container start AFTER the deploy —
|
|
5079
|
+
* the marker makes `reconcileImage` treat the container as stale until the
|
|
5080
|
+
* cycle happens, so this is called only behind that fresh start (or on an
|
|
5081
|
+
* inactive resident, whose next start is on the new image by construction).
|
|
5082
|
+
* The marker is deleted only after a successful report: a transient failure
|
|
5083
|
+
* keeps it for the next reconcile, and the drain's `until` is the backstop
|
|
5084
|
+
* for a report that never lands. */
|
|
5085
|
+
private async reportPendingImageCurrent(where: string): Promise<boolean> {
|
|
5086
|
+
const pending = await this.ctx.storage.get<{ resource: string }>(IMAGE_REPORT_PENDING_KEY);
|
|
5087
|
+
if (pending === undefined) return true;
|
|
5088
|
+
try {
|
|
5089
|
+
const { lifted } = await registryStub(this.env).reportContainerImageCurrent(pending.resource);
|
|
5090
|
+
await this.ctx.storage.delete(IMAGE_REPORT_PENDING_KEY);
|
|
5091
|
+
console.log(
|
|
5092
|
+
`[reconcile] ${pending.resource} reports the deploy's image (${where})${lifted ? " — drain lifted" : ""}`,
|
|
5093
|
+
);
|
|
5094
|
+
return true;
|
|
5095
|
+
} catch (err) {
|
|
5096
|
+
console.log(
|
|
5097
|
+
`[reconcile] ${pending.resource}: the new-image report failed (${where}): ${errMsg(err)} — retried on the next reconcile`,
|
|
5098
|
+
);
|
|
5099
|
+
return false;
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
5102
|
+
|
|
4966
5103
|
/** The deploy's reconcile, inside the drain window (item 69's order: the
|
|
4967
5104
|
* runs in flight end, the swap lands, the containers reconcile, the fleet
|
|
4968
5105
|
* reopens): `POST /reconcile` calls this on every resident after the Worker
|
|
4969
|
-
* deploy landed and BEFORE the drain is lifted
|
|
4970
|
-
*
|
|
4971
|
-
*
|
|
4972
|
-
|
|
4973
|
-
|
|
5106
|
+
* deploy landed and BEFORE the drain is lifted. The pool-user probe cannot
|
|
5107
|
+
* tell the pre-deploy image from the new one when the pool did not change,
|
|
5108
|
+
* and the platform replaces the container processes asynchronously after an
|
|
5109
|
+
* image-changing deploy (issue 1931) — so this path never trusts "current":
|
|
5110
|
+
* an active, quiet container is always cycled, and the fresh start —
|
|
5111
|
+
* necessarily on the deploy's image, which landed before this call — is
|
|
5112
|
+
* probed. `verified` true is the fact the reopen may stand on; anything
|
|
5113
|
+
* else (deferred, a failed fresh probe) leaves a hold on the drain, cleared
|
|
5114
|
+
* by this resident's later report (`reportPendingImageCurrent`), never by a
|
|
5115
|
+
* timer.
|
|
5116
|
+
*
|
|
5117
|
+
* The hold lands BEFORE the marker: the marker is what lets any concurrent
|
|
5118
|
+
* attach/refresh reconcile report, and a report that reaches the registry
|
|
5119
|
+
* before the hold is a no-op — the marker would be consumed and the hold
|
|
5120
|
+
* added afterwards would wait for a report nothing sends (until the drain's
|
|
5121
|
+
* `until`). Held first, every report finds its hold. */
|
|
5122
|
+
async reconcileForDeploy(resource: string): Promise<{ result: ImageReconcileResult; verified: boolean }> {
|
|
5123
|
+
await registryStub(this.env).holdDrainFor([resource]);
|
|
5124
|
+
await this.ctx.storage.put(IMAGE_REPORT_PENDING_KEY, { resource });
|
|
5125
|
+
const result = await this.reconcileImage("deploy", true);
|
|
5126
|
+
if (result === "deferred") return { result, verified: false };
|
|
5127
|
+
// Inactive: the inactive early-return reported (the next start is on the
|
|
5128
|
+
// new image by construction). Restarted: the cycle's fresh probe and
|
|
5129
|
+
// report ran inside reconcileImage. Either way the marker gone is the
|
|
5130
|
+
// verification.
|
|
5131
|
+
const verified = (await this.ctx.storage.get(IMAGE_REPORT_PENDING_KEY)) === undefined;
|
|
5132
|
+
return { result, verified };
|
|
4974
5133
|
}
|
|
4975
5134
|
|
|
4976
5135
|
// -- watchdog (the sparse cron; it re-arms nothing) --------------------------
|
|
@@ -7467,6 +7626,19 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7467
7626
|
}
|
|
7468
7627
|
|
|
7469
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
|
+
}
|
|
7470
7642
|
try {
|
|
7471
7643
|
await this.ensureHydrated();
|
|
7472
7644
|
} catch (err) {
|
|
@@ -7940,6 +8112,7 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
7940
8112
|
AUTO_REBUILDS_KEY,
|
|
7941
8113
|
INFRA_STREAK_KEY,
|
|
7942
8114
|
MEMORY_KEY,
|
|
8115
|
+
IMAGE_REPORT_PENDING_KEY,
|
|
7943
8116
|
]);
|
|
7944
8117
|
const facts = map.get(FACTS_KEY) as RepoFacts | undefined;
|
|
7945
8118
|
const snap = map.get(SNAPSHOT_KEY) as SnapshotRecord | undefined;
|
|
@@ -8017,6 +8190,11 @@ export class ResidentDO extends Sandbox<Env> {
|
|
|
8017
8190
|
inFlight: this.inFlightCount() + registeredRuns,
|
|
8018
8191
|
// The runs alone (no refresh cycle): what the deploy preflight refuses on.
|
|
8019
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",
|
|
8020
8198
|
// Item 22: who holds what, as the rows say — the mirror mutex and the
|
|
8021
8199
|
// cycle/hydration leases, each judged against this incarnation.
|
|
8022
8200
|
incarnation: this.incarnation,
|
|
@@ -9118,32 +9296,47 @@ async function handleDrain(env: Env, body: Record<string, unknown>): Promise<Res
|
|
|
9118
9296
|
return json({ draining: record, planeOutbox: await registryStub(env).getDrainOutbox() });
|
|
9119
9297
|
}
|
|
9120
9298
|
|
|
9121
|
-
/** POST /undrain (admin): reopen the fleet. Idempotent — `cleared` says
|
|
9122
|
-
* a drain stood.
|
|
9299
|
+
/** POST /undrain (admin): reopen the fleet. Idempotent — `cleared` says
|
|
9300
|
+
* whether a drain stood and was lifted. With containers still to report the
|
|
9301
|
+
* deploy's image (issue 1931) the fleet STAYS closed: `held` names them, the
|
|
9302
|
+
* record stands with the lift asked, and the last container's report reopens
|
|
9303
|
+
* the fleet — a fact, never a timer; the drain's `until` is the backstop. */
|
|
9123
9304
|
async function handleUndrain(env: Env): Promise<Response> {
|
|
9124
|
-
const
|
|
9125
|
-
|
|
9126
|
-
|
|
9305
|
+
const lift = await registryStub(env).clearDrain();
|
|
9306
|
+
if (!lift.cleared && lift.held.length > 0) {
|
|
9307
|
+
console.log(`[drain] fleet stays closed — containers still to report the deploy's image: ${lift.held.join(", ")}`);
|
|
9308
|
+
const draining = liveDrain(await registryStub(env).getDrain(), systemClock());
|
|
9309
|
+
return json({ draining, cleared: false, held: lift.held, planeOutbox: await registryStub(env).getDrainOutbox() });
|
|
9310
|
+
}
|
|
9311
|
+
console.log(`[drain] fleet reopened (${lift.cleared ? "a drain stood" : "no drain stood"})`);
|
|
9312
|
+
return json({ draining: null, cleared: lift.cleared, planeOutbox: await registryStub(env).getDrainOutbox() });
|
|
9127
9313
|
}
|
|
9128
9314
|
|
|
9129
9315
|
/** POST /reconcile (drain scope): reconcile every resident's container onto
|
|
9130
9316
|
* the current image — the deploy runner posts it after its Worker deploy
|
|
9131
9317
|
* landed and BEFORE its `/undrain`, so a stale container restarts inside the
|
|
9132
9318
|
* drain window (item 69's order) and never under a run the reopened fleet
|
|
9133
|
-
* admits. Each resident answers what its reconcile decided
|
|
9134
|
-
*
|
|
9135
|
-
*
|
|
9319
|
+
* admits. Each resident answers what its reconcile decided and whether its
|
|
9320
|
+
* fresh container was VERIFIED on the deploy's image (issue 1931); one not
|
|
9321
|
+
* verified — deferred, a failed fresh probe, an error — leaves a hold on the
|
|
9322
|
+
* drain, so the fleet reopens only on that container's later report. */
|
|
9136
9323
|
async function handleReconcile(env: Env): Promise<Response> {
|
|
9137
9324
|
const residents = await registryStub(env).list();
|
|
9138
9325
|
const settled = await Promise.allSettled(
|
|
9139
|
-
residents.map((record) => residentStub(env, record.resource).reconcileForDeploy()),
|
|
9326
|
+
residents.map((record) => residentStub(env, record.resource).reconcileForDeploy(record.resource)),
|
|
9140
9327
|
);
|
|
9141
9328
|
const reconciled = residents.map((record, i) => {
|
|
9142
9329
|
const s = settled[i];
|
|
9143
9330
|
return s.status === "fulfilled"
|
|
9144
|
-
? { resource: record.resource, result: s.value.result }
|
|
9145
|
-
: { resource: record.resource, result: "error" as const, error: errMsg(s.reason) };
|
|
9331
|
+
? { resource: record.resource, result: s.value.result, verified: s.value.verified }
|
|
9332
|
+
: { resource: record.resource, result: "error" as const, verified: false, error: errMsg(s.reason) };
|
|
9146
9333
|
});
|
|
9334
|
+
// Each resident held ITSELF before its marker (reconcileForDeploy), so no
|
|
9335
|
+
// report can outrun its hold; this pass is the backstop for a resident whose
|
|
9336
|
+
// call rejected before it could — holdDrain deduplicates, so re-holding a
|
|
9337
|
+
// deferred resident changes nothing.
|
|
9338
|
+
const unverified = reconciled.filter((r) => !r.verified).map((r) => r.resource);
|
|
9339
|
+
if (unverified.length > 0) await registryStub(env).holdDrainFor(unverified);
|
|
9147
9340
|
console.log(`[reconcile] deploy image reconcile: ${JSON.stringify(reconciled)}`);
|
|
9148
9341
|
return json({ reconciled });
|
|
9149
9342
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.257.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.257.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.257.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.257.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
|
@@ -254,6 +254,48 @@ Keep notes with the \`notes\` tool: one short document, replaced whole each time
|
|
|
254
254
|
// classes are by duration, the project's own scripts and CI say which is which.
|
|
255
255
|
export const CHECKS_BY_COST = `CHECKS BY COST — push before the expensive ones. Every check you might run has a cost class: seconds (a formatter or a linter on the files you touched, one test file, a docs, link or spec check, the typecheck of one package) or minutes (the whole test suite, a build, a dependency install, an end-to-end or full verification). Know a command's class before you run it — from the project's own scripts and CI configuration, from how long it took last time, or by the class above when you have nothing better. Prove each change with the cheapest check that can prove it, matched to the change's scope and scoped to the changed set — the tests nearest your change, the touched project's typecheck, the changed files' formatting, never the whole tree: a documentation change gets the documentation checks, one module gets its own tests, a shared type gets the typecheck. Every CI pipeline runs the tests, the types, the formatting and the full verification on your push, so you never run them again: you validate and fix your own change before pushing, at the changed-set scope. Passing the full test suite and the full typecheck is NOT part of your criteria: CI is that gate and the only place they run — on a shared machine they cost minutes that every other run pays for. As soon as the change exists and those checks pass, commit and push — the pushed branch is the deliverable, and an unpushed tree does not survive the run's end. Beyond the changed set, use judgement about what this change needs rather than a checklist, fixing forward with further commits and pushes. Never start an operation whose expected duration does not fit the time you have left minus what a commit, a push and the description need: push what there is and say plainly what is unverified instead. At the wind-down note, commit and push what compiles, say what does not, then answer. ${TIMEOUT_ON_LONG_COMMANDS} The description's validation names exactly what ran; what did not run is CI's to gate, and you say so.`;
|
|
256
256
|
|
|
257
|
+
// Every coding prompt carries this verbatim, right after the checks-by-cost
|
|
258
|
+
// rule (docs/reference/specs/agent-coding.md item 13; issue 1796): the fast
|
|
259
|
+
// gates before every push, each the changed-set form with its command named,
|
|
260
|
+
// and the full verification named as CI's gate. The paragraph lived in the
|
|
261
|
+
// ship contract's first instruction alone, so every ask that was not a plan
|
|
262
|
+
// unit had to repeat it by hand or watch the run spend most of its budget on
|
|
263
|
+
// the whole suite or the full verification before its first push (issue 1909
|
|
264
|
+
// measured three such command shapes at 85–95 % of a child's life). One
|
|
265
|
+
// constant, one text: the three coding variants carry the same bytes, the
|
|
266
|
+
// contract's first instruction points here instead of re-stating it
|
|
267
|
+
// (src/core/ship/contract.ts), and no requester repeats it. Unlike the
|
|
268
|
+
// checks-by-cost rule above, this paragraph names its commands on purpose —
|
|
269
|
+
// children handed only the classes chose wrong in both directions — and a
|
|
270
|
+
// repository on another stack maps each gate by its class.
|
|
271
|
+
export const TOUCHED_TESTS_COMMAND = "`npx vitest run` on the test files you touched, by name,";
|
|
272
|
+
|
|
273
|
+
export const FAST_GATES_BEFORE_PUSH =
|
|
274
|
+
"THE FAST GATES, before every push — each scoped to the changed set, never the whole project: " +
|
|
275
|
+
`${TOUCHED_TESTS_COMMAND} once (never \`--changed\`, never a directory: on a moving base that is most of the suite), ` +
|
|
276
|
+
"`tsc --noEmit -p` the touched tsconfig under `NODE_OPTIONS=--max-old-space-size=6144`, " +
|
|
277
|
+
"`npx prettier --check` on the changed files, `npm run hygiene:check` and `npm run specs:check` — " +
|
|
278
|
+
"then your judgement on what else this change needs, not a longer checklist. The full verification is " +
|
|
279
|
+
"CI's gate — `npm run verify` runs there on your push, never here: push a head early and let CI judge it, " +
|
|
280
|
+
"fixing forward with further commits and pushes.";
|
|
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
|
+
|
|
257
299
|
const CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
|
|
258
300
|
|
|
259
301
|
You work inside a dedicated workspace directory with bash, read_file, and write_file tools. ${SANDBOX_TOOLCHAIN}
|
|
@@ -276,6 +318,10 @@ Workflow for shipping a PR:
|
|
|
276
318
|
|
|
277
319
|
${CHECKS_BY_COST}
|
|
278
320
|
|
|
321
|
+
${FAST_GATES_BEFORE_PUSH}
|
|
322
|
+
|
|
323
|
+
${REBASE_BEFORE_PUSH}
|
|
324
|
+
|
|
279
325
|
${NEVER_MERGE}
|
|
280
326
|
|
|
281
327
|
${UNIT_CONTRACT}
|
|
@@ -323,6 +369,10 @@ Workflow for shipping a change:
|
|
|
323
369
|
|
|
324
370
|
${CHECKS_BY_COST}
|
|
325
371
|
|
|
372
|
+
${FAST_GATES_BEFORE_PUSH}
|
|
373
|
+
|
|
374
|
+
${REBASE_BEFORE_PUSH}
|
|
375
|
+
|
|
326
376
|
${NEVER_MERGE}
|
|
327
377
|
|
|
328
378
|
${UNIT_CONTRACT}
|
|
@@ -366,6 +416,10 @@ Workflow for shipping a change:
|
|
|
366
416
|
|
|
367
417
|
${CHECKS_BY_COST}
|
|
368
418
|
|
|
419
|
+
${FAST_GATES_BEFORE_PUSH}
|
|
420
|
+
|
|
421
|
+
${REBASE_BEFORE_PUSH}
|
|
422
|
+
|
|
369
423
|
${NEVER_MERGE}
|
|
370
424
|
|
|
371
425
|
${UNIT_CONTRACT}
|
|
@@ -558,7 +612,7 @@ Your tools work without a workspace: the GitHub tools — \`github_repos\` (the
|
|
|
558
612
|
|
|
559
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.
|
|
560
614
|
|
|
561
|
-
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).`;
|
|
562
616
|
|
|
563
617
|
// The explore agent (docs/reference/specs/agent-explore.md): a long, read-only
|
|
564
618
|
// investigation — "run our CI locally and validate the claims", "how long does
|
|
@@ -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. */
|