pi-gauntlet 4.13.2 → 5.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.
- package/CHANGELOG.md +10 -0
- package/README.md +8 -8
- package/agents/spec-council-member.md +6 -2
- package/agents/spec-council-synthesizer.md +1 -1
- package/package.json +1 -1
- package/skills/brainstorming/SKILL.md +4 -3
- package/skills/chase-bug/SKILL.md +320 -0
- package/skills/dispatching-parallel-agents/SKILL.md +1 -1
- package/skills/roasting-the-spec/SKILL.md +19 -13
- package/skills/shape-ticket/SKILL.md +32 -16
- package/skills/writing-skills/SKILL.md +3 -3
- package/skills/systematic-debugging/SKILL.md +0 -151
- package/skills/systematic-debugging/condition-based-waiting-example.ts +0 -158
- package/skills/systematic-debugging/condition-based-waiting.md +0 -115
- package/skills/systematic-debugging/defense-in-depth.md +0 -122
- package/skills/systematic-debugging/find-polluter.sh +0 -63
- package/skills/systematic-debugging/reference/rationalizations.md +0 -61
- package/skills/systematic-debugging/root-cause-tracing.md +0 -169
|
@@ -173,23 +173,39 @@ No block, no split. A block failing the identity or outcome test, or naming a no
|
|
|
173
173
|
Inline council dispatch, reusing spec-council config and personas - **not** `/skill:roasting-the-spec` (that skill's contract is spec-file apply mechanics; a tracker draft is not a spec file). No new agents.
|
|
174
174
|
|
|
175
175
|
1. Resolve `gauntlet_setting({ key: "specCouncil" })` when the tool exists. Verdict `council` -> dispatch `spec-council-member`s in parallel plus a `spec-council-synthesizer` chair. Verdict `worker` (or empty members) -> one fresh `worker` critique. Malformed config -> one warning line, then branch on verdict.
|
|
176
|
-
2. **Dispatch shape**, mirroring `/skill:roasting-the-spec`: write the draft body and the source snapshot (original ticket + comments, or the create-mode inputs) to absolute temp files under `mktemp -d`; delimit untrusted snapshots as data. When a split is proposed, the draft artifact holds all N proposed bodies plus their three-line justification blocks (see Split rule) in one file, not a single body.
|
|
177
|
-
3. **Effort: cheap by default.** Append a `:low` thinking suffix to each member's model string at dispatch (this beats the persona's frontmatter `xhigh` pin). Same for the chair: a configured chair string gets any existing suffix replaced with `:low`; an unconfigured chair is dispatched as the parent's model with `:low` appended. The `worker` fallback carries no thinking pin - it runs at the preset's default. **Full-roast escape:** the user may request a full roast, dispatching all model strings bare/as-configured, restoring the xhigh pins.
|
|
176
|
+
2. **Dispatch shape**, mirroring `/skill:roasting-the-spec`: write the draft body and the source snapshot (original ticket + comments, or the create-mode inputs) to absolute temp files under `mktemp -d`; delimit untrusted snapshots as data. When a split is proposed, the draft artifact holds all N proposed bodies plus their three-line justification blocks (see Split rule) in one file, not a single body. Two separate calls - never fuse members and chair into one chain (a fused chain lets one member failure kill the roast before the chair runs). Call 1: one member fanout with `cwd` = repo root, absolute `output` paths per member, run-level `control: { needsAttentionAfterMs: 60000, inFlightSilenceCeilingMs: 240000, inFlightSilenceKillMs: 300000 }` (sits beside `tasks`, not inside each task; effective silence-kill max(300s, 240+60) = 300s - record all three fields verbatim so a pi-cohort default change cannot stretch the kill). Then probe the member output files on disk with item 7's usable test. Call 2: the chair, with the usable member files via `reads`, the same control block (`:low` chair turns are short), and task text that (a) forbids repository access - member disagreement on a fact is reported in the synthesis, never verified against the repo - and (b) states coverage: `Coverage: N of M members reported; <slug>: <reason>` (pi-cohort's kill diagnostic when present, else "no output produced"; omit reasons at full coverage; singular wording when one member reported). Member task text: *the draft at `<path>` is the artifact under review; this ticket brief supersedes your spec-axis template - emit the same findings format against the draft; content-only review: the temp files plus the referenced split-axes reference path are the entire permitted input - do not read, search, or scan the repository; do not edit any file.* Include the absolute path to `reference/split-axes.md` (resolved against this skill's own directory) in each member's task text - members run with `cwd` = the consumer repo, where a package-relative path does not resolve.
|
|
177
|
+
3. **Effort: cheap by default.** Append a `:low` thinking suffix to each member's model string at dispatch (this beats the persona's frontmatter `xhigh` pin). Same for the chair: a configured chair string gets any existing suffix replaced with `:low`; an unconfigured chair is dispatched as the parent's model with `:low` appended. The `worker` fallback carries no thinking pin - it runs at the preset's default. **Full-roast escape:** the user may request a full roast, dispatching all model strings bare/as-configured, restoring the xhigh pins; a full roast reuses the spec-roast control blocks (members `{ needsAttentionAfterMs: 300000, inFlightSilenceCeilingMs: 300000, inFlightSilenceKillMs: 600000 }`, chair `{ needsAttentionAfterMs: 300000, inFlightSilenceCeilingMs: 600000, inFlightSilenceKillMs: 900000 }`) - the 5-minute figures in item 2 are `:low`-only.
|
|
178
178
|
4. **Brief covers three axes**, absorbing the fidelity-review role without a new persona: *fidelity* - compare draft against source intent (original ticket + comments in repair; prompt + answers in create), flag `lost` / `added` / `gap`; and *quality* - problem framing, AC integrity beyond the deterministic gate, scope, wording; and *split soundness* - if the draft proposes a split, test each slice against the split-axes reference (path provided in the task text); an architecture-shaped boundary is reported as a finding line containing the marker `split-axis:` (members keep their existing spec-axis findings template; the marker is a substring flag within it, not a new findings kind), e.g. `- [major] split-axis: <slice> - <why> -> merge`. Members may argue toward one ticket, never propose or endorse a split.
|
|
179
|
-
5. Disposition: unambiguous concrete fixes applied to the draft (one re-pass max); ambiguous findings surfaced at the confirmation gate. Roast edits affect the body draft pre-write only, never posted as a tracker comment, and re-run the deterministic gates (pipeline step 5). Additionally, the parent scans the **member output files directly** for lines containing `split-axis:` (substring match), independent of the chair synthesis; any such finding auto-applies a merge - the split is withdrawn and the draft becomes one ticket with phased AC groups, inside the same one-re-pass budget, and the pre-merge N-body draft is kept alongside: a human re-request of the split at the gate re-presents those N bodies old->new as the approval diff (see the Split rule's sticky override). The chair keeps every other axis; clearing a `split-axis:` finding is not on its path. The same directional rule - toward one ticket, never toward a split - binds the `worker` fallback and the runtime conditional (item 6).
|
|
179
|
+
5. Disposition: unambiguous concrete fixes applied to the draft (one re-pass max); ambiguous findings surfaced at the confirmation gate. Roast edits affect the body draft pre-write only, never posted as a tracker comment, and re-run the deterministic gates (pipeline step 5). Additionally, the parent scans the **usable member output files (item 7's structural test) directly** for lines containing `split-axis:` (substring match), independent of the chair synthesis; any such finding auto-applies a merge - the split is withdrawn and the draft becomes one ticket with phased AC groups, inside the same one-re-pass budget, and the pre-merge N-body draft is kept alongside: a human re-request of the split at the gate re-presents those N bodies old->new as the approval diff (see the Split rule's sticky override). The chair keeps every other axis; clearing a `split-axis:` finding is not on its path. The same directional rule - toward one ticket, never toward a split - binds the `worker` fallback and the runtime conditional (item 6).
|
|
180
180
|
6. **Runtime conditional (the one allowed):** on a harness with no `gauntlet_setting`/`subagent()` (e.g. Claude Code), dispatch fresh general-purpose subagents via that harness's native facility at low effort, with the same three-axis brief and temp-file artifacts.
|
|
181
|
-
7. **
|
|
182
|
-
|
|
183
|
-
(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
181
|
+
7. **Usable outputs, targeted retry, and roast failure.** A member output
|
|
182
|
+
file is usable iff it is non-empty AND contains both a
|
|
183
|
+
`^verdict:\s*(sound|needs-work|unsound)` line and an
|
|
184
|
+
`^addresses-problem:` line (a `findings:` header with zero bullets is
|
|
185
|
+
valid; the test is a mechanical structural probe - existence plus
|
|
186
|
+
header regex, no reading of findings content - and the parent judges
|
|
187
|
+
by files on disk, not the fanout tool result's labels). A chair
|
|
188
|
+
synthesis is usable iff it contains a `^consensus:` line. Members
|
|
189
|
+
whose file is missing or not usable are re-dispatched **once**,
|
|
190
|
+
together, in a second parallel call carrying the same control block,
|
|
191
|
+
with fresh output paths preserving the `member-<i>-<slug>` basename
|
|
192
|
+
under a `retry/` subdir of the same temp dir; members with usable
|
|
193
|
+
files are never re-run. Partial member loss with a usable chair
|
|
194
|
+
synthesis is success, not failure: at least one usable member file ->
|
|
195
|
+
dispatch the chair over the usable files only, with the coverage note
|
|
196
|
+
(item 2). When coverage was partial, the confirmation gate renders the
|
|
197
|
+
same `Coverage: N of M members reported; <slug>: <reason>` line
|
|
198
|
+
alongside the draft - the human approves knowing review coverage was
|
|
199
|
+
reduced. A wedge-killed or unusable chair retries once with the same
|
|
200
|
+
`:low`-suffixed model (item 3). The roast has failed only when zero
|
|
201
|
+
member files are usable after retry, or the chair fails its one retry
|
|
202
|
+
(worker path: the worker output is missing, empty, or not
|
|
203
|
+
findings-shaped after one full re-dispatch) - then proceed to the
|
|
204
|
+
confirmation gate with the failure rendered inline in the gate message
|
|
205
|
+
itself: `roast unavailable (<reason>)` - the human approves knowing
|
|
206
|
+
review didn't run. Retries are dispatch retries only - they never
|
|
207
|
+
grant a second draft-edit re-pass (the one-re-pass limit is
|
|
208
|
+
unchanged). **Roast failure never blocks the run.**
|
|
193
209
|
|
|
194
210
|
## Tracker abstraction and capability ladder
|
|
195
211
|
|
|
@@ -286,7 +302,7 @@ Read this when applying the AC integrity gate (drafting, repairing, or adjudicat
|
|
|
286
302
|
- Headless run -> stops at the confirmation gate.
|
|
287
303
|
- Ref with no fetch path -> ask; never guess.
|
|
288
304
|
- Split declined -> single ticket with phased AC groups.
|
|
289
|
-
- Roast failure
|
|
305
|
+
- Roast failure = zero usable members after targeted retry, or the chair fails its one retry -> gate with `roast unavailable (<reason>)` inline.
|
|
290
306
|
|
|
291
307
|
## Red flags - STOP
|
|
292
308
|
|
|
@@ -40,9 +40,9 @@ reference/ # optional progressive-disclosure files
|
|
|
40
40
|
<supporting>.md # prompt templates (dispatch payloads)
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
`reference/` is the pi pattern for keeping SKILL.md tight while still shipping deep guidance. See `.pi/skills/test-driven-development/reference/`
|
|
43
|
+
`reference/` is the pi pattern for keeping SKILL.md tight while still shipping deep guidance. See `.pi/skills/test-driven-development/reference/` for a working example.
|
|
44
44
|
|
|
45
|
-
Prompt templates and other dispatch payloads - files filled in and passed wholesale into a subagent `task` - live as siblings of SKILL.md, not under `reference/`. See `requesting-code-review/code-reviewer.md` and the three `subagent-driven-development/*-prompt.md` files. The decision criterion is destination, not format: a file passed wholesale into a subagent's `task` is a sibling; a file read at a decision point for deep guidance, examples, or rationale is `reference/`. Some older skills (`
|
|
45
|
+
Prompt templates and other dispatch payloads - files filled in and passed wholesale into a subagent `task` - live as siblings of SKILL.md, not under `reference/`. See `requesting-code-review/code-reviewer.md` and the three `subagent-driven-development/*-prompt.md` files. The decision criterion is destination, not format: a file passed wholesale into a subagent's `task` is a sibling; a file read at a decision point for deep guidance, examples, or rationale is `reference/`. Some older skills (`test-driven-development`) keep deep-guidance `*.md` files flat as siblings, predating the `reference/` convention (obra/superpowers lineage) - that is descriptive history, not a mandate to move them.
|
|
46
46
|
|
|
47
47
|
### Reference Files Bundled With This Skill
|
|
48
48
|
|
|
@@ -151,7 +151,7 @@ description: Use when implementing any feature or bugfix, before writing impleme
|
|
|
151
151
|
Use skill name with explicit requirement markers. **Never** force-load with `@` syntax — that burns context before the file is needed.
|
|
152
152
|
|
|
153
153
|
- ✅ `**REQUIRED SUB-SKILL:** Use /skill:test-driven-development`
|
|
154
|
-
- ✅ `**REQUIRED BACKGROUND:** You MUST understand /skill:
|
|
154
|
+
- ✅ `**REQUIRED BACKGROUND:** You MUST understand /skill:verification-before-completion`
|
|
155
155
|
- ✅ `> **Related skills:** Pair with /skill:verification-before-completion`
|
|
156
156
|
- ❌ `@.pi/skills/test-driven-development/SKILL.md`
|
|
157
157
|
|
|
@@ -1,151 +0,0 @@
|
|
|
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 a gauntlet overrides file exists - checked in order: `.pi/gauntlet-overrides.md`, `<repo root>/gauntlet-overrides.md`, `<repo root>/doc/gauntlet-overrides.md`; first found wins - 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.
|
|
@@ -1,158 +0,0 @@
|
|
|
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
|
|
@@ -1,115 +0,0 @@
|
|
|
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
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
# Defense-in-Depth Validation
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
When you fix a bug caused by invalid data, adding validation at one place feels sufficient. But that single check can be bypassed by different code paths, refactoring, or mocks.
|
|
6
|
-
|
|
7
|
-
**Core principle:** Validate at EVERY layer data passes through. Make the bug structurally impossible.
|
|
8
|
-
|
|
9
|
-
## Why Multiple Layers
|
|
10
|
-
|
|
11
|
-
Single validation: "We fixed the bug"
|
|
12
|
-
Multiple layers: "We made the bug impossible"
|
|
13
|
-
|
|
14
|
-
Different layers catch different cases:
|
|
15
|
-
- Entry validation catches most bugs
|
|
16
|
-
- Business logic catches edge cases
|
|
17
|
-
- Environment guards prevent context-specific dangers
|
|
18
|
-
- Debug logging helps when other layers fail
|
|
19
|
-
|
|
20
|
-
## The Four Layers
|
|
21
|
-
|
|
22
|
-
### Layer 1: Entry Point Validation
|
|
23
|
-
**Purpose:** Reject obviously invalid input at API boundary
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
function createProject(name: string, workingDirectory: string) {
|
|
27
|
-
if (!workingDirectory || workingDirectory.trim() === '') {
|
|
28
|
-
throw new Error('workingDirectory cannot be empty');
|
|
29
|
-
}
|
|
30
|
-
if (!existsSync(workingDirectory)) {
|
|
31
|
-
throw new Error(`workingDirectory does not exist: ${workingDirectory}`);
|
|
32
|
-
}
|
|
33
|
-
if (!statSync(workingDirectory).isDirectory()) {
|
|
34
|
-
throw new Error(`workingDirectory is not a directory: ${workingDirectory}`);
|
|
35
|
-
}
|
|
36
|
-
// ... proceed
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### Layer 2: Business Logic Validation
|
|
41
|
-
**Purpose:** Ensure data makes sense for this operation
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
function initializeWorkspace(projectDir: string, sessionId: string) {
|
|
45
|
-
if (!projectDir) {
|
|
46
|
-
throw new Error('projectDir required for workspace initialization');
|
|
47
|
-
}
|
|
48
|
-
// ... proceed
|
|
49
|
-
}
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Layer 3: Environment Guards
|
|
53
|
-
**Purpose:** Prevent dangerous operations in specific contexts
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
async function gitInit(directory: string) {
|
|
57
|
-
// In tests, refuse git init outside temp directories
|
|
58
|
-
if (process.env.NODE_ENV === 'test') {
|
|
59
|
-
const normalized = normalize(resolve(directory));
|
|
60
|
-
const tmpDir = normalize(resolve(tmpdir()));
|
|
61
|
-
|
|
62
|
-
if (!normalized.startsWith(tmpDir)) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
`Refusing git init outside temp dir during tests: ${directory}`
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
// ... proceed
|
|
69
|
-
}
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Layer 4: Debug Instrumentation
|
|
73
|
-
**Purpose:** Capture context for forensics
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
async function gitInit(directory: string) {
|
|
77
|
-
const stack = new Error().stack;
|
|
78
|
-
logger.debug('About to git init', {
|
|
79
|
-
directory,
|
|
80
|
-
cwd: process.cwd(),
|
|
81
|
-
stack,
|
|
82
|
-
});
|
|
83
|
-
// ... proceed
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## Applying the Pattern
|
|
88
|
-
|
|
89
|
-
When you find a bug:
|
|
90
|
-
|
|
91
|
-
1. **Trace the data flow** - Where does bad value originate? Where used?
|
|
92
|
-
2. **Map all checkpoints** - List every point data passes through
|
|
93
|
-
3. **Add validation at each layer** - Entry, business, environment, debug
|
|
94
|
-
4. **Test each layer** - Try to bypass layer 1, verify layer 2 catches it
|
|
95
|
-
|
|
96
|
-
## Example from Session
|
|
97
|
-
|
|
98
|
-
Bug: Empty `projectDir` caused `git init` in source code
|
|
99
|
-
|
|
100
|
-
**Data flow:**
|
|
101
|
-
1. Test setup → empty string
|
|
102
|
-
2. `Project.create(name, '')`
|
|
103
|
-
3. `WorkspaceManager.createWorkspace('')`
|
|
104
|
-
4. `git init` runs in `process.cwd()`
|
|
105
|
-
|
|
106
|
-
**Four layers added:**
|
|
107
|
-
- Layer 1: `Project.create()` validates not empty/exists/writable
|
|
108
|
-
- Layer 2: `WorkspaceManager` validates projectDir not empty
|
|
109
|
-
- Layer 3: `WorktreeManager` refuses git init outside tmpdir in tests
|
|
110
|
-
- Layer 4: Stack trace logging before git init
|
|
111
|
-
|
|
112
|
-
**Result:** All 1847 tests passed, bug impossible to reproduce
|
|
113
|
-
|
|
114
|
-
## Key Insight
|
|
115
|
-
|
|
116
|
-
All four layers were necessary. During testing, each layer caught bugs the others missed:
|
|
117
|
-
- Different code paths bypassed entry validation
|
|
118
|
-
- Mocks bypassed business logic checks
|
|
119
|
-
- Edge cases on different platforms needed environment guards
|
|
120
|
-
- Debug logging identified structural misuse
|
|
121
|
-
|
|
122
|
-
**Don't stop at one validation point.** Add checks at every layer.
|