@shanepadgett/tau-agent 0.33.1 → 0.34.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 (190) hide show
  1. package/docs/context.md +29 -20
  2. package/docs/extending-tau-agent.md +63 -24
  3. package/extensions/appshot/index.ts +112 -111
  4. package/extensions/aside/README.md +7 -0
  5. package/extensions/aside/index.ts +168 -0
  6. package/extensions/aside/panel.ts +83 -0
  7. package/extensions/branch/index.ts +38 -21
  8. package/extensions/cache-diagnostics/index.ts +70 -58
  9. package/extensions/checkpoint/README.md +9 -0
  10. package/extensions/checkpoint/checkpoint-budget.ts +79 -0
  11. package/extensions/checkpoint/checkpoint.ts +277 -0
  12. package/extensions/checkpoint/index.ts +102 -0
  13. package/extensions/checkpoint/messages.ts +169 -0
  14. package/extensions/checkpoint/prompt.ts +24 -0
  15. package/extensions/checkpoint/settings.ts +27 -0
  16. package/extensions/commit/commit-effort.ts +3 -3
  17. package/extensions/context/README.md +12 -8
  18. package/extensions/context/definitions.ts +178 -55
  19. package/extensions/context/index.ts +192 -160
  20. package/extensions/context/panel.ts +57 -37
  21. package/extensions/context/sync.ts +139 -89
  22. package/extensions/context/validation.ts +37 -25
  23. package/extensions/effort/README.md +1 -1
  24. package/extensions/effort/index.ts +3 -3
  25. package/extensions/effort/state.ts +3 -18
  26. package/extensions/explore/guidance.ts +6 -47
  27. package/extensions/explore/index.ts +38 -53
  28. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  29. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  30. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  31. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  32. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  33. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  34. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  35. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  36. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  37. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  38. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  39. package/extensions/explore/tools/target-params.ts +9 -0
  40. package/extensions/footer/index.ts +75 -56
  41. package/extensions/handoff/index.ts +8 -30
  42. package/extensions/image-gen/index.ts +46 -40
  43. package/extensions/patch/executor.ts +190 -135
  44. package/extensions/patch/matcher.ts +82 -48
  45. package/extensions/patch/parser.ts +149 -105
  46. package/extensions/patch/render.ts +32 -39
  47. package/extensions/patch/summary.ts +30 -26
  48. package/extensions/ready/README.md +7 -0
  49. package/extensions/ready/index.ts +58 -0
  50. package/extensions/ready/model.ts +128 -0
  51. package/extensions/ready/packs.ts +272 -0
  52. package/extensions/ready/render.ts +445 -0
  53. package/extensions/ready/scan.ts +760 -0
  54. package/extensions/reference/panel.ts +28 -16
  55. package/extensions/review/index.ts +63 -51
  56. package/extensions/review/panel.ts +5 -34
  57. package/extensions/runtime-context/context.ts +30 -26
  58. package/extensions/script-runner/README.md +2 -2
  59. package/extensions/script-runner/index.ts +122 -82
  60. package/extensions/silent-command-runner/index.ts +38 -23
  61. package/extensions/subagent/agents/context-sync.md +185 -33
  62. package/extensions/subagent/agents/scout.md +2 -2
  63. package/extensions/subagent/agents.ts +137 -84
  64. package/extensions/subagent/cmux-dashboard.ts +39 -30
  65. package/extensions/subagent/index.ts +79 -59
  66. package/extensions/subagent/resume.ts +1 -1
  67. package/extensions/subagent/run.ts +304 -193
  68. package/extensions/subagent/runtime.ts +781 -343
  69. package/extensions/tau/index.ts +47 -38
  70. package/extensions/tau-help/help.md +14 -6
  71. package/extensions/tool-loader/README.md +4 -2
  72. package/extensions/tool-loader/index.ts +77 -52
  73. package/extensions/web/index.ts +6 -3
  74. package/extensions/web/webfetch.ts +114 -86
  75. package/package.json +3 -3
  76. package/schemas/tau.schema.json +20 -33
  77. package/shared/bounded-text-result.ts +1 -0
  78. package/shared/checkpoint-visibility.ts +9 -0
  79. package/shared/events.ts +28 -10
  80. package/shared/isolated-session.ts +71 -29
  81. package/shared/model-effort.ts +9 -19
  82. package/shared/model-fallback/index.ts +66 -41
  83. package/shared/settings/load.ts +45 -38
  84. package/shared/temporary-output-store.ts +16 -10
  85. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  86. package/{extensions/explore → src}/ast/engine.ts +54 -38
  87. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  88. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  89. package/src/ast/format/deps.ts +94 -0
  90. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  91. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  92. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  93. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  94. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  95. package/src/ast/format/targeted-result.ts +20 -0
  96. package/src/ast/grammars/c_sharp.wasm +0 -0
  97. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  98. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  99. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  100. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  101. package/{extensions/explore → src}/ast/ir.ts +6 -0
  102. package/src/ast/languages/csharp-file-deps.ts +90 -0
  103. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  104. package/src/ast/languages/dotted-index.ts +131 -0
  105. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  106. package/src/ast/languages/fixtures/sample.cs +53 -0
  107. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  108. package/src/ast/languages/fixtures/sample.go +37 -0
  109. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  110. package/src/ast/languages/fixtures/sample.java +40 -0
  111. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  112. package/src/ast/languages/fixtures/sample.kt +31 -0
  113. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  114. package/src/ast/languages/fixtures/sample.md +23 -0
  115. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  116. package/src/ast/languages/fixtures/sample.rs +54 -0
  117. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  118. package/src/ast/languages/fixtures/sample.swift +45 -0
  119. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  120. package/src/ast/languages/fixtures/sample.ts +45 -0
  121. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  122. package/src/ast/languages/fixtures/sample.tsx +36 -0
  123. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  124. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  125. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  126. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  127. package/src/ast/languages/jvm-file-deps.ts +135 -0
  128. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  129. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  130. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  131. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  132. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  133. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  134. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  135. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  136. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  137. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  138. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  139. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  140. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  141. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  142. package/src/ast/queries/impact.ts +291 -0
  143. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  144. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  145. package/{extensions/explore → src}/ast/query.ts +1 -1
  146. package/{extensions/explore → src}/ast/registry.ts +1 -2
  147. package/{extensions/explore → src}/ast/scan.ts +1 -1
  148. package/src/ast/session.ts +50 -0
  149. package/{extensions/explore → src}/ast/slice.ts +8 -0
  150. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  151. package/src/file-injection/index.ts +547 -0
  152. package/src/image-generation/client.ts +50 -31
  153. package/src/image-generation/index.ts +177 -134
  154. package/src/index.ts +2 -0
  155. package/src/tool-loading/index.ts +58 -0
  156. package/extensions/context/evidence.ts +0 -517
  157. package/extensions/context/projection.ts +0 -141
  158. package/extensions/context/state.ts +0 -30
  159. package/extensions/context/write-scope.ts +0 -109
  160. package/extensions/explore/ast/format/deps.ts +0 -86
  161. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  162. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  163. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  164. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  165. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  166. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  167. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  168. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  169. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  170. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  171. package/extensions/explore/ast/languages/odin.ts +0 -407
  172. package/extensions/explore/ast/queries/impact.ts +0 -250
  173. package/extensions/explore/outline-injection.ts +0 -151
  174. package/extensions/explore/read/autoread.ts +0 -118
  175. package/extensions/working-memory/README.md +0 -17
  176. package/extensions/working-memory/checkpoint.ts +0 -242
  177. package/extensions/working-memory/index.ts +0 -341
  178. package/extensions/working-memory/memory.ts +0 -235
  179. package/extensions/working-memory/render.ts +0 -185
  180. package/extensions/working-memory/settings.ts +0 -38
  181. package/extensions/working-memory/state.ts +0 -158
  182. package/shared/autoread.ts +0 -234
  183. package/shared/context-messages.ts +0 -19
  184. package/shared/outline-injection.ts +0 -56
  185. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  186. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  187. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  188. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  189. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  190. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -11,16 +11,14 @@ import {
11
11
  extensionPathsForTools,
12
12
  runSubagentTurn,
13
13
  } from "../subagent/run.ts";
14
- import { collectSyncEvidence, CONTEXT_SYNC_EVIDENCE_TOOL } from "./evidence.ts";
15
14
  import { pathExists } from "./definitions.ts";
16
15
  import contextSettings from "./settings.ts";
17
- import { formatContextValidationFailure, validateContextCatalog } from "./validation.ts";
18
- import { restoreOutsideContextMutations, snapshotOutsideContext } from "./write-scope.ts";
16
+ import { formatContextValidationFailure, listEligibleDirtyPaths, validateContextCatalog } from "./validation.ts";
19
17
 
20
18
  const CONTEXT_SYNC_AGENT = "context-sync";
21
19
 
22
20
  export interface ContextSyncDetails {
23
- outcome: "applied" | "no-change" | "failed";
21
+ outcome: "applied" | "no-change" | "failed" | "cancelled";
24
22
  summary: string;
25
23
  reason: string;
26
24
  changedContextFiles: string[];
@@ -28,53 +26,80 @@ export interface ContextSyncDetails {
28
26
  validationFailure?: string;
29
27
  }
30
28
 
29
+ type SyncStatus = (status: string) => void | Promise<void>;
30
+ type GitRunner = ReturnType<typeof createGitRunner>;
31
+
31
32
  let syncQueue = Promise.resolve();
32
33
 
33
34
  export async function runContextSync(
34
35
  pi: ExtensionAPI,
35
36
  ctx: ExtensionContext,
36
- options: { nudge?: string; signal?: AbortSignal; onStatus?: (status: string) => void | Promise<void> } = {},
37
+ options: { nudge?: string; signal?: AbortSignal; onStatus?: SyncStatus } = {},
37
38
  ): Promise<ContextSyncDetails> {
38
39
  return withSyncLock(() => runContextSyncLocked(pi, ctx, options));
39
40
  }
40
41
 
41
- async function runContextSyncLocked(
42
+ function noChangeResult(reason: string, agentResponse?: string): ContextSyncDetails {
43
+ return {
44
+ outcome: "no-change",
45
+ summary: "Existing context mappings already fit the changed scope.",
46
+ reason,
47
+ changedContextFiles: [],
48
+ agentResponse,
49
+ };
50
+ }
51
+
52
+ async function inspectSyncNeed(
42
53
  pi: ExtensionAPI,
43
54
  ctx: ExtensionContext,
44
- options: { nudge?: string; signal?: AbortSignal; onStatus?: (status: string) => void | Promise<void> },
45
- ): Promise<ContextSyncDetails> {
46
- if (!ctx.isProjectTrusted()) throw new Error("Context sync requires a trusted project");
47
- await options.onStatus?.("Inspecting repository context");
55
+ nudge: string | undefined,
56
+ onStatus?: SyncStatus,
57
+ ): Promise<
58
+ | { kind: "done"; result: ContextSyncDetails }
59
+ | { kind: "run"; git: GitRunner; root: string; ignoreGlobs: readonly string[] }
60
+ > {
61
+ await onStatus?.("Inspecting repository context");
48
62
  const git = createGitRunner(pi, ctx);
49
63
  const status = await loadRepoStatus(git);
50
64
  if (!status) throw new Error("No Git repository found");
51
65
  const settings = await loadTauExtensionSettings(ctx, contextSettings);
52
- const evidence = await collectSyncEvidence(git, status.root, settings.validation.ignoreGlobs);
53
- if (evidence.files.length === 0 && evidence.missingPaths.size === 0)
66
+ const ignoreGlobs = settings.validation.ignoreGlobs;
67
+ const validation = await validateContextCatalog(git, status.root, ignoreGlobs);
68
+ const dirtyEligible = await listEligibleDirtyPaths(git, status.root, ignoreGlobs);
69
+ const force = Boolean(nudge?.trim());
70
+ if (!force && validation.stale.length === 0 && validation.uncovered.length === 0 && dirtyEligible.length === 0) {
54
71
  return {
55
- outcome: "no-change",
56
- summary: "Existing context mappings already fit the changed scope.",
57
- reason: "Changed files are outside context catalog scope.",
58
- changedContextFiles: [],
72
+ kind: "done",
73
+ result: noChangeResult("No eligible dirty files, stale catalog paths, or sync nudge."),
59
74
  };
75
+ }
76
+ return { kind: "run", git, root: status.root, ignoreGlobs };
77
+ }
60
78
 
61
- const beforeCatalog = await catalogFileSnapshot(status.root);
62
- const outsideBefore = await snapshotOutsideContext(git, status.root);
79
+ async function resolveContextSyncDefinition(ctx: ExtensionContext) {
63
80
  const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
64
81
  const definition = discovery.agents.get(CONTEXT_SYNC_AGENT);
65
- if (!definition) {
66
- const reason =
67
- discovery.invalid
68
- .get(CONTEXT_SYNC_AGENT)
69
- ?.map((item) => item.reason)
70
- .join("; ") ?? "unknown agent";
71
- throw new Error(`Context sync agent unavailable: ${reason}`);
72
- }
73
-
74
- const task = buildContextSyncTask(status.root, options.nudge);
75
- const signal = options.signal ?? ctx.signal ?? new AbortController().signal;
82
+ if (definition) return definition;
83
+ const reason =
84
+ discovery.invalid
85
+ .get(CONTEXT_SYNC_AGENT)
86
+ ?.map((item) => item.reason)
87
+ .join("; ") ?? "unknown agent";
88
+ throw new Error(`Context sync agent unavailable: ${reason}`);
89
+ }
76
90
 
77
- await options.onStatus?.("Running context-sync subagent");
91
+ async function runContextSyncAgent(
92
+ pi: ExtensionAPI,
93
+ ctx: ExtensionContext,
94
+ root: string,
95
+ nudge: string | undefined,
96
+ signal: AbortSignal,
97
+ onStatus: SyncStatus | undefined,
98
+ beforeCatalog: Map<string, string>,
99
+ ): Promise<ContextSyncDetails | { agentResponse: string }> {
100
+ const definition = await resolveContextSyncDefinition(ctx);
101
+ const task = buildContextSyncTask(root, nudge);
102
+ await onStatus?.("Running context-sync subagent");
78
103
  const thread = await createSubagentThread({
79
104
  id: `context-sync-${Date.now()}`,
80
105
  displayName: definition.names[0] ?? definition.name,
@@ -88,7 +113,6 @@ async function runContextSyncLocked(
88
113
  ctx.ui.notify(`Context sync agent: ${warning}`, "warning");
89
114
  },
90
115
  });
91
- let agentResponse = "";
92
116
  try {
93
117
  const result = await runSubagentTurn({
94
118
  thread,
@@ -97,49 +121,36 @@ async function runContextSyncLocked(
97
121
  signal,
98
122
  onUpdate: async (details) => {
99
123
  const activity = details.currentActivity ?? details.response;
100
- if (activity) await options.onStatus?.(activity.slice(0, 160));
124
+ if (activity) await onStatus?.(activity.slice(0, 160));
101
125
  },
102
126
  });
103
- agentResponse = result.content;
127
+ if (signal.aborted || result.details.status === "aborted") return cancelledResult(result.content);
104
128
  if (result.details.status !== "completed") {
105
- const writeScopeViolations = await restoreOutsideContextMutations(git, status.root, outsideBefore);
106
129
  return {
107
130
  outcome: "failed",
108
131
  summary: "Context sync subagent failed.",
109
- reason: [
110
- result.details.error ?? result.content,
111
- writeScopeViolations.length
112
- ? `Restored out-of-scope writes:\n${writeScopeViolations.map((path) => `- ${path}`).join("\n")}`
113
- : undefined,
114
- ]
115
- .filter((line): line is string => Boolean(line))
116
- .join("\n\n"),
117
- changedContextFiles: changedCatalogPaths(beforeCatalog, await catalogFileSnapshot(status.root)),
118
- agentResponse,
132
+ reason: result.details.error ?? result.content,
133
+ changedContextFiles: changedCatalogPaths(beforeCatalog, await catalogFileSnapshot(root)),
134
+ agentResponse: result.content,
119
135
  };
120
136
  }
137
+ return { agentResponse: result.content };
121
138
  } finally {
122
139
  await disposeSubagentThread(thread);
123
140
  }
141
+ }
124
142
 
125
- await options.onStatus?.("Verifying write scope");
126
- const writeScopeViolations = await restoreOutsideContextMutations(git, status.root, outsideBefore);
127
- const afterCatalog = await catalogFileSnapshot(status.root);
128
- const changedContextFiles = changedCatalogPaths(beforeCatalog, afterCatalog);
129
- if (writeScopeViolations.length > 0) {
130
- return {
131
- outcome: "failed",
132
- summary: "Context sync wrote outside .pi/contexts; out-of-scope paths were restored.",
133
- reason: `Out-of-scope writes restored:\n${writeScopeViolations.map((path) => `- ${path}`).join("\n")}`,
134
- changedContextFiles,
135
- agentResponse,
136
- };
137
- }
138
-
139
- await options.onStatus?.("Verifying context catalog");
140
- const validationFailure = formatContextValidationFailure(
141
- await validateContextCatalog(git, status.root, settings.validation.ignoreGlobs),
142
- );
143
+ async function finalizeContextSync(
144
+ git: GitRunner,
145
+ root: string,
146
+ ignoreGlobs: readonly string[],
147
+ beforeCatalog: Map<string, string>,
148
+ agentResponse: string,
149
+ onStatus?: SyncStatus,
150
+ ): Promise<ContextSyncDetails> {
151
+ const changedContextFiles = changedCatalogPaths(beforeCatalog, await catalogFileSnapshot(root));
152
+ await onStatus?.("Verifying context catalog");
153
+ const validationFailure = formatContextValidationFailure(await validateContextCatalog(git, root, ignoreGlobs));
143
154
  if (validationFailure) {
144
155
  return {
145
156
  outcome: "failed",
@@ -151,13 +162,7 @@ async function runContextSyncLocked(
151
162
  };
152
163
  }
153
164
  if (changedContextFiles.length === 0) {
154
- return {
155
- outcome: "no-change",
156
- summary: "Existing context mappings already fit the changed scope.",
157
- reason: agentResponse.trim() || "Context-sync subagent made no catalog edits.",
158
- changedContextFiles: [],
159
- agentResponse,
160
- };
165
+ return noChangeResult(agentResponse.trim() || "Context-sync subagent made no catalog edits.", agentResponse);
161
166
  }
162
167
  return {
163
168
  outcome: "applied",
@@ -168,13 +173,59 @@ async function runContextSyncLocked(
168
173
  };
169
174
  }
170
175
 
171
- export function buildContextSyncTask(root: string, nudge?: string): string {
176
+ async function runContextSyncLocked(
177
+ pi: ExtensionAPI,
178
+ ctx: ExtensionContext,
179
+ options: { nudge?: string; signal?: AbortSignal; onStatus?: SyncStatus },
180
+ ): Promise<ContextSyncDetails> {
181
+ if (!ctx.isProjectTrusted()) throw new Error("Context sync requires a trusted project");
182
+ const signal = options.signal ?? ctx.signal ?? new AbortController().signal;
183
+ if (signal.aborted) return cancelledResult();
184
+
185
+ const need = await inspectSyncNeed(pi, ctx, options.nudge, options.onStatus);
186
+ if (need.kind === "done") return need.result;
187
+ if (signal.aborted) return cancelledResult();
188
+
189
+ const beforeCatalog = await catalogFileSnapshot(need.root);
190
+ const agentResult = await runContextSyncAgent(
191
+ pi,
192
+ ctx,
193
+ need.root,
194
+ options.nudge,
195
+ signal,
196
+ options.onStatus,
197
+ beforeCatalog,
198
+ );
199
+ if ("outcome" in agentResult) return agentResult;
200
+ if (signal.aborted) return cancelledResult(agentResult.agentResponse);
201
+ return finalizeContextSync(
202
+ need.git,
203
+ need.root,
204
+ need.ignoreGlobs,
205
+ beforeCatalog,
206
+ agentResult.agentResponse,
207
+ options.onStatus,
208
+ );
209
+ }
210
+
211
+ function cancelledResult(agentResponse?: string): ContextSyncDetails {
212
+ return {
213
+ outcome: "cancelled",
214
+ summary: "Context sync cancelled",
215
+ reason: agentResponse?.trim() || "Cancelled by user.",
216
+ changedContextFiles: [],
217
+ agentResponse,
218
+ };
219
+ }
220
+
221
+ function buildContextSyncTask(root: string, nudge?: string): string {
172
222
  const trimmed = nudge?.trim();
173
223
  return [
174
- `Synchronize the repository context catalog at ${root}.`,
175
- "Use context_sync_evidence for git/catalog facts. Edit only .pi/contexts with patch.",
176
- "Walk the typology ladder out loud before path placement. Recheck invariants before finishing.",
177
- trimmed ? `Human nudge (soft steer, does not skip evidence or ladder):\n${trimmed}` : undefined,
224
+ `Keep the repository context catalog at ${root} honest.`,
225
+ "Job: update `.pi/contexts` so work packs match the codebase.",
226
+ "Use normal repo tools. Prefer patch for catalog edits under `.pi/contexts`.",
227
+ "Walk domain → concept → job entry before placing paths. Harness verifies catalog coverage after you finish.",
228
+ trimmed ? `Human nudge (soft steer, does not skip the ladder or coverage):\n${trimmed}` : undefined,
178
229
  ]
179
230
  .filter((line): line is string => Boolean(line))
180
231
  .join("\n\n");
@@ -198,18 +249,20 @@ async function catalogFileSnapshot(root: string): Promise<Map<string, string>> {
198
249
  const snapshot = new Map<string, string>();
199
250
  const base = join(root, ".pi", "contexts");
200
251
  if (!(await pathExists(base))) return snapshot;
201
- for (const tab of (await readdir(base, { withFileTypes: true }))
202
- .filter((item) => item.isDirectory())
203
- .sort((a, b) => a.name.localeCompare(b.name))) {
204
- for (const file of (await readdir(join(base, tab.name), { withFileTypes: true }))
205
- .filter((item) => item.isFile() && extname(item.name) === ".toml")
206
- .sort((a, b) => a.name.localeCompare(b.name))) {
207
- const absolute = join(base, tab.name, file.name);
208
- const path = relative(root, absolute).split(sep).join("/");
209
- const hash = createHash("sha256")
210
- .update(await readFile(absolute))
211
- .digest("hex");
212
- snapshot.set(path, hash);
252
+ const stack = [base];
253
+ while (stack.length > 0) {
254
+ const directory = stack.pop();
255
+ if (!directory) continue;
256
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
257
+ const fullPath = join(directory, entry.name);
258
+ if (entry.isDirectory()) {
259
+ stack.push(fullPath);
260
+ continue;
261
+ }
262
+ if (!entry.isFile() || extname(entry.name) !== ".toml") continue;
263
+ const relativePath = relative(root, fullPath).split(sep).join("/");
264
+ const content = await readFile(fullPath, "utf8");
265
+ snapshot.set(relativePath, createHash("sha256").update(content).digest("hex"));
213
266
  }
214
267
  }
215
268
  return snapshot;
@@ -221,6 +274,3 @@ function changedCatalogPaths(before: Map<string, string>, after: Map<string, str
221
274
  .filter((path) => before.get(path) !== after.get(path))
222
275
  .sort((left, right) => left.localeCompare(right));
223
276
  }
224
-
225
- // Ensure evidence tool name stays aligned with agent definition tooling.
226
- export const CONTEXT_SYNC_REQUIRED_TOOLS = ["read", "bash", "patch", CONTEXT_SYNC_EVIDENCE_TOOL] as const;
@@ -8,6 +8,17 @@ export interface ContextValidationResult {
8
8
  uncovered: string[];
9
9
  }
10
10
 
11
+ export async function listEligibleDirtyPaths(
12
+ git: GitRunner,
13
+ root: string,
14
+ ignoreGlobs: readonly string[],
15
+ ): Promise<string[]> {
16
+ const status = await git.run(["status", "--porcelain=v2", "-z", "--untracked-files=all"], { cwd: root });
17
+ return [...parseDirtyPaths(status)]
18
+ .filter((path) => !isSensitiveContextPath(path) && isContextEligiblePath(path, ignoreGlobs))
19
+ .sort((left, right) => left.localeCompare(right));
20
+ }
21
+
11
22
  export async function validateContextCatalog(
12
23
  git: GitRunner,
13
24
  root: string,
@@ -28,31 +39,8 @@ export async function validateContextCatalog(
28
39
  }
29
40
  }
30
41
 
31
- const status = await git.run(["status", "--porcelain=v2", "-z", "--untracked-files=all"], { cwd: root });
32
- const records = status.split("\0");
33
- const dirty = new Set<string>();
34
- for (let index = 0; index < records.length; index++) {
35
- const record = records[index];
36
- if (!record) continue;
37
- if (record.startsWith("? ")) {
38
- dirty.add(record.slice(2).replaceAll("\\", "/"));
39
- continue;
40
- }
41
- const fields = record.split(" ");
42
- const state = fields[1] ?? "..";
43
- if (record.startsWith("1 ") && !state.includes("D")) {
44
- dirty.add(fields.slice(8).join(" ").replaceAll("\\", "/"));
45
- } else if (record.startsWith("2 ")) {
46
- if (!state.includes("D")) dirty.add(fields.slice(9).join(" ").replaceAll("\\", "/"));
47
- index += 1;
48
- } else if (record.startsWith("u ")) {
49
- dirty.add(fields.slice(10).join(" ").replaceAll("\\", "/"));
50
- }
51
- }
52
- const uncovered = [...dirty]
53
- .filter(
54
- (path) => !isSensitiveContextPath(path) && isContextEligiblePath(path, ignoreGlobs) && !memberships.has(path),
55
- )
42
+ const uncovered = (await listEligibleDirtyPaths(git, root, ignoreGlobs))
43
+ .filter((path) => !memberships.has(path))
56
44
  .sort((left, right) => left.localeCompare(right));
57
45
  return { stale: stale.sort((left, right) => left.path.localeCompare(right.path)), uncovered };
58
46
  }
@@ -74,3 +62,27 @@ export function formatContextValidationFailure(result: ContextValidationResult):
74
62
  );
75
63
  return output.join("\n");
76
64
  }
65
+
66
+ function parseDirtyPaths(status: string): Set<string> {
67
+ const records = status.split("\0");
68
+ const dirty = new Set<string>();
69
+ for (let index = 0; index < records.length; index++) {
70
+ const record = records[index];
71
+ if (!record) continue;
72
+ if (record.startsWith("? ")) {
73
+ dirty.add(record.slice(2).replaceAll("\\", "/"));
74
+ continue;
75
+ }
76
+ const fields = record.split(" ");
77
+ const state = fields[1] ?? "..";
78
+ if (record.startsWith("1 ") && !state.includes("D")) {
79
+ dirty.add(fields.slice(8).join(" ").replaceAll("\\", "/"));
80
+ } else if (record.startsWith("2 ")) {
81
+ if (!state.includes("D")) dirty.add(fields.slice(9).join(" ").replaceAll("\\", "/"));
82
+ index += 1;
83
+ } else if (record.startsWith("u ")) {
84
+ dirty.add(fields.slice(10).join(" ").replaceAll("\\", "/"));
85
+ }
86
+ }
87
+ return dirty;
88
+ }
@@ -2,6 +2,6 @@
2
2
 
3
3
  Switches active chat model and thinking level as one effort tier. Available providers come from current logins, and each provider can fall back through its ranked models.
4
4
 
5
- Run `/effort` to choose a tier and provider. `/effort low`, `/effort medium`, and `/effort high` skip the tier prompt. Press `Ctrl+Shift+E` to cycle tiers on current provider.
5
+ Run `/effort` to choose a tier and provider. `/effort quick`, `/effort standard`, and `/effort deep` skip the tier prompt. Press `Ctrl+Shift+E` to cycle tiers on current provider.
6
6
 
7
7
  If selected provider has no usable model for tier, current model stays active. Footer shows an effort label whenever current provider, model, and thinking level match a configured tier; otherwise it shows no label.
@@ -9,7 +9,7 @@ import {
9
9
  } from "../../shared/model-effort.ts";
10
10
  import { EFFORT_STATE_TYPE, effortState, nextEffort } from "./state.ts";
11
11
 
12
- const EFFORTS: readonly ModelEffort[] = ["low", "medium", "high"];
12
+ const EFFORTS: readonly ModelEffort[] = ["quick", "standard", "deep"];
13
13
 
14
14
  export default function effortExtension(pi: ExtensionAPI): void {
15
15
  let activeEffort: ModelEffort | undefined;
@@ -81,7 +81,7 @@ export default function effortExtension(pi: ExtensionAPI): void {
81
81
  }
82
82
  const arg = args.trim().toLowerCase();
83
83
  if (arg && !isModelEffort(arg)) {
84
- ctx.ui.notify("Usage: /effort [low|medium|high]", "error");
84
+ ctx.ui.notify("Usage: /effort [quick|standard|deep]", "error");
85
85
  return;
86
86
  }
87
87
  let effort: ModelEffort | undefined = isModelEffort(arg) ? arg : undefined;
@@ -130,5 +130,5 @@ export default function effortExtension(pi: ExtensionAPI): void {
130
130
  }
131
131
 
132
132
  function isModelEffort(value: string): value is ModelEffort {
133
- return value === "low" || value === "medium" || value === "high";
133
+ return value === "quick" || value === "standard" || value === "deep";
134
134
  }
@@ -1,4 +1,3 @@
1
- import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
1
  import type { ModelEffort } from "../../shared/model-effort.ts";
3
2
 
4
3
  export const EFFORT_STATE_TYPE = "tau.model-effort.state";
@@ -12,22 +11,8 @@ export function effortState(effort: ModelEffort | undefined): EffortStateV1 {
12
11
  return { v: 1, effort: effort ?? null };
13
12
  }
14
13
 
15
- export function replayEffortState(branch: readonly SessionEntry[]): ModelEffort | undefined {
16
- for (let index = branch.length - 1; index >= 0; index -= 1) {
17
- const entry = branch[index];
18
- if (entry?.type !== "custom" || entry.customType !== EFFORT_STATE_TYPE) continue;
19
- const data = entry.data;
20
- if (!data || typeof data !== "object") continue;
21
- const value = data as Record<string, unknown>;
22
- if (value.v !== 1) continue;
23
- if (value.effort === null) return undefined;
24
- if (value.effort === "low" || value.effort === "medium" || value.effort === "high") return value.effort;
25
- }
26
- return undefined;
27
- }
28
-
29
14
  export function nextEffort(effort: ModelEffort | undefined): ModelEffort {
30
- if (effort === "low") return "medium";
31
- if (effort === "medium") return "high";
32
- return "low";
15
+ if (effort === "quick") return "standard";
16
+ if (effort === "standard") return "deep";
17
+ return "quick";
33
18
  }
@@ -1,52 +1,11 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import type { ExploreEngine } from "./ast/engine.ts";
3
- import { walkPaths } from "./traverse.ts";
4
2
 
5
- const GUIDANCE_SCAN_MAX_ENTRIES = 4096;
3
+ const GUIDANCE = `## Explore
4
+ Shape-backed languages: \`markdown\`, \`typescript\`, \`tsx\`, \`go\`, \`rust\`, \`c_sharp\`, \`java\`, \`kotlin\`, \`swift\`.
6
5
 
7
- export function registerExploreGuidance(pi: ExtensionAPI, engineFor: (cwd: string) => ExploreEngine): void {
8
- pi.on("before_agent_start", async (event, ctx) => {
9
- try {
10
- const engine = engineFor(ctx.cwd);
11
- const advertised = engine.registry
12
- .registeredLanguages()
13
- .filter((language) => language.capabilities.shape)
14
- .map((language) => language.id);
15
- const advertisedSet = new Set(advertised);
16
- const detected = new Set<string>();
6
+ Structural tools (\`outline\`, \`show\`, \`discover\`, \`ast_search\`, deps/relationships, \`impact\`, \`context\`) apply to those languages. Other files: harness \`read\` / \`grep\` / \`find\` / \`ls\`.
7
+ Full \`read\` of a large registered source returns outline + follow-up hint, not the body — use ranged \`read\` or \`show\`.`;
17
8
 
18
- await walkPaths({
19
- cwd: engine.cwd,
20
- root: ".",
21
- filesOnly: true,
22
- budgets: { maxFiles: GUIDANCE_SCAN_MAX_ENTRIES },
23
- onFile(hit) {
24
- const language = engine.registry.adapterForPath(hit.absolutePath)?.id;
25
- if (language !== undefined && advertisedSet.has(language)) detected.add(language);
26
- return detected.size < advertisedSet.size;
27
- },
28
- });
29
-
30
- const backed = advertised.filter((language) => detected.has(language));
31
- if (backed.length === 0) return undefined;
32
-
33
- const guidance = `## Explore source policy
34
- Structurally backed languages detected in this workspace: ${backed.map((language) => `\`${language}\``).join(", ")}.
35
-
36
- - Use the same workflow for every backed language. Identify the job type before loading deeply: locate, reuse, edit, explain, or debug.
37
- - Ask the smallest sufficient query and stop when it answers the question.
38
- - Prefer \`outline\` for unfamiliar trees and known packages. A large full harness \`read\` returns an outline; use ranged \`read\` or \`show\` for bodies.
39
- - Prefer \`discover\` when reuse is the goal and the path is unknown. Search package/public surfaces first.
40
- - Bind targets with path + name (+ line when needed). Use \`show\` views cheapest to richest: \`signature\`, \`signatureWithDocs\`, \`declaration\`, \`declarationWithImports\`.
41
- - Run \`impact\` before a non-trivial symbol change. Use \`context\` to understand one symbol in one bounded pack.
42
- - After selecting a target, use \`callers\`, \`callees\`, \`references\`, or \`implementations\` for focused relationship questions.
43
- - Use \`ast_search\` for code shapes and harness \`grep\` for literals. Use harness \`read\` for formatting, comments, gaps, and unsupported files.
44
- - List and search paths with harness \`ls\`, \`find\`, and \`grep\`. Edit only with harness patch/edit/write tools.
45
- - Trust structural hits as exact path, line, and source text. Do not ask for scores or engine metadata.`;
46
-
47
- return { systemPrompt: `${event.systemPrompt}\n\n${guidance}` };
48
- } catch {
49
- return undefined;
50
- }
51
- });
9
+ export function registerExploreGuidance(pi: ExtensionAPI): void {
10
+ pi.on("before_agent_start", (event) => ({ systemPrompt: `${event.systemPrompt}\n\n${GUIDANCE}` }));
52
11
  }
@@ -3,50 +3,42 @@ import { resolve } from "node:path";
3
3
  import { onTauEvent } from "../../shared/events.ts";
4
4
  import { createTemporaryOutputStore } from "../../shared/temporary-output-store.ts";
5
5
  import { createToolRowStateStore } from "../../shared/tool-row-state.ts";
6
- import { createExploreEngine, type ExploreEngine } from "./ast/engine.ts";
7
- import { createFileGraph, type ExploreFileGraph } from "./ast/graph/file-graph.ts";
8
- import { registerReadOutlineHook } from "./ast/read/hook.ts";
9
- import { createAstSearchTool } from "./ast/tools/ast-search.ts";
10
- import { createContextTool } from "./ast/tools/context.ts";
11
- import { createDepsTool } from "./ast/tools/deps.ts";
12
- import { createDiscoverTool } from "./ast/tools/discover.ts";
13
- import { createImpactTool } from "./ast/tools/impact.ts";
14
- import { createOutlineTool } from "./ast/tools/outline.ts";
6
+ import type { ExploreEngine } from "../../src/ast/engine.ts";
7
+ import type { ExploreFileGraph } from "../../src/ast/graph/file-graph.ts";
8
+ import {
9
+ astEngineFor,
10
+ astGraphFor,
11
+ clearAstSession,
12
+ invalidateAstPaths,
13
+ restartAstSession,
14
+ shutdownAstSession,
15
+ } from "../../src/ast/session.ts";
16
+ import { registerFileInjection } from "../../src/file-injection/index.ts";
17
+ import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
18
+ import { registerReadOutlineHook } from "./read/hook.ts";
19
+ import { createAstSearchTool } from "./tools/ast-search.ts";
20
+ import { createContextTool } from "./tools/context.ts";
21
+ import { createDepsTool } from "./tools/deps.ts";
22
+ import { createDiscoverTool } from "./tools/discover.ts";
23
+ import { createImpactTool } from "./tools/impact.ts";
24
+ import { createOutlineTool } from "./tools/outline.ts";
15
25
  import {
16
26
  createCalleesTool,
17
27
  createCallersTool,
18
28
  createImplementationsTool,
19
29
  createReferencesTool,
20
- } from "./ast/tools/relationships.ts";
21
- import { createReverseDepsTool } from "./ast/tools/reverse-deps.ts";
22
- import { createShowTool } from "./ast/tools/show.ts";
30
+ } from "./tools/relationships.ts";
31
+ import { createReverseDepsTool } from "./tools/reverse-deps.ts";
32
+ import { createShowTool } from "./tools/show.ts";
23
33
  import { registerExploreGuidance } from "./guidance.ts";
24
- import { registerExploreOutlineInjection } from "./outline-injection.ts";
25
- import { registerExploreAutoread } from "./read/autoread.ts";
34
+ import exploreSettings from "./settings.ts";
26
35
 
27
36
  export default function exploreExtension(pi: ExtensionAPI): void {
28
37
  const rowState = createToolRowStateStore(pi, "explore.tool-row-state");
29
38
  const temporaryOutput = createTemporaryOutputStore();
30
- let engine: ExploreEngine | undefined;
31
- let graph: ExploreFileGraph | undefined;
32
-
33
- const engineFor = (cwd: string): ExploreEngine => {
34
- const absoluteCwd = resolve(cwd);
35
- if (engine !== undefined && engine.cwd === absoluteCwd) return engine;
36
- engine?.shutdown();
37
- graph?.clear();
38
- engine = createExploreEngine({ cwd: absoluteCwd });
39
- graph = createFileGraph(engine);
40
- return engine;
41
- };
42
-
43
- const graphFor = (cwd: string): ExploreFileGraph => {
44
- engineFor(cwd);
45
- if (graph === undefined) {
46
- throw new Error("Explore file graph failed to initialize");
47
- }
48
- return graph;
49
- };
39
+ let readSettings = exploreSettings.defaults.read;
40
+ const engineFor = (cwd: string): ExploreEngine => astEngineFor(cwd);
41
+ const graphFor = (cwd: string): ExploreFileGraph => astGraphFor(cwd);
50
42
 
51
43
  pi.registerTool(createOutlineTool(rowState, temporaryOutput, engineFor));
52
44
  pi.registerTool(createShowTool(rowState, engineFor));
@@ -61,45 +53,38 @@ export default function exploreExtension(pi: ExtensionAPI): void {
61
53
  pi.registerTool(createImpactTool(rowState, temporaryOutput, engineFor, graphFor));
62
54
  pi.registerTool(createContextTool(rowState, temporaryOutput, engineFor, graphFor));
63
55
  registerReadOutlineHook(pi, engineFor);
64
- registerExploreAutoread(pi, rowState, engineFor);
65
- registerExploreOutlineInjection(pi, rowState, temporaryOutput, engineFor);
66
- registerExploreGuidance(pi, engineFor);
56
+ registerFileInjection(pi, rowState, {
57
+ temporaryOutput,
58
+ autoOutline: () => ({ enabled: readSettings.enabled, thresholdLines: readSettings.structureThresholdLines }),
59
+ });
60
+ registerExploreGuidance(pi);
67
61
 
68
62
  pi.on("session_start", async (_event, ctx) => {
63
+ readSettings = (await loadTauExtensionSettings(ctx, exploreSettings)).read;
69
64
  await temporaryOutput.shutdown();
70
65
  await temporaryOutput.start();
71
66
  rowState.clear();
72
- engine?.shutdown();
73
- graph?.clear();
74
- engine = createExploreEngine({ cwd: ctx.cwd });
75
- graph = createFileGraph(engine);
67
+ restartAstSession(ctx.cwd);
76
68
  });
77
69
 
78
70
  onTauEvent(pi, "explore.ast", "tau:file-mutation.applied", (event) => {
79
- if (engine === undefined) return;
80
71
  const paths = event.changes.flatMap((change) => {
81
72
  const main = resolve(event.cwd, change.path);
82
73
  if (change.move === undefined) return [main];
83
74
  return [main, resolve(event.cwd, change.move.from), resolve(event.cwd, change.move.to)];
84
75
  });
85
- engine.invalidate(paths);
86
- const topologyChanged = event.changes.some(
87
- (change) => change.kind === "add" || change.kind === "delete" || change.move !== undefined,
76
+ invalidateAstPaths(
77
+ paths,
78
+ event.changes.some((change) => change.kind === "add" || change.kind === "delete" || change.move !== undefined),
88
79
  );
89
- if (topologyChanged) graph?.clear();
90
- else graph?.invalidate(paths);
91
80
  });
92
81
 
93
82
  pi.on("session_tree", () => {
94
- engine?.clear();
95
- graph?.clear();
83
+ clearAstSession();
96
84
  });
97
85
 
98
86
  pi.on("session_shutdown", async () => {
99
- engine?.shutdown();
100
- engine = undefined;
101
- graph?.clear();
102
- graph = undefined;
87
+ shutdownAstSession();
103
88
  await temporaryOutput.shutdown();
104
89
  });
105
90
  }