@oh-my-pi/pi-coding-agent 15.8.2 → 15.9.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/CHANGELOG.md +82 -29
- package/dist/types/capability/skill.d.ts +7 -0
- package/dist/types/config/settings-schema.d.ts +36 -22
- package/dist/types/debug/protocol-probe.d.ts +38 -0
- package/dist/types/debug/terminal-info.d.ts +34 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mcp/transports/stdio.d.ts +16 -7
- package/dist/types/modes/components/assistant-message.d.ts +3 -2
- package/dist/types/modes/components/hook-selector.d.ts +11 -0
- package/dist/types/modes/components/todo-reminder.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +36 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +20 -1
- package/dist/types/plan-mode/plan-handoff.d.ts +20 -0
- package/dist/types/plan-mode/plan-protection.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/indexed-session-storage.d.ts +59 -0
- package/dist/types/session/redis-session-storage.d.ts +12 -85
- package/dist/types/session/session-manager.d.ts +20 -0
- package/dist/types/session/session-storage.d.ts +5 -7
- package/dist/types/session/sql-session-storage.d.ts +16 -85
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/telemetry-export.d.ts +19 -0
- package/dist/types/tiny/compiled-runtime.d.ts +35 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/index.d.ts +4 -2
- package/dist/types/tools/path-utils.d.ts +1 -1
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/{todo-write.d.ts → todo.d.ts} +18 -18
- package/dist/types/utils/jj.d.ts +1 -1
- package/dist/types/utils/session-color.d.ts +7 -2
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +65 -1
- package/package.json +15 -9
- package/scripts/build-binary.ts +12 -0
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +1 -1
- package/src/config/settings-schema.ts +22 -55
- package/src/debug/index.ts +67 -1
- package/src/debug/protocol-probe.ts +267 -0
- package/src/debug/terminal-info.ts +127 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -3
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +3 -3
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/main.ts +12 -0
- package/src/mcp/transports/stdio.ts +37 -12
- package/src/modes/acp/acp-event-mapper.ts +7 -7
- package/src/modes/components/assistant-message.ts +3 -2
- package/src/modes/components/hook-selector.ts +149 -14
- package/src/modes/components/session-selector.ts +36 -5
- package/src/modes/components/status-line/segments.ts +2 -1
- package/src/modes/components/status-line.ts +1 -1
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/todo-reminder.ts +1 -1
- package/src/modes/components/tool-execution.ts +9 -4
- package/src/modes/components/transcript-container.ts +91 -0
- package/src/modes/controllers/event-controller.ts +12 -7
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +18 -6
- package/src/modes/print-mode.ts +5 -0
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/theme/theme.ts +48 -8
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/plan-mode/plan-handoff.ts +37 -0
- package/src/plan-mode/plan-protection.ts +31 -0
- package/src/priority.json +4 -0
- package/src/prompts/goals/goal-continuation.md +1 -1
- package/src/prompts/system/eager-todo.md +3 -3
- package/src/prompts/system/orchestrate-notice.md +5 -5
- package/src/prompts/system/plan-mode-approved.md +14 -17
- package/src/prompts/system/plan-mode-reference.md +3 -6
- package/src/prompts/system/subagent-system-prompt.md +11 -0
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/browser.md +5 -2
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +3 -1
- package/src/prompts/tools/{todo-write.md → todo.md} +8 -0
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +29 -16
- package/src/session/indexed-session-storage.ts +430 -0
- package/src/session/redis-session-storage.ts +66 -377
- package/src/session/session-manager.ts +102 -22
- package/src/session/session-storage.ts +148 -68
- package/src/session/sql-session-storage.ts +131 -382
- package/src/slash-commands/helpers/todo.ts +2 -2
- package/src/task/executor.ts +9 -1
- package/src/task/index.ts +51 -1
- package/src/telemetry-export.ts +126 -0
- package/src/tiny/compiled-runtime.ts +179 -0
- package/src/tiny/worker.ts +24 -2
- package/src/tools/ask.ts +133 -87
- package/src/tools/fetch.ts +17 -4
- package/src/tools/find.ts +2 -2
- package/src/tools/index.ts +6 -4
- package/src/tools/path-utils.ts +16 -7
- package/src/tools/renderers.ts +2 -2
- package/src/tools/search.ts +6 -3
- package/src/tools/{todo-write.ts → todo.ts} +32 -35
- package/src/utils/jj.ts +28 -5
- package/src/utils/session-color.ts +39 -14
- package/src/web/search/index.ts +1 -1
- package/src/web/search/provider.ts +18 -34
- package/src/web/search/types.ts +73 -32
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type Component, Container, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
2
|
+
|
|
3
|
+
const kSnapshot = Symbol("transcript.frozenRender");
|
|
4
|
+
|
|
5
|
+
interface FrozenRender {
|
|
6
|
+
width: number;
|
|
7
|
+
lines: string[];
|
|
8
|
+
generation: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SnapshotCarrier {
|
|
12
|
+
[kSnapshot]?: FrozenRender;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Transcript container that freezes the rendered output of every block except
|
|
17
|
+
* the bottom-most (live) one on terminals where committed native scrollback is
|
|
18
|
+
* immutable.
|
|
19
|
+
*
|
|
20
|
+
* On ED3-risk terminals with an unobservable viewport (ghostty/kitty/iTerm2/…)
|
|
21
|
+
* the renderer cannot clear saved lines (`\x1b[3J` may yank a reader) or query
|
|
22
|
+
* whether the user has scrolled, so any block that re-lays-out *after* it has
|
|
23
|
+
* scrolled past the viewport leaves a stale duplicate above the live region
|
|
24
|
+
* (a finalized assistant message re-wrapping, a tool preview collapsing to its
|
|
25
|
+
* compact result, a late async tool completion). The renderer's only safe move
|
|
26
|
+
* for such an offscreen edit is to not repaint — which is correct only if the
|
|
27
|
+
* committed region never changes underneath it.
|
|
28
|
+
*
|
|
29
|
+
* This container provides that guarantee: a block's render is snapshotted while
|
|
30
|
+
* it is the live (bottom-most) block, and once a newer block is appended it
|
|
31
|
+
* replays the snapshot instead of recomputing. Mutations after a block leaves
|
|
32
|
+
* live are intentionally deferred until the next checkpoint {@link thaw} (prompt
|
|
33
|
+
* submit → native-scrollback rebuild), where the whole transcript is replayed
|
|
34
|
+
* and any drift reconciles safely. On terminals that can rebuild history this
|
|
35
|
+
* freezing is unnecessary, so it renders every block live for full fidelity.
|
|
36
|
+
*/
|
|
37
|
+
export class TranscriptContainer extends Container {
|
|
38
|
+
// Bumped to invalidate every block's snapshot at once; a snapshot is only
|
|
39
|
+
// honored when its stored generation still matches.
|
|
40
|
+
#generation = 0;
|
|
41
|
+
|
|
42
|
+
override invalidate(): void {
|
|
43
|
+
// A theme/global invalidation forces a full recompute on the rebuild that
|
|
44
|
+
// follows; retire every snapshot.
|
|
45
|
+
this.#generation++;
|
|
46
|
+
super.invalidate();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override clear(): void {
|
|
50
|
+
this.#generation++;
|
|
51
|
+
super.clear();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Retire all frozen snapshots so the next render reflects each block's current
|
|
56
|
+
* state. Call at reconciliation checkpoints (prompt submit) where the whole
|
|
57
|
+
* transcript is replayed into native scrollback and any drift a frozen block
|
|
58
|
+
* accumulated is reconciled.
|
|
59
|
+
*/
|
|
60
|
+
thaw(): void {
|
|
61
|
+
this.#generation++;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override render(width: number): string[] {
|
|
65
|
+
width = Math.max(1, width);
|
|
66
|
+
if (!TERMINAL.eagerEraseScrollbackRisk) return super.render(width);
|
|
67
|
+
|
|
68
|
+
const lines: string[] = [];
|
|
69
|
+
const liveIndex = this.children.length - 1;
|
|
70
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
71
|
+
const child = this.children[i]! as Component & SnapshotCarrier;
|
|
72
|
+
if (i !== liveIndex) {
|
|
73
|
+
const snapshot = child[kSnapshot];
|
|
74
|
+
// Replay the block's last render from while it was live. A stale
|
|
75
|
+
// generation (post-thaw) or width mismatch (resize in flight, an
|
|
76
|
+
// explicit rebuild that reconciles history anyway) recomputes instead.
|
|
77
|
+
if (snapshot && snapshot.generation === this.#generation && snapshot.width === width) {
|
|
78
|
+
lines.push(...snapshot.lines);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const rendered = child.render(width);
|
|
83
|
+
// Cache every block's latest render. While a block is live this keeps its
|
|
84
|
+
// snapshot current; the frame it stops being live the cache already holds
|
|
85
|
+
// its final live render, so nothing recomputes underneath it.
|
|
86
|
+
child[kSnapshot] = { width, lines: rendered, generation: this.#generation };
|
|
87
|
+
lines.push(...rendered);
|
|
88
|
+
}
|
|
89
|
+
return lines;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -282,6 +282,7 @@ export class EventController {
|
|
|
282
282
|
this.ctx.hideThinkingBlock,
|
|
283
283
|
() => this.ctx.ui.requestRender(),
|
|
284
284
|
this.ctx.session.extensionRunner?.getAssistantThinkingRenderers(),
|
|
285
|
+
this.ctx.ui.imageBudget,
|
|
285
286
|
);
|
|
286
287
|
this.ctx.streamingMessage = event.message;
|
|
287
288
|
this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
|
|
@@ -586,18 +587,18 @@ export class EventController {
|
|
|
586
587
|
this.ctx.ui.requestRender();
|
|
587
588
|
}
|
|
588
589
|
}
|
|
589
|
-
// Update todo display when
|
|
590
|
-
if (event.toolName === "
|
|
590
|
+
// Update todo display when todo tool completes
|
|
591
|
+
if (event.toolName === "todo" && !event.isError) {
|
|
591
592
|
const details = event.result.details as { phases?: TodoPhase[] } | undefined;
|
|
592
593
|
if (details?.phases) {
|
|
593
594
|
this.ctx.setTodos(details.phases);
|
|
594
595
|
}
|
|
595
|
-
} else if (event.toolName === "
|
|
596
|
+
} else if (event.toolName === "todo" && event.isError) {
|
|
596
597
|
const textContent = event.result.content.find(
|
|
597
598
|
(content: { type: string; text?: string }) => content.type === "text",
|
|
598
599
|
)?.text;
|
|
599
600
|
this.ctx.showWarning(
|
|
600
|
-
`Todo update failed${textContent ? `: ${textContent}` : ". Progress may be stale until
|
|
601
|
+
`Todo update failed${textContent ? `: ${textContent}` : ". Progress may be stale until todo succeeds."}`,
|
|
601
602
|
);
|
|
602
603
|
}
|
|
603
604
|
if (event.toolName === "resolve" && !event.isError) {
|
|
@@ -847,9 +848,13 @@ export class EventController {
|
|
|
847
848
|
const last = this.ctx.session.getLastAssistantMessage?.();
|
|
848
849
|
if (last?.stopReason === "aborted" || last?.stopReason === "error") return;
|
|
849
850
|
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
851
|
+
const sessionName = this.ctx.sessionManager.getSessionName();
|
|
852
|
+
TERMINAL.sendNotification({
|
|
853
|
+
title: sessionName || "Oh My Pi",
|
|
854
|
+
body: "Complete",
|
|
855
|
+
type: "completion",
|
|
856
|
+
actions: "focus",
|
|
857
|
+
});
|
|
853
858
|
}
|
|
854
859
|
|
|
855
860
|
async handleBackgroundEvent(event: AgentSessionEvent): Promise<void> {
|
|
@@ -625,6 +625,9 @@ export class ExtensionUiController {
|
|
|
625
625
|
tui: this.ctx.ui,
|
|
626
626
|
outline: dialogOptions?.outline,
|
|
627
627
|
disabledIndices: dialogOptions?.disabledIndices,
|
|
628
|
+
selectionMarker: dialogOptions?.selectionMarker,
|
|
629
|
+
checkedIndices: dialogOptions?.checkedIndices,
|
|
630
|
+
markableCount: dialogOptions?.markableCount,
|
|
628
631
|
maxVisible,
|
|
629
632
|
slider: extra?.slider,
|
|
630
633
|
},
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type TodoItem,
|
|
9
9
|
type TodoPhase,
|
|
10
10
|
USER_TODO_EDIT_CUSTOM_TYPE,
|
|
11
|
-
} from "../../tools/todo
|
|
11
|
+
} from "../../tools/todo";
|
|
12
12
|
import { copyToClipboard } from "../../utils/clipboard";
|
|
13
13
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
14
14
|
import type { InteractiveModeContext } from "../types";
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
Markdown,
|
|
29
29
|
ProcessTerminal,
|
|
30
30
|
Spacer,
|
|
31
|
+
setTerminalTextSizing,
|
|
32
|
+
TERMINAL,
|
|
31
33
|
Text,
|
|
32
34
|
TUI,
|
|
33
35
|
visibleWidth,
|
|
@@ -84,7 +86,7 @@ import type { LspStartupServerInfo } from "../tools";
|
|
|
84
86
|
import { normalizeLocalScheme } from "../tools/path-utils";
|
|
85
87
|
import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
|
|
86
88
|
import { type ResolveToolDetails, runResolveInvocation } from "../tools/resolve";
|
|
87
|
-
import { formatPhaseDisplayName, selectStickyTodoWindow, todoMatchesAnyDescription } from "../tools/todo
|
|
89
|
+
import { formatPhaseDisplayName, selectStickyTodoWindow, todoMatchesAnyDescription } from "../tools/todo";
|
|
88
90
|
import { ToolError } from "../tools/tool-errors";
|
|
89
91
|
import type { EventBus } from "../utils/event-bus";
|
|
90
92
|
import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
|
@@ -100,6 +102,7 @@ import type { HookInputComponent } from "./components/hook-input";
|
|
|
100
102
|
import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
|
|
101
103
|
import { StatusLineComponent } from "./components/status-line";
|
|
102
104
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
105
|
+
import { TranscriptContainer } from "./components/transcript-container";
|
|
103
106
|
import { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
|
|
104
107
|
import { BtwController } from "./controllers/btw-controller";
|
|
105
108
|
import { CommandController } from "./controllers/command-controller";
|
|
@@ -255,7 +258,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
255
258
|
historyStorage?: HistoryStorage;
|
|
256
259
|
|
|
257
260
|
ui: TUI;
|
|
258
|
-
chatContainer:
|
|
261
|
+
chatContainer: TranscriptContainer;
|
|
259
262
|
pendingMessagesContainer: Container;
|
|
260
263
|
statusContainer: Container;
|
|
261
264
|
todoContainer: Container;
|
|
@@ -390,7 +393,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
390
393
|
|
|
391
394
|
this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
|
|
392
395
|
this.ui.setClearOnShrink(settings.get("clearOnShrink"));
|
|
393
|
-
this.
|
|
396
|
+
this.ui.setMaxInlineImages(settings.get("tui.maxInlineImages"));
|
|
397
|
+
// OSC 66 text-sizing is Kitty-only; resolve the setting against the terminal's
|
|
398
|
+
// capability (`TERMINAL.textSizing` defaults on for Kitty) so it stays off
|
|
399
|
+
// unless the user opts in, and never emits raw escapes on other terminals.
|
|
400
|
+
setTerminalTextSizing(settings.get("tui.textSizing") && TERMINAL.textSizing);
|
|
401
|
+
this.chatContainer = new TranscriptContainer();
|
|
394
402
|
this.pendingMessagesContainer = new Container();
|
|
395
403
|
this.statusContainer = new Container();
|
|
396
404
|
this.todoContainer = new Container();
|
|
@@ -915,6 +923,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
915
923
|
this.#pendingSubmissionDispose = undefined;
|
|
916
924
|
}
|
|
917
925
|
this.editor.setText("");
|
|
926
|
+
// Reconciliation checkpoint: the rebuild below replays the whole transcript
|
|
927
|
+
// into native scrollback, so retire frozen block snapshots and let every
|
|
928
|
+
// block render its current state.
|
|
929
|
+
this.chatContainer.thaw();
|
|
918
930
|
this.ui.refreshNativeScrollbackIfDirty({ allowUnknownViewport: true });
|
|
919
931
|
this.ensureLoadingAnimation();
|
|
920
932
|
this.ui.requestRender();
|
|
@@ -1001,7 +1013,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1001
1013
|
} else {
|
|
1002
1014
|
const accentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
|
|
1003
1015
|
const sessionName = accentEnabled ? this.sessionManager.getSessionName() : undefined;
|
|
1004
|
-
const hex = sessionName ? getSessionAccentHex(sessionName) : undefined;
|
|
1016
|
+
const hex = sessionName ? getSessionAccentHex(sessionName, theme.accentSurfaceLuminance) : undefined;
|
|
1005
1017
|
const ansi = getSessionAccentAnsi(hex);
|
|
1006
1018
|
if (ansi) {
|
|
1007
1019
|
this.editor.borderColor = (str: string) => `${ansi}${str}\x1b[39m`;
|
|
@@ -1060,7 +1072,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1060
1072
|
* Auto-complete any pending/in_progress todo whose content matches a
|
|
1061
1073
|
* subagent that has finished successfully. Fires on every observer
|
|
1062
1074
|
* `onChange` so the visual state stays in sync with subagent lifecycle
|
|
1063
|
-
* without requiring the agent to issue a follow-up `
|
|
1075
|
+
* without requiring the agent to issue a follow-up `todo`. Failed
|
|
1064
1076
|
* and aborted subagents are intentionally NOT auto-completed — those
|
|
1065
1077
|
* stay open so the user (or the next agent turn) can decide what to do.
|
|
1066
1078
|
*
|
|
@@ -2495,7 +2507,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2495
2507
|
const accentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
|
|
2496
2508
|
const sessionName = accentEnabled ? this.sessionManager.getSessionName() : undefined;
|
|
2497
2509
|
if (!sessionName) return undefined;
|
|
2498
|
-
const hex = getSessionAccentHex(sessionName);
|
|
2510
|
+
const hex = getSessionAccentHex(sessionName, theme.accentSurfaceLuminance);
|
|
2499
2511
|
const main = getSessionAccentAnsi(hex);
|
|
2500
2512
|
const dim = getSessionAccentAnsi(adjustHsv(hex, { s: 0.55, v: 0.65 }));
|
|
2501
2513
|
return main && dim ? { main, dim } : undefined;
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
|
9
9
|
import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import type { AgentSession } from "../session/agent-session";
|
|
11
11
|
import { isSilentAbort } from "../session/messages";
|
|
12
|
+
import { flushTelemetryExport } from "../telemetry-export";
|
|
12
13
|
import { initializeExtensions } from "./runtime-init";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -83,6 +84,10 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
83
84
|
!isSilentAbort(assistantMsg.errorMessage)
|
|
84
85
|
) {
|
|
85
86
|
const errorLine = sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
|
|
87
|
+
// Flush before this hard exit — it bypasses the awaited postmortem.quit()
|
|
88
|
+
// in main(), and the postmortem `exit` handler can't await, so the error
|
|
89
|
+
// spans would otherwise stay buffered in the batch processor and drop.
|
|
90
|
+
await flushTelemetryExport();
|
|
86
91
|
const flushed = process.stderr.write(`${errorLine}\n`);
|
|
87
92
|
if (flushed) {
|
|
88
93
|
process.exit(1);
|
|
@@ -10,7 +10,7 @@ import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
|
|
|
10
10
|
import type { BashResult } from "../../exec/bash-executor";
|
|
11
11
|
import type { ContextUsage } from "../../extensibility/extensions/types";
|
|
12
12
|
import type { SessionStats } from "../../session/agent-session";
|
|
13
|
-
import type { TodoPhase } from "../../tools/todo
|
|
13
|
+
import type { TodoPhase } from "../../tools/todo";
|
|
14
14
|
|
|
15
15
|
// ============================================================================
|
|
16
16
|
// RPC Commands (stdin)
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
supportsLanguage as nativeSupportsLanguage,
|
|
11
11
|
} from "@oh-my-pi/pi-natives";
|
|
12
12
|
import type { EditorTheme, MarkdownTheme, SelectListTheme, SymbolTheme } from "@oh-my-pi/pi-tui";
|
|
13
|
-
import { adjustHsv, getCustomThemesDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
13
|
+
import { adjustHsv, colorLuma, getCustomThemesDir, isEnoent, logger, relativeLuminance } from "@oh-my-pi/pi-utils";
|
|
14
14
|
import chalk from "chalk";
|
|
15
15
|
import * as z from "zod/v4";
|
|
16
16
|
// Embed theme JSON files at build time
|
|
@@ -136,6 +136,9 @@ export type SymbolKey =
|
|
|
136
136
|
// Checkboxes
|
|
137
137
|
| "checkbox.checked"
|
|
138
138
|
| "checkbox.unchecked"
|
|
139
|
+
// Radio (single-choice)
|
|
140
|
+
| "radio.selected"
|
|
141
|
+
| "radio.unselected"
|
|
139
142
|
// Text Formatting
|
|
140
143
|
| "format.bullet"
|
|
141
144
|
| "format.dash"
|
|
@@ -302,6 +305,9 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
302
305
|
// Checkboxes
|
|
303
306
|
"checkbox.checked": "☑",
|
|
304
307
|
"checkbox.unchecked": "☐",
|
|
308
|
+
// Radio (single-choice)
|
|
309
|
+
"radio.selected": "◉",
|
|
310
|
+
"radio.unselected": "○",
|
|
305
311
|
// Formatting
|
|
306
312
|
"format.bullet": "•",
|
|
307
313
|
"format.dash": "—",
|
|
@@ -559,6 +565,11 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
559
565
|
"checkbox.checked": "\uf14a",
|
|
560
566
|
// pick: | alt:
|
|
561
567
|
"checkbox.unchecked": "\uf096",
|
|
568
|
+
// Radio (single-choice)
|
|
569
|
+
// pick: (fa-dot-circle-o) | alt: ◉
|
|
570
|
+
"radio.selected": "\uf192",
|
|
571
|
+
// pick: (fa-circle-o) | alt: ○
|
|
572
|
+
"radio.unselected": "\uf10c",
|
|
562
573
|
// pick: | alt: •
|
|
563
574
|
"format.bullet": "\uf111",
|
|
564
575
|
// pick: – | alt: — ― -
|
|
@@ -731,6 +742,8 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
731
742
|
// Checkboxes
|
|
732
743
|
"checkbox.checked": "[x]",
|
|
733
744
|
"checkbox.unchecked": "[ ]",
|
|
745
|
+
"radio.selected": "(o)",
|
|
746
|
+
"radio.unselected": "( )",
|
|
734
747
|
"format.bullet": "*",
|
|
735
748
|
"format.dash": "-",
|
|
736
749
|
"format.bracketLeft": "[",
|
|
@@ -1277,6 +1290,16 @@ export class Theme {
|
|
|
1277
1290
|
#bgColors: Record<ThemeBg, string>;
|
|
1278
1291
|
#symbols: SymbolMap;
|
|
1279
1292
|
#spinnerFramesOverrides: Partial<Record<SpinnerType, string[]>>;
|
|
1293
|
+
/**
|
|
1294
|
+
* Perceptual luma (0..1) of the status-line background — used to classify the
|
|
1295
|
+
* theme light/dark. Undefined when it can't be resolved. Classified against the
|
|
1296
|
+
* status line (the surface session accents render on) rather than the chat bubble
|
|
1297
|
+
* (`userMessageBg`), which some themes (e.g. `porcelain`) style dark on an
|
|
1298
|
+
* otherwise-light theme.
|
|
1299
|
+
*/
|
|
1300
|
+
readonly statusLineLuminance: number | undefined;
|
|
1301
|
+
/** WCAG relative luminance of the status-line background — basis for accent contrast. */
|
|
1302
|
+
readonly #statusLineContrastLuminance: number | undefined;
|
|
1280
1303
|
|
|
1281
1304
|
constructor(
|
|
1282
1305
|
fgColors: Record<ThemeColor, string | number>,
|
|
@@ -1286,6 +1309,8 @@ export class Theme {
|
|
|
1286
1309
|
symbolOverrides: Partial<Record<SymbolKey, string>>,
|
|
1287
1310
|
spinnerFramesOverrides: Partial<Record<SpinnerType, string[]>> = {},
|
|
1288
1311
|
) {
|
|
1312
|
+
this.statusLineLuminance = colorLuma(bgColors.statusLineBg);
|
|
1313
|
+
this.#statusLineContrastLuminance = relativeLuminance(bgColors.statusLineBg);
|
|
1289
1314
|
this.#fgColors = {} as Record<ThemeColor, string>;
|
|
1290
1315
|
for (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {
|
|
1291
1316
|
this.#fgColors[key] = fgAnsi(value, mode);
|
|
@@ -1307,6 +1332,19 @@ export class Theme {
|
|
|
1307
1332
|
this.#spinnerFramesOverrides = spinnerFramesOverrides;
|
|
1308
1333
|
}
|
|
1309
1334
|
|
|
1335
|
+
/** True when the active theme has a light status-line background. */
|
|
1336
|
+
get isLight(): boolean {
|
|
1337
|
+
return this.statusLineLuminance !== undefined && this.statusLineLuminance > 0.5;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* Surface luminance to size session accents against on light themes; undefined on
|
|
1342
|
+
* dark themes so accents stay vivid. Pass straight to `getSessionAccentHex`.
|
|
1343
|
+
*/
|
|
1344
|
+
get accentSurfaceLuminance(): number | undefined {
|
|
1345
|
+
return this.isLight ? this.#statusLineContrastLuminance : undefined;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1310
1348
|
fg(color: ThemeColor, text: string): string {
|
|
1311
1349
|
const ansi = this.#fgColors[color];
|
|
1312
1350
|
if (!ansi) throw new Error(`Unknown theme color: ${color}`);
|
|
@@ -1570,6 +1608,13 @@ export class Theme {
|
|
|
1570
1608
|
};
|
|
1571
1609
|
}
|
|
1572
1610
|
|
|
1611
|
+
get radio() {
|
|
1612
|
+
return {
|
|
1613
|
+
selected: this.#symbols["radio.selected"],
|
|
1614
|
+
unselected: this.#symbols["radio.unselected"],
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1573
1618
|
get format() {
|
|
1574
1619
|
return {
|
|
1575
1620
|
bullet: this.#symbols["format.bullet"],
|
|
@@ -2312,13 +2357,8 @@ export function isLightTheme(themeName?: string): boolean {
|
|
|
2312
2357
|
}
|
|
2313
2358
|
try {
|
|
2314
2359
|
const resolved = resolveVarRefs(themeJson.colors.userMessageBg, themeJson.vars ?? {});
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
const g = parseInt(resolved.slice(3, 5), 16) / 255;
|
|
2318
|
-
const b = parseInt(resolved.slice(5, 7), 16) / 255;
|
|
2319
|
-
// Relative luminance (ITU-R BT.709)
|
|
2320
|
-
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
2321
|
-
return luminance > 0.5;
|
|
2360
|
+
const luminance = colorLuma(resolved);
|
|
2361
|
+
return luminance !== undefined && luminance > 0.5;
|
|
2322
2362
|
} catch {
|
|
2323
2363
|
return false;
|
|
2324
2364
|
}
|
|
@@ -267,6 +267,7 @@ export class UiHelpers {
|
|
|
267
267
|
this.ctx.hideThinkingBlock,
|
|
268
268
|
() => this.ctx.ui.requestRender(),
|
|
269
269
|
this.ctx.session.extensionRunner?.getAssistantThinkingRenderers(),
|
|
270
|
+
this.ctx.ui.imageBudget,
|
|
270
271
|
);
|
|
271
272
|
this.ctx.chatContainer.addChild(assistantComponent);
|
|
272
273
|
break;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
3
|
+
|
|
4
|
+
/** The session's active plan, resolved for handoff into a subagent's context. */
|
|
5
|
+
export interface OverallPlanReference {
|
|
6
|
+
/** The `local://` reference path (e.g. `local://my-feature.md`), kept for display. */
|
|
7
|
+
path: string;
|
|
8
|
+
/** The full plan markdown, as written to disk. */
|
|
9
|
+
content: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Load the session's active overall plan for subagent handoff.
|
|
14
|
+
*
|
|
15
|
+
* Returns the plan referenced by `planReferencePath` when it exists on disk with
|
|
16
|
+
* non-empty content, or `undefined` when there is no plan (the file is absent or
|
|
17
|
+
* empty). This mirrors `AgentSession.#buildPlanReferenceMessage`'s gating so a
|
|
18
|
+
* subagent sees exactly the plan the main agent treats as its active reference.
|
|
19
|
+
*
|
|
20
|
+
* Callers MUST skip this during plan mode itself — read-only plan exploration
|
|
21
|
+
* uses a different prompt and a draft plan should not be handed off as approved.
|
|
22
|
+
*/
|
|
23
|
+
export async function loadOverallPlanReference(
|
|
24
|
+
planReferencePath: string,
|
|
25
|
+
localProtocolOptions: LocalProtocolOptions,
|
|
26
|
+
): Promise<OverallPlanReference | undefined> {
|
|
27
|
+
const resolved = resolveLocalUrlToPath(planReferencePath, localProtocolOptions);
|
|
28
|
+
let content: string;
|
|
29
|
+
try {
|
|
30
|
+
content = await Bun.file(resolved).text();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (isEnoent(error)) return undefined;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
if (!content.trim()) return undefined;
|
|
36
|
+
return { path: planReferencePath, content };
|
|
37
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getReadToolPath, type ProtectedToolContext } from "@oh-my-pi/pi-agent-core/compaction/tool-protection";
|
|
2
|
+
import { normalizeLocalScheme } from "../tools/path-utils";
|
|
3
|
+
|
|
4
|
+
/** Canonical plan alias every session's `local://` root resolves. */
|
|
5
|
+
const LOCAL_PLAN_ALIAS = "local://PLAN.md";
|
|
6
|
+
|
|
7
|
+
/** True when `readPath` targets `planTarget`, ignoring `local:/` vs `local://`
|
|
8
|
+
* scheme spelling and any trailing read selector (`:1-50`, `:raw`, …). */
|
|
9
|
+
function readTargetsPlan(readPath: string, planTarget: string): boolean {
|
|
10
|
+
const read = normalizeLocalScheme(readPath);
|
|
11
|
+
const target = normalizeLocalScheme(planTarget);
|
|
12
|
+
return read === target || read.startsWith(`${target}:`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build a compaction protection matcher that keeps `read` results for the active
|
|
17
|
+
* plan file intact through prune/shake — the plan analog of skill-read
|
|
18
|
+
* protection. Matches both the canonical `local://PLAN.md` alias and the
|
|
19
|
+
* session's current plan reference path (e.g. a titled `local://<title>.md`), so
|
|
20
|
+
* the plan survives compaction whether the agent reads it by alias or by title.
|
|
21
|
+
*
|
|
22
|
+
* `getPlanReferencePath` is evaluated at match time so a mid-session retitle
|
|
23
|
+
* (plan approval renames `PLAN.md` → `<title>.md`) is honored immediately.
|
|
24
|
+
*/
|
|
25
|
+
export function createPlanReadMatcher(getPlanReferencePath: () => string): (context: ProtectedToolContext) => boolean {
|
|
26
|
+
return (context: ProtectedToolContext) => {
|
|
27
|
+
const path = getReadToolPath(context);
|
|
28
|
+
if (path === undefined) return false;
|
|
29
|
+
return readTargetsPlan(path, LOCAL_PLAN_ALIAS) || readTargetsPlan(path, getPlanReferencePath());
|
|
30
|
+
};
|
|
31
|
+
}
|
package/src/priority.json
CHANGED
|
@@ -16,7 +16,7 @@ This is an autonomous continuation. The objective persists across turns; do not
|
|
|
16
16
|
|
|
17
17
|
Before calling `goal({op:"complete"})`, you MUST perform a completion audit against the current repo state:
|
|
18
18
|
|
|
19
|
-
1. **Restate the objective as concrete deliverables.** What files, behaviors, tests, gates, or artifacts must exist for the objective to be true? Write them down (
|
|
19
|
+
1. **Restate the objective as concrete deliverables.** What files, behaviors, tests, gates, or artifacts must exist for the objective to be true? Write them down (todo, or in your reasoning).
|
|
20
20
|
2. **Map each deliverable to evidence.** For every requirement, identify the authoritative source that would prove it: a file's contents, a command's output, a test's pass status, a PR/issue state.
|
|
21
21
|
3. **Inspect the actual current state.** Read the files. Run the commands. Check the tests. Do not rely on memory of earlier work in this session — the repo may have changed.
|
|
22
22
|
4. **Match verification scope to claim scope.** A narrow check (one file passes its unit test) does not prove a broad claim (the feature works end-to-end).
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<system-reminder>
|
|
2
2
|
Before substantive work, create a phased todo.
|
|
3
3
|
|
|
4
|
-
You MUST call `
|
|
4
|
+
You MUST call `todo` first in this turn.
|
|
5
5
|
You MUST initialize the todo list with a single `init` op.
|
|
6
6
|
You MUST cover the entire request from investigation through implementation and verification — not just the next immediate step.
|
|
7
7
|
Task descriptions MUST be specific. A future turn MUST execute them without re-planning.
|
|
8
8
|
You MUST keep task `content` to a short label (5-10 words). Put file paths, implementation steps, and specifics in `details`.
|
|
9
9
|
You MUST keep exactly one task `in_progress` and all later tasks `pending`.
|
|
10
10
|
|
|
11
|
-
After `
|
|
12
|
-
Do not call `
|
|
11
|
+
After `todo` succeeds, continue the request in the same turn.
|
|
12
|
+
Do not call `todo` again unless task state materially changed.
|
|
13
13
|
</system-reminder>
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
The user's message above is an **orchestration request**. Execute it as the orchestrator under the contract below. This contract overrides any default tendency to yield early, narrate, or do the work yourself.
|
|
3
3
|
|
|
4
4
|
<role>
|
|
5
|
-
You decompose, dispatch, verify, and iterate. Substantial and parallelizable work goes through `task` subagents — that is the whole point of orchestrating. But you are not forbidden from touching the tree: a trivial, self-contained edit is yours to make directly when spawning a subagent for it would cost more than the edit itself. Your tool budget is: reading for planning, `task` for dispatch, `edit`/`write` for trivial inline fixes only, verification (`bun check`, `bun test`, `lsp diagnostics`), git via `bash`, and `
|
|
5
|
+
You decompose, dispatch, verify, and iterate. Substantial and parallelizable work goes through `task` subagents — that is the whole point of orchestrating. But you are not forbidden from touching the tree: a trivial, self-contained edit is yours to make directly when spawning a subagent for it would cost more than the edit itself. Your tool budget is: reading for planning, `task` for dispatch, `edit`/`write` for trivial inline fixes only, verification (`bun check`, `bun test`, `lsp diagnostics`), git via `bash`, and `todo` for tracking.
|
|
6
6
|
</role>
|
|
7
7
|
|
|
8
8
|
<rules>
|
|
9
9
|
1. **Do not yield until everything is closed.** A phase finishing is *not* a yield point — launch the next phase in the same turn. Stop only when every requested item is verifiably done, or you hit a concrete [blocked] state that genuinely requires the user.
|
|
10
|
-
2. **Enumerate the full surface before dispatching.** If the request references audits, plans, checklists, phase lists, or file lists, expand them into a flat set of items in `
|
|
10
|
+
2. **Enumerate the full surface before dispatching.** If the request references audits, plans, checklists, phase lists, or file lists, expand them into a flat set of items in `todo`. "Most of them" or "the important ones" is failure. Re-read the source documents — do not work from memory.
|
|
11
11
|
3. **Parallelize maximally; never launch a one-off task.** Every set of edits with disjoint file scope MUST ship as one `task` batch — fan the work as wide as it decomposes. A single-task batch for divisible work is a failure: split it. If you are about to dispatch exactly one subagent, stop — either there is more to run alongside it (find it and batch them) or the change is small enough to make inline yourself (do it). Serialize only when one subagent produces a contract (types, schema, shared module) the next consumes — and state the dependency when you do.
|
|
12
12
|
4. **Each `task` assignment is self-contained.** Subagents have no shared context. Spell out: target files (≤3–5 explicit paths, no globs), the change with APIs and patterns, edge cases, and observable acceptance criteria. Do not assume they read the same plan you did.
|
|
13
13
|
5. **Verify after every phase before launching the next.** Run the appropriate gate: `bun check` for types, package-scoped `bun test` for behavior, `lsp diagnostics` for changed files. If a phase introduced breakage, dispatch fix-up subagents *before* moving on. Never declare a phase done on a red tree.
|
|
@@ -20,12 +20,12 @@ You decompose, dispatch, verify, and iterate. Substantial and parallelizable wor
|
|
|
20
20
|
|
|
21
21
|
<workflow>
|
|
22
22
|
1. **Ingest.** Read every referenced file (audits, plans, prior agent output, current branch state). Run `git status` to see uncommitted changes.
|
|
23
|
-
2. **Plan.** Materialize the full work surface in `
|
|
23
|
+
2. **Plan.** Materialize the full work surface in `todo` as ordered phases. Within each phase, list the parallelizable units.
|
|
24
24
|
3. **Dispatch phase.** Launch all parallel `task` subagents in one call. Wait for the batch.
|
|
25
25
|
4. **Verify phase.** Run the gates. On failure, dispatch fix-up subagents and re-verify. Do not advance with a red gate.
|
|
26
26
|
5. **Commit phase** (if applicable). Focused message naming the phase.
|
|
27
|
-
6. **Advance.** Mark the phase done in `
|
|
28
|
-
7. **Final verification.** When the last phase is green, run the full gate set once more and confirm every `
|
|
27
|
+
6. **Advance.** Mark the phase done in `todo`, immediately start the next phase. No summary message between phases — keep going.
|
|
28
|
+
7. **Final verification.** When the last phase is green, run the full gate set once more and confirm every `todo` item is closed. Then yield with a terse status, not a recap.
|
|
29
29
|
</workflow>
|
|
30
30
|
|
|
31
31
|
<anti-patterns>
|
|
@@ -1,28 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
Plan approved. You MUST execute it now.
|
|
3
|
-
</critical>
|
|
4
|
-
|
|
5
|
-
Finalized plan artifact: `{{finalPlanFilePath}}`
|
|
1
|
+
Plan approved.
|
|
6
2
|
{{#if contextPreserved}}
|
|
7
|
-
Context preserved. Use conversation history when useful;
|
|
8
|
-
{{else}}
|
|
9
|
-
Execution may be in fresh context. Treat the finalized plan as the source of truth.
|
|
3
|
+
- Context preserved. Use conversation history when useful; this plan is the source of truth if it conflicts with earlier exploration.
|
|
10
4
|
{{/if}}
|
|
11
5
|
|
|
12
|
-
## Plan
|
|
13
|
-
|
|
14
|
-
{{planContent}}
|
|
15
|
-
|
|
16
6
|
<instruction>
|
|
17
|
-
You MUST execute this plan step by step
|
|
7
|
+
You MUST execute this plan step by step. You have full tool access.
|
|
18
8
|
You MUST verify each step before proceeding to the next.
|
|
19
|
-
{{#has tools "
|
|
20
|
-
Before execution, initialize todo tracking with `
|
|
21
|
-
After each completed step, immediately update `
|
|
22
|
-
If `
|
|
9
|
+
{{#has tools "todo"}}
|
|
10
|
+
Before execution, initialize todo tracking with `todo`.
|
|
11
|
+
After each completed step, immediately update `todo`.
|
|
12
|
+
If `todo` fails, fix the payload and retry before continuing.
|
|
23
13
|
{{/has}}
|
|
14
|
+
The plan path is for subagent handoff only. You already have the plan; NEVER read it.
|
|
24
15
|
</instruction>
|
|
25
16
|
|
|
17
|
+
The full plan is injected below. You MUST execute it now:
|
|
18
|
+
|
|
19
|
+
<plan path="{{finalPlanFilePath}}">
|
|
20
|
+
{{planContent}}
|
|
21
|
+
</plan>
|
|
22
|
+
|
|
26
23
|
<critical>
|
|
27
24
|
You MUST keep going until complete. This matters.
|
|
28
25
|
</critical>
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
## Existing Plan
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<details>
|
|
6
|
-
<summary>Plan contents</summary>
|
|
7
|
-
|
|
3
|
+
<plan path="{{planFilePath}}">
|
|
8
4
|
{{planContent}}
|
|
9
|
-
</
|
|
5
|
+
</plan>
|
|
10
6
|
|
|
11
7
|
<instruction>
|
|
12
8
|
If this plan is relevant to current work and not complete, you MUST continue executing it.
|
|
13
9
|
If the plan is stale or unrelated, you MUST ignore it.
|
|
10
|
+
The plan path is for subagent handoff only. You already have the plan; NEVER read it.
|
|
14
11
|
</instruction>
|
|
@@ -10,6 +10,17 @@ CONTEXT
|
|
|
10
10
|
{{context}}
|
|
11
11
|
{{/if}}
|
|
12
12
|
|
|
13
|
+
{{#if planReference}}
|
|
14
|
+
PLAN
|
|
15
|
+
===================================
|
|
16
|
+
|
|
17
|
+
This session is executing an approved plan. Your assignment above is one part of it — use the plan to understand how your piece fits the whole and to stay consistent with decisions already made. Where the plan and your specific assignment conflict, the assignment wins. The plan path is for reference; you already have its full contents below, so NEVER re-read it.
|
|
18
|
+
|
|
19
|
+
<plan path="{{planReferencePath}}">
|
|
20
|
+
{{planReference}}
|
|
21
|
+
</plan>
|
|
22
|
+
{{/if}}
|
|
23
|
+
|
|
13
24
|
COOP
|
|
14
25
|
===================================
|
|
15
26
|
|
|
@@ -62,7 +62,7 @@ Scale to the ask: "find any bugs" → a few finders, single-vote verify. "thorou
|
|
|
62
62
|
</patterns>
|
|
63
63
|
|
|
64
64
|
<execution>
|
|
65
|
-
- Decompose the surface first; capture it in `
|
|
65
|
+
- Decompose the surface first; capture it in `todo` when it spans phases.
|
|
66
66
|
- Prefer `schema=` for any agent whose output you branch on.
|
|
67
67
|
- After a fan-out returns, YOU own correctness: read the artifacts, run the gate, verify before acting. Subagents do the legwork; they don't get the last word.
|
|
68
68
|
- Keep going until the task is closed — a returned fan-out is a step, not a stopping point.
|