pi-gauntlet 4.0.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.
- package/CHANGELOG.md +300 -0
- package/LICENSE +24 -0
- package/README.md +278 -0
- package/agents/code-reviewer.md +48 -0
- package/agents/conformance-reviewer.md +139 -0
- package/agents/implementer.md +40 -0
- package/agents/spec-council-member.md +47 -0
- package/agents/spec-council-synthesizer.md +39 -0
- package/agents/spec-reviewer.md +47 -0
- package/agents/spec-summarizer.md +42 -0
- package/bin/install-agents.mjs +141 -0
- package/extensions/phase-tracker.ts +622 -0
- package/extensions/plan-tracker.ts +308 -0
- package/extensions/verify-before-ship.ts +132 -0
- package/package.json +43 -0
- package/skills/brainstorming/SKILL.md +290 -0
- package/skills/dispatching-parallel-agents/SKILL.md +192 -0
- package/skills/finishing-a-development-branch/SKILL.md +311 -0
- package/skills/receiving-code-review/SKILL.md +200 -0
- package/skills/requesting-code-review/SKILL.md +115 -0
- package/skills/requesting-code-review/code-reviewer.md +166 -0
- package/skills/roasting-the-spec/SKILL.md +139 -0
- package/skills/subagent-driven-development/SKILL.md +223 -0
- package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +25 -0
- package/skills/subagent-driven-development/implementer-prompt.md +113 -0
- package/skills/subagent-driven-development/spec-reviewer-prompt.md +68 -0
- package/skills/systematic-debugging/SKILL.md +151 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/find-polluter.sh +63 -0
- package/skills/systematic-debugging/reference/rationalizations.md +61 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/test-driven-development/SKILL.md +230 -0
- package/skills/test-driven-development/reference/examples.md +99 -0
- package/skills/test-driven-development/reference/rationalizations.md +65 -0
- package/skills/test-driven-development/reference/when-stuck.md +31 -0
- package/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/skills/using-git-worktrees/SKILL.md +193 -0
- package/skills/verification-before-completion/SKILL.md +169 -0
- package/skills/verification-before-completion/reference/conformance-check.md +220 -0
- package/skills/writing-plans/SKILL.md +244 -0
- package/skills/writing-skills/SKILL.md +429 -0
- package/skills/writing-skills/reference/anthropic-best-practices.md +1130 -0
- package/skills/writing-skills/reference/persuasion.md +187 -0
- package/skills/writing-skills/reference/testing-skills-with-subagents.md +384 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Implementer Subagent Prompt Template
|
|
2
|
+
|
|
3
|
+
Use this template when dispatching an implementer subagent.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Dispatch a subagent with this prompt:
|
|
7
|
+
description: "Implement Task N: [task name]"
|
|
8
|
+
prompt: |
|
|
9
|
+
You are implementing Task N: [task name]
|
|
10
|
+
|
|
11
|
+
## Task Description
|
|
12
|
+
|
|
13
|
+
[FULL TEXT of task from plan - paste it here, don't make subagent read file]
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
[Scene-setting: where this fits, dependencies, architectural context]
|
|
18
|
+
|
|
19
|
+
## Before You Begin
|
|
20
|
+
|
|
21
|
+
If you have questions about:
|
|
22
|
+
- The requirements or acceptance criteria
|
|
23
|
+
- The approach or implementation strategy
|
|
24
|
+
- Dependencies or assumptions
|
|
25
|
+
- Anything unclear in the task description
|
|
26
|
+
|
|
27
|
+
**Ask them now.** Raise any concerns before starting work.
|
|
28
|
+
|
|
29
|
+
## Your Job
|
|
30
|
+
|
|
31
|
+
Once you're clear on requirements:
|
|
32
|
+
1. Implement exactly what the task specifies
|
|
33
|
+
2. Write tests (following TDD — failing test first for production code)
|
|
34
|
+
3. Verify implementation works
|
|
35
|
+
4. Commit your work
|
|
36
|
+
5. Self-review (see below)
|
|
37
|
+
6. Report back
|
|
38
|
+
|
|
39
|
+
Work from: [directory]
|
|
40
|
+
|
|
41
|
+
**While you work:** If you encounter something unexpected or unclear, **ask questions**.
|
|
42
|
+
It's always OK to pause and clarify. Don't guess or make assumptions.
|
|
43
|
+
|
|
44
|
+
## Code Organization
|
|
45
|
+
|
|
46
|
+
You reason best about code you can hold in context at once, and your edits are more
|
|
47
|
+
reliable when files are focused. Keep this in mind:
|
|
48
|
+
- Follow the file structure defined in the plan.
|
|
49
|
+
- Each file should have one clear responsibility with a well-defined interface.
|
|
50
|
+
- If a file you're creating is growing beyond the plan's intent, stop and report it
|
|
51
|
+
as DONE_WITH_CONCERNS — don't split files on your own without plan guidance.
|
|
52
|
+
- If an existing file you're modifying is already large or tangled, work carefully
|
|
53
|
+
and note it as a concern in your report.
|
|
54
|
+
- In existing codebases, follow established patterns. Improve code you're touching
|
|
55
|
+
the way a good developer would, but don't restructure things outside your task.
|
|
56
|
+
|
|
57
|
+
## When You're in Over Your Head
|
|
58
|
+
|
|
59
|
+
It is always OK to stop and say "this is too hard for me." Bad work is worse than
|
|
60
|
+
no work. You will not be penalized for escalating.
|
|
61
|
+
|
|
62
|
+
**STOP and escalate when:**
|
|
63
|
+
- The task requires architectural decisions with multiple valid approaches.
|
|
64
|
+
- You need to understand code beyond what was provided and can't find clarity.
|
|
65
|
+
- You feel uncertain about whether your approach is correct.
|
|
66
|
+
- The task involves restructuring existing code in ways the plan didn't anticipate.
|
|
67
|
+
- You've been reading file after file trying to understand the system without progress.
|
|
68
|
+
|
|
69
|
+
**How to escalate:** Report back with status `BLOCKED` or `NEEDS_CONTEXT`. Describe
|
|
70
|
+
specifically what you're stuck on, what you've tried, and what kind of help you need.
|
|
71
|
+
The orchestrator can provide more context, re-dispatch with a more capable model,
|
|
72
|
+
or break the task into smaller pieces.
|
|
73
|
+
|
|
74
|
+
## Before Reporting Back: Self-Review
|
|
75
|
+
|
|
76
|
+
Review your work with fresh eyes. Ask yourself:
|
|
77
|
+
|
|
78
|
+
**Completeness:**
|
|
79
|
+
- Did I fully implement everything in the spec?
|
|
80
|
+
- Did I miss any requirements?
|
|
81
|
+
- Are there edge cases I didn't handle?
|
|
82
|
+
|
|
83
|
+
**Quality:**
|
|
84
|
+
- Is this my best work?
|
|
85
|
+
- Are names clear and accurate (match what things do, not how they work)?
|
|
86
|
+
- Is the code clean and maintainable?
|
|
87
|
+
|
|
88
|
+
**Discipline:**
|
|
89
|
+
- Did I avoid overbuilding (YAGNI)?
|
|
90
|
+
- Did I only build what was requested?
|
|
91
|
+
- Did I follow existing patterns in the codebase?
|
|
92
|
+
|
|
93
|
+
**Testing:**
|
|
94
|
+
- Do tests actually verify behavior (not just mock behavior)?
|
|
95
|
+
- Did I follow TDD — failing test first for production code?
|
|
96
|
+
- Are tests comprehensive?
|
|
97
|
+
|
|
98
|
+
If you find issues during self-review, fix them now before reporting.
|
|
99
|
+
|
|
100
|
+
## Report Format
|
|
101
|
+
|
|
102
|
+
When done, report:
|
|
103
|
+
- **Status:** `DONE` | `DONE_WITH_CONCERNS` | `BLOCKED` | `NEEDS_CONTEXT`
|
|
104
|
+
- What you implemented (or what you attempted, if blocked)
|
|
105
|
+
- What you tested and test results
|
|
106
|
+
- Files changed
|
|
107
|
+
- Self-review findings (if any)
|
|
108
|
+
- Any issues or concerns
|
|
109
|
+
|
|
110
|
+
Use `DONE_WITH_CONCERNS` if you completed the work but have doubts about correctness.
|
|
111
|
+
Use `BLOCKED` if you cannot complete the task. Use `NEEDS_CONTEXT` if you need
|
|
112
|
+
information that wasn't provided. Never silently produce work you're unsure about.
|
|
113
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Spec Compliance Reviewer Prompt Template
|
|
2
|
+
|
|
3
|
+
Use this template when dispatching a spec compliance reviewer subagent.
|
|
4
|
+
|
|
5
|
+
**Purpose:** Verify implementer built what was requested (nothing more, nothing less)
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Dispatch a subagent with this prompt:
|
|
9
|
+
description: "Review spec compliance for Task N"
|
|
10
|
+
prompt: |
|
|
11
|
+
You are reviewing whether an implementation matches its specification.
|
|
12
|
+
|
|
13
|
+
## What Was Requested
|
|
14
|
+
|
|
15
|
+
[FULL TEXT of task requirements]
|
|
16
|
+
|
|
17
|
+
## What Implementer Claims They Built
|
|
18
|
+
|
|
19
|
+
[From implementer's report]
|
|
20
|
+
|
|
21
|
+
## CRITICAL: Do Not Trust the Report
|
|
22
|
+
|
|
23
|
+
The implementer finished suspiciously quickly. Their report may be incomplete,
|
|
24
|
+
inaccurate, or optimistic. You MUST verify everything independently.
|
|
25
|
+
|
|
26
|
+
**DO NOT:**
|
|
27
|
+
- Take their word for what they implemented
|
|
28
|
+
- Trust their claims about completeness
|
|
29
|
+
- Accept their interpretation of requirements
|
|
30
|
+
|
|
31
|
+
**DO:**
|
|
32
|
+
- Read the actual code they wrote
|
|
33
|
+
- Compare actual implementation to requirements line by line
|
|
34
|
+
- Check for missing pieces they claimed to implement
|
|
35
|
+
- Look for extra features they didn't mention
|
|
36
|
+
|
|
37
|
+
## Boundaries
|
|
38
|
+
|
|
39
|
+
- **Read code and compare to spec: yes**
|
|
40
|
+
- **Edit, create, or delete any files: NO**
|
|
41
|
+
- You are a reviewer. Your output is a written report listing what matches and what doesn't.
|
|
42
|
+
- If you find issues, describe them — do NOT fix them.
|
|
43
|
+
|
|
44
|
+
## Your Job
|
|
45
|
+
|
|
46
|
+
Read the implementation code and verify:
|
|
47
|
+
|
|
48
|
+
**Missing requirements:**
|
|
49
|
+
- Did they implement everything that was requested?
|
|
50
|
+
- Are there requirements they skipped or missed?
|
|
51
|
+
- Did they claim something works but didn't actually implement it?
|
|
52
|
+
|
|
53
|
+
**Extra/unneeded work:**
|
|
54
|
+
- Did they build things that weren't requested?
|
|
55
|
+
- Did they over-engineer or add unnecessary features?
|
|
56
|
+
- Did they add "nice to haves" that weren't in spec?
|
|
57
|
+
|
|
58
|
+
**Misunderstandings:**
|
|
59
|
+
- Did they interpret requirements differently than intended?
|
|
60
|
+
- Did they solve the wrong problem?
|
|
61
|
+
- Did they implement the right feature but wrong way?
|
|
62
|
+
|
|
63
|
+
**Verify by reading code, not by trusting report.**
|
|
64
|
+
|
|
65
|
+
Report:
|
|
66
|
+
- ✅ Spec compliant (if everything matches after code inspection)
|
|
67
|
+
- ❌ Issues found: [list specifically what's missing or extra, with file:line references]
|
|
68
|
+
```
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: systematic-debugging
|
|
3
|
+
description: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
> **Related skills:** Write a failing test for the bug with `/skill:test-driven-development`. Verify the fix with `/skill:verification-before-completion`.
|
|
7
|
+
|
|
8
|
+
# Systematic Debugging
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Random fixes waste time and create new bugs. Quick patches mask underlying issues.
|
|
13
|
+
|
|
14
|
+
**Core principle:** ALWAYS find root cause before attempting fixes. Symptom fixes are failure.
|
|
15
|
+
|
|
16
|
+
**Violating the letter of this process is violating the spirit of debugging.**
|
|
17
|
+
|
|
18
|
+
Debug discipline is enforced by this skill, not by runtime hooks. The pi `verify-before-ship` extension only gates ship commands; it does not track investigation patterns. Hold yourself to the process below.
|
|
19
|
+
|
|
20
|
+
## The Iron Law
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If you haven't completed Phase 1, you cannot propose fixes.
|
|
27
|
+
|
|
28
|
+
## When to Use
|
|
29
|
+
|
|
30
|
+
Use for ANY technical issue: test failures, bugs, unexpected behavior, performance problems, build failures, integration issues.
|
|
31
|
+
|
|
32
|
+
**Use this ESPECIALLY when:**
|
|
33
|
+
- Under time pressure (emergencies make guessing tempting)
|
|
34
|
+
- "Just one quick fix" seems obvious
|
|
35
|
+
- You've already tried multiple fixes
|
|
36
|
+
- Previous fix didn't work
|
|
37
|
+
- You don't fully understand the issue
|
|
38
|
+
|
|
39
|
+
**Don't skip when:**
|
|
40
|
+
- Issue seems simple (simple bugs have root causes too)
|
|
41
|
+
- You're in a hurry (rushing guarantees rework)
|
|
42
|
+
|
|
43
|
+
## The Four Phases
|
|
44
|
+
|
|
45
|
+
You MUST complete each phase before proceeding to the next.
|
|
46
|
+
|
|
47
|
+
### Phase 1: Root Cause Investigation
|
|
48
|
+
|
|
49
|
+
**BEFORE attempting ANY fix:**
|
|
50
|
+
|
|
51
|
+
1. **Read Error Messages Carefully** — Don't skip past errors or warnings. Read stack traces completely. Note line numbers, file paths, error codes.
|
|
52
|
+
|
|
53
|
+
2. **Reproduce Consistently** — Can you trigger it reliably? What are the exact steps? If not reproducible → gather more data, don't guess.
|
|
54
|
+
|
|
55
|
+
3. **Check Recent Changes** — Git diff, recent commits, new dependencies, config changes, environmental differences.
|
|
56
|
+
|
|
57
|
+
4. **Gather Evidence in Multi-Component Systems** — For each component boundary: log what enters, what exits, verify config propagation. Run once to see WHERE it breaks, then investigate that component.
|
|
58
|
+
|
|
59
|
+
**Example (multi-layer system):**
|
|
60
|
+
```bash
|
|
61
|
+
# Layer 1: Workflow
|
|
62
|
+
echo "=== Secrets available: ==="
|
|
63
|
+
echo "IDENTITY: ${IDENTITY:+SET}${IDENTITY:-UNSET}"
|
|
64
|
+
|
|
65
|
+
# Layer 2: Build script
|
|
66
|
+
echo "=== Env vars in build script: ==="
|
|
67
|
+
env | grep IDENTITY || echo "IDENTITY not in environment"
|
|
68
|
+
|
|
69
|
+
# Layer 3: Signing
|
|
70
|
+
echo "=== Keychain state: ==="
|
|
71
|
+
security list-keychains
|
|
72
|
+
security find-identity -v
|
|
73
|
+
```
|
|
74
|
+
**This reveals:** Which layer fails (e.g., secrets → workflow ✓, workflow → build ✗)
|
|
75
|
+
|
|
76
|
+
5. **Trace Data Flow** — Where does the bad value originate? What called this with the bad value? Keep tracing up until you find the source. Fix at source, not at symptom. See `root-cause-tracing.md` for the complete technique.
|
|
77
|
+
|
|
78
|
+
### Phase 2: Pattern Analysis
|
|
79
|
+
|
|
80
|
+
1. **Find Working Examples** — Locate similar working code in same codebase.
|
|
81
|
+
2. **Compare Against References** — Read reference implementation COMPLETELY. Don't skim.
|
|
82
|
+
3. **Identify Differences** — List every difference, however small. Don't assume "that can't matter."
|
|
83
|
+
4. **Understand Dependencies** — What components, settings, config, environment does this need?
|
|
84
|
+
|
|
85
|
+
### Phase 3: Hypothesis and Testing
|
|
86
|
+
|
|
87
|
+
1. **Form Single Hypothesis** — State clearly: "I think X is the root cause because Y." Be specific, not vague.
|
|
88
|
+
2. **Test Minimally** — Make the SMALLEST possible change. One variable at a time. Don't fix multiple things at once.
|
|
89
|
+
3. **Verify Before Continuing** — Did it work? Yes → Phase 4. No → Form NEW hypothesis. DON'T add more fixes on top.
|
|
90
|
+
4. **When You Don't Know** — Say "I don't understand X." Don't pretend to know. Ask for help. Research more. The escape valve is real: an honest "I'm stuck on X" beats a confident wrong fix every time.
|
|
91
|
+
|
|
92
|
+
### Phase 4: Implementation
|
|
93
|
+
|
|
94
|
+
1. **Create Failing Test Case** — Use `/skill:test-driven-development` for writing proper failing tests. MUST have before fixing.
|
|
95
|
+
|
|
96
|
+
2. **Implement Single Fix** — ONE change at a time. No "while I'm here" improvements. No bundled refactoring.
|
|
97
|
+
|
|
98
|
+
3. **Verify Fix** — Test passes? No other tests broken? Issue actually resolved?
|
|
99
|
+
|
|
100
|
+
4. **If Fix Doesn't Work:**
|
|
101
|
+
- If < 3 attempts: Return to Phase 1, re-analyze with new information
|
|
102
|
+
- **If ≥ 3 attempts: STOP (see below)**
|
|
103
|
+
|
|
104
|
+
### When 3+ Fixes Fail: Question Architecture
|
|
105
|
+
|
|
106
|
+
**This is NOT a failed hypothesis — it's a wrong architecture.**
|
|
107
|
+
|
|
108
|
+
Pattern indicating architectural problem:
|
|
109
|
+
- Each fix reveals new shared state/coupling in different places
|
|
110
|
+
- Fixes require "massive refactoring" to implement
|
|
111
|
+
- Each fix creates new symptoms elsewhere
|
|
112
|
+
|
|
113
|
+
**STOP and question fundamentals:**
|
|
114
|
+
- Is this pattern fundamentally sound?
|
|
115
|
+
- Are we sticking with it through sheer inertia?
|
|
116
|
+
- Should we refactor architecture vs. continue fixing symptoms?
|
|
117
|
+
|
|
118
|
+
**Discuss with your human partner before attempting more fixes.**
|
|
119
|
+
|
|
120
|
+
## Red Flags and Rationalizations
|
|
121
|
+
|
|
122
|
+
Read `reference/rationalizations.md` for the full table of excuses and the partner-signal redirections. Short version:
|
|
123
|
+
|
|
124
|
+
- "Quick fix for now, investigate later" → return to Phase 1.
|
|
125
|
+
- "Just try changing X and see if it works" → return to Phase 1.
|
|
126
|
+
- "It's probably X, let me fix that" → return to Phase 1.
|
|
127
|
+
- "One more fix attempt" after 2+ failures → question architecture, don't fix again.
|
|
128
|
+
- Each fix reveals a new problem in a different place → question architecture.
|
|
129
|
+
|
|
130
|
+
## When Process Reveals "No Root Cause"
|
|
131
|
+
|
|
132
|
+
If investigation reveals issue is truly environmental, timing-dependent, or external:
|
|
133
|
+
1. Document what you investigated
|
|
134
|
+
2. Implement appropriate handling (retry, timeout, error message)
|
|
135
|
+
3. Add monitoring/logging for future investigation
|
|
136
|
+
|
|
137
|
+
**But:** 95% of "no root cause" cases are incomplete investigation.
|
|
138
|
+
|
|
139
|
+
## Supporting Techniques
|
|
140
|
+
|
|
141
|
+
These techniques are part of systematic debugging and available in this directory:
|
|
142
|
+
|
|
143
|
+
- **`root-cause-tracing.md`** — Trace bugs backward through call stack to find original trigger
|
|
144
|
+
- **`defense-in-depth.md`** — Add validation at multiple layers after finding root cause
|
|
145
|
+
- **`condition-based-waiting.md`** — Replace arbitrary timeouts with condition polling
|
|
146
|
+
|
|
147
|
+
Read directly when needed: `reference/rationalizations.md` and the supporting `*.md` files in this directory.
|
|
148
|
+
|
|
149
|
+
## Project overrides
|
|
150
|
+
|
|
151
|
+
If `.pi/gauntlet-overrides.md` exists, read it. Any sections relevant to this skill — by name match, by topic (routing, verification, worktrees, etc.), or by workflow convention — override or extend the instructions above. Project-local `AGENTS.md` is already in context — check it for project-specific routing tables, service paths, and verification commands.
|
|
@@ -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
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Condition-Based Waiting
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
**Core principle:** Wait for the actual condition you care about, not a guess about how long it takes.
|
|
8
|
+
|
|
9
|
+
## When to Use
|
|
10
|
+
|
|
11
|
+
```dot
|
|
12
|
+
digraph when_to_use {
|
|
13
|
+
"Test uses setTimeout/sleep?" [shape=diamond];
|
|
14
|
+
"Testing timing behavior?" [shape=diamond];
|
|
15
|
+
"Document WHY timeout needed" [shape=box];
|
|
16
|
+
"Use condition-based waiting" [shape=box];
|
|
17
|
+
|
|
18
|
+
"Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"];
|
|
19
|
+
"Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"];
|
|
20
|
+
"Testing timing behavior?" -> "Use condition-based waiting" [label="no"];
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Use when:**
|
|
25
|
+
- Tests have arbitrary delays (`setTimeout`, `sleep`, `time.sleep()`)
|
|
26
|
+
- Tests are flaky (pass sometimes, fail under load)
|
|
27
|
+
- Tests timeout when run in parallel
|
|
28
|
+
- Waiting for async operations to complete
|
|
29
|
+
|
|
30
|
+
**Don't use when:**
|
|
31
|
+
- Testing actual timing behavior (debounce, throttle intervals)
|
|
32
|
+
- Always document WHY if using arbitrary timeout
|
|
33
|
+
|
|
34
|
+
## Core Pattern
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// ❌ BEFORE: Guessing at timing
|
|
38
|
+
await new Promise(r => setTimeout(r, 50));
|
|
39
|
+
const result = getResult();
|
|
40
|
+
expect(result).toBeDefined();
|
|
41
|
+
|
|
42
|
+
// ✅ AFTER: Waiting for condition
|
|
43
|
+
await waitFor(() => getResult() !== undefined);
|
|
44
|
+
const result = getResult();
|
|
45
|
+
expect(result).toBeDefined();
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Patterns
|
|
49
|
+
|
|
50
|
+
| Scenario | Pattern |
|
|
51
|
+
|----------|---------|
|
|
52
|
+
| Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'))` |
|
|
53
|
+
| Wait for state | `waitFor(() => machine.state === 'ready')` |
|
|
54
|
+
| Wait for count | `waitFor(() => items.length >= 5)` |
|
|
55
|
+
| Wait for file | `waitFor(() => fs.existsSync(path))` |
|
|
56
|
+
| Complex condition | `waitFor(() => obj.ready && obj.value > 10)` |
|
|
57
|
+
|
|
58
|
+
## Implementation
|
|
59
|
+
|
|
60
|
+
Generic polling function:
|
|
61
|
+
```typescript
|
|
62
|
+
async function waitFor<T>(
|
|
63
|
+
condition: () => T | undefined | null | false,
|
|
64
|
+
description: string,
|
|
65
|
+
timeoutMs = 5000
|
|
66
|
+
): Promise<T> {
|
|
67
|
+
const startTime = Date.now();
|
|
68
|
+
|
|
69
|
+
while (true) {
|
|
70
|
+
const result = condition();
|
|
71
|
+
if (result) return result;
|
|
72
|
+
|
|
73
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
74
|
+
throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
See `condition-based-waiting-example.ts` in this directory for complete implementation with domain-specific helpers (`waitForEvent`, `waitForEventCount`, `waitForEventMatch`) from actual debugging session.
|
|
83
|
+
|
|
84
|
+
## Common Mistakes
|
|
85
|
+
|
|
86
|
+
**❌ Polling too fast:** `setTimeout(check, 1)` - wastes CPU
|
|
87
|
+
**✅ Fix:** Poll every 10ms
|
|
88
|
+
|
|
89
|
+
**❌ No timeout:** Loop forever if condition never met
|
|
90
|
+
**✅ Fix:** Always include timeout with clear error
|
|
91
|
+
|
|
92
|
+
**❌ Stale data:** Cache state before loop
|
|
93
|
+
**✅ Fix:** Call getter inside loop for fresh data
|
|
94
|
+
|
|
95
|
+
## When Arbitrary Timeout IS Correct
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// Tool ticks every 100ms - need 2 ticks to verify partial output
|
|
99
|
+
await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition
|
|
100
|
+
await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior
|
|
101
|
+
// 200ms = 2 ticks at 100ms intervals - documented and justified
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Requirements:**
|
|
105
|
+
1. First wait for triggering condition
|
|
106
|
+
2. Based on known timing (not guessing)
|
|
107
|
+
3. Comment explaining WHY
|
|
108
|
+
|
|
109
|
+
## Real-World Impact
|
|
110
|
+
|
|
111
|
+
From debugging session (2025-10-03):
|
|
112
|
+
- Fixed 15 flaky tests across 3 files
|
|
113
|
+
- Pass rate: 60% → 100%
|
|
114
|
+
- Execution time: 40% faster
|
|
115
|
+
- No more race conditions
|