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.
@@ -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. Dispatch members with `cwd` = repo root, absolute `output` paths per member, run-level `control: { needsAttentionAfterMs: 600000 }` (sits beside `tasks`, not inside each task). Give the chair the member files via `reads`. 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; 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.
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. **Roast failure and retry.** A roast has failed when the dispatch
182
- errored, or the artifact the parent reads - the chair synthesis
183
- (council path) or the worker output (worker path) - is missing, empty,
184
- or not findings-shaped; partial member loss with a usable chair
185
- synthesis is success, not failure. On failure, retry once: re-run the
186
- same full configured dispatch with fresh temp artifacts. If the retry
187
- also fails, proceed to the confirmation gate with the failure rendered
188
- inline in the gate message itself: `roast unavailable (dispatch failed
189
- twice: <reason>)` - the human approves knowing review didn't run. The
190
- retry is a dispatch retry only - it never grants a second draft-edit
191
- re-pass (the one-re-pass limit is unchanged). **Roast failure never
192
- blocks the run.**
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 -> retry once; second failure -> gate with `roast unavailable (dispatch failed twice: <reason>)` inline.
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/` and `.pi/skills/systematic-debugging/reference/` for working examples.
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 (`systematic-debugging`, `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.
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:systematic-debugging`
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.