@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.
- package/docs/context.md +29 -20
- package/docs/extending-tau-agent.md +63 -24
- package/extensions/appshot/index.ts +112 -111
- package/extensions/aside/README.md +7 -0
- package/extensions/aside/index.ts +168 -0
- package/extensions/aside/panel.ts +83 -0
- package/extensions/bash-approval/README.md +20 -0
- package/extensions/bash-approval/index.ts +235 -0
- package/extensions/bash-approval/settings.ts +24 -0
- package/extensions/branch/index.ts +38 -21
- package/extensions/cache-diagnostics/index.ts +70 -58
- package/extensions/checkpoint/README.md +9 -0
- package/extensions/checkpoint/checkpoint-budget.ts +66 -0
- package/extensions/checkpoint/checkpoint.ts +279 -0
- package/extensions/checkpoint/index.ts +102 -0
- package/extensions/checkpoint/messages.ts +170 -0
- package/extensions/checkpoint/prompt.ts +24 -0
- package/extensions/checkpoint/settings.ts +27 -0
- package/extensions/commit/commit-effort.ts +3 -3
- package/extensions/commit/commit-plan.ts +2 -2
- package/extensions/context/README.md +12 -8
- package/extensions/context/definitions.ts +178 -55
- package/extensions/context/index.ts +192 -160
- package/extensions/context/panel.ts +57 -37
- package/extensions/context/sync.ts +139 -89
- package/extensions/context/validation.ts +37 -25
- package/extensions/effort/README.md +1 -1
- package/extensions/effort/index.ts +3 -3
- package/extensions/effort/state.ts +3 -18
- package/extensions/explore/guidance.ts +6 -47
- package/extensions/explore/index.ts +38 -53
- package/extensions/explore/{ast/read → read}/hook.ts +12 -7
- package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
- package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
- package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
- package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
- package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
- package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
- package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
- package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
- package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
- package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
- package/extensions/explore/tools/target-params.ts +9 -0
- package/extensions/footer/index.ts +75 -56
- package/extensions/handoff/index.ts +8 -30
- package/extensions/image-gen/index.ts +46 -40
- package/extensions/patch/executor.ts +190 -135
- package/extensions/patch/matcher.ts +82 -48
- package/extensions/patch/parser.ts +149 -105
- package/extensions/patch/render.ts +32 -39
- package/extensions/patch/summary.ts +30 -26
- package/extensions/ready/README.md +7 -0
- package/extensions/ready/index.ts +58 -0
- package/extensions/ready/model.ts +128 -0
- package/extensions/ready/packs.ts +272 -0
- package/extensions/ready/render.ts +445 -0
- package/extensions/ready/scan.ts +760 -0
- package/extensions/reference/index.ts +2 -0
- package/extensions/reference/panel.ts +94 -31
- package/extensions/review/README.md +14 -6
- package/extensions/review/index.ts +58 -99
- package/extensions/review/model.ts +13 -33
- package/extensions/review/session.ts +9 -23
- package/extensions/runtime-context/context.ts +30 -26
- package/extensions/script-runner/README.md +2 -2
- package/extensions/script-runner/index.ts +122 -82
- package/extensions/silent-command-runner/index.ts +38 -23
- package/extensions/soul/README.md +2 -2
- package/extensions/soul/index.ts +4 -4
- package/extensions/soul/prompt.ts +6 -10
- package/extensions/soul/settings.ts +6 -3
- package/extensions/subagent/agents/context-sync.md +185 -33
- package/extensions/subagent/agents/scout.md +2 -2
- package/extensions/subagent/agents/web-research.md +2 -2
- package/extensions/subagent/agents.ts +137 -84
- package/extensions/subagent/cmux-dashboard.ts +39 -30
- package/extensions/subagent/index.ts +79 -59
- package/extensions/subagent/resume.ts +1 -1
- package/extensions/subagent/run.ts +308 -193
- package/extensions/subagent/runtime.ts +781 -343
- package/extensions/tau/index.ts +47 -38
- package/extensions/tau-help/help.md +20 -8
- package/extensions/tool-loader/README.md +4 -2
- package/extensions/tool-loader/index.ts +77 -52
- package/extensions/web/index.ts +6 -3
- package/extensions/web/webfetch.ts +114 -86
- package/package.json +3 -3
- package/schemas/tau.schema.json +38 -35
- package/shared/bounded-text-result.ts +1 -0
- package/shared/checkpoint-visibility.ts +9 -0
- package/shared/events.ts +28 -10
- package/shared/isolated-session.ts +72 -30
- package/shared/model-effort.ts +23 -22
- package/shared/model-fallback/index.ts +66 -41
- package/shared/settings/load.ts +45 -38
- package/shared/temporary-output-store.ts +16 -10
- package/{extensions/explore → src}/ast/adapter.ts +11 -2
- package/{extensions/explore → src}/ast/engine.ts +54 -38
- package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
- package/{extensions/explore → src}/ast/format/context.ts +3 -6
- package/src/ast/format/deps.ts +94 -0
- package/{extensions/explore → src}/ast/format/discover.ts +2 -2
- package/{extensions/explore → src}/ast/format/impact.ts +9 -11
- package/{extensions/explore → src}/ast/format/outline.ts +2 -2
- package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
- package/{extensions/explore → src}/ast/format/show.ts +1 -1
- package/src/ast/format/targeted-result.ts +20 -0
- package/src/ast/grammars/c_sharp.wasm +0 -0
- package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
- package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
- package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
- package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
- package/{extensions/explore → src}/ast/ir.ts +6 -0
- package/src/ast/languages/csharp-file-deps.ts +90 -0
- package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
- package/src/ast/languages/dotted-index.ts +131 -0
- package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
- package/src/ast/languages/fixtures/sample.cs +53 -0
- package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
- package/src/ast/languages/fixtures/sample.go +37 -0
- package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
- package/src/ast/languages/fixtures/sample.java +40 -0
- package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
- package/src/ast/languages/fixtures/sample.kt +31 -0
- package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
- package/src/ast/languages/fixtures/sample.md +23 -0
- package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
- package/src/ast/languages/fixtures/sample.rs +54 -0
- package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
- package/src/ast/languages/fixtures/sample.swift +45 -0
- package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
- package/src/ast/languages/fixtures/sample.ts +45 -0
- package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
- package/src/ast/languages/fixtures/sample.tsx +36 -0
- package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
- package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
- package/{extensions/explore → src}/ast/languages/go.ts +21 -3
- package/{extensions/explore → src}/ast/languages/java.ts +5 -38
- package/src/ast/languages/jvm-file-deps.ts +135 -0
- package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
- package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
- package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
- package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
- package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
- package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
- package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
- package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
- package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
- package/{extensions/explore → src}/ast/markdown.ts +72 -47
- package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
- package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
- package/{extensions/explore → src}/ast/queries/context.ts +116 -80
- package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
- package/src/ast/queries/impact.ts +291 -0
- package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
- package/{extensions/explore → src}/ast/queries/show.ts +46 -42
- package/{extensions/explore → src}/ast/query.ts +1 -1
- package/{extensions/explore → src}/ast/registry.ts +1 -2
- package/{extensions/explore → src}/ast/scan.ts +1 -1
- package/src/ast/session.ts +50 -0
- package/{extensions/explore → src}/ast/slice.ts +8 -0
- package/{extensions/explore → src/ast}/traverse.ts +32 -20
- package/src/file-injection/index.ts +547 -0
- package/src/image-generation/client.ts +50 -31
- package/src/image-generation/index.ts +177 -134
- package/src/index.ts +2 -0
- package/src/tool-loading/index.ts +58 -0
- package/extensions/context/evidence.ts +0 -517
- package/extensions/context/projection.ts +0 -141
- package/extensions/context/state.ts +0 -30
- package/extensions/context/write-scope.ts +0 -109
- package/extensions/explore/ast/format/deps.ts +0 -86
- package/extensions/explore/ast/grammars/odin.wasm +0 -0
- package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
- package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
- package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
- package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
- package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
- package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
- package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
- package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
- package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
- package/extensions/explore/ast/languages/odin.ts +0 -407
- package/extensions/explore/ast/queries/impact.ts +0 -250
- package/extensions/explore/outline-injection.ts +0 -151
- package/extensions/explore/read/autoread.ts +0 -118
- package/extensions/review/panel.ts +0 -128
- package/extensions/working-memory/README.md +0 -17
- package/extensions/working-memory/checkpoint.ts +0 -242
- package/extensions/working-memory/index.ts +0 -341
- package/extensions/working-memory/memory.ts +0 -235
- package/extensions/working-memory/render.ts +0 -185
- package/extensions/working-memory/settings.ts +0 -38
- package/extensions/working-memory/state.ts +0 -158
- package/shared/autoread.ts +0 -234
- package/shared/context-messages.ts +0 -19
- package/shared/outline-injection.ts +0 -56
- /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
- /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
- /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
- /package/{extensions/explore → src}/ast/identity.ts +0 -0
- /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
- /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`.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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([
|
|
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
|
-
.
|
|
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
|
|
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
|
-
|
|
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;
|