opencodekit 0.0.1

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 (123) hide show
  1. package/README.md +258 -0
  2. package/dist/index.js +3391 -0
  3. package/dist/template/.opencode/.env.example +193 -0
  4. package/dist/template/.opencode/AGENTS.md +214 -0
  5. package/dist/template/.opencode/README.md +269 -0
  6. package/dist/template/.opencode/agent/build.md +75 -0
  7. package/dist/template/.opencode/agent/explore.md +66 -0
  8. package/dist/template/.opencode/agent/planner.md +83 -0
  9. package/dist/template/.opencode/agent/review.md +90 -0
  10. package/dist/template/.opencode/agent/rush.md +85 -0
  11. package/dist/template/.opencode/agent/scout.md +93 -0
  12. package/dist/template/.opencode/command/analyze-project.md +39 -0
  13. package/dist/template/.opencode/command/brainstorm.md +11 -0
  14. package/dist/template/.opencode/command/commit.md +11 -0
  15. package/dist/template/.opencode/command/create.md +118 -0
  16. package/dist/template/.opencode/command/design.md +15 -0
  17. package/dist/template/.opencode/command/finish.md +233 -0
  18. package/dist/template/.opencode/command/fix-ci.md +20 -0
  19. package/dist/template/.opencode/command/fix-types.md +10 -0
  20. package/dist/template/.opencode/command/fix-ui.md +22 -0
  21. package/dist/template/.opencode/command/fix.md +22 -0
  22. package/dist/template/.opencode/command/handoff.md +146 -0
  23. package/dist/template/.opencode/command/implement.md +167 -0
  24. package/dist/template/.opencode/command/import-plan.md +188 -0
  25. package/dist/template/.opencode/command/integration-test.md +36 -0
  26. package/dist/template/.opencode/command/issue.md +41 -0
  27. package/dist/template/.opencode/command/plan.md +158 -0
  28. package/dist/template/.opencode/command/pr.md +36 -0
  29. package/dist/template/.opencode/command/quick-build.md +13 -0
  30. package/dist/template/.opencode/command/research-and-implement.md +21 -0
  31. package/dist/template/.opencode/command/research-ui.md +32 -0
  32. package/dist/template/.opencode/command/research.md +153 -0
  33. package/dist/template/.opencode/command/resume.md +127 -0
  34. package/dist/template/.opencode/command/review-codebase.md +13 -0
  35. package/dist/template/.opencode/command/skill-create.md +29 -0
  36. package/dist/template/.opencode/command/skill-optimize.md +28 -0
  37. package/dist/template/.opencode/command/status.md +109 -0
  38. package/dist/template/.opencode/command/ui-review.md +28 -0
  39. package/dist/template/.opencode/dcp.jsonc +34 -0
  40. package/dist/template/.opencode/memory/README.md +128 -0
  41. package/dist/template/.opencode/memory/_templates/handoff.md +33 -0
  42. package/dist/template/.opencode/memory/_templates/research.md +29 -0
  43. package/dist/template/.opencode/memory/_templates/task-prd.md +43 -0
  44. package/dist/template/.opencode/memory/_templates/task-review.md +73 -0
  45. package/dist/template/.opencode/memory/_templates/task-spec.md +71 -0
  46. package/dist/template/.opencode/memory/design-guidelines.md +281 -0
  47. package/dist/template/.opencode/memory/handoffs/README.md +83 -0
  48. package/dist/template/.opencode/opencode.json +469 -0
  49. package/dist/template/.opencode/package.json +23 -0
  50. package/dist/template/.opencode/pickle-thinker.jsonc +11 -0
  51. package/dist/template/.opencode/plugin/README.md +162 -0
  52. package/dist/template/.opencode/plugin/notification.ts +88 -0
  53. package/dist/template/.opencode/plugin/sessions.ts +434 -0
  54. package/dist/template/.opencode/plugin/superpowers.ts +332 -0
  55. package/dist/template/.opencode/plugin/tsconfig.json +15 -0
  56. package/dist/template/.opencode/superpowers/.claude/settings.local.json +141 -0
  57. package/dist/template/.opencode/superpowers/.claude-plugin/marketplace.json +20 -0
  58. package/dist/template/.opencode/superpowers/.claude-plugin/plugin.json +13 -0
  59. package/dist/template/.opencode/superpowers/.codex/INSTALL.md +35 -0
  60. package/dist/template/.opencode/superpowers/.codex/superpowers-bootstrap.md +33 -0
  61. package/dist/template/.opencode/superpowers/.codex/superpowers-codex +267 -0
  62. package/dist/template/.opencode/superpowers/.github/FUNDING.yml +3 -0
  63. package/dist/template/.opencode/superpowers/.opencode/INSTALL.md +135 -0
  64. package/dist/template/.opencode/superpowers/.opencode/plugin/superpowers.js +215 -0
  65. package/dist/template/.opencode/superpowers/LICENSE +21 -0
  66. package/dist/template/.opencode/superpowers/README.md +165 -0
  67. package/dist/template/.opencode/superpowers/RELEASE-NOTES.md +493 -0
  68. package/dist/template/.opencode/superpowers/agents/code-reviewer.md +48 -0
  69. package/dist/template/.opencode/superpowers/commands/brainstorm.md +5 -0
  70. package/dist/template/.opencode/superpowers/commands/execute-plan.md +5 -0
  71. package/dist/template/.opencode/superpowers/commands/write-plan.md +5 -0
  72. package/dist/template/.opencode/superpowers/docs/README.codex.md +153 -0
  73. package/dist/template/.opencode/superpowers/docs/README.opencode.md +234 -0
  74. package/dist/template/.opencode/superpowers/docs/plans/2025-11-22-opencode-support-design.md +294 -0
  75. package/dist/template/.opencode/superpowers/docs/plans/2025-11-22-opencode-support-implementation.md +1095 -0
  76. package/dist/template/.opencode/superpowers/hooks/hooks.json +15 -0
  77. package/dist/template/.opencode/superpowers/hooks/session-start.sh +34 -0
  78. package/dist/template/.opencode/superpowers/lib/skills-core.js +208 -0
  79. package/dist/template/.opencode/superpowers/skills/brainstorming/SKILL.md +54 -0
  80. package/dist/template/.opencode/superpowers/skills/condition-based-waiting/SKILL.md +120 -0
  81. package/dist/template/.opencode/superpowers/skills/condition-based-waiting/example.ts +158 -0
  82. package/dist/template/.opencode/superpowers/skills/defense-in-depth/SKILL.md +127 -0
  83. package/dist/template/.opencode/superpowers/skills/dispatching-parallel-agents/SKILL.md +180 -0
  84. package/dist/template/.opencode/superpowers/skills/executing-plans/SKILL.md +76 -0
  85. package/dist/template/.opencode/superpowers/skills/finishing-a-development-branch/SKILL.md +200 -0
  86. package/dist/template/.opencode/superpowers/skills/frontend-aesthetics/SKILL.md +137 -0
  87. package/dist/template/.opencode/superpowers/skills/gemini-large-context/SKILL.md +205 -0
  88. package/dist/template/.opencode/superpowers/skills/receiving-code-review/SKILL.md +209 -0
  89. package/dist/template/.opencode/superpowers/skills/requesting-code-review/SKILL.md +105 -0
  90. package/dist/template/.opencode/superpowers/skills/requesting-code-review/code-reviewer.md +146 -0
  91. package/dist/template/.opencode/superpowers/skills/root-cause-tracing/SKILL.md +174 -0
  92. package/dist/template/.opencode/superpowers/skills/root-cause-tracing/find-polluter.sh +63 -0
  93. package/dist/template/.opencode/superpowers/skills/sharing-skills/SKILL.md +194 -0
  94. package/dist/template/.opencode/superpowers/skills/subagent-driven-development/SKILL.md +189 -0
  95. package/dist/template/.opencode/superpowers/skills/systematic-debugging/CREATION-LOG.md +119 -0
  96. package/dist/template/.opencode/superpowers/skills/systematic-debugging/SKILL.md +295 -0
  97. package/dist/template/.opencode/superpowers/skills/systematic-debugging/test-academic.md +14 -0
  98. package/dist/template/.opencode/superpowers/skills/systematic-debugging/test-pressure-1.md +58 -0
  99. package/dist/template/.opencode/superpowers/skills/systematic-debugging/test-pressure-2.md +68 -0
  100. package/dist/template/.opencode/superpowers/skills/systematic-debugging/test-pressure-3.md +69 -0
  101. package/dist/template/.opencode/superpowers/skills/test-driven-development/SKILL.md +364 -0
  102. package/dist/template/.opencode/superpowers/skills/testing-anti-patterns/SKILL.md +302 -0
  103. package/dist/template/.opencode/superpowers/skills/testing-skills-with-subagents/SKILL.md +387 -0
  104. package/dist/template/.opencode/superpowers/skills/testing-skills-with-subagents/examples/CLAUDE_MD_TESTING.md +189 -0
  105. package/dist/template/.opencode/superpowers/skills/ui-ux-research/SKILL.md +191 -0
  106. package/dist/template/.opencode/superpowers/skills/using-git-worktrees/SKILL.md +213 -0
  107. package/dist/template/.opencode/superpowers/skills/using-superpowers/SKILL.md +101 -0
  108. package/dist/template/.opencode/superpowers/skills/verification-before-completion/SKILL.md +139 -0
  109. package/dist/template/.opencode/superpowers/skills/writing-plans/SKILL.md +116 -0
  110. package/dist/template/.opencode/superpowers/skills/writing-skills/SKILL.md +622 -0
  111. package/dist/template/.opencode/superpowers/skills/writing-skills/anthropic-best-practices.md +1150 -0
  112. package/dist/template/.opencode/superpowers/skills/writing-skills/graphviz-conventions.dot +172 -0
  113. package/dist/template/.opencode/superpowers/skills/writing-skills/persuasion-principles.md +187 -0
  114. package/dist/template/.opencode/superpowers/tests/opencode/run-tests.sh +165 -0
  115. package/dist/template/.opencode/superpowers/tests/opencode/setup.sh +73 -0
  116. package/dist/template/.opencode/superpowers/tests/opencode/test-plugin-loading.sh +81 -0
  117. package/dist/template/.opencode/superpowers/tests/opencode/test-priority.sh +198 -0
  118. package/dist/template/.opencode/superpowers/tests/opencode/test-skills-core.sh +440 -0
  119. package/dist/template/.opencode/superpowers/tests/opencode/test-tools.sh +104 -0
  120. package/dist/template/.opencode/tool/memory-read.ts +66 -0
  121. package/dist/template/.opencode/tool/memory-update.ts +61 -0
  122. package/dist/template/.opencode/tsconfig.json +21 -0
  123. package/package.json +52 -0
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "startup|resume|clear|compact",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # SessionStart hook for superpowers plugin
3
+
4
+ set -euo pipefail
5
+
6
+ # Determine plugin root directory
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
8
+ PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
9
+
10
+ # Check if legacy skills directory exists and build warning
11
+ warning_message=""
12
+ legacy_skills_dir="${HOME}/.config/superpowers/skills"
13
+ if [ -d "$legacy_skills_dir" ]; then
14
+ warning_message="\n\n<important-reminder>IN YOUR FIRST REPLY AFTER SEEING THIS MESSAGE YOU MUST TELL THE USER:⚠️ **WARNING:** Superpowers now uses Claude Code's skills system. Custom skills in ~/.config/superpowers/skills will not be read. Move custom skills to ~/.claude/skills instead. To make this message go away, remove ~/.config/superpowers/skills</important-reminder>"
15
+ fi
16
+
17
+ # Read using-superpowers content
18
+ using_superpowers_content=$(cat "${PLUGIN_ROOT}/skills/using-superpowers/SKILL.md" 2>&1 || echo "Error reading using-superpowers skill")
19
+
20
+ # Escape outputs for JSON
21
+ using_superpowers_escaped=$(echo "$using_superpowers_content" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
22
+ warning_escaped=$(echo "$warning_message" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
23
+
24
+ # Output context injection as JSON
25
+ cat <<EOF
26
+ {
27
+ "hookSpecificOutput": {
28
+ "hookEventName": "SessionStart",
29
+ "additionalContext": "<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**\n\n${using_superpowers_escaped}\n\n${warning_escaped}\n</EXTREMELY_IMPORTANT>"
30
+ }
31
+ }
32
+ EOF
33
+
34
+ exit 0
@@ -0,0 +1,208 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { execSync } from 'child_process';
4
+
5
+ /**
6
+ * Extract YAML frontmatter from a skill file.
7
+ * Current format:
8
+ * ---
9
+ * name: skill-name
10
+ * description: Use when [condition] - [what it does]
11
+ * ---
12
+ *
13
+ * @param {string} filePath - Path to SKILL.md file
14
+ * @returns {{name: string, description: string}}
15
+ */
16
+ function extractFrontmatter(filePath) {
17
+ try {
18
+ const content = fs.readFileSync(filePath, 'utf8');
19
+ const lines = content.split('\n');
20
+
21
+ let inFrontmatter = false;
22
+ let name = '';
23
+ let description = '';
24
+
25
+ for (const line of lines) {
26
+ if (line.trim() === '---') {
27
+ if (inFrontmatter) break;
28
+ inFrontmatter = true;
29
+ continue;
30
+ }
31
+
32
+ if (inFrontmatter) {
33
+ const match = line.match(/^(\w+):\s*(.*)$/);
34
+ if (match) {
35
+ const [, key, value] = match;
36
+ switch (key) {
37
+ case 'name':
38
+ name = value.trim();
39
+ break;
40
+ case 'description':
41
+ description = value.trim();
42
+ break;
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ return { name, description };
49
+ } catch (error) {
50
+ return { name: '', description: '' };
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Find all SKILL.md files in a directory recursively.
56
+ *
57
+ * @param {string} dir - Directory to search
58
+ * @param {string} sourceType - 'personal' or 'superpowers' for namespacing
59
+ * @param {number} maxDepth - Maximum recursion depth (default: 3)
60
+ * @returns {Array<{path: string, name: string, description: string, sourceType: string}>}
61
+ */
62
+ function findSkillsInDir(dir, sourceType, maxDepth = 3) {
63
+ const skills = [];
64
+
65
+ if (!fs.existsSync(dir)) return skills;
66
+
67
+ function recurse(currentDir, depth) {
68
+ if (depth > maxDepth) return;
69
+
70
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
71
+
72
+ for (const entry of entries) {
73
+ const fullPath = path.join(currentDir, entry.name);
74
+
75
+ if (entry.isDirectory()) {
76
+ // Check for SKILL.md in this directory
77
+ const skillFile = path.join(fullPath, 'SKILL.md');
78
+ if (fs.existsSync(skillFile)) {
79
+ const { name, description } = extractFrontmatter(skillFile);
80
+ skills.push({
81
+ path: fullPath,
82
+ skillFile: skillFile,
83
+ name: name || entry.name,
84
+ description: description || '',
85
+ sourceType: sourceType
86
+ });
87
+ }
88
+
89
+ // Recurse into subdirectories
90
+ recurse(fullPath, depth + 1);
91
+ }
92
+ }
93
+ }
94
+
95
+ recurse(dir, 0);
96
+ return skills;
97
+ }
98
+
99
+ /**
100
+ * Resolve a skill name to its file path, handling shadowing
101
+ * (personal skills override superpowers skills).
102
+ *
103
+ * @param {string} skillName - Name like "superpowers:brainstorming" or "my-skill"
104
+ * @param {string} superpowersDir - Path to superpowers skills directory
105
+ * @param {string} personalDir - Path to personal skills directory
106
+ * @returns {{skillFile: string, sourceType: string, skillPath: string} | null}
107
+ */
108
+ function resolveSkillPath(skillName, superpowersDir, personalDir) {
109
+ // Strip superpowers: prefix if present
110
+ const forceSuperpowers = skillName.startsWith('superpowers:');
111
+ const actualSkillName = forceSuperpowers ? skillName.replace(/^superpowers:/, '') : skillName;
112
+
113
+ // Try personal skills first (unless explicitly superpowers:)
114
+ if (!forceSuperpowers && personalDir) {
115
+ const personalPath = path.join(personalDir, actualSkillName);
116
+ const personalSkillFile = path.join(personalPath, 'SKILL.md');
117
+ if (fs.existsSync(personalSkillFile)) {
118
+ return {
119
+ skillFile: personalSkillFile,
120
+ sourceType: 'personal',
121
+ skillPath: actualSkillName
122
+ };
123
+ }
124
+ }
125
+
126
+ // Try superpowers skills
127
+ if (superpowersDir) {
128
+ const superpowersPath = path.join(superpowersDir, actualSkillName);
129
+ const superpowersSkillFile = path.join(superpowersPath, 'SKILL.md');
130
+ if (fs.existsSync(superpowersSkillFile)) {
131
+ return {
132
+ skillFile: superpowersSkillFile,
133
+ sourceType: 'superpowers',
134
+ skillPath: actualSkillName
135
+ };
136
+ }
137
+ }
138
+
139
+ return null;
140
+ }
141
+
142
+ /**
143
+ * Check if a git repository has updates available.
144
+ *
145
+ * @param {string} repoDir - Path to git repository
146
+ * @returns {boolean} - True if updates are available
147
+ */
148
+ function checkForUpdates(repoDir) {
149
+ try {
150
+ // Quick check with 3 second timeout to avoid delays if network is down
151
+ const output = execSync('git fetch origin && git status --porcelain=v1 --branch', {
152
+ cwd: repoDir,
153
+ timeout: 3000,
154
+ encoding: 'utf8',
155
+ stdio: 'pipe'
156
+ });
157
+
158
+ // Parse git status output to see if we're behind
159
+ const statusLines = output.split('\n');
160
+ for (const line of statusLines) {
161
+ if (line.startsWith('## ') && line.includes('[behind ')) {
162
+ return true; // We're behind remote
163
+ }
164
+ }
165
+ return false; // Up to date
166
+ } catch (error) {
167
+ // Network down, git error, timeout, etc. - don't block bootstrap
168
+ return false;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Strip YAML frontmatter from skill content, returning just the content.
174
+ *
175
+ * @param {string} content - Full content including frontmatter
176
+ * @returns {string} - Content without frontmatter
177
+ */
178
+ function stripFrontmatter(content) {
179
+ const lines = content.split('\n');
180
+ let inFrontmatter = false;
181
+ let frontmatterEnded = false;
182
+ const contentLines = [];
183
+
184
+ for (const line of lines) {
185
+ if (line.trim() === '---') {
186
+ if (inFrontmatter) {
187
+ frontmatterEnded = true;
188
+ continue;
189
+ }
190
+ inFrontmatter = true;
191
+ continue;
192
+ }
193
+
194
+ if (frontmatterEnded || !inFrontmatter) {
195
+ contentLines.push(line);
196
+ }
197
+ }
198
+
199
+ return contentLines.join('\n').trim();
200
+ }
201
+
202
+ export {
203
+ extractFrontmatter,
204
+ findSkillsInDir,
205
+ resolveSkillPath,
206
+ checkForUpdates,
207
+ stripFrontmatter
208
+ };
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: brainstorming
3
+ description: Use when creating or developing, before writing code or implementation plans - refines rough ideas into fully-formed designs through collaborative questioning, alternative exploration, and incremental validation. Don't use during clear 'mechanical' processes
4
+ ---
5
+
6
+ # Brainstorming Ideas Into Designs
7
+
8
+ ## Overview
9
+
10
+ Help turn ideas into fully formed designs and specs through natural collaborative dialogue.
11
+
12
+ Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design in small sections (200-300 words), checking after each section whether it looks right so far.
13
+
14
+ ## The Process
15
+
16
+ **Understanding the idea:**
17
+ - Check out the current project state first (files, docs, recent commits)
18
+ - Ask questions one at a time to refine the idea
19
+ - Prefer multiple choice questions when possible, but open-ended is fine too
20
+ - Only one question per message - if a topic needs more exploration, break it into multiple questions
21
+ - Focus on understanding: purpose, constraints, success criteria
22
+
23
+ **Exploring approaches:**
24
+ - Propose 2-3 different approaches with trade-offs
25
+ - Present options conversationally with your recommendation and reasoning
26
+ - Lead with your recommended option and explain why
27
+
28
+ **Presenting the design:**
29
+ - Once you believe you understand what you're building, present the design
30
+ - Break it into sections of 200-300 words
31
+ - Ask after each section whether it looks right so far
32
+ - Cover: architecture, components, data flow, error handling, testing
33
+ - Be ready to go back and clarify if something doesn't make sense
34
+
35
+ ## After the Design
36
+
37
+ **Documentation:**
38
+ - Write the validated design to `docs/plans/YYYY-MM-DD-<topic>-design.md`
39
+ - Use elements-of-style:writing-clearly-and-concisely skill if available
40
+ - Commit the design document to git
41
+
42
+ **Implementation (if continuing):**
43
+ - Ask: "Ready to set up for implementation?"
44
+ - Use superpowers:using-git-worktrees to create isolated workspace
45
+ - Use superpowers:writing-plans to create detailed implementation plan
46
+
47
+ ## Key Principles
48
+
49
+ - **One question at a time** - Don't overwhelm with multiple questions
50
+ - **Multiple choice preferred** - Easier to answer than open-ended when possible
51
+ - **YAGNI ruthlessly** - Remove unnecessary features from all designs
52
+ - **Explore alternatives** - Always propose 2-3 approaches before settling
53
+ - **Incremental validation** - Present design in sections, validate each
54
+ - **Be flexible** - Go back and clarify when something doesn't make sense
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: condition-based-waiting
3
+ description: Use when tests have race conditions, timing dependencies, or inconsistent pass/fail behavior - replaces arbitrary timeouts with condition polling to wait for actual state changes, eliminating flaky tests from timing guesses
4
+ ---
5
+
6
+ # Condition-Based Waiting
7
+
8
+ ## Overview
9
+
10
+ Flaky tests often guess at timing with arbitrary delays. This creates race conditions where tests pass on fast machines but fail under load or in CI.
11
+
12
+ **Core principle:** Wait for the actual condition you care about, not a guess about how long it takes.
13
+
14
+ ## When to Use
15
+
16
+ ```dot
17
+ digraph when_to_use {
18
+ "Test uses setTimeout/sleep?" [shape=diamond];
19
+ "Testing timing behavior?" [shape=diamond];
20
+ "Document WHY timeout needed" [shape=box];
21
+ "Use condition-based waiting" [shape=box];
22
+
23
+ "Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"];
24
+ "Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"];
25
+ "Testing timing behavior?" -> "Use condition-based waiting" [label="no"];
26
+ }
27
+ ```
28
+
29
+ **Use when:**
30
+ - Tests have arbitrary delays (`setTimeout`, `sleep`, `time.sleep()`)
31
+ - Tests are flaky (pass sometimes, fail under load)
32
+ - Tests timeout when run in parallel
33
+ - Waiting for async operations to complete
34
+
35
+ **Don't use when:**
36
+ - Testing actual timing behavior (debounce, throttle intervals)
37
+ - Always document WHY if using arbitrary timeout
38
+
39
+ ## Core Pattern
40
+
41
+ ```typescript
42
+ // ❌ BEFORE: Guessing at timing
43
+ await new Promise(r => setTimeout(r, 50));
44
+ const result = getResult();
45
+ expect(result).toBeDefined();
46
+
47
+ // ✅ AFTER: Waiting for condition
48
+ await waitFor(() => getResult() !== undefined);
49
+ const result = getResult();
50
+ expect(result).toBeDefined();
51
+ ```
52
+
53
+ ## Quick Patterns
54
+
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)` |
62
+
63
+ ## Implementation
64
+
65
+ Generic polling function:
66
+ ```typescript
67
+ async function waitFor<T>(
68
+ condition: () => T | undefined | null | false,
69
+ description: string,
70
+ timeoutMs = 5000
71
+ ): Promise<T> {
72
+ const startTime = Date.now();
73
+
74
+ while (true) {
75
+ const result = condition();
76
+ if (result) return result;
77
+
78
+ if (Date.now() - startTime > timeoutMs) {
79
+ throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
80
+ }
81
+
82
+ await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
83
+ }
84
+ }
85
+ ```
86
+
87
+ See @example.ts for complete implementation with domain-specific helpers (`waitForEvent`, `waitForEventCount`, `waitForEventMatch`) from actual debugging session.
88
+
89
+ ## Common Mistakes
90
+
91
+ **❌ Polling too fast:** `setTimeout(check, 1)` - wastes CPU
92
+ **✅ Fix:** Poll every 10ms
93
+
94
+ **❌ No timeout:** Loop forever if condition never met
95
+ **✅ Fix:** Always include timeout with clear error
96
+
97
+ **❌ Stale data:** Cache state before loop
98
+ **✅ Fix:** Call getter inside loop for fresh data
99
+
100
+ ## When Arbitrary Timeout IS Correct
101
+
102
+ ```typescript
103
+ // 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
106
+ // 200ms = 2 ticks at 100ms intervals - documented and justified
107
+ ```
108
+
109
+ **Requirements:**
110
+ 1. First wait for triggering condition
111
+ 2. Based on known timing (not guessing)
112
+ 3. Comment explaining WHY
113
+
114
+ ## Real-World Impact
115
+
116
+ From debugging session (2025-10-03):
117
+ - Fixed 15 flaky tests across 3 files
118
+ - Pass rate: 60% → 100%
119
+ - Execution time: 40% faster
120
+ - No more race conditions
@@ -0,0 +1,158 @@
1
+ // Complete implementation of condition-based waiting utilities
2
+ // From: Lace test infrastructure improvements (2025-10-03)
3
+ // Context: Fixed 15 flaky tests by replacing arbitrary timeouts
4
+
5
+ import type { ThreadManager } from '~/threads/thread-manager';
6
+ import type { LaceEvent, LaceEventType } from '~/threads/types';
7
+
8
+ /**
9
+ * Wait for a specific event type to appear in thread
10
+ *
11
+ * @param threadManager - The thread manager to query
12
+ * @param threadId - Thread to check for events
13
+ * @param eventType - Type of event to wait for
14
+ * @param timeoutMs - Maximum time to wait (default 5000ms)
15
+ * @returns Promise resolving to the first matching event
16
+ *
17
+ * Example:
18
+ * await waitForEvent(threadManager, agentThreadId, 'TOOL_RESULT');
19
+ */
20
+ export function waitForEvent(
21
+ threadManager: ThreadManager,
22
+ threadId: string,
23
+ eventType: LaceEventType,
24
+ timeoutMs = 5000
25
+ ): Promise<LaceEvent> {
26
+ return new Promise((resolve, reject) => {
27
+ const startTime = Date.now();
28
+
29
+ const check = () => {
30
+ const events = threadManager.getEvents(threadId);
31
+ const event = events.find((e) => e.type === eventType);
32
+
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
+ };
41
+
42
+ check();
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Wait for a specific number of events of a given type
48
+ *
49
+ * @param threadManager - The thread manager to query
50
+ * @param threadId - Thread to check for events
51
+ * @param eventType - Type of event to wait for
52
+ * @param count - Number of events to wait for
53
+ * @param timeoutMs - Maximum time to wait (default 5000ms)
54
+ * @returns Promise resolving to all matching events once count is reached
55
+ *
56
+ * Example:
57
+ * // Wait for 2 AGENT_MESSAGE events (initial response + continuation)
58
+ * await waitForEventCount(threadManager, agentThreadId, 'AGENT_MESSAGE', 2);
59
+ */
60
+ export function waitForEventCount(
61
+ threadManager: ThreadManager,
62
+ threadId: string,
63
+ eventType: LaceEventType,
64
+ count: number,
65
+ timeoutMs = 5000
66
+ ): Promise<LaceEvent[]> {
67
+ return new Promise((resolve, reject) => {
68
+ const startTime = Date.now();
69
+
70
+ const check = () => {
71
+ const events = threadManager.getEvents(threadId);
72
+ const matchingEvents = events.filter((e) => e.type === eventType);
73
+
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
+ };
86
+
87
+ check();
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Wait for an event matching a custom predicate
93
+ * Useful when you need to check event data, not just type
94
+ *
95
+ * @param threadManager - The thread manager to query
96
+ * @param threadId - Thread to check for events
97
+ * @param predicate - Function that returns true when event matches
98
+ * @param description - Human-readable description for error messages
99
+ * @param timeoutMs - Maximum time to wait (default 5000ms)
100
+ * @returns Promise resolving to the first matching event
101
+ *
102
+ * Example:
103
+ * // Wait for TOOL_RESULT with specific ID
104
+ * await waitForEventMatch(
105
+ * threadManager,
106
+ * agentThreadId,
107
+ * (e) => e.type === 'TOOL_RESULT' && e.data.id === 'call_123',
108
+ * 'TOOL_RESULT with id=call_123'
109
+ * );
110
+ */
111
+ export function waitForEventMatch(
112
+ threadManager: ThreadManager,
113
+ threadId: string,
114
+ predicate: (event: LaceEvent) => boolean,
115
+ description: string,
116
+ timeoutMs = 5000
117
+ ): Promise<LaceEvent> {
118
+ return new Promise((resolve, reject) => {
119
+ const startTime = Date.now();
120
+
121
+ const check = () => {
122
+ const events = threadManager.getEvents(threadId);
123
+ const event = events.find(predicate);
124
+
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
+ };
133
+
134
+ check();
135
+ });
136
+ }
137
+
138
+ // Usage example from actual debugging session:
139
+ //
140
+ // BEFORE (flaky):
141
+ // ---------------
142
+ // const messagePromise = agent.sendMessage('Execute tools');
143
+ // await new Promise(r => setTimeout(r, 300)); // Hope tools start in 300ms
144
+ // agent.abort();
145
+ // await messagePromise;
146
+ // await new Promise(r => setTimeout(r, 50)); // Hope results arrive in 50ms
147
+ // expect(toolResults.length).toBe(2); // Fails randomly
148
+ //
149
+ // AFTER (reliable):
150
+ // ----------------
151
+ // const messagePromise = agent.sendMessage('Execute tools');
152
+ // await waitForEventCount(threadManager, threadId, 'TOOL_CALL', 2); // Wait for tools to start
153
+ // agent.abort();
154
+ // await messagePromise;
155
+ // await waitForEventCount(threadManager, threadId, 'TOOL_RESULT', 2); // Wait for results
156
+ // expect(toolResults.length).toBe(2); // Always succeeds
157
+ //
158
+ // Result: 60% pass rate → 100%, 40% faster execution