@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.
- package/package.json +1 -1
- package/skills/assess-findings/SKILL.md +167 -0
- package/skills/code-search/SKILL.md +12 -0
- package/skills/git/SKILL.md +10 -0
- package/skills/memorise/SKILL.md +89 -11
- package/skills/patterns/SKILL.md +182 -0
- package/skills/plan-swarm/SKILL.md +5 -0
- package/skills/ralph/SKILL.md +15 -8
- package/skills/review/SKILL.md +3 -0
- package/skills/swarm/SKILL.md +75 -24
- package/src/cli/config.ts +4 -1
- package/src/core/aide-client.ts +15 -12
- package/src/core/mcp-sync.ts +19 -3
- package/src/core/partial-memory.ts +46 -55
- package/src/core/persistence-logic.ts +28 -4
- package/src/core/session-init.ts +10 -6
- package/src/core/session-summary-logic.ts +9 -2
- package/src/core/todo-checker.ts +53 -18
- package/src/core/tool-tracking.ts +3 -5
- package/src/core/types.ts +21 -0
- package/src/lib/aide-downloader.ts +0 -8
- package/src/lib/hook-utils.ts +47 -115
- package/src/lib/hud.ts +2 -2
- package/src/lib/skills-registry.ts +32 -4
- package/src/lib/worktree.ts +11 -37
- package/src/opencode/hooks.ts +27 -1
- package/src/lib/aide-memory.ts +0 -400
package/skills/swarm/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
435
|
+
## OpenCode Mode
|
|
436
436
|
|
|
437
|
-
|
|
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
|
-
|
|
439
|
+
### Task System Roles (OpenCode)
|
|
442
440
|
|
|
443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
453
|
+
### Orchestrator Workflow
|
|
452
454
|
|
|
453
|
-
|
|
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.
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/core/aide-client.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* platform-specific options instead of relying on CLAUDE_PLUGIN_ROOT.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
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 =
|
|
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
|
-
*
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
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;
|
package/src/core/mcp-sync.ts
CHANGED
|
@@ -157,10 +157,20 @@ function readAideConfig(path: string): Record<string, CanonicalMcpServer> {
|
|
|
157
157
|
if (!existsSync(path)) return {};
|
|
158
158
|
|
|
159
159
|
try {
|
|
160
|
-
const
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
-
*
|
|
163
|
+
* Query session partials and map to a caller-chosen type.
|
|
157
164
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
165
|
+
* Runs `aide memory list --tags=partial --format=json`, filters to the
|
|
166
|
+
* given session, and maps each match through `mapFn`.
|
|
160
167
|
*/
|
|
161
|
-
|
|
168
|
+
function querySessionPartials<T>(
|
|
162
169
|
binary: string,
|
|
163
170
|
cwd: string,
|
|
164
171
|
sessionId: string,
|
|
165
|
-
|
|
172
|
+
mapFn: (m: PartialMemoryEntry) => T,
|
|
173
|
+
label: string,
|
|
174
|
+
): T[] {
|
|
166
175
|
try {
|
|
167
|
-
const sessionTag = `session:${sessionId.slice(0,
|
|
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
|
-
|
|
190
|
-
|
|
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
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
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
|
}
|
package/src/core/session-init.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
statSync,
|
|
17
17
|
} from "fs";
|
|
18
18
|
import { join } from "path";
|
|
19
|
-
import {
|
|
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 =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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,
|
|
224
|
+
const tags = `session-summary,session:${sessionId.slice(0, 12)}`;
|
|
218
225
|
|
|
219
226
|
execFileSync(
|
|
220
227
|
binary,
|
package/src/core/todo-checker.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Todo continuation checker — platform-agnostic.
|
|
3
3
|
*
|
|
4
|
-
* Reads
|
|
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
|
|
7
|
+
* specific incomplete tasks.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
57
|
-
|
|
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:
|
|
91
|
+
totalCount: relevant.length,
|
|
65
92
|
incompleteItems: [],
|
|
66
93
|
message: "",
|
|
67
94
|
};
|
|
68
95
|
}
|
|
69
96
|
|
|
70
|
-
const completedCount =
|
|
97
|
+
const completedCount = relevant.length - incomplete.length;
|
|
71
98
|
const lines: string[] = [
|
|
72
|
-
`**TODO CONTINUATION** — ${incomplete.length} of ${
|
|
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 =
|
|
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:
|
|
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
|
-
/\[(
|
|
143
|
+
/\[(\w+)\]\s+(\S+):\s+(.+?)(?:\s+\(agent:(\S+)\))?$/,
|
|
110
144
|
);
|
|
111
145
|
if (match) {
|
|
112
146
|
todos.push({
|
|
113
|
-
status: match[1]
|
|
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
|
|
100
|
-
|
|
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);
|