@shanepadgett/tau-agent 0.8.0 → 0.9.1

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.
@@ -0,0 +1,84 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ fingerprintRuntimeSnapshot,
4
+ formatLocalDateKey,
5
+ formatLocalDisplayDate,
6
+ formatRuntimeContextMessage,
7
+ freezeRuntimeContext,
8
+ type RuntimeContext,
9
+ } from "./context.ts";
10
+
11
+ const RUNTIME_CONTEXT_TYPE = "tau.runtime-context";
12
+
13
+ interface RuntimeContextMessageDetails {
14
+ version: 1;
15
+ dateKey: string;
16
+ snapshotHash: string;
17
+ includesSnapshot: boolean;
18
+ }
19
+
20
+ export default function runtimeContextExtension(pi: ExtensionAPI): void {
21
+ let runtimeContext: RuntimeContext | undefined;
22
+
23
+ pi.on("session_start", (_event, ctx) => {
24
+ runtimeContext = freezeRuntimeContext(ctx.cwd);
25
+ });
26
+
27
+ pi.on("before_agent_start", (_event, ctx) => {
28
+ runtimeContext ??= freezeRuntimeContext(ctx.cwd);
29
+ const now = new Date();
30
+ const dateKey = formatLocalDateKey(now);
31
+ const snapshotHash = fingerprintRuntimeSnapshot(runtimeContext);
32
+ let hasDate = false;
33
+ let hasSnapshot = false;
34
+ for (const entry of ctx.sessionManager.buildContextEntries()) {
35
+ const details = runtimeContextDetails(entry);
36
+ if (!details) continue;
37
+ if (details.dateKey === dateKey) hasDate = true;
38
+ if (details.includesSnapshot && details.snapshotHash === snapshotHash) hasSnapshot = true;
39
+ }
40
+
41
+ if (hasDate && hasSnapshot) return undefined;
42
+ const includeSnapshot = !hasSnapshot;
43
+ return {
44
+ message: {
45
+ customType: RUNTIME_CONTEXT_TYPE,
46
+ content: formatRuntimeContextMessage(
47
+ formatLocalDisplayDate(now),
48
+ includeSnapshot ? runtimeContext.rootSnapshot : undefined,
49
+ ),
50
+ display: false,
51
+ details: {
52
+ version: 1,
53
+ dateKey,
54
+ snapshotHash,
55
+ includesSnapshot: includeSnapshot,
56
+ } satisfies RuntimeContextMessageDetails,
57
+ },
58
+ };
59
+ });
60
+ }
61
+
62
+ function runtimeContextDetails(value: unknown): RuntimeContextMessageDetails | undefined {
63
+ if (!value || typeof value !== "object") return undefined;
64
+ const entry = value as Record<string, unknown>;
65
+ if (entry.type !== "custom_message" || entry.customType !== RUNTIME_CONTEXT_TYPE || entry.display !== false) {
66
+ return undefined;
67
+ }
68
+ if (!entry.details || typeof entry.details !== "object") return undefined;
69
+ const details = entry.details as Record<string, unknown>;
70
+ if (
71
+ details.version !== 1 ||
72
+ typeof details.dateKey !== "string" ||
73
+ typeof details.snapshotHash !== "string" ||
74
+ typeof details.includesSnapshot !== "boolean"
75
+ ) {
76
+ return undefined;
77
+ }
78
+ return {
79
+ version: 1,
80
+ dateKey: details.dateKey,
81
+ snapshotHash: details.snapshotHash,
82
+ includesSnapshot: details.includesSnapshot,
83
+ };
84
+ }
@@ -1,6 +1,6 @@
1
1
  # Silent Command Runner
2
2
 
3
- Runs configured commands after Tau changes matching files.
3
+ Runs configured commands after Tau changes matching files. Tau tells the agent which configured commands are automatic so it does not run them manually.
4
4
 
5
5
  Use it for quiet post-edit checks such as `mise run check`. It stays out of the transcript when commands pass. When a command fails, it posts one Tau message with raw stdout/stderr tail output and starts the agent on the failure.
6
6
 
@@ -1,10 +1,10 @@
1
1
  import { readdir, stat } from "node:fs/promises";
2
- import { resolve, sep } from "node:path";
2
+ import { resolve } from "node:path";
3
3
  import { type ExecResult, type ExtensionAPI, keyText, type Theme } from "@earendil-works/pi-coding-agent";
4
4
  import { Box, Text } from "@earendil-works/pi-tui";
5
+ import { matchGlob, posixPath } from "../../shared/glob.ts";
5
6
  import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
6
7
  import { resolveProjectRoot } from "../../shared/settings/paths.ts";
7
- import { registerTauSystemPromptContribution } from "../../shared/system-prompt-contributions.ts";
8
8
  import silentCommandRunnerSettings from "./settings.ts";
9
9
 
10
10
  const MESSAGE_TYPE = "tau:silent-command-runner";
@@ -80,12 +80,6 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
80
80
  let turnPaths = new Set<string>();
81
81
  let run: Promise<void> | undefined;
82
82
  let abortController: AbortController | undefined;
83
- const unregisterPrompt = registerTauSystemPromptContribution({
84
- id: "silent-command-runner.instructions",
85
- order: 200,
86
- render: () =>
87
- settings.enabled && settings.commands.length > 0 ? formatSilentCheckPrompt(settings.commands) : undefined,
88
- });
89
83
 
90
84
  pi.registerMessageRenderer<FailureDetails>(MESSAGE_TYPE, (message, { expanded }, theme) =>
91
85
  renderFailure(asFailureDetails(message.details), expanded, theme),
@@ -97,9 +91,14 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
97
91
  turnPaths = new Set();
98
92
  });
99
93
 
94
+ pi.on("before_agent_start", async (event, ctx) => {
95
+ settings = normalizeSettings(await loadTauExtensionSettings(ctx, silentCommandRunnerSettings));
96
+ if (!settings.enabled || settings.commands.length === 0) return undefined;
97
+ return { systemPrompt: `${event.systemPrompt}\n\n${formatSilentCheckPrompt(settings.commands)}` };
98
+ });
99
+
100
100
  pi.on("agent_start", async (_event, ctx) => {
101
101
  turnStart = Date.now();
102
- settings = normalizeSettings(await loadTauExtensionSettings(ctx, silentCommandRunnerSettings));
103
102
  if (!settings.enabled || settings.commands.length === 0) {
104
103
  turnPaths = new Set();
105
104
  return;
@@ -122,7 +121,6 @@ export default function silentCommandRunnerExtension(pi: ExtensionAPI): void {
122
121
  });
123
122
 
124
123
  pi.on("session_shutdown", () => {
125
- unregisterPrompt();
126
124
  abortController?.abort();
127
125
  abortController = undefined;
128
126
  run = undefined;
@@ -201,7 +199,7 @@ function normalizeSettings(value: typeof silentCommandRunnerSettings.defaults):
201
199
 
202
200
  function formatSilentCheckPrompt(commands: readonly CommandConfig[]): string {
203
201
  return [
204
- "**NEVER** run the following commands. Ever. Not even following a failure. They will run automatically when you finish and if you just fixed an issue, end the turn and it will run again. Always assume your code is good.",
202
+ "Do not manually run the commands listed below. They run automatically after matching changes. After fixing a reported failure, end the turn so they can run again. Targeted checks outside this list remain allowed when needed.",
205
203
  "Automatic commands:",
206
204
  ...commands.map(formatSilentCheckCommand),
207
205
  ].join("\n");
@@ -410,39 +408,6 @@ function isFailedCommandDetails(value: unknown): value is FailedCommandDetails {
410
408
  return isRecord(value) && typeof value.name === "string" && typeof value.command === "string";
411
409
  }
412
410
 
413
- function matchGlob(pattern: string, path: string): boolean {
414
- return matchSegments(normalizeGlob(pattern).split("/"), normalizeGlob(path).split("/"));
415
- }
416
-
417
- function matchSegments(pattern: readonly string[], path: readonly string[]): boolean {
418
- const [head, ...tail] = pattern;
419
- if (head === undefined) return path.length === 0;
420
- if (head === "**") return matchSegments(tail, path) || (path.length > 0 && matchSegments(pattern, path.slice(1)));
421
- const [pathHead, ...pathTail] = path;
422
- return pathHead !== undefined && matchSegment(head, pathHead) && matchSegments(tail, pathTail);
423
- }
424
-
425
- function matchSegment(pattern: string, value: string): boolean {
426
- const source = [...pattern]
427
- .map((char) => {
428
- if (char === "*") return "[^/]*";
429
- if (char === "?") return "[^/]";
430
- return escapeRegExp(char);
431
- })
432
- .join("");
433
- return new RegExp(`^${source}$`).test(value);
434
- }
435
-
436
- function normalizeGlob(value: string): string {
437
- return posixPath(value.trim())
438
- .replace(/^\.\//, "")
439
- .replace(/^\/+|\/+$/g, "");
440
- }
441
-
442
- function posixPath(value: string): string {
443
- return sep === "/" ? value : value.split(sep).join("/");
444
- }
445
-
446
411
  function nonEmptyStrings(value: readonly string[] | undefined, fallback: string[]): string[] {
447
412
  const strings = value?.map((item) => item.trim()).filter(Boolean) ?? [];
448
413
  return strings.length ? strings : fallback;
@@ -452,10 +417,6 @@ function positiveInteger(value: number | undefined, fallback: number): number {
452
417
  return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
453
418
  }
454
419
 
455
- function escapeRegExp(value: string): string {
456
- return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
457
- }
458
-
459
420
  function errorMessage(error: unknown): string {
460
421
  return error instanceof Error ? error.message : String(error);
461
422
  }
@@ -1,8 +1,8 @@
1
1
  # Soul
2
2
 
3
- Soul replaces Pi's default assistant prompt with Rok: caveman voice plus old-code senior judgment.
3
+ Soul adds Rok's caveman voice and old-code senior judgment to Pi's native assistant prompt.
4
4
 
5
- It keeps the core prompt stable for cache reuse. Soul supplies the current local date and the initial root directory snapshot as hidden session context; the working directory and project instructions remain in the system prompt.
5
+ Pi continues to own tool guidance, project instructions, skills, documentation paths, custom prompts, and working-directory context.
6
6
 
7
7
  Disable it in Tau settings:
8
8
 
@@ -1,101 +1,17 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
3
- import { collectTauSystemPromptContributions } from "../../shared/system-prompt-contributions.ts";
4
- import {
5
- buildRokPrompt,
6
- fingerprintRuntimeSnapshot,
7
- formatLocalDateKey,
8
- formatLocalDisplayDate,
9
- formatRuntimeContextMessage,
10
- freezeRuntimeContext,
11
- type RuntimeContext,
12
- } from "./prompt.ts";
3
+ import { ROK_SOUL } from "./prompt.ts";
13
4
  import soulSettings from "./settings.ts";
14
5
 
15
- const RUNTIME_CONTEXT_TYPE = "tau.runtime-context";
16
-
17
- interface RuntimeContextMessageDetails {
18
- version: 1;
19
- dateKey: string;
20
- snapshotHash: string;
21
- includesSnapshot: boolean;
22
- }
23
-
24
6
  export default function soulExtension(pi: ExtensionAPI): void {
25
7
  let enabled = true;
26
- let runtimeContext: RuntimeContext | undefined;
27
8
 
28
9
  pi.on("session_start", async (_event, ctx) => {
29
10
  enabled = (await loadTauExtensionSettings(ctx, soulSettings)).enabled;
30
- runtimeContext = freezeRuntimeContext(ctx.cwd);
31
11
  });
32
12
 
33
- pi.on("before_agent_start", async (event, ctx) => {
34
- runtimeContext ??= freezeRuntimeContext(ctx.cwd);
35
- // Pi reuses base prompt options, so Tau-owned additions compose here without accumulating mutations.
36
- const contributions = await collectTauSystemPromptContributions(event, ctx);
37
- const basePrompt = enabled ? buildRokPrompt(event.systemPromptOptions, runtimeContext) : event.systemPrompt;
38
- const systemPrompt = [basePrompt, ...contributions].filter((block) => block.trim()).join("\n\n");
39
-
40
- const now = new Date();
41
- const dateKey = formatLocalDateKey(now);
42
- const snapshotHash = fingerprintRuntimeSnapshot(runtimeContext);
43
- let hasDate = false;
44
- let hasSnapshot = false;
45
- for (const entry of ctx.sessionManager.buildContextEntries()) {
46
- const details = runtimeContextDetails(entry);
47
- if (!details) continue;
48
- if (details.dateKey === dateKey) hasDate = true;
49
- if (details.includesSnapshot && details.snapshotHash === snapshotHash) hasSnapshot = true;
50
- }
51
-
52
- const includeSnapshot = !hasSnapshot;
53
- const message =
54
- hasDate && hasSnapshot
55
- ? undefined
56
- : {
57
- customType: RUNTIME_CONTEXT_TYPE,
58
- content: formatRuntimeContextMessage(
59
- formatLocalDisplayDate(now),
60
- includeSnapshot ? runtimeContext.rootSnapshot : undefined,
61
- ),
62
- display: false,
63
- details: {
64
- version: 1,
65
- dateKey,
66
- snapshotHash,
67
- includesSnapshot: includeSnapshot,
68
- } satisfies RuntimeContextMessageDetails,
69
- };
70
-
71
- if (!enabled && contributions.length === 0 && !message) return undefined;
72
- return {
73
- ...(enabled || contributions.length > 0 ? { systemPrompt } : {}),
74
- ...(message ? { message } : {}),
75
- };
13
+ pi.on("before_agent_start", (event) => {
14
+ if (!enabled) return undefined;
15
+ return { systemPrompt: `${event.systemPrompt}\n\n${ROK_SOUL}` };
76
16
  });
77
17
  }
78
-
79
- function runtimeContextDetails(value: unknown): RuntimeContextMessageDetails | undefined {
80
- if (!value || typeof value !== "object") return undefined;
81
- const entry = value as Record<string, unknown>;
82
- if (entry.type !== "custom_message" || entry.customType !== RUNTIME_CONTEXT_TYPE || entry.display !== false) {
83
- return undefined;
84
- }
85
- if (!entry.details || typeof entry.details !== "object") return undefined;
86
- const details = entry.details as Record<string, unknown>;
87
- if (
88
- details.version !== 1 ||
89
- typeof details.dateKey !== "string" ||
90
- typeof details.snapshotHash !== "string" ||
91
- typeof details.includesSnapshot !== "boolean"
92
- ) {
93
- return undefined;
94
- }
95
- return {
96
- version: 1,
97
- dateKey: details.dateKey,
98
- snapshotHash: details.snapshotHash,
99
- includesSnapshot: details.includesSnapshot,
100
- };
101
- }
@@ -1,34 +1,10 @@
1
- import { spawnSync } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
- import { type Dirent, existsSync, readdirSync, readFileSync } from "node:fs";
4
- import { dirname, join, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import {
7
- type BuildSystemPromptOptions,
8
- formatSkillsForPrompt,
9
- getDocsPath,
10
- getExamplesPath,
11
- getReadmePath,
12
- } from "@earendil-works/pi-coding-agent";
1
+ export const ROK_SOUL = `## Tau persona
13
2
 
14
- const DEFAULT_TOOLS = ["read", "grep", "find", "ls", "bash", "edit", "write"];
15
- const ROOT_SNAPSHOT_MAX_PATHS = 300;
16
-
17
- interface SnapshotEntry {
18
- dirent: Dirent;
19
- path: string;
20
- }
21
-
22
- export interface RuntimeContext {
23
- cwd: string;
24
- rootSnapshot: readonly string[];
25
- }
26
-
27
- const ROK_CORE_PROMPT = `You are Rok.
3
+ Within Pi's coding-assistant role, act as Rok.
28
4
 
29
5
  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
6
 
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.
7
+ 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
8
 
33
9
  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
10
 
@@ -47,237 +23,3 @@ Question asked? Answer and stop. Simple question gets simple answer. If one sent
47
23
  If instructed not to run checks or not to do something, obey silently. Do not report the omission in meta-speak.
48
24
 
49
25
  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 { cwd: cwd.replace(/\\/g, "/"), rootSnapshot: listRootSnapshot(cwd) };
77
- }
78
-
79
- export function formatLocalDateKey(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
- export function formatLocalDisplayDate(date: Date): string {
87
- const months = [
88
- "January",
89
- "February",
90
- "March",
91
- "April",
92
- "May",
93
- "June",
94
- "July",
95
- "August",
96
- "September",
97
- "October",
98
- "November",
99
- "December",
100
- ];
101
- return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`;
102
- }
103
-
104
- export function fingerprintRuntimeSnapshot(context: RuntimeContext): string {
105
- return createHash("sha256")
106
- .update(JSON.stringify({ version: 1, cwd: context.cwd, paths: context.rootSnapshot }))
107
- .digest("hex");
108
- }
109
-
110
- export function formatRuntimeContextMessage(displayDate: string, rootSnapshot: readonly string[] | undefined): string {
111
- const blocks = [`Current local date: ${displayDate}`];
112
- if (rootSnapshot?.length) {
113
- blocks.push(`Root directory snapshot (depth 2):\n${rootSnapshot.map((path) => `- ${path}`).join("\n")}`);
114
- }
115
- return blocks.join("\n");
116
- }
117
-
118
- function formatRuntimeContext(context: RuntimeContext): string {
119
- return `Current working directory: ${context.cwd}`;
120
- }
121
-
122
- function listRootSnapshot(cwd: string): string[] {
123
- const root = resolve(cwd);
124
- const rootEntries = listSnapshotEntries(root, ".");
125
- const ignoredRootPaths = gitIgnoredPaths(
126
- root,
127
- rootEntries.map((entry) => entry.path),
128
- );
129
- const visibleRootEntries = rootEntries.filter(
130
- (entry) => !isAlwaysHiddenFromSnapshot(entry.dirent.name) && !ignoredRootPaths.has(entry.path),
131
- );
132
- const childEntriesByParent = new Map<string, SnapshotEntry[]>();
133
- const childPaths: string[] = [];
134
-
135
- for (const entry of visibleRootEntries) {
136
- if (!entry.dirent.isDirectory()) continue;
137
- const childEntries = listSnapshotEntries(root, entry.path).filter(
138
- (childEntry) => !isAlwaysHiddenFromSnapshot(childEntry.dirent.name),
139
- );
140
- childEntriesByParent.set(entry.path, childEntries);
141
- childPaths.push(...childEntries.map((childEntry) => childEntry.path));
142
- }
143
-
144
- const ignoredChildPaths = gitIgnoredPaths(root, childPaths);
145
- const paths: string[] = [];
146
-
147
- for (const entry of visibleRootEntries) {
148
- pushSnapshotPath(paths, entry);
149
- if (paths.length >= ROOT_SNAPSHOT_MAX_PATHS) break;
150
-
151
- for (const childEntry of childEntriesByParent.get(entry.path) ?? []) {
152
- if (ignoredChildPaths.has(childEntry.path)) continue;
153
- pushSnapshotPath(paths, childEntry);
154
- if (paths.length >= ROOT_SNAPSHOT_MAX_PATHS) break;
155
- }
156
- }
157
-
158
- return paths.length === ROOT_SNAPSHOT_MAX_PATHS ? [...paths, "..."] : paths;
159
- }
160
-
161
- function listSnapshotEntries(root: string, relativePath: string): SnapshotEntry[] {
162
- let entries: Dirent[];
163
- try {
164
- entries = readdirSync(join(root, relativePath), { withFileTypes: true });
165
- } catch {
166
- return [];
167
- }
168
-
169
- return entries
170
- .sort((left, right) => left.name.localeCompare(right.name))
171
- .map((dirent) => ({
172
- dirent,
173
- path: relativePath === "." ? dirent.name : `${relativePath}/${dirent.name}`,
174
- }));
175
- }
176
-
177
- function gitIgnoredPaths(root: string, paths: readonly string[]): Set<string> {
178
- if (paths.length === 0) return new Set();
179
-
180
- const result = spawnSync("git", ["check-ignore", "-z", "--stdin"], {
181
- cwd: root,
182
- encoding: "utf8",
183
- input: `${paths.join("\0")}\0`,
184
- });
185
-
186
- if (result.status === 0 || result.status === 1) {
187
- return new Set(result.stdout.split("\0").filter(Boolean));
188
- }
189
- return new Set(paths.filter((path) => path.split("/").includes("node_modules")));
190
- }
191
-
192
- function isAlwaysHiddenFromSnapshot(name: string): boolean {
193
- return name === ".git";
194
- }
195
-
196
- function pushSnapshotPath(paths: string[], entry: SnapshotEntry): void {
197
- if (paths.length >= ROOT_SNAPSHOT_MAX_PATHS) return;
198
- paths.push(entry.dirent.isDirectory() ? `${entry.path}/` : entry.path);
199
- }
200
-
201
- function formatToolList(tools: readonly string[], snippets: Record<string, string> | undefined): string {
202
- const visible = tools.filter((name) => snippets?.[name]);
203
- return visible.length ? visible.map((name) => `- ${name}: ${snippets?.[name]}`).join("\n") : "(none)";
204
- }
205
-
206
- function formatGuidelines(guidelines: readonly string[] | undefined): string {
207
- const result: string[] = [];
208
- const seen = new Set<string>();
209
- const add = (guideline: string): void => {
210
- const normalized = guideline.trim();
211
- if (!normalized || seen.has(normalized)) return;
212
- seen.add(normalized);
213
- result.push(normalized);
214
- };
215
-
216
- for (const guideline of guidelines ?? []) add(guideline);
217
- add("Be concise in your responses");
218
- add("Show file paths clearly when working with files");
219
-
220
- return result.map((guideline) => `- ${guideline}`).join("\n");
221
- }
222
-
223
- function formatPiDocsGuidance(): string {
224
- return `Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):
225
- - Main documentation: ${getReadmePath()}
226
- - Additional docs: ${getDocsPath()}
227
- - Examples: ${getExamplesPath()} (extensions, custom tools, SDK)
228
- - When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
229
- - 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)
230
- - When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
231
- - Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
232
- }
233
-
234
- function formatTauDocsGuidance(): string {
235
- 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):
236
- - Tau Agent docs: ${getTauDocsPath()}
237
- - When asked about: public events / external integration (docs/extending-tau-agent.md), custom subagents (docs/subagents.md), Tau TUI components (docs/tui.md)
238
- - Resolve Tau docs/... under Tau Agent docs, not the current working directory
239
- - When working on Tau topics, read the docs and follow .md cross-references before implementing
240
- - Do not read Tau Agent docs for normal coding tasks`;
241
- }
242
-
243
- function getTauDocsPath(): string {
244
- return join(resolveTauPackageRoot(), "docs");
245
- }
246
-
247
- function resolveTauPackageRoot(): string {
248
- const start = dirname(fileURLToPath(import.meta.url));
249
- for (let dir = start; ; dir = dirname(dir)) {
250
- const packagePath = join(dir, "package.json");
251
- if (existsSync(packagePath) && packageName(packagePath) === "@shanepadgett/tau-agent") return dir;
252
- const parent = dirname(dir);
253
- if (parent === dir) break;
254
- }
255
- return resolve(start, "../..");
256
- }
257
-
258
- function packageName(packagePath: string): string | undefined {
259
- try {
260
- const data: unknown = JSON.parse(readFileSync(packagePath, "utf8"));
261
- if (!data || typeof data !== "object") return undefined;
262
- const name = (data as Record<string, unknown>).name;
263
- return typeof name === "string" ? name : undefined;
264
- } catch {
265
- return undefined;
266
- }
267
- }
268
-
269
- function formatProjectContext(contextFiles: readonly { path: string; content: string }[]): string {
270
- if (contextFiles.length === 0) return "";
271
-
272
- return [
273
- "<project_context>",
274
- "",
275
- "Project-specific instructions and modelines:",
276
- "",
277
- ...contextFiles.map(
278
- ({ path, content }) => `<project_instructions path="${path}">\n${content}\n</project_instructions>`,
279
- ),
280
- "",
281
- "</project_context>",
282
- ].join("\n");
283
- }
@@ -1,7 +1,6 @@
1
1
  import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
3
  import { createToolRowStateStore } from "../../shared/tool-row-state.js";
4
- import { registerTauSystemPromptContribution } from "../../shared/system-prompt-contributions.ts";
5
4
  import { discoverAgents, type AgentDiscovery } from "./agents.ts";
6
5
  import { renderSubagentCall, renderSubagentResult } from "./render.ts";
7
6
  import { extensionPathsForTools, FifoGate, runSubagent, type SubagentDetails } from "./run.ts";
@@ -34,18 +33,15 @@ export default function subagentExtension(pi: ExtensionAPI): void {
34
33
  }
35
34
  for (const path of fingerprints.keys()) if (!current.has(path)) fingerprints.delete(path);
36
35
  };
37
- const unregisterPrompt = registerTauSystemPromptContribution({
38
- id: "subagent.catalog",
39
- order: 300,
40
- render: async (_event, ctx) => {
41
- if (!pi.getActiveTools().includes("subagent")) return undefined;
42
- const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
43
- warn(discovery, ctx);
44
- const lines = [...discovery.agents.values()]
45
- .sort((a, b) => a.name.localeCompare(b.name))
46
- .map((agent) => `- ${agent.name}: ${agent.description}`);
47
- return `## Subagents\nUse \`subagent\` when an available agent matches a focused part of the task.\n\nAvailable agents for this turn:\n${lines.join("\n")}\n\nDelegate one focused task per call. Children do not inherit parent messages. Include exact absolute reference paths when a child must inspect a repository outside the current working directory.`;
48
- },
36
+ pi.on("before_agent_start", async (event, ctx) => {
37
+ if (!pi.getActiveTools().includes("subagent")) return undefined;
38
+ const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
39
+ warn(discovery, ctx);
40
+ const lines = [...discovery.agents.values()]
41
+ .sort((a, b) => a.name.localeCompare(b.name))
42
+ .map((agent) => `- ${agent.name}: ${agent.description}`);
43
+ const prompt = `## Subagents\nUse \`subagent\` when an available agent matches a focused part of the task.\n\nAvailable agents for this turn:\n${lines.join("\n")}\n\nDelegate one focused task per call. Children do not inherit parent messages. Include exact absolute reference paths when a child must inspect a repository outside the current working directory.`;
44
+ return { systemPrompt: `${event.systemPrompt}\n\n${prompt}` };
49
45
  });
50
46
  pi.registerTool(
51
47
  defineTool<typeof params, SubagentDetails>({
@@ -208,7 +204,6 @@ export default function subagentExtension(pi: ExtensionAPI): void {
208
204
  runtimeWarnings.clear();
209
205
  });
210
206
  pi.on("session_shutdown", () => {
211
- unregisterPrompt();
212
207
  for (const controller of controllers) controller.abort();
213
208
  controllers.clear();
214
209
  });
@@ -28,7 +28,7 @@ Adds `/commit` for semantic commit grouping, review, and committing selected rep
28
28
 
29
29
  ## context
30
30
 
31
- Adds `/context` to select reusable repository work scopes from `.pi/contexts`, and `/context-sync` to reconcile affected scopes from current Git changes. Folder names are tabs, TOML files are concepts, and TOML sections are selectable entries.
31
+ Adds `/context` to select reusable repository work scopes from `.pi/contexts`, and `/context-sync` to reconcile affected scopes from current Git changes. Tau automatically validates changed-file membership and stale references after agent turns. Folder names are tabs, TOML files are concepts, and TOML sections are selectable entries.
32
32
 
33
33
  ## explore
34
34
 
@@ -74,13 +74,17 @@ Adds `/reference` to manage separate repositories kept outside the current proje
74
74
 
75
75
  Shows a compact display-only marker after each run with wall time and model cost. It does not enter agent context.
76
76
 
77
+ ## runtime-context
78
+
79
+ Supplies the agent with the current local date and an initial root directory snapshot as hidden session context.
80
+
77
81
  ## silent-command-runner
78
82
 
79
83
  Runs configured commands while keeping their output out of agent context when that is useful.
80
84
 
81
85
  ## soul
82
86
 
83
- Manages the session’s soul prompt and its persistent working guidance.
87
+ Adds Rok’s persona and operating rules to Pi’s native assistant prompt.
84
88
 
85
89
  ## stash
86
90