@shanepadgett/tau-agent 0.13.0 → 0.14.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/extensions/context/README.md +3 -2
- package/extensions/context/definitions.ts +19 -3
- package/extensions/context/index.ts +24 -8
- package/extensions/context/panel.ts +31 -14
- package/extensions/context/sync.ts +30 -13
- package/extensions/context/validation.ts +2 -2
- package/extensions/tau-help/help.md +1 -1
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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.
|
|
4
4
|
|
|
5
|
-
Use `/context` to select entries
|
|
5
|
+
Use `/context` to select entries. Entry `files` are injected through Tau autoread. Entry `anchors` supply lazy navigation paths that the agent can grep or read in ranges when needed. Use `/context-sync` to reconcile affected scopes from the current Git changes. Tau validates both file classes as context membership after agent turns and asks the agent to sync uncovered changed files or stale references automatically.
|
|
6
6
|
|
|
7
7
|
Validation is disabled by default. Enable it globally or per project in Tau settings:
|
|
8
8
|
|
|
@@ -30,6 +30,7 @@ files = ["src/player/input.ts"]
|
|
|
30
30
|
[movement]
|
|
31
31
|
description = "Player locomotion and collision"
|
|
32
32
|
files = ["src/player/movement.ts"]
|
|
33
|
+
anchors = ["src/runtime/fetch-handler.ts"]
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
For example, `.pi/contexts/gameplay/player.toml` appears in the `gameplay` tab as the `Player` concept with `input` and `movement` entries.
|
|
36
|
+
For example, `.pi/contexts/gameplay/player.toml` appears in the `gameplay` tab as the `Player` concept with `input` and `movement` entries. When `movement` is selected, Tau autoreads `src/player/movement.ts` and lists `src/runtime/fetch-handler.ts` as an unloaded anchor. If one selected entry marks a path as a file and another marks it as an anchor, autoread wins.
|
|
@@ -13,6 +13,7 @@ export interface ContextEntry {
|
|
|
13
13
|
name: string;
|
|
14
14
|
description: string;
|
|
15
15
|
files: string[];
|
|
16
|
+
anchors: string[];
|
|
16
17
|
path: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -36,6 +37,7 @@ const CONTEXT_IGNORED_FILENAMES = new Set([
|
|
|
36
37
|
"uv.lock",
|
|
37
38
|
"yarn.lock",
|
|
38
39
|
]);
|
|
40
|
+
const CONTEXT_ENTRY_FIELDS = new Set(["description", "files", "anchors"]);
|
|
39
41
|
|
|
40
42
|
export function isContextEligiblePath(path: string, ignoreGlobs: readonly string[] = []): boolean {
|
|
41
43
|
return (
|
|
@@ -97,6 +99,10 @@ function sortedUnique(values: readonly string[]): string[] {
|
|
|
97
99
|
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
export function contextEntryPaths(entry: Pick<ContextEntry, "files" | "anchors">): string[] {
|
|
103
|
+
return sortedUnique([...entry.files, ...entry.anchors]);
|
|
104
|
+
}
|
|
105
|
+
|
|
100
106
|
export async function requireFiles(root: string, inputs: readonly string[]): Promise<string[]> {
|
|
101
107
|
const files = sortedUnique(inputs.map((input) => normalizeProjectPath(root, input)));
|
|
102
108
|
for (const file of files) {
|
|
@@ -132,15 +138,24 @@ export async function loadContextEntries(root: string): Promise<ContextEntry[]>
|
|
|
132
138
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
133
139
|
throw new Error(`Invalid context entry: ${path} [${name}]`);
|
|
134
140
|
const record = value as Record<string, unknown>;
|
|
141
|
+
const unknownField = Object.keys(record).find((field) => !CONTEXT_ENTRY_FIELDS.has(field));
|
|
142
|
+
if (unknownField) throw new Error(`Invalid context entry field: ${path} [${name}] ${unknownField}`);
|
|
143
|
+
const anchors = record.anchors ?? [];
|
|
135
144
|
if (
|
|
136
145
|
typeof record.description !== "string" ||
|
|
137
146
|
!record.description.trim() ||
|
|
138
147
|
!Array.isArray(record.files) ||
|
|
139
|
-
record.files.
|
|
140
|
-
|
|
148
|
+
record.files.some((item) => typeof item !== "string") ||
|
|
149
|
+
!Array.isArray(anchors) ||
|
|
150
|
+
anchors.some((item) => typeof item !== "string") ||
|
|
151
|
+
(record.files.length === 0 && anchors.length === 0)
|
|
141
152
|
)
|
|
142
153
|
throw new Error(`Invalid context entry: ${path} [${name}]`);
|
|
143
154
|
const entry = validSlug(name, "Context entry");
|
|
155
|
+
const entryFiles = sortedUnique((record.files as string[]).map((item) => normalizeProjectPath(root, item)));
|
|
156
|
+
const entryAnchors = sortedUnique((anchors as string[]).map((item) => normalizeProjectPath(root, item)));
|
|
157
|
+
const overlap = entryFiles.find((item) => entryAnchors.includes(item));
|
|
158
|
+
if (overlap) throw new Error(`Context path cannot be both file and anchor: ${path} [${name}] ${overlap}`);
|
|
144
159
|
result.push({
|
|
145
160
|
id: `${tab}/${concept}/${entry}`,
|
|
146
161
|
tab,
|
|
@@ -149,7 +164,8 @@ export async function loadContextEntries(root: string): Promise<ContextEntry[]>
|
|
|
149
164
|
conceptDescription,
|
|
150
165
|
name: entry,
|
|
151
166
|
description: record.description.trim(),
|
|
152
|
-
files:
|
|
167
|
+
files: entryFiles,
|
|
168
|
+
anchors: entryAnchors,
|
|
153
169
|
path,
|
|
154
170
|
});
|
|
155
171
|
}
|
|
@@ -53,19 +53,35 @@ export default function contextExtension(pi: ExtensionAPI): void {
|
|
|
53
53
|
);
|
|
54
54
|
if (!selected?.length) return;
|
|
55
55
|
const files = [...new Set(selected.flatMap((entry) => entry.files))].sort();
|
|
56
|
+
const fileSet = new Set(files);
|
|
57
|
+
const anchors = [...new Set(selected.flatMap((entry) => entry.anchors))]
|
|
58
|
+
.filter((path) => !fileSet.has(path))
|
|
59
|
+
.sort();
|
|
56
60
|
pi.sendMessage(
|
|
57
61
|
createInjectedContext(
|
|
58
|
-
|
|
62
|
+
[
|
|
63
|
+
"Selected repository context:",
|
|
64
|
+
...selected.map((entry) => `- ${entry.id}: ${entry.description}`),
|
|
65
|
+
"",
|
|
66
|
+
"Eager snapshots supplied through autoread:",
|
|
67
|
+
...(files.length ? files.map((path) => `- ${path}`) : ["(none)"]),
|
|
68
|
+
"",
|
|
69
|
+
"Lazy navigation anchors whose contents have not been loaded:",
|
|
70
|
+
...(anchors.length ? anchors.map((path) => `- ${path}`) : ["(none)"]),
|
|
71
|
+
"",
|
|
72
|
+
"Treat eager snapshots as authoritative current project context. Do not reread them or search for coverage around them. Inspect only the anchors needed for the request, using grep or bounded reads. Explore elsewhere only when the request or concrete evidence requires missing information.",
|
|
73
|
+
].join("\n"),
|
|
59
74
|
{ source: "context", title: "Project context" },
|
|
60
75
|
),
|
|
61
76
|
);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
77
|
+
if (files.length)
|
|
78
|
+
emitTauEvent(pi, "tau:autoread.requested", {
|
|
79
|
+
source: "context",
|
|
80
|
+
title: "Project context",
|
|
81
|
+
cwd: root,
|
|
82
|
+
batchId: randomUUID(),
|
|
83
|
+
files: files.map((path) => ({ path })),
|
|
84
|
+
});
|
|
69
85
|
},
|
|
70
86
|
});
|
|
71
87
|
|
|
@@ -73,14 +73,15 @@ export class ContextPanel implements Component {
|
|
|
73
73
|
const list = this.activeList();
|
|
74
74
|
if (!list?.isFilterFocused()) {
|
|
75
75
|
const keys = getKeybindings();
|
|
76
|
-
const
|
|
77
|
-
|
|
76
|
+
const paths = this.currentPaths();
|
|
77
|
+
const pageSize = this.pathPageSize();
|
|
78
|
+
if (paths.length > pageSize && matchesKey(data, Key.alt("up"))) {
|
|
78
79
|
this.fileOffset = Math.max(0, this.fileOffset - pageSize);
|
|
79
80
|
this.sync();
|
|
80
81
|
return;
|
|
81
82
|
}
|
|
82
|
-
if (
|
|
83
|
-
this.fileOffset = Math.min(
|
|
83
|
+
if (paths.length > pageSize && matchesKey(data, Key.alt("down"))) {
|
|
84
|
+
this.fileOffset = Math.min(paths.length - pageSize, this.fileOffset + pageSize);
|
|
84
85
|
this.sync();
|
|
85
86
|
return;
|
|
86
87
|
}
|
|
@@ -122,35 +123,51 @@ export class ContextPanel implements Component {
|
|
|
122
123
|
return {
|
|
123
124
|
render: (width) => {
|
|
124
125
|
const tabs = this.tabs.render(width);
|
|
125
|
-
const
|
|
126
|
-
const
|
|
126
|
+
const paths = this.currentPaths();
|
|
127
|
+
const pageSize = this.pathPageSize(tabs.length);
|
|
128
|
+
const maxOffset = Math.max(0, paths.length - pageSize);
|
|
127
129
|
this.fileOffset = Math.min(this.fileOffset, maxOffset);
|
|
128
|
-
const
|
|
130
|
+
const visible = paths.slice(this.fileOffset, this.fileOffset + pageSize);
|
|
129
131
|
const range =
|
|
130
|
-
|
|
132
|
+
paths.length > pageSize
|
|
131
133
|
? this.theme.fg(
|
|
132
134
|
"dim",
|
|
133
|
-
`${this.fileOffset + 1}-${this.fileOffset +
|
|
135
|
+
`${this.fileOffset + 1}-${this.fileOffset + visible.length} of ${paths.length} paths`,
|
|
134
136
|
)
|
|
135
137
|
: undefined;
|
|
136
138
|
return [
|
|
137
139
|
...tabs,
|
|
138
140
|
"",
|
|
139
141
|
...(range ? [truncateToWidth(range, width, "…")] : []),
|
|
140
|
-
...
|
|
142
|
+
...visible.map(({ kind, path }) =>
|
|
143
|
+
truncateToWidth(
|
|
144
|
+
this.theme.fg(
|
|
145
|
+
kind === "read" ? "muted" : "dim",
|
|
146
|
+
`${kind === "read" ? "read" : "anchor"} • ${path}`,
|
|
147
|
+
),
|
|
148
|
+
width,
|
|
149
|
+
"…",
|
|
150
|
+
),
|
|
151
|
+
),
|
|
141
152
|
];
|
|
142
153
|
},
|
|
143
154
|
invalidate: () => this.tabs.invalidate(),
|
|
144
155
|
};
|
|
145
156
|
}
|
|
146
|
-
private
|
|
157
|
+
private currentPaths(): Array<{ kind: "read" | "anchor"; path: string }> {
|
|
158
|
+
return [
|
|
159
|
+
...this.current.files.map((path) => ({ kind: "read" as const, path })),
|
|
160
|
+
...this.current.anchors.map((path) => ({ kind: "anchor" as const, path })),
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
private pathPageSize(tabLines = this.tabs.render(this.tui.terminal.columns).length): number {
|
|
147
164
|
const overlayHeight = Math.floor(this.tui.terminal.rows * 0.8);
|
|
148
165
|
const available = Math.max(1, overlayHeight - tabLines - 7);
|
|
149
166
|
const pageSize = Math.min(8, available);
|
|
150
|
-
return this.
|
|
167
|
+
return this.currentPaths().length > pageSize ? Math.max(1, pageSize - 1) : pageSize;
|
|
151
168
|
}
|
|
152
169
|
private secondary(): string {
|
|
153
|
-
return `${[...this.selected.values()].reduce((sum, items) => sum + items.length, 0)} selected`;
|
|
170
|
+
return `${[...this.selected.values()].reduce((sum, items) => sum + items.length, 0)} selected · ${this.current.files.length} read · ${this.current.anchors.length} anchors`;
|
|
154
171
|
}
|
|
155
172
|
private hints() {
|
|
156
173
|
const list = this.activeList();
|
|
@@ -158,7 +175,7 @@ export class ContextPanel implements Component {
|
|
|
158
175
|
? list.getKeyHints()
|
|
159
176
|
: [
|
|
160
177
|
...this.tabs.getKeyHints(),
|
|
161
|
-
...(this.
|
|
178
|
+
...(this.currentPaths().length > this.pathPageSize() ? [rawHint("option+↑/↓", "scroll paths")] : []),
|
|
162
179
|
rawHint("ctrl+c", "clear all"),
|
|
163
180
|
bindingHint("tui.select.confirm", "inject"),
|
|
164
181
|
bindingHint("tui.select.cancel", "cancel"),
|
|
@@ -10,6 +10,7 @@ import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
|
10
10
|
import { truncAt } from "../../shared/text.ts";
|
|
11
11
|
import { XAI_CHAT_MODEL, XAI_PROVIDER } from "../xai/constants.ts";
|
|
12
12
|
import {
|
|
13
|
+
contextEntryPaths,
|
|
13
14
|
isContextEligiblePath,
|
|
14
15
|
isSensitiveContextPath,
|
|
15
16
|
loadContextEntries,
|
|
@@ -216,13 +217,14 @@ async function collectSyncEvidence(
|
|
|
216
217
|
for (const path of dependencies) if (!isContextEligiblePath(path, ignoreGlobs)) dependencies.delete(path);
|
|
217
218
|
const missingPaths = new Set<string>();
|
|
218
219
|
for (const entry of entries)
|
|
219
|
-
for (const path of entry
|
|
220
|
+
for (const path of contextEntryPaths(entry))
|
|
220
221
|
if (isContextEligiblePath(path, ignoreGlobs) && !(await isFile(join(root, path)))) missingPaths.add(path);
|
|
221
222
|
const affectedIds = new Set<string>();
|
|
222
223
|
for (const entry of entries) {
|
|
224
|
+
const paths = contextEntryPaths(entry);
|
|
223
225
|
if (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
paths.some((path) => missingPaths.has(path)) ||
|
|
227
|
+
paths.some(
|
|
226
228
|
(path) => files.some((file) => file.path === path || file.oldPath === path) || dependencies.has(path),
|
|
227
229
|
)
|
|
228
230
|
)
|
|
@@ -231,7 +233,7 @@ async function collectSyncEvidence(
|
|
|
231
233
|
const affectedConcepts = new Set([...affectedIds].map((id) => id.split("/").slice(0, 2).join("/")));
|
|
232
234
|
const siblingEntries = entries.filter((entry) => affectedConcepts.has(`${entry.tab}/${entry.concept}`));
|
|
233
235
|
const siblingFiles = siblingEntries
|
|
234
|
-
.flatMap((entry) => entry
|
|
236
|
+
.flatMap((entry) => contextEntryPaths(entry))
|
|
235
237
|
.filter((path) => !missingPaths.has(path) && isContextEligiblePath(path, ignoreGlobs));
|
|
236
238
|
const eligibleFiles = new Set([...dirtyExisting, ...dependencies, ...siblingFiles]);
|
|
237
239
|
const structuralPreviews = new Map<string, string>();
|
|
@@ -327,9 +329,13 @@ async function collectDirtyFiles(
|
|
|
327
329
|
sorted.slice(offset, offset + EVIDENCE_CONCURRENCY).map(async (file, inner) => ({
|
|
328
330
|
...file,
|
|
329
331
|
id: offset + inner + 1,
|
|
330
|
-
memberships: entries
|
|
332
|
+
memberships: entries
|
|
333
|
+
.filter((entry) => contextEntryPaths(entry).includes(file.path))
|
|
334
|
+
.map((entry) => entry.id),
|
|
331
335
|
oldMemberships: file.oldPath
|
|
332
|
-
? entries
|
|
336
|
+
? entries
|
|
337
|
+
.filter((entry) => contextEntryPaths(entry).includes(file.oldPath ?? ""))
|
|
338
|
+
.map((entry) => entry.id)
|
|
333
339
|
: [],
|
|
334
340
|
evidence: await dirtyEvidence(git, root, file),
|
|
335
341
|
})),
|
|
@@ -431,7 +437,9 @@ async function isFile(path: string): Promise<boolean> {
|
|
|
431
437
|
|
|
432
438
|
function buildContextSyncPrompt(evidence: SyncEvidence): string {
|
|
433
439
|
const stale = evidence.entries.flatMap((entry) =>
|
|
434
|
-
|
|
440
|
+
contextEntryPaths(entry)
|
|
441
|
+
.filter((path) => evidence.missingPaths.has(path))
|
|
442
|
+
.map((path) => `${entry.id}: ${path}`),
|
|
435
443
|
);
|
|
436
444
|
const affected = evidence.entries.filter((entry) => evidence.affectedIds.has(entry.id));
|
|
437
445
|
const previews = [...evidence.structuralPreviews].map(([path, preview]) => `${path}\n${preview}`);
|
|
@@ -441,6 +449,7 @@ function buildContextSyncPrompt(evidence: SyncEvidence): string {
|
|
|
441
449
|
"Call submit_context_sync exactly once and produce no prose response.",
|
|
442
450
|
"Return no-change only when every eligible changed file has context membership and no stale catalog paths remain.",
|
|
443
451
|
"Context entries are reusable work scopes, not inventories of every touched file. Prefer updating an existing entry over creating a near-duplicate. Do not create one entry per file.",
|
|
452
|
+
"The files field in your changes is desired membership, including eager files and lazy anchors. Paths already classified anywhere in the catalog preserve their loading class; new paths become eager files. Do not change loading policy.",
|
|
444
453
|
"Follow direct local dependency candidates only when needed. Do not add package dependencies, generated files, incidental imports, or recursive dependencies.",
|
|
445
454
|
"Reconsider granularity only inside affected concepts. Preserve broad entries when splitting would duplicate files without improving future work.",
|
|
446
455
|
"Every eligible changed file must belong to at least one entry. Remove every stale catalog path. Use only supplied candidate paths.",
|
|
@@ -457,8 +466,9 @@ function buildContextSyncPrompt(evidence: SyncEvidence): string {
|
|
|
457
466
|
conceptName: entry.conceptName,
|
|
458
467
|
conceptDescription: entry.conceptDescription,
|
|
459
468
|
description: entry.description,
|
|
460
|
-
|
|
461
|
-
|
|
469
|
+
eagerFiles: entry.files,
|
|
470
|
+
anchors: entry.anchors,
|
|
471
|
+
missingFiles: contextEntryPaths(entry).filter((path) => evidence.missingPaths.has(path)),
|
|
462
472
|
})),
|
|
463
473
|
),
|
|
464
474
|
"Changed files with no membership:",
|
|
@@ -540,7 +550,7 @@ export function normalizeContextSyncPlan(input: unknown, evidence: SyncEvidence)
|
|
|
540
550
|
throw new Error(`Entry is unrelated: ${id}`);
|
|
541
551
|
if (conceptName !== existing.conceptName || conceptDescription !== existing.conceptDescription)
|
|
542
552
|
throw new Error(`Existing concept metadata cannot change: ${conceptId}`);
|
|
543
|
-
if (description === existing.description && files.join("\0") === existing.
|
|
553
|
+
if (description === existing.description && files.join("\0") === contextEntryPaths(existing).join("\0"))
|
|
544
554
|
throw new Error(`Set-entry is identical: ${id}`);
|
|
545
555
|
} else {
|
|
546
556
|
const existingConcept = evidence.entries.find((item) => item.tab === tab && item.concept === concept);
|
|
@@ -560,7 +570,7 @@ export function normalizeContextSyncPlan(input: unknown, evidence: SyncEvidence)
|
|
|
560
570
|
}
|
|
561
571
|
changes.push({ action: "set-entry", tab, concept, conceptName, conceptDescription, entry, description, files });
|
|
562
572
|
}
|
|
563
|
-
const final = new Map(evidence.entries.map((entry) => [entry.id,
|
|
573
|
+
const final = new Map(evidence.entries.map((entry) => [entry.id, contextEntryPaths(entry)]));
|
|
564
574
|
for (const change of changes) {
|
|
565
575
|
const id = `${change.tab}/${change.concept}/${change.entry}`;
|
|
566
576
|
if (change.action === "delete-entry") final.delete(id);
|
|
@@ -571,7 +581,7 @@ export function normalizeContextSyncPlan(input: unknown, evidence: SyncEvidence)
|
|
|
571
581
|
)) {
|
|
572
582
|
const old = current.get(`${change.tab}/${change.concept}/${change.entry}`);
|
|
573
583
|
if (!old) continue;
|
|
574
|
-
for (const path of old
|
|
584
|
+
for (const path of contextEntryPaths(old))
|
|
575
585
|
if (evidence.eligibleFiles.has(path) && ![...final.values()].some((files) => files.includes(path)))
|
|
576
586
|
throw new Error(`Deleting entry would orphan surviving file: ${path}`);
|
|
577
587
|
}
|
|
@@ -640,6 +650,8 @@ export async function applyContextSyncPlan(
|
|
|
640
650
|
): Promise<ContextSyncDetails> {
|
|
641
651
|
for (const change of plan.changes) if (change.action === "set-entry") await requireFiles(root, change.files);
|
|
642
652
|
const concepts = [...new Set(plan.changes.map((change) => `${change.tab}/${change.concept}`))].sort();
|
|
653
|
+
const eagerPaths = new Set(entries.flatMap((entry) => entry.files));
|
|
654
|
+
const anchorPaths = new Set(entries.flatMap((entry) => entry.anchors).filter((path) => !eagerPaths.has(path)));
|
|
643
655
|
const outputs = new Map<string, string | undefined>();
|
|
644
656
|
for (const key of concepts) {
|
|
645
657
|
const [tab, concept] = key.split("/");
|
|
@@ -653,7 +665,12 @@ export async function applyContextSyncPlan(
|
|
|
653
665
|
else {
|
|
654
666
|
if (raw.name === undefined) raw.name = change.conceptName;
|
|
655
667
|
if (raw.description === undefined) raw.description = change.conceptDescription;
|
|
656
|
-
|
|
668
|
+
const anchors = change.files.filter((path) => anchorPaths.has(path));
|
|
669
|
+
raw[change.entry] = {
|
|
670
|
+
description: change.description,
|
|
671
|
+
files: change.files.filter((path) => !anchors.includes(path)),
|
|
672
|
+
...(anchors.length ? { anchors } : {}),
|
|
673
|
+
};
|
|
657
674
|
}
|
|
658
675
|
}
|
|
659
676
|
outputs.set(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { GitRunner } from "../../shared/git.ts";
|
|
4
|
-
import { isContextEligiblePath, isSensitiveContextPath, loadContextEntries } from "./definitions.ts";
|
|
4
|
+
import { contextEntryPaths, isContextEligiblePath, isSensitiveContextPath, loadContextEntries } from "./definitions.ts";
|
|
5
5
|
|
|
6
6
|
export interface ContextValidationResult {
|
|
7
7
|
stale: Array<{ path: string; ids: string[] }>;
|
|
@@ -16,7 +16,7 @@ export async function validateContextCatalog(
|
|
|
16
16
|
const entries = await loadContextEntries(root);
|
|
17
17
|
const memberships = new Map<string, string[]>();
|
|
18
18
|
for (const entry of entries)
|
|
19
|
-
for (const file of entry
|
|
19
|
+
for (const file of contextEntryPaths(entry)) memberships.set(file, [...(memberships.get(file) ?? []), entry.id]);
|
|
20
20
|
|
|
21
21
|
const stale: Array<{ path: string; ids: string[] }> = [];
|
|
22
22
|
for (const [path, ids] of memberships) {
|
|
@@ -28,7 +28,7 @@ Adds `/commit` for semantic commit grouping, review, and committing selected rep
|
|
|
28
28
|
|
|
29
29
|
## context
|
|
30
30
|
|
|
31
|
-
Adds `/context` to select reusable repository work scopes from `.pi/contexts`, and `/context-sync` to reconcile affected scopes from current Git changes. Context validation is disabled by default; when enabled, Tau checks changed-file membership and stale references after agent turns. Folder names are tabs, TOML files are concepts, and TOML sections are selectable entries.
|
|
31
|
+
Adds `/context` to select reusable repository work scopes from `.pi/contexts`, and `/context-sync` to reconcile affected scopes from current Git changes. Entry `files` are autoread; entry `anchors` are unloaded navigation paths the agent can inspect when needed. Context validation is disabled by default; when enabled, Tau checks both classes for changed-file membership and stale references after agent turns. Folder names are tabs, TOML files are concepts, and TOML sections are selectable entries.
|
|
32
32
|
|
|
33
33
|
## explore
|
|
34
34
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shanepadgett/tau-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Tau is a custom agentic harness built with pi extensions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@shanepadgett/tau-tui": "0.
|
|
31
|
+
"@shanepadgett/tau-tui": "0.14.0",
|
|
32
32
|
"@toon-format/toon": "2.3.0",
|
|
33
33
|
"smol-toml": "1.7.0"
|
|
34
34
|
},
|