ashlrcode 1.0.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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/package.json +46 -0
  4. package/src/__tests__/branded-types.test.ts +47 -0
  5. package/src/__tests__/context.test.ts +163 -0
  6. package/src/__tests__/cost-tracker.test.ts +274 -0
  7. package/src/__tests__/cron.test.ts +197 -0
  8. package/src/__tests__/dream.test.ts +204 -0
  9. package/src/__tests__/error-handler.test.ts +192 -0
  10. package/src/__tests__/features.test.ts +69 -0
  11. package/src/__tests__/file-history.test.ts +177 -0
  12. package/src/__tests__/hooks.test.ts +145 -0
  13. package/src/__tests__/keybindings.test.ts +159 -0
  14. package/src/__tests__/model-patches.test.ts +82 -0
  15. package/src/__tests__/permissions-rules.test.ts +121 -0
  16. package/src/__tests__/permissions.test.ts +108 -0
  17. package/src/__tests__/project-config.test.ts +63 -0
  18. package/src/__tests__/retry.test.ts +321 -0
  19. package/src/__tests__/router.test.ts +158 -0
  20. package/src/__tests__/session-compact.test.ts +191 -0
  21. package/src/__tests__/session.test.ts +145 -0
  22. package/src/__tests__/skill-registry.test.ts +130 -0
  23. package/src/__tests__/speculation.test.ts +196 -0
  24. package/src/__tests__/tasks-v2.test.ts +267 -0
  25. package/src/__tests__/telemetry.test.ts +149 -0
  26. package/src/__tests__/tool-executor.test.ts +141 -0
  27. package/src/__tests__/tool-registry.test.ts +166 -0
  28. package/src/__tests__/undercover.test.ts +93 -0
  29. package/src/__tests__/workflow.test.ts +195 -0
  30. package/src/agent/async-context.ts +64 -0
  31. package/src/agent/context.ts +245 -0
  32. package/src/agent/cron.ts +189 -0
  33. package/src/agent/dream.ts +165 -0
  34. package/src/agent/error-handler.ts +108 -0
  35. package/src/agent/ipc.ts +256 -0
  36. package/src/agent/kairos.ts +207 -0
  37. package/src/agent/loop.ts +314 -0
  38. package/src/agent/model-patches.ts +68 -0
  39. package/src/agent/speculation.ts +219 -0
  40. package/src/agent/sub-agent.ts +125 -0
  41. package/src/agent/system-prompt.ts +231 -0
  42. package/src/agent/team.ts +220 -0
  43. package/src/agent/tool-executor.ts +162 -0
  44. package/src/agent/workflow.ts +189 -0
  45. package/src/agent/worktree-manager.ts +86 -0
  46. package/src/autopilot/queue.ts +186 -0
  47. package/src/autopilot/scanner.ts +245 -0
  48. package/src/autopilot/types.ts +58 -0
  49. package/src/bridge/bridge-client.ts +57 -0
  50. package/src/bridge/bridge-server.ts +81 -0
  51. package/src/cli.ts +1120 -0
  52. package/src/config/features.ts +51 -0
  53. package/src/config/git.ts +137 -0
  54. package/src/config/hooks.ts +201 -0
  55. package/src/config/permissions.ts +251 -0
  56. package/src/config/project-config.ts +63 -0
  57. package/src/config/remote-settings.ts +163 -0
  58. package/src/config/settings-sync.ts +170 -0
  59. package/src/config/settings.ts +113 -0
  60. package/src/config/undercover.ts +76 -0
  61. package/src/config/upgrade-notice.ts +65 -0
  62. package/src/mcp/client.ts +197 -0
  63. package/src/mcp/manager.ts +125 -0
  64. package/src/mcp/oauth.ts +252 -0
  65. package/src/mcp/types.ts +61 -0
  66. package/src/persistence/memory.ts +129 -0
  67. package/src/persistence/session.ts +289 -0
  68. package/src/planning/plan-mode.ts +128 -0
  69. package/src/planning/plan-tools.ts +138 -0
  70. package/src/providers/anthropic.ts +177 -0
  71. package/src/providers/cost-tracker.ts +184 -0
  72. package/src/providers/retry.ts +264 -0
  73. package/src/providers/router.ts +159 -0
  74. package/src/providers/types.ts +79 -0
  75. package/src/providers/xai.ts +217 -0
  76. package/src/repl.tsx +1384 -0
  77. package/src/setup.ts +119 -0
  78. package/src/skills/loader.ts +78 -0
  79. package/src/skills/registry.ts +78 -0
  80. package/src/skills/types.ts +11 -0
  81. package/src/state/file-history.ts +264 -0
  82. package/src/telemetry/event-log.ts +116 -0
  83. package/src/tools/agent.ts +133 -0
  84. package/src/tools/ask-user.ts +229 -0
  85. package/src/tools/bash.ts +146 -0
  86. package/src/tools/config.ts +147 -0
  87. package/src/tools/diff.ts +137 -0
  88. package/src/tools/file-edit.ts +123 -0
  89. package/src/tools/file-read.ts +82 -0
  90. package/src/tools/file-write.ts +82 -0
  91. package/src/tools/glob.ts +76 -0
  92. package/src/tools/grep.ts +187 -0
  93. package/src/tools/ls.ts +77 -0
  94. package/src/tools/lsp.ts +375 -0
  95. package/src/tools/mcp-resources.ts +83 -0
  96. package/src/tools/mcp-tool.ts +47 -0
  97. package/src/tools/memory.ts +148 -0
  98. package/src/tools/notebook-edit.ts +133 -0
  99. package/src/tools/peers.ts +113 -0
  100. package/src/tools/powershell.ts +83 -0
  101. package/src/tools/registry.ts +114 -0
  102. package/src/tools/send-message.ts +75 -0
  103. package/src/tools/sleep.ts +50 -0
  104. package/src/tools/snip.ts +143 -0
  105. package/src/tools/tasks.ts +349 -0
  106. package/src/tools/team.ts +309 -0
  107. package/src/tools/todo-write.ts +93 -0
  108. package/src/tools/tool-search.ts +83 -0
  109. package/src/tools/types.ts +52 -0
  110. package/src/tools/web-browser.ts +263 -0
  111. package/src/tools/web-fetch.ts +118 -0
  112. package/src/tools/web-search.ts +107 -0
  113. package/src/tools/workflow.ts +188 -0
  114. package/src/tools/worktree.ts +143 -0
  115. package/src/types/branded.ts +22 -0
  116. package/src/ui/App.tsx +184 -0
  117. package/src/ui/BuddyPanel.tsx +52 -0
  118. package/src/ui/PermissionPrompt.tsx +29 -0
  119. package/src/ui/banner.ts +217 -0
  120. package/src/ui/buddy-ai.ts +108 -0
  121. package/src/ui/buddy.ts +466 -0
  122. package/src/ui/context-bar.ts +60 -0
  123. package/src/ui/effort.ts +65 -0
  124. package/src/ui/keybindings.ts +143 -0
  125. package/src/ui/markdown.ts +271 -0
  126. package/src/ui/message-renderer.ts +73 -0
  127. package/src/ui/mode.ts +80 -0
  128. package/src/ui/notifications.ts +57 -0
  129. package/src/ui/speech-bubble.ts +95 -0
  130. package/src/ui/spinner.ts +116 -0
  131. package/src/ui/theme.ts +98 -0
  132. package/src/version.ts +5 -0
  133. package/src/voice/voice-mode.ts +169 -0
@@ -0,0 +1,50 @@
1
+ /**
2
+ * SleepTool — pause the agent for a specified duration.
3
+ * Useful for polling, rate limit backoff, or waiting for external processes.
4
+ */
5
+
6
+ import type { Tool, ToolContext } from "./types.ts";
7
+
8
+ export const sleepTool: Tool = {
9
+ name: "Sleep",
10
+
11
+ prompt() {
12
+ return "Pause execution for a specified number of seconds. Use when waiting for a process to complete, implementing polling, or backing off from rate limits. Maximum 60 seconds.";
13
+ },
14
+
15
+ inputSchema() {
16
+ return {
17
+ type: "object",
18
+ properties: {
19
+ seconds: {
20
+ type: "number",
21
+ description: "Number of seconds to sleep (1-60)",
22
+ },
23
+ reason: {
24
+ type: "string",
25
+ description: "Why the agent is sleeping (displayed to user)",
26
+ },
27
+ },
28
+ required: ["seconds"],
29
+ };
30
+ },
31
+
32
+ isReadOnly() { return true; },
33
+ isDestructive() { return false; },
34
+ isConcurrencySafe() { return true; },
35
+
36
+ validateInput(input) {
37
+ const seconds = input.seconds as number;
38
+ if (!seconds || seconds < 1 || seconds > 60) {
39
+ return "seconds must be between 1 and 60";
40
+ }
41
+ return null;
42
+ },
43
+
44
+ async call(input, _context) {
45
+ const seconds = input.seconds as number;
46
+ const reason = (input.reason as string) ?? "";
47
+ await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
48
+ return `Slept for ${seconds}s${reason ? ` (${reason})` : ""}`;
49
+ },
50
+ };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * SnipTool — aggressive conversation history trimming.
3
+ * Removes stale messages, truncates verbose tool outputs, deduplicates.
4
+ */
5
+
6
+ import type { Tool, ToolContext } from "./types.ts";
7
+
8
+ // Module-level reference to history (set by cli.ts)
9
+ let _getHistory: (() => import("../providers/types.ts").Message[]) | null = null;
10
+ let _setHistory: ((msgs: import("../providers/types.ts").Message[]) => void) | null = null;
11
+
12
+ export function initSnipTool(
13
+ getHistory: () => import("../providers/types.ts").Message[],
14
+ setHistory: (msgs: import("../providers/types.ts").Message[]) => void,
15
+ ): void {
16
+ _getHistory = getHistory;
17
+ _setHistory = setHistory;
18
+ }
19
+
20
+ export const snipTool: Tool = {
21
+ name: "Snip",
22
+ prompt() {
23
+ return `Aggressively trim conversation history to free up context window space.
24
+ Actions:
25
+ - truncate: Shorten verbose tool outputs (>2000 chars) keeping first + last portions
26
+ - dedup: Remove consecutive duplicate tool results
27
+ - stale: Remove old assistant messages with no tool calls
28
+ - all: Apply all trimming strategies`;
29
+ },
30
+ inputSchema() {
31
+ return {
32
+ type: "object",
33
+ properties: {
34
+ strategy: {
35
+ type: "string",
36
+ enum: ["truncate", "dedup", "stale", "all"],
37
+ description: "Trimming strategy to apply",
38
+ },
39
+ maxOutputLength: {
40
+ type: "number",
41
+ description: "Max chars per tool output (default: 2000, for truncate strategy)",
42
+ },
43
+ },
44
+ required: ["strategy"],
45
+ };
46
+ },
47
+ isReadOnly() { return false; },
48
+ isDestructive() { return false; },
49
+ isConcurrencySafe() { return false; },
50
+ validateInput(input) {
51
+ if (!input.strategy) return "strategy required";
52
+ if (!_getHistory || !_setHistory) return "SnipTool not initialized";
53
+ return null;
54
+ },
55
+ async call(input) {
56
+ if (!_getHistory || !_setHistory) return "SnipTool not initialized";
57
+
58
+ const strategy = input.strategy as string;
59
+ const maxLen = (input.maxOutputLength as number) ?? 2000;
60
+ let history = _getHistory();
61
+ const beforeCount = history.length;
62
+ const beforeTokens = estimateTokens(history);
63
+
64
+ if (strategy === "truncate" || strategy === "all") {
65
+ history = truncateOutputs(history, maxLen);
66
+ }
67
+ if (strategy === "dedup" || strategy === "all") {
68
+ history = deduplicateResults(history);
69
+ }
70
+ if (strategy === "stale" || strategy === "all") {
71
+ history = removeStaleMessages(history);
72
+ }
73
+
74
+ _setHistory(history);
75
+ const afterTokens = estimateTokens(history);
76
+ const saved = beforeTokens - afterTokens;
77
+
78
+ return `Snipped: ${beforeCount} → ${history.length} messages, ~${saved} tokens freed`;
79
+ },
80
+ };
81
+
82
+ function estimateTokens(messages: any[]): number {
83
+ let total = 0;
84
+ for (const m of messages) {
85
+ const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
86
+ total += Math.ceil(content.length / 4);
87
+ }
88
+ return total;
89
+ }
90
+
91
+ function truncateOutputs(messages: any[], maxLen: number): any[] {
92
+ return messages.map(m => {
93
+ if (typeof m.content !== "object" || !Array.isArray(m.content)) return m;
94
+ const newContent = m.content.map((block: any) => {
95
+ if (block.type === "tool_result" && typeof block.content === "string" && block.content.length > maxLen) {
96
+ const half = Math.floor(maxLen / 2);
97
+ return {
98
+ ...block,
99
+ content: block.content.slice(0, half) +
100
+ `\n[...truncated ${block.content.length - maxLen} chars...]\n` +
101
+ block.content.slice(-half),
102
+ };
103
+ }
104
+ return block;
105
+ });
106
+ return { ...m, content: newContent };
107
+ });
108
+ }
109
+
110
+ function deduplicateResults(messages: any[]): any[] {
111
+ const result: any[] = [];
112
+ let lastToolResult = "";
113
+ for (const m of messages) {
114
+ if (typeof m.content === "object" && Array.isArray(m.content)) {
115
+ const toolResults = m.content.filter((b: any) => b.type === "tool_result");
116
+ if (toolResults.length > 0) {
117
+ const key = toolResults.map((b: any) => String(b.content).slice(0, 100)).join("|");
118
+ if (key === lastToolResult) continue; // Skip duplicate
119
+ lastToolResult = key;
120
+ }
121
+ }
122
+ result.push(m);
123
+ }
124
+ return result;
125
+ }
126
+
127
+ function removeStaleMessages(messages: any[]): any[] {
128
+ // Keep last 10 messages always, remove old short assistant messages without tool calls
129
+ if (messages.length <= 10) return messages;
130
+ const keep = messages.slice(-10);
131
+ const candidates = messages.slice(0, -10);
132
+
133
+ const filtered = candidates.filter(m => {
134
+ if (m.role !== "assistant") return true;
135
+ const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
136
+ // Keep if it contains tool_use blocks or is substantial
137
+ if (content.includes("tool_use")) return true;
138
+ if (content.length > 100) return true;
139
+ return false; // Remove short assistant messages
140
+ });
141
+
142
+ return [...filtered, ...keep];
143
+ }
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Task management tools — let the model track its own work.
3
+ *
4
+ * Tasks are persisted to disk at ~/.ashlrcode/tasks/<session-id>.json.
5
+ * Pattern from Claude Code's TaskCreate/TaskUpdate/TaskList.
6
+ */
7
+
8
+ import { existsSync } from "fs";
9
+ import { readFile, writeFile, mkdir } from "fs/promises";
10
+ import { join } from "path";
11
+ import { getConfigDir } from "../config/settings.ts";
12
+ import type { Tool, ToolContext } from "./types.ts";
13
+
14
+ type TaskSource = "u" | "a" | "t" | "k"; // user, agent, team, kairos
15
+
16
+ interface Task {
17
+ id: string;
18
+ subject: string;
19
+ description: string;
20
+ status: "pending" | "in_progress" | "completed";
21
+ createdAt: string;
22
+ source: TaskSource;
23
+ owner?: string;
24
+ blocks?: string[];
25
+ blockedBy?: string[];
26
+ completedAt?: string;
27
+ }
28
+
29
+ let tasks: Task[] = [];
30
+ let nextIdCounter = 1;
31
+ let sessionId: string | null = null;
32
+
33
+ function getTasksDir(): string {
34
+ return join(getConfigDir(), "tasks");
35
+ }
36
+
37
+ function getTasksPath(): string | null {
38
+ if (!sessionId) return null;
39
+ return join(getTasksDir(), `${sessionId}.json`);
40
+ }
41
+
42
+ async function saveTasks(): Promise<void> {
43
+ const path = getTasksPath();
44
+ if (!path) return;
45
+ await mkdir(getTasksDir(), { recursive: true });
46
+ await writeFile(path, JSON.stringify({ tasks, nextIdCounter }, null, 2), "utf-8");
47
+ }
48
+
49
+ export async function initTasks(sid: string): Promise<void> {
50
+ sessionId = sid;
51
+ const path = getTasksPath();
52
+ if (path && existsSync(path)) {
53
+ try {
54
+ const raw = await readFile(path, "utf-8");
55
+ const data = JSON.parse(raw) as { tasks: Task[]; nextIdCounter: number };
56
+ tasks = data.tasks;
57
+ nextIdCounter = data.nextIdCounter;
58
+ } catch {
59
+ tasks = [];
60
+ nextIdCounter = 1;
61
+ }
62
+ }
63
+ }
64
+
65
+ export function resetTasks() {
66
+ tasks = [];
67
+ nextIdCounter = 1;
68
+ }
69
+
70
+ export const taskCreateTool: Tool = {
71
+ name: "TaskCreate",
72
+
73
+ prompt() {
74
+ return "Create a task to track your work. Use for multi-step tasks to show progress. Tasks have a subject, description, and status.";
75
+ },
76
+
77
+ inputSchema() {
78
+ return {
79
+ type: "object",
80
+ properties: {
81
+ subject: {
82
+ type: "string",
83
+ description: "Brief title for the task",
84
+ },
85
+ description: {
86
+ type: "string",
87
+ description: "What needs to be done",
88
+ },
89
+ blockedBy: {
90
+ type: "array",
91
+ items: { type: "string" },
92
+ description: "IDs of tasks that must complete before this one (e.g. 'u-001')",
93
+ },
94
+ owner: {
95
+ type: "string",
96
+ description: "Agent name that owns this task",
97
+ },
98
+ source: {
99
+ type: "string",
100
+ enum: ["u", "a", "t", "k"],
101
+ description: "Task source: u=user, a=agent, t=team, k=kairos. Defaults to 'u'.",
102
+ },
103
+ },
104
+ required: ["subject", "description"],
105
+ };
106
+ },
107
+
108
+ isReadOnly() { return true; },
109
+ isDestructive() { return false; },
110
+ isConcurrencySafe() { return false; },
111
+
112
+ validateInput(input) {
113
+ if (!input.subject) return "subject is required";
114
+ return null;
115
+ },
116
+
117
+ async call(input, _context) {
118
+ const blockedByIds = (input.blockedBy as string[] | undefined) ?? [];
119
+ const owner = input.owner as string | undefined;
120
+ const source = (input.source as TaskSource) ?? "u";
121
+ const id = `${source}-${String(nextIdCounter++).padStart(3, "0")}`;
122
+
123
+ const task: Task = {
124
+ id,
125
+ subject: input.subject as string,
126
+ description: (input.description as string) ?? "",
127
+ status: "pending",
128
+ source,
129
+ createdAt: new Date().toISOString(),
130
+ ...(owner ? { owner } : {}),
131
+ ...(blockedByIds.length > 0 ? { blockedBy: blockedByIds } : {}),
132
+ };
133
+
134
+ // Wire up the reverse side: each blocker now blocks this task
135
+ for (const bid of blockedByIds) {
136
+ const blocker = tasks.find((t) => t.id === bid);
137
+ if (blocker) {
138
+ blocker.blocks = blocker.blocks ?? [];
139
+ if (!blocker.blocks.includes(task.id)) {
140
+ blocker.blocks.push(task.id);
141
+ }
142
+ }
143
+ }
144
+
145
+ tasks.push(task);
146
+ await saveTasks();
147
+ return `Task #${task.id} created: ${task.subject}`;
148
+ },
149
+ };
150
+
151
+ export const taskUpdateTool: Tool = {
152
+ name: "TaskUpdate",
153
+
154
+ prompt() {
155
+ return "Update a task's status. Use to mark tasks as in_progress when starting or completed when done.";
156
+ },
157
+
158
+ inputSchema() {
159
+ return {
160
+ type: "object",
161
+ properties: {
162
+ taskId: {
163
+ type: "string",
164
+ description: "ID of the task to update (e.g. 'u-001')",
165
+ },
166
+ status: {
167
+ type: "string",
168
+ enum: ["pending", "in_progress", "completed"],
169
+ description: "New status",
170
+ },
171
+ owner: {
172
+ type: "string",
173
+ description: "Agent name that owns this task",
174
+ },
175
+ addBlocks: {
176
+ type: "array",
177
+ items: { type: "string" },
178
+ description: "Task IDs that this task now blocks (e.g. 'a-002')",
179
+ },
180
+ addBlockedBy: {
181
+ type: "array",
182
+ items: { type: "string" },
183
+ description: "Task IDs that now block this task (e.g. 'u-001')",
184
+ },
185
+ },
186
+ required: ["taskId"],
187
+ };
188
+ },
189
+
190
+ isReadOnly() { return true; },
191
+ isDestructive() { return false; },
192
+ isConcurrencySafe() { return false; },
193
+
194
+ validateInput(input) {
195
+ if (!input.taskId) return "taskId is required";
196
+ return null;
197
+ },
198
+
199
+ async call(input, _context) {
200
+ const id = input.taskId as string;
201
+ const task = tasks.find((t) => t.id === id);
202
+ if (!task) return `Task #${id} not found`;
203
+
204
+ // Update status
205
+ if (input.status) {
206
+ const status = input.status as Task["status"];
207
+ task.status = status;
208
+ if (status === "completed") {
209
+ task.completedAt = new Date().toISOString();
210
+ }
211
+ }
212
+
213
+ // Update owner
214
+ if (input.owner) {
215
+ task.owner = input.owner as string;
216
+ }
217
+
218
+ // Add blocks: this task blocks the given IDs
219
+ const addBlocks = (input.addBlocks as string[] | undefined) ?? [];
220
+ for (const targetId of addBlocks) {
221
+ task.blocks = task.blocks ?? [];
222
+ if (!task.blocks.includes(targetId)) {
223
+ task.blocks.push(targetId);
224
+ }
225
+ // Wire reverse: target is now blockedBy this task
226
+ const target = tasks.find((t) => t.id === targetId);
227
+ if (target) {
228
+ target.blockedBy = target.blockedBy ?? [];
229
+ if (!target.blockedBy.includes(id)) {
230
+ target.blockedBy.push(id);
231
+ }
232
+ }
233
+ }
234
+
235
+ // Add blockedBy: this task is now blocked by the given IDs
236
+ const addBlockedBy = (input.addBlockedBy as string[] | undefined) ?? [];
237
+ for (const blockerId of addBlockedBy) {
238
+ task.blockedBy = task.blockedBy ?? [];
239
+ if (!task.blockedBy.includes(blockerId)) {
240
+ task.blockedBy.push(blockerId);
241
+ }
242
+ // Wire reverse: blocker now blocks this task
243
+ const blocker = tasks.find((t) => t.id === blockerId);
244
+ if (blocker) {
245
+ blocker.blocks = blocker.blocks ?? [];
246
+ if (!blocker.blocks.includes(id)) {
247
+ blocker.blocks.push(id);
248
+ }
249
+ }
250
+ }
251
+
252
+ await saveTasks();
253
+ return `Task #${id} updated`;
254
+ },
255
+ };
256
+
257
+ export const taskListTool: Tool = {
258
+ name: "TaskList",
259
+
260
+ prompt() {
261
+ return "List all tasks and their current status. Use to check progress and find your next task.";
262
+ },
263
+
264
+ inputSchema() {
265
+ return {
266
+ type: "object",
267
+ properties: {},
268
+ required: [],
269
+ };
270
+ },
271
+
272
+ isReadOnly() { return true; },
273
+ isDestructive() { return false; },
274
+ isConcurrencySafe() { return true; },
275
+
276
+ validateInput() { return null; },
277
+
278
+ async call(_input, _context) {
279
+ if (tasks.length === 0) return "No tasks.";
280
+
281
+ const lines = tasks.map((t) => {
282
+ const icon =
283
+ t.status === "completed"
284
+ ? "✓"
285
+ : t.status === "in_progress"
286
+ ? "●"
287
+ : "○";
288
+
289
+ // Check if blocked by any incomplete task
290
+ const isBlocked =
291
+ t.blockedBy?.some((bid) => {
292
+ const blocker = tasks.find((b) => b.id === bid);
293
+ return blocker && blocker.status !== "completed";
294
+ }) ?? false;
295
+
296
+ let line = `${icon} #${t.id} [${t.status}]${isBlocked ? " (blocked)" : ""} ${t.subject}`;
297
+
298
+ if (t.owner) line += ` @${t.owner}`;
299
+
300
+ const deps: string[] = [];
301
+ if (t.blocks?.length) deps.push(`→ blocks ${t.blocks.map((id) => `#${id}`).join(", ")}`);
302
+ if (t.blockedBy?.length) deps.push(`← blocked by ${t.blockedBy.map((id) => `#${id}`).join(", ")}`);
303
+ if (deps.length) line += ` ${deps.join(" ")}`;
304
+
305
+ return line;
306
+ });
307
+
308
+ const pending = tasks.filter((t) => t.status === "pending").length;
309
+ const inProgress = tasks.filter((t) => t.status === "in_progress").length;
310
+ const completed = tasks.filter((t) => t.status === "completed").length;
311
+
312
+ return `${lines.join("\n")}\n\n${completed}/${tasks.length} completed, ${inProgress} in progress, ${pending} pending`;
313
+ },
314
+ };
315
+
316
+ export const taskGetTool: Tool = {
317
+ name: "TaskGet",
318
+
319
+ prompt() {
320
+ return "Get full details of a specific task by ID.";
321
+ },
322
+
323
+ inputSchema() {
324
+ return {
325
+ type: "object",
326
+ properties: {
327
+ taskId: {
328
+ type: "string",
329
+ description: "ID of the task to retrieve (e.g. 'u-001')",
330
+ },
331
+ },
332
+ required: ["taskId"],
333
+ };
334
+ },
335
+
336
+ isReadOnly() { return true; },
337
+ isDestructive() { return false; },
338
+ isConcurrencySafe() { return true; },
339
+
340
+ validateInput(input) {
341
+ return input.taskId ? null : "taskId required";
342
+ },
343
+
344
+ async call(input, _context) {
345
+ const task = tasks.find((t) => t.id === (input.taskId as string));
346
+ if (!task) return `Task #${input.taskId} not found`;
347
+ return JSON.stringify(task, null, 2);
348
+ },
349
+ };