@pi-unipi/subagents 2.6.1 → 2.6.2

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.
Files changed (107) hide show
  1. package/README.md +89 -78
  2. package/agents/delegate.md +14 -0
  3. package/agents/oracle.md +78 -0
  4. package/agents/researcher.md +52 -0
  5. package/agents/reviewer.md +79 -0
  6. package/agents/scout.md +50 -0
  7. package/agents/worker.md +59 -0
  8. package/package.json +19 -17
  9. package/prompts/council.md +48 -0
  10. package/prompts/gather-context-and-clarify.md +13 -0
  11. package/prompts/parallel-cleanup.md +59 -0
  12. package/prompts/parallel-research.md +50 -0
  13. package/prompts/parallel-review.md +54 -0
  14. package/prompts/review-loop.md +43 -0
  15. package/skills/council-mode/SKILL.md +230 -0
  16. package/skills/subagents/SKILL.md +49 -0
  17. package/skills/subagents/references/constraints-and-recipes.md +259 -0
  18. package/skills/subagents/references/execution-controls.md +454 -0
  19. package/skills/subagents/references/management-authoring-rpc.md +161 -0
  20. package/skills/subagents/references/multi-lane-orchestration.md +39 -0
  21. package/skills/subagents/references/prompting-and-roles.md +267 -0
  22. package/src/acceptance.ts +331 -0
  23. package/src/agent-manager.ts +405 -0
  24. package/src/agent-memory.ts +300 -0
  25. package/src/agent-overrides.ts +183 -0
  26. package/src/agent-runner.ts +265 -0
  27. package/src/async-runner.ts +417 -0
  28. package/src/authority-policy.ts +47 -0
  29. package/src/budgets.ts +285 -0
  30. package/src/child-safety.ts +140 -0
  31. package/src/config.ts +226 -0
  32. package/src/conversation-viewer.ts +281 -0
  33. package/src/core-compat.ts +94 -0
  34. package/src/custom-agents.ts +323 -0
  35. package/src/file-system-retry.ts +50 -0
  36. package/src/fleet-data.ts +61 -0
  37. package/src/fleet-view.ts +316 -0
  38. package/src/foreground-detach.ts +59 -0
  39. package/src/fork-context.ts +285 -0
  40. package/src/global.d.ts +7 -0
  41. package/src/guide.ts +129 -0
  42. package/src/index.ts +1149 -0
  43. package/src/mission-state.ts +133 -0
  44. package/src/mission-store.ts +434 -0
  45. package/src/model-resolver.ts +79 -0
  46. package/src/output-limits.ts +142 -0
  47. package/src/parity-types.ts +315 -0
  48. package/src/pi-args.ts +161 -0
  49. package/src/pi-spawn.ts +147 -0
  50. package/src/result-files.ts +232 -0
  51. package/src/result-watcher.ts +221 -0
  52. package/src/retained-children.ts +176 -0
  53. package/src/run-fanout-budget.ts +356 -0
  54. package/src/scheduled-runs.ts +351 -0
  55. package/src/schemas.ts +263 -0
  56. package/src/slash-commands.ts +77 -0
  57. package/src/supervisor-channel.ts +249 -0
  58. package/src/tool-handler.ts +1144 -0
  59. package/src/types.ts +238 -0
  60. package/src/widget.ts +447 -0
  61. package/src/workflow-script.ts +737 -0
  62. package/src/workflow-worker.ts +384 -0
  63. package/src/worktree.ts +614 -0
  64. package/dist/agent-manager.d.ts +0 -81
  65. package/dist/agent-manager.d.ts.map +0 -1
  66. package/dist/agent-manager.js +0 -295
  67. package/dist/agent-manager.js.map +0 -1
  68. package/dist/agent-runner.d.ts +0 -51
  69. package/dist/agent-runner.d.ts.map +0 -1
  70. package/dist/agent-runner.js +0 -262
  71. package/dist/agent-runner.js.map +0 -1
  72. package/dist/config.d.ts +0 -24
  73. package/dist/config.d.ts.map +0 -1
  74. package/dist/config.js +0 -132
  75. package/dist/config.js.map +0 -1
  76. package/dist/conversation-viewer.d.ts +0 -40
  77. package/dist/conversation-viewer.d.ts.map +0 -1
  78. package/dist/conversation-viewer.js +0 -276
  79. package/dist/conversation-viewer.js.map +0 -1
  80. package/dist/core-compat.d.ts +0 -21
  81. package/dist/core-compat.d.ts.map +0 -1
  82. package/dist/core-compat.js +0 -86
  83. package/dist/core-compat.js.map +0 -1
  84. package/dist/custom-agents.d.ts +0 -14
  85. package/dist/custom-agents.d.ts.map +0 -1
  86. package/dist/custom-agents.js +0 -113
  87. package/dist/custom-agents.js.map +0 -1
  88. package/dist/file-lock.d.ts +0 -42
  89. package/dist/file-lock.d.ts.map +0 -1
  90. package/dist/file-lock.js +0 -91
  91. package/dist/file-lock.js.map +0 -1
  92. package/dist/index.d.ts +0 -10
  93. package/dist/index.d.ts.map +0 -1
  94. package/dist/index.js +0 -758
  95. package/dist/index.js.map +0 -1
  96. package/dist/model-resolver.d.ts +0 -19
  97. package/dist/model-resolver.d.ts.map +0 -1
  98. package/dist/model-resolver.js +0 -61
  99. package/dist/model-resolver.js.map +0 -1
  100. package/dist/types.d.ts +0 -98
  101. package/dist/types.d.ts.map +0 -1
  102. package/dist/types.js +0 -47
  103. package/dist/types.js.map +0 -1
  104. package/dist/widget.d.ts +0 -56
  105. package/dist/widget.d.ts.map +0 -1
  106. package/dist/widget.js +0 -396
  107. package/dist/widget.js.map +0 -1
@@ -0,0 +1,285 @@
1
+ /**
2
+ * @pi-unipi/subagents — Fork context resolution
3
+ *
4
+ * Ported from pi-subagents src/shared/fork-context.ts. Creates branched child
5
+ * sessions from the parent session file, sanitizes signed Anthropic thinking
6
+ * blocks (redacted_thinking always; signed thinking on Anthropic models),
7
+ * appends a thinking_level_change:off entry when sanitization occurred, and
8
+ * aligns the forked session cwd to the child launch cwd. Fork files nest
9
+ * under <parent-session-dir>/<parent-stem>/forks/ so top-level session
10
+ * discovery never picks them up.
11
+ */
12
+
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+ import { randomUUID } from "node:crypto";
16
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
17
+
18
+ export type SubagentExecutionContext = "fresh" | "fork";
19
+
20
+ interface BranchSessionEntry {
21
+ type: string;
22
+ id?: string;
23
+ parentId?: string | null;
24
+ timestamp?: string;
25
+ message?: {
26
+ role?: string;
27
+ content?: unknown;
28
+ provider?: string;
29
+ api?: string;
30
+ model?: string;
31
+ };
32
+ thinkingLevel?: string;
33
+ cwd?: string;
34
+ [extra: string]: unknown;
35
+ }
36
+
37
+ export interface PreferredForkAvailability {
38
+ getSessionFile(): string | undefined;
39
+ getLeafId(): string | null;
40
+ }
41
+
42
+ export interface PreferredForkSnapshot {
43
+ parentSessionFile?: string;
44
+ leafId?: string | null;
45
+ }
46
+
47
+ export function resolveSubagentContext(value: unknown): SubagentExecutionContext {
48
+ return value === "fork" ? "fork" : "fresh";
49
+ }
50
+
51
+ export function canPreferFork(sessionManager: PreferredForkAvailability): boolean {
52
+ const parentSessionFile = sessionManager.getSessionFile();
53
+ const leafId = sessionManager.getLeafId();
54
+ if (!parentSessionFile || !leafId) return false;
55
+ try {
56
+ return fs.existsSync(parentSessionFile);
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ export function canPreferForkFromSnapshot(input: PreferredForkSnapshot): boolean {
63
+ if (!input.parentSessionFile || !input.leafId) return false;
64
+ try {
65
+ return fs.existsSync(input.parentSessionFile);
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ /** Anthropic provider/api models require thinking off on a sanitized fork. */
72
+ export function forkedChildRequiresThinkingOff(
73
+ model: string | undefined,
74
+ modelInfoProvider?: string,
75
+ modelInfoApi?: string,
76
+ ): boolean {
77
+ if (!model) return true;
78
+ if (modelInfoProvider?.toLowerCase() === "anthropic") return true;
79
+ if (modelInfoApi?.toLowerCase() === "anthropic-messages") return true;
80
+ // Unknown models stay conservative (reference rule).
81
+ if (modelInfoProvider === undefined && modelInfoApi === undefined) return true;
82
+ return false;
83
+ }
84
+
85
+ function isUnsafeAnthropicThinkingBlock(message: BranchSessionEntry["message"], block: unknown): boolean {
86
+ if (!message || !block || typeof block !== "object" || !("type" in block)) return false;
87
+ const provider = typeof message.provider === "string" ? message.provider.toLowerCase() : "";
88
+ const api = typeof message.api === "string" ? message.api.toLowerCase() : "";
89
+ const model = typeof message.model === "string" ? message.model.toLowerCase() : "";
90
+ const isAnthropic = provider === "anthropic" || api === "anthropic-messages" || model.startsWith("anthropic/");
91
+ const b = block as Record<string, unknown>;
92
+ if (b.type === "redacted_thinking") return true;
93
+ if (b.type !== "thinking" || !isAnthropic) return false;
94
+ const signature =
95
+ "thinkingSignature" in b ? b.thinkingSignature : "signature" in b ? b.signature : undefined;
96
+ return b.redacted === true || (typeof signature === "string" && (signature as string).length > 0);
97
+ }
98
+
99
+ function createEntryId(entries: BranchSessionEntry[]): string {
100
+ const ids = new Set(entries.map((entry) => entry.id).filter((id): id is string => typeof id === "string"));
101
+ for (let attempt = 0; attempt < 100; attempt++) {
102
+ const id = randomUUID().slice(0, 8);
103
+ if (!ids.has(id)) return id;
104
+ }
105
+ return randomUUID();
106
+ }
107
+
108
+ function appendThinkingOffEntry(entries: BranchSessionEntry[]): void {
109
+ const last = entries[entries.length - 1];
110
+ if (last?.type === "thinking_level_change" && last.thinkingLevel === "off") return;
111
+ const parent = [...entries].reverse().find((entry) => typeof entry.id === "string");
112
+ entries.push({
113
+ type: "thinking_level_change",
114
+ id: createEntryId(entries),
115
+ parentId: parent?.id ?? null,
116
+ timestamp: new Date().toISOString(),
117
+ thinkingLevel: "off",
118
+ });
119
+ }
120
+
121
+ /** Strip signed/redacted Anthropic thinking blocks from forked entries. */
122
+ export function sanitizeUnsafeThinkingBlocks(entries: BranchSessionEntry[]): boolean {
123
+ let sanitized = false;
124
+ for (const entry of entries) {
125
+ if (entry.type !== "message" || entry.message?.role !== "assistant" || !Array.isArray(entry.message.content)) continue;
126
+ const filtered = (entry.message.content as unknown[]).filter(
127
+ (block) => !isUnsafeAnthropicThinkingBlock(entry.message, block),
128
+ );
129
+ if (filtered.length === (entry.message.content as unknown[]).length) continue;
130
+ entry.message.content = filtered;
131
+ sanitized = true;
132
+ }
133
+ return sanitized;
134
+ }
135
+
136
+ function readSessionEntries(sessionFile: string): BranchSessionEntry[] {
137
+ const lines = fs.readFileSync(sessionFile, "utf-8").split("\n").filter((line) => line.trim().length > 0);
138
+ return lines.map((line, index) => {
139
+ try {
140
+ return JSON.parse(line) as BranchSessionEntry;
141
+ } catch (error) {
142
+ const cause = error instanceof Error ? error : new Error(String(error));
143
+ throw new Error(
144
+ `Unable to inspect forked session ${sessionFile}: invalid JSONL on line ${index + 1}: ${cause.message}`,
145
+ { cause },
146
+ );
147
+ }
148
+ });
149
+ }
150
+
151
+ /** Align a forked session's cwd to the child launch cwd. */
152
+ export function alignForkedSessionCwd(sessionFile: string, cwd: string): void {
153
+ const entries = readSessionEntries(sessionFile);
154
+ const header = entries[0];
155
+ if (header?.type !== "session") {
156
+ throw new Error(`Forked session ${sessionFile} does not start with a session header.`);
157
+ }
158
+ const resolvedCwd = path.resolve(cwd);
159
+ const effectiveCwd = fs.realpathSync.native(resolvedCwd);
160
+ if (header.cwd === effectiveCwd) return;
161
+ header.cwd = effectiveCwd;
162
+ fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
163
+ }
164
+
165
+ export interface ForkContextResolution {
166
+ sessionFile: string;
167
+ thinkingOverride?: "off";
168
+ }
169
+
170
+ export interface ForkContextResolver {
171
+ sessionFileForIndex(index?: number): string | undefined;
172
+ thinkingOverrideForIndex(index?: number): "off" | undefined;
173
+ }
174
+
175
+ interface ForkableSessionManager extends PreferredForkAvailability {
176
+ openSession?: (file: string, dir?: string) => {
177
+ createBranchedSession(leafId: string): string | undefined;
178
+ getHeader?: () => BranchSessionEntry | undefined;
179
+ getEntries?: () => BranchSessionEntry[] | undefined;
180
+ };
181
+ }
182
+
183
+ export interface ForkContextResolverOptions {
184
+ openSession?: ForkableSessionManager["openSession"];
185
+ forceThinkingOffForIndex?: (index: number) => boolean;
186
+ }
187
+
188
+ /**
189
+ * Create the fork resolver. Errors follow reference semantics: an explicit
190
+ * fork requires a persisted parent session + current leaf, failing fast
191
+ * rather than silently downgrading to fresh.
192
+ */
193
+ export function createForkContextResolver(
194
+ sessionManager: ForkableSessionManager,
195
+ requestedContext: unknown,
196
+ options: ForkContextResolverOptions = {},
197
+ ): ForkContextResolver {
198
+ if (resolveSubagentContext(requestedContext) !== "fork") {
199
+ return {
200
+ sessionFileForIndex: () => undefined,
201
+ thinkingOverrideForIndex: () => undefined,
202
+ };
203
+ }
204
+
205
+ const parentSessionFile = sessionManager.getSessionFile();
206
+ if (!parentSessionFile) {
207
+ throw new Error("Forked subagent context requires a persisted parent session.");
208
+ }
209
+
210
+ const leafId = sessionManager.getLeafId();
211
+ if (!leafId) {
212
+ throw new Error("Forked subagent context requires a current leaf to fork from.");
213
+ }
214
+
215
+ const openSession =
216
+ options.openSession ?? sessionManager.openSession ?? ((file: string, dir?: string) => SessionManager.open(file, dir));
217
+
218
+ // Fork files nest under <parent-dir>/<parent-stem>/forks/ so top-level
219
+ // session discovery (largest-mtime *.jsonl, non-recursive) never hijacks.
220
+ const sessionDir = path.join(
221
+ path.dirname(parentSessionFile),
222
+ path.basename(parentSessionFile, ".jsonl"),
223
+ "forks",
224
+ );
225
+ const cachedResolutions = new Map<number, ForkContextResolution>();
226
+
227
+ const resolveFork = (index = 0): ForkContextResolution => {
228
+ const cached = cachedResolutions.get(index);
229
+ if (cached) return cached;
230
+ try {
231
+ if (!fs.existsSync(parentSessionFile)) {
232
+ throw new Error(
233
+ `Parent session file does not exist: ${parentSessionFile}. Pi has not persisted enough history to fork yet.`,
234
+ );
235
+ }
236
+ const sourceManager = openSession(parentSessionFile, sessionDir);
237
+ const sessionFile = sourceManager.createBranchedSession(leafId);
238
+ if (!sessionFile) {
239
+ throw new Error("Session manager did not return a forked session file.");
240
+ }
241
+ const forceThinkingOff = (sanitized: boolean): boolean =>
242
+ sanitized && (options.forceThinkingOffForIndex?.(index) ?? true);
243
+ let thinkingOverride: "off" | undefined;
244
+ if (!fs.existsSync(sessionFile)) {
245
+ const header = sourceManager.getHeader?.();
246
+ const entries = sourceManager.getEntries?.() as BranchSessionEntry[] | undefined;
247
+ if (!header || !entries) {
248
+ throw new Error(
249
+ `Session manager returned a forked session file that does not exist and cannot be persisted by fallback: ${sessionFile}`,
250
+ );
251
+ }
252
+ if (forceThinkingOff(sanitizeUnsafeThinkingBlocks(entries as BranchSessionEntry[]))) {
253
+ appendThinkingOffEntry(entries);
254
+ thinkingOverride = "off";
255
+ }
256
+ fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
257
+ fs.writeFileSync(sessionFile, `${[header, ...entries].map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
258
+ } else {
259
+ const entries = readSessionEntries(sessionFile);
260
+ if (sanitizeUnsafeThinkingBlocks(entries)) {
261
+ if (forceThinkingOff(true)) {
262
+ appendThinkingOffEntry(entries);
263
+ thinkingOverride = "off";
264
+ }
265
+ fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
266
+ }
267
+ }
268
+ const resolution = { sessionFile, ...(thinkingOverride ? { thinkingOverride } : {}) };
269
+ cachedResolutions.set(index, resolution);
270
+ return resolution;
271
+ } catch (error) {
272
+ const cause = error instanceof Error ? error : new Error(String(error));
273
+ throw new Error(`Failed to create forked subagent session: ${cause.message}`, { cause });
274
+ }
275
+ };
276
+
277
+ return {
278
+ sessionFileForIndex(index = 0): string | undefined {
279
+ return resolveFork(index).sessionFile;
280
+ },
281
+ thinkingOverrideForIndex(index = 0): "off" | undefined {
282
+ return resolveFork(index).thinkingOverride;
283
+ },
284
+ };
285
+ }
@@ -0,0 +1,7 @@
1
+ import type { InfoRegistryLike } from "@pi-unipi/core/global-types";
2
+
3
+ declare global {
4
+ var __unipi_info_registry: InfoRegistryLike | undefined;
5
+ }
6
+
7
+ export {};
package/src/guide.ts ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @pi-unipi/subagents — Guide topics (action: "guide")
3
+ *
4
+ * Bundled quick-reference for the spawn_helper guide action. Adapted from
5
+ * pi-subagents docs/ (condensed to agent-facing essentials, our tool names).
6
+ */
7
+
8
+ import { SUBAGENT_ACTIONS } from "./parity-types.js";
9
+
10
+ const TOPICS: Record<string, string> = {
11
+ overview: `# Subagents overview
12
+
13
+ spawn_helper delegates work to focused child agents; get_helper_result waits on
14
+ or inspects them. Builtins: explore/work (unipi) + scout/researcher/worker/
15
+ reviewer/oracle/delegate (parity). Custom agents live in
16
+ ~/.unipi/config/agents/*.md and <workspace>/.unipi/config/agents/*.md.
17
+
18
+ Execution modes:
19
+ - single child: { agent, task } (foreground or run_in_background)
20
+ - workflow: { workflowScript } — runs.run/runs.all/runs.steer in a sandbox
21
+ - management: { action } for list/get/status/children.list/resume/doctor/...
22
+
23
+ Rule of thumb: scout before you understand code, researcher before you trust
24
+ external facts, worker to implement, reviewer to check, oracle when the
25
+ decision itself feels risky.`,
26
+
27
+ workflows: `# Workflows
28
+
29
+ All orchestration is code-driven through workflowScript:
30
+
31
+ // sequential
32
+ const scan = await runs.run("scan", { agent: "scout", task: "Analyze auth" });
33
+ const fix = await runs.run("fix", { agent: "worker", task: "Implement: " + scan.output });
34
+ return fix.output;
35
+
36
+ // parallel fanout (await it; do not read .output from unawaited launches)
37
+ const reviews = await runs.all([
38
+ { key: "correctness", agent: "reviewer", task: "Review correctness" },
39
+ { key: "tests", agent: "reviewer", task: "Review tests" }
40
+ ]);
41
+ return reviews.map((r) => r.output);
42
+
43
+ Steering: await runs.steer(key, message, { mode: "auto" }) after a launch.
44
+ Plain helper functions returning runs.run(...) are fine; nested async helpers
45
+ are rejected. Recommended loop: clarify → scout → worker → fresh reviewers → worker.`,
46
+
47
+ agents: `# Agents
48
+
49
+ An agent is a markdown file: YAML frontmatter + system prompt.
50
+
51
+ Fields: name, description, tools (comma list), model, thinking, aliases,
52
+ systemPromptMode (replace|append), inheritProjectContext, inheritSkills,
53
+ defaultContext (fresh|fork), timeoutMs, toolTimeoutMs, maxSubagentDepth,
54
+ memory ({ scope: user|project, path: role-name }), enabled.
55
+
56
+ Discovery priority: project .unipi/config/agents > global ~/.unipi/config/agents
57
+ > builtin files > code builtins. agentOverrides in the subagents.json
58
+ "subagents" block can override any builtin field per agent; disableBuiltins
59
+ disables all builtins while custom agents keep working.`,
60
+
61
+ observability: `# Observability
62
+
63
+ FleetView (persistent panel): active work from both transports — in-process
64
+ agents and child-process runs. ↓/← activates; j/k navigate; enter inspects
65
+ (live transcript for local agents, result tail for process runs); esc closes.
66
+
67
+ spawn_helper({ action: "status" }) reports active work + spawn budgets.
68
+ children.list shows retained (completed) children with resumability.
69
+ Async completions arrive as <task-notification> follow-ups automatically.`,
70
+
71
+ "tool-reference": `# spawn_helper reference
72
+
73
+ Execution: { agent, task, async?, context?, timeoutMs?, maxTurns?, turnBudget?,
74
+ toolBudget?, usageBudget?, model?, thinking?, worktree? }
75
+ Legacy aliases: type (= agent), prompt (= task), run_in_background (= async),
76
+ max_turns (= maxTurns).
77
+
78
+ workflowScript: { workflowScript, async?, timeoutMs?, maxOutput? }
79
+
80
+ Management actions: ${SUBAGENT_ACTIONS.slice(0, 12).join(", ")}, ...
81
+
82
+ get_helper_result: { id, wait?, nonBlocking?, all?, timeoutMs?, stopOnAttention? }
83
+ - blocking wait returns when a run needs attention (default) or completes
84
+ - nonBlocking: persist a subscription and return immediately; the session is
85
+ woken on completion/failure/attention`,
86
+
87
+ configuration: `# Configuration
88
+
89
+ ~/.unipi/config/subagents.json (global) + <workspace>/.unipi/config/subagents.json
90
+ (workspace overrides global). The "subagents" block carries parity settings:
91
+
92
+ { "subagents": { "defaultModel", "defaultThinking", "agentOverrides",
93
+ "disableBuiltins", "asyncByDefault", "defaultSubagentContext", "timeoutMs",
94
+ "toolTimeoutMs", "maxSubagentSpawnsPerRun" (64), "maxSubagentSpawnsPerSession",
95
+ "maxActiveAsyncRunsPerSession", "maxSubagentDepth" (2), "parallel",
96
+ "fleetView", "fleetViewPlacement", "inlineToolDisplay", "resultScanLogging" } }
97
+
98
+ Env overrides: UNIPI_SUBAGENT_PI_BINARY, UNIPI_SUBAGENT_TASK_DELIVERY,
99
+ UNIPI_SUBAGENT_TOOL_TIMEOUT_MS, UNIPI_SUBAGENTS_WORKTREE_DIR, UNIPI_SUBAGENT_MAX_DEPTH.`,
100
+
101
+ missions: `# Missions (planned phase)
102
+
103
+ Durable mission records with delivery receipts, timed and recurring runs.
104
+ Not yet wired in this build — see the roadmap in the package README.`,
105
+
106
+ models: `# Models
107
+
108
+ Children inherit the parent model by default. Override per call ({ model }),
109
+ per agent (frontmatter model), or globally (subagents.defaultModel).
110
+ Thinking: per call ({ thinking }), per agent, or subagents.defaultThinking.
111
+ Fallback models: agent frontmatter fallbackModels (ordered).`,
112
+
113
+ watchdog: `# Watchdog (planned phase)
114
+
115
+ Opt-in adversarial change reviewer, scope monitoring, LSP diagnostics, child
116
+ tool permissions. Not yet wired in this build.`,
117
+
118
+ "extension-api": `# Extension API (planned phase)
119
+
120
+ Runtime agent registration (AgentManager.registerRuntimeAgent) exists; the
121
+ full RPC bridge for external hosts lands in a later phase.`,
122
+ };
123
+
124
+ export function buildGuideText(topic: string): string {
125
+ const normalized = topic.toLowerCase().trim();
126
+ const doc = TOPICS[normalized];
127
+ if (doc) return doc;
128
+ return `Unknown guide topic "${topic}". Available topics: ${Object.keys(TOPICS).join(", ")}.`;
129
+ }