@coreplane/switchboard 1.257.0 → 1.259.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 +1 -1
- 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/agents/registry.ts +41 -1
- package/dist/assets/src/core/budgets.ts +15 -1
- package/dist/assets/src/core/pipelineStanding.ts +2 -0
- package/dist/assets/src/core/plane/decide.ts +9 -0
- package/dist/assets/src/core/provider.ts +3 -3
- package/dist/assets/src/core/runEvents.ts +13 -0
- package/dist/assets/src/core/ship/coordinator.ts +81 -22
- package/dist/assets/src/core/ship/renewal.ts +4 -3
- package/dist/assets/src/core/types.ts +1 -1
- package/dist/assets/web/dist/.vite/manifest.json +67 -67
- package/dist/assets/web/dist/assets/{DeliveryPage-BBAyLwPq.js → DeliveryPage-DadntjSp.js} +1 -1
- package/dist/assets/web/dist/assets/{HomePage-DJqJQzrL.js → HomePage-BEEpTnVT.js} +1 -1
- package/dist/assets/web/dist/assets/{PendingTurnRow-fhKqKUVv.js → PendingTurnRow-DBPMLVsm.js} +1 -1
- package/dist/assets/web/dist/assets/{PlanePage-Bood0hNO.js → PlanePage-DLusMGQ0.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DOe1HtUC.js → ResidentDetailPage-CPnHnrxy.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-TyCGjXM4.js → ResidentsIndexPage-C9hASwHL.js} +1 -1
- package/dist/assets/web/dist/assets/{RunFoldRow-C_4t-sit.js → RunFoldRow-OPe7Z0Cz.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-BNepPMRX.js → RunRoutePage-o1xzkTGZ.js} +3 -3
- package/dist/assets/web/dist/assets/{RunsIndexPage-jZnMw1mM.js → RunsIndexPage-0YAClYQh.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-BkNoudDP.js → ScheduledPage-BIdfC0V3.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-COvWGXl-.js → SettingsPage-CnqWAjl3.js} +1 -1
- package/dist/assets/web/dist/assets/{SilentTurn-DNAANKgN.js → SilentTurn-DGNg0sIZ.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-BgmkSV0S.js → StatusDot-D_D_iJS8.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-DEUPuCPW.js → Tooltip-DVFmM7on.js} +1 -1
- package/dist/assets/web/dist/assets/{UnitRoutePage-DNhdzxvY.js → UnitRoutePage-7fAXFoB_.js} +1 -1
- package/dist/assets/web/dist/assets/budgets-BRLm6oID.js +1 -0
- package/dist/assets/web/dist/assets/{dist-CF3jz9LM.js → dist-C1VX-_mm.js} +1 -1
- package/dist/assets/web/dist/assets/{indexRow-DborJPFp.js → indexRow-BO_GIdF6.js} +1 -1
- package/dist/assets/web/dist/assets/{main-7SAujY_s.js → main-Cl7xHG34.js} +2 -2
- package/dist/assets/web/dist/assets/{sseReplay-BRJEIh83.js → sseReplay-BjlKmQsY.js} +1 -1
- package/dist/cli.js +469 -62
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/budgets-c1eumrqD.js +0 -1
|
@@ -164,7 +164,7 @@ defaults:
|
|
|
164
164
|
# addressSeverity: minor # blocking | major | minor (default) | nit
|
|
165
165
|
# readingDiff:
|
|
166
166
|
# provider: git # git (default) | meat | off
|
|
167
|
-
# meatModel: claude-opus-5 # meat's -model; Opus-class is the floor that actually abridges
|
|
167
|
+
# meatModel: claude-opus-5 # meat's -model; required with provider: meat (no built-in model) — Opus-class is the floor that actually abridges
|
|
168
168
|
# meatTimeoutS: 240 # meat's own budget on the host; past it the abridging fails by name
|
|
169
169
|
|
|
170
170
|
# Static per-channel defaults (channel ID -> scope). Runtime overrides set via
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.259.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.259.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.259.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.259.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
|
@@ -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
|
|
@@ -647,6 +647,32 @@ ${BREVITY_RULE}
|
|
|
647
647
|
${FENCED_CONTENT_RULE}
|
|
648
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.`;
|
|
649
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
|
+
|
|
650
676
|
// The conductor (docs/reference/specs/agent-conductor.md): a run that starts
|
|
651
677
|
// other runs instead of doing the work — the spawn/await substrate's first
|
|
652
678
|
// preset. A child is a `dispatch()` run as the requesting user, in a thread of
|
|
@@ -825,6 +851,20 @@ const WORK_PRESETS = {
|
|
|
825
851
|
...loopBudget("explore"),
|
|
826
852
|
// No built-in effort: the deployment decides, as for coding.
|
|
827
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
|
+
},
|
|
828
868
|
} satisfies Record<string, AgentDef>;
|
|
829
869
|
|
|
830
870
|
export const AGENTS: Record<string, AgentDef> = {
|
|
@@ -144,7 +144,15 @@ export function intakeReceiptRetentionMs(catchUpWindowMs: number): number {
|
|
|
144
144
|
export const INTAKE_RECOVERY_WINDOW_MS = 10 * MINUTE_MS;
|
|
145
145
|
|
|
146
146
|
/** The presets that run the tool loop, and the one pipeline preset. */
|
|
147
|
-
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;
|
|
148
156
|
export type LoopPreset = (typeof LOOP_PRESETS)[number];
|
|
149
157
|
export type Preset = LoopPreset | "ship";
|
|
150
158
|
|
|
@@ -165,6 +173,10 @@ export const ASKS: Readonly<Record<Preset, number>> = {
|
|
|
165
173
|
research: 8,
|
|
166
174
|
explore: 120,
|
|
167
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,
|
|
168
180
|
};
|
|
169
181
|
|
|
170
182
|
/** The rounds a ship loop is made of. `findings` is a coding child handed the
|
|
@@ -187,6 +199,7 @@ export const PRESET_FLOORS: Readonly<Record<LoopPreset, number>> = {
|
|
|
187
199
|
research: 3,
|
|
188
200
|
explore: 15,
|
|
189
201
|
conductor: 15,
|
|
202
|
+
orchestrator: 2,
|
|
190
203
|
};
|
|
191
204
|
export const FLOORS: Readonly<Record<RoundKind, number>> = {
|
|
192
205
|
coding: PRESET_FLOORS.coding,
|
|
@@ -265,6 +278,7 @@ export const POST_STEP_MINUTES: Readonly<Record<Preset, number>> = {
|
|
|
265
278
|
research: 0,
|
|
266
279
|
explore: 0,
|
|
267
280
|
conductor: 0,
|
|
281
|
+
orchestrator: 0,
|
|
268
282
|
ship: 0,
|
|
269
283
|
};
|
|
270
284
|
|
|
@@ -99,6 +99,7 @@ export const ROUND_STAGE = {
|
|
|
99
99
|
dequeued: "fix",
|
|
100
100
|
aborted: "hold",
|
|
101
101
|
stopped: "hold",
|
|
102
|
+
held: "hold",
|
|
102
103
|
continued: "continue",
|
|
103
104
|
idle: "idle",
|
|
104
105
|
} as const satisfies Record<ShipRoundOutcome, Stage | "start" | "continue" | "hold">;
|
|
@@ -171,6 +172,7 @@ export const ROUND_OUTCOME_WORDS = {
|
|
|
171
172
|
dequeued: "removed from the merge queue",
|
|
172
173
|
aborted: "aborted",
|
|
173
174
|
stopped: "stopped",
|
|
175
|
+
held: "held",
|
|
174
176
|
continued: "continuing",
|
|
175
177
|
idle: "idle",
|
|
176
178
|
} as const satisfies Record<ShipRoundOutcome, string>;
|
|
@@ -265,6 +265,15 @@ export function reissueSteerSentence(provider: string): string {
|
|
|
265
265
|
return `the model provider ${provider} is answering again — re-issue the held turn and continue`;
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
/** The reissue steer read back (model-proxy item 12a): what the pi harness
|
|
269
|
+
* releases a held turn on — the sentence above with any provider's name,
|
|
270
|
+
* judged whole against the inbox row's text beside its `plane` sender, so a
|
|
271
|
+
* parked turn is only re-driven by the plane's own words, never a person's
|
|
272
|
+
* follow-up that happens to mention a provider. */
|
|
273
|
+
export function isReissueSteerText(text: string): boolean {
|
|
274
|
+
return /^the model provider .+ is answering again — re-issue the held turn and continue$/.test(text.trim());
|
|
275
|
+
}
|
|
276
|
+
|
|
268
277
|
/** The checkpoint steer's causes: an in-flight call past its bound (or the
|
|
269
278
|
* no-bound line) and a coding round with no pushed head past `noPushMinutes`. */
|
|
270
279
|
export type SteerCause = "long_call" | "no_push";
|
|
@@ -187,7 +187,7 @@ export interface VendorRef {
|
|
|
187
187
|
/** The one vendor parse (record 0052): `parseModelRef` is the only other parser of a
|
|
188
188
|
* ref. A block that declares a vendor name uses it; a block that declares
|
|
189
189
|
* `vendor: model`, or a model id that carries its own vendor prefix
|
|
190
|
-
* (`openrouter
|
|
190
|
+
* (`openrouter/<vendor>/<model>`), reads the vendor off the id's
|
|
191
191
|
* first segment; otherwise the block's own name is the vendor. `catalog`
|
|
192
192
|
* never enters: a block named unlike its catalog still serves the vendor the
|
|
193
193
|
* declaration or the id names. */
|
|
@@ -272,11 +272,11 @@ export function billerHarnessProvider(biller: string | undefined): BillerHarness
|
|
|
272
272
|
/** The env var Anthropic's own SDK reads when an `anthropic` provider block names none. */
|
|
273
273
|
export const ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY";
|
|
274
274
|
|
|
275
|
-
/** "
|
|
275
|
+
/** "<provider>/<model>" -> { provider, model }: the split at the ref's first slash. */
|
|
276
276
|
export function parseModelRef(ref: string): { provider: string; model: string } {
|
|
277
277
|
const i = ref.indexOf("/");
|
|
278
278
|
if (i === -1) {
|
|
279
|
-
throw new Error(`Model "${ref}" must be qualified as "<provider>/<model>"
|
|
279
|
+
throw new Error(`Model "${ref}" must be qualified as "<provider>/<model>"`);
|
|
280
280
|
}
|
|
281
281
|
return { provider: ref.slice(0, i), model: ref.slice(i + 1) };
|
|
282
282
|
}
|
|
@@ -128,6 +128,13 @@ export type RunNoteKind =
|
|
|
128
128
|
* (docs/reference/specs/run-history.md item 37); the summary says how many calls were
|
|
129
129
|
* in flight at the kill and how each was settled. Published by the runner. */
|
|
130
130
|
| "resumed"
|
|
131
|
+
/** The restart the run's `restarting` close promised never claimed the run
|
|
132
|
+
* (issue 2081): its dispatch died between the close and the successor's
|
|
133
|
+
* claim, so this interrupted close is the run's end, not a restart. The
|
|
134
|
+
* summary names how the dispatch ended. Published by the dispatcher over
|
|
135
|
+
* the closed record — a kind of its own, so the interruption's cause is
|
|
136
|
+
* still read off the roll's earlier words, never off the dispatch error. */
|
|
137
|
+
| "restart_died"
|
|
131
138
|
/** A control was decided against the model card and is not native (record
|
|
132
139
|
* 0052): a fallback (`applied` differs from `asked`, `vouched` true) or
|
|
133
140
|
* an unvouched send (`vouched` false). One note per degraded control,
|
|
@@ -332,6 +339,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
332
339
|
"mcp_unavailable",
|
|
333
340
|
"follow_up",
|
|
334
341
|
"resumed",
|
|
342
|
+
"restart_died",
|
|
335
343
|
"control_degraded",
|
|
336
344
|
"seed",
|
|
337
345
|
"redispatch",
|
|
@@ -436,6 +444,11 @@ export type ShipRoundOutcome =
|
|
|
436
444
|
| "dequeued"
|
|
437
445
|
| "aborted"
|
|
438
446
|
| "stopped"
|
|
447
|
+
/** The round-0 coding child concluded its round with an answer instead of a
|
|
448
|
+
* pull request (issue 2086): its handoff records a deviation — a stop
|
|
449
|
+
* condition, a blocked precondition — so the unit ends held for the
|
|
450
|
+
* person's word and no renewal is judged. */
|
|
451
|
+
| "held"
|
|
439
452
|
/** The coding round ended at its lease with the unit unfinished and the row
|
|
440
453
|
* showing progress, and the grant renewed: the next segment opens (decision 0046). */
|
|
441
454
|
| "continued"
|
|
@@ -776,6 +776,25 @@ export type UnitEnding =
|
|
|
776
776
|
round: RoundRef;
|
|
777
777
|
reviewRounds: number;
|
|
778
778
|
}
|
|
779
|
+
/** The round-0 coding child concluded its round with an answer instead of a
|
|
780
|
+
* pull request (issue 2086): its handoff records a deviation — a stop
|
|
781
|
+
* condition it hit, a precondition that does not hold, a question it could
|
|
782
|
+
* not resolve — read off the record, never its prose. The round has ended;
|
|
783
|
+
* the unit ends held with the deviations as its reason and waits for the
|
|
784
|
+
* person's word (it idles when the flag is on — the one held cause that
|
|
785
|
+
* does), and the round is never renewed as a second segment: the renewal
|
|
786
|
+
* grant (decision 0046, amended) is for a lease that ran out mid-work, not
|
|
787
|
+
* for a round with an answer. */
|
|
788
|
+
| {
|
|
789
|
+
kind: "held";
|
|
790
|
+
cause: "blocked";
|
|
791
|
+
pr?: PrRef;
|
|
792
|
+
round: RoundRef;
|
|
793
|
+
runId: string;
|
|
794
|
+
reason: string;
|
|
795
|
+
finalReply?: string;
|
|
796
|
+
reviewRounds: number;
|
|
797
|
+
}
|
|
779
798
|
| { kind: "merge_refused"; pr: PrRef; reason: string; reviewRounds: number }
|
|
780
799
|
| { kind: "round_cap"; maxRounds: number; reviewRounds: number }
|
|
781
800
|
| {
|
|
@@ -870,22 +889,20 @@ export type UnitEnding =
|
|
|
870
889
|
|
|
871
890
|
/** The kinds that idle: every ending but the ended ones — the unit is
|
|
872
891
|
* unfinished (a cap, a stop, an abort, a refused merge, a segment's end) and
|
|
873
|
-
* a reply could continue it. `merged`, `already_landed`, `merge_ready
|
|
874
|
-
* `
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
>;
|
|
892
|
+
* a reply could continue it. `merged`, `already_landed`, `merge_ready` and
|
|
893
|
+
* `refused` never idle: the first two are done, merge-ready waits only for a
|
|
894
|
+
* person's merge, and a refused child would be refused again. A gated or
|
|
895
|
+
* draft `held` never idles either — it waits only for a person's receipt at
|
|
896
|
+
* the pull request, which no thread reply can produce — but a blocked hold
|
|
897
|
+
* (issue 2086) waits for exactly the person's word the idle's wake carries,
|
|
898
|
+
* so it alone idles; `idleEnding` draws that line by cause. */
|
|
899
|
+
export type IdleWhy = Exclude<UnitEnding["kind"], "idle" | "merged" | "already_landed" | "merge_ready" | "refused">;
|
|
882
900
|
|
|
883
901
|
const NEVER_IDLES: ReadonlySet<UnitEnding["kind"]> = new Set([
|
|
884
902
|
"idle",
|
|
885
903
|
"merged",
|
|
886
904
|
"already_landed",
|
|
887
905
|
"merge_ready",
|
|
888
|
-
"held",
|
|
889
906
|
"refused",
|
|
890
907
|
]);
|
|
891
908
|
|
|
@@ -1354,10 +1371,11 @@ function end(s: UnitPipelineState, ending: UnitEnding, notes: CoordinatorNote[]
|
|
|
1354
1371
|
function idleEnding(s: UnitPipelineState, ending: UnitEnding): Extract<UnitEnding, { kind: "idle" }> | undefined {
|
|
1355
1372
|
if ((s.input.idleDays ?? 0) <= 0) return undefined;
|
|
1356
1373
|
if (NEVER_IDLES.has(ending.kind)) return undefined;
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1374
|
+
// A gated or draft hold waits only for a person's receipt at the pull
|
|
1375
|
+
// request — no thread reply can continue it — while a blocked hold (issue
|
|
1376
|
+
// 2086) waits for exactly the person's word the wake carries, so it idles.
|
|
1377
|
+
if (ending.kind === "held" && ending.cause !== "blocked") return undefined;
|
|
1378
|
+
const old = ending as Exclude<UnitEnding, { kind: "idle" | "merged" | "already_landed" | "merge_ready" | "refused" }>;
|
|
1361
1379
|
const grant = s.input.grant ?? DEFAULT_GRANT;
|
|
1362
1380
|
// Unspent: an idle spends no renewal — the wake's segment does (this plan's
|
|
1363
1381
|
// fifth unit) — so the row says what the grant still holds.
|
|
@@ -1996,14 +2014,42 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
1996
2014
|
return end(s, { kind: "already_landed", landed, round, runId: phase.runId, reviewRounds: s.reviewRounds }, [
|
|
1997
2015
|
roundNote(round, "completed"),
|
|
1998
2016
|
]);
|
|
1999
|
-
//
|
|
2000
|
-
//
|
|
2001
|
-
//
|
|
2002
|
-
//
|
|
2003
|
-
//
|
|
2004
|
-
//
|
|
2005
|
-
//
|
|
2006
|
-
//
|
|
2017
|
+
// A concluded round is never renewed (issue 2086; decision 0046,
|
|
2018
|
+
// amended): a handoff recording a deviation is the round's own ending —
|
|
2019
|
+
// a stop condition the child hit, a precondition that does not hold, a
|
|
2020
|
+
// question it could not resolve — a typed fact off the record, never its
|
|
2021
|
+
// prose. The unit ends held with the deviations as its reason and waits
|
|
2022
|
+
// for the person's word; no renewal is judged and no second segment
|
|
2023
|
+
// opens — the grant is for a lease that ran out mid-work, and a renewed
|
|
2024
|
+
// segment would re-run the same brief with no memory of the answer. A
|
|
2025
|
+
// handoff without deviations is the lease-end checkpoint (follow-ups,
|
|
2026
|
+
// unproven criteria) and leaves the renewal decision below to the grant.
|
|
2027
|
+
const deviations = phase.childHandoff?.deviations ?? [];
|
|
2028
|
+
if (deviations.length > 0)
|
|
2029
|
+
return end(
|
|
2030
|
+
s,
|
|
2031
|
+
{
|
|
2032
|
+
kind: "held",
|
|
2033
|
+
cause: "blocked",
|
|
2034
|
+
...(s.pr !== undefined ? { pr: s.pr } : {}),
|
|
2035
|
+
round,
|
|
2036
|
+
runId: phase.runId,
|
|
2037
|
+
reason: deviations.map((dv) => `${dv.from} → ${dv.to} — ${dv.why}`).join("; "),
|
|
2038
|
+
...(phase.finalReply !== undefined ? { finalReply: phase.finalReply } : {}),
|
|
2039
|
+
reviewRounds: s.reviewRounds,
|
|
2040
|
+
},
|
|
2041
|
+
[roundNote(round, "held")],
|
|
2042
|
+
);
|
|
2043
|
+
// Round 0 ended without a pull request and without an ending of its own:
|
|
2044
|
+
// the segment is over with the unit unfinished — a lease end, never a
|
|
2045
|
+
// concluded round (those ended above) — and the grant decides whether
|
|
2046
|
+
// the next opens (decision 0046, Renewal). Progress is read off the
|
|
2047
|
+
// child's record — a head pushed to the unit's branch since the segment
|
|
2048
|
+
// started, or a checkpoint handoff that moved — never off its words; the
|
|
2049
|
+
// decision then asks the grant's count, the cap and the fit, in that
|
|
2050
|
+
// order, and a refusal names the clause. A plain abort keeps its old
|
|
2051
|
+
// shape when nothing was pushed under a grant of zero: a clarifying
|
|
2052
|
+
// question is not a stop to explain.
|
|
2007
2053
|
const grant = s.input.grant ?? DEFAULT_GRANT;
|
|
2008
2054
|
const session = s.input.session;
|
|
2009
2055
|
const progress = progressOf({
|
|
@@ -2707,6 +2753,19 @@ export function renderUnitReport(
|
|
|
2707
2753
|
draftReissue,
|
|
2708
2754
|
]);
|
|
2709
2755
|
}
|
|
2756
|
+
// The blocked hold (issue 2086): the coding child concluded its round
|
|
2757
|
+
// with an answer instead of a pull request — its handoff's deviations
|
|
2758
|
+
// are the reason — so the report leads with that answer and says plainly
|
|
2759
|
+
// that no renewal was spent and no second coding child ran: the person's
|
|
2760
|
+
// word in this thread is what continues the unit.
|
|
2761
|
+
if (e.cause === "blocked") {
|
|
2762
|
+
if (!shows(verbosity, "verbose")) return `⏸️ Held: blocked — ${e.reason}${prLine}`;
|
|
2763
|
+
return join([
|
|
2764
|
+
`⏸️ Held after ${rounds}: the coding child of round ${e.round.index} concluded its round blocked instead of opening a pull request — ${e.reason}.${prLine}`,
|
|
2765
|
+
writeUpPointer(s, e.round.kind, e.runId),
|
|
2766
|
+
`No renewal was spent and no second coding child ran — a concluded round is not renewed (a renewal continues a budget that ran out mid-work). Next step: your word in this thread — answer what the child raised, and the unit continues from there. ${reissue}`,
|
|
2767
|
+
]);
|
|
2768
|
+
}
|
|
2710
2769
|
// The person's next step is the report's whole point (issue 1990): the
|
|
2711
2770
|
// human-gated rows are named with the reviewer's own words, and the
|
|
2712
2771
|
// re-issue line says the attempt resumes at the review round — item 10's
|
|
@@ -54,7 +54,9 @@ const sameSha = (a: string, b: string): boolean => {
|
|
|
54
54
|
/** Progress is a fact the row records after unit eight and the runner reads
|
|
55
55
|
* without a model: the last head pushed to the unit's branch that differs from
|
|
56
56
|
* the head the segment started at and is not older than the lease; failing
|
|
57
|
-
* that, a handoff whose follow-ups shrank
|
|
57
|
+
* that, a checkpoint handoff whose follow-ups shrank. A handoff carrying
|
|
58
|
+
* deviations never reaches this decision: it is the round's own ending (issue
|
|
59
|
+
* 2086) and the unit ends held before any renewal is judged. */
|
|
58
60
|
export function progressOf(input: ProgressInput): Progress {
|
|
59
61
|
const last = [...input.pushed].reverse().find((h) => h.ref === input.branch);
|
|
60
62
|
if (last !== undefined) {
|
|
@@ -69,8 +71,7 @@ export function progressOf(input: ProgressInput): Progress {
|
|
|
69
71
|
const pushWhy = `no head newer than the budget's start was pushed to \`${input.branch}\``;
|
|
70
72
|
if (input.handoff === undefined) return { progressed: false, why: pushWhy };
|
|
71
73
|
const { previous, current } = input.handoff;
|
|
72
|
-
if (current.followUps.length < previous.followUps.length
|
|
73
|
-
return { progressed: true, by: "handoff" };
|
|
74
|
+
if (current.followUps.length < previous.followUps.length) return { progressed: true, by: "handoff" };
|
|
74
75
|
return { progressed: false, why: `${pushWhy} and the write-up is unchanged` };
|
|
75
76
|
}
|
|
76
77
|
|
|
@@ -162,7 +162,7 @@ export type StatusActivity = { kind: "command"; tool: string; command: string }
|
|
|
162
162
|
|
|
163
163
|
/** A structured progress frame; adapters decide how to render it. */
|
|
164
164
|
export interface StatusUpdate {
|
|
165
|
-
/** one-line headline, e.g. "⚡ review on
|
|
165
|
+
/** one-line headline, e.g. "⚡ review on <provider>/<model> · 42s" */
|
|
166
166
|
title: string;
|
|
167
167
|
/** the agent's checklist and any lead lines, one per line */
|
|
168
168
|
detail?: string;
|