@jmylchreest/aide-plugin 0.0.38 → 0.0.40

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
@@ -17,6 +17,7 @@ Write comprehensive tests and run test suites.
17
17
  ## Prerequisites
18
18
 
19
19
  Before starting:
20
+
20
21
  - Identify the code to be tested (function, module, feature)
21
22
  - Understand the testing framework used in the project
22
23
 
@@ -27,6 +28,7 @@ Before starting:
27
28
  Use the `mcp__plugin_aide_aide__decision_get` tool with topic `testing` to check for testing framework decisions.
28
29
 
29
30
  Common frameworks by language:
31
+
30
32
  - **TypeScript/JavaScript:** Vitest, Jest, Mocha
31
33
  - **Go:** built-in `go test`
32
34
  - **Python:** pytest, unittest
@@ -34,13 +36,18 @@ Common frameworks by language:
34
36
  ### Step 2: Discover Existing Test Patterns
35
37
 
36
38
  Use `Glob` to find test files:
39
+
37
40
  - Pattern: `**/*.test.ts`, `**/*.spec.ts` (TypeScript)
38
41
  - Pattern: `**/*_test.go` (Go)
39
42
  - Pattern: `**/test_*.py`, `**/*_test.py` (Python)
40
43
 
41
- Use `mcp__plugin_aide_aide__code_search` with query `describe` and `it` to find test patterns.
44
+ Use **Grep** to find test patterns in existing test files:
45
+
46
+ - `Grep pattern="describe\(" include="*.test.*"` — find test suites
47
+ - `Grep pattern="it\(|test\(" include="*.test.*"` — find test cases
42
48
 
43
49
  Read an existing test file to understand:
50
+
44
51
  - Import patterns
45
52
  - Setup/teardown patterns
46
53
  - Mocking approach
@@ -52,6 +59,7 @@ Use `mcp__plugin_aide_aide__code_symbols` with the target file path to get funct
52
59
  Use `mcp__plugin_aide_aide__code_search` to find related types.
53
60
 
54
61
  Identify:
62
+
55
63
  - Input parameters and types
56
64
  - Return type
57
65
  - Side effects
@@ -63,12 +71,14 @@ Identify:
63
71
  Follow the project's testing conventions. Cover these scenarios:
64
72
 
65
73
  **Test Categories:**
74
+
66
75
  1. **Happy path** - Normal, expected inputs
67
76
  2. **Edge cases** - Empty, null, boundary values
68
77
  3. **Error cases** - Invalid inputs, expected failures
69
78
  4. **Async behavior** - If applicable
70
79
 
71
80
  **Naming convention:**
81
+
72
82
  - Descriptive names that explain what is being tested
73
83
  - Format: "should [expected behavior] when [condition]"
74
84
 
@@ -103,51 +113,52 @@ go tool cover -html=coverage.out
103
113
  ```
104
114
 
105
115
  **Coverage targets:**
116
+
106
117
  - New code: aim for >80%
107
118
  - Critical paths: aim for >90%
108
119
  - Focus on meaningful tests, not just coverage numbers
109
120
 
110
121
  ## Failure Handling
111
122
 
112
- | Failure | Action |
113
- |---------|--------|
114
- | Test imports fail | Check path aliases, ensure test config matches main |
115
- | Mock not working | Verify mock setup, check dependency injection |
116
- | Async test timeout | Add proper await, increase timeout if needed |
117
- | Flaky test | Check for shared state, timing issues, or external deps |
118
- | Coverage too low | Add edge case tests, error path tests |
123
+ | Failure | Action |
124
+ | ------------------ | ------------------------------------------------------- |
125
+ | Test imports fail | Check path aliases, ensure test config matches main |
126
+ | Mock not working | Verify mock setup, check dependency injection |
127
+ | Async test timeout | Add proper await, increase timeout if needed |
128
+ | Flaky test | Check for shared state, timing issues, or external deps |
129
+ | Coverage too low | Add edge case tests, error path tests |
119
130
 
120
131
  ## Test Structure Templates
121
132
 
122
133
  ### TypeScript/JavaScript (Vitest/Jest)
123
134
 
124
135
  ```typescript
125
- import { describe, it, expect, beforeEach, vi } from 'vitest';
126
- import { functionToTest } from './module';
136
+ import { describe, it, expect, beforeEach, vi } from "vitest";
137
+ import { functionToTest } from "./module";
127
138
 
128
- describe('functionToTest', () => {
139
+ describe("functionToTest", () => {
129
140
  beforeEach(() => {
130
141
  // Reset state before each test
131
142
  vi.clearAllMocks();
132
143
  });
133
144
 
134
- it('should return expected result for valid input', () => {
135
- const result = functionToTest('valid input');
136
- expect(result).toBe('expected output');
145
+ it("should return expected result for valid input", () => {
146
+ const result = functionToTest("valid input");
147
+ expect(result).toBe("expected output");
137
148
  });
138
149
 
139
- it('should handle empty input', () => {
140
- const result = functionToTest('');
141
- expect(result).toBe('');
150
+ it("should handle empty input", () => {
151
+ const result = functionToTest("");
152
+ expect(result).toBe("");
142
153
  });
143
154
 
144
- it('should throw error for null input', () => {
145
- expect(() => functionToTest(null)).toThrow('Input required');
155
+ it("should throw error for null input", () => {
156
+ expect(() => functionToTest(null)).toThrow("Input required");
146
157
  });
147
158
 
148
- it('should handle async operation', async () => {
149
- const result = await functionToTest('async input');
150
- expect(result).resolves.toBe('async output');
159
+ it("should handle async operation", async () => {
160
+ const result = await functionToTest("async input");
161
+ expect(result).resolves.toBe("async output");
151
162
  });
152
163
  });
153
164
  ```
@@ -213,6 +224,7 @@ class TestFunctionToTest:
213
224
  ## Verification Criteria
214
225
 
215
226
  Before completing:
227
+
216
228
  - [ ] All new tests pass
217
229
  - [ ] Existing tests still pass
218
230
  - [ ] Coverage meets project standards
@@ -225,9 +237,11 @@ Before completing:
225
237
  ## Tests Added
226
238
 
227
239
  ### Files
240
+
228
241
  - `path/to/file.test.ts` - 5 tests for UserService
229
242
 
230
243
  ### Test Cases
244
+
231
245
  1. should create user with valid data
232
246
  2. should reject duplicate email
233
247
  3. should hash password before saving
@@ -235,10 +249,12 @@ Before completing:
235
249
  5. should validate email format
236
250
 
237
251
  ### Coverage
252
+
238
253
  - New code: 92%
239
254
  - Total project: 84%
240
255
 
241
256
  ### Verification
257
+
242
258
  - All tests: PASS
243
259
  - No flaky tests observed
244
260
  ```
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Context Guard — platform-agnostic core logic.
3
+ *
4
+ * Monitors Read tool calls and advises agents to use code_outline
5
+ * before reading large files. This preserves context window for
6
+ * the actual task by avoiding dumping entire files into conversation.
7
+ *
8
+ * Behaviour:
9
+ * - Triggers on Read tool calls for files > 5KB (~150 lines)
10
+ * - Tracks which files have been outlined (code_outline/code_symbols)
11
+ * - Returns an advisory message (never blocks)
12
+ * - Also tracks code_outline/code_symbols calls to mark files as "known"
13
+ *
14
+ * Used by both Claude Code hooks (PreToolUse) and OpenCode plugin.
15
+ */
16
+
17
+ import { statSync, readFileSync, writeFileSync, existsSync } from "fs";
18
+ import { resolve, isAbsolute, normalize } from "path";
19
+ import { tmpdir } from "os";
20
+ import { join } from "path";
21
+ import { debug } from "../lib/logger.js";
22
+
23
+ const SOURCE = "context-guard";
24
+
25
+ /** Default size threshold in bytes (~150 lines) */
26
+ const DEFAULT_SIZE_THRESHOLD = 5120;
27
+
28
+ /** File extensions that are typically not source code (skip advisory) */
29
+ const SKIP_EXTENSIONS = new Set([
30
+ ".json",
31
+ ".lock",
32
+ ".sum",
33
+ ".mod",
34
+ ".yaml",
35
+ ".yml",
36
+ ".toml",
37
+ ".env",
38
+ ".md",
39
+ ".txt",
40
+ ".csv",
41
+ ".svg",
42
+ ".png",
43
+ ".jpg",
44
+ ".gif",
45
+ ".ico",
46
+ ".woff",
47
+ ".woff2",
48
+ ".ttf",
49
+ ".eot",
50
+ ]);
51
+
52
+ export interface ContextGuardResult {
53
+ /** Whether to inject an advisory message */
54
+ shouldAdvise: boolean;
55
+ /** Advisory message to inject */
56
+ advisory?: string;
57
+ /** Whether this call should be tracked (code_outline/code_symbols) */
58
+ tracked?: boolean;
59
+ }
60
+
61
+ /**
62
+ * Get the path to the tracking file for this session.
63
+ */
64
+ function getTrackingPath(sessionId: string): string {
65
+ return join(tmpdir(), `aide-context-guard-${sessionId}.json`);
66
+ }
67
+
68
+ /**
69
+ * Load the set of files that have been outlined in this session.
70
+ */
71
+ function loadOutlinedFiles(sessionId: string): Set<string> {
72
+ const trackingPath = getTrackingPath(sessionId);
73
+ try {
74
+ if (existsSync(trackingPath)) {
75
+ const data = JSON.parse(readFileSync(trackingPath, "utf-8"));
76
+ return new Set(data.files || []);
77
+ }
78
+ } catch {
79
+ // Corrupted file, start fresh
80
+ }
81
+ return new Set();
82
+ }
83
+
84
+ /**
85
+ * Save a file as "outlined" in the tracking file.
86
+ */
87
+ function trackOutlinedFile(sessionId: string, filePath: string): void {
88
+ const files = loadOutlinedFiles(sessionId);
89
+ files.add(filePath);
90
+ const trackingPath = getTrackingPath(sessionId);
91
+ try {
92
+ writeFileSync(
93
+ trackingPath,
94
+ JSON.stringify({ files: Array.from(files) }),
95
+ "utf-8",
96
+ );
97
+ } catch (err) {
98
+ debug(SOURCE, `Failed to write tracking file: ${err}`);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Estimate line count from file size (rough: ~35 bytes per line average).
104
+ */
105
+ function estimateLines(sizeBytes: number): number {
106
+ return Math.round(sizeBytes / 35);
107
+ }
108
+
109
+ /**
110
+ * Check whether a Read call should receive a context-efficiency advisory.
111
+ *
112
+ * Also handles tracking code_outline/code_symbols calls.
113
+ */
114
+ export function checkContextGuard(
115
+ toolName: string,
116
+ toolInput: Record<string, unknown>,
117
+ cwd: string,
118
+ sessionId: string,
119
+ ): ContextGuardResult {
120
+ const normalizedTool = toolName.toLowerCase();
121
+
122
+ // Track code_outline and code_symbols calls
123
+ if (
124
+ normalizedTool.includes("code_outline") ||
125
+ normalizedTool.includes("code_symbols")
126
+ ) {
127
+ const filePath =
128
+ (toolInput.file as string) ||
129
+ (toolInput.filePath as string) ||
130
+ (toolInput.file_path as string);
131
+ if (filePath && sessionId) {
132
+ const resolved = normalize(
133
+ isAbsolute(filePath) ? filePath : resolve(cwd, filePath),
134
+ );
135
+ trackOutlinedFile(sessionId, resolved);
136
+ debug(SOURCE, `Tracked outline for: ${filePath}`);
137
+ }
138
+ return { shouldAdvise: false, tracked: true };
139
+ }
140
+
141
+ // Only advise on Read tool calls
142
+ if (normalizedTool !== "read") {
143
+ return { shouldAdvise: false };
144
+ }
145
+
146
+ // Extract file path from tool input
147
+ const filePath =
148
+ (toolInput.filePath as string) ||
149
+ (toolInput.file_path as string) ||
150
+ (toolInput.path as string);
151
+
152
+ if (!filePath) {
153
+ return { shouldAdvise: false };
154
+ }
155
+
156
+ // Resolve to absolute path
157
+ const resolvedPath = normalize(
158
+ isAbsolute(filePath) ? filePath : resolve(cwd, filePath),
159
+ );
160
+
161
+ // Skip non-source-code files
162
+ const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase();
163
+ if (SKIP_EXTENSIONS.has(ext)) {
164
+ return { shouldAdvise: false };
165
+ }
166
+
167
+ // Check if the agent is already using offset/limit (targeted read)
168
+ const offset = toolInput.offset as number | undefined;
169
+ const limit = toolInput.limit as number | undefined;
170
+ if (offset !== undefined && offset > 1) {
171
+ // Agent is doing a targeted read — no advisory needed
172
+ return { shouldAdvise: false };
173
+ }
174
+ if (limit !== undefined && limit < 100) {
175
+ // Agent is limiting the read — no advisory needed
176
+ return { shouldAdvise: false };
177
+ }
178
+
179
+ // Check file size
180
+ let fileSize: number;
181
+ try {
182
+ const stat = statSync(resolvedPath);
183
+ fileSize = stat.size;
184
+ } catch {
185
+ // Can't stat file — don't advise (file might not exist yet)
186
+ return { shouldAdvise: false };
187
+ }
188
+
189
+ // Skip small files
190
+ if (fileSize < DEFAULT_SIZE_THRESHOLD) {
191
+ return { shouldAdvise: false };
192
+ }
193
+
194
+ // Check if this file has already been outlined in this session
195
+ if (sessionId) {
196
+ const outlinedFiles = loadOutlinedFiles(sessionId);
197
+ if (outlinedFiles.has(resolvedPath)) {
198
+ debug(SOURCE, `File already outlined, skipping advisory: ${filePath}`);
199
+ return { shouldAdvise: false };
200
+ }
201
+ }
202
+
203
+ // Generate advisory
204
+ const estLines = estimateLines(fileSize);
205
+ const sizeKB = (fileSize / 1024).toFixed(1);
206
+
207
+ const advisory =
208
+ `[aide:context] This file is ~${estLines} lines (${sizeKB}KB). Consider using \`code_outline\` ` +
209
+ `first to see its structure, then \`Read\` with offset/limit for specific sections. ` +
210
+ `This preserves your context window for the full task.`;
211
+
212
+ debug(SOURCE, `Advisory for ${filePath}: ${estLines} lines, ${sizeKB}KB`);
213
+ return { shouldAdvise: true, advisory };
214
+ }
@@ -74,17 +74,23 @@ 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)
86
91
  const iterStr = getState(binary, cwd, `${mode}_iterations`) || "0";
87
- const iteration = parseInt(iterStr, 10) + 1;
92
+ const parsed = parseInt(iterStr, 10);
93
+ const iteration = (Number.isNaN(parsed) ? 0 : parsed) + 1;
88
94
  setState(binary, cwd, `${mode}_iterations`, String(iteration));
89
95
 
90
96
  if (iteration > MAX_PERSISTENCE_ITERATIONS) {
@@ -94,21 +100,37 @@ export function checkPersistence(
94
100
  return null;
95
101
  }
96
102
 
97
- // Fetch todos and build a specific continuation message if incomplete tasks exist
103
+ // Fetch todos and build a specific continuation message if incomplete tasks exist.
104
+ // If all tasks are complete (or no tasks exist), auto-release: allow stop.
98
105
  let todoSummary: string | undefined;
106
+ let allTasksComplete = false;
99
107
  try {
100
108
  const todos = fetchTodosFromAide(binary, cwd);
101
- const todoResult = checkTodos(todos);
109
+ const todoResult = checkTodos(todos, agentId);
102
110
  if (todoResult.hasIncomplete) {
103
111
  todoSummary = todoResult.message;
104
112
  debug(
105
113
  SOURCE,
106
114
  `Found ${todoResult.incompleteCount} incomplete todos for persistence reinforcement`,
107
115
  );
116
+ } else if (todoResult.totalCount > 0) {
117
+ // All tasks exist and are in terminal states — work is done
118
+ allTasksComplete = true;
119
+ debug(
120
+ SOURCE,
121
+ `All ${todoResult.totalCount} tasks complete — auto-releasing ${mode} mode`,
122
+ );
108
123
  }
109
124
  } catch (err) {
110
125
  debug(SOURCE, `Failed to fetch todos for persistence (non-fatal): ${err}`);
111
126
  }
112
127
 
128
+ // Auto-release: if tasks exist and all are complete, allow stop
129
+ if (allTasksComplete) {
130
+ setState(binary, cwd, "mode", "");
131
+ setState(binary, cwd, `${mode}_iterations`, "0");
132
+ return null;
133
+ }
134
+
113
135
  return { reason: buildReinforcement(mode, iteration, todoSummary) };
114
136
  }