@tea-agent/loop-agent 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/AGENTS.md +142 -142
  2. package/CHANGELOG.md +132 -98
  3. package/README.md +195 -195
  4. package/bin/agent-worker.js +22 -22
  5. package/bin/loop-agent.js +21 -21
  6. package/dist/application/dag/args.js +9 -1
  7. package/dist/application/dag/run-dag.js +16 -2
  8. package/dist/cli/command-definitions.js +22 -4
  9. package/dist/cli/help.js +3 -2
  10. package/dist/cli/program.js +7 -5
  11. package/dist/commands/import-prd.js +76 -0
  12. package/dist/commands/init.js +467 -457
  13. package/dist/commands/instructions.js +90 -58
  14. package/dist/commands/loop-benchmark.js +11 -11
  15. package/dist/commands/pi-reuse-benchmark.js +16 -16
  16. package/dist/executors/cursor-executor.js +1 -1
  17. package/dist/executors/dag-pi-executor.js +1 -0
  18. package/dist/executors/pi-sdk-executor.js +63 -1
  19. package/dist/shared/preview.js +39 -0
  20. package/dist/task/config-types.js +3 -0
  21. package/dist/task/runtime.js +27 -27
  22. package/dist/task/source-references.js +221 -0
  23. package/dist/worker/cli.js +62 -1
  24. package/dist/worker/loop-agent/loop-agent-client.js +97 -5
  25. package/dist/worker/materialize/harness-task-materializer.js +166 -5
  26. package/dist/worker/observability/event-store.js +82 -0
  27. package/dist/worker/observability/events.js +79 -0
  28. package/dist/worker/observability/progress-composite.js +33 -0
  29. package/dist/worker/observability/read-model.js +1013 -0
  30. package/dist/worker/observability/snapshot-store.js +43 -0
  31. package/dist/worker/observability/types.js +1 -0
  32. package/dist/worker/observe/paths.js +64 -0
  33. package/dist/worker/observe/routes.js +423 -0
  34. package/dist/worker/observe/server.js +61 -0
  35. package/dist/worker/observe/static/app.js +1419 -0
  36. package/dist/worker/observe/static/index.html +63 -0
  37. package/dist/worker/observe/static/styles.css +613 -0
  38. package/dist/worker/pool/failure-routing.js +41 -6
  39. package/dist/worker/pool/run-store.js +50 -0
  40. package/dist/worker/progress-reporter.js +0 -18
  41. package/dist/worker/run-task/run-task.js +327 -92
  42. package/dist/worker/runner/run-ready.js +112 -4
  43. package/dist/worker/task-spec/schema.js +2 -1
  44. package/dist/workflows/dag/canvas-observer.js +275 -275
  45. package/dist/workflows/dag/event-observer.js +132 -0
  46. package/dist/workflows/dag/init-hybrid.js +182 -21
  47. package/dist/workflows/dag/observer-compose.js +52 -0
  48. package/docs/README.md +75 -72
  49. package/docs/agent-dag-recovery-playbook.md +184 -184
  50. package/docs/agent-dag-runner.md +42 -42
  51. package/docs/architecture/runtime-boundaries.md +162 -147
  52. package/docs/cursor-executor-usage.md +25 -25
  53. package/docs/decisions/README.md +3 -3
  54. package/docs/design/README.md +49 -36
  55. package/docs/development-principles.md +73 -73
  56. package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -1749
  57. package/docs/exec-plans/README.md +6 -6
  58. package/docs/exec-plans/active/README.md +12 -7
  59. package/docs/exec-plans/completed/README.md +32 -19
  60. package/docs/feature-workflow.md +186 -186
  61. package/docs/harness-methodology-debugging.md +153 -153
  62. package/docs/harness-methodology-tdd.md +130 -130
  63. package/docs/harness-methodology-verification.md +27 -27
  64. package/docs/init-surface.manifest.json +208 -199
  65. package/docs/loop-agent-harness.md +55 -42
  66. package/docs/production-readiness.md +96 -96
  67. package/docs/progress/README.md +3 -3
  68. package/docs/reports/README.md +9 -5
  69. package/docs/skills/README.md +6 -6
  70. package/docs/skills/vetted-skill-registry.md +26 -26
  71. package/docs/templates/adr.md +60 -60
  72. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  73. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  74. package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -117
  75. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  76. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  77. package/docs/templates/agent-dag-report.schema.json +454 -454
  78. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  79. package/docs/templates/agent-dag.base.json +195 -195
  80. package/docs/templates/agent-dag.final-verification.json +190 -190
  81. package/docs/templates/agent-dag.schema.json +316 -316
  82. package/docs/templates/agent-dag.supervised-implementation.json +500 -500
  83. package/docs/templates/exec-plan.md +64 -64
  84. package/docs/templates/feature-spec.md +53 -53
  85. package/docs/templates/hybrid-dag.json +193 -193
  86. package/docs/templates/init-evolution-review.md +33 -33
  87. package/docs/templates/interactive-ui-round2-experiment.md +66 -0
  88. package/docs/templates/production-readiness-checklist.md +57 -57
  89. package/docs/templates/progress-log.md +17 -17
  90. package/docs/templates/project-start-checklist.md +9 -9
  91. package/docs/templates/qa-report.md +48 -48
  92. package/docs/templates/sprint-contract.md +29 -29
  93. package/docs/templates/worker-dogfood-evidence.md +52 -0
  94. package/docs/templates/worker-dogfood-setup.md +48 -0
  95. package/docs/verification-matrix.md +41 -41
  96. package/examples/decision-gate-agent-dag.json +123 -123
  97. package/examples/example-dag.json +51 -51
  98. package/examples/hybrid-loop-agent-dag.json +194 -194
  99. package/harness.json +70 -69
  100. package/package.json +66 -66
  101. package/skills/ai-engineering-context/SKILL.md +48 -48
  102. package/skills/code-review-core/SKILL.md +20 -20
  103. package/skills/codebase-scout/SKILL.md +19 -19
  104. package/skills/init-capability-evolution/SKILL.md +69 -69
  105. package/skills/loop-agent/SKILL.md +149 -147
  106. package/skills/loop-agent/references/README.md +67 -67
  107. package/skills/loop-agent/references/command-reference.md +412 -403
  108. package/skills/loop-agent/references/harness-policy.md +263 -259
  109. package/skills/loop-agent/references/hybrid-dag.md +216 -216
  110. package/skills/loop-agent/references/learned/README.md +21 -21
  111. package/skills/loop-agent/references/long-running-loop.md +59 -59
  112. package/skills/loop-agent/references/model-routing.md +36 -36
  113. package/skills/loop-agent/references/multi-worktree.md +54 -54
  114. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  115. package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
  116. package/skills/loop-agent/references/pi-prompt.md +23 -23
  117. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +81 -81
  118. package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -44
  119. package/skills/loop-agent/references/task-workflow.md +89 -84
  120. package/skills/loop-agent/references/verification-and-failure-handling.md +128 -128
  121. package/skills/requesting-code-review/SKILL.md +101 -101
  122. package/skills/requesting-code-review/code-reviewer.md +168 -168
  123. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  124. package/skills/systematic-debugging/SKILL.md +296 -296
  125. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  126. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  127. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  128. package/skills/systematic-debugging/find-polluter.sh +63 -63
  129. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  130. package/skills/systematic-debugging/test-academic.md +14 -14
  131. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  132. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  133. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  134. package/skills/test-driven-development/SKILL.md +20 -20
  135. package/skills/verification-before-completion/SKILL.md +154 -154
  136. package/skills/webapp-testing/SKILL.md +19 -19
@@ -1,158 +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
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 +1,115 @@
1
- # Condition-Based Waiting
2
-
3
- ## Overview
4
-
5
- Flaky tests 常用 arbitrary delays 猜 timing。这制造 race conditions:fast machines 上 pass,load 或 CI 下 fail。
6
-
7
- **Core principle:** Wait for 你真正关心的 actual condition,不是猜需要多久。
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 有 arbitrary delays(`setTimeout`、`sleep`、`time.sleep()`)
26
- - Tests flaky(有时 pass,load 下 fail)
27
- - Parallel 运行时 timeout
28
- - 等待 async operations 完成
29
-
30
- **Don't use when:**
31
- - 测试 actual timing behavior(debounce、throttle intervals)
32
- - 若用 arbitrary timeout,ALWAYS document WHY
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
- 完整实现及 domain-specific helpers(`waitForEvent`、`waitForEventCount`、`waitForEventMatch`)见本目录 `condition-based-waiting-example.ts`,来自 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
90
- **✅ Fix:** Always include timeout with clear error
91
-
92
- **❌ Stale data:** Loop 前 cache state
93
- **✅ Fix:** Loop 内 call getter 取 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
- 来自 debugging session (2025-10-03):
112
- - 修复 3 个文件中 15 个 flaky tests
113
- - Pass rate:60% → 100%
114
- - Execution time:40% faster
115
- - No more race conditions
1
+ # Condition-Based Waiting
2
+
3
+ ## Overview
4
+
5
+ Flaky tests 常用 arbitrary delays 猜 timing。这制造 race conditions:fast machines 上 pass,load 或 CI 下 fail。
6
+
7
+ **Core principle:** Wait for 你真正关心的 actual condition,不是猜需要多久。
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 有 arbitrary delays(`setTimeout`、`sleep`、`time.sleep()`)
26
+ - Tests flaky(有时 pass,load 下 fail)
27
+ - Parallel 运行时 timeout
28
+ - 等待 async operations 完成
29
+
30
+ **Don't use when:**
31
+ - 测试 actual timing behavior(debounce、throttle intervals)
32
+ - 若用 arbitrary timeout,ALWAYS document WHY
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
+ 完整实现及 domain-specific helpers(`waitForEvent`、`waitForEventCount`、`waitForEventMatch`)见本目录 `condition-based-waiting-example.ts`,来自 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
90
+ **✅ Fix:** Always include timeout with clear error
91
+
92
+ **❌ Stale data:** Loop 前 cache state
93
+ **✅ Fix:** Loop 内 call getter 取 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
+ 来自 debugging session (2025-10-03):
112
+ - 修复 3 个文件中 15 个 flaky tests
113
+ - Pass rate:60% → 100%
114
+ - Execution time:40% faster
115
+ - No more race conditions