@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
@@ -156,111 +156,144 @@ function parseChunkLine(rawLine: string): { prefix: " " | "+" | "-"; text: strin
156
156
  return { prefix, text: rawLine.slice(1) };
157
157
  }
158
158
 
159
- function parseUpdateBody(
160
- lines: string[],
161
- startIndex: number,
162
- operationPath: string,
163
- ): { movePath?: string; chunks: UpdateFileChunk[]; linesAdded: number; linesRemoved: number; nextIndex: number } {
164
- const chunks: UpdateFileChunk[] = [];
165
- let index = startIndex;
166
- let movePath: string | undefined;
167
- let current: MutableChunk | undefined;
168
- let sawAnyChunk = false;
169
- let linesAdded = 0;
170
- let linesRemoved = 0;
159
+ interface UpdateBodyState {
160
+ chunks: UpdateFileChunk[];
161
+ index: number;
162
+ movePath?: string;
163
+ current?: MutableChunk;
164
+ sawAnyChunk: boolean;
165
+ linesAdded: number;
166
+ linesRemoved: number;
167
+ }
171
168
 
172
- while (index < lines.length) {
173
- const line = lines[index];
174
- if (line === undefined) break;
175
- const currentDirective = updateDirective(line);
176
- if (isUpdateBoundary(line)) break;
169
+ function changeContextFromDirective(directive: string): string | undefined {
170
+ if (directive === CHANGE_CONTEXT_MARKER) return undefined;
171
+ const context = directive.slice(CHANGE_CONTEXT_MARKER.length + 1).trim();
172
+ return context.length > 0 ? context : undefined;
173
+ }
177
174
 
178
- if (currentDirective.startsWith(MOVE_TO_MARKER)) {
179
- if (sawAnyChunk || current) throw new Error(`Move to must appear before update chunks: ${operationPath}`);
180
- movePath = requireUpdatePath(line, MOVE_TO_MARKER);
181
- index += 1;
182
- continue;
183
- }
175
+ function openOrReuseChunk(state: UpdateBodyState, operationPath: string): MutableChunk {
176
+ if (state.current) return state.current;
177
+ if (state.sawAnyChunk || state.chunks.length > 0) {
178
+ throw new Error(`Only the first update chunk may omit @@: ${operationPath}`);
179
+ }
180
+ state.current = createChunk();
181
+ state.sawAnyChunk = true;
182
+ return state.current;
183
+ }
184
184
 
185
- if (currentDirective === CHANGE_CONTEXT_MARKER || currentDirective.startsWith(`${CHANGE_CONTEXT_MARKER} `)) {
186
- finalizeChunk(current, chunks, operationPath);
187
- const context =
188
- currentDirective === CHANGE_CONTEXT_MARKER
189
- ? undefined
190
- : currentDirective.slice(CHANGE_CONTEXT_MARKER.length + 1).trim();
191
- current = createChunk(context && context.length > 0 ? context : undefined);
192
- sawAnyChunk = true;
193
- index += 1;
194
- continue;
195
- }
185
+ function appendHunkLine(state: UpdateBodyState, current: MutableChunk, line: string): void {
186
+ const parsedLine = parseChunkLine(line);
187
+ current.hasLines = true;
188
+ if (parsedLine.prefix === "+") state.linesAdded += 1;
189
+ if (parsedLine.prefix === "-") state.linesRemoved += 1;
190
+ current.lines.push({ prefix: parsedLine.prefix, text: parsedLine.text });
191
+ state.index += 1;
192
+ }
196
193
 
197
- if (!current) {
198
- if (sawAnyChunk || chunks.length > 0) {
199
- throw new Error(`Only the first update chunk may omit @@: ${operationPath}`);
200
- }
201
- current = createChunk();
202
- sawAnyChunk = true;
203
- }
194
+ function consumeUpdateLine(state: UpdateBodyState, line: string, operationPath: string): boolean {
195
+ const directive = updateDirective(line);
196
+ if (isUpdateBoundary(line)) return false;
204
197
 
205
- if (currentDirective === EOF_MARKER) {
206
- current.isEndOfFile = true;
207
- index += 1;
208
- continue;
198
+ if (directive.startsWith(MOVE_TO_MARKER)) {
199
+ if (state.sawAnyChunk || state.current) {
200
+ throw new Error(`Move to must appear before update chunks: ${operationPath}`);
209
201
  }
202
+ state.movePath = requireUpdatePath(line, MOVE_TO_MARKER);
203
+ state.index += 1;
204
+ return true;
205
+ }
210
206
 
211
- const parsedLine = parseChunkLine(line);
212
- current.hasLines = true;
207
+ if (directive === CHANGE_CONTEXT_MARKER || directive.startsWith(`${CHANGE_CONTEXT_MARKER} `)) {
208
+ finalizeChunk(state.current, state.chunks, operationPath);
209
+ state.current = createChunk(changeContextFromDirective(directive));
210
+ state.sawAnyChunk = true;
211
+ state.index += 1;
212
+ return true;
213
+ }
213
214
 
214
- if (parsedLine.prefix === "+") linesAdded += 1;
215
- if (parsedLine.prefix === "-") linesRemoved += 1;
215
+ const current = openOrReuseChunk(state, operationPath);
216
+ if (directive === EOF_MARKER) {
217
+ current.isEndOfFile = true;
218
+ state.index += 1;
219
+ return true;
220
+ }
216
221
 
217
- current.lines.push({ prefix: parsedLine.prefix, text: parsedLine.text });
218
- index += 1;
222
+ appendHunkLine(state, current, line);
223
+ return true;
224
+ }
225
+
226
+ function parseUpdateBody(
227
+ lines: string[],
228
+ startIndex: number,
229
+ operationPath: string,
230
+ ): { movePath?: string; chunks: UpdateFileChunk[]; linesAdded: number; linesRemoved: number; nextIndex: number } {
231
+ const state: UpdateBodyState = {
232
+ chunks: [],
233
+ index: startIndex,
234
+ sawAnyChunk: false,
235
+ linesAdded: 0,
236
+ linesRemoved: 0,
237
+ };
238
+
239
+ while (state.index < lines.length) {
240
+ const line = lines[state.index];
241
+ if (line === undefined || !consumeUpdateLine(state, line, operationPath)) break;
219
242
  }
220
243
 
221
- finalizeChunk(current, chunks, operationPath);
222
- if (chunks.length === 0 && !movePath)
244
+ finalizeChunk(state.current, state.chunks, operationPath);
245
+ if (state.chunks.length === 0 && !state.movePath) {
223
246
  throw new Error(`Update file patch is missing chunk content: ${operationPath}`);
247
+ }
248
+
249
+ return {
250
+ movePath: state.movePath,
251
+ chunks: state.chunks,
252
+ linesAdded: state.linesAdded,
253
+ linesRemoved: state.linesRemoved,
254
+ nextIndex: state.index,
255
+ };
256
+ }
257
+
258
+ function parseWholeFileSection(header: string, sectionLines: string[], isReplace: boolean): ParsedSection {
259
+ const prefix = isReplace ? REPLACE_FILE_MARKER : ADD_FILE_MARKER;
260
+ const path = requireTopLevelPath(header, prefix);
261
+ const body = parseAddBody(sectionLines, 1);
262
+ if (body.nextIndex !== sectionLines.length) {
263
+ throw new Error(`Malformed ${isReplace ? "Replace" : "Add"} File section: ${path}`);
264
+ }
265
+ return { type: isReplace ? "replace" : "add", path, content: body.content, linesAdded: body.lineCount };
266
+ }
267
+
268
+ function parseDeleteSection(header: string, sectionLines: string[]): ParsedSection {
269
+ const path = requireTopLevelPath(header, DELETE_FILE_MARKER);
270
+ if (sectionLines.length !== 1) throw new Error(`Malformed Delete File section: ${path}`);
271
+ return { type: "delete", path };
272
+ }
224
273
 
225
- return { movePath, chunks, linesAdded, linesRemoved, nextIndex: index };
274
+ function parseUpdateSection(header: string, sectionLines: string[]): ParsedSection {
275
+ const path = requireTopLevelPath(header, UPDATE_FILE_MARKER);
276
+ const updateBody = parseUpdateBody(sectionLines, 1, path);
277
+ if (updateBody.nextIndex !== sectionLines.length) throw new Error(`Malformed Update File section: ${path}`);
278
+ return {
279
+ type: "update",
280
+ path,
281
+ movePath: updateBody.movePath,
282
+ chunks: updateBody.chunks,
283
+ linesAdded: updateBody.linesAdded,
284
+ linesRemoved: updateBody.linesRemoved,
285
+ };
226
286
  }
227
287
 
228
288
  function parseSection(sectionLines: string[]): ParsedSection {
229
289
  const header = sectionLines[0] ?? "";
230
-
231
290
  const headerDirective = topLevelDirective(header);
232
291
 
233
292
  if (headerDirective.startsWith(ADD_FILE_MARKER) || headerDirective.startsWith(REPLACE_FILE_MARKER)) {
234
- const isReplace = headerDirective.startsWith(REPLACE_FILE_MARKER);
235
- const prefix = isReplace ? REPLACE_FILE_MARKER : ADD_FILE_MARKER;
236
- const path = requireTopLevelPath(header, prefix);
237
- const body = parseAddBody(sectionLines, 1);
238
- if (body.nextIndex !== sectionLines.length) {
239
- throw new Error(`Malformed ${isReplace ? "Replace" : "Add"} File section: ${path}`);
240
- }
241
- return { type: isReplace ? "replace" : "add", path, content: body.content, linesAdded: body.lineCount };
242
- }
243
-
244
- if (headerDirective.startsWith(DELETE_FILE_MARKER)) {
245
- const path = requireTopLevelPath(header, DELETE_FILE_MARKER);
246
- if (sectionLines.length !== 1) throw new Error(`Malformed Delete File section: ${path}`);
247
- return { type: "delete", path };
248
- }
249
-
250
- if (headerDirective.startsWith(UPDATE_FILE_MARKER)) {
251
- const path = requireTopLevelPath(header, UPDATE_FILE_MARKER);
252
- const updateBody = parseUpdateBody(sectionLines, 1, path);
253
- if (updateBody.nextIndex !== sectionLines.length) throw new Error(`Malformed Update File section: ${path}`);
254
- return {
255
- type: "update",
256
- path,
257
- movePath: updateBody.movePath,
258
- chunks: updateBody.chunks,
259
- linesAdded: updateBody.linesAdded,
260
- linesRemoved: updateBody.linesRemoved,
261
- };
293
+ return parseWholeFileSection(header, sectionLines, headerDirective.startsWith(REPLACE_FILE_MARKER));
262
294
  }
263
-
295
+ if (headerDirective.startsWith(DELETE_FILE_MARKER)) return parseDeleteSection(header, sectionLines);
296
+ if (headerDirective.startsWith(UPDATE_FILE_MARKER)) return parseUpdateSection(header, sectionLines);
264
297
  throw new Error(`Unexpected patch line: ${header}`);
265
298
  }
266
299
 
@@ -285,15 +318,41 @@ function envelopeFailure(message: string): ParsedPatch {
285
318
  return { operations: [], parseFailures: [{ phase: "parse", sectionIndex: 0, message }], totalSections: 1 };
286
319
  }
287
320
 
288
- export function parsePatch(input: string): ParsedPatch {
321
+ function parseEnvelopeLines(input: string): string[] | ParsedPatch {
289
322
  const normalized = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
290
323
  if (!normalized) return envelopeFailure("Patch must not be empty.");
291
-
292
324
  const lines = normalized.split("\n");
293
- if (topLevelDirective(lines[0] ?? "") !== BEGIN_PATCH_MARKER)
325
+ if (topLevelDirective(lines[0] ?? "") !== BEGIN_PATCH_MARKER) {
294
326
  return envelopeFailure("Patch must start with *** Begin Patch.");
295
- if (topLevelDirective(lines[lines.length - 1] ?? "") !== END_PATCH_MARKER)
327
+ }
328
+ if (topLevelDirective(lines[lines.length - 1] ?? "") !== END_PATCH_MARKER) {
296
329
  return envelopeFailure("Patch must end with *** End Patch.");
330
+ }
331
+ return lines;
332
+ }
333
+
334
+ function pushParsedSection(
335
+ operations: PatchOperation[],
336
+ parseFailures: PatchFailure[],
337
+ sectionIndex: number,
338
+ sectionLines: string[],
339
+ ): void {
340
+ try {
341
+ operations.push({ ...parseSection(sectionLines), sectionIndex });
342
+ } catch (error) {
343
+ parseFailures.push({
344
+ phase: "parse",
345
+ sectionIndex,
346
+ ...parseHeaderMetadata(sectionLines[0] ?? ""),
347
+ message: error instanceof Error ? error.message : String(error),
348
+ });
349
+ }
350
+ }
351
+
352
+ export function parsePatch(input: string): ParsedPatch {
353
+ const envelope = parseEnvelopeLines(input);
354
+ if (!Array.isArray(envelope)) return envelope;
355
+ const lines = envelope;
297
356
 
298
357
  const operations: PatchOperation[] = [];
299
358
  const parseFailures: PatchFailure[] = [];
@@ -308,30 +367,15 @@ export function parsePatch(input: string): ParsedPatch {
308
367
  continue;
309
368
  }
310
369
 
311
- const lineDirective = topLevelDirective(line);
312
- if (!isTopLevelBoundary(line) || lineDirective === END_PATCH_MARKER) {
370
+ if (!isTopLevelBoundary(line) || topLevelDirective(line) === END_PATCH_MARKER) {
313
371
  parseFailures.push({ phase: "parse", sectionIndex, message: `Unexpected patch line: ${line}` });
314
372
  index += 1;
315
373
  continue;
316
374
  }
317
375
 
318
376
  sectionIndex += 1;
319
- const sectionStart = index;
320
377
  const nextBoundary = nextSectionBoundary(lines, index + 1, isUpdateHeader(line));
321
-
322
- const sectionLines = lines.slice(sectionStart, nextBoundary);
323
- try {
324
- operations.push({ ...parseSection(sectionLines), sectionIndex });
325
- } catch (error) {
326
- const message = error instanceof Error ? error.message : String(error);
327
- parseFailures.push({
328
- phase: "parse",
329
- sectionIndex,
330
- ...parseHeaderMetadata(sectionLines[0] ?? ""),
331
- message,
332
- });
333
- }
334
-
378
+ pushParsedSection(operations, parseFailures, sectionIndex, lines.slice(index, nextBoundary));
335
379
  index = nextBoundary;
336
380
  }
337
381
 
@@ -44,6 +44,34 @@ function startPreviewOp(ops: PreviewOp[], kind: PreviewOp["kind"], path: string)
44
44
  return op;
45
45
  }
46
46
 
47
+ function matchPreviewSectionStart(ops: PreviewOp[], value: string): PreviewOp | undefined {
48
+ const addPath = pathAfter(value, ADD_FILE_MARKER);
49
+ if (addPath !== undefined) return startPreviewOp(ops, "add", addPath);
50
+ const replacePath = pathAfter(value, REPLACE_FILE_MARKER);
51
+ if (replacePath !== undefined) return startPreviewOp(ops, "replace", replacePath);
52
+ const deletePath = pathAfter(value, DELETE_FILE_MARKER);
53
+ if (deletePath !== undefined) return startPreviewOp(ops, "delete", deletePath);
54
+ const updatePath = pathAfter(value, UPDATE_FILE_MARKER);
55
+ if (updatePath !== undefined) return startPreviewOp(ops, "update", updatePath);
56
+ return undefined;
57
+ }
58
+
59
+ function accumulatePreviewBodyLine(current: PreviewOp, line: string): void {
60
+ if (current.kind === "update") {
61
+ const movePath = pathAfter(updateDirective(line), MOVE_TO_MARKER);
62
+ if (movePath !== undefined) {
63
+ current.moveTo = movePath;
64
+ return;
65
+ }
66
+ if (line.startsWith("+")) current.linesAdded += 1;
67
+ else if (line.startsWith("-")) current.linesRemoved += 1;
68
+ return;
69
+ }
70
+ if ((current.kind === "add" || current.kind === "replace") && line.startsWith("+")) {
71
+ current.linesAdded += 1;
72
+ }
73
+ }
74
+
47
75
  // Tolerant preview scan — never throws, display-oriented (partial patches during streaming).
48
76
  function scanPreview(input: string | undefined): PreviewOp[] {
49
77
  if (typeof input !== "string" || !input) return [];
@@ -54,47 +82,12 @@ function scanPreview(input: string | undefined): PreviewOp[] {
54
82
  for (const line of lines) {
55
83
  const value = current?.kind === "update" ? updateDirective(line) : topLevelDirective(line);
56
84
  if (value === END_PATCH_MARKER) break;
57
-
58
- const addPath = pathAfter(value, ADD_FILE_MARKER);
59
- if (addPath !== undefined) {
60
- current = startPreviewOp(ops, "add", addPath);
61
- continue;
62
- }
63
-
64
- const replacePath = pathAfter(value, REPLACE_FILE_MARKER);
65
- if (replacePath !== undefined) {
66
- current = startPreviewOp(ops, "replace", replacePath);
85
+ const started = matchPreviewSectionStart(ops, value);
86
+ if (started) {
87
+ current = started;
67
88
  continue;
68
89
  }
69
-
70
- const deletePath = pathAfter(value, DELETE_FILE_MARKER);
71
- if (deletePath !== undefined) {
72
- current = startPreviewOp(ops, "delete", deletePath);
73
- continue;
74
- }
75
-
76
- const updatePath = pathAfter(value, UPDATE_FILE_MARKER);
77
- if (updatePath !== undefined) {
78
- current = startPreviewOp(ops, "update", updatePath);
79
- continue;
80
- }
81
-
82
- if (!current) continue;
83
-
84
- if (current.kind === "update") {
85
- const movePath = pathAfter(updateDirective(line), MOVE_TO_MARKER);
86
- if (movePath !== undefined) {
87
- current.moveTo = movePath;
88
- continue;
89
- }
90
- if (line.startsWith("+")) current.linesAdded += 1;
91
- else if (line.startsWith("-")) current.linesRemoved += 1;
92
- continue;
93
- }
94
-
95
- if (current.kind === "add" || current.kind === "replace") {
96
- if (line.startsWith("+")) current.linesAdded += 1;
97
- }
90
+ if (current) accumulatePreviewBodyLine(current, line);
98
91
  }
99
92
 
100
93
  return ops;
@@ -1,35 +1,39 @@
1
- import { type ApplyPatchSummary, deriveStats } from "./executor.ts";
1
+ import { type ApplyPatchStats, type ApplyPatchSummary, deriveStats } from "./executor.ts";
2
+ import type { PatchFailure } from "./parser.ts";
2
3
 
3
- export function formatPatchSummary(summary: ApplyPatchSummary): string {
4
- const s = deriveStats(summary);
4
+ function formatStatusLine(summary: ApplyPatchSummary, stats: ApplyPatchStats): string {
5
+ if (summary.status === "failed") return "No changes applied.";
6
+ const parts: string[] = [];
7
+ if (stats.linesAdded > 0) parts.push(`+${stats.linesAdded}`);
8
+ if (stats.linesRemoved > 0) parts.push(`-${stats.linesRemoved}`);
9
+ const badge = parts.length > 0 ? ` [${parts.join(" ")}]` : "";
10
+ return `Applied ${stats.completedOperations}/${summary.totalSections} sections.${badge}`;
11
+ }
12
+
13
+ function formatChangeLines(stats: ApplyPatchStats): string[] {
5
14
  const lines: string[] = [];
6
- if (summary.status === "failed") {
7
- lines.push("No changes applied.");
8
- } else {
9
- const parts: string[] = [];
10
- if (s.linesAdded > 0) parts.push(`+${s.linesAdded}`);
11
- if (s.linesRemoved > 0) parts.push(`-${s.linesRemoved}`);
12
- const badge = parts.length > 0 ? ` [${parts.join(" ")}]` : "";
13
- lines.push(`Applied ${s.completedOperations}/${summary.totalSections} sections.${badge}`);
14
- }
15
+ for (const path of stats.added) lines.push(`A ${path}`);
16
+ for (const path of stats.replaced) lines.push(`M ${path}`);
17
+ for (const path of stats.updated) lines.push(`M ${path}`);
18
+ for (const path of stats.deleted) lines.push(`D ${path}`);
19
+ for (const move of stats.moved) lines.push(`R ${move.from} -> ${move.to}`);
20
+ return lines;
21
+ }
15
22
 
16
- for (const path of s.added) lines.push(`A ${path}`);
17
- for (const path of s.replaced) lines.push(`M ${path}`);
18
- for (const path of s.updated) lines.push(`M ${path}`);
19
- for (const path of s.deleted) lines.push(`D ${path}`);
20
- for (const move of s.moved) lines.push(`R ${move.from} -> ${move.to}`);
23
+ function formatFailureLine(failure: PatchFailure): string {
24
+ const kind = failure.kind ? `${failure.kind} ` : "";
25
+ const path = failure.path ?? "";
26
+ const chunk = failure.chunkIndex && failure.totalChunks ? ` chunk ${failure.chunkIndex}/${failure.totalChunks}` : "";
27
+ const ctx = failure.contextHint ? ` (context: "${failure.contextHint}")` : "";
28
+ return `- ${kind}${path}${chunk}: ${failure.message}${ctx}`.trim();
29
+ }
21
30
 
31
+ export function formatPatchSummary(summary: ApplyPatchSummary): string {
32
+ const stats = deriveStats(summary);
33
+ const lines = [formatStatusLine(summary, stats), ...formatChangeLines(stats)];
22
34
  if (summary.failures.length > 0) {
23
35
  lines.push("Failures:");
24
- for (const failure of summary.failures) {
25
- const kind = failure.kind ? `${failure.kind} ` : "";
26
- const path = failure.path ?? "";
27
- const chunk =
28
- failure.chunkIndex && failure.totalChunks ? ` chunk ${failure.chunkIndex}/${failure.totalChunks}` : "";
29
- const ctx = failure.contextHint ? ` (context: "${failure.contextHint}")` : "";
30
- lines.push(`- ${kind}${path}${chunk}: ${failure.message}${ctx}`.trim());
31
- }
36
+ for (const failure of summary.failures) lines.push(formatFailureLine(failure));
32
37
  }
33
-
34
38
  return lines.join("\n");
35
39
  }
@@ -0,0 +1,7 @@
1
+ # Ready
2
+
3
+ Adds `/ready` to scan the repository for agent-readiness rails and write one report file.
4
+
5
+ You pick Markdown or HTML. Tau runs a deterministic scan (no model), builds the report in memory, and writes a timestamped file under `.pi/tau/ready/` using your local timezone in the filename. A notification shows the path and status counts.
6
+
7
+ The scan looks for cold-start docs, toolchain pins, task/verify entrypoints, silent command runner config, policy and vocabulary files, standards and context catalogs, and language-specific format/lint/types/entropy tools (TypeScript, Deno, Go, Rust packs in v1). Statuses are pass, weak, missing, n/a, or unknown — no scores.
@@ -0,0 +1,58 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join, relative } from "node:path";
3
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
4
+ import { resolveProjectRoot } from "../../shared/settings/paths.ts";
5
+ import { errorText } from "../../shared/text.ts";
6
+ import { type ReadyFormat, localTimestampSlug, summarizeCounts } from "./model.ts";
7
+ import { readyFileExtension, renderReadyReport } from "./render.ts";
8
+ import { scanReadyReport } from "./scan.ts";
9
+
10
+ const OUTPUT_DIR = join(".pi", "tau", "ready");
11
+
12
+ export default function readyExtension(pi: ExtensionAPI): void {
13
+ pi.registerCommand("ready", {
14
+ description: "Scan agent readiness and write a markdown or HTML report",
15
+ handler: async (_args, ctx) => {
16
+ try {
17
+ await runReady(ctx);
18
+ } catch (error) {
19
+ ctx.ui.notify(`/ready failed: ${errorText(error)}`, "error");
20
+ }
21
+ },
22
+ });
23
+ }
24
+
25
+ async function runReady(ctx: ExtensionCommandContext): Promise<void> {
26
+ if (!ctx.isProjectTrusted()) {
27
+ ctx.ui.notify("/ready requires a trusted project", "warning");
28
+ return;
29
+ }
30
+
31
+ const format = await pickFormat(ctx);
32
+ if (!format) return;
33
+
34
+ const root = await resolveProjectRoot(ctx.cwd);
35
+ const report = await scanReadyReport(ctx.cwd);
36
+ const body = renderReadyReport(report, format);
37
+ const stamp = localTimestampSlug(new Date(report.generatedAt));
38
+ const fileName = `ready-${stamp}.${readyFileExtension(format)}`;
39
+ const outDir = join(root, OUTPUT_DIR);
40
+ const outPath = join(outDir, fileName);
41
+
42
+ await mkdir(outDir, { recursive: true });
43
+ await writeFile(outPath, body, "utf8");
44
+
45
+ const displayPath = relative(ctx.cwd, outPath) || outPath;
46
+ ctx.ui.notify(`/ready ${summarizeCounts(report.counts)}\n${displayPath}`, "info");
47
+ }
48
+
49
+ async function pickFormat(ctx: ExtensionCommandContext): Promise<ReadyFormat | undefined> {
50
+ if (ctx.mode !== "tui") {
51
+ ctx.ui.notify("/ready needs the TUI to choose Markdown or HTML", "warning");
52
+ return undefined;
53
+ }
54
+ const selected = await ctx.ui.select("Ready report format", ["Markdown", "HTML"]);
55
+ if (selected === "Markdown") return "markdown";
56
+ if (selected === "HTML") return "html";
57
+ return undefined;
58
+ }
@@ -0,0 +1,128 @@
1
+ export type ReadyStatus = "pass" | "weak" | "missing" | "na" | "unknown";
2
+ export type ReadyHow = "scan" | "judgment" | "mixed";
3
+ export type ReadyFormat = "markdown" | "html";
4
+
5
+ export type ReadyAreaId =
6
+ | "cold-start"
7
+ | "toolchain"
8
+ | "verify"
9
+ | "lint-entropy"
10
+ | "policy"
11
+ | "standards"
12
+ | "context"
13
+ | "reuse"
14
+ | "markers"
15
+ | "harness"
16
+ | "side-effect-verbs";
17
+
18
+ export interface ReadyRow {
19
+ id: string;
20
+ area: ReadyAreaId;
21
+ title: string;
22
+ status: ReadyStatus;
23
+ how: ReadyHow;
24
+ evidence: string[];
25
+ note: string;
26
+ next?: string;
27
+ }
28
+
29
+ export interface ReadyReport {
30
+ generatedAt: string;
31
+ generatedAtLabel: string;
32
+ root: string;
33
+ languages: string[];
34
+ rows: ReadyRow[];
35
+ counts: Record<ReadyStatus, number>;
36
+ }
37
+
38
+ export const AREA_LABELS: Record<ReadyAreaId, string> = {
39
+ "cold-start": "Cold start",
40
+ toolchain: "Toolchain",
41
+ verify: "Verify",
42
+ "lint-entropy": "Lint and entropy",
43
+ policy: "Policy",
44
+ standards: "Standards",
45
+ context: "Context",
46
+ reuse: "Reuse",
47
+ markers: "Markers",
48
+ harness: "Harness gates",
49
+ "side-effect-verbs": "Side-effect verbs",
50
+ };
51
+
52
+ export const AREA_ORDER: ReadyAreaId[] = [
53
+ "cold-start",
54
+ "toolchain",
55
+ "verify",
56
+ "lint-entropy",
57
+ "policy",
58
+ "standards",
59
+ "context",
60
+ "reuse",
61
+ "markers",
62
+ "harness",
63
+ "side-effect-verbs",
64
+ ];
65
+
66
+ export function countStatuses(rows: readonly ReadyRow[]): Record<ReadyStatus, number> {
67
+ const counts: Record<ReadyStatus, number> = {
68
+ pass: 0,
69
+ weak: 0,
70
+ missing: 0,
71
+ na: 0,
72
+ unknown: 0,
73
+ };
74
+ for (const row of rows) counts[row.status] += 1;
75
+ return counts;
76
+ }
77
+
78
+ export function summarizeCounts(counts: Record<ReadyStatus, number>): string {
79
+ const parts: string[] = [];
80
+ for (const status of ["pass", "weak", "missing", "na", "unknown"] as const) {
81
+ if (counts[status] > 0) parts.push(`${counts[status]} ${status}`);
82
+ }
83
+ return parts.join(" · ") || "no rows";
84
+ }
85
+
86
+ /** Local-timezone stamp for filenames: 2026-04-08-143052 */
87
+ export function localTimestampSlug(date = new Date()): string {
88
+ const pad = (value: number) => String(value).padStart(2, "0");
89
+ return [
90
+ date.getFullYear(),
91
+ pad(date.getMonth() + 1),
92
+ pad(date.getDate()),
93
+ "-",
94
+ pad(date.getHours()),
95
+ pad(date.getMinutes()),
96
+ pad(date.getSeconds()),
97
+ ].join("");
98
+ }
99
+
100
+ export function localTimestampLabel(date = new Date()): string {
101
+ return date.toLocaleString(undefined, {
102
+ year: "numeric",
103
+ month: "short",
104
+ day: "numeric",
105
+ hour: "2-digit",
106
+ minute: "2-digit",
107
+ second: "2-digit",
108
+ timeZoneName: "short",
109
+ });
110
+ }
111
+
112
+ export function row(
113
+ partial: Omit<ReadyRow, "how" | "evidence"> & {
114
+ how?: ReadyHow;
115
+ evidence?: string[];
116
+ },
117
+ ): ReadyRow {
118
+ return {
119
+ how: partial.how ?? "scan",
120
+ evidence: partial.evidence ?? [],
121
+ id: partial.id,
122
+ area: partial.area,
123
+ title: partial.title,
124
+ status: partial.status,
125
+ note: partial.note,
126
+ next: partial.next,
127
+ };
128
+ }