@shanepadgett/tau-agent 0.33.1 → 0.35.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 (203) 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/bash-approval/README.md +20 -0
  8. package/extensions/bash-approval/index.ts +235 -0
  9. package/extensions/bash-approval/settings.ts +24 -0
  10. package/extensions/branch/index.ts +38 -21
  11. package/extensions/cache-diagnostics/index.ts +70 -58
  12. package/extensions/checkpoint/README.md +9 -0
  13. package/extensions/checkpoint/checkpoint-budget.ts +66 -0
  14. package/extensions/checkpoint/checkpoint.ts +279 -0
  15. package/extensions/checkpoint/index.ts +102 -0
  16. package/extensions/checkpoint/messages.ts +170 -0
  17. package/extensions/checkpoint/prompt.ts +24 -0
  18. package/extensions/checkpoint/settings.ts +27 -0
  19. package/extensions/commit/commit-effort.ts +3 -3
  20. package/extensions/commit/commit-plan.ts +2 -2
  21. package/extensions/context/README.md +12 -8
  22. package/extensions/context/definitions.ts +178 -55
  23. package/extensions/context/index.ts +192 -160
  24. package/extensions/context/panel.ts +57 -37
  25. package/extensions/context/sync.ts +139 -89
  26. package/extensions/context/validation.ts +37 -25
  27. package/extensions/effort/README.md +1 -1
  28. package/extensions/effort/index.ts +3 -3
  29. package/extensions/effort/state.ts +3 -18
  30. package/extensions/explore/guidance.ts +6 -47
  31. package/extensions/explore/index.ts +38 -53
  32. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  33. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  34. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  35. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  36. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  37. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  38. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  39. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  40. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  41. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  42. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  43. package/extensions/explore/tools/target-params.ts +9 -0
  44. package/extensions/footer/index.ts +75 -56
  45. package/extensions/handoff/index.ts +8 -30
  46. package/extensions/image-gen/index.ts +46 -40
  47. package/extensions/patch/executor.ts +190 -135
  48. package/extensions/patch/matcher.ts +82 -48
  49. package/extensions/patch/parser.ts +149 -105
  50. package/extensions/patch/render.ts +32 -39
  51. package/extensions/patch/summary.ts +30 -26
  52. package/extensions/ready/README.md +7 -0
  53. package/extensions/ready/index.ts +58 -0
  54. package/extensions/ready/model.ts +128 -0
  55. package/extensions/ready/packs.ts +272 -0
  56. package/extensions/ready/render.ts +445 -0
  57. package/extensions/ready/scan.ts +760 -0
  58. package/extensions/reference/index.ts +2 -0
  59. package/extensions/reference/panel.ts +94 -31
  60. package/extensions/review/README.md +14 -6
  61. package/extensions/review/index.ts +58 -99
  62. package/extensions/review/model.ts +13 -33
  63. package/extensions/review/session.ts +9 -23
  64. package/extensions/runtime-context/context.ts +30 -26
  65. package/extensions/script-runner/README.md +2 -2
  66. package/extensions/script-runner/index.ts +122 -82
  67. package/extensions/silent-command-runner/index.ts +38 -23
  68. package/extensions/soul/README.md +2 -2
  69. package/extensions/soul/index.ts +4 -4
  70. package/extensions/soul/prompt.ts +6 -10
  71. package/extensions/soul/settings.ts +6 -3
  72. package/extensions/subagent/agents/context-sync.md +185 -33
  73. package/extensions/subagent/agents/scout.md +2 -2
  74. package/extensions/subagent/agents/web-research.md +2 -2
  75. package/extensions/subagent/agents.ts +137 -84
  76. package/extensions/subagent/cmux-dashboard.ts +39 -30
  77. package/extensions/subagent/index.ts +79 -59
  78. package/extensions/subagent/resume.ts +1 -1
  79. package/extensions/subagent/run.ts +308 -193
  80. package/extensions/subagent/runtime.ts +781 -343
  81. package/extensions/tau/index.ts +47 -38
  82. package/extensions/tau-help/help.md +20 -8
  83. package/extensions/tool-loader/README.md +4 -2
  84. package/extensions/tool-loader/index.ts +77 -52
  85. package/extensions/web/index.ts +6 -3
  86. package/extensions/web/webfetch.ts +114 -86
  87. package/package.json +3 -3
  88. package/schemas/tau.schema.json +38 -35
  89. package/shared/bounded-text-result.ts +1 -0
  90. package/shared/checkpoint-visibility.ts +9 -0
  91. package/shared/events.ts +28 -10
  92. package/shared/isolated-session.ts +72 -30
  93. package/shared/model-effort.ts +23 -22
  94. package/shared/model-fallback/index.ts +66 -41
  95. package/shared/settings/load.ts +45 -38
  96. package/shared/temporary-output-store.ts +16 -10
  97. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  98. package/{extensions/explore → src}/ast/engine.ts +54 -38
  99. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  100. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  101. package/src/ast/format/deps.ts +94 -0
  102. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  103. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  104. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  105. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  106. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  107. package/src/ast/format/targeted-result.ts +20 -0
  108. package/src/ast/grammars/c_sharp.wasm +0 -0
  109. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  110. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  111. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  112. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  113. package/{extensions/explore → src}/ast/ir.ts +6 -0
  114. package/src/ast/languages/csharp-file-deps.ts +90 -0
  115. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  116. package/src/ast/languages/dotted-index.ts +131 -0
  117. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  118. package/src/ast/languages/fixtures/sample.cs +53 -0
  119. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  120. package/src/ast/languages/fixtures/sample.go +37 -0
  121. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  122. package/src/ast/languages/fixtures/sample.java +40 -0
  123. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  124. package/src/ast/languages/fixtures/sample.kt +31 -0
  125. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  126. package/src/ast/languages/fixtures/sample.md +23 -0
  127. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  128. package/src/ast/languages/fixtures/sample.rs +54 -0
  129. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  130. package/src/ast/languages/fixtures/sample.swift +45 -0
  131. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  132. package/src/ast/languages/fixtures/sample.ts +45 -0
  133. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  134. package/src/ast/languages/fixtures/sample.tsx +36 -0
  135. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  136. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  137. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  138. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  139. package/src/ast/languages/jvm-file-deps.ts +135 -0
  140. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  141. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  142. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  143. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  144. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  145. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  146. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  147. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  148. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  149. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  150. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  151. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  152. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  153. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  154. package/src/ast/queries/impact.ts +291 -0
  155. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  156. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  157. package/{extensions/explore → src}/ast/query.ts +1 -1
  158. package/{extensions/explore → src}/ast/registry.ts +1 -2
  159. package/{extensions/explore → src}/ast/scan.ts +1 -1
  160. package/src/ast/session.ts +50 -0
  161. package/{extensions/explore → src}/ast/slice.ts +8 -0
  162. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  163. package/src/file-injection/index.ts +547 -0
  164. package/src/image-generation/client.ts +50 -31
  165. package/src/image-generation/index.ts +177 -134
  166. package/src/index.ts +2 -0
  167. package/src/tool-loading/index.ts +58 -0
  168. package/extensions/context/evidence.ts +0 -517
  169. package/extensions/context/projection.ts +0 -141
  170. package/extensions/context/state.ts +0 -30
  171. package/extensions/context/write-scope.ts +0 -109
  172. package/extensions/explore/ast/format/deps.ts +0 -86
  173. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  174. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  175. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  176. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  177. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  178. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  179. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  180. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  181. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  182. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  183. package/extensions/explore/ast/languages/odin.ts +0 -407
  184. package/extensions/explore/ast/queries/impact.ts +0 -250
  185. package/extensions/explore/outline-injection.ts +0 -151
  186. package/extensions/explore/read/autoread.ts +0 -118
  187. package/extensions/review/panel.ts +0 -128
  188. package/extensions/working-memory/README.md +0 -17
  189. package/extensions/working-memory/checkpoint.ts +0 -242
  190. package/extensions/working-memory/index.ts +0 -341
  191. package/extensions/working-memory/memory.ts +0 -235
  192. package/extensions/working-memory/render.ts +0 -185
  193. package/extensions/working-memory/settings.ts +0 -38
  194. package/extensions/working-memory/state.ts +0 -158
  195. package/shared/autoread.ts +0 -234
  196. package/shared/context-messages.ts +0 -19
  197. package/shared/outline-injection.ts +0 -56
  198. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  199. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  200. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  201. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  202. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  203. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -1,12 +1,12 @@
1
1
  # Context
2
2
 
3
- Context stores reusable repository work scopes in `.pi/contexts`. Folder names become selector tabs, TOML files become concepts, and TOML sections become selectable entries.
3
+ Context stores reusable repository work scopes in `.pi/contexts`. Domain folders use `NN_slug` (`01_extensions`, `02_core`, …) so `/context` can order tabs; the UI shows the slug only. TOML files become concepts, and TOML sections become selectable entries.
4
4
 
5
5
  See [Context management](../../docs/context.md) for catalog structure and taxonomy guidance.
6
6
 
7
- Use `/context` to set active entries for the current session branch. Tau stores only the selected entry IDs. Before each model call it rebuilds one ephemeral projection from current files, so repeated selections do not accumulate snapshots in conversation history. Open `/context`, press `Ctrl+C`, then confirm to clear active context.
7
+ Use `/context` to inject entries into the conversation. Selecting entries reads `read` paths in full, resolves `show` targets to current declaration slices, outlines `outline` paths, and adds one hidden note listing `references` and instructing the agent to treat the injected material as current. Injection happens once, immediately, before your next prompt; nothing is rebuilt on later model calls. Prefer complete reads for bounded work files; use `show` for thin contracts from larger neighbors; reserve `outline` for large or noisy files.
8
8
 
9
- After meaningful uncommitted work (new/moved ownership, not trivial already-covered polish), the coding agent should run the `context-sync` subagent so `.pi/contexts` stays aligned. Context sync catalogs durable code and long-lived documentation. Scratch pads, working plans, interviews, rough ideas, and other temporary artifacts should stay out; add recurring transient paths to `validation.ignoreGlobs`. Humans can also run `/context-sync` or `/context-sync <nudge>` and press Escape to cancel a running sync. It walks domain → concept → entry → membership, edits only `.pi/contexts` with `patch`, and the harness verifies write scope plus catalog invariants afterward. Out-of-scope writes are restored and the run fails. Optional nudge text soft-steers judgment without skipping evidence.
9
+ After meaningful uncommitted work (new/moved ownership, not trivial already-covered polish), the coding agent should run the `context-sync` subagent so `.pi/contexts` stays aligned. Context sync catalogs durable code and long-lived documentation as **work packs** (job-shaped entries an agent can start from), not bare path indexes. Scratch pads, working plans, interviews, rough ideas, and other temporary artifacts should stay out; add recurring transient paths to `validation.ignoreGlobs`. Humans can also run `/context-sync` or `/context-sync <nudge>`; the command replaces the editor with a status panel (Escape / Ctrl+C cancel). It uses normal repo tools, walks domain → concept → entry → start-pack modes, and prefers `patch` under `.pi/contexts`. The harness verifies catalog coverage afterward. Optional nudge text soft-steers judgment (including quality rewrites of weak bags) without skipping the ladder or coverage.
10
10
 
11
11
  Sync surface is configurable:
12
12
 
@@ -37,15 +37,19 @@ description = "Player-owned gameplay systems"
37
37
 
38
38
  [input]
39
39
  description = "Input mapping and command handling"
40
- read = []
41
- outline = ["src/player/input.ts"]
40
+ read = ["src/player/input.ts"]
41
+ show = []
42
+ outline = []
42
43
  references = []
43
44
 
44
45
  [movement]
45
46
  description = "Player locomotion and collision"
46
- read = []
47
- outline = ["src/player/movement.ts"]
47
+ read = ["src/player/movement.ts"]
48
+ show = [
49
+ { path = "src/runtime/fetch-handler.ts", name = "handleFetch" },
50
+ ]
51
+ outline = []
48
52
  references = ["src/runtime/fetch-handler.ts"]
49
53
  ```
50
54
 
51
- Every entry must declare all three loading arrays. `read` supplies exact complete contents, `outline` asks Explore for current structural outlines, and `references` lists unloaded paths. If selected entries classify the same path differently, precedence is `read`, then `outline`, then `references`.
55
+ Every entry must declare all four loading arrays. `read` supplies exact complete contents, `show` resolves path+name declaration slices, `outline` asks Explore for structural outlines, and `references` lists unloaded paths. If selected entries classify the same path differently, precedence is `read`, then `show`, then `outline`, then `references`.
@@ -4,6 +4,14 @@ import { basename, dirname, extname, join, relative, resolve, sep } from "node:p
4
4
  import { parse } from "smol-toml";
5
5
  import { matchGlob } from "../../shared/glob.ts";
6
6
 
7
+ export type ContextShowView = "signature" | "signatureWithDocs" | "declaration" | "declarationWithImports";
8
+
9
+ export interface ContextShowTarget {
10
+ path: string;
11
+ name: string;
12
+ view: ContextShowView;
13
+ }
14
+
7
15
  export interface ContextEntry {
8
16
  id: string;
9
17
  tab: string;
@@ -13,6 +21,7 @@ export interface ContextEntry {
13
21
  name: string;
14
22
  description: string;
15
23
  read: string[];
24
+ show: ContextShowTarget[];
16
25
  outline: string[];
17
26
  references: string[];
18
27
  path: string;
@@ -38,7 +47,14 @@ const CONTEXT_IGNORED_FILENAMES = new Set([
38
47
  "uv.lock",
39
48
  "yarn.lock",
40
49
  ]);
41
- const CONTEXT_ENTRY_FIELDS = new Set(["description", "read", "outline", "references"]);
50
+ const CONTEXT_ENTRY_FIELDS = new Set(["description", "read", "show", "outline", "references"]);
51
+ const CONTEXT_SHOW_VIEWS = new Set<ContextShowView>([
52
+ "signature",
53
+ "signatureWithDocs",
54
+ "declaration",
55
+ "declarationWithImports",
56
+ ]);
57
+ const CONTEXT_SHOW_TARGET_FIELDS = new Set(["path", "name", "view"]);
42
58
 
43
59
  export function isContextEligiblePath(path: string, ignoreGlobs: readonly string[] = []): boolean {
44
60
  return (
@@ -84,12 +100,48 @@ export async function findProjectRoot(cwd: string): Promise<string> {
84
100
  return gitRoot ?? resolve(cwd);
85
101
  }
86
102
 
103
+ const TAB_FOLDER_PATTERN = /^(\d{2})_([a-z0-9]+(?:-[a-z0-9]+)*)$/;
104
+
87
105
  function validSlug(value: string, label: string): string {
88
106
  const slug = value.trim();
89
107
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) throw new Error(`${label} must use lowercase kebab-case: ${value}`);
90
108
  return slug;
91
109
  }
92
110
 
111
+ /** Domain folder: `01_extensions` → order 1, tab slug `extensions` (id/label). */
112
+ function parseContextTabFolder(name: string): { order: number; slug: string; folder: string } {
113
+ const match = TAB_FOLDER_PATTERN.exec(name);
114
+ const digits = match?.[1];
115
+ const slug = match?.[2];
116
+ if (!match || digits === undefined || slug === undefined) {
117
+ throw new Error(
118
+ `Context tab folder must be NN_kebab-slug with a two-digit order starting at 01 (e.g. 01_extensions): ${name}`,
119
+ );
120
+ }
121
+ const order = Number(digits);
122
+ if (!Number.isInteger(order) || order < 1 || order > 99) {
123
+ throw new Error(`Context tab order must be an integer from 01 to 99: ${name}`);
124
+ }
125
+ return { order, slug, folder: name };
126
+ }
127
+
128
+ function assertContiguousTabOrders(tabs: readonly { order: number; slug: string; folder: string }[]): void {
129
+ const byOrder = [...tabs].sort((left, right) => left.order - right.order);
130
+ const seenSlugs = new Set<string>();
131
+ for (let index = 0; index < byOrder.length; index++) {
132
+ const tab = byOrder[index];
133
+ if (!tab) continue;
134
+ const expected = index + 1;
135
+ if (tab.order !== expected) {
136
+ throw new Error(
137
+ `Context tab orders must be contiguous from 01 (expected ${String(expected).padStart(2, "0")}_…, found ${tab.folder})`,
138
+ );
139
+ }
140
+ if (seenSlugs.has(tab.slug)) throw new Error(`Duplicate context tab slug: ${tab.slug}`);
141
+ seenSlugs.add(tab.slug);
142
+ }
143
+ }
144
+
93
145
  function normalizeProjectPath(root: string, input: string): string {
94
146
  const absolute = resolve(root, input.trim().replace(/^@/, ""));
95
147
  const path = relative(root, absolute).split(sep).join("/");
@@ -102,8 +154,126 @@ function sortedUnique(values: readonly string[]): string[] {
102
154
  return [...new Set(values)].sort((a, b) => a.localeCompare(b));
103
155
  }
104
156
 
105
- export function contextEntryPaths(entry: Pick<ContextEntry, "read" | "outline" | "references">): string[] {
106
- return sortedUnique([...entry.read, ...entry.outline, ...entry.references]);
157
+ export function contextEntryPaths(entry: Pick<ContextEntry, "read" | "show" | "outline" | "references">): string[] {
158
+ return sortedUnique([
159
+ ...entry.read,
160
+ ...entry.show.map((target) => target.path),
161
+ ...entry.outline,
162
+ ...entry.references,
163
+ ]);
164
+ }
165
+
166
+ function parseOneShowTarget(root: string, catalogPath: string, entryName: string, item: unknown): ContextShowTarget {
167
+ const record = item as Record<string, unknown>;
168
+ const unknownField = Object.keys(record).find((field) => !CONTEXT_SHOW_TARGET_FIELDS.has(field));
169
+ if (unknownField) throw new Error(`Invalid context show field: ${catalogPath} [${entryName}] ${unknownField}`);
170
+ if (typeof record.path !== "string" || !record.path.trim() || typeof record.name !== "string" || !record.name.trim())
171
+ throw new Error(`Invalid context show target: ${catalogPath} [${entryName}]`);
172
+ let view: ContextShowView = "declaration";
173
+ if (record.view !== undefined) {
174
+ if (typeof record.view !== "string" || !CONTEXT_SHOW_VIEWS.has(record.view as ContextShowView))
175
+ throw new Error(`Invalid context show view: ${catalogPath} [${entryName}] ${String(record.view)}`);
176
+ view = record.view as ContextShowView;
177
+ }
178
+ return {
179
+ path: normalizeProjectPath(root, record.path),
180
+ name: record.name.trim(),
181
+ view,
182
+ };
183
+ }
184
+
185
+ function dedupeShowTargets(catalogPath: string, entryName: string, targets: ContextShowTarget[]): ContextShowTarget[] {
186
+ const seen = new Map<string, ContextShowTarget>();
187
+ for (const target of targets) {
188
+ const key = `${target.path}\0${target.name}`;
189
+ const existing = seen.get(key);
190
+ if (existing && existing.view !== target.view)
191
+ throw new Error(
192
+ `Context show target has multiple views: ${catalogPath} [${entryName}] ${target.path} ${target.name}`,
193
+ );
194
+ if (!existing) seen.set(key, target);
195
+ }
196
+ return [...seen.values()].sort(
197
+ (left, right) => left.path.localeCompare(right.path) || left.name.localeCompare(right.name),
198
+ );
199
+ }
200
+
201
+ function parseShowTargets(root: string, catalogPath: string, entryName: string, raw: unknown): ContextShowTarget[] {
202
+ if (!Array.isArray(raw) || raw.some((item) => !item || typeof item !== "object" || Array.isArray(item)))
203
+ throw new Error(`Invalid context entry show list: ${catalogPath} [${entryName}]`);
204
+ return dedupeShowTargets(
205
+ catalogPath,
206
+ entryName,
207
+ raw.map((item) => parseOneShowTarget(root, catalogPath, entryName, item)),
208
+ );
209
+ }
210
+
211
+ function parseContextEntryRecord(
212
+ root: string,
213
+ path: string,
214
+ tab: string,
215
+ concept: string,
216
+ conceptName: string,
217
+ conceptDescription: string,
218
+ name: string,
219
+ value: unknown,
220
+ ): ContextEntry {
221
+ if (!value || typeof value !== "object" || Array.isArray(value))
222
+ throw new Error(`Invalid context entry: ${path} [${name}]`);
223
+ const record = value as Record<string, unknown>;
224
+ const unknownField = Object.keys(record).find((field) => !CONTEXT_ENTRY_FIELDS.has(field));
225
+ if (unknownField) throw new Error(`Invalid context entry field: ${path} [${name}] ${unknownField}`);
226
+ if (
227
+ typeof record.description !== "string" ||
228
+ !record.description.trim() ||
229
+ !Array.isArray(record.read) ||
230
+ record.read.some((item) => typeof item !== "string") ||
231
+ !Array.isArray(record.outline) ||
232
+ record.outline.some((item) => typeof item !== "string") ||
233
+ !Array.isArray(record.references) ||
234
+ record.references.some((item) => typeof item !== "string")
235
+ )
236
+ throw new Error(`Invalid context entry: ${path} [${name}]`);
237
+ const entry = validSlug(name, "Context entry");
238
+ const entryRead = sortedUnique((record.read as string[]).map((item) => normalizeProjectPath(root, item)));
239
+ const entryShow = parseShowTargets(root, path, name, record.show ?? []);
240
+ const entryOutline = sortedUnique((record.outline as string[]).map((item) => normalizeProjectPath(root, item)));
241
+ const entryReferences = sortedUnique(
242
+ (record.references as string[]).map((item) => normalizeProjectPath(root, item)),
243
+ );
244
+ if (entryRead.length === 0 && entryShow.length === 0 && entryOutline.length === 0 && entryReferences.length === 0)
245
+ throw new Error(`Invalid context entry: ${path} [${name}]`);
246
+ const classified = [...entryRead, ...entryOutline, ...entryReferences];
247
+ const overlap = classified.find((item, index) => classified.indexOf(item) !== index);
248
+ if (overlap) throw new Error(`Context path has multiple loading modes: ${path} [${name}] ${overlap}`);
249
+ return {
250
+ id: `${tab}/${concept}/${entry}`,
251
+ tab,
252
+ concept,
253
+ conceptName,
254
+ conceptDescription,
255
+ name: entry,
256
+ description: record.description.trim(),
257
+ read: entryRead,
258
+ show: entryShow,
259
+ outline: entryOutline,
260
+ references: entryReferences,
261
+ path,
262
+ };
263
+ }
264
+
265
+ async function loadConceptFile(root: string, tab: string, folder: string, fileName: string): Promise<ContextEntry[]> {
266
+ const path = join(root, ".pi", "contexts", folder, fileName);
267
+ const concept = validSlug(basename(fileName, ".toml"), "Context concept");
268
+ const raw = parse(await readFile(path, "utf8")) as Record<string, unknown>;
269
+ const conceptName = typeof raw.name === "string" && raw.name.trim() ? raw.name.trim() : concept;
270
+ const conceptDescription = typeof raw.description === "string" ? raw.description.trim() : "";
271
+ const entries: ContextEntry[] = [];
272
+ for (const [name, value] of Object.entries(raw)) {
273
+ if (name === "name" || name === "description") continue;
274
+ entries.push(parseContextEntryRecord(root, path, tab, concept, conceptName, conceptDescription, name, value));
275
+ }
276
+ return entries;
107
277
  }
108
278
 
109
279
  export async function loadContextEntries(root: string): Promise<ContextEntry[]> {
@@ -111,63 +281,16 @@ export async function loadContextEntries(root: string): Promise<ContextEntry[]>
111
281
  if (!(await pathExists(contextsRoot))) return [];
112
282
  const tabs = (await readdir(contextsRoot, { withFileTypes: true }))
113
283
  .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
114
- .sort((a, b) => a.name.localeCompare(b.name));
284
+ .map((entry) => parseContextTabFolder(entry.name));
285
+ assertContiguousTabOrders(tabs);
286
+ tabs.sort((left, right) => left.order - right.order);
115
287
  const result: ContextEntry[] = [];
116
288
  for (const tabEntry of tabs) {
117
- const tab = validSlug(tabEntry.name, "Context tab");
118
- const files = (await readdir(join(contextsRoot, tab), { withFileTypes: true }))
289
+ const files = (await readdir(join(contextsRoot, tabEntry.folder), { withFileTypes: true }))
119
290
  .filter((entry) => entry.isFile() && extname(entry.name) === ".toml")
120
291
  .sort((a, b) => a.name.localeCompare(b.name));
121
292
  for (const file of files) {
122
- const path = join(contextsRoot, tab, file.name);
123
- const concept = validSlug(basename(file.name, ".toml"), "Context concept");
124
- const raw = parse(await readFile(path, "utf8")) as Record<string, unknown>;
125
- const conceptName = typeof raw.name === "string" && raw.name.trim() ? raw.name.trim() : concept;
126
- const conceptDescription = typeof raw.description === "string" ? raw.description.trim() : "";
127
- for (const [name, value] of Object.entries(raw)) {
128
- if (name === "name" || name === "description") continue;
129
- if (!value || typeof value !== "object" || Array.isArray(value))
130
- throw new Error(`Invalid context entry: ${path} [${name}]`);
131
- const record = value as Record<string, unknown>;
132
- const unknownField = Object.keys(record).find((field) => !CONTEXT_ENTRY_FIELDS.has(field));
133
- if (unknownField) throw new Error(`Invalid context entry field: ${path} [${name}] ${unknownField}`);
134
- if (
135
- typeof record.description !== "string" ||
136
- !record.description.trim() ||
137
- !Array.isArray(record.read) ||
138
- record.read.some((item) => typeof item !== "string") ||
139
- !Array.isArray(record.outline) ||
140
- record.outline.some((item) => typeof item !== "string") ||
141
- !Array.isArray(record.references) ||
142
- record.references.some((item) => typeof item !== "string") ||
143
- (record.read.length === 0 && record.outline.length === 0 && record.references.length === 0)
144
- )
145
- throw new Error(`Invalid context entry: ${path} [${name}]`);
146
- const entry = validSlug(name, "Context entry");
147
- const entryRead = sortedUnique((record.read as string[]).map((item) => normalizeProjectPath(root, item)));
148
- const entryOutline = sortedUnique(
149
- (record.outline as string[]).map((item) => normalizeProjectPath(root, item)),
150
- );
151
- const entryReferences = sortedUnique(
152
- (record.references as string[]).map((item) => normalizeProjectPath(root, item)),
153
- );
154
- const classified = [...entryRead, ...entryOutline, ...entryReferences];
155
- const overlap = classified.find((item, index) => classified.indexOf(item) !== index);
156
- if (overlap) throw new Error(`Context path has multiple loading modes: ${path} [${name}] ${overlap}`);
157
- result.push({
158
- id: `${tab}/${concept}/${entry}`,
159
- tab,
160
- concept,
161
- conceptName,
162
- conceptDescription,
163
- name: entry,
164
- description: record.description.trim(),
165
- read: entryRead,
166
- outline: entryOutline,
167
- references: entryReferences,
168
- path,
169
- });
170
- }
293
+ result.push(...(await loadConceptFile(root, tabEntry.slug, tabEntry.folder, file.name)));
171
294
  }
172
295
  }
173
296
  return result;