@shanepadgett/tau-agent 0.42.3 → 0.43.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/README.md +0 -1
- package/docs/context.md +1 -1
- package/docs/extending-tau-agent.md +1 -2
- package/extensions/attention/README.md +1 -1
- package/extensions/cache-diagnostics/index.ts +4 -0
- package/extensions/context/README.md +1 -24
- package/extensions/context/definitions.ts +1 -52
- package/extensions/context/index.ts +1 -150
- package/extensions/context/panel.ts +0 -72
- package/extensions/explore/guidance.ts +9 -1
- package/extensions/footer/README.md +1 -1
- package/extensions/footer/index.ts +3 -25
- package/extensions/qna/choice-question-body.ts +7 -2
- package/extensions/run-summary/README.md +1 -1
- package/extensions/run-summary/index.ts +18 -25
- package/extensions/runtime-context/README.md +1 -1
- package/extensions/runtime-context/context.ts +1 -1
- package/extensions/runtime-context/index.ts +10 -16
- package/extensions/silent-command-runner/README.md +0 -2
- package/extensions/silent-command-runner/index.ts +12 -41
- package/extensions/soul/README.md +5 -8
- package/extensions/soul/context.ts +115 -0
- package/extensions/soul/index.ts +141 -7
- package/extensions/soul/prompt.ts +47 -102
- package/extensions/soul/state.ts +114 -0
- package/extensions/soul/tools.ts +31 -0
- package/extensions/tau-help/help.md +6 -18
- package/extensions/tau-help/index.ts +10 -4
- package/extensions/tool-approval/README.md +2 -0
- package/extensions/tool-approval/index.ts +65 -44
- package/extensions/tool-approval/panel.ts +153 -0
- package/extensions/tool-loader/README.md +1 -1
- package/extensions/tool-loader/index.ts +91 -22
- package/package.json +2 -2
- package/schemas/tau.schema.json +0 -104
- package/shared/bounded-text-result.ts +0 -1
- package/shared/events.ts +19 -15
- package/shared/isolated-session.ts +1 -2
- package/shared/model-effort.ts +15 -21
- package/shared/prompt-contributions.ts +24 -0
- package/src/index.ts +1 -1
- package/docs/subagents.md +0 -92
- package/extensions/auto-compact/README.md +0 -9
- package/extensions/auto-compact/index.ts +0 -122
- package/extensions/auto-compact/settings.ts +0 -30
- package/extensions/context/settings.ts +0 -62
- package/extensions/context/sync.ts +0 -276
- package/extensions/context/validation.ts +0 -88
- package/extensions/effort/README.md +0 -7
- package/extensions/effort/index.ts +0 -134
- package/extensions/effort/state.ts +0 -18
- package/extensions/qna/inline-editor-row.ts +0 -56
- package/extensions/soul/overseer.ts +0 -273
- package/extensions/soul/settings.ts +0 -34
- package/extensions/subagent/README.md +0 -67
- package/extensions/subagent/agents/context-sync.md +0 -244
- package/extensions/subagent/agents/dormant/generalist.md +0 -33
- package/extensions/subagent/agents/scout.md +0 -104
- package/extensions/subagent/agents/web-research.md +0 -101
- package/extensions/subagent/agents.ts +0 -261
- package/extensions/subagent/cmux-dashboard.ts +0 -495
- package/extensions/subagent/index.ts +0 -425
- package/extensions/subagent/panel.ts +0 -124
- package/extensions/subagent/render.ts +0 -74
- package/extensions/subagent/resume.ts +0 -78
- package/extensions/subagent/run.ts +0 -641
- package/extensions/subagent/runtime.ts +0 -1296
- package/extensions/subagent/session-resource.ts +0 -61
- package/extensions/subagent/settings.ts +0 -18
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
3
|
-
import { emitTauEvent } from "../../shared/events.ts";
|
|
4
|
-
import autoCompactSettings, { DEFAULT_AUTO_COMPACT_TOKEN_LIMIT } from "./settings.ts";
|
|
5
|
-
|
|
6
|
-
const CONTINUATION_TYPE = "tau.auto-compact";
|
|
7
|
-
const CONTINUATION_MESSAGE =
|
|
8
|
-
"Continue the current work directly from the compacted context. Do not mention compaction or wait for user input.";
|
|
9
|
-
|
|
10
|
-
export default function autoCompactExtension(pi: ExtensionAPI): void {
|
|
11
|
-
let enabled = true;
|
|
12
|
-
let tokenLimit = DEFAULT_AUTO_COMPACT_TOKEN_LIMIT;
|
|
13
|
-
let armed = true;
|
|
14
|
-
let compacting = false;
|
|
15
|
-
let sessionVersion = 0;
|
|
16
|
-
let attentionHoldSequence = 0;
|
|
17
|
-
let attentionHoldId: string | undefined;
|
|
18
|
-
let silentCheckPauseId: string | undefined;
|
|
19
|
-
let continuationPending = false;
|
|
20
|
-
|
|
21
|
-
function releaseAttentionHold(disposition: "notify" | "discard"): void {
|
|
22
|
-
const holdId = attentionHoldId;
|
|
23
|
-
attentionHoldId = undefined;
|
|
24
|
-
if (holdId) emitTauEvent(pi, "tau:attention.hold.release", { id: holdId, disposition });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function pauseSilentChecks(): void {
|
|
28
|
-
if (silentCheckPauseId) return;
|
|
29
|
-
silentCheckPauseId = `auto-compact:${++attentionHoldSequence}`;
|
|
30
|
-
emitTauEvent(pi, "tau:silent-command-runner.pause", { id: silentCheckPauseId });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function resumeSilentChecks(disposition: "continue" | "settle"): void {
|
|
34
|
-
const pauseId = silentCheckPauseId;
|
|
35
|
-
silentCheckPauseId = undefined;
|
|
36
|
-
if (pauseId) emitTauEvent(pi, "tau:silent-command-runner.resume", { id: pauseId, disposition });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
40
|
-
const version = ++sessionVersion;
|
|
41
|
-
const settings = await loadTauExtensionSettings(ctx, autoCompactSettings);
|
|
42
|
-
if (version !== sessionVersion) return;
|
|
43
|
-
enabled = settings.enabled;
|
|
44
|
-
tokenLimit = settings.tokenLimit;
|
|
45
|
-
armed = true;
|
|
46
|
-
compacting = false;
|
|
47
|
-
attentionHoldSequence = 0;
|
|
48
|
-
attentionHoldId = undefined;
|
|
49
|
-
silentCheckPauseId = undefined;
|
|
50
|
-
continuationPending = false;
|
|
51
|
-
});
|
|
52
|
-
pi.on("session_shutdown", () => {
|
|
53
|
-
sessionVersion++;
|
|
54
|
-
armed = true;
|
|
55
|
-
compacting = false;
|
|
56
|
-
attentionHoldId = undefined;
|
|
57
|
-
silentCheckPauseId = undefined;
|
|
58
|
-
continuationPending = false;
|
|
59
|
-
});
|
|
60
|
-
pi.on("session_tree", () => {
|
|
61
|
-
armed = true;
|
|
62
|
-
});
|
|
63
|
-
pi.on("session_compact", () => {
|
|
64
|
-
armed = false;
|
|
65
|
-
});
|
|
66
|
-
pi.on("agent_settled", () => {
|
|
67
|
-
if (!continuationPending) return;
|
|
68
|
-
continuationPending = false;
|
|
69
|
-
releaseAttentionHold("notify");
|
|
70
|
-
});
|
|
71
|
-
pi.on("turn_start", (_event, ctx) => {
|
|
72
|
-
if (!enabled || compacting) return;
|
|
73
|
-
|
|
74
|
-
const tokens = ctx.getContextUsage()?.tokens;
|
|
75
|
-
if (tokens === undefined || tokens === null) return;
|
|
76
|
-
if (tokens < tokenLimit) {
|
|
77
|
-
armed = true;
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
if (!armed) return;
|
|
81
|
-
|
|
82
|
-
armed = false;
|
|
83
|
-
compacting = true;
|
|
84
|
-
// Pause before compact: compact aborts the agent, and that agent_end must not run checks.
|
|
85
|
-
pauseSilentChecks();
|
|
86
|
-
if (ctx.mode !== "print") {
|
|
87
|
-
attentionHoldId = `auto-compact:${++attentionHoldSequence}`;
|
|
88
|
-
emitTauEvent(pi, "tau:attention.hold.acquire", { id: attentionHoldId });
|
|
89
|
-
}
|
|
90
|
-
const version = sessionVersion;
|
|
91
|
-
const continueWork = (): void => {
|
|
92
|
-
if (version !== sessionVersion) return;
|
|
93
|
-
compacting = false;
|
|
94
|
-
// Keep the silent-check baseline across the new agent run started below.
|
|
95
|
-
resumeSilentChecks("continue");
|
|
96
|
-
continuationPending = true;
|
|
97
|
-
pi.sendMessage(
|
|
98
|
-
{
|
|
99
|
-
customType: CONTINUATION_TYPE,
|
|
100
|
-
content: CONTINUATION_MESSAGE,
|
|
101
|
-
display: false,
|
|
102
|
-
details: { v: 1, kind: "auto-compact.continuation", source: "auto-compact" },
|
|
103
|
-
},
|
|
104
|
-
{ triggerTurn: true },
|
|
105
|
-
);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
ctx.compact({
|
|
109
|
-
onComplete: continueWork,
|
|
110
|
-
onError: (error) => {
|
|
111
|
-
if (version !== sessionVersion) return;
|
|
112
|
-
compacting = false;
|
|
113
|
-
if (error.name === "AbortError" || error.message === "Compaction cancelled") {
|
|
114
|
-
resumeSilentChecks("settle");
|
|
115
|
-
releaseAttentionHold("notify");
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
continueWork();
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { Type } from "typebox";
|
|
2
|
-
import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
|
|
3
|
-
|
|
4
|
-
export const DEFAULT_AUTO_COMPACT_TOKEN_LIMIT = 175_000;
|
|
5
|
-
|
|
6
|
-
export default defineTauExtensionSettings({
|
|
7
|
-
key: "autoCompact",
|
|
8
|
-
defaults: {
|
|
9
|
-
enabled: true as boolean,
|
|
10
|
-
tokenLimit: DEFAULT_AUTO_COMPACT_TOKEN_LIMIT,
|
|
11
|
-
},
|
|
12
|
-
schema: Type.Object(
|
|
13
|
-
{
|
|
14
|
-
enabled: Type.Optional(
|
|
15
|
-
Type.Boolean({
|
|
16
|
-
default: true,
|
|
17
|
-
description: "Enable automatic compaction when context reaches the token limit.",
|
|
18
|
-
}),
|
|
19
|
-
),
|
|
20
|
-
tokenLimit: Type.Optional(
|
|
21
|
-
Type.Integer({
|
|
22
|
-
minimum: 1,
|
|
23
|
-
default: DEFAULT_AUTO_COMPACT_TOKEN_LIMIT,
|
|
24
|
-
description: "Absolute context-token count that triggers compaction before the next model turn.",
|
|
25
|
-
}),
|
|
26
|
-
),
|
|
27
|
-
},
|
|
28
|
-
{ additionalProperties: false },
|
|
29
|
-
),
|
|
30
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { Type } from "typebox";
|
|
2
|
-
import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
|
|
3
|
-
|
|
4
|
-
export default defineTauExtensionSettings({
|
|
5
|
-
key: "context",
|
|
6
|
-
defaults: {
|
|
7
|
-
sync: {
|
|
8
|
-
enabled: true as boolean,
|
|
9
|
-
automation: true as boolean,
|
|
10
|
-
},
|
|
11
|
-
validation: {
|
|
12
|
-
enabled: false as boolean,
|
|
13
|
-
ignoreGlobs: [] as string[],
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
schema: Type.Object(
|
|
17
|
-
{
|
|
18
|
-
sync: Type.Optional(
|
|
19
|
-
Type.Object(
|
|
20
|
-
{
|
|
21
|
-
enabled: Type.Optional(
|
|
22
|
-
Type.Boolean({
|
|
23
|
-
default: true,
|
|
24
|
-
description:
|
|
25
|
-
"Master switch for context-sync. When false: no /context-sync, parent cannot call the agent, validation does not auto-run sync.",
|
|
26
|
-
}),
|
|
27
|
-
),
|
|
28
|
-
automation: Type.Optional(
|
|
29
|
-
Type.Boolean({
|
|
30
|
-
default: true,
|
|
31
|
-
description:
|
|
32
|
-
"When true with sync.enabled, the coding agent may call context-sync. When false, only manual /context-sync (validation auto-run still allowed if validation.enabled).",
|
|
33
|
-
}),
|
|
34
|
-
),
|
|
35
|
-
},
|
|
36
|
-
{ additionalProperties: false },
|
|
37
|
-
),
|
|
38
|
-
),
|
|
39
|
-
validation: Type.Optional(
|
|
40
|
-
Type.Object(
|
|
41
|
-
{
|
|
42
|
-
enabled: Type.Optional(
|
|
43
|
-
Type.Boolean({
|
|
44
|
-
default: false,
|
|
45
|
-
description:
|
|
46
|
-
"Validate context membership after agent turns and auto-run context-sync on failure (requires sync.enabled).",
|
|
47
|
-
}),
|
|
48
|
-
),
|
|
49
|
-
ignoreGlobs: Type.Optional(
|
|
50
|
-
Type.Array(Type.String(), {
|
|
51
|
-
default: [],
|
|
52
|
-
description: "Project-relative files excluded from context membership validation and sync.",
|
|
53
|
-
}),
|
|
54
|
-
),
|
|
55
|
-
},
|
|
56
|
-
{ additionalProperties: false },
|
|
57
|
-
),
|
|
58
|
-
),
|
|
59
|
-
},
|
|
60
|
-
{ additionalProperties: false },
|
|
61
|
-
),
|
|
62
|
-
});
|
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
-
import { extname, join, relative, sep } from "node:path";
|
|
4
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { createGitRunner, loadRepoStatus } from "../../shared/git.ts";
|
|
6
|
-
import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
|
|
7
|
-
import { discoverAgents } from "../subagent/agents.ts";
|
|
8
|
-
import {
|
|
9
|
-
createSubagentThread,
|
|
10
|
-
disposeSubagentThread,
|
|
11
|
-
extensionPathsForTools,
|
|
12
|
-
runSubagentTurn,
|
|
13
|
-
} from "../subagent/run.ts";
|
|
14
|
-
import { pathExists } from "./definitions.ts";
|
|
15
|
-
import contextSettings from "./settings.ts";
|
|
16
|
-
import { formatContextValidationFailure, listEligibleDirtyPaths, validateContextCatalog } from "./validation.ts";
|
|
17
|
-
|
|
18
|
-
const CONTEXT_SYNC_AGENT = "context-sync";
|
|
19
|
-
|
|
20
|
-
export interface ContextSyncDetails {
|
|
21
|
-
outcome: "applied" | "no-change" | "failed" | "cancelled";
|
|
22
|
-
summary: string;
|
|
23
|
-
reason: string;
|
|
24
|
-
changedContextFiles: string[];
|
|
25
|
-
agentResponse?: string;
|
|
26
|
-
validationFailure?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type SyncStatus = (status: string) => void | Promise<void>;
|
|
30
|
-
type GitRunner = ReturnType<typeof createGitRunner>;
|
|
31
|
-
|
|
32
|
-
let syncQueue = Promise.resolve();
|
|
33
|
-
|
|
34
|
-
export async function runContextSync(
|
|
35
|
-
pi: ExtensionAPI,
|
|
36
|
-
ctx: ExtensionContext,
|
|
37
|
-
options: { nudge?: string; signal?: AbortSignal; onStatus?: SyncStatus } = {},
|
|
38
|
-
): Promise<ContextSyncDetails> {
|
|
39
|
-
return withSyncLock(() => runContextSyncLocked(pi, ctx, options));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function noChangeResult(reason: string, agentResponse?: string): ContextSyncDetails {
|
|
43
|
-
return {
|
|
44
|
-
outcome: "no-change",
|
|
45
|
-
summary: "Existing context mappings already fit the changed scope.",
|
|
46
|
-
reason,
|
|
47
|
-
changedContextFiles: [],
|
|
48
|
-
agentResponse,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function inspectSyncNeed(
|
|
53
|
-
pi: ExtensionAPI,
|
|
54
|
-
ctx: ExtensionContext,
|
|
55
|
-
nudge: string | undefined,
|
|
56
|
-
onStatus?: SyncStatus,
|
|
57
|
-
): Promise<
|
|
58
|
-
| { kind: "done"; result: ContextSyncDetails }
|
|
59
|
-
| { kind: "run"; git: GitRunner; root: string; ignoreGlobs: readonly string[] }
|
|
60
|
-
> {
|
|
61
|
-
await onStatus?.("Inspecting repository context");
|
|
62
|
-
const git = createGitRunner(pi, ctx);
|
|
63
|
-
const status = await loadRepoStatus(git);
|
|
64
|
-
if (!status) throw new Error("No Git repository found");
|
|
65
|
-
const settings = await loadTauExtensionSettings(ctx, contextSettings);
|
|
66
|
-
const ignoreGlobs = settings.validation.ignoreGlobs;
|
|
67
|
-
const validation = await validateContextCatalog(git, status.root, ignoreGlobs);
|
|
68
|
-
const dirtyEligible = await listEligibleDirtyPaths(git, status.root, ignoreGlobs);
|
|
69
|
-
const force = Boolean(nudge?.trim());
|
|
70
|
-
if (!force && validation.stale.length === 0 && validation.uncovered.length === 0 && dirtyEligible.length === 0) {
|
|
71
|
-
return {
|
|
72
|
-
kind: "done",
|
|
73
|
-
result: noChangeResult("No eligible dirty files, stale catalog paths, or sync nudge."),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
return { kind: "run", git, root: status.root, ignoreGlobs };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function resolveContextSyncDefinition(ctx: ExtensionContext) {
|
|
80
|
-
const discovery = await discoverAgents(ctx.cwd, ctx.isProjectTrusted());
|
|
81
|
-
const definition = discovery.agents.get(CONTEXT_SYNC_AGENT);
|
|
82
|
-
if (definition) return definition;
|
|
83
|
-
const reason =
|
|
84
|
-
discovery.invalid
|
|
85
|
-
.get(CONTEXT_SYNC_AGENT)
|
|
86
|
-
?.map((item) => item.reason)
|
|
87
|
-
.join("; ") ?? "unknown agent";
|
|
88
|
-
throw new Error(`Context sync agent unavailable: ${reason}`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function runContextSyncAgent(
|
|
92
|
-
pi: ExtensionAPI,
|
|
93
|
-
ctx: ExtensionContext,
|
|
94
|
-
root: string,
|
|
95
|
-
nudge: string | undefined,
|
|
96
|
-
signal: AbortSignal,
|
|
97
|
-
onStatus: SyncStatus | undefined,
|
|
98
|
-
beforeCatalog: Map<string, string>,
|
|
99
|
-
): Promise<ContextSyncDetails | { agentResponse: string }> {
|
|
100
|
-
const definition = await resolveContextSyncDefinition(ctx);
|
|
101
|
-
const task = buildContextSyncTask(root, nudge);
|
|
102
|
-
await onStatus?.("Running context-sync subagent");
|
|
103
|
-
const thread = await createSubagentThread({
|
|
104
|
-
id: `context-sync-${Date.now()}`,
|
|
105
|
-
displayName: definition.names[0] ?? definition.name,
|
|
106
|
-
definition,
|
|
107
|
-
extensionPaths: extensionPathsForTools(pi, definition.tools),
|
|
108
|
-
initialTask: task,
|
|
109
|
-
ctx,
|
|
110
|
-
thinkingLevel: pi.getThinkingLevel(),
|
|
111
|
-
signal,
|
|
112
|
-
onWarning: (warning) => {
|
|
113
|
-
ctx.ui.notify(`Context sync agent: ${warning}`, "warning");
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
try {
|
|
117
|
-
const result = await runSubagentTurn({
|
|
118
|
-
thread,
|
|
119
|
-
task,
|
|
120
|
-
initial: true,
|
|
121
|
-
signal,
|
|
122
|
-
onUpdate: async (details) => {
|
|
123
|
-
const activity = details.currentActivity ?? details.response;
|
|
124
|
-
if (activity) await onStatus?.(activity.slice(0, 160));
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
if (signal.aborted || result.details.status === "aborted") return cancelledResult(result.content);
|
|
128
|
-
if (result.details.status !== "completed") {
|
|
129
|
-
return {
|
|
130
|
-
outcome: "failed",
|
|
131
|
-
summary: "Context sync subagent failed.",
|
|
132
|
-
reason: result.details.error ?? result.content,
|
|
133
|
-
changedContextFiles: changedCatalogPaths(beforeCatalog, await catalogFileSnapshot(root)),
|
|
134
|
-
agentResponse: result.content,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
return { agentResponse: result.content };
|
|
138
|
-
} finally {
|
|
139
|
-
await disposeSubagentThread(thread);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function finalizeContextSync(
|
|
144
|
-
git: GitRunner,
|
|
145
|
-
root: string,
|
|
146
|
-
ignoreGlobs: readonly string[],
|
|
147
|
-
beforeCatalog: Map<string, string>,
|
|
148
|
-
agentResponse: string,
|
|
149
|
-
onStatus?: SyncStatus,
|
|
150
|
-
): Promise<ContextSyncDetails> {
|
|
151
|
-
const changedContextFiles = changedCatalogPaths(beforeCatalog, await catalogFileSnapshot(root));
|
|
152
|
-
await onStatus?.("Verifying context catalog");
|
|
153
|
-
const validationFailure = formatContextValidationFailure(await validateContextCatalog(git, root, ignoreGlobs));
|
|
154
|
-
if (validationFailure) {
|
|
155
|
-
return {
|
|
156
|
-
outcome: "failed",
|
|
157
|
-
summary: "Context sync finished but catalog invariants still fail.",
|
|
158
|
-
reason: validationFailure,
|
|
159
|
-
changedContextFiles,
|
|
160
|
-
agentResponse,
|
|
161
|
-
validationFailure,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
if (changedContextFiles.length === 0) {
|
|
165
|
-
return noChangeResult(agentResponse.trim() || "Context-sync subagent made no catalog edits.", agentResponse);
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
outcome: "applied",
|
|
169
|
-
summary: `Updated context catalog (${changedContextFiles.length} file${changedContextFiles.length === 1 ? "" : "s"}).`,
|
|
170
|
-
reason: agentResponse.trim() || "Context-sync subagent updated the catalog.",
|
|
171
|
-
changedContextFiles,
|
|
172
|
-
agentResponse,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function runContextSyncLocked(
|
|
177
|
-
pi: ExtensionAPI,
|
|
178
|
-
ctx: ExtensionContext,
|
|
179
|
-
options: { nudge?: string; signal?: AbortSignal; onStatus?: SyncStatus },
|
|
180
|
-
): Promise<ContextSyncDetails> {
|
|
181
|
-
if (!ctx.isProjectTrusted()) throw new Error("Context sync requires a trusted project");
|
|
182
|
-
const signal = options.signal ?? ctx.signal ?? new AbortController().signal;
|
|
183
|
-
if (signal.aborted) return cancelledResult();
|
|
184
|
-
|
|
185
|
-
const need = await inspectSyncNeed(pi, ctx, options.nudge, options.onStatus);
|
|
186
|
-
if (need.kind === "done") return need.result;
|
|
187
|
-
if (signal.aborted) return cancelledResult();
|
|
188
|
-
|
|
189
|
-
const beforeCatalog = await catalogFileSnapshot(need.root);
|
|
190
|
-
const agentResult = await runContextSyncAgent(
|
|
191
|
-
pi,
|
|
192
|
-
ctx,
|
|
193
|
-
need.root,
|
|
194
|
-
options.nudge,
|
|
195
|
-
signal,
|
|
196
|
-
options.onStatus,
|
|
197
|
-
beforeCatalog,
|
|
198
|
-
);
|
|
199
|
-
if ("outcome" in agentResult) return agentResult;
|
|
200
|
-
if (signal.aborted) return cancelledResult(agentResult.agentResponse);
|
|
201
|
-
return finalizeContextSync(
|
|
202
|
-
need.git,
|
|
203
|
-
need.root,
|
|
204
|
-
need.ignoreGlobs,
|
|
205
|
-
beforeCatalog,
|
|
206
|
-
agentResult.agentResponse,
|
|
207
|
-
options.onStatus,
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function cancelledResult(agentResponse?: string): ContextSyncDetails {
|
|
212
|
-
return {
|
|
213
|
-
outcome: "cancelled",
|
|
214
|
-
summary: "Context sync cancelled",
|
|
215
|
-
reason: agentResponse?.trim() || "Cancelled by user.",
|
|
216
|
-
changedContextFiles: [],
|
|
217
|
-
agentResponse,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function buildContextSyncTask(root: string, nudge?: string): string {
|
|
222
|
-
const trimmed = nudge?.trim();
|
|
223
|
-
return [
|
|
224
|
-
`Keep the repository context catalog at ${root} honest.`,
|
|
225
|
-
"Job: update `.pi/contexts` so work packs match the codebase.",
|
|
226
|
-
"Use normal repo tools. Prefer patch for catalog edits under `.pi/contexts`.",
|
|
227
|
-
"Walk domain → concept → job entry before placing paths. Harness verifies catalog coverage after you finish.",
|
|
228
|
-
trimmed ? `Human nudge (soft steer, does not skip the ladder or coverage):\n${trimmed}` : undefined,
|
|
229
|
-
]
|
|
230
|
-
.filter((line): line is string => Boolean(line))
|
|
231
|
-
.join("\n\n");
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function withSyncLock<T>(task: () => Promise<T>): Promise<T> {
|
|
235
|
-
const previous = syncQueue;
|
|
236
|
-
let release = () => {};
|
|
237
|
-
syncQueue = new Promise<void>((resolve) => {
|
|
238
|
-
release = resolve;
|
|
239
|
-
});
|
|
240
|
-
await previous;
|
|
241
|
-
try {
|
|
242
|
-
return await task();
|
|
243
|
-
} finally {
|
|
244
|
-
release();
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async function catalogFileSnapshot(root: string): Promise<Map<string, string>> {
|
|
249
|
-
const snapshot = new Map<string, string>();
|
|
250
|
-
const base = join(root, ".pi", "contexts");
|
|
251
|
-
if (!(await pathExists(base))) return snapshot;
|
|
252
|
-
const stack = [base];
|
|
253
|
-
while (stack.length > 0) {
|
|
254
|
-
const directory = stack.pop();
|
|
255
|
-
if (!directory) continue;
|
|
256
|
-
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
257
|
-
const fullPath = join(directory, entry.name);
|
|
258
|
-
if (entry.isDirectory()) {
|
|
259
|
-
stack.push(fullPath);
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
if (!entry.isFile() || extname(entry.name) !== ".toml") continue;
|
|
263
|
-
const relativePath = relative(root, fullPath).split(sep).join("/");
|
|
264
|
-
const content = await readFile(fullPath, "utf8");
|
|
265
|
-
snapshot.set(relativePath, createHash("sha256").update(content).digest("hex"));
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return snapshot;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function changedCatalogPaths(before: Map<string, string>, after: Map<string, string>): string[] {
|
|
272
|
-
const paths = new Set([...before.keys(), ...after.keys()]);
|
|
273
|
-
return [...paths]
|
|
274
|
-
.filter((path) => before.get(path) !== after.get(path))
|
|
275
|
-
.sort((left, right) => left.localeCompare(right));
|
|
276
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { stat } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import type { GitRunner } from "../../shared/git.ts";
|
|
4
|
-
import { contextEntryPaths, isContextEligiblePath, isSensitiveContextPath, loadContextEntries } from "./definitions.ts";
|
|
5
|
-
|
|
6
|
-
export interface ContextValidationResult {
|
|
7
|
-
stale: Array<{ path: string; ids: string[] }>;
|
|
8
|
-
uncovered: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export async function listEligibleDirtyPaths(
|
|
12
|
-
git: GitRunner,
|
|
13
|
-
root: string,
|
|
14
|
-
ignoreGlobs: readonly string[],
|
|
15
|
-
): Promise<string[]> {
|
|
16
|
-
const status = await git.run(["status", "--porcelain=v2", "-z", "--untracked-files=all"], { cwd: root });
|
|
17
|
-
return [...parseDirtyPaths(status)]
|
|
18
|
-
.filter((path) => !isSensitiveContextPath(path) && isContextEligiblePath(path, ignoreGlobs))
|
|
19
|
-
.sort((left, right) => left.localeCompare(right));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function validateContextCatalog(
|
|
23
|
-
git: GitRunner,
|
|
24
|
-
root: string,
|
|
25
|
-
ignoreGlobs: readonly string[],
|
|
26
|
-
): Promise<ContextValidationResult> {
|
|
27
|
-
const entries = await loadContextEntries(root);
|
|
28
|
-
const memberships = new Map<string, string[]>();
|
|
29
|
-
for (const entry of entries)
|
|
30
|
-
for (const file of contextEntryPaths(entry)) memberships.set(file, [...(memberships.get(file) ?? []), entry.id]);
|
|
31
|
-
|
|
32
|
-
const stale: Array<{ path: string; ids: string[] }> = [];
|
|
33
|
-
for (const [path, ids] of memberships) {
|
|
34
|
-
if (!isContextEligiblePath(path, ignoreGlobs)) continue;
|
|
35
|
-
try {
|
|
36
|
-
if (!(await stat(join(root, path))).isFile()) stale.push({ path, ids });
|
|
37
|
-
} catch {
|
|
38
|
-
stale.push({ path, ids });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const uncovered = (await listEligibleDirtyPaths(git, root, ignoreGlobs))
|
|
43
|
-
.filter((path) => !memberships.has(path))
|
|
44
|
-
.sort((left, right) => left.localeCompare(right));
|
|
45
|
-
return { stale: stale.sort((left, right) => left.path.localeCompare(right.path)), uncovered };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function formatContextValidationFailure(result: ContextValidationResult): string | undefined {
|
|
49
|
-
if (result.stale.length === 0 && result.uncovered.length === 0) return undefined;
|
|
50
|
-
const output = ["Context catalog validation failed."];
|
|
51
|
-
if (result.stale.length)
|
|
52
|
-
output.push(
|
|
53
|
-
"",
|
|
54
|
-
"Stale context file references:",
|
|
55
|
-
...result.stale.map((item) => `- ${item.path} (${item.ids.sort().join(", ")})`),
|
|
56
|
-
);
|
|
57
|
-
if (result.uncovered.length)
|
|
58
|
-
output.push("", "Changed files with no context membership:", ...result.uncovered.map((path) => `- ${path}`));
|
|
59
|
-
output.push(
|
|
60
|
-
"",
|
|
61
|
-
"With validation enabled, Tau runs context-sync automatically after agent turns. Manual: /context-sync or /context-sync <nudge>.",
|
|
62
|
-
);
|
|
63
|
-
return output.join("\n");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function parseDirtyPaths(status: string): Set<string> {
|
|
67
|
-
const records = status.split("\0");
|
|
68
|
-
const dirty = new Set<string>();
|
|
69
|
-
for (let index = 0; index < records.length; index++) {
|
|
70
|
-
const record = records[index];
|
|
71
|
-
if (!record) continue;
|
|
72
|
-
if (record.startsWith("? ")) {
|
|
73
|
-
dirty.add(record.slice(2).replaceAll("\\", "/"));
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
const fields = record.split(" ");
|
|
77
|
-
const state = fields[1] ?? "..";
|
|
78
|
-
if (record.startsWith("1 ") && !state.includes("D")) {
|
|
79
|
-
dirty.add(fields.slice(8).join(" ").replaceAll("\\", "/"));
|
|
80
|
-
} else if (record.startsWith("2 ")) {
|
|
81
|
-
if (!state.includes("D")) dirty.add(fields.slice(9).join(" ").replaceAll("\\", "/"));
|
|
82
|
-
index += 1;
|
|
83
|
-
} else if (record.startsWith("u ")) {
|
|
84
|
-
dirty.add(fields.slice(10).join(" ").replaceAll("\\", "/"));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return dirty;
|
|
88
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# Effort Extension
|
|
2
|
-
|
|
3
|
-
Switches active chat model and thinking level as one effort tier. Available providers come from current logins, and each provider can fall back through its ranked models.
|
|
4
|
-
|
|
5
|
-
Run `/effort` to choose a tier and provider. `/effort quick`, `/effort standard`, and `/effort deep` skip the tier prompt. Press `Ctrl+Shift+E` to cycle tiers on current provider.
|
|
6
|
-
|
|
7
|
-
If selected provider has no usable model for tier, current model stays active. Footer shows an effort label whenever current provider, model, and thinking level match a configured tier; otherwise it shows no label.
|