@shanepadgett/tau-agent 0.27.0 → 0.28.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 (113) hide show
  1. package/extensions/context/sync.ts +1 -9
  2. package/extensions/context-pruning/prune.ts +2 -2
  3. package/extensions/explore/README.md +13 -21
  4. package/extensions/explore/ast/adapter.ts +97 -0
  5. package/extensions/explore/ast/engine.ts +470 -0
  6. package/extensions/explore/ast/format/ast-search.ts +97 -0
  7. package/extensions/explore/ast/format/composite.ts +26 -0
  8. package/extensions/explore/ast/format/context.ts +66 -0
  9. package/extensions/explore/ast/format/deps.ts +86 -0
  10. package/extensions/explore/ast/format/discover.ts +85 -0
  11. package/extensions/explore/ast/format/impact.ts +105 -0
  12. package/extensions/explore/ast/format/outline.ts +66 -0
  13. package/extensions/explore/ast/format/relationships.ts +81 -0
  14. package/extensions/explore/ast/format/show.ts +38 -0
  15. package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
  16. package/extensions/explore/ast/grammars/manifest.json +41 -0
  17. package/extensions/explore/ast/grammars/manifest.ts +99 -0
  18. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  19. package/extensions/explore/ast/grammars/swift.wasm +0 -0
  20. package/extensions/explore/ast/graph/file-graph.ts +360 -0
  21. package/extensions/explore/ast/graph/relationships.ts +597 -0
  22. package/extensions/explore/ast/identity.ts +119 -0
  23. package/extensions/explore/ast/ir.ts +130 -0
  24. package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
  25. package/extensions/explore/ast/languages/csharp.ts +493 -0
  26. package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
  27. package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
  28. package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
  29. package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
  30. package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
  31. package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
  32. package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
  33. package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
  34. package/extensions/explore/ast/languages/go.ts +545 -0
  35. package/extensions/explore/ast/languages/java.ts +412 -0
  36. package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
  37. package/extensions/explore/ast/languages/kotlin.ts +611 -0
  38. package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
  39. package/extensions/explore/ast/languages/odin.ts +407 -0
  40. package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
  41. package/extensions/explore/ast/languages/rust.ts +506 -0
  42. package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
  43. package/extensions/explore/ast/languages/swift.ts +466 -0
  44. package/extensions/explore/ast/languages/tree.ts +220 -0
  45. package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
  46. package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
  47. package/extensions/explore/ast/languages/typescript.ts +716 -0
  48. package/extensions/explore/ast/markdown.ts +145 -0
  49. package/extensions/explore/ast/package-surface.ts +42 -0
  50. package/extensions/explore/ast/queries/ast-search.ts +306 -0
  51. package/extensions/explore/ast/queries/composite-target.ts +77 -0
  52. package/extensions/explore/ast/queries/context.ts +406 -0
  53. package/extensions/explore/ast/queries/discover.ts +427 -0
  54. package/extensions/explore/ast/queries/impact.ts +250 -0
  55. package/extensions/explore/ast/queries/outline.ts +203 -0
  56. package/extensions/explore/ast/queries/show.ts +262 -0
  57. package/extensions/explore/ast/query.ts +48 -0
  58. package/extensions/explore/ast/read/hook.ts +125 -0
  59. package/extensions/explore/ast/read/policy.ts +55 -0
  60. package/extensions/explore/ast/registry.ts +120 -0
  61. package/extensions/explore/ast/scan.ts +76 -0
  62. package/extensions/explore/ast/slice.ts +31 -0
  63. package/extensions/explore/ast/tools/ast-search.ts +138 -0
  64. package/extensions/explore/ast/tools/context.ts +126 -0
  65. package/extensions/explore/ast/tools/deps.ts +72 -0
  66. package/extensions/explore/ast/tools/discover.ts +208 -0
  67. package/extensions/explore/ast/tools/impact.ts +135 -0
  68. package/extensions/explore/ast/tools/outline.ts +200 -0
  69. package/extensions/explore/ast/tools/relationships.ts +160 -0
  70. package/extensions/explore/ast/tools/render.ts +121 -0
  71. package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
  72. package/extensions/explore/ast/tools/show.ts +116 -0
  73. package/extensions/explore/guidance.ts +52 -0
  74. package/extensions/explore/index.ts +82 -92
  75. package/extensions/explore/read/autoread.ts +118 -0
  76. package/extensions/explore/settings.ts +49 -31
  77. package/extensions/explore/traverse.ts +254 -91
  78. package/extensions/handoff/index.ts +1 -1
  79. package/extensions/publish/index.ts +18 -85
  80. package/extensions/publish/publish-ui.ts +0 -4
  81. package/extensions/soul/README.md +7 -4
  82. package/extensions/soul/index.ts +11 -5
  83. package/extensions/soul/prompt.ts +31 -13
  84. package/extensions/soul/settings.ts +7 -2
  85. package/extensions/subagent/agents/context-sync.md +6 -10
  86. package/extensions/subagent/agents/review.md +24 -55
  87. package/extensions/subagent/agents/scout.md +38 -115
  88. package/extensions/subagent/run.ts +1 -1
  89. package/extensions/tau-help/help.md +2 -4
  90. package/package.json +6 -4
  91. package/schemas/tau.schema.json +35 -30
  92. package/{extensions/explore → shared}/autoread.ts +21 -7
  93. package/shared/events.ts +1 -1
  94. package/shared/full-file-knowledge.ts +49 -0
  95. package/extensions/explore/ast-guidance.ts +0 -67
  96. package/extensions/explore/ast-languages.ts +0 -61
  97. package/extensions/explore/ast-tools.ts +0 -1994
  98. package/extensions/explore/ast-worker.ts +0 -1025
  99. package/extensions/explore/find.ts +0 -147
  100. package/extensions/explore/full-file-knowledge.ts +0 -234
  101. package/extensions/explore/grep.ts +0 -749
  102. package/extensions/explore/limits.ts +0 -22
  103. package/extensions/explore/ls.ts +0 -113
  104. package/extensions/explore/orientation-state.ts +0 -280
  105. package/extensions/explore/path-display.ts +0 -41
  106. package/extensions/explore/path-tree.ts +0 -150
  107. package/extensions/explore/read-cache.ts +0 -273
  108. package/extensions/explore/read-snapshots.ts +0 -57
  109. package/extensions/explore/read-stats-panel.ts +0 -176
  110. package/extensions/explore/read-stats.ts +0 -138
  111. package/extensions/explore/read.ts +0 -372
  112. package/extensions/explore/result.ts +0 -29
  113. package/native-bin/darwin-arm64/tau-ast +0 -0
@@ -1,40 +1,80 @@
1
1
  import type { Stats } from "node:fs";
2
2
  import { lstat, opendir, readFile } from "node:fs/promises";
3
- import { dirname, join, matchesGlob, resolve, sep } from "node:path";
4
- import { formatPathForDisplay, isWithinPath, relativeSlash, toSlashPath } from "./path-display.ts";
3
+ import { isAbsolute, join, matchesGlob, relative, resolve, sep } from "node:path";
4
+
5
+ /** Default recursive budgets for structural directory ops (path-conventions.md). */
6
+ export const DEFAULT_TRAVERSAL_BUDGETS = {
7
+ maxFiles: 2000,
8
+ maxSourceBytes: 64 * 1024 * 1024,
9
+ maxDepth: 32,
10
+ maxElapsedMs: 20_000,
11
+ } as const;
12
+
13
+ export type TraversalBudgets = {
14
+ maxFiles: number;
15
+ maxSourceBytes: number;
16
+ maxDepth: number;
17
+ maxElapsedMs: number;
18
+ };
5
19
 
6
20
  export type PathKind = "file" | "dir";
7
21
 
8
- export interface TraversalEntry {
22
+ export type TraversalHit = {
9
23
  absolutePath: string;
10
24
  displayPath: string;
11
25
  type: PathKind;
12
26
  stats: Stats;
13
27
  depth: number;
14
- empty?: boolean;
15
- }
28
+ };
16
29
 
17
- export interface CollectPathOptions {
30
+ export type TraversalLimit = "maxFiles" | "maxSourceBytes" | "maxDepth" | "maxElapsedMs" | "cancelled";
31
+
32
+ export type TraversalResult = {
33
+ entries: TraversalHit[];
34
+ /** Set when a budget or cancellation stopped the walk before the tree was complete. */
35
+ limit: TraversalLimit | undefined;
36
+ filesVisited: number;
37
+ sourceBytes: number;
38
+ elapsedMs: number;
39
+ };
40
+
41
+ export type WalkOptions = {
18
42
  cwd: string;
19
43
  root: string;
20
- maxDepth?: number;
21
- maxEntries?: number;
22
- includeRoot: boolean;
23
- includeHidden: boolean;
24
- includeIgnored: boolean;
25
- includeNoise: boolean;
26
- }
44
+ /** Include the root path itself when it is a file or directory. Default true. */
45
+ includeRoot?: boolean;
46
+ /** Descend into hidden path segments (name starts with `.`). Default false. */
47
+ includeHidden?: boolean;
48
+ /** Include paths matching gitignore-style rules. Default false. */
49
+ includeIgnored?: boolean;
50
+ /** Include common noise directories (node_modules, dist, …). Default false. */
51
+ includeNoise?: boolean;
52
+ /** Only yield files (skip directory entries in the result list). Default true. */
53
+ filesOnly?: boolean;
54
+ budgets?: Partial<TraversalBudgets>;
55
+ signal?: AbortSignal;
56
+ /**
57
+ * Called after each accepted file. Return false to stop early without a budget limit
58
+ * (caller owns the stop reason). Async allowed for cooperative yielding.
59
+ */
60
+ onFile?: (hit: TraversalHit) => void | boolean | Promise<void | boolean>;
61
+ /**
62
+ * If set, files for which this returns false are skipped without counting toward budgets.
63
+ * Directories are not filtered here.
64
+ */
65
+ matchFile?: (hit: TraversalHit) => boolean;
66
+ };
27
67
 
28
- interface IgnoreRule {
68
+ type IgnoreRule = {
29
69
  baseDir: string;
30
70
  pattern: string;
31
71
  negated: boolean;
32
72
  dirOnly: boolean;
33
73
  hasSlash: boolean;
34
74
  anchored: boolean;
35
- }
75
+ };
36
76
 
37
- const NOISE_PATH_NAMES = new Set([
77
+ const NOISE_NAMES = new Set([
38
78
  ".git",
39
79
  "node_modules",
40
80
  "dist",
@@ -47,20 +87,65 @@ const NOISE_PATH_NAMES = new Set([
47
87
  "out",
48
88
  ]);
49
89
 
50
- function entryKind(stats: Stats): PathKind {
51
- return stats.isDirectory() ? "dir" : "file";
90
+ export function stripLeadingAt(value: string): string {
91
+ return value.startsWith("@") ? value.slice(1) : value;
92
+ }
93
+
94
+ function toSlashPath(value: string): string {
95
+ return value.replaceAll("\\", "/");
96
+ }
97
+
98
+ export function resolveExplorePath(cwd: string, input: string): string {
99
+ const path = stripLeadingAt(input);
100
+ if (path.length === 0) return resolve(cwd);
101
+ return isAbsolute(path) ? resolve(path) : resolve(cwd, path);
102
+ }
103
+
104
+ export function pathResolutionError(error: unknown, input: string): Error {
105
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
106
+ return new Error(`Path not found: ${stripLeadingAt(input)}`);
107
+ }
108
+ return error instanceof Error ? error : new Error(String(error));
109
+ }
110
+
111
+ function isWithinPath(parent: string, child: string): boolean {
112
+ const rel = relative(resolve(parent), resolve(child));
113
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
114
+ }
115
+
116
+ export function formatPathForDisplay(absolutePath: string, cwd: string): string {
117
+ const resolvedCwd = resolve(cwd);
118
+ const resolvedPath = resolve(absolutePath);
119
+ const rel = relative(resolvedCwd, resolvedPath);
120
+ if (rel === "") return ".";
121
+ if (!rel.startsWith("..") && !isAbsolute(rel)) return toSlashPath(rel);
122
+ return toSlashPath(resolvedPath);
123
+ }
124
+
125
+ function relativeSlash(from: string, to: string): string {
126
+ const rel = relative(resolve(from), resolve(to));
127
+ return rel === "" ? "." : toSlashPath(rel);
128
+ }
129
+
130
+ function mergeBudgets(partial: Partial<TraversalBudgets> | undefined): TraversalBudgets {
131
+ return {
132
+ maxFiles: partial?.maxFiles ?? DEFAULT_TRAVERSAL_BUDGETS.maxFiles,
133
+ maxSourceBytes: partial?.maxSourceBytes ?? DEFAULT_TRAVERSAL_BUDGETS.maxSourceBytes,
134
+ maxDepth: partial?.maxDepth ?? DEFAULT_TRAVERSAL_BUDGETS.maxDepth,
135
+ maxElapsedMs: partial?.maxElapsedMs ?? DEFAULT_TRAVERSAL_BUDGETS.maxElapsedMs,
136
+ };
52
137
  }
53
138
 
54
139
  function pathSegments(displayPath: string): string[] {
55
140
  return displayPath.split("/").filter((segment) => segment.length > 0 && segment !== ".");
56
141
  }
57
142
 
58
- function hasHiddenPathSegment(displayPath: string): boolean {
143
+ function hasHiddenSegment(displayPath: string): boolean {
59
144
  return pathSegments(displayPath).some((segment) => segment.startsWith("."));
60
145
  }
61
146
 
62
- function hasNoisePathSegment(displayPath: string): boolean {
63
- return pathSegments(displayPath).some((segment) => NOISE_PATH_NAMES.has(segment));
147
+ function hasNoiseSegment(displayPath: string): boolean {
148
+ return pathSegments(displayPath).some((segment) => NOISE_NAMES.has(segment));
64
149
  }
65
150
 
66
151
  function parseIgnoreLine(baseDir: string, rawLine: string): IgnoreRule | undefined {
@@ -75,7 +160,14 @@ function parseIgnoreLine(baseDir: string, rawLine: string): IgnoreRule | undefin
75
160
  if (anchored) line = line.slice(1);
76
161
  line = toSlashPath(line);
77
162
  if (!line) return undefined;
78
- return { baseDir, pattern: line, negated, dirOnly, hasSlash: line.includes("/"), anchored };
163
+ return {
164
+ baseDir,
165
+ pattern: line,
166
+ negated,
167
+ dirOnly,
168
+ hasSlash: line.includes("/"),
169
+ anchored,
170
+ };
79
171
  }
80
172
 
81
173
  async function readIgnoreRules(baseDir: string): Promise<IgnoreRule[]> {
@@ -90,27 +182,27 @@ async function readIgnoreRules(baseDir: string): Promise<IgnoreRule[]> {
90
182
  }
91
183
  }
92
184
 
93
- async function appendIgnoreRulesForDirectory(
94
- directory: string,
95
- inherited: readonly IgnoreRule[],
96
- ): Promise<IgnoreRule[]> {
97
- const rules = await readIgnoreRules(directory);
98
- return rules.length === 0 ? [...inherited] : [...inherited, ...rules];
185
+ async function appendIgnoreRules(directory: string, inherited: readonly IgnoreRule[]): Promise<IgnoreRule[]> {
186
+ const local = await readIgnoreRules(directory);
187
+ return local.length === 0 ? [...inherited] : [...inherited, ...local];
99
188
  }
100
189
 
101
190
  async function collectIgnoreRulesToDirectory(cwd: string, directory: string): Promise<IgnoreRule[]> {
102
191
  const resolvedCwd = resolve(cwd);
103
192
  const resolvedDirectory = resolve(directory);
104
- if (!isWithinPath(resolvedCwd, resolvedDirectory)) return [];
193
+ // Outside the session cwd (absolute foreign roots): load that directory's own ignore only.
194
+ if (!isWithinPath(resolvedCwd, resolvedDirectory)) {
195
+ return appendIgnoreRules(resolvedDirectory, []);
196
+ }
105
197
 
106
198
  let current = resolvedCwd;
107
- let rules: IgnoreRule[] = await appendIgnoreRulesForDirectory(current, []);
199
+ let rules = await appendIgnoreRules(current, []);
108
200
  const rel = relativeSlash(resolvedCwd, resolvedDirectory);
109
201
  if (rel === ".") return rules;
110
202
 
111
203
  for (const segment of rel.split("/")) {
112
204
  current = join(current, segment.split("/").join(sep));
113
- rules = await appendIgnoreRulesForDirectory(current, rules);
205
+ rules = await appendIgnoreRules(current, rules);
114
206
  }
115
207
  return rules;
116
208
  }
@@ -131,31 +223,43 @@ function isIgnored(absolutePath: string, kind: PathKind, rules: readonly IgnoreR
131
223
  return ignored;
132
224
  }
133
225
 
134
- function shouldSkipPath(
226
+ function shouldSkip(
135
227
  absolutePath: string,
136
228
  kind: PathKind,
137
229
  rules: readonly IgnoreRule[],
138
- options: CollectPathOptions,
230
+ options: {
231
+ walkRoot: string;
232
+ includeHidden: boolean;
233
+ includeIgnored: boolean;
234
+ includeNoise: boolean;
235
+ },
139
236
  ): boolean {
140
- const displayPath = formatPathForDisplay(absolutePath, options.cwd);
141
- if (!options.includeHidden && hasHiddenPathSegment(displayPath)) return true;
142
- if (!options.includeNoise && hasNoisePathSegment(displayPath)) return true;
237
+ // Hidden/noise are relative to the walk root — not the session cwd display path.
238
+ // Otherwise absolute roots under ~/.local (etc.) look entirely "hidden".
239
+ const rel = relativeSlash(options.walkRoot, absolutePath);
240
+ const underRoot = rel === "." || (!rel.startsWith("..") && !isAbsolute(rel));
241
+ const segmentPath = underRoot && rel !== "." ? rel : "";
242
+ if (!options.includeHidden && segmentPath.length > 0 && hasHiddenSegment(segmentPath)) return true;
243
+ if (!options.includeNoise && segmentPath.length > 0 && hasNoiseSegment(segmentPath)) return true;
143
244
  return !options.includeIgnored && isIgnored(absolutePath, kind, rules);
144
245
  }
145
246
 
146
- function sortEntries(entries: TraversalEntry[]): TraversalEntry[] {
147
- return entries.sort((a, b) => {
247
+ function entryKind(stats: Stats): PathKind {
248
+ return stats.isDirectory() ? "dir" : "file";
249
+ }
250
+
251
+ function sortHits(hits: TraversalHit[]): TraversalHit[] {
252
+ return hits.sort((a, b) => {
148
253
  if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
149
254
  return a.displayPath.localeCompare(b.displayPath);
150
255
  });
151
256
  }
152
257
 
153
- async function readDirectoryNames(directory: string, cwd: string, limit: number | undefined): Promise<string[]> {
258
+ async function listNames(directory: string, cwd: string): Promise<string[]> {
154
259
  try {
155
260
  const names: string[] = [];
156
261
  for await (const entry of await opendir(directory)) {
157
262
  names.push(entry.name);
158
- if (limit !== undefined && names.length >= limit) break;
159
263
  }
160
264
  return names;
161
265
  } catch {
@@ -163,46 +267,109 @@ async function readDirectoryNames(directory: string, cwd: string, limit: number
163
267
  }
164
268
  }
165
269
 
166
- export async function collectPaths(options: CollectPathOptions): Promise<TraversalEntry[]> {
167
- const entries: TraversalEntry[] = [];
168
- const entryByPath = new Map<string, TraversalEntry>();
169
- let inspectedEntries = 0;
170
- const rootStats = await lstat(options.root);
171
- const rootKind = entryKind(rootStats);
172
- const rootDirectory = rootKind === "dir" ? options.root : dirname(options.root);
173
- const rootRules = options.includeIgnored ? [] : await collectIgnoreRulesToDirectory(options.cwd, rootDirectory);
174
-
175
- function pushEntry(absolutePath: string, stats: Stats, depth: number): TraversalEntry {
176
- const entry = {
177
- absolutePath,
178
- displayPath: formatPathForDisplay(absolutePath, options.cwd),
179
- type: entryKind(stats),
180
- stats,
181
- depth,
182
- };
183
- entries.push(entry);
184
- entryByPath.set(absolutePath, entry);
185
- return entry;
270
+ /**
271
+ * Ignore-aware directory walk for structural scans.
272
+ * Budgets (files / source bytes / depth / elapsed) are separate from model output limits.
273
+ * Hitting a budget sets `result.limit` — not silent complete-tree success.
274
+ */
275
+ export async function walkPaths(options: WalkOptions): Promise<TraversalResult> {
276
+ const budgets = mergeBudgets(options.budgets);
277
+ const includeRoot = options.includeRoot !== false;
278
+ const includeHidden = options.includeHidden === true;
279
+ const includeIgnored = options.includeIgnored === true;
280
+ const includeNoise = options.includeNoise === true;
281
+ const filesOnly = options.filesOnly !== false;
282
+ const started = Date.now();
283
+
284
+ const entries: TraversalHit[] = [];
285
+ let filesVisited = 0;
286
+ let sourceBytes = 0;
287
+ let limit: TraversalLimit | undefined;
288
+
289
+ const elapsed = () => Date.now() - started;
290
+
291
+ const budgetHit = (): TraversalLimit | undefined => {
292
+ if (options.signal?.aborted) return "cancelled";
293
+ if (elapsed() >= budgets.maxElapsedMs) return "maxElapsedMs";
294
+ if (filesVisited >= budgets.maxFiles) return "maxFiles";
295
+ if (sourceBytes > budgets.maxSourceBytes) return "maxSourceBytes";
296
+ return undefined;
297
+ };
298
+
299
+ const stopIfNeeded = (): boolean => {
300
+ const hit = budgetHit();
301
+ if (hit === undefined) return false;
302
+ limit = limit ?? hit;
303
+ return true;
304
+ };
305
+
306
+ async function pushFile(hit: TraversalHit): Promise<boolean> {
307
+ if (options.matchFile !== undefined && !options.matchFile(hit)) return true;
308
+ if (stopIfNeeded()) return false;
309
+ if (filesVisited >= budgets.maxFiles) {
310
+ limit = "maxFiles";
311
+ return false;
312
+ }
313
+ const nextBytes = sourceBytes + hit.stats.size;
314
+ // Allow the first file even if it alone exceeds the byte budget; report the limit.
315
+ if (filesVisited > 0 && nextBytes > budgets.maxSourceBytes) {
316
+ limit = "maxSourceBytes";
317
+ return false;
318
+ }
319
+ filesVisited += 1;
320
+ sourceBytes = nextBytes;
321
+ if (sourceBytes > budgets.maxSourceBytes) limit = "maxSourceBytes";
322
+ entries.push(hit);
323
+ const cont = await options.onFile?.(hit);
324
+ if (cont === false) return false;
325
+ return !stopIfNeeded();
186
326
  }
187
327
 
188
- if (options.includeRoot) pushEntry(options.root, rootStats, 0);
189
- if (rootKind !== "dir") return entries;
328
+ const root = resolveExplorePath(options.cwd, options.root);
329
+ const rootStats = await lstat(root);
330
+ const rootKind = entryKind(rootStats);
331
+ const rootDir = rootKind === "dir" ? root : resolve(root, "..");
332
+ const walkRoot = rootKind === "dir" ? root : rootDir;
333
+ const skipOpts = { walkRoot, includeHidden, includeIgnored, includeNoise };
334
+ const rootRules = includeIgnored ? [] : await collectIgnoreRulesToDirectory(options.cwd, rootDir);
190
335
 
191
- async function walkDirectory(directory: string, depth: number, rules: readonly IgnoreRule[]): Promise<number> {
192
- if (options.maxDepth !== undefined && depth >= options.maxDepth) return 0;
193
- if (options.maxEntries !== undefined && inspectedEntries >= options.maxEntries) return 0;
336
+ if (includeRoot) {
337
+ const rootHit: TraversalHit = {
338
+ absolutePath: root,
339
+ displayPath: formatPathForDisplay(root, options.cwd),
340
+ type: rootKind,
341
+ stats: rootStats,
342
+ depth: 0,
343
+ };
344
+ if (rootKind === "file") {
345
+ await pushFile(rootHit);
346
+ return { entries, limit, filesVisited, sourceBytes, elapsedMs: elapsed() };
347
+ }
348
+ if (!filesOnly) entries.push(rootHit);
349
+ } else if (rootKind === "file") {
350
+ return { entries, limit, filesVisited, sourceBytes, elapsedMs: elapsed() };
351
+ }
194
352
 
195
- const remaining = options.maxEntries === undefined ? undefined : options.maxEntries - inspectedEntries;
196
- const names = await readDirectoryNames(directory, options.cwd, remaining);
353
+ async function walkDir(directory: string, depth: number, rules: readonly IgnoreRule[]): Promise<void> {
354
+ if (stopIfNeeded()) return;
355
+ if (depth >= budgets.maxDepth) {
356
+ limit = limit ?? "maxDepth";
357
+ return;
358
+ }
197
359
 
198
- const children: TraversalEntry[] = [];
360
+ const names = await listNames(directory, options.cwd);
361
+ const children: TraversalHit[] = [];
199
362
  for (const name of names) {
200
- if (options.maxEntries !== undefined && inspectedEntries >= options.maxEntries) break;
201
- const childPath = `${directory}/${name}`;
202
- const stats = await lstat(childPath);
203
- inspectedEntries += 1;
363
+ if (stopIfNeeded()) return;
364
+ const childPath = join(directory, name);
365
+ let stats: Stats;
366
+ try {
367
+ stats = await lstat(childPath);
368
+ } catch {
369
+ continue;
370
+ }
204
371
  const kind = entryKind(stats);
205
- if (shouldSkipPath(childPath, kind, rules, options)) continue;
372
+ if (shouldSkip(childPath, kind, rules, skipOpts)) continue;
206
373
  children.push({
207
374
  absolutePath: childPath,
208
375
  displayPath: formatPathForDisplay(childPath, options.cwd),
@@ -212,26 +379,22 @@ export async function collectPaths(options: CollectPathOptions): Promise<Travers
212
379
  });
213
380
  }
214
381
 
215
- for (const child of sortEntries(children)) {
216
- entries.push(child);
217
- entryByPath.set(child.absolutePath, child);
218
- if (child.type !== "dir") continue;
219
- const childRules = options.includeIgnored
220
- ? rules
221
- : await appendIgnoreRulesForDirectory(child.absolutePath, rules);
222
- const visibleChildren = await walkDirectory(child.absolutePath, child.depth, childRules);
223
- if (visibleChildren === 0 && (options.maxDepth === undefined || child.depth < options.maxDepth)) {
224
- child.empty = true;
382
+ for (const child of sortHits(children)) {
383
+ if (stopIfNeeded()) return;
384
+ if (child.type === "file") {
385
+ if (!(await pushFile(child))) return;
386
+ continue;
225
387
  }
388
+ if (!filesOnly) entries.push(child);
389
+ if (child.depth >= budgets.maxDepth) {
390
+ limit = limit ?? "maxDepth";
391
+ continue;
392
+ }
393
+ const childRules = includeIgnored ? rules : await appendIgnoreRules(child.absolutePath, rules);
394
+ await walkDir(child.absolutePath, child.depth, childRules);
226
395
  }
227
-
228
- return children.length;
229
396
  }
230
397
 
231
- const visibleChildren = await walkDirectory(options.root, 0, rootRules);
232
- const rootEntry = entryByPath.get(options.root);
233
- if (rootEntry && visibleChildren === 0 && (options.maxDepth === undefined || options.maxDepth > 0)) {
234
- rootEntry.empty = true;
235
- }
236
- return entries;
398
+ if (rootKind === "dir") await walkDir(root, 0, rootRules);
399
+ return { entries, limit, filesVisited, sourceBytes, elapsedMs: elapsed() };
237
400
  }
@@ -6,7 +6,7 @@ import {
6
6
  type ExtensionAPI,
7
7
  serializeConversation,
8
8
  } from "@earendil-works/pi-coding-agent";
9
- import { prepareAutoreadMessage, type AutoreadDetails } from "../explore/autoread.ts";
9
+ import { prepareAutoreadMessage, type AutoreadDetails } from "../../shared/autoread.ts";
10
10
  import { generateToolValidated, resolveCandidates } from "../../shared/model-fallback/index.ts";
11
11
  import { errorText } from "../../shared/text.ts";
12
12
  import { buildHandoffRequest, type HandoffDraft, handoffDraftFromToolInput, HANDOFF_TOOL } from "./model.ts";
@@ -11,8 +11,6 @@ const WORKFLOW_NAME = "publish.yml";
11
11
  const WORKFLOW_WAIT_MS = 10 * 60_000;
12
12
  const POLL_INTERVAL_MS = 5_000;
13
13
  const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
14
- const CARGO_BUILD_TIMEOUT_MS = 15 * 60_000;
15
- const TAU_AST_SCRIPT = "packages/agent/scripts/package-tau-ast.ts";
16
14
 
17
15
  type Bump = "patch" | "minor" | "major";
18
16
 
@@ -68,8 +66,6 @@ async function publish(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<
68
66
  if (!repo) throw new Error("No git repository found.");
69
67
  if (repo.fileCount > 0)
70
68
  throw new Error("Working tree is not clean. Commit or stash every change before publishing.");
71
- if (process.platform !== "darwin" || process.arch !== "arm64")
72
- throw new Error(`/publish requires darwin-arm64; this host is ${process.platform}-${process.arch}.`);
73
69
  await run(pi, ctx, "gh", ["auth", "status"], repo.root, DEFAULT_COMMAND_TIMEOUT_MS);
74
70
  await run(pi, ctx, "npm", ["run", "check:package-sources"], repo.root, DEFAULT_COMMAND_TIMEOUT_MS);
75
71
 
@@ -129,87 +125,24 @@ async function completePublish(
129
125
  progress?.update("write package versions");
130
126
  await writeReleaseVersion(root, manifests, version);
131
127
  }
132
- try {
133
- progress?.update("build tau-ast darwin-arm64");
134
- await run(
135
- pi,
136
- ctx,
137
- "cargo",
138
- [
139
- "build",
140
- "--release",
141
- "--locked",
142
- "--target",
143
- "aarch64-apple-darwin",
144
- "--manifest-path",
145
- "packages/agent/native/tau-ast/Cargo.toml",
146
- ],
147
- root,
148
- CARGO_BUILD_TIMEOUT_MS,
149
- );
150
- progress?.update("stage tau-ast");
151
- await run(
152
- pi,
153
- ctx,
154
- process.execPath,
155
- ["--experimental-strip-types", TAU_AST_SCRIPT, "stage"],
156
- root,
157
- DEFAULT_COMMAND_TIMEOUT_MS,
158
- );
159
- progress?.update("smoke test tau-ast");
160
- await run(
161
- pi,
162
- ctx,
163
- process.execPath,
164
- ["--experimental-strip-types", TAU_AST_SCRIPT, "smoke"],
165
- root,
166
- DEFAULT_COMMAND_TIMEOUT_MS,
167
- );
168
- progress?.update("npm pack --dry-run @shanepadgett/tau-tui");
169
- await run(
170
- pi,
171
- ctx,
172
- "npm",
173
- ["pack", "--dry-run", "--workspace", "@shanepadgett/tau-tui"],
174
- root,
175
- DEFAULT_COMMAND_TIMEOUT_MS,
176
- );
177
- progress?.update("npm pack --dry-run @shanepadgett/tau-agent");
178
- await run(
179
- pi,
180
- ctx,
181
- "npm",
182
- ["pack", "--dry-run", "--workspace", "@shanepadgett/tau-agent"],
183
- root,
184
- DEFAULT_COMMAND_TIMEOUT_MS,
185
- );
186
- progress?.update("verify packed tau-ast");
187
- await run(
188
- pi,
189
- ctx,
190
- process.execPath,
191
- ["--experimental-strip-types", TAU_AST_SCRIPT, "verify-pack"],
192
- root,
193
- DEFAULT_COMMAND_TIMEOUT_MS,
194
- );
195
- } finally {
196
- await run(
197
- pi,
198
- ctx,
199
- process.execPath,
200
- ["--experimental-strip-types", TAU_AST_SCRIPT, "clean"],
201
- root,
202
- DEFAULT_COMMAND_TIMEOUT_MS,
203
- );
204
- }
205
-
206
- const trackedNativeFiles = await git.run(["ls-files", "--", "packages/agent/native-bin"], { cwd: root });
207
- const stagedNativeFiles = await git.run(["diff", "--cached", "--name-only", "--", "packages/agent/native-bin"], {
208
- cwd: root,
209
- });
210
- if (trackedNativeFiles || stagedNativeFiles)
211
- throw new Error("Release blocked: packages/agent/native-bin/ contains tracked or staged files.");
212
-
128
+ progress?.update("npm pack --dry-run @shanepadgett/tau-tui");
129
+ await run(
130
+ pi,
131
+ ctx,
132
+ "npm",
133
+ ["pack", "--dry-run", "--workspace", "@shanepadgett/tau-tui"],
134
+ root,
135
+ DEFAULT_COMMAND_TIMEOUT_MS,
136
+ );
137
+ progress?.update("npm pack --dry-run @shanepadgett/tau-agent");
138
+ await run(
139
+ pi,
140
+ ctx,
141
+ "npm",
142
+ ["pack", "--dry-run", "--workspace", "@shanepadgett/tau-agent"],
143
+ root,
144
+ DEFAULT_COMMAND_TIMEOUT_MS,
145
+ );
213
146
  if (tag) {
214
147
  progress?.update("git add release files");
215
148
  await git.run(["add", ...PACKAGE_PATHS, LOCKFILE_PATH], { cwd: root });
@@ -4,12 +4,8 @@ import { ToolPanel } from "@shanepadgett/tau-tui";
4
4
 
5
5
  const RELEASE_STEPS = [
6
6
  "write package versions",
7
- "build tau-ast darwin-arm64",
8
- "stage tau-ast",
9
- "smoke test tau-ast",
10
7
  "npm pack --dry-run @shanepadgett/tau-tui",
11
8
  "npm pack --dry-run @shanepadgett/tau-agent",
12
- "verify packed tau-ast",
13
9
  "git add release files",
14
10
  "git commit",
15
11
  "git tag",
@@ -1,19 +1,22 @@
1
1
  # Soul
2
2
 
3
- Soul adds Rok's caveman voice and old-code senior judgment to Pi's native assistant prompt.
3
+ Soul shapes how Tau works and talks by adding two independent sections to Pi's native assistant prompt:
4
+
5
+ - **ponytail** — a lazy-senior-dev build ethos: do the smallest correct thing, reuse before writing, YAGNI, fix bugs at the root, never cut validation, security, or accessibility.
6
+ - **caveman** — a terse communication style: drop filler, keep technical substance exact, quote the shortest decisive line, spell out anything where brevity would risk safety.
4
7
 
5
8
  Pi continues to own tool guidance, project instructions, skills, documentation paths, custom prompts, and working-directory context.
6
9
 
7
- Disable it in Tau settings:
10
+ Toggle each section in Tau settings (both on by default):
8
11
 
9
12
  ```json
10
13
  {
11
14
  "extensions": {
12
- "soul": { "enabled": false }
15
+ "soul": { "ponytail": true, "caveman": false }
13
16
  }
14
17
  }
15
18
  ```
16
19
 
17
- The setting takes effect on session start.
20
+ Settings take effect on session start.
18
21
 
19
22
  After changing this extension, run `/reload` before testing the new behavior.
@@ -1,17 +1,23 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
3
- import { ROK_SOUL } from "./prompt.ts";
3
+ import { CAVEMAN_STYLE, PONYTAIL_ETHOS } from "./prompt.ts";
4
4
  import soulSettings from "./settings.ts";
5
5
 
6
6
  export default function soulExtension(pi: ExtensionAPI): void {
7
- let enabled = true;
7
+ let ponytail = true;
8
+ let caveman = true;
8
9
 
9
10
  pi.on("session_start", async (_event, ctx) => {
10
- enabled = (await loadTauExtensionSettings(ctx, soulSettings)).enabled;
11
+ const settings = await loadTauExtensionSettings(ctx, soulSettings);
12
+ ponytail = settings.ponytail;
13
+ caveman = settings.caveman;
11
14
  });
12
15
 
13
16
  pi.on("before_agent_start", (event) => {
14
- if (!enabled) return undefined;
15
- return { systemPrompt: `${event.systemPrompt}\n\n${ROK_SOUL}` };
17
+ const sections: string[] = [];
18
+ if (ponytail) sections.push(PONYTAIL_ETHOS);
19
+ if (caveman) sections.push(CAVEMAN_STYLE);
20
+ if (sections.length === 0) return undefined;
21
+ return { systemPrompt: [event.systemPrompt, ...sections].join("\n\n") };
16
22
  });
17
23
  }