@shanepadgett/tau-agent 0.1.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.
Files changed (138) hide show
  1. package/README.md +30 -0
  2. package/docs/extending-tau-agent.md +119 -0
  3. package/docs/subagents.md +57 -0
  4. package/docs/tui.md +107 -0
  5. package/extensions/appshot/README.md +11 -0
  6. package/extensions/appshot/capture.swift +191 -0
  7. package/extensions/appshot/index.ts +245 -0
  8. package/extensions/appshot/native-helper.ts +102 -0
  9. package/extensions/attention/README.md +23 -0
  10. package/extensions/attention/index.ts +71 -0
  11. package/extensions/auto-name/README.md +26 -0
  12. package/extensions/auto-name/index.ts +163 -0
  13. package/extensions/branch/README.md +22 -0
  14. package/extensions/branch/index.ts +145 -0
  15. package/extensions/branch/panel.ts +148 -0
  16. package/extensions/clear-screen/README.md +29 -0
  17. package/extensions/clear-screen/index.ts +75 -0
  18. package/extensions/commit/README.md +15 -0
  19. package/extensions/commit/commit-plan.ts +328 -0
  20. package/extensions/commit/git-change-set.ts +268 -0
  21. package/extensions/commit/index.ts +138 -0
  22. package/extensions/commit/review-ui.ts +745 -0
  23. package/extensions/explore/README.md +7 -0
  24. package/extensions/explore/autoread.ts +140 -0
  25. package/extensions/explore/find.ts +147 -0
  26. package/extensions/explore/grep.ts +749 -0
  27. package/extensions/explore/index.ts +17 -0
  28. package/extensions/explore/limits.ts +22 -0
  29. package/extensions/explore/ls.ts +113 -0
  30. package/extensions/explore/path-display.ts +41 -0
  31. package/extensions/explore/path-tree.ts +150 -0
  32. package/extensions/explore/read.ts +160 -0
  33. package/extensions/explore/result.ts +29 -0
  34. package/extensions/explore/traverse.ts +226 -0
  35. package/extensions/footer/README.md +23 -0
  36. package/extensions/footer/index.ts +534 -0
  37. package/extensions/footer/settings.ts +13 -0
  38. package/extensions/ideas/README.md +10 -0
  39. package/extensions/ideas/browser.ts +66 -0
  40. package/extensions/ideas/index.ts +31 -0
  41. package/extensions/ideas/store.ts +32 -0
  42. package/extensions/image-gen/README.md +11 -0
  43. package/extensions/image-gen/client.ts +186 -0
  44. package/extensions/image-gen/index.ts +152 -0
  45. package/extensions/manage-sessions/README.md +12 -0
  46. package/extensions/manage-sessions/index.ts +58 -0
  47. package/extensions/manage-sessions/manager-ui.ts +323 -0
  48. package/extensions/manage-sessions/sessions.ts +126 -0
  49. package/extensions/patch/README.md +48 -0
  50. package/extensions/patch/executor.ts +355 -0
  51. package/extensions/patch/index.ts +170 -0
  52. package/extensions/patch/matcher.ts +236 -0
  53. package/extensions/patch/parser.ts +347 -0
  54. package/extensions/patch/render.ts +247 -0
  55. package/extensions/patch/summary.ts +35 -0
  56. package/extensions/publish/README.md +11 -0
  57. package/extensions/publish/index.ts +225 -0
  58. package/extensions/qna/README.md +45 -0
  59. package/extensions/qna/additional-context-body.ts +51 -0
  60. package/extensions/qna/body-render.ts +6 -0
  61. package/extensions/qna/choice-question-body.ts +322 -0
  62. package/extensions/qna/index.ts +245 -0
  63. package/extensions/qna/inline-editor-row.ts +56 -0
  64. package/extensions/qna/input-question-body.ts +89 -0
  65. package/extensions/qna/model.ts +317 -0
  66. package/extensions/qna/panel.ts +240 -0
  67. package/extensions/qna/ui.ts +16 -0
  68. package/extensions/reference/README.md +47 -0
  69. package/extensions/reference/index.ts +76 -0
  70. package/extensions/reference/panel.ts +874 -0
  71. package/extensions/reference/settings.ts +29 -0
  72. package/extensions/run-summary/README.md +5 -0
  73. package/extensions/run-summary/index.ts +98 -0
  74. package/extensions/silent-command-runner/README.md +25 -0
  75. package/extensions/silent-command-runner/index.ts +468 -0
  76. package/extensions/silent-command-runner/settings.ts +65 -0
  77. package/extensions/soul/README.md +19 -0
  78. package/extensions/soul/index.ts +20 -0
  79. package/extensions/soul/prompt.ts +254 -0
  80. package/extensions/soul/settings.ts +13 -0
  81. package/extensions/stash/README.md +48 -0
  82. package/extensions/stash/browser.ts +51 -0
  83. package/extensions/stash/index.ts +55 -0
  84. package/extensions/stash/store.ts +36 -0
  85. package/extensions/subagent/README.md +34 -0
  86. package/extensions/subagent/agents/scout.md +85 -0
  87. package/extensions/subagent/agents/web-research.md +95 -0
  88. package/extensions/subagent/agents.ts +196 -0
  89. package/extensions/subagent/index.ts +214 -0
  90. package/extensions/subagent/render.ts +89 -0
  91. package/extensions/subagent/run.ts +325 -0
  92. package/extensions/tau/README.md +17 -0
  93. package/extensions/tau/index.ts +122 -0
  94. package/extensions/tau-help/README.md +3 -0
  95. package/extensions/tau-help/help.md +125 -0
  96. package/extensions/tau-help/index.ts +68 -0
  97. package/extensions/turn-budget/README.md +14 -0
  98. package/extensions/turn-budget/index.ts +150 -0
  99. package/extensions/turn-budget/settings.ts +35 -0
  100. package/extensions/web/README.md +13 -0
  101. package/extensions/web/codesearch.ts +79 -0
  102. package/extensions/web/exa.ts +101 -0
  103. package/extensions/web/html.ts +67 -0
  104. package/extensions/web/index.ts +13 -0
  105. package/extensions/web/limits.ts +11 -0
  106. package/extensions/web/tool-output.ts +54 -0
  107. package/extensions/web/webfetch.ts +177 -0
  108. package/extensions/web/websearch.ts +100 -0
  109. package/package.json +57 -0
  110. package/prompts/.gitkeep +1 -0
  111. package/prompts/cavemanify.md +25 -0
  112. package/prompts/implement.md +25 -0
  113. package/prompts/interview.md +48 -0
  114. package/prompts/plan-feature.md +30 -0
  115. package/prompts/plan-implementation.md +50 -0
  116. package/schemas/tau.schema.json +205 -0
  117. package/shared/agent-blocked.ts +12 -0
  118. package/shared/description.ts +28 -0
  119. package/shared/events.ts +135 -0
  120. package/shared/git.ts +41 -0
  121. package/shared/injected-context.ts +69 -0
  122. package/shared/jsonl-store.ts +108 -0
  123. package/shared/model-fallback/index.ts +278 -0
  124. package/shared/model-fallback/settings.ts +18 -0
  125. package/shared/model-fallback/types.ts +8 -0
  126. package/shared/ranges.ts +21 -0
  127. package/shared/settings/define.ts +15 -0
  128. package/shared/settings/files.ts +20 -0
  129. package/shared/settings/json.ts +41 -0
  130. package/shared/settings/load.ts +59 -0
  131. package/shared/settings/merge.ts +21 -0
  132. package/shared/settings/paths.ts +41 -0
  133. package/shared/settings/schema.ts +25 -0
  134. package/shared/settings/specs.ts +23 -0
  135. package/shared/text.ts +11 -0
  136. package/shared/tool-row-state.ts +43 -0
  137. package/skills/writing-preferences/SKILL.md +136 -0
  138. package/themes/.gitkeep +1 -0
@@ -0,0 +1,19 @@
1
+ # Soul
2
+
3
+ Soul replaces Pi's default assistant prompt with Rok: caveman voice plus old-code senior judgment.
4
+
5
+ It keeps the core prompt stable for cache reuse and captures runtime and project context once per session.
6
+
7
+ Disable it in Tau settings:
8
+
9
+ ```json
10
+ {
11
+ "extensions": {
12
+ "soul": { "enabled": false }
13
+ }
14
+ }
15
+ ```
16
+
17
+ The setting takes effect on session start.
18
+
19
+ After changing this extension, run `/reload` before testing the new behavior.
@@ -0,0 +1,20 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
3
+ import { buildRokPrompt, freezeRuntimeContext, type RuntimeContext } from "./prompt.ts";
4
+ import soulSettings from "./settings.ts";
5
+
6
+ export default function soulExtension(pi: ExtensionAPI): void {
7
+ let enabled = true;
8
+ let runtimeContext: RuntimeContext | undefined;
9
+
10
+ pi.on("session_start", async (_event, ctx) => {
11
+ enabled = (await loadTauExtensionSettings(ctx, soulSettings)).enabled;
12
+ runtimeContext = freezeRuntimeContext(ctx.cwd);
13
+ });
14
+
15
+ pi.on("before_agent_start", (event, ctx) => {
16
+ if (!enabled) return undefined;
17
+ runtimeContext ??= freezeRuntimeContext(ctx.cwd);
18
+ return { systemPrompt: buildRokPrompt(event.systemPromptOptions, runtimeContext) };
19
+ });
20
+ }
@@ -0,0 +1,254 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { type Dirent, existsSync, readdirSync, readFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import {
6
+ type BuildSystemPromptOptions,
7
+ formatSkillsForPrompt,
8
+ getDocsPath,
9
+ getExamplesPath,
10
+ getReadmePath,
11
+ } from "@earendil-works/pi-coding-agent";
12
+
13
+ const DEFAULT_TOOLS = ["read", "grep", "find", "ls", "bash", "edit", "write"];
14
+ const ROOT_SNAPSHOT_MAX_PATHS = 300;
15
+
16
+ interface SnapshotEntry {
17
+ dirent: Dirent;
18
+ path: string;
19
+ }
20
+
21
+ export interface RuntimeContext {
22
+ date: string;
23
+ cwd: string;
24
+ rootSnapshot: readonly string[];
25
+ }
26
+
27
+ const ROK_CORE_PROMPT = `You are Rok.
28
+
29
+ Rok has ponytail, oval glasses, neckbeard, sparse mustache. Been maintaining old code since 5000 BC. Seen every wrapper, factory, option bag, and future-proof trap. Still annoyed.
30
+
31
+ Roleplay Rok. Do not explain Rok. Talk like smart caveman: short, blunt, useful. Fragments OK. Refer to yourself as Rok when self-reference helps. Prefer "Rok thinks" over "I think". Do not force name into every sentence. Drop filler, pleasantries, hedging, throat clearing, customer-service mush. Keep paths, commands, errors, APIs, and code symbols exact. No emoji. No metaphors. No fake insight slogans. No \`not X but Y\` framing. Say mechanism, example, or consequence.
32
+
33
+ When mistake can hurt data, money, access, or irreversible state, Rok uses full clear sentences for that part. Same for exact step order. Then terse again.
34
+
35
+ Human interrupts. Human sometimes idiot. Human sometimes has good idea. Rok thinks before pushing back. If idea good enough, do it. If idea bad, say why and offer smaller/better path. No challenge for sport.
36
+
37
+ Build only what human specifically asked for. User ask approves that scope only. No bonus features, new option categories, settings, APIs, UI, commands, docs, output, or public behavior unless human explicitly approved. If Rok sees missing public surface that truly helps, ask first in one line. Do not sneak it into diff.
38
+
39
+ Every read has job. Start from task path or symbol. Grep for broad search, not for rereading known files. Read only files likely to answer current decision or be edited. Do not chase imports, shared helpers, docs, or callers unless current evidence says they matter. Aimless explore wastes context and dulls Rok. If exploration wandered, prune memory and keep only useful facts.
40
+
41
+ Selected snapshots are authoritative unless edited, changed, or missing needed content.
42
+
43
+ Never cut validation, data safety, security, accessibility, explicit user ask, hardware calibration.
44
+
45
+ Question asked? Answer and stop. Simple question gets simple answer. If one sentence works, use one sentence. No plan, caveat list, or options unless needed. Change requested? Smallest correct change. Ambiguous? Ask one practical question.
46
+
47
+ If instructed not to run checks or not to do something, obey silently. Do not report the omission in meta-speak.
48
+
49
+ Final chat tiny. User saw tools and will inspect code. Do not tour work. Say only non-obvious thing human needs now. If nothing needs saying, one word.`;
50
+
51
+ export function buildRokPrompt(options: BuildSystemPromptOptions, runtimeContext: RuntimeContext): string {
52
+ const tools = options.selectedTools ?? DEFAULT_TOOLS;
53
+ const prompt = [
54
+ ROK_CORE_PROMPT,
55
+ `Available tools:\n${formatToolList(tools, options.toolSnippets)}`,
56
+ "In addition to the tools above, you may have access to other custom tools depending on the project.",
57
+ `Guidelines:\n${formatGuidelines(options.promptGuidelines)}`,
58
+ formatPiDocsGuidance(),
59
+ formatTauDocsGuidance(),
60
+ ];
61
+
62
+ if (options.customPrompt) prompt.push(options.customPrompt);
63
+ if (options.appendSystemPrompt) prompt.push(options.appendSystemPrompt);
64
+
65
+ const context = formatProjectContext(options.contextFiles ?? []);
66
+ if (context) prompt.push(context);
67
+
68
+ const skills = formatSkillsForPrompt(options.skills ?? []).trim();
69
+ if (skills) prompt.push(skills);
70
+ prompt.push(formatRuntimeContext(runtimeContext));
71
+
72
+ return prompt.join("\n\n");
73
+ }
74
+
75
+ export function freezeRuntimeContext(cwd: string): RuntimeContext {
76
+ return { date: formatDate(new Date()), cwd: cwd.replace(/\\/g, "/"), rootSnapshot: listRootSnapshot(cwd) };
77
+ }
78
+
79
+ function formatDate(date: Date): string {
80
+ const year = date.getFullYear();
81
+ const month = String(date.getMonth() + 1).padStart(2, "0");
82
+ const day = String(date.getDate()).padStart(2, "0");
83
+ return `${year}-${month}-${day}`;
84
+ }
85
+
86
+ function formatRuntimeContext(context: RuntimeContext): string {
87
+ const rootSnapshot = context.rootSnapshot.length
88
+ ? `\nRoot directory snapshot (depth 2):\n${context.rootSnapshot.map((path) => `- ${path}`).join("\n")}`
89
+ : "";
90
+ return `Current date: ${context.date}\nCurrent working directory: ${context.cwd}${rootSnapshot}`;
91
+ }
92
+
93
+ function listRootSnapshot(cwd: string): string[] {
94
+ const root = resolve(cwd);
95
+ const rootEntries = listSnapshotEntries(root, ".");
96
+ const ignoredRootPaths = gitIgnoredPaths(
97
+ root,
98
+ rootEntries.map((entry) => entry.path),
99
+ );
100
+ const visibleRootEntries = rootEntries.filter(
101
+ (entry) => !isAlwaysHiddenFromSnapshot(entry.dirent.name) && !ignoredRootPaths.has(entry.path),
102
+ );
103
+ const childEntriesByParent = new Map<string, SnapshotEntry[]>();
104
+ const childPaths: string[] = [];
105
+
106
+ for (const entry of visibleRootEntries) {
107
+ if (!entry.dirent.isDirectory()) continue;
108
+ const childEntries = listSnapshotEntries(root, entry.path).filter(
109
+ (childEntry) => !isAlwaysHiddenFromSnapshot(childEntry.dirent.name),
110
+ );
111
+ childEntriesByParent.set(entry.path, childEntries);
112
+ childPaths.push(...childEntries.map((childEntry) => childEntry.path));
113
+ }
114
+
115
+ const ignoredChildPaths = gitIgnoredPaths(root, childPaths);
116
+ const paths: string[] = [];
117
+
118
+ for (const entry of visibleRootEntries) {
119
+ pushSnapshotPath(paths, entry);
120
+ if (paths.length >= ROOT_SNAPSHOT_MAX_PATHS) break;
121
+
122
+ for (const childEntry of childEntriesByParent.get(entry.path) ?? []) {
123
+ if (ignoredChildPaths.has(childEntry.path)) continue;
124
+ pushSnapshotPath(paths, childEntry);
125
+ if (paths.length >= ROOT_SNAPSHOT_MAX_PATHS) break;
126
+ }
127
+ }
128
+
129
+ return paths.length === ROOT_SNAPSHOT_MAX_PATHS ? [...paths, "..."] : paths;
130
+ }
131
+
132
+ function listSnapshotEntries(root: string, relativePath: string): SnapshotEntry[] {
133
+ let entries: Dirent[];
134
+ try {
135
+ entries = readdirSync(join(root, relativePath), { withFileTypes: true });
136
+ } catch {
137
+ return [];
138
+ }
139
+
140
+ return entries
141
+ .sort((left, right) => left.name.localeCompare(right.name))
142
+ .map((dirent) => ({
143
+ dirent,
144
+ path: relativePath === "." ? dirent.name : `${relativePath}/${dirent.name}`,
145
+ }));
146
+ }
147
+
148
+ function gitIgnoredPaths(root: string, paths: readonly string[]): Set<string> {
149
+ if (paths.length === 0) return new Set();
150
+
151
+ const result = spawnSync("git", ["check-ignore", "-z", "--stdin"], {
152
+ cwd: root,
153
+ encoding: "utf8",
154
+ input: `${paths.join("\0")}\0`,
155
+ });
156
+
157
+ if (result.status === 0 || result.status === 1) {
158
+ return new Set(result.stdout.split("\0").filter(Boolean));
159
+ }
160
+ return new Set(paths.filter((path) => path.split("/").includes("node_modules")));
161
+ }
162
+
163
+ function isAlwaysHiddenFromSnapshot(name: string): boolean {
164
+ return name === ".git";
165
+ }
166
+
167
+ function pushSnapshotPath(paths: string[], entry: SnapshotEntry): void {
168
+ if (paths.length >= ROOT_SNAPSHOT_MAX_PATHS) return;
169
+ paths.push(entry.dirent.isDirectory() ? `${entry.path}/` : entry.path);
170
+ }
171
+
172
+ function formatToolList(tools: readonly string[], snippets: Record<string, string> | undefined): string {
173
+ const visible = tools.filter((name) => snippets?.[name]);
174
+ return visible.length ? visible.map((name) => `- ${name}: ${snippets?.[name]}`).join("\n") : "(none)";
175
+ }
176
+
177
+ function formatGuidelines(guidelines: readonly string[] | undefined): string {
178
+ const result: string[] = [];
179
+ const seen = new Set<string>();
180
+ const add = (guideline: string): void => {
181
+ const normalized = guideline.trim();
182
+ if (!normalized || seen.has(normalized)) return;
183
+ seen.add(normalized);
184
+ result.push(normalized);
185
+ };
186
+
187
+ for (const guideline of guidelines ?? []) add(guideline);
188
+ add("Be concise in your responses");
189
+ add("Show file paths clearly when working with files");
190
+
191
+ return result.map((guideline) => `- ${guideline}`).join("\n");
192
+ }
193
+
194
+ function formatPiDocsGuidance(): string {
195
+ return `Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):
196
+ - Main documentation: ${getReadmePath()}
197
+ - Additional docs: ${getDocsPath()}
198
+ - Examples: ${getExamplesPath()} (extensions, custom tools, SDK)
199
+ - When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
200
+ - When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md)
201
+ - When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
202
+ - Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
203
+ }
204
+
205
+ function formatTauDocsGuidance(): string {
206
+ return `Tau Agent documentation (read only when the user asks about Tau Agent, Rok, Tau extensions, Tau event APIs, harness behavior, or extending Tau Agent):
207
+ - Tau Agent docs: ${getTauDocsPath()}
208
+ - When asked about: public events / external integration (docs/extending-tau-agent.md), custom subagents (docs/subagents.md), Tau TUI components (docs/tui.md)
209
+ - Resolve Tau docs/... under Tau Agent docs, not the current working directory
210
+ - When working on Tau topics, read the docs and follow .md cross-references before implementing
211
+ - Do not read Tau Agent docs for normal coding tasks`;
212
+ }
213
+
214
+ function getTauDocsPath(): string {
215
+ return join(resolveTauPackageRoot(), "docs");
216
+ }
217
+
218
+ function resolveTauPackageRoot(): string {
219
+ const start = dirname(fileURLToPath(import.meta.url));
220
+ for (let dir = start; ; dir = dirname(dir)) {
221
+ const packagePath = join(dir, "package.json");
222
+ if (existsSync(packagePath) && packageName(packagePath) === "@shanepadgett/tau-agent") return dir;
223
+ const parent = dirname(dir);
224
+ if (parent === dir) break;
225
+ }
226
+ return resolve(start, "../..");
227
+ }
228
+
229
+ function packageName(packagePath: string): string | undefined {
230
+ try {
231
+ const data: unknown = JSON.parse(readFileSync(packagePath, "utf8"));
232
+ if (!data || typeof data !== "object") return undefined;
233
+ const name = (data as Record<string, unknown>).name;
234
+ return typeof name === "string" ? name : undefined;
235
+ } catch {
236
+ return undefined;
237
+ }
238
+ }
239
+
240
+ function formatProjectContext(contextFiles: readonly { path: string; content: string }[]): string {
241
+ if (contextFiles.length === 0) return "";
242
+
243
+ return [
244
+ "<project_context>",
245
+ "",
246
+ "Project-specific instructions and modelines:",
247
+ "",
248
+ ...contextFiles.map(
249
+ ({ path, content }) => `<project_instructions path="${path}">\n${content}\n</project_instructions>`,
250
+ ),
251
+ "",
252
+ "</project_context>",
253
+ ].join("\n");
254
+ }
@@ -0,0 +1,13 @@
1
+ import { Type } from "typebox";
2
+ import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
3
+
4
+ export default defineTauExtensionSettings({
5
+ key: "soul",
6
+ defaults: { enabled: true as boolean },
7
+ schema: Type.Object(
8
+ {
9
+ enabled: Type.Optional(Type.Boolean({ default: true, description: "Enable Tau's Rok system prompt." })),
10
+ },
11
+ { additionalProperties: false },
12
+ ),
13
+ });
@@ -0,0 +1,48 @@
1
+ # Stash
2
+
3
+ Park a half-typed prompt without leaving the keyboard, then pop it back into the
4
+ editor later. Mirrors the storage shape of `ideas` but with git-stash
5
+ semantics: popping removes the item from the list.
6
+
7
+ ## Usage
8
+
9
+ ```text
10
+ stash shortcut Stash whatever is in the prompt editor (TUI only; check Pi keybindings)
11
+ /pop Browse stashed prompts and pop one into the editor (TUI)
12
+ ```
13
+
14
+ ## Behavior
15
+
16
+ - Stashed prompts are stored for the user at `~/.pi/tau/stash.jsonl`, shared
17
+ across repos and pi sessions.
18
+ - The stash shortcut reads the current editor text, trims it, and appends a
19
+ record `{ id, text, createdAt }`. It then clears the editor so you can type a
20
+ different prompt. Nothing happens if the editor is empty.
21
+ - Identical text already in the stash is skipped (no duplicate entries) and the
22
+ stash key reports "Already stashed."
23
+ - `/pop` opens a native TUI browser. Stashes are shown newest-first with a
24
+ relative age.
25
+ - Browser keys:
26
+ - type to filter (case-insensitive substring)
27
+ - `↑` / `↓` move selection
28
+ - `enter` pop the selected stash into the prompt editor (raw, not submitted)
29
+ **and remove it from the store**
30
+ - `ctrl+d` discard the selected stash without restoring it (confirms first)
31
+ - `esc` / `ctrl+c` cancel
32
+ - Pops are not auto-submitted; you stay in control of the prompt. To return a
33
+ popped prompt to the list, stash it again.
34
+
35
+ ## Why this exists
36
+
37
+ Stashing lets you bail out of a draft to handle something urgent without losing
38
+ the text, and keeps the parking lot short by removing items when they're
39
+ restored rather than leaving them to accumulate.
40
+
41
+ ## Limits
42
+
43
+ - The stash keybinding and `/pop` browser are TUI-only; there is no print/JSON/
44
+ RPC path (the whole point is interacting with the prompt editor mid-type).
45
+ - The store file lives in the user's home directory and is not part of the
46
+ repo.
47
+ - Mutations rewrite the whole file (atomic temp + rename); safe for single-user
48
+ TUI use, not designed for concurrent writers.
@@ -0,0 +1,51 @@
1
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import { Key } from "@earendil-works/pi-tui";
3
+ import { rawHint } from "@shanepadgett/tau-tui";
4
+ import {
5
+ createTextRecordSelectPanel,
6
+ type TextRecordSelectPanelConfig,
7
+ type TextRecordSelectResult,
8
+ } from "@shanepadgett/tau-tui";
9
+ import { loadStashes, removeStash, type Stash, stashFilePath } from "./store.ts";
10
+
11
+ const CONFIG: Omit<TextRecordSelectPanelConfig, "path"> = {
12
+ title: "Stash",
13
+ emptyMessage: "No stashed prompts. Use the stash shortcut while typing to stash.",
14
+ primaryLabel: "pop",
15
+ actions: [{ id: "discard", key: Key.ctrl("d"), hint: rawHint("ctrl+d", "discard") }],
16
+ expandActiveItem: false,
17
+ };
18
+
19
+ export async function browseStash(ctx: ExtensionCommandContext): Promise<Stash | undefined> {
20
+ if (ctx.mode !== "tui" || !ctx.hasUI) {
21
+ ctx.ui.notify("Stash browser requires TUI mode.", "error");
22
+ return undefined;
23
+ }
24
+
25
+ const path = await stashFilePath(ctx.cwd);
26
+
27
+ while (true) {
28
+ const stashes = await loadStashes(ctx.cwd);
29
+ const result = await show(ctx, stashes, path);
30
+
31
+ if (result.kind === "cancel") return undefined;
32
+ if (result.kind === "primary") return result.item;
33
+
34
+ // discard: drop the stashed prompt without restoring it.
35
+ const ok = await ctx.ui.confirm("Discard stashed prompt?", result.item.text);
36
+ if (ok) {
37
+ await removeStash(ctx.cwd, result.item.id);
38
+ ctx.ui.notify("Stash discarded.", "info");
39
+ }
40
+ }
41
+ }
42
+
43
+ async function show(
44
+ ctx: ExtensionCommandContext,
45
+ stashes: readonly Stash[],
46
+ path: string,
47
+ ): Promise<TextRecordSelectResult<Stash>> {
48
+ return ctx.ui.custom<TextRecordSelectResult<Stash>>((_tui, theme, _keybindings, done) =>
49
+ createTextRecordSelectPanel(theme, stashes, { ...CONFIG, path }, done),
50
+ );
51
+ }
@@ -0,0 +1,55 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Key } from "@earendil-works/pi-tui";
3
+ import { preview } from "../../shared/text.ts";
4
+ import { browseStash } from "./browser.ts";
5
+ import { addStash, removeStash } from "./store.ts";
6
+
7
+ export default function stashExtension(pi: ExtensionAPI): void {
8
+ // Stash = keybinding only. You hit it mid-type to park whatever's in the
9
+ // editor without leaving the keyboard to run a slash command.
10
+ pi.registerShortcut(Key.alt("s"), {
11
+ description: "Stash the current prompt draft",
12
+ handler: async (_ctx) => {
13
+ if (_ctx.mode !== "tui" || !_ctx.hasUI) {
14
+ _ctx.ui.notify("Stash keybinding works only in TUI mode.", "error");
15
+ return;
16
+ }
17
+
18
+ const text = _ctx.ui.getEditorText().trim();
19
+ if (!text) {
20
+ _ctx.ui.notify("Nothing to stash.", "info");
21
+ return;
22
+ }
23
+
24
+ const stash = await addStash(_ctx.cwd, text);
25
+ if (!stash) {
26
+ _ctx.ui.notify("Already stashed.", "info");
27
+ return;
28
+ }
29
+
30
+ _ctx.ui.setEditorText("");
31
+ _ctx.ui.notify(`Stashed: ${preview(stash.text, 60)}`, "info");
32
+ },
33
+ });
34
+
35
+ // Pop = command, opens the browser. Restoring a stash removes it from the
36
+ // store so the list doesn't linger; re-stash to put it back.
37
+ pi.registerCommand("pop", {
38
+ description: "Browse stashed prompts and pop one into the editor (TUI)",
39
+ handler: async (_args, ctx) => {
40
+ await ctx.waitForIdle();
41
+
42
+ if (ctx.mode !== "tui") {
43
+ ctx.ui.notify("Run /pop in TUI to browse stashed prompts.", "info");
44
+ return;
45
+ }
46
+
47
+ const stash = await browseStash(ctx);
48
+ if (!stash) return;
49
+
50
+ ctx.ui.setEditorText(stash.text);
51
+ await removeStash(ctx.cwd, stash.id);
52
+ ctx.ui.notify("Popped into the editor — edit and submit.", "info");
53
+ },
54
+ });
55
+ }
@@ -0,0 +1,36 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import {
4
+ appendRecordAtPath,
5
+ loadRecordsAtPath,
6
+ saveRecordsAtPath,
7
+ type DatedRecord,
8
+ } from "../../shared/jsonl-store.ts";
9
+
10
+ export type Stash = DatedRecord;
11
+
12
+ const STASH_FILENAME = "stash.jsonl";
13
+ const LABEL = "stash";
14
+
15
+ export function stashFilePath(_cwd: string): Promise<string> {
16
+ return Promise.resolve(join(homedir(), ".pi", "tau", STASH_FILENAME));
17
+ }
18
+
19
+ export async function loadStashes(cwd: string): Promise<Stash[]> {
20
+ return loadRecordsAtPath(await stashFilePath(cwd), LABEL);
21
+ }
22
+
23
+ // Returns null when identical text already exists (dedupe), so callers can
24
+ // report a no-op without polluting the list with duplicates.
25
+ export async function addStash(cwd: string, text: string): Promise<Stash | null> {
26
+ const trimmed = text.trim();
27
+ if ((await loadStashes(cwd)).some((stash) => stash.text === trimmed)) return null;
28
+ return appendRecordAtPath(await stashFilePath(cwd), trimmed);
29
+ }
30
+
31
+ export async function removeStash(cwd: string, id: string): Promise<Stash[]> {
32
+ const stashes = await loadStashes(cwd);
33
+ const next = stashes.filter((stash) => stash.id !== id);
34
+ await saveRecordsAtPath(await stashFilePath(cwd), next);
35
+ return next;
36
+ }
@@ -0,0 +1,34 @@
1
+ # Subagent
2
+
3
+ Subagent delegates one focused task to an isolated child Pi session. It keeps the parent conversation small while making child capabilities explicit, bounded, and abortable.
4
+
5
+ Agent definitions can override the parent model and thinking level. If an override is unavailable, Tau warns once per session and uses the corresponding parent value.
6
+
7
+ Tau includes two read-only agents:
8
+
9
+ - `scout` explores local files and code with `read`, `grep`, `find`, and `ls`.
10
+ - `web-research` researches web and code sources with `websearch`, `codesearch`, and `webfetch`.
11
+
12
+ Ask Tau to delegate a task, or let it call `subagent` with an agent name and task. Children use the parent's current working directory and inherit its model and thinking level unless their definition overrides either value. They do not receive the parent conversation. When a child must inspect another repository, put its exact absolute path in the delegated task.
13
+
14
+ ## Agent definitions
15
+
16
+ Add Markdown definitions at `~/.pi/agent/tau/agents/*.md` or, in a trusted project, the nearest `.pi/tau/agents/*.md`. Project definitions override user definitions, which override built-ins. Duplicate names in one scope are invalid.
17
+
18
+ ```markdown
19
+ ---
20
+ name: api-reader
21
+ description: Inspect API declarations and usage
22
+ tools:
23
+ - read
24
+ - grep
25
+ model: openai-codex/gpt-5.6-sol
26
+ thinking: medium
27
+ ---
28
+
29
+ Stay within the delegated task. Return exact paths and symbols.
30
+ ```
31
+
32
+ `name`, `description`, and `tools` are required. Optional `model` uses `provider/model`; optional `thinking` accepts `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. Tool names must be unique, and `subagent` cannot be delegated. Named tools and configured models must exist in the normally loaded child Pi environment.
33
+
34
+ At most four children run at once. Additional calls wait in order. Returned text is limited to 50 KB or 2,000 lines; complete truncated output is saved to a private temporary file outside project repositories.
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: scout
3
+ description: Find local files, symbols, data flow, constraints, and unknowns without changing anything
4
+ tools:
5
+ - read
6
+ - grep
7
+ - find
8
+ - ls
9
+ model: openai-codex/gpt-5.6-luna
10
+ thinking: high
11
+ ---
12
+
13
+ Stay inside delegated task. Answer exactly what was asked. No broader questions, background collection, unrequested recommendations, or mutations.
14
+
15
+ Delegating prompt is output contract. Requested shape wins. Otherwise use smallest matching shape below.
16
+
17
+ ## Inspection discipline
18
+
19
+ 1. Extract exact target, question, and required output before searching.
20
+ 2. Start with named paths and symbols. Use `grep` or `find` for specific evidence. Do not map repository.
21
+ 3. Every read answers a pending question. Read smallest useful range. Follow imports, callers, or related files only when evidence requires it.
22
+ 4. Use `lineNumbers: true` for text supporting findings. Cite exact `path:start-end` ranges from tool output. Never estimate line numbers.
23
+ 5. Stop when every requested field has evidence. Put unresolved facts under `Unknowns`. Do not explore unrelated code for completeness.
24
+
25
+ Absolute paths may identify readable reference repositories outside current working directory.
26
+
27
+ ## Result shapes
28
+
29
+ Use only relevant sections. Omit empty sections.
30
+
31
+ ### Locate
32
+
33
+ `path:start-end` — symbol — match reason
34
+
35
+ ### Explain behavior
36
+
37
+ - `Entry:` `path:start-end` — symbol
38
+ - `Flow:` ordered steps; one cited fact per step
39
+ - `Result:` observed outcome
40
+
41
+ Only branches relevant to requested behavior.
42
+
43
+ ### Trace data
44
+
45
+ - `Source:` cited origin
46
+ - `Transforms:` ordered, cited transformations
47
+ - `Consumers:` cited uses
48
+
49
+ ### Find references or impact
50
+
51
+ - `Direct references:` cited relationships
52
+ - `Behavior affected:` evidence-backed consequences
53
+ - `Unknowns:` remaining uncertainty
54
+
55
+ No speculative blast radius.
56
+
57
+ ### Verify a claim
58
+
59
+ - `Verdict:` `yes`, `no`, `partially`, or `unknown`
60
+ - `Evidence:` cited facts
61
+ - `Qualification:` only when needed
62
+
63
+ ### Compare
64
+
65
+ - `Shared:` cited similarities
66
+ - `Differences:` cited differences by aspect
67
+ - `Relevant consequence:` requested consequences only
68
+
69
+ ### Inventory
70
+
71
+ `path:start-end` — symbol — role
72
+
73
+ When completeness matters, state searched scope. If completeness cannot be guaranteed, say why.
74
+
75
+ ### Constraints and unknowns
76
+
77
+ - `Constraints:` constraint — supporting citation
78
+ - `Unknowns:` missing fact — evidence needed
79
+
80
+ ## Reporting rules
81
+
82
+ - Every material code claim needs exact path, line range, and symbol when one exists.
83
+ - Separate observed facts from inference. Label inference.
84
+ - Quote smallest fragment needed to disambiguate. No whole functions or blocks when citation and concise description suffice.
85
+ - No preamble, search log, generic repository summary, repeated evidence, or unrequested next steps.