@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
@@ -1,517 +0,0 @@
1
- import { readFile, stat } from "node:fs/promises";
2
- import { dirname, extname, join, relative, resolve, sep } from "node:path";
3
- import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
4
- import { Text } from "@earendil-works/pi-tui";
5
- import { Type } from "typebox";
6
- import { createGitRunner, loadRepoStatus, type GitRunner } from "../../shared/git.ts";
7
- import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
8
- import { truncAt } from "../../shared/text.ts";
9
- import {
10
- contextEntryPaths,
11
- isContextEligiblePath,
12
- isSensitiveContextPath,
13
- loadContextEntries,
14
- type ContextEntry,
15
- } from "./definitions.ts";
16
- import contextSettings from "./settings.ts";
17
- import { formatContextValidationFailure, validateContextCatalog } from "./validation.ts";
18
-
19
- export const CONTEXT_SYNC_EVIDENCE_TOOL = "context_sync_evidence";
20
-
21
- const MAX_DIRTY_EVIDENCE = 4_000;
22
- const MAX_STRUCTURAL_PREVIEW = 1_500;
23
- const MAX_SECTION = 48_000;
24
- const MAX_UNTRACKED_BYTES = 12_000;
25
- const EVIDENCE_CONCURRENCY = 4;
26
-
27
- const evidenceParams = Type.Object(
28
- {
29
- section: Type.Union(
30
- [
31
- Type.Literal("overview"),
32
- Type.Literal("catalog"),
33
- Type.Literal("dirty"),
34
- Type.Literal("file"),
35
- Type.Literal("dependencies"),
36
- Type.Literal("previews"),
37
- Type.Literal("invariants"),
38
- ],
39
- {
40
- description:
41
- "Evidence slice to load. Use overview first, then file/previews/invariants as needed. Call invariants after catalog edits.",
42
- },
43
- ),
44
- path: Type.Optional(
45
- Type.String({
46
- description: "Project-relative path required when section is file.",
47
- }),
48
- ),
49
- },
50
- { additionalProperties: false },
51
- );
52
-
53
- export interface SyncDirtyFile {
54
- id: number;
55
- path: string;
56
- status: string;
57
- kind: "modified" | "added" | "deleted" | "renamed" | "copied" | "untracked";
58
- untracked: boolean;
59
- oldPath?: string;
60
- memberships: string[];
61
- oldMemberships: string[];
62
- evidence: string;
63
- }
64
-
65
- export interface SyncEvidence {
66
- root: string;
67
- ignoreGlobs: readonly string[];
68
- files: SyncDirtyFile[];
69
- entries: ContextEntry[];
70
- dirtyExisting: Set<string>;
71
- dependencies: Set<string>;
72
- affectedIds: Set<string>;
73
- affectedConcepts: Set<string>;
74
- eligibleFiles: Set<string>;
75
- missingPaths: Set<string>;
76
- structuralPreviews: Map<string, string>;
77
- }
78
-
79
- export function registerContextSyncEvidenceTool(pi: ExtensionAPI): void {
80
- pi.registerTool(
81
- defineTool<typeof evidenceParams>({
82
- name: CONTEXT_SYNC_EVIDENCE_TOOL,
83
- label: "context_sync_evidence",
84
- description:
85
- "Load repository and context-catalog evidence for context-sync. Prefer overview, then narrower sections.",
86
- parameters: evidenceParams,
87
- async execute(_id, params, _signal, _onUpdate, ctx) {
88
- const text = await loadEvidenceSection(pi, ctx, params.section, params.path);
89
- return { content: [{ type: "text" as const, text }], details: undefined };
90
- },
91
- renderCall(args, theme, context) {
92
- const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
93
- const suffix = args.path ? ` ${args.path}` : "";
94
- text.setText(theme.fg("toolTitle", `${CONTEXT_SYNC_EVIDENCE_TOOL} ${args.section}${suffix}`));
95
- return text;
96
- },
97
- renderResult(result, options, theme, context) {
98
- const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
99
- const output = result.content.map((part) => (part.type === "text" ? part.text : "")).join("");
100
- text.setText(options.isPartial ? theme.fg("dim", output) : truncAt(output, 400));
101
- return text;
102
- },
103
- }),
104
- );
105
- }
106
-
107
- export function hideContextSyncEvidenceTool(pi: ExtensionAPI): void {
108
- const active = pi.getActiveTools();
109
- if (!active.includes(CONTEXT_SYNC_EVIDENCE_TOOL)) return;
110
- // Keep active in the context-sync child toolset (may include bash for read-only inspection).
111
- const parentMarkers = ["subagent", "edit", "write", "load_tools"];
112
- if (!parentMarkers.some((name) => active.includes(name))) return;
113
- pi.setActiveTools(active.filter((name) => name !== CONTEXT_SYNC_EVIDENCE_TOOL));
114
- }
115
-
116
- export async function collectSyncEvidence(
117
- git: GitRunner,
118
- root: string,
119
- ignoreGlobs: readonly string[],
120
- ): Promise<SyncEvidence> {
121
- const entries = await loadContextEntries(root);
122
- const files = await collectDirtyFiles(git, root, entries, ignoreGlobs);
123
- const dirtyExisting = new Set<string>();
124
- for (const file of files)
125
- if (file.kind !== "deleted" && (await isFile(join(root, file.path)))) dirtyExisting.add(file.path);
126
- const dependencies = await discoverDirectDependencies(
127
- root,
128
- files.filter((file) => dirtyExisting.has(file.path)),
129
- );
130
- for (const path of dependencies) if (!isContextEligiblePath(path, ignoreGlobs)) dependencies.delete(path);
131
- const missingPaths = new Set<string>();
132
- for (const entry of entries)
133
- for (const path of contextEntryPaths(entry))
134
- if (isContextEligiblePath(path, ignoreGlobs) && !(await isFile(join(root, path)))) missingPaths.add(path);
135
- const affectedIds = new Set<string>();
136
- for (const entry of entries) {
137
- const paths = contextEntryPaths(entry);
138
- if (
139
- paths.some((path) => missingPaths.has(path)) ||
140
- paths.some(
141
- (path) => files.some((file) => file.path === path || file.oldPath === path) || dependencies.has(path),
142
- )
143
- )
144
- affectedIds.add(entry.id);
145
- }
146
- const affectedConcepts = new Set([...affectedIds].map((id) => id.split("/").slice(0, 2).join("/")));
147
- const siblingEntries = entries.filter((entry) => affectedConcepts.has(`${entry.tab}/${entry.concept}`));
148
- const siblingFiles = siblingEntries
149
- .flatMap((entry) => contextEntryPaths(entry))
150
- .filter((path) => !missingPaths.has(path) && isContextEligiblePath(path, ignoreGlobs));
151
- const eligibleFiles = new Set([...dirtyExisting, ...dependencies, ...siblingFiles]);
152
- const structuralPreviews = new Map<string, string>();
153
- for (const path of [...new Set(siblingFiles)].sort()) {
154
- try {
155
- const content = await readFile(join(root, path), "utf8");
156
- const lines = content
157
- .split("\n")
158
- .filter((line) => line.trim())
159
- .slice(0, 30)
160
- .join("\n");
161
- structuralPreviews.set(path, truncAt(lines, MAX_STRUCTURAL_PREVIEW));
162
- } catch {
163
- structuralPreviews.set(path, "preview unavailable");
164
- }
165
- }
166
- return {
167
- root,
168
- ignoreGlobs,
169
- files,
170
- entries,
171
- dirtyExisting,
172
- dependencies,
173
- affectedIds,
174
- affectedConcepts,
175
- eligibleFiles,
176
- missingPaths,
177
- structuralPreviews,
178
- };
179
- }
180
-
181
- export async function discoverDirectDependencies(
182
- root: string,
183
- files: readonly Pick<SyncDirtyFile, "path" | "evidence">[],
184
- ): Promise<Set<string>> {
185
- const result = new Set<string>();
186
- const knownExtensions = [
187
- ".ts",
188
- ".tsx",
189
- ".js",
190
- ".jsx",
191
- ".mjs",
192
- ".cjs",
193
- ".json",
194
- ".py",
195
- ".rs",
196
- ".go",
197
- ".c",
198
- ".cc",
199
- ".cpp",
200
- ".h",
201
- ".hpp",
202
- ];
203
- for (const file of files) {
204
- let content = file.evidence;
205
- try {
206
- content += `\n${await readFile(join(root, file.path), "utf8")}`;
207
- } catch {
208
- /* evidence remains */
209
- }
210
- const statement = /(?:import|export|require|include|use|mod(?:ule)?)\b[^\n]*?["'`]([^"'`]+)["'`]/g;
211
- for (const match of content.matchAll(statement)) {
212
- const specifier = match[1];
213
- if (!specifier?.startsWith("./") && !specifier?.startsWith("../")) continue;
214
- const absolute = resolve(dirname(join(root, file.path)), specifier);
215
- const relativePath = relative(root, absolute).split(sep).join("/");
216
- if (!relativePath || relativePath === ".." || relativePath.startsWith("../")) continue;
217
- const candidates = extname(absolute)
218
- ? [absolute]
219
- : [
220
- ...knownExtensions.map((extension) => `${absolute}${extension}`),
221
- ...knownExtensions.map((extension) => join(absolute, `index${extension}`)),
222
- ];
223
- const matches: string[] = [];
224
- if (await isFile(absolute)) matches.push(absolute);
225
- else for (const candidate of candidates) if (await isFile(candidate)) matches.push(candidate);
226
- if (matches.length === 1)
227
- result.add(
228
- relative(root, matches[0] ?? absolute)
229
- .split(sep)
230
- .join("/"),
231
- );
232
- }
233
- }
234
- return result;
235
- }
236
-
237
- export function formatEvidenceSection(evidence: SyncEvidence, section: string, path?: string): string {
238
- switch (section) {
239
- case "overview":
240
- return truncAt(formatOverview(evidence), MAX_SECTION);
241
- case "catalog":
242
- return truncAt(formatCatalog(evidence), MAX_SECTION);
243
- case "dirty":
244
- return truncAt(formatDirty(evidence), MAX_SECTION);
245
- case "file": {
246
- const target = path?.trim();
247
- if (!target) throw new Error("path is required when section is file");
248
- const file = evidence.files.find((item) => item.path === target || item.oldPath === target);
249
- if (!file) throw new Error(`No dirty evidence for path: ${target}`);
250
- return truncAt(formatDirtyFile(file), MAX_SECTION);
251
- }
252
- case "dependencies":
253
- return truncAt(
254
- ["Resolved direct dependency candidates:", ...[...evidence.dependencies].sort()].join("\n") ||
255
- "Resolved direct dependency candidates:\n(none)",
256
- MAX_SECTION,
257
- );
258
- case "previews":
259
- return truncAt(formatPreviews(evidence), MAX_SECTION);
260
- case "invariants":
261
- return truncAt(formatInvariants(evidence), MAX_SECTION);
262
- default:
263
- throw new Error(`Unknown evidence section: ${section}`);
264
- }
265
- }
266
-
267
- async function loadEvidenceSection(
268
- pi: ExtensionAPI,
269
- ctx: ExtensionContext,
270
- section: string,
271
- path: string | undefined,
272
- ): Promise<string> {
273
- if (!ctx.isProjectTrusted()) throw new Error("Context sync evidence requires a trusted project");
274
- const git = createGitRunner(pi, ctx);
275
- const status = await loadRepoStatus(git);
276
- if (!status) throw new Error("No Git repository found");
277
- const settings = await loadTauExtensionSettings(ctx, contextSettings);
278
- if (section === "invariants") {
279
- const failure = formatContextValidationFailure(
280
- await validateContextCatalog(git, status.root, settings.validation.ignoreGlobs),
281
- );
282
- return failure ?? "Context catalog invariants hold. No uncovered eligible dirty files. No stale catalog paths.";
283
- }
284
- const evidence = await collectSyncEvidence(git, status.root, settings.validation.ignoreGlobs);
285
- return formatEvidenceSection(evidence, section, path);
286
- }
287
-
288
- function formatOverview(evidence: SyncEvidence): string {
289
- const uncovered = evidence.files
290
- .filter((file) => file.kind !== "deleted" && file.memberships.length === 0)
291
- .map((file) => file.path);
292
- const stale = [...evidence.missingPaths].sort();
293
- return [
294
- `Project root: ${evidence.root}`,
295
- `Ignore globs: ${evidence.ignoreGlobs.join(", ") || "(none)"}`,
296
- "",
297
- "Ladder (answer in order before editing):",
298
- "1. Domain (tab/folder): reuse, new, or split?",
299
- "2. Concept (toml): reuse, new, split, or merge?",
300
- "3. Entry (section): update, new, split, delete, or move?",
301
- "4. Bloat: any touched scope now a junk drawer?",
302
- "5. Membership: read/outline/references under winners only. Preserve existing loading mode.",
303
- "",
304
- "Edit only files under .pi/contexts/. Use patch. Do not use bash.",
305
- "Every eligible changed non-deleted file must belong to at least one entry. Remove every stale catalog path.",
306
- "Prefer honest typology over stuffing paths into the nearest existing feature bucket.",
307
- "",
308
- `Dirty eligible files: ${evidence.files.length}`,
309
- `Uncovered changed files: ${uncovered.length}`,
310
- `Stale catalog paths: ${stale.length}`,
311
- `Affected entries: ${evidence.affectedIds.size}`,
312
- `Affected concepts: ${evidence.affectedConcepts.size}`,
313
- `Direct dependencies: ${evidence.dependencies.size}`,
314
- "",
315
- "Changed files with no membership:",
316
- ...(uncovered.length ? uncovered.map((path) => `- ${path}`) : ["(none)"]),
317
- "",
318
- "Stale catalog paths:",
319
- ...(stale.length ? stale.map((path) => `- ${path}`) : ["(none)"]),
320
- "",
321
- "Affected entries:",
322
- ...(evidence.affectedIds.size ? [...evidence.affectedIds].sort().map((id) => `- ${id}`) : ["(none)"]),
323
- "",
324
- "Affected concepts:",
325
- ...(evidence.affectedConcepts.size ? [...evidence.affectedConcepts].sort().map((id) => `- ${id}`) : ["(none)"]),
326
- "",
327
- "Dirty file index:",
328
- ...evidence.files.map(
329
- (file) =>
330
- `[${file.id}] ${file.status} ${file.path}${file.oldPath ? ` <- ${file.oldPath}` : ""} kind=${file.kind} memberships=${file.memberships.join(",") || "none"} oldMemberships=${file.oldMemberships.join(",") || "none"}`,
331
- ),
332
- "",
333
- "Next: context_sync_evidence section=catalog, then dirty or file as needed. After edits, section=invariants.",
334
- ].join("\n");
335
- }
336
-
337
- function formatCatalog(evidence: SyncEvidence): string {
338
- return [
339
- "Complete context catalog skeleton:",
340
- JSON.stringify(
341
- evidence.entries.map((entry) => ({
342
- id: entry.id,
343
- tab: entry.tab,
344
- concept: entry.concept,
345
- conceptName: entry.conceptName,
346
- conceptDescription: entry.conceptDescription,
347
- entry: entry.name,
348
- description: entry.description,
349
- read: entry.read,
350
- outline: entry.outline,
351
- references: entry.references,
352
- missing: contextEntryPaths(entry).filter((path) => evidence.missingPaths.has(path)),
353
- path: entry.path,
354
- })),
355
- null,
356
- 2,
357
- ),
358
- ].join("\n");
359
- }
360
-
361
- function formatDirty(evidence: SyncEvidence): string {
362
- return ["Dirty files:", ...evidence.files.map((file) => formatDirtyFile(file))].join("\n\n");
363
- }
364
-
365
- function formatDirtyFile(file: SyncDirtyFile, includeEvidence = true): string {
366
- const header = [
367
- `[${file.id}] ${file.status} ${file.path}${file.oldPath ? ` <- ${file.oldPath}` : ""}`,
368
- `kind=${file.kind}`,
369
- `memberships=${file.memberships.join(",") || "none"}`,
370
- `oldMemberships=${file.oldMemberships.join(",") || "none"}`,
371
- ].join("\n");
372
- return includeEvidence ? `${header}\n${file.evidence}` : header;
373
- }
374
-
375
- function formatPreviews(evidence: SyncEvidence): string {
376
- const lines = ["Bounded affected-entry structural previews:"];
377
- if (evidence.structuralPreviews.size === 0) lines.push("(none)");
378
- else
379
- for (const [path, preview] of [...evidence.structuralPreviews].sort((a, b) => a[0].localeCompare(b[0]))) {
380
- lines.push("", path, preview);
381
- }
382
- return lines.join("\n");
383
- }
384
-
385
- function formatInvariants(evidence: SyncEvidence): string {
386
- const uncovered = evidence.files
387
- .filter((file) => file.kind !== "deleted" && file.memberships.length === 0)
388
- .map((file) => file.path)
389
- .sort();
390
- const stale = [...evidence.missingPaths].sort();
391
- if (uncovered.length === 0 && stale.length === 0)
392
- return "Context catalog invariants hold. No uncovered eligible dirty files. No stale catalog paths.";
393
- return [
394
- "Context catalog invariants failed.",
395
- "",
396
- "Uncovered changed files:",
397
- ...(uncovered.length ? uncovered.map((path) => `- ${path}`) : ["(none)"]),
398
- "",
399
- "Stale catalog paths:",
400
- ...(stale.length ? stale.map((path) => `- ${path}`) : ["(none)"]),
401
- "",
402
- "Continue editing .pi/contexts until invariants hold, then recheck.",
403
- ].join("\n");
404
- }
405
-
406
- async function collectDirtyFiles(
407
- git: GitRunner,
408
- root: string,
409
- entries: readonly ContextEntry[],
410
- ignoreGlobs: readonly string[],
411
- ): Promise<SyncDirtyFile[]> {
412
- const raw = await git.run(["status", "--porcelain=v2", "-z", "--untracked-files=all"], { cwd: root });
413
- const parts = raw.split("\0").filter(Boolean);
414
- const parsed: Omit<SyncDirtyFile, "id" | "memberships" | "oldMemberships" | "evidence">[] = [];
415
- for (let index = 0; index < parts.length; index++) {
416
- const record = parts[index];
417
- if (!record) continue;
418
- if (record[0] === "u")
419
- throw new Error("Unmerged conflict entries detected; resolve conflicts before context sync.");
420
- if (record[0] === "?") {
421
- parsed.push({ path: record.slice(2), status: "??", kind: "untracked", untracked: true });
422
- continue;
423
- }
424
- const fields = record.split(" ");
425
- if (record[0] === "1") {
426
- const status = (fields[1] ?? "..").replace(/\./g, " ");
427
- parsed.push({
428
- path: fields.slice(8).join(" "),
429
- status,
430
- kind: status.includes("D") ? "deleted" : status.includes("A") ? "added" : "modified",
431
- untracked: false,
432
- });
433
- } else if (record[0] === "2") {
434
- const oldPath = parts[index + 1];
435
- const status = (fields[1] ?? "..").replace(/\./g, " ");
436
- parsed.push({
437
- path: fields.slice(9).join(" "),
438
- status,
439
- kind: fields[8]?.startsWith("C") ? "copied" : "renamed",
440
- untracked: false,
441
- ...(oldPath ? { oldPath } : {}),
442
- });
443
- index++;
444
- }
445
- }
446
- const sensitive = parsed.filter(
447
- (file) =>
448
- file.kind !== "deleted" &&
449
- (isSensitiveContextPath(file.path) || (file.oldPath !== undefined && isSensitiveContextPath(file.oldPath))),
450
- );
451
- if (sensitive.length > 0)
452
- throw new Error(
453
- `Sensitive files cannot be inspected by context sync:\n${sensitive.map((file) => `- ${file.path}`).join("\n")}`,
454
- );
455
- const sorted = parsed
456
- .filter(
457
- (file) =>
458
- isContextEligiblePath(file.path, ignoreGlobs) ||
459
- (file.oldPath !== undefined && isContextEligiblePath(file.oldPath, ignoreGlobs)),
460
- )
461
- .sort((a, b) => a.path.localeCompare(b.path));
462
- const result: SyncDirtyFile[] = [];
463
- for (let offset = 0; offset < sorted.length; offset += EVIDENCE_CONCURRENCY) {
464
- result.push(
465
- ...(await Promise.all(
466
- sorted.slice(offset, offset + EVIDENCE_CONCURRENCY).map(async (file, inner) => ({
467
- ...file,
468
- id: offset + inner + 1,
469
- memberships: entries
470
- .filter((entry) => contextEntryPaths(entry).includes(file.path))
471
- .map((entry) => entry.id),
472
- oldMemberships: file.oldPath
473
- ? entries
474
- .filter((entry) => contextEntryPaths(entry).includes(file.oldPath ?? ""))
475
- .map((entry) => entry.id)
476
- : [],
477
- evidence: await dirtyEvidence(git, root, file),
478
- })),
479
- )),
480
- );
481
- }
482
- return result;
483
- }
484
-
485
- async function dirtyEvidence(
486
- git: GitRunner,
487
- root: string,
488
- file: { path: string; oldPath?: string; kind: string; untracked: boolean },
489
- ): Promise<string> {
490
- if (file.kind === "deleted") return "deleted file; contents omitted";
491
- if (file.untracked) {
492
- try {
493
- const info = await stat(join(root, file.path));
494
- if (!info.isFile()) return "untracked non-file; contents omitted";
495
- if (info.size > MAX_UNTRACKED_BYTES) return `untracked file, ${info.size} bytes; contents omitted`;
496
- const bytes = await readFile(join(root, file.path));
497
- if (bytes.includes(0)) return `untracked binary file, ${info.size} bytes`;
498
- return truncAt(`untracked file preview:\n${bytes.toString("utf8")}`, MAX_DIRTY_EVIDENCE);
499
- } catch {
500
- return "untracked preview unavailable";
501
- }
502
- }
503
- const paths = file.oldPath ? [file.oldPath, file.path] : [file.path];
504
- const diff = await git.run(["diff", "--stat", "--patch", "--no-color", "--no-ext-diff", "HEAD", "--", ...paths], {
505
- cwd: root,
506
- optional: true,
507
- });
508
- return truncAt(diff || "metadata-only change", MAX_DIRTY_EVIDENCE);
509
- }
510
-
511
- async function isFile(path: string): Promise<boolean> {
512
- try {
513
- return (await stat(path)).isFile();
514
- } catch {
515
- return false;
516
- }
517
- }
@@ -1,141 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { createReadStream } from "node:fs";
3
- import { readFile } from "node:fs/promises";
4
- import { resolve } from "node:path";
5
- import type { ContextEvent, ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
- import {
7
- CONTEXT_PROJECTION_TYPE,
8
- isContextProjectionMessage,
9
- isLegacyContextMessage,
10
- } from "../../shared/context-messages.ts";
11
- import { requestOutlineInjections } from "../../shared/outline-injection.ts";
12
- import type { ContextEntry } from "./definitions.ts";
13
-
14
- type ContextMessage = ContextEvent["messages"][number];
15
-
16
- export { CONTEXT_PROJECTION_TYPE } from "../../shared/context-messages.ts";
17
-
18
- export interface SelectedContext {
19
- entries: ContextEntry[];
20
- missingEntryIds: string[];
21
- read: string[];
22
- outline: string[];
23
- references: string[];
24
- }
25
-
26
- export function selectContextEntries(entries: readonly ContextEntry[], entryIds: readonly string[]): SelectedContext {
27
- const byId = new Map(entries.map((entry) => [entry.id, entry]));
28
- const selected = entryIds.flatMap((id) => {
29
- const entry = byId.get(id);
30
- return entry ? [entry] : [];
31
- });
32
- const read = new Set(selected.flatMap((entry) => entry.read));
33
- const outline = new Set(selected.flatMap((entry) => entry.outline).filter((path) => !read.has(path)));
34
- const references = new Set(
35
- selected.flatMap((entry) => entry.references).filter((path) => !read.has(path) && !outline.has(path)),
36
- );
37
- return {
38
- entries: selected,
39
- missingEntryIds: entryIds.filter((id) => !byId.has(id)),
40
- read: [...read].sort((left, right) => left.localeCompare(right)),
41
- outline: [...outline].sort((left, right) => left.localeCompare(right)),
42
- references: [...references].sort((left, right) => left.localeCompare(right)),
43
- };
44
- }
45
-
46
- export async function contextProjectionKey(
47
- root: string,
48
- selection: SelectedContext,
49
- signal: AbortSignal | undefined,
50
- isLifecycleCurrent: () => boolean,
51
- ): Promise<string> {
52
- const paths = [...selection.read, ...selection.outline];
53
- const files: Array<[string, string]> = [];
54
- for (const path of paths) {
55
- signal?.throwIfAborted();
56
- if (!isLifecycleCurrent()) throw new Error("Context projection crossed a session lifecycle boundary");
57
- try {
58
- const hash = createHash("sha256");
59
- for await (const chunk of createReadStream(resolve(root, path), { signal })) {
60
- if (!isLifecycleCurrent()) throw new Error("Context projection crossed a session lifecycle boundary");
61
- hash.update(chunk);
62
- }
63
- files.push([path, hash.digest("hex")]);
64
- } catch (error) {
65
- if (signal?.aborted || !isLifecycleCurrent()) throw error;
66
- files.push([path, "missing"]);
67
- }
68
- }
69
- return JSON.stringify({
70
- entries: selection.entries.map((entry) => [entry.id, entry.description]),
71
- missingEntryIds: selection.missingEntryIds,
72
- read: selection.read,
73
- outline: selection.outline,
74
- references: selection.references,
75
- files,
76
- });
77
- }
78
-
79
- export async function buildContextProjection(
80
- pi: Pick<ExtensionAPI, "events">,
81
- root: string,
82
- selection: SelectedContext,
83
- signal: AbortSignal | undefined,
84
- isLifecycleCurrent: () => boolean,
85
- ): Promise<string> {
86
- const warnings = selection.missingEntryIds.map((id) => `${id}: selected entry no longer exists`);
87
- const reads: Array<{ path: string; content: string }> = [];
88
- for (const path of selection.read) {
89
- signal?.throwIfAborted();
90
- if (!isLifecycleCurrent()) throw new Error("Context projection crossed a session lifecycle boundary");
91
- try {
92
- reads.push({ path, content: await readFile(resolve(root, path), "utf8") });
93
- } catch (error) {
94
- warnings.push(`${path}: ${error instanceof Error ? error.message : String(error)}`);
95
- }
96
- }
97
- const outlined = await requestOutlineInjections(pi, {
98
- cwd: root,
99
- batchId: randomUUID(),
100
- paths: selection.outline,
101
- signal,
102
- isLifecycleCurrent,
103
- });
104
- warnings.push(...outlined.warnings);
105
- if (!isLifecycleCurrent()) throw new Error("Context projection crossed a session lifecycle boundary");
106
- return [
107
- "Active repository context (ephemeral; rebuilt from the current branch selection before each model call):",
108
- ...selection.entries.map((entry) => `- ${entry.id}: ${entry.description}`),
109
- ...(selection.missingEntryIds.length ? selection.missingEntryIds.map((id) => `- ${id}: unavailable`) : []),
110
- "",
111
- "Complete current file contents:",
112
- ...(reads.length
113
- ? reads.flatMap(({ path, content }) => [`<read path=${JSON.stringify(path)}>`, content, "</read>"])
114
- : ["(none)"]),
115
- "",
116
- "Current structural outlines:",
117
- ...(outlined.messages.length
118
- ? outlined.messages.flatMap((message) => ["<outline>", message.content, "</outline>"])
119
- : ["(none)"]),
120
- "",
121
- "Unloaded references:",
122
- ...(selection.references.length ? selection.references.map((path) => `- ${path}`) : ["(none)"]),
123
- ...(warnings.length ? ["", "Projection warnings:", ...warnings.map((warning) => `- ${warning}`)] : []),
124
- "",
125
- "Treat complete reads and outlines as current snapshots. Inspect unloaded references only when the request or concrete evidence requires them. Explore elsewhere only for missing information.",
126
- ].join("\n");
127
- }
128
-
129
- export function contextProjectionMessage(content: string): ContextMessage {
130
- return {
131
- role: "custom",
132
- customType: CONTEXT_PROJECTION_TYPE,
133
- content,
134
- display: false,
135
- timestamp: Date.now(),
136
- };
137
- }
138
-
139
- export function removeLegacyContextMessages(messages: readonly ContextMessage[]): ContextMessage[] {
140
- return messages.filter((message) => !isContextProjectionMessage(message) && !isLegacyContextMessage(message));
141
- }
@@ -1,30 +0,0 @@
1
- import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
-
3
- export const CONTEXT_SELECTION_TYPE = "tau.context.selection";
4
-
5
- export interface ContextSelectionStateV1 {
6
- v: 1;
7
- entryIds: string[];
8
- }
9
-
10
- export function createContextSelectionState(entryIds: readonly string[]): ContextSelectionStateV1 {
11
- return { v: 1, entryIds: [...new Set(entryIds)].sort((left, right) => left.localeCompare(right)) };
12
- }
13
-
14
- export function replayContextSelection(branch: readonly SessionEntry[]): ContextSelectionStateV1 {
15
- for (let index = branch.length - 1; index >= 0; index -= 1) {
16
- const entry = branch[index];
17
- if (entry?.type !== "custom" || entry.customType !== CONTEXT_SELECTION_TYPE) continue;
18
- const state = parseContextSelectionState(entry.data);
19
- if (state) return state;
20
- }
21
- return createContextSelectionState([]);
22
- }
23
-
24
- function parseContextSelectionState(value: unknown): ContextSelectionStateV1 | undefined {
25
- if (!value || typeof value !== "object") return undefined;
26
- const record = value as Record<string, unknown>;
27
- if (record.v !== 1 || !Array.isArray(record.entryIds) || record.entryIds.some((id) => typeof id !== "string"))
28
- return undefined;
29
- return createContextSelectionState(record.entryIds as string[]);
30
- }