opencodekit 0.14.5 → 0.15.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 (94) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +100 -58
  3. package/dist/template/.opencode/.env.example +1 -0
  4. package/dist/template/.opencode/AGENTS.md +13 -24
  5. package/dist/template/.opencode/README.md +8 -119
  6. package/dist/template/.opencode/agent/explore.md +2 -3
  7. package/dist/template/.opencode/agent/general.md +56 -0
  8. package/dist/template/.opencode/agent/plan.md +54 -0
  9. package/dist/template/.opencode/agent/scout.md +15 -5
  10. package/dist/template/.opencode/command/analyze-project.md +2 -2
  11. package/dist/template/.opencode/command/brainstorm.md +1 -1
  12. package/dist/template/.opencode/command/design-audit.md +4 -5
  13. package/dist/template/.opencode/command/design.md +4 -13
  14. package/dist/template/.opencode/command/generate-pattern.md +2 -9
  15. package/dist/template/.opencode/command/implement.md +4 -4
  16. package/dist/template/.opencode/command/init.md +1 -1
  17. package/dist/template/.opencode/command/new-feature.md +2 -3
  18. package/dist/template/.opencode/command/plan.md +1 -1
  19. package/dist/template/.opencode/command/pr.md +0 -1
  20. package/dist/template/.opencode/command/research.md +20 -6
  21. package/dist/template/.opencode/command/restore-image.md +1 -9
  22. package/dist/template/.opencode/command/revert-feature.md +1 -1
  23. package/dist/template/.opencode/command/review-codebase.md +4 -4
  24. package/dist/template/.opencode/command/status.md +1 -2
  25. package/dist/template/.opencode/command/summarize.md +1 -2
  26. package/dist/template/.opencode/command/triage.md +4 -32
  27. package/dist/template/.opencode/dcp.jsonc +68 -68
  28. package/dist/template/.opencode/memory/_templates/README.md +35 -0
  29. package/dist/template/.opencode/memory/_templates/project/architecture.md +60 -0
  30. package/dist/template/.opencode/memory/_templates/project/commands.md +72 -0
  31. package/dist/template/.opencode/memory/_templates/project/conventions.md +68 -0
  32. package/dist/template/.opencode/memory/_templates/project/gotchas.md +41 -0
  33. package/dist/template/.opencode/memory/beads-workflow.md +30 -29
  34. package/dist/template/.opencode/memory/project/architecture.md +31 -50
  35. package/dist/template/.opencode/memory/project/commands.md +41 -22
  36. package/dist/template/.opencode/memory/project/conventions.md +39 -177
  37. package/dist/template/.opencode/memory/project/gotchas.md +21 -177
  38. package/dist/template/.opencode/memory/user.example.md +5 -0
  39. package/dist/template/.opencode/opencode.json +644 -533
  40. package/dist/template/.opencode/package.json +18 -21
  41. package/dist/template/.opencode/plugin/compaction.ts +79 -85
  42. package/dist/template/.opencode/plugin/env-ctx.ts +34 -0
  43. package/dist/template/.opencode/plugin/lib/notify.ts +41 -45
  44. package/dist/template/.opencode/plugin/lsp.ts +197 -200
  45. package/dist/template/.opencode/plugin/memory.ts +14 -112
  46. package/dist/template/.opencode/plugin/package.json +5 -5
  47. package/dist/template/.opencode/plugin/sessions.ts +1 -1
  48. package/dist/template/.opencode/plugin/skill-mcp.ts +486 -521
  49. package/dist/template/.opencode/plugin/truncator.ts +47 -50
  50. package/dist/template/.opencode/plugin/tsconfig.json +14 -14
  51. package/dist/template/.opencode/skill/chrome-devtools/mcp.json +17 -17
  52. package/dist/template/.opencode/skill/condition-based-waiting/SKILL.md +17 -12
  53. package/dist/template/.opencode/skill/condition-based-waiting/example.ts +63 -69
  54. package/dist/template/.opencode/skill/defense-in-depth/SKILL.md +14 -8
  55. package/dist/template/.opencode/skill/dispatching-parallel-agents/SKILL.md +14 -3
  56. package/dist/template/.opencode/skill/playwright/mcp.json +14 -14
  57. package/dist/template/.opencode/skill/receiving-code-review/SKILL.md +21 -8
  58. package/dist/template/.opencode/skill/requesting-code-review/review.md +14 -0
  59. package/dist/template/.opencode/skill/root-cause-tracing/SKILL.md +18 -4
  60. package/dist/template/.opencode/skill/source-code-research/SKILL.md +9 -7
  61. package/dist/template/.opencode/skill/test-driven-development/SKILL.md +49 -32
  62. package/dist/template/.opencode/skill/testing-anti-patterns/SKILL.md +40 -22
  63. package/dist/template/.opencode/skill/testing-skills-with-subagents/SKILL.md +46 -26
  64. package/dist/template/.opencode/skill/tool-priority/SKILL.md +117 -44
  65. package/dist/template/.opencode/skill/v0/SKILL.md +1 -7
  66. package/dist/template/.opencode/skill/verification-before-completion/SKILL.md +27 -19
  67. package/dist/template/.opencode/skill/writing-skills/anthropic-best-practices.md +171 -148
  68. package/dist/template/.opencode/skill/writing-skills/persuasion-principles.md +39 -6
  69. package/dist/template/.opencode/tool/memory-read.ts +44 -56
  70. package/dist/template/.opencode/tool/memory-search.ts +8 -291
  71. package/dist/template/.opencode/tool/memory-update.ts +47 -51
  72. package/dist/template/.opencode/tool/observation.ts +6 -180
  73. package/dist/template/.opencode/tsconfig.json +19 -19
  74. package/package.json +19 -15
  75. package/dist/template/.opencode/.background-tasks.json +0 -114
  76. package/dist/template/.opencode/.ralph-state.json +0 -12
  77. package/dist/template/.opencode/agent/build.md +0 -327
  78. package/dist/template/.opencode/agent/planner.md +0 -281
  79. package/dist/template/.opencode/agent/rush.md +0 -223
  80. package/dist/template/.opencode/memory/handoffs/README.md +0 -83
  81. package/dist/template/.opencode/memory/observations/.gitkeep +0 -0
  82. package/dist/template/.opencode/memory/observations/2026-01-09-pattern-ampcode-mcp-json-includetools-pattern.md +0 -42
  83. package/dist/template/.opencode/memory/vector_db/memories.lance/_transactions/0-0d25ba80-ba3b-4209-9046-b45d6093b4da.txn +0 -0
  84. package/dist/template/.opencode/memory/vector_db/memories.lance/_versions/1.manifest +0 -0
  85. package/dist/template/.opencode/memory/vector_db/memories.lance/data/1111100101010101011010004a9ef34df6b29f36a9a53a2892.lance +0 -0
  86. package/dist/template/.opencode/tool/ast-grep.ts +0 -245
  87. package/dist/template/.opencode/tool/background.ts +0 -509
  88. package/dist/template/.opencode/tool/bd-inbox.ts +0 -110
  89. package/dist/template/.opencode/tool/bd-msg.ts +0 -62
  90. package/dist/template/.opencode/tool/bd-release.ts +0 -71
  91. package/dist/template/.opencode/tool/bd-reserve.ts +0 -121
  92. package/dist/template/.opencode/tool/memory-embed.ts +0 -183
  93. package/dist/template/.opencode/tool/memory-index.ts +0 -769
  94. package/dist/template/.opencode/tool/repo-map.ts +0 -451
@@ -9,54 +9,51 @@
9
9
  import type { Plugin } from "@opencode-ai/plugin";
10
10
 
11
11
  export const TruncatorPlugin: Plugin = async ({ client }) => {
12
- const sessionContext = new Map<string, number>();
13
-
14
- return {
15
- event: async ({ event }) => {
16
- const props = event.properties as Record<string, unknown>;
17
-
18
- if (event.type === "session.updated") {
19
- const info = props?.info as Record<string, unknown> | undefined;
20
- const tokenStats = (info?.tokens || props?.tokens) as
21
- | { used: number; limit: number }
22
- | undefined;
23
- const sessionId = (info?.id || props?.sessionID) as string | undefined;
24
-
25
- if (sessionId && tokenStats?.used && tokenStats?.limit) {
26
- sessionContext.set(
27
- sessionId,
28
- Math.round((tokenStats.used / tokenStats.limit) * 100),
29
- );
30
- }
31
- }
32
-
33
- if (event.type === "session.deleted") {
34
- const sessionId = props?.sessionID as string | undefined;
35
- if (sessionId) {
36
- sessionContext.delete(sessionId);
37
- }
38
- }
39
- },
40
-
41
- "tool.execute.after": async (input, output) => {
42
- const pct = sessionContext.get(input.sessionID) || 0;
43
- if (pct < 70) return; // Only warn under pressure
44
-
45
- // Thresholds get tighter as context fills up
46
- const threshold = pct >= 95 ? 5000 : pct >= 85 ? 10000 : 20000;
47
- const outputStr = output.output || "";
48
-
49
- if (outputStr.length > threshold) {
50
- await client.app
51
- .log({
52
- body: {
53
- service: "truncator",
54
- level: pct >= 95 ? "warn" : "info",
55
- message: `Large output from ${input.tool}: ${outputStr.length} chars (threshold: ${threshold}, context: ${pct}%)`,
56
- },
57
- })
58
- .catch(() => {});
59
- }
60
- },
61
- };
12
+ const sessionContext = new Map<string, number>();
13
+
14
+ return {
15
+ event: async ({ event }) => {
16
+ const props = event.properties as Record<string, unknown>;
17
+
18
+ if (event.type === "session.updated") {
19
+ const info = props?.info as Record<string, unknown> | undefined;
20
+ const tokenStats = (info?.tokens || props?.tokens) as
21
+ | { used: number; limit: number }
22
+ | undefined;
23
+ const sessionId = (info?.id || props?.sessionID) as string | undefined;
24
+
25
+ if (sessionId && tokenStats?.used && tokenStats?.limit) {
26
+ sessionContext.set(sessionId, Math.round((tokenStats.used / tokenStats.limit) * 100));
27
+ }
28
+ }
29
+
30
+ if (event.type === "session.deleted") {
31
+ const sessionId = props?.sessionID as string | undefined;
32
+ if (sessionId) {
33
+ sessionContext.delete(sessionId);
34
+ }
35
+ }
36
+ },
37
+
38
+ "tool.execute.after": async (input, output) => {
39
+ const pct = sessionContext.get(input.sessionID) || 0;
40
+ if (pct < 70) return; // Only warn under pressure
41
+
42
+ // Thresholds get tighter as context fills up
43
+ const threshold = pct >= 95 ? 5000 : pct >= 85 ? 10000 : 20000;
44
+ const outputStr = output.output || "";
45
+
46
+ if (outputStr.length > threshold) {
47
+ await client.app
48
+ .log({
49
+ body: {
50
+ service: "truncator",
51
+ level: pct >= 95 ? "warn" : "info",
52
+ message: `Large output from ${input.tool}: ${outputStr.length} chars (threshold: ${threshold}, context: ${pct}%)`,
53
+ },
54
+ })
55
+ .catch(() => {});
56
+ }
57
+ },
58
+ };
62
59
  };
@@ -1,16 +1,16 @@
1
1
  {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "nodenext",
6
- "allowSyntheticDefaultImports": true,
7
- "esModuleInterop": true,
8
- "strict": true,
9
- "noImplicitAny": false,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "types": ["node"]
13
- },
14
- "include": ["**/*.ts"],
15
- "exclude": ["node_modules"]
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "nodenext",
6
+ "allowSyntheticDefaultImports": true,
7
+ "esModuleInterop": true,
8
+ "strict": true,
9
+ "noImplicitAny": false,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": ["**/*.ts"],
15
+ "exclude": ["node_modules"]
16
16
  }
@@ -1,19 +1,19 @@
1
1
  {
2
- "chrome-devtools": {
3
- "command": "npx",
4
- "args": ["-y", "chrome-devtools-mcp@latest", "--stdio"],
5
- "includeTools": [
6
- "take_snapshot",
7
- "take_screenshot",
8
- "navigate_page",
9
- "new_page",
10
- "list_pages",
11
- "click",
12
- "fill",
13
- "hover",
14
- "press_key",
15
- "evaluate_script",
16
- "wait_for"
17
- ]
18
- }
2
+ "chrome-devtools": {
3
+ "command": "npx",
4
+ "args": ["-y", "chrome-devtools-mcp@latest", "--stdio"],
5
+ "includeTools": [
6
+ "take_snapshot",
7
+ "take_screenshot",
8
+ "navigate_page",
9
+ "new_page",
10
+ "list_pages",
11
+ "click",
12
+ "fill",
13
+ "hover",
14
+ "press_key",
15
+ "evaluate_script",
16
+ "wait_for"
17
+ ]
18
+ }
19
19
  }
@@ -27,12 +27,14 @@ digraph when_to_use {
27
27
  ```
28
28
 
29
29
  **Use when:**
30
+
30
31
  - Tests have arbitrary delays (`setTimeout`, `sleep`, `time.sleep()`)
31
32
  - Tests are flaky (pass sometimes, fail under load)
32
33
  - Tests timeout when run in parallel
33
34
  - Waiting for async operations to complete
34
35
 
35
36
  **Don't use when:**
37
+
36
38
  - Testing actual timing behavior (debounce, throttle intervals)
37
39
  - Always document WHY if using arbitrary timeout
38
40
 
@@ -40,7 +42,7 @@ digraph when_to_use {
40
42
 
41
43
  ```typescript
42
44
  // ❌ BEFORE: Guessing at timing
43
- await new Promise(r => setTimeout(r, 50));
45
+ await new Promise((r) => setTimeout(r, 50));
44
46
  const result = getResult();
45
47
  expect(result).toBeDefined();
46
48
 
@@ -52,22 +54,23 @@ expect(result).toBeDefined();
52
54
 
53
55
  ## Quick Patterns
54
56
 
55
- | Scenario | Pattern |
56
- |----------|---------|
57
- | Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'))` |
58
- | Wait for state | `waitFor(() => machine.state === 'ready')` |
59
- | Wait for count | `waitFor(() => items.length >= 5)` |
60
- | Wait for file | `waitFor(() => fs.existsSync(path))` |
61
- | Complex condition | `waitFor(() => obj.ready && obj.value > 10)` |
57
+ | Scenario | Pattern |
58
+ | ----------------- | ---------------------------------------------------- |
59
+ | Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'))` |
60
+ | Wait for state | `waitFor(() => machine.state === 'ready')` |
61
+ | Wait for count | `waitFor(() => items.length >= 5)` |
62
+ | Wait for file | `waitFor(() => fs.existsSync(path))` |
63
+ | Complex condition | `waitFor(() => obj.ready && obj.value > 10)` |
62
64
 
63
65
  ## Implementation
64
66
 
65
67
  Generic polling function:
68
+
66
69
  ```typescript
67
70
  async function waitFor<T>(
68
71
  condition: () => T | undefined | null | false,
69
72
  description: string,
70
- timeoutMs = 5000
73
+ timeoutMs = 5000,
71
74
  ): Promise<T> {
72
75
  const startTime = Date.now();
73
76
 
@@ -79,7 +82,7 @@ async function waitFor<T>(
79
82
  throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
80
83
  }
81
84
 
82
- await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
85
+ await new Promise((r) => setTimeout(r, 10)); // Poll every 10ms
83
86
  }
84
87
  }
85
88
  ```
@@ -101,12 +104,13 @@ See @example.ts for complete implementation with domain-specific helpers (`waitF
101
104
 
102
105
  ```typescript
103
106
  // Tool ticks every 100ms - need 2 ticks to verify partial output
104
- await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition
105
- await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior
107
+ await waitForEvent(manager, "TOOL_STARTED"); // First: wait for condition
108
+ await new Promise((r) => setTimeout(r, 200)); // Then: wait for timed behavior
106
109
  // 200ms = 2 ticks at 100ms intervals - documented and justified
107
110
  ```
108
111
 
109
112
  **Requirements:**
113
+
110
114
  1. First wait for triggering condition
111
115
  2. Based on known timing (not guessing)
112
116
  3. Comment explaining WHY
@@ -114,6 +118,7 @@ await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior
114
118
  ## Real-World Impact
115
119
 
116
120
  From debugging session (2025-10-03):
121
+
117
122
  - Fixed 15 flaky tests across 3 files
118
123
  - Pass rate: 60% → 100%
119
124
  - Execution time: 40% faster
@@ -18,33 +18,29 @@ import type { LaceEvent, LaceEventType } from "~/threads/types";
18
18
  * await waitForEvent(threadManager, agentThreadId, 'TOOL_RESULT');
19
19
  */
20
20
  export function waitForEvent(
21
- threadManager: ThreadManager,
22
- threadId: string,
23
- eventType: LaceEventType,
24
- timeoutMs = 5000,
21
+ threadManager: ThreadManager,
22
+ threadId: string,
23
+ eventType: LaceEventType,
24
+ timeoutMs = 5000,
25
25
  ): Promise<LaceEvent> {
26
- return new Promise((resolve, reject) => {
27
- const startTime = Date.now();
26
+ return new Promise((resolve, reject) => {
27
+ const startTime = Date.now();
28
28
 
29
- const check = () => {
30
- const events = threadManager.getEvents(threadId);
31
- const event = events.find((e) => e.type === eventType);
29
+ const check = () => {
30
+ const events = threadManager.getEvents(threadId);
31
+ const event = events.find((e) => e.type === eventType);
32
32
 
33
- if (event) {
34
- resolve(event);
35
- } else if (Date.now() - startTime > timeoutMs) {
36
- reject(
37
- new Error(
38
- `Timeout waiting for ${eventType} event after ${timeoutMs}ms`,
39
- ),
40
- );
41
- } else {
42
- setTimeout(check, 10); // Poll every 10ms for efficiency
43
- }
44
- };
33
+ if (event) {
34
+ resolve(event);
35
+ } else if (Date.now() - startTime > timeoutMs) {
36
+ reject(new Error(`Timeout waiting for ${eventType} event after ${timeoutMs}ms`));
37
+ } else {
38
+ setTimeout(check, 10); // Poll every 10ms for efficiency
39
+ }
40
+ };
45
41
 
46
- check();
47
- });
42
+ check();
43
+ });
48
44
  }
49
45
 
50
46
  /**
@@ -62,34 +58,34 @@ export function waitForEvent(
62
58
  * await waitForEventCount(threadManager, agentThreadId, 'AGENT_MESSAGE', 2);
63
59
  */
64
60
  export function waitForEventCount(
65
- threadManager: ThreadManager,
66
- threadId: string,
67
- eventType: LaceEventType,
68
- count: number,
69
- timeoutMs = 5000,
61
+ threadManager: ThreadManager,
62
+ threadId: string,
63
+ eventType: LaceEventType,
64
+ count: number,
65
+ timeoutMs = 5000,
70
66
  ): Promise<LaceEvent[]> {
71
- return new Promise((resolve, reject) => {
72
- const startTime = Date.now();
67
+ return new Promise((resolve, reject) => {
68
+ const startTime = Date.now();
73
69
 
74
- const check = () => {
75
- const events = threadManager.getEvents(threadId);
76
- const matchingEvents = events.filter((e) => e.type === eventType);
70
+ const check = () => {
71
+ const events = threadManager.getEvents(threadId);
72
+ const matchingEvents = events.filter((e) => e.type === eventType);
77
73
 
78
- if (matchingEvents.length >= count) {
79
- resolve(matchingEvents);
80
- } else if (Date.now() - startTime > timeoutMs) {
81
- reject(
82
- new Error(
83
- `Timeout waiting for ${count} ${eventType} events after ${timeoutMs}ms (got ${matchingEvents.length})`,
84
- ),
85
- );
86
- } else {
87
- setTimeout(check, 10);
88
- }
89
- };
74
+ if (matchingEvents.length >= count) {
75
+ resolve(matchingEvents);
76
+ } else if (Date.now() - startTime > timeoutMs) {
77
+ reject(
78
+ new Error(
79
+ `Timeout waiting for ${count} ${eventType} events after ${timeoutMs}ms (got ${matchingEvents.length})`,
80
+ ),
81
+ );
82
+ } else {
83
+ setTimeout(check, 10);
84
+ }
85
+ };
90
86
 
91
- check();
92
- });
87
+ check();
88
+ });
93
89
  }
94
90
 
95
91
  /**
@@ -113,32 +109,30 @@ export function waitForEventCount(
113
109
  * );
114
110
  */
115
111
  export function waitForEventMatch(
116
- threadManager: ThreadManager,
117
- threadId: string,
118
- predicate: (event: LaceEvent) => boolean,
119
- description: string,
120
- timeoutMs = 5000,
112
+ threadManager: ThreadManager,
113
+ threadId: string,
114
+ predicate: (event: LaceEvent) => boolean,
115
+ description: string,
116
+ timeoutMs = 5000,
121
117
  ): Promise<LaceEvent> {
122
- return new Promise((resolve, reject) => {
123
- const startTime = Date.now();
118
+ return new Promise((resolve, reject) => {
119
+ const startTime = Date.now();
124
120
 
125
- const check = () => {
126
- const events = threadManager.getEvents(threadId);
127
- const event = events.find(predicate);
121
+ const check = () => {
122
+ const events = threadManager.getEvents(threadId);
123
+ const event = events.find(predicate);
128
124
 
129
- if (event) {
130
- resolve(event);
131
- } else if (Date.now() - startTime > timeoutMs) {
132
- reject(
133
- new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`),
134
- );
135
- } else {
136
- setTimeout(check, 10);
137
- }
138
- };
125
+ if (event) {
126
+ resolve(event);
127
+ } else if (Date.now() - startTime > timeoutMs) {
128
+ reject(new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`));
129
+ } else {
130
+ setTimeout(check, 10);
131
+ }
132
+ };
139
133
 
140
- check();
141
- });
134
+ check();
135
+ });
142
136
  }
143
137
 
144
138
  // Usage example from actual debugging session:
@@ -17,6 +17,7 @@ Single validation: "We fixed the bug"
17
17
  Multiple layers: "We made the bug impossible"
18
18
 
19
19
  Different layers catch different cases:
20
+
20
21
  - Entry validation catches most bugs
21
22
  - Business logic catches edge cases
22
23
  - Environment guards prevent context-specific dangers
@@ -25,12 +26,13 @@ Different layers catch different cases:
25
26
  ## The Four Layers
26
27
 
27
28
  ### Layer 1: Entry Point Validation
29
+
28
30
  **Purpose:** Reject obviously invalid input at API boundary
29
31
 
30
32
  ```typescript
31
33
  function createProject(name: string, workingDirectory: string) {
32
- if (!workingDirectory || workingDirectory.trim() === '') {
33
- throw new Error('workingDirectory cannot be empty');
34
+ if (!workingDirectory || workingDirectory.trim() === "") {
35
+ throw new Error("workingDirectory cannot be empty");
34
36
  }
35
37
  if (!existsSync(workingDirectory)) {
36
38
  throw new Error(`workingDirectory does not exist: ${workingDirectory}`);
@@ -43,31 +45,31 @@ function createProject(name: string, workingDirectory: string) {
43
45
  ```
44
46
 
45
47
  ### Layer 2: Business Logic Validation
48
+
46
49
  **Purpose:** Ensure data makes sense for this operation
47
50
 
48
51
  ```typescript
49
52
  function initializeWorkspace(projectDir: string, sessionId: string) {
50
53
  if (!projectDir) {
51
- throw new Error('projectDir required for workspace initialization');
54
+ throw new Error("projectDir required for workspace initialization");
52
55
  }
53
56
  // ... proceed
54
57
  }
55
58
  ```
56
59
 
57
60
  ### Layer 3: Environment Guards
61
+
58
62
  **Purpose:** Prevent dangerous operations in specific contexts
59
63
 
60
64
  ```typescript
61
65
  async function gitInit(directory: string) {
62
66
  // In tests, refuse git init outside temp directories
63
- if (process.env.NODE_ENV === 'test') {
67
+ if (process.env.NODE_ENV === "test") {
64
68
  const normalized = normalize(resolve(directory));
65
69
  const tmpDir = normalize(resolve(tmpdir()));
66
70
 
67
71
  if (!normalized.startsWith(tmpDir)) {
68
- throw new Error(
69
- `Refusing git init outside temp dir during tests: ${directory}`
70
- );
72
+ throw new Error(`Refusing git init outside temp dir during tests: ${directory}`);
71
73
  }
72
74
  }
73
75
  // ... proceed
@@ -75,12 +77,13 @@ async function gitInit(directory: string) {
75
77
  ```
76
78
 
77
79
  ### Layer 4: Debug Instrumentation
80
+
78
81
  **Purpose:** Capture context for forensics
79
82
 
80
83
  ```typescript
81
84
  async function gitInit(directory: string) {
82
85
  const stack = new Error().stack;
83
- logger.debug('About to git init', {
86
+ logger.debug("About to git init", {
84
87
  directory,
85
88
  cwd: process.cwd(),
86
89
  stack,
@@ -103,12 +106,14 @@ When you find a bug:
103
106
  Bug: Empty `projectDir` caused `git init` in source code
104
107
 
105
108
  **Data flow:**
109
+
106
110
  1. Test setup → empty string
107
111
  2. `Project.create(name, '')`
108
112
  3. `WorkspaceManager.createWorkspace('')`
109
113
  4. `git init` runs in `process.cwd()`
110
114
 
111
115
  **Four layers added:**
116
+
112
117
  - Layer 1: `Project.create()` validates not empty/exists/writable
113
118
  - Layer 2: `WorkspaceManager` validates projectDir not empty
114
119
  - Layer 3: `WorktreeManager` refuses git init outside tmpdir in tests
@@ -119,6 +124,7 @@ Bug: Empty `projectDir` caused `git init` in source code
119
124
  ## Key Insight
120
125
 
121
126
  All four layers were necessary. During testing, each layer caught bugs the others missed:
127
+
122
128
  - Different code paths bypassed entry validation
123
129
  - Mocks bypassed business logic checks
124
130
  - Edge cases on different platforms needed environment guards
@@ -32,12 +32,14 @@ digraph when_to_use {
32
32
  ```
33
33
 
34
34
  **Use when:**
35
+
35
36
  - 3+ test files failing with different root causes
36
37
  - Multiple subsystems broken independently
37
38
  - Each problem can be understood without context from others
38
39
  - No shared state between investigations
39
40
 
40
41
  **Don't use when:**
42
+
41
43
  - Failures are related (fix one might fix others)
42
44
  - Need to understand full system state
43
45
  - Agents would interfere with each other
@@ -47,6 +49,7 @@ digraph when_to_use {
47
49
  ### 1. Identify Independent Domains
48
50
 
49
51
  Group failures by what's broken:
52
+
50
53
  - File A tests: Tool approval flow
51
54
  - File B tests: Batch completion behavior
52
55
  - File C tests: Abort functionality
@@ -56,6 +59,7 @@ Each domain is independent - fixing tool approval doesn't affect abort tests.
56
59
  ### 2. Create Focused Agent Tasks
57
60
 
58
61
  Each agent gets:
62
+
59
63
  - **Specific scope:** One test file or subsystem
60
64
  - **Clear goal:** Make these tests pass
61
65
  - **Constraints:** Don't change other code
@@ -65,15 +69,16 @@ Each agent gets:
65
69
 
66
70
  ```typescript
67
71
  // In Claude Code / AI environment
68
- Task("Fix agent-tool-abort.test.ts failures")
69
- Task("Fix batch-completion-behavior.test.ts failures")
70
- Task("Fix tool-approval-race-conditions.test.ts failures")
72
+ Task("Fix agent-tool-abort.test.ts failures");
73
+ Task("Fix batch-completion-behavior.test.ts failures");
74
+ Task("Fix tool-approval-race-conditions.test.ts failures");
71
75
  // All three run concurrently
72
76
  ```
73
77
 
74
78
  ### 4. Review and Integrate
75
79
 
76
80
  When agents return:
81
+
77
82
  - Read each summary
78
83
  - Verify fixes don't conflict
79
84
  - Run full test suite
@@ -82,6 +87,7 @@ When agents return:
82
87
  ## Agent Prompt Structure
83
88
 
84
89
  Good agent prompts are:
90
+
85
91
  1. **Focused** - One clear problem domain
86
92
  2. **Self-contained** - All context needed to understand the problem
87
93
  3. **Specific about output** - What should the agent return?
@@ -133,6 +139,7 @@ Return: Summary of what you found and what you fixed.
133
139
  **Scenario:** 6 test failures across 3 files after major refactoring
134
140
 
135
141
  **Failures:**
142
+
136
143
  - agent-tool-abort.test.ts: 3 failures (timing issues)
137
144
  - batch-completion-behavior.test.ts: 2 failures (tools not executing)
138
145
  - tool-approval-race-conditions.test.ts: 1 failure (execution count = 0)
@@ -140,6 +147,7 @@ Return: Summary of what you found and what you fixed.
140
147
  **Decision:** Independent domains - abort logic separate from batch completion separate from race conditions
141
148
 
142
149
  **Dispatch:**
150
+
143
151
  ```
144
152
  Agent 1 → Fix agent-tool-abort.test.ts
145
153
  Agent 2 → Fix batch-completion-behavior.test.ts
@@ -147,6 +155,7 @@ Agent 3 → Fix tool-approval-race-conditions.test.ts
147
155
  ```
148
156
 
149
157
  **Results:**
158
+
150
159
  - Agent 1: Replaced timeouts with event-based waiting
151
160
  - Agent 2: Fixed event structure bug (threadId in wrong place)
152
161
  - Agent 3: Added wait for async tool execution to complete
@@ -165,6 +174,7 @@ Agent 3 → Fix tool-approval-race-conditions.test.ts
165
174
  ## Verification
166
175
 
167
176
  After agents return:
177
+
168
178
  1. **Review each summary** - Understand what changed
169
179
  2. **Check for conflicts** - Did agents edit same code?
170
180
  3. **Run full suite** - Verify all fixes work together
@@ -173,6 +183,7 @@ After agents return:
173
183
  ## Real-World Impact
174
184
 
175
185
  From debugging session (2025-10-03):
186
+
176
187
  - 6 failures across 3 files
177
188
  - 3 agents dispatched in parallel
178
189
  - All investigations completed concurrently
@@ -1,16 +1,16 @@
1
1
  {
2
- "playwright": {
3
- "command": "npx",
4
- "args": ["@playwright/mcp@latest"],
5
- "includeTools": [
6
- "browser_navigate",
7
- "browser_snapshot",
8
- "browser_take_screenshot",
9
- "browser_click",
10
- "browser_type",
11
- "browser_fill",
12
- "browser_wait_for",
13
- "browser_resize"
14
- ]
15
- }
2
+ "playwright": {
3
+ "command": "npx",
4
+ "args": ["@playwright/mcp@latest"],
5
+ "includeTools": [
6
+ "browser_navigate",
7
+ "browser_snapshot",
8
+ "browser_take_screenshot",
9
+ "browser_click",
10
+ "browser_type",
11
+ "browser_fill",
12
+ "browser_wait_for",
13
+ "browser_resize"
14
+ ]
15
+ }
16
16
  }