@jmylchreest/aide-plugin 0.0.39 → 0.0.42

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.
@@ -432,43 +432,90 @@ message_ack: message_id=42, agent_id="agent-auth"
432
432
  ./.aide/bin/aide memory add --category=discovery "User model needs email validation"
433
433
  ```
434
434
 
435
- **Memory** (shared discoveries):
435
+ ## OpenCode Mode
436
436
 
437
- ```bash
438
- ./.aide/bin/aide memory add --category=discovery "User model needs email validation"
439
- ```
437
+ OpenCode has native `todowrite`/`todoread` for per-agent progress tracking, and a `task` tool for spawning subagents. However, OpenCode's todos are **session-private** — they are NOT shared across agents. For multi-agent coordination, use **aide tasks** (MCP tools) as the shared task system.
440
438
 
441
- ## OpenCode Mode
439
+ ### Task System Roles (OpenCode)
442
440
 
443
- OpenCode does not have native subagent support. For multi-agent swarms with OpenCode:
441
+ | System | Role | Scope |
442
+ | ------------------------------------------------------------------------------- | --------------------------------------------------- | -------------------------- |
443
+ | **aide tasks** (MCP: `task_create`, `task_list`, `task_claim`, `task_complete`) | Shared coordination — all agents see the same board | Cross-session, persistent |
444
+ | **todowrite** (native) | Personal progress tracking within each agent | Session-private, per-agent |
445
+ | **aide messages** (MCP: `message_send`, `message_list`) | Real-time coordination, status broadcasts, blockers | Cross-session |
444
446
 
445
- **Setup:**
447
+ ### Setup
446
448
 
447
449
  1. Create worktrees as normal (one per story)
448
450
  2. Launch separate OpenCode terminal sessions, one per story
449
451
  3. Each session works in its assigned worktree directory
450
452
 
451
- **Coordination:**
453
+ ### Orchestrator Workflow
452
454
 
453
- - **No TaskList** OpenCode sessions don't share a task system
454
- - **Use aide messages** as the primary coordination mechanism:
455
- - Each session uses `message_send` to report status, blockers, and completion
456
- - Check `message_list` at each stage transition
457
- - The orchestrator monitors all agents via `message_list` with their own agent_id
458
- - **Use aide state** for progress tracking:
459
- ```bash
460
- ./.aide/bin/aide state set "agent-auth:stage" "TEST"
461
- ./.aide/bin/aide state set "agent-auth:status" "running"
462
- ```
463
- - Monitor all agents: `mcp__plugin_aide_aide__state_list`
464
-
465
- **Orchestrator role (human or primary session):**
455
+ The orchestrator (human or primary session):
466
456
 
467
457
  1. Decompose stories (use `/aide:plan-swarm` first)
468
458
  2. Create worktrees
469
- 3. Launch terminal sessions with instructions
470
- 4. Monitor via `message_list` and `state_list`
471
- 5. When all sessions report `completion`, run `/aide:worktree-resolve`
459
+ 3. Create aide tasks for all SDLC stages upfront:
460
+ ```
461
+ task_create: title="[story-auth][DESIGN] Design auth module"
462
+ task_create: title="[story-auth][TEST] Write auth tests"
463
+ task_create: title="[story-auth][DEV] Implement auth"
464
+ task_create: title="[story-auth][VERIFY] Verify auth"
465
+ task_create: title="[story-auth][DOCS] Document auth"
466
+ ```
467
+ 4. Launch terminal sessions with instructions (include agent ID and story assignment)
468
+ 5. Monitor progress via `task_list` (MCP tool) or `./.aide/bin/aide task list` (CLI)
469
+ 6. When all tasks show `done`, run `/aide:worktree-resolve`
470
+
471
+ ### Story Agent Workflow (OpenCode)
472
+
473
+ Each story agent follows the same SDLC pipeline. Use aide tasks for shared tracking and native `todowrite` for personal step-by-step progress:
474
+
475
+ ```
476
+ ## Per SDLC Stage:
477
+
478
+ 1. Claim the stage task:
479
+ task_claim: task_id=<id>, agent_id=agent-auth
480
+
481
+ 2. Use todowrite for personal tracking:
482
+ todowrite: [{"content": "Design interfaces for auth", "status": "in_progress", "priority": "high"}]
483
+
484
+ 3. Execute the stage (use appropriate /aide: skill)
485
+
486
+ 4. Complete the aide task:
487
+ task_complete: task_id=<id>, result="Designed JWT auth with refresh tokens"
488
+
489
+ 5. Send status message:
490
+ message_send: from="agent-auth", type="status", content="[DESIGN] complete"
491
+
492
+ 6. Check for messages from other agents:
493
+ message_list: agent_id="agent-auth"
494
+ ```
495
+
496
+ **Note:** aide tasks do not have `blockedBy` dependency chaining like Claude Code native tasks. Stage ordering is enforced by the SDLC pipeline instructions — each agent processes stages sequentially (DESIGN → TEST → DEV → VERIFY → DOCS).
497
+
498
+ ### Coordination (OpenCode)
499
+
500
+ ```
501
+ # Shared task board — all agents see the same tasks
502
+ task_list # View all tasks
503
+ task_list: status="pending" # View unclaimed work
504
+
505
+ # Messages — real-time coordination
506
+ message_send: from="agent-auth", type="status", content="[DESIGN] complete, starting TEST"
507
+ message_send: from="agent-auth", to="agent-payments", type="request", content="Need payment API schema"
508
+ message_list: agent_id="agent-auth"
509
+ message_ack: message_id=42, agent_id="agent-auth"
510
+
511
+ # State — supplementary progress tracking
512
+ ./.aide/bin/aide state set "agent-auth:stage" "TEST"
513
+
514
+ # Decisions and discoveries — shared knowledge
515
+ mcp__plugin_aide_aide__decision_get with topic="auth-strategy"
516
+ ./.aide/bin/aide decision set "auth-strategy" "JWT with refresh tokens"
517
+ ./.aide/bin/aide memory add --category=discovery "User model needs email validation"
518
+ ```
472
519
 
473
520
  ## Completion (MANDATORY STEPS)
474
521
 
@@ -477,7 +524,11 @@ Swarm completion checklist - ALL REQUIRED:
477
524
  ### Step 1: Verify All Stories Complete
478
525
 
479
526
  ```
527
+ # Claude Code:
480
528
  TaskList # All story tasks must show [completed]
529
+
530
+ # OpenCode:
531
+ task_list # All aide tasks must show [done]
481
532
  ```
482
533
 
483
534
  - Every story must have completed all 5 SDLC stages
package/src/cli/config.ts CHANGED
@@ -51,7 +51,10 @@ export function readConfig(configPath: string): OpenCodeConfig {
51
51
  }
52
52
  try {
53
53
  const raw = readFileSync(configPath, "utf-8");
54
- return JSON.parse(raw) as OpenCodeConfig;
54
+ const parsed: unknown = JSON.parse(raw);
55
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
56
+ return {};
57
+ return parsed as OpenCodeConfig;
55
58
  } catch {
56
59
  return {};
57
60
  }
@@ -8,7 +8,7 @@
8
8
  * platform-specific options instead of relying on CLAUDE_PLUGIN_ROOT.
9
9
  */
10
10
 
11
- import { execSync, execFileSync } from "child_process";
11
+ import { execFileSync } from "child_process";
12
12
  import { existsSync, realpathSync } from "fs";
13
13
  import { join } from "path";
14
14
  import type { FindBinaryOptions } from "./types.js";
@@ -64,7 +64,10 @@ export function findAideBinary(opts: FindBinaryOptions = {}): string | null {
64
64
 
65
65
  // 4. PATH fallback
66
66
  try {
67
- const result = execSync("which aide", { stdio: "pipe", timeout: 2000 })
67
+ const result = execFileSync("which", ["aide"], {
68
+ stdio: "pipe",
69
+ timeout: 2000,
70
+ })
68
71
  .toString()
69
72
  .trim();
70
73
  if (result) return result;
@@ -187,15 +190,15 @@ export function clearAgentState(
187
190
  }
188
191
 
189
192
  /**
190
- * Escape a string for safe shell usage (when shell is unavoidable)
193
+ * Sanitize a string for safe inclusion in log messages and CLI arguments.
194
+ *
195
+ * Strips control characters and limits length. This is NOT shell escaping —
196
+ * use execFileSync (which avoids shells entirely) for subprocess execution.
191
197
  */
192
- export function shellEscape(str: string): string {
193
- return str
194
- .replace(/\\/g, "\\\\")
195
- .replace(/"/g, '\\"')
196
- .replace(/\$/g, "\\$")
197
- .replace(/`/g, "\\`")
198
- .replace(/!/g, "\\!")
199
- .replace(/\n/g, " ")
200
- .slice(0, 1000);
198
+ export function sanitizeForLog(str: string): string {
199
+ // eslint-disable-next-line no-control-regex
200
+ return str.replace(/[\x00-\x1f\x7f]/g, " ").slice(0, 1000);
201
201
  }
202
+
203
+ /** @deprecated Use sanitizeForLog instead */
204
+ export const shellEscape = sanitizeForLog;
@@ -157,10 +157,20 @@ function readAideConfig(path: string): Record<string, CanonicalMcpServer> {
157
157
  if (!existsSync(path)) return {};
158
158
 
159
159
  try {
160
- const raw = JSON.parse(readFileSync(path, "utf-8")) as AideMcpConfig;
160
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf-8"));
161
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
162
+ return {};
163
+ const raw = parsed as AideMcpConfig;
164
+ const mcpServers = raw.mcpServers;
165
+ if (
166
+ typeof mcpServers !== "object" ||
167
+ mcpServers === null ||
168
+ Array.isArray(mcpServers)
169
+ )
170
+ return {};
161
171
  const servers: Record<string, CanonicalMcpServer> = {};
162
172
 
163
- for (const [name, def] of Object.entries(raw.mcpServers || {})) {
173
+ for (const [name, def] of Object.entries(mcpServers)) {
164
174
  servers[name] = { name, ...def };
165
175
  }
166
176
 
@@ -444,7 +454,13 @@ function readJournal(path: string): McpSyncJournal {
444
454
  }
445
455
 
446
456
  try {
447
- return JSON.parse(readFileSync(path, "utf-8")) as McpSyncJournal;
457
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf-8"));
458
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
459
+ return { entries: [], removed: [] };
460
+ const journal = parsed as McpSyncJournal;
461
+ if (!Array.isArray(journal.entries)) return { entries: [], removed: [] };
462
+ if (!Array.isArray(journal.removed)) journal.removed = [];
463
+ return journal;
448
464
  } catch {
449
465
  return { entries: [], removed: [] };
450
466
  }
@@ -105,7 +105,7 @@ export function buildPartialTags(
105
105
  ): string[] {
106
106
  const tags = [
107
107
  "partial",
108
- `session:${sessionId.slice(0, 8)}`,
108
+ `session:${sessionId.slice(0, 12)}`,
109
109
  `tool:${info.toolName.toLowerCase()}`,
110
110
  ];
111
111
  if (info.filePath) {
@@ -152,19 +152,28 @@ export function storePartialMemory(
152
152
  }
153
153
  }
154
154
 
155
+ /** Shape returned by `aide memory list --format=json`. */
156
+ interface PartialMemoryEntry {
157
+ id: string;
158
+ tags: string[];
159
+ content: string;
160
+ }
161
+
155
162
  /**
156
- * Gather all partial memories for a session.
163
+ * Query session partials and map to a caller-chosen type.
157
164
  *
158
- * Uses `aide memory list` with tag filtering to find all partials.
159
- * Returns the raw output or null if none found.
165
+ * Runs `aide memory list --tags=partial --format=json`, filters to the
166
+ * given session, and maps each match through `mapFn`.
160
167
  */
161
- export function gatherPartials(
168
+ function querySessionPartials<T>(
162
169
  binary: string,
163
170
  cwd: string,
164
171
  sessionId: string,
165
- ): string[] {
172
+ mapFn: (m: PartialMemoryEntry) => T,
173
+ label: string,
174
+ ): T[] {
166
175
  try {
167
- const sessionTag = `session:${sessionId.slice(0, 8)}`;
176
+ const sessionTag = `session:${sessionId.slice(0, 12)}`;
168
177
 
169
178
  const output = execFileSync(
170
179
  binary,
@@ -186,23 +195,34 @@ export function gatherPartials(
186
195
 
187
196
  if (!output || output === "[]") return [];
188
197
 
189
- interface PartialMemory {
190
- id: string;
191
- tags: string[];
192
- content: string;
193
- }
194
-
195
- const memories: PartialMemory[] = JSON.parse(output);
196
- // Filter to this session's partials
197
- return memories
198
- .filter((m) => m.tags?.includes(sessionTag))
199
- .map((m) => m.content);
198
+ const memories: PartialMemoryEntry[] = JSON.parse(output);
199
+ return memories.filter((m) => m.tags?.includes(sessionTag)).map(mapFn);
200
200
  } catch (err) {
201
- debug(SOURCE, `Failed to gather partials: ${err}`);
201
+ debug(SOURCE, `Failed to gather ${label}: ${err}`);
202
202
  return [];
203
203
  }
204
204
  }
205
205
 
206
+ /**
207
+ * Gather all partial memories for a session.
208
+ *
209
+ * Uses `aide memory list` with tag filtering to find all partials.
210
+ * Returns the content strings or an empty array if none found.
211
+ */
212
+ export function gatherPartials(
213
+ binary: string,
214
+ cwd: string,
215
+ sessionId: string,
216
+ ): string[] {
217
+ return querySessionPartials(
218
+ binary,
219
+ cwd,
220
+ sessionId,
221
+ (m) => m.content,
222
+ "partials",
223
+ );
224
+ }
225
+
206
226
  /**
207
227
  * Gather partial memory IDs for a session (for cleanup).
208
228
  */
@@ -211,42 +231,13 @@ export function gatherPartialIds(
211
231
  cwd: string,
212
232
  sessionId: string,
213
233
  ): string[] {
214
- try {
215
- const sessionTag = `session:${sessionId.slice(0, 8)}`;
216
-
217
- const output = execFileSync(
218
- binary,
219
- [
220
- "memory",
221
- "list",
222
- "--tags=partial",
223
- "--all",
224
- "--format=json",
225
- "--limit=500",
226
- ],
227
- {
228
- cwd,
229
- encoding: "utf-8",
230
- stdio: ["pipe", "pipe", "pipe"],
231
- timeout: 5000,
232
- },
233
- ).trim();
234
-
235
- if (!output || output === "[]") return [];
236
-
237
- interface PartialMemory {
238
- id: string;
239
- tags: string[];
240
- }
241
-
242
- const memories: PartialMemory[] = JSON.parse(output);
243
- return memories
244
- .filter((m) => m.tags?.includes(sessionTag))
245
- .map((m) => m.id);
246
- } catch (err) {
247
- debug(SOURCE, `Failed to gather partial IDs: ${err}`);
248
- return [];
249
- }
234
+ return querySessionPartials(
235
+ binary,
236
+ cwd,
237
+ sessionId,
238
+ (m) => m.id,
239
+ "partial IDs",
240
+ );
250
241
  }
251
242
 
252
243
  /**
@@ -74,17 +74,25 @@ export function buildReinforcement(
74
74
  * Returns null if stop is allowed, or { reason } if stop should be blocked.
75
75
  * When a persistence mode is active and todos exist, the reinforcement
76
76
  * message includes the specific incomplete tasks.
77
+ *
78
+ * When agentId is provided, only tasks claimed by that agent are considered
79
+ * for blocking. This prevents subagents from being blocked by tasks that
80
+ * belong to other agents. Global (unclaimed) tasks still count for all agents.
77
81
  */
78
82
  export function checkPersistence(
79
83
  binary: string,
80
84
  cwd: string,
85
+ agentId?: string,
81
86
  ): { reason: string } | null {
82
87
  const mode = getActiveMode(binary, cwd);
83
88
  if (!mode) return null;
84
89
 
85
- // Get and increment iteration counter
90
+ // Get and increment iteration counter (guard against NaN from corrupted state).
91
+ // NOTE: read-then-write is not atomic, but concurrent Stop events are extremely
92
+ // rare in practice. The counter is a safety cap, not a precise meter.
86
93
  const iterStr = getState(binary, cwd, `${mode}_iterations`) || "0";
87
- const iteration = parseInt(iterStr, 10) + 1;
94
+ const parsed = parseInt(iterStr, 10);
95
+ const iteration = (Number.isNaN(parsed) ? 0 : parsed) + 1;
88
96
  setState(binary, cwd, `${mode}_iterations`, String(iteration));
89
97
 
90
98
  if (iteration > MAX_PERSISTENCE_ITERATIONS) {
@@ -94,21 +102,37 @@ export function checkPersistence(
94
102
  return null;
95
103
  }
96
104
 
97
- // Fetch todos and build a specific continuation message if incomplete tasks exist
105
+ // Fetch todos and build a specific continuation message if incomplete tasks exist.
106
+ // If all tasks are complete (or no tasks exist), auto-release: allow stop.
98
107
  let todoSummary: string | undefined;
108
+ let allTasksComplete = false;
99
109
  try {
100
110
  const todos = fetchTodosFromAide(binary, cwd);
101
- const todoResult = checkTodos(todos);
111
+ const todoResult = checkTodos(todos, agentId);
102
112
  if (todoResult.hasIncomplete) {
103
113
  todoSummary = todoResult.message;
104
114
  debug(
105
115
  SOURCE,
106
116
  `Found ${todoResult.incompleteCount} incomplete todos for persistence reinforcement`,
107
117
  );
118
+ } else if (todoResult.totalCount > 0) {
119
+ // All tasks exist and are in terminal states — work is done
120
+ allTasksComplete = true;
121
+ debug(
122
+ SOURCE,
123
+ `All ${todoResult.totalCount} tasks complete — auto-releasing ${mode} mode`,
124
+ );
108
125
  }
109
126
  } catch (err) {
110
127
  debug(SOURCE, `Failed to fetch todos for persistence (non-fatal): ${err}`);
111
128
  }
112
129
 
130
+ // Auto-release: if tasks exist and all are complete, allow stop
131
+ if (allTasksComplete) {
132
+ setState(binary, cwd, "mode", "");
133
+ setState(binary, cwd, `${mode}_iterations`, "0");
134
+ return null;
135
+ }
136
+
113
137
  return { reason: buildReinforcement(mode, iteration, todoSummary) };
114
138
  }
@@ -16,7 +16,7 @@ import {
16
16
  statSync,
17
17
  } from "fs";
18
18
  import { join } from "path";
19
- import { execSync, execFileSync } from "child_process";
19
+ import { execFileSync } from "child_process";
20
20
  import { homedir } from "os";
21
21
  import type {
22
22
  AideConfig,
@@ -138,11 +138,15 @@ config/mcp-sync.journal.json
138
138
  */
139
139
  export function getProjectName(cwd: string): string {
140
140
  try {
141
- const remoteUrl = execSync("git config --get remote.origin.url", {
142
- cwd,
143
- stdio: ["pipe", "pipe", "pipe"],
144
- timeout: 2000,
145
- })
141
+ const remoteUrl = execFileSync(
142
+ "git",
143
+ ["config", "--get", "remote.origin.url"],
144
+ {
145
+ cwd,
146
+ stdio: ["pipe", "pipe", "pipe"],
147
+ timeout: 2000,
148
+ },
149
+ )
146
150
  .toString()
147
151
  .trim();
148
152
 
@@ -76,7 +76,14 @@ export function buildSessionSummary(
76
76
  const entries: TranscriptEntry[] = [];
77
77
  for (const line of lines) {
78
78
  try {
79
- entries.push(JSON.parse(line) as TranscriptEntry);
79
+ const parsed: unknown = JSON.parse(line);
80
+ if (
81
+ typeof parsed === "object" &&
82
+ parsed !== null &&
83
+ !Array.isArray(parsed)
84
+ ) {
85
+ entries.push(parsed as TranscriptEntry);
86
+ }
80
87
  } catch {
81
88
  // Skip malformed
82
89
  }
@@ -214,7 +221,7 @@ export function storeSessionSummary(
214
221
  summary: string,
215
222
  ): boolean {
216
223
  try {
217
- const tags = `session-summary,session:${sessionId.slice(0, 8)}`;
224
+ const tags = `session-summary,session:${sessionId.slice(0, 12)}`;
218
225
 
219
226
  execFileSync(
220
227
  binary,
@@ -1,17 +1,18 @@
1
1
  /**
2
2
  * Todo continuation checker — platform-agnostic.
3
3
  *
4
- * Reads the agent's todo list and checks for incomplete items.
4
+ * Reads aide tasks (`aide task list`) and checks for incomplete items.
5
5
  * Used to enhance persistence-logic.ts with precise todo-aware blocking:
6
6
  * instead of a generic "verify your work is complete", we list the
7
- * specific incomplete todos.
7
+ * specific incomplete tasks.
8
8
  *
9
- * For Claude Code: reads todos from the transcript (TodoWrite tool outputs)
10
- * or from aide task list.
11
- * For OpenCode: reads todos via client.session.todo() API.
9
+ * Only checks aide tasks (persistent, cross-session). Native todos
10
+ * (Claude Code TodoWrite, OpenCode todowrite) are session-scoped
11
+ * personal tracking and are intentionally not checked here — neither
12
+ * platform exposes an API for reading them from hooks.
12
13
  *
13
14
  * This module provides the platform-agnostic core. Platform hooks
14
- * call it with however they obtained the todo list.
15
+ * call it via persistence-logic.ts.
15
16
  */
16
17
 
17
18
  import { runAide } from "./aide-client.js";
@@ -19,10 +20,24 @@ import { debug } from "../lib/logger.js";
19
20
 
20
21
  const SOURCE = "todo-checker";
21
22
 
23
+ /**
24
+ * Known terminal statuses — tasks in these states are considered "done".
25
+ * Any status NOT in this set is treated as incomplete (including unknown
26
+ * statuses from future aide versions), which is the safe default for
27
+ * persistence enforcement.
28
+ *
29
+ * Covers both aide backend statuses (done) and any legacy/alias statuses
30
+ * (completed, cancelled) for forward/backward compatibility.
31
+ */
32
+ export const TERMINAL_STATUSES = new Set(["done", "completed", "cancelled"]);
33
+
22
34
  export interface TodoItem {
23
35
  id: string;
24
36
  content: string;
25
- status: "pending" | "in_progress" | "completed" | "cancelled";
37
+ /** Raw status string from aide may be any value, not just known ones. */
38
+ status: string;
39
+ /** Agent that claimed this task, if any. */
40
+ claimedBy?: string;
26
41
  priority?: string;
27
42
  }
28
43
 
@@ -41,8 +56,16 @@ export interface TodoCheckResult {
41
56
 
42
57
  /**
43
58
  * Check a list of todos for incomplete items and build a continuation message.
59
+ *
60
+ * When agentId is provided, only tasks relevant to this agent are considered:
61
+ * - Unclaimed tasks (pending, blocked) — everyone's responsibility
62
+ * - Tasks claimed by this agent — this agent's responsibility
63
+ * Tasks claimed by other agents are filtered out.
44
64
  */
45
- export function checkTodos(todos: TodoItem[]): TodoCheckResult {
65
+ export function checkTodos(
66
+ todos: TodoItem[],
67
+ agentId?: string,
68
+ ): TodoCheckResult {
46
69
  if (!todos || todos.length === 0) {
47
70
  return {
48
71
  hasIncomplete: false,
@@ -53,29 +76,34 @@ export function checkTodos(todos: TodoItem[]): TodoCheckResult {
53
76
  };
54
77
  }
55
78
 
56
- const incomplete = todos.filter(
57
- (t) => t.status !== "completed" && t.status !== "cancelled",
58
- );
79
+ // When scoped to a specific agent, filter out tasks claimed by other agents.
80
+ // Unclaimed tasks (no claimedBy) are considered everyone's responsibility.
81
+ const relevant = agentId
82
+ ? todos.filter((t) => !t.claimedBy || t.claimedBy === agentId)
83
+ : todos;
84
+
85
+ const incomplete = relevant.filter((t) => !TERMINAL_STATUSES.has(t.status));
59
86
 
60
87
  if (incomplete.length === 0) {
61
88
  return {
62
89
  hasIncomplete: false,
63
90
  incompleteCount: 0,
64
- totalCount: todos.length,
91
+ totalCount: relevant.length,
65
92
  incompleteItems: [],
66
93
  message: "",
67
94
  };
68
95
  }
69
96
 
70
- const completedCount = todos.length - incomplete.length;
97
+ const completedCount = relevant.length - incomplete.length;
71
98
  const lines: string[] = [
72
- `**TODO CONTINUATION** — ${incomplete.length} of ${todos.length} tasks incomplete (${completedCount} done)`,
99
+ `**TODO CONTINUATION** — ${incomplete.length} of ${relevant.length} tasks incomplete (${completedCount} done)`,
73
100
  "",
74
101
  "Remaining tasks:",
75
102
  ];
76
103
 
77
104
  for (const item of incomplete) {
78
- const statusIcon = item.status === "in_progress" ? ">" : " ";
105
+ const statusIcon =
106
+ item.status === "in_progress" || item.status === "claimed" ? ">" : " ";
79
107
  lines.push(` [${statusIcon}] ${item.content}`);
80
108
  }
81
109
 
@@ -87,7 +115,7 @@ export function checkTodos(todos: TodoItem[]): TodoCheckResult {
87
115
  return {
88
116
  hasIncomplete: true,
89
117
  incompleteCount: incomplete.length,
90
- totalCount: todos.length,
118
+ totalCount: relevant.length,
91
119
  incompleteItems: incomplete,
92
120
  message: lines.join("\n"),
93
121
  };
@@ -99,6 +127,12 @@ export function checkTodos(todos: TodoItem[]): TodoCheckResult {
99
127
  * Output format from `aide task list`:
100
128
  * [status] id: content
101
129
  * e.g.: [pending] abc123: Implement feature X
130
+ * [claimed] task-def: Deploy service
131
+ *
132
+ * The regex accepts any alphanumeric/underscore status to handle
133
+ * current statuses (pending, claimed, done, blocked) and any future
134
+ * additions without code changes. Unknown statuses are treated as
135
+ * incomplete by checkTodos().
102
136
  */
103
137
  export function parseTodosFromAide(output: string): TodoItem[] {
104
138
  const todos: TodoItem[] = [];
@@ -106,13 +140,14 @@ export function parseTodosFromAide(output: string): TodoItem[] {
106
140
 
107
141
  for (const line of lines) {
108
142
  const match = line.match(
109
- /\[(pending|in_progress|completed|cancelled)\]\s+(\S+):\s+(.+)/,
143
+ /\[(\w+)\]\s+(\S+):\s+(.+?)(?:\s+\(agent:(\S+)\))?$/,
110
144
  );
111
145
  if (match) {
112
146
  todos.push({
113
- status: match[1] as TodoItem["status"],
147
+ status: match[1],
114
148
  id: match[2],
115
149
  content: match[3].trim(),
150
+ claimedBy: match[4] || undefined,
116
151
  });
117
152
  }
118
153
  }
@@ -95,11 +95,9 @@ export function updateToolStats(
95
95
  setState(binary, cwd, "startedAt", new Date().toISOString());
96
96
  }
97
97
 
98
- // Track tool calls
99
- const currentToolCalls = parseInt(
100
- getState(binary, cwd, "toolCalls") || "0",
101
- 10,
102
- );
98
+ // Track tool calls (guard against NaN from corrupted state)
99
+ const parsed = parseInt(getState(binary, cwd, "toolCalls") || "0", 10);
100
+ const currentToolCalls = Number.isNaN(parsed) ? 0 : parsed;
103
101
  setState(binary, cwd, "toolCalls", String(currentToolCalls + 1));
104
102
  setState(binary, cwd, "lastToolUse", new Date().toISOString());
105
103
  setState(binary, cwd, "lastTool", toolName);