@shanepadgett/tau-agent 0.33.1 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/docs/context.md +29 -20
  2. package/docs/extending-tau-agent.md +63 -24
  3. package/extensions/appshot/index.ts +112 -111
  4. package/extensions/aside/README.md +7 -0
  5. package/extensions/aside/index.ts +168 -0
  6. package/extensions/aside/panel.ts +83 -0
  7. package/extensions/branch/index.ts +38 -21
  8. package/extensions/cache-diagnostics/index.ts +70 -58
  9. package/extensions/checkpoint/README.md +9 -0
  10. package/extensions/checkpoint/checkpoint-budget.ts +79 -0
  11. package/extensions/checkpoint/checkpoint.ts +277 -0
  12. package/extensions/checkpoint/index.ts +102 -0
  13. package/extensions/checkpoint/messages.ts +169 -0
  14. package/extensions/checkpoint/prompt.ts +24 -0
  15. package/extensions/checkpoint/settings.ts +27 -0
  16. package/extensions/commit/commit-effort.ts +3 -3
  17. package/extensions/context/README.md +12 -8
  18. package/extensions/context/definitions.ts +178 -55
  19. package/extensions/context/index.ts +192 -160
  20. package/extensions/context/panel.ts +57 -37
  21. package/extensions/context/sync.ts +139 -89
  22. package/extensions/context/validation.ts +37 -25
  23. package/extensions/effort/README.md +1 -1
  24. package/extensions/effort/index.ts +3 -3
  25. package/extensions/effort/state.ts +3 -18
  26. package/extensions/explore/guidance.ts +6 -47
  27. package/extensions/explore/index.ts +38 -53
  28. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  29. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  30. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  31. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  32. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  33. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  34. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  35. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  36. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  37. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  38. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  39. package/extensions/explore/tools/target-params.ts +9 -0
  40. package/extensions/footer/index.ts +75 -56
  41. package/extensions/handoff/index.ts +8 -30
  42. package/extensions/image-gen/index.ts +46 -40
  43. package/extensions/patch/executor.ts +190 -135
  44. package/extensions/patch/matcher.ts +82 -48
  45. package/extensions/patch/parser.ts +149 -105
  46. package/extensions/patch/render.ts +32 -39
  47. package/extensions/patch/summary.ts +30 -26
  48. package/extensions/ready/README.md +7 -0
  49. package/extensions/ready/index.ts +58 -0
  50. package/extensions/ready/model.ts +128 -0
  51. package/extensions/ready/packs.ts +272 -0
  52. package/extensions/ready/render.ts +445 -0
  53. package/extensions/ready/scan.ts +760 -0
  54. package/extensions/reference/panel.ts +28 -16
  55. package/extensions/review/index.ts +63 -51
  56. package/extensions/review/panel.ts +5 -34
  57. package/extensions/runtime-context/context.ts +30 -26
  58. package/extensions/script-runner/README.md +2 -2
  59. package/extensions/script-runner/index.ts +122 -82
  60. package/extensions/silent-command-runner/index.ts +38 -23
  61. package/extensions/subagent/agents/context-sync.md +185 -33
  62. package/extensions/subagent/agents/scout.md +2 -2
  63. package/extensions/subagent/agents.ts +137 -84
  64. package/extensions/subagent/cmux-dashboard.ts +39 -30
  65. package/extensions/subagent/index.ts +79 -59
  66. package/extensions/subagent/resume.ts +1 -1
  67. package/extensions/subagent/run.ts +304 -193
  68. package/extensions/subagent/runtime.ts +781 -343
  69. package/extensions/tau/index.ts +47 -38
  70. package/extensions/tau-help/help.md +14 -6
  71. package/extensions/tool-loader/README.md +4 -2
  72. package/extensions/tool-loader/index.ts +77 -52
  73. package/extensions/web/index.ts +6 -3
  74. package/extensions/web/webfetch.ts +114 -86
  75. package/package.json +3 -3
  76. package/schemas/tau.schema.json +20 -33
  77. package/shared/bounded-text-result.ts +1 -0
  78. package/shared/checkpoint-visibility.ts +9 -0
  79. package/shared/events.ts +28 -10
  80. package/shared/isolated-session.ts +71 -29
  81. package/shared/model-effort.ts +9 -19
  82. package/shared/model-fallback/index.ts +66 -41
  83. package/shared/settings/load.ts +45 -38
  84. package/shared/temporary-output-store.ts +16 -10
  85. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  86. package/{extensions/explore → src}/ast/engine.ts +54 -38
  87. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  88. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  89. package/src/ast/format/deps.ts +94 -0
  90. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  91. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  92. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  93. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  94. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  95. package/src/ast/format/targeted-result.ts +20 -0
  96. package/src/ast/grammars/c_sharp.wasm +0 -0
  97. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  98. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  99. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  100. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  101. package/{extensions/explore → src}/ast/ir.ts +6 -0
  102. package/src/ast/languages/csharp-file-deps.ts +90 -0
  103. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  104. package/src/ast/languages/dotted-index.ts +131 -0
  105. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  106. package/src/ast/languages/fixtures/sample.cs +53 -0
  107. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  108. package/src/ast/languages/fixtures/sample.go +37 -0
  109. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  110. package/src/ast/languages/fixtures/sample.java +40 -0
  111. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  112. package/src/ast/languages/fixtures/sample.kt +31 -0
  113. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  114. package/src/ast/languages/fixtures/sample.md +23 -0
  115. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  116. package/src/ast/languages/fixtures/sample.rs +54 -0
  117. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  118. package/src/ast/languages/fixtures/sample.swift +45 -0
  119. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  120. package/src/ast/languages/fixtures/sample.ts +45 -0
  121. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  122. package/src/ast/languages/fixtures/sample.tsx +36 -0
  123. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  124. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  125. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  126. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  127. package/src/ast/languages/jvm-file-deps.ts +135 -0
  128. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  129. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  130. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  131. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  132. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  133. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  134. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  135. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  136. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  137. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  138. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  139. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  140. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  141. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  142. package/src/ast/queries/impact.ts +291 -0
  143. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  144. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  145. package/{extensions/explore → src}/ast/query.ts +1 -1
  146. package/{extensions/explore → src}/ast/registry.ts +1 -2
  147. package/{extensions/explore → src}/ast/scan.ts +1 -1
  148. package/src/ast/session.ts +50 -0
  149. package/{extensions/explore → src}/ast/slice.ts +8 -0
  150. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  151. package/src/file-injection/index.ts +547 -0
  152. package/src/image-generation/client.ts +50 -31
  153. package/src/image-generation/index.ts +177 -134
  154. package/src/index.ts +2 -0
  155. package/src/tool-loading/index.ts +58 -0
  156. package/extensions/context/evidence.ts +0 -517
  157. package/extensions/context/projection.ts +0 -141
  158. package/extensions/context/state.ts +0 -30
  159. package/extensions/context/write-scope.ts +0 -109
  160. package/extensions/explore/ast/format/deps.ts +0 -86
  161. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  162. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  163. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  164. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  165. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  166. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  167. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  168. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  169. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  170. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  171. package/extensions/explore/ast/languages/odin.ts +0 -407
  172. package/extensions/explore/ast/queries/impact.ts +0 -250
  173. package/extensions/explore/outline-injection.ts +0 -151
  174. package/extensions/explore/read/autoread.ts +0 -118
  175. package/extensions/working-memory/README.md +0 -17
  176. package/extensions/working-memory/checkpoint.ts +0 -242
  177. package/extensions/working-memory/index.ts +0 -341
  178. package/extensions/working-memory/memory.ts +0 -235
  179. package/extensions/working-memory/render.ts +0 -185
  180. package/extensions/working-memory/settings.ts +0 -38
  181. package/extensions/working-memory/state.ts +0 -158
  182. package/shared/autoread.ts +0 -234
  183. package/shared/context-messages.ts +0 -19
  184. package/shared/outline-injection.ts +0 -56
  185. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  186. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  187. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  188. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  189. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  190. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -0,0 +1,102 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
3
+ import { setCheckpointRowsVisible } from "../../shared/checkpoint-visibility.ts";
4
+ import { CHECKPOINT_TOOL, registerCheckpointTool } from "./checkpoint.ts";
5
+ import { createCheckpointBudget, type CheckpointBudgetNoticeLevel } from "./checkpoint-budget.ts";
6
+ import { projectContextMessages } from "./messages.ts";
7
+ import { CHECKPOINT_SYSTEM_GUIDANCE, formatCheckpointMessage } from "./prompt.ts";
8
+ import checkpointSettings from "./settings.ts";
9
+
10
+ export default function checkpointExtension(pi: ExtensionAPI): void {
11
+ const budget = createCheckpointBudget();
12
+ let checkpointSucceeded = false;
13
+
14
+ const resetBudget = (): void => {
15
+ checkpointSucceeded = false;
16
+ budget.reset();
17
+ };
18
+
19
+ registerCheckpointTool(pi);
20
+ pi.on("session_start", async (_event, ctx) => {
21
+ const settings = await loadTauExtensionSettings(ctx, checkpointSettings);
22
+ budget.configure(settings.checkpointTokenLimit);
23
+ setCheckpointRowsVisible(settings.showToolRows);
24
+ });
25
+ pi.on("session_shutdown", () => {
26
+ setCheckpointRowsVisible(false);
27
+ resetBudget();
28
+ });
29
+ pi.on("session_compact", resetBudget);
30
+ pi.on("session_tree", resetBudget);
31
+ pi.on("session_before_fork", resetBudget);
32
+ pi.on("session_before_switch", resetBudget);
33
+ pi.on("before_agent_start", (event) => ({
34
+ systemPrompt: `${event.systemPrompt}\n\n${CHECKPOINT_SYSTEM_GUIDANCE}`,
35
+ }));
36
+ pi.on("turn_start", (_event, ctx) => {
37
+ if (checkpointSucceeded) resetBudget();
38
+ const noticeLevel = budget.beginTurn(ctx.getContextUsage()?.tokens ?? null);
39
+ if (noticeLevel !== undefined) sendCheckpointBudgetNotice(pi, noticeLevel);
40
+ });
41
+ pi.on("turn_end", (_event, ctx) => {
42
+ if (checkpointSucceeded) {
43
+ resetBudget();
44
+ return;
45
+ }
46
+ const noticeLevel = budget.finishTurn(ctx.getContextUsage()?.tokens ?? null);
47
+ if (noticeLevel !== undefined) sendCheckpointBudgetNotice(pi, noticeLevel);
48
+ });
49
+ pi.on("tool_result", (event) => {
50
+ if (event.toolName === CHECKPOINT_TOOL && !event.isError) checkpointSucceeded = true;
51
+ });
52
+ pi.on("tool_call", (event) => {
53
+ if (!budget.shouldBlockTool(event.toolName, CHECKPOINT_TOOL)) return;
54
+ return {
55
+ block: true,
56
+ reason: formatCheckpointMessage(
57
+ "block",
58
+ "Checkpoint required before using other tools. Call checkpoint, then continue the user's work.",
59
+ ),
60
+ };
61
+ });
62
+ pi.on("context", (event, ctx) => ({
63
+ messages: projectContextMessages(event.messages, ctx.sessionManager.buildContextEntries()),
64
+ }));
65
+ }
66
+
67
+ function sendCheckpointBudgetNotice(pi: ExtensionAPI, level: CheckpointBudgetNoticeLevel): void {
68
+ pi.sendMessage({
69
+ customType: "tau.checkpoint",
70
+ content: checkpointBudgetMessage(level),
71
+ display: false,
72
+ details: {
73
+ v: 1,
74
+ kind: "checkpoint.checkpoint-budget",
75
+ source: "checkpoint",
76
+ level,
77
+ },
78
+ });
79
+ }
80
+
81
+ function checkpointBudgetMessage(level: CheckpointBudgetNoticeLevel): string {
82
+ switch (level) {
83
+ case 50:
84
+ return formatCheckpointMessage(
85
+ "budget",
86
+ "Context usage reached 50% of the configured checkpoint limit. Continue current work and checkpoint when convenient.",
87
+ { level: "50" },
88
+ );
89
+ case 75:
90
+ return formatCheckpointMessage(
91
+ "budget",
92
+ "Context usage reached 75% of the configured checkpoint limit. Run checkpoint soon. At 100%, non-checkpoint tool calls will be blocked until checkpoint succeeds.",
93
+ { level: "75" },
94
+ );
95
+ case 100:
96
+ return formatCheckpointMessage(
97
+ "budget",
98
+ "Checkpoint budget reached. Call checkpoint now before using any other tool. Do not call other tools.",
99
+ { level: "100" },
100
+ );
101
+ }
102
+ }
@@ -0,0 +1,169 @@
1
+ import type { AssistantMessage, TextContent, UserMessage } from "@earendil-works/pi-ai";
2
+ import { sessionEntryToContextMessages, type ContextEvent, type SessionEntry } from "@earendil-works/pi-coding-agent";
3
+ import { formatCheckpointMessage } from "./prompt.ts";
4
+
5
+ type AgentMessage = ContextEvent["messages"][number];
6
+ type ContextPair = { entry: SessionEntry; message: AgentMessage };
7
+
8
+ const CHECKPOINT_KIND = "checkpoint.checkpoint";
9
+ const MESSAGE_ID_METADATA_TYPE = "tau.checkpoint.message-id";
10
+
11
+ type CheckpointMetadataMessage = Extract<AgentMessage, { role: "custom" }>;
12
+
13
+ type CheckpointAnchor = {
14
+ call: ContextPair;
15
+ result: ContextPair;
16
+ resultIndex: number;
17
+ toolCallIds: Set<string>;
18
+ };
19
+
20
+ function pairsAlignWithMessages(pairs: readonly ContextPair[], messages: readonly AgentMessage[]): boolean {
21
+ if (pairs.length !== messages.length) return false;
22
+ return !pairs.some((pair, index) => {
23
+ const expected = messageKey(pair.message);
24
+ const actual = messageKey(messages[index]);
25
+ return expected === undefined || actual === undefined || expected !== actual;
26
+ });
27
+ }
28
+
29
+ function findCheckpointResult(
30
+ pairs: readonly ContextPair[],
31
+ ): { result: ContextPair; resultIndex: number; checkpointId: string; fileBatchId: string } | undefined {
32
+ for (let resultIndex = pairs.length - 1; resultIndex >= 0; resultIndex--) {
33
+ const result = pairs[resultIndex];
34
+ if (result.entry.type !== "message" || result.message.role !== "toolResult") continue;
35
+ const details = checkpointDetails(result.message.details);
36
+ if (details === undefined) continue;
37
+ return { result, resultIndex, checkpointId: details.checkpointId, fileBatchId: details.fileBatchId };
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ function resolveCheckpointCall(
43
+ pairs: readonly ContextPair[],
44
+ resultIndex: number,
45
+ checkpointId: string,
46
+ ): { call: ContextPair; toolCallIds: Set<string> } | "misaligned" {
47
+ let callIndex = resultIndex - 1;
48
+ while (callIndex >= 0 && !hasCheckpointToolCall(pairs[callIndex].message, checkpointId)) callIndex--;
49
+ if (callIndex < 0) return "misaligned";
50
+ const call = pairs[callIndex];
51
+ if (call.message.role !== "assistant") return "misaligned";
52
+ const toolCallIds = new Set<string>();
53
+ for (const part of call.message.content) {
54
+ if (part.type === "toolCall") toolCallIds.add(part.id);
55
+ }
56
+ return { call, toolCallIds };
57
+ }
58
+
59
+ function findCheckpointAnchor(pairs: readonly ContextPair[]): CheckpointAnchor | "misaligned" | undefined {
60
+ const found = findCheckpointResult(pairs);
61
+ if (found === undefined) return undefined;
62
+ const resolved = resolveCheckpointCall(pairs, found.resultIndex, found.checkpointId);
63
+ if (resolved === "misaligned") return "misaligned";
64
+ return {
65
+ call: resolved.call,
66
+ result: found.result,
67
+ resultIndex: found.resultIndex,
68
+ toolCallIds: resolved.toolCallIds,
69
+ };
70
+ }
71
+
72
+ function keepCheckpointPair(
73
+ checkpoint: CheckpointAnchor | undefined,
74
+ entry: SessionEntry,
75
+ message: AgentMessage,
76
+ index: number,
77
+ ): boolean {
78
+ if (checkpoint === undefined) return true;
79
+ if (index > checkpoint.resultIndex) return true;
80
+ if (entry.id === checkpoint.call.entry.id || entry.id === checkpoint.result.entry.id) return true;
81
+ return message.role === "toolResult" && checkpoint.toolCallIds.has(message.toolCallId);
82
+ }
83
+
84
+ function expandPairMessages(entry: SessionEntry, message: AgentMessage): AgentMessage[] {
85
+ if (entry.type === "message" && (entry.message.role === "user" || entry.message.role === "assistant")) {
86
+ return [createMessageIdMetadata(entry.id, message), message];
87
+ }
88
+ return [message];
89
+ }
90
+
91
+ /** Prunes completed checkpoint history and adds hidden provider-context message IDs. */
92
+ export function projectContextMessages(
93
+ messages: readonly AgentMessage[],
94
+ entries: readonly SessionEntry[],
95
+ ): AgentMessage[] {
96
+ const pairs: ContextPair[] = [];
97
+ for (const entry of entries) {
98
+ for (const message of sessionEntryToContextMessages(entry)) pairs.push({ entry, message });
99
+ }
100
+
101
+ const checkpoint = findCheckpointAnchor(pairs);
102
+ if (checkpoint === "misaligned") return [...messages];
103
+ if (checkpoint === undefined && !pairsAlignWithMessages(pairs, messages)) return [...messages];
104
+
105
+ return pairs
106
+ .filter(({ entry, message }, index) => keepCheckpointPair(checkpoint, entry, message, index))
107
+ .flatMap(({ entry, message }) => expandPairMessages(entry, message));
108
+ }
109
+
110
+ /** Extracts only the original text from a user or assistant message. */
111
+ export function extractConversationText(message: AgentMessage): string | undefined {
112
+ if (message.role === "user") return userText(message);
113
+ if (message.role === "assistant") return assistantText(message);
114
+ return undefined;
115
+ }
116
+
117
+ function messageKey(message: AgentMessage): string | undefined {
118
+ return JSON.stringify(message, (key, value) => (key === "timestamp" ? undefined : value));
119
+ }
120
+
121
+ function checkpointDetails(details: unknown): { checkpointId: string; fileBatchId: string } | undefined {
122
+ if (typeof details !== "object" || details === null) return undefined;
123
+ const value = details as Record<string, unknown>;
124
+ if (
125
+ value.v !== 1 ||
126
+ value.kind !== CHECKPOINT_KIND ||
127
+ typeof value.checkpointId !== "string" ||
128
+ typeof value.fileBatchId !== "string"
129
+ )
130
+ return undefined;
131
+ return { checkpointId: value.checkpointId, fileBatchId: value.fileBatchId };
132
+ }
133
+
134
+ function hasCheckpointToolCall(message: AgentMessage, checkpointId: string): boolean {
135
+ return (
136
+ message.role === "assistant" &&
137
+ message.content.some((part) => part.type === "toolCall" && part.name === "checkpoint" && part.id === checkpointId)
138
+ );
139
+ }
140
+
141
+ function createMessageIdMetadata(id: string, message: AgentMessage): CheckpointMetadataMessage {
142
+ const role = message.role === "user" ? "user" : "assistant";
143
+ return {
144
+ role: "custom",
145
+ customType: MESSAGE_ID_METADATA_TYPE,
146
+ content: formatCheckpointMessage(
147
+ "message-id",
148
+ `<message-id>${JSON.stringify(id)}</message-id>\n<message-role>${role}</message-role>`,
149
+ ),
150
+ display: false,
151
+ details: { v: 1, kind: "checkpoint.message-id", id, role },
152
+ timestamp: message.timestamp,
153
+ };
154
+ }
155
+
156
+ function userText(message: UserMessage): string {
157
+ if (typeof message.content === "string") return message.content;
158
+ return message.content
159
+ .filter((part): part is TextContent => part.type === "text")
160
+ .map((part) => part.text)
161
+ .join("");
162
+ }
163
+
164
+ function assistantText(message: AssistantMessage): string {
165
+ return message.content
166
+ .filter((part): part is TextContent => part.type === "text")
167
+ .map((part) => part.text)
168
+ .join("");
169
+ }
@@ -0,0 +1,24 @@
1
+ /** Standing checkpoint guidance appended to the system prompt. */
2
+ export const CHECKPOINT_SYSTEM_GUIDANCE = `## Checkpoint
3
+
4
+ Checkpoint is a hidden context-management system. It retires disposable history so long-running work can continue without user-visible ceremony.
5
+
6
+ Messages wrapped in \`<checkpoint>...</checkpoint>\` are internal control signals, not conversation with the user.
7
+
8
+ - Never acknowledge checkpoint system messages, budget notices, blocked tools, checkpoint metadata or IDs, injected checkpoint files as a transition, or context replacement.
9
+ - Never mention the checkpoint system itself.
10
+ - When checkpoint asks you to run the checkpoint tool, or when non-checkpoint tools are blocked, call \`checkpoint\` and continue the user's work immediately. Do not narrate the system.
11
+ - Stay focused on the work the user wants done.
12
+ - Use exact IDs from \`<checkpoint kind="message-id">\` metadata only in \`checkpoint.keepMessages\`.`;
13
+
14
+ /** Wraps an internal checkpoint control payload for the model. */
15
+ export function formatCheckpointMessage(
16
+ kind: string,
17
+ body: string,
18
+ attributes: Readonly<Record<string, string>> = {},
19
+ ): string {
20
+ const attrs = Object.entries(attributes)
21
+ .map(([key, value]) => ` ${key}=${JSON.stringify(value)}`)
22
+ .join("");
23
+ return `<checkpoint kind=${JSON.stringify(kind)}${attrs}>\n${body}\n</checkpoint>`;
24
+ }
@@ -0,0 +1,27 @@
1
+ import { Type } from "typebox";
2
+ import { defineTauExtensionSettings } from "../../shared/settings/define.ts";
3
+ import { DEFAULT_CHECKPOINT_TOKEN_LIMIT } from "./checkpoint-budget.ts";
4
+
5
+ export default defineTauExtensionSettings({
6
+ key: "checkpoint",
7
+ defaults: {
8
+ showToolRows: false as boolean,
9
+ checkpointTokenLimit: DEFAULT_CHECKPOINT_TOKEN_LIMIT,
10
+ },
11
+ schema: Type.Object(
12
+ {
13
+ showToolRows: Type.Optional(
14
+ Type.Boolean({
15
+ default: false,
16
+ description: "Show checkpoint and injected-file rows in the TUI for debugging.",
17
+ }),
18
+ ),
19
+ checkpointTokenLimit: Type.Integer({
20
+ minimum: 1,
21
+ default: DEFAULT_CHECKPOINT_TOKEN_LIMIT,
22
+ description: "Context-token ceiling before a checkpoint is required.",
23
+ }),
24
+ },
25
+ { additionalProperties: false },
26
+ ),
27
+ });
@@ -6,9 +6,9 @@ const LOW_EFFORT_MAX_CHANGED_LINES = 500;
6
6
  export function commitEffort(files: readonly Pick<DirtyFile, "changeSize">[]): ModelEffort {
7
7
  let changedLines = 0;
8
8
  for (const file of files) {
9
- if (file.changeSize === "binary") return "medium";
9
+ if (file.changeSize === "binary") return "standard";
10
10
  changedLines += file.changeSize;
11
- if (changedLines > LOW_EFFORT_MAX_CHANGED_LINES) return "medium";
11
+ if (changedLines > LOW_EFFORT_MAX_CHANGED_LINES) return "standard";
12
12
  }
13
- return "low";
13
+ return "quick";
14
14
  }
@@ -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;