memorix 1.2.1 → 1.2.2

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 (199) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +14 -2
  3. package/README.zh-CN.md +14 -2
  4. package/TEAM.md +86 -86
  5. package/dist/cli/index.js +15407 -13779
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1321 -529
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +8458 -8087
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +16 -0
  13. package/dist/sdk.d.ts +7 -2
  14. package/dist/sdk.js +1349 -535
  15. package/dist/sdk.js.map +1 -1
  16. package/dist/types.d.ts +49 -1
  17. package/dist/types.js.map +1 -1
  18. package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
  19. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
  20. package/docs/API_REFERENCE.md +24 -4
  21. package/docs/DESIGN_DECISIONS.md +357 -357
  22. package/docs/README.md +1 -1
  23. package/docs/dev-log/progress.txt +91 -11
  24. package/package.json +1 -1
  25. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  26. package/src/audit/index.ts +156 -156
  27. package/src/cli/command-guide.ts +192 -0
  28. package/src/cli/commands/audit-list.ts +89 -89
  29. package/src/cli/commands/audit.ts +9 -4
  30. package/src/cli/commands/background.ts +659 -659
  31. package/src/cli/commands/cleanup.ts +5 -1
  32. package/src/cli/commands/codegraph.ts +15 -5
  33. package/src/cli/commands/context.ts +3 -2
  34. package/src/cli/commands/doctor.ts +4 -2
  35. package/src/cli/commands/explain.ts +9 -3
  36. package/src/cli/commands/formation.ts +48 -48
  37. package/src/cli/commands/git-hook-install.ts +111 -111
  38. package/src/cli/commands/handoff.ts +75 -61
  39. package/src/cli/commands/hooks-status.ts +63 -63
  40. package/src/cli/commands/identity.ts +116 -0
  41. package/src/cli/commands/ingest-commit.ts +153 -153
  42. package/src/cli/commands/ingest-image.ts +71 -69
  43. package/src/cli/commands/ingest-log.ts +180 -180
  44. package/src/cli/commands/ingest.ts +44 -44
  45. package/src/cli/commands/integrate-shared.ts +15 -15
  46. package/src/cli/commands/lock.ts +93 -92
  47. package/src/cli/commands/memory.ts +58 -21
  48. package/src/cli/commands/message.ts +123 -118
  49. package/src/cli/commands/operator-shared.ts +98 -3
  50. package/src/cli/commands/poll.ts +74 -64
  51. package/src/cli/commands/purge-all-memory.ts +85 -85
  52. package/src/cli/commands/purge-project-memory.ts +83 -83
  53. package/src/cli/commands/reasoning.ts +135 -121
  54. package/src/cli/commands/retention.ts +9 -4
  55. package/src/cli/commands/serve-http.ts +8 -2
  56. package/src/cli/commands/serve-shared.ts +118 -118
  57. package/src/cli/commands/session.ts +29 -3
  58. package/src/cli/commands/skills.ts +124 -119
  59. package/src/cli/commands/status.ts +4 -3
  60. package/src/cli/commands/task.ts +193 -184
  61. package/src/cli/commands/team.ts +14 -10
  62. package/src/cli/commands/transfer.ts +108 -55
  63. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  64. package/src/cli/identity.ts +89 -0
  65. package/src/cli/index.ts +96 -19
  66. package/src/cli/invocation.ts +115 -0
  67. package/src/cli/tui/ChatView.tsx +234 -234
  68. package/src/cli/tui/CommandBar.tsx +312 -312
  69. package/src/cli/tui/ContextRail.tsx +118 -118
  70. package/src/cli/tui/HeaderBar.tsx +72 -72
  71. package/src/cli/tui/LogoBanner.tsx +51 -51
  72. package/src/cli/tui/Sidebar.tsx +179 -179
  73. package/src/cli/tui/chat-service.ts +41 -18
  74. package/src/cli/tui/data.ts +23 -44
  75. package/src/cli/tui/index.ts +41 -41
  76. package/src/cli/tui/markdown-render.tsx +371 -371
  77. package/src/cli/tui/operator-context.ts +60 -0
  78. package/src/cli/tui/use-mouse.ts +157 -157
  79. package/src/cli/tui/useNavigation.ts +56 -56
  80. package/src/cli/tui/views/MemoryView.tsx +10 -8
  81. package/src/cli/update-checker.ts +211 -211
  82. package/src/cli/version.ts +7 -7
  83. package/src/cli/workbench.ts +1 -1
  84. package/src/codegraph/auto-context.ts +31 -2
  85. package/src/codegraph/context-pack.ts +1 -0
  86. package/src/codegraph/project-context.ts +2 -0
  87. package/src/compact/engine.ts +26 -10
  88. package/src/compact/index-format.ts +25 -2
  89. package/src/compact/token-budget.ts +74 -74
  90. package/src/dashboard/project-classification.ts +64 -64
  91. package/src/dashboard/server.ts +46 -9
  92. package/src/embedding/fastembed-provider.ts +142 -142
  93. package/src/embedding/transformers-provider.ts +111 -111
  94. package/src/git/extractor.ts +209 -209
  95. package/src/git/hooks-path.ts +85 -85
  96. package/src/hooks/admission.ts +117 -0
  97. package/src/hooks/handler.ts +98 -91
  98. package/src/hooks/pattern-detector.ts +173 -173
  99. package/src/hooks/significance-filter.ts +250 -250
  100. package/src/knowledge/context-assembly.ts +97 -0
  101. package/src/knowledge/workset.ts +179 -10
  102. package/src/llm/memory-manager.ts +328 -328
  103. package/src/llm/provider.ts +885 -885
  104. package/src/llm/quality.ts +248 -248
  105. package/src/memory/admission.ts +57 -0
  106. package/src/memory/attribution-guard.ts +249 -249
  107. package/src/memory/consolidation.ts +13 -2
  108. package/src/memory/disclosure-policy.ts +140 -135
  109. package/src/memory/entity-extractor.ts +197 -197
  110. package/src/memory/export-import.ts +11 -3
  111. package/src/memory/formation/evaluate.ts +217 -217
  112. package/src/memory/formation/extract.ts +361 -361
  113. package/src/memory/formation/index.ts +417 -417
  114. package/src/memory/formation/resolve.ts +344 -344
  115. package/src/memory/formation/types.ts +315 -315
  116. package/src/memory/freshness.ts +122 -122
  117. package/src/memory/graph-context.ts +8 -2
  118. package/src/memory/graph.ts +197 -197
  119. package/src/memory/observations.ts +162 -4
  120. package/src/memory/quality-audit.ts +2 -0
  121. package/src/memory/refs.ts +94 -94
  122. package/src/memory/retention.ts +22 -2
  123. package/src/memory/secret-filter.ts +79 -79
  124. package/src/memory/session.ts +5 -2
  125. package/src/memory/visibility.ts +80 -0
  126. package/src/multimodal/image-loader.ts +143 -143
  127. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  128. package/src/orchestrate/adapters/claude.ts +111 -111
  129. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  130. package/src/orchestrate/adapters/codex.ts +41 -41
  131. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  132. package/src/orchestrate/adapters/gemini.ts +42 -42
  133. package/src/orchestrate/adapters/index.ts +73 -73
  134. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  135. package/src/orchestrate/adapters/opencode.ts +47 -47
  136. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  137. package/src/orchestrate/adapters/types.ts +77 -77
  138. package/src/orchestrate/capability-router.ts +284 -284
  139. package/src/orchestrate/context-compact.ts +188 -188
  140. package/src/orchestrate/cost-tracker.ts +219 -219
  141. package/src/orchestrate/error-recovery.ts +191 -191
  142. package/src/orchestrate/evidence.ts +140 -140
  143. package/src/orchestrate/ledger.ts +110 -110
  144. package/src/orchestrate/memorix-bridge.ts +378 -340
  145. package/src/orchestrate/output-budget.ts +80 -80
  146. package/src/orchestrate/permission.ts +152 -152
  147. package/src/orchestrate/pipeline-trace.ts +131 -131
  148. package/src/orchestrate/prompt-builder.ts +155 -155
  149. package/src/orchestrate/ring-buffer.ts +37 -37
  150. package/src/orchestrate/task-graph.ts +389 -389
  151. package/src/orchestrate/worktree.ts +232 -232
  152. package/src/project/aliases.ts +374 -374
  153. package/src/project/detector.ts +268 -268
  154. package/src/rules/adapters/claude-code.ts +99 -99
  155. package/src/rules/adapters/codex.ts +97 -97
  156. package/src/rules/adapters/copilot.ts +124 -124
  157. package/src/rules/adapters/cursor.ts +114 -114
  158. package/src/rules/adapters/kiro.ts +126 -126
  159. package/src/rules/adapters/trae.ts +56 -56
  160. package/src/rules/adapters/windsurf.ts +83 -83
  161. package/src/rules/syncer.ts +235 -235
  162. package/src/runtime/control-plane-maintenance.ts +1 -0
  163. package/src/runtime/isolated-maintenance.ts +1 -0
  164. package/src/runtime/lifecycle.ts +18 -0
  165. package/src/runtime/maintenance-jobs.ts +1 -0
  166. package/src/runtime/maintenance-runner.ts +2 -0
  167. package/src/runtime/project-maintenance.ts +89 -0
  168. package/src/sdk.ts +334 -304
  169. package/src/search/intent-detector.ts +289 -289
  170. package/src/search/query-expansion.ts +52 -52
  171. package/src/server/formation-timeout.ts +27 -27
  172. package/src/server.ts +260 -81
  173. package/src/skills/mini-skills.ts +386 -386
  174. package/src/store/chat-store.ts +119 -119
  175. package/src/store/graph-store.ts +249 -249
  176. package/src/store/mini-skill-store.ts +349 -349
  177. package/src/store/orama-store.ts +61 -6
  178. package/src/store/persistence-json.ts +212 -212
  179. package/src/store/persistence.ts +291 -291
  180. package/src/store/project-affinity.ts +195 -195
  181. package/src/store/sqlite-db.ts +23 -1
  182. package/src/store/sqlite-store.ts +12 -2
  183. package/src/team/event-bus.ts +76 -76
  184. package/src/team/file-locks.ts +173 -173
  185. package/src/team/handoff.ts +168 -161
  186. package/src/team/messages.ts +203 -203
  187. package/src/team/poll.ts +132 -132
  188. package/src/team/tasks.ts +211 -211
  189. package/src/types.ts +51 -0
  190. package/src/wiki/generator.ts +2 -0
  191. package/src/workspace/mcp-adapters/codex.ts +191 -191
  192. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  193. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  194. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  195. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  196. package/src/workspace/mcp-adapters/trae.ts +134 -134
  197. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  198. package/src/workspace/sanitizer.ts +60 -60
  199. package/src/workspace/workflow-sync.ts +131 -131
@@ -1,191 +1,191 @@
1
- /**
2
- * Error Recovery — Phase 7, Step 3: Layered error classification and recovery.
3
- *
4
- * Detects three failure modes from agent output/exit and recommends a
5
- * recovery strategy. The coordinator uses this to decide between:
6
- * - continuation (truncated output)
7
- * - context compaction + re-dispatch (context overflow)
8
- * - exponential backoff + retry (transient errors)
9
- * - normal retry (unknown / unrecoverable)
10
- *
11
- * Design principle: detection is heuristic-based pattern matching on
12
- * stderr/tailOutput. No false positive should crash the pipeline —
13
- * the worst case of a wrong classification is a wasted retry attempt.
14
- */
15
-
16
- // ── Types ──────────────────────────────────────────────────────────
17
-
18
- export type ErrorCategory = 'truncated' | 'context_overflow' | 'transient' | 'unknown';
19
-
20
- export interface RecoveryAction {
21
- category: ErrorCategory;
22
- /** Recommended recovery strategy */
23
- strategy: 'continue' | 'compact_and_retry' | 'backoff_and_retry' | 'normal_retry';
24
- /** Suggested delay before retry in ms (0 = immediate) */
25
- delayMs: number;
26
- /** Human-readable explanation of the detection */
27
- reason: string;
28
- /** Suggested continuation prompt (for 'continue' strategy) */
29
- continuationPrompt?: string;
30
- /** Backoff attempt number (for 'backoff_and_retry') */
31
- backoffAttempt?: number;
32
- }
33
-
34
- export interface ErrorContext {
35
- exitCode: number | null;
36
- killed: boolean;
37
- tailOutput: string;
38
- /** Whether the agent stream had an end_turn signal */
39
- hasEndTurn?: boolean;
40
- }
41
-
42
- // ── Pattern Registry ───────────────────────────────────────────────
43
-
44
- const TRUNCATED_PATTERNS = [
45
- /max.?output/i,
46
- /output.?limit/i,
47
- /max.?tokens/i,
48
- /response.?truncat/i,
49
- /generation.?limit/i,
50
- ];
51
-
52
- const CONTEXT_OVERFLOW_PATTERNS = [
53
- /overlong.?prompt/i,
54
- /context.?length.?exceed/i,
55
- /prompt.?too.?long/i,
56
- /maximum.?context/i,
57
- /token.?limit.?exceed/i,
58
- /input.?too.?long/i,
59
- /request.?too.?large/i,
60
- /max.?input.?tokens/i,
61
- /content.?too.?long/i,
62
- /exceeds.?(?:the\s+)?(?:model'?s?\s+)?(?:maximum|max).?(?:context|token)/i,
63
- ];
64
-
65
- const TRANSIENT_PATTERNS = [
66
- /rate.?limit/i,
67
- /429/,
68
- /too.?many.?requests/i,
69
- /server.?error/i,
70
- /502|503|504/,
71
- /bad.?gateway/i,
72
- /service.?unavailable/i,
73
- /gateway.?timeout/i,
74
- /ECONNRESET/i,
75
- /ECONNREFUSED/i,
76
- /ETIMEDOUT/i,
77
- /socket.?hang.?up/i,
78
- /network.?error/i,
79
- /temporary.?failure/i,
80
- /overloaded/i,
81
- /capacity/i,
82
- ];
83
-
84
- // ── Backoff State ──────────────────────────────────────────────────
85
-
86
- const backoffState = new Map<string, number>(); // taskId → consecutive transient failures
87
-
88
- /** Reset backoff state for a task (call on success or non-transient failure) */
89
- export function resetBackoff(taskId: string): void {
90
- backoffState.delete(taskId);
91
- }
92
-
93
- /** Get current backoff attempt count for a task */
94
- export function getBackoffAttempt(taskId: string): number {
95
- return backoffState.get(taskId) ?? 0;
96
- }
97
-
98
- // ── Core Classification ────────────────────────────────────────────
99
-
100
- /**
101
- * Classify an agent failure and recommend a recovery action.
102
- *
103
- * Priority order: truncated > context_overflow > transient > unknown.
104
- * This ensures the most specific recovery strategy is chosen.
105
- */
106
- export function classifyError(ctx: ErrorContext, taskId?: string): RecoveryAction {
107
- const { exitCode, killed, tailOutput, hasEndTurn } = ctx;
108
- const text = tailOutput.slice(-2000); // Only check last 2KB
109
-
110
- // 1. Truncated output detection
111
- // Agent was cut off mid-response (no end_turn + non-zero exit)
112
- if (!killed && exitCode !== 0 && hasEndTurn === false) {
113
- return {
114
- category: 'truncated',
115
- strategy: 'continue',
116
- delayMs: 0,
117
- reason: 'Agent output appears truncated (no end_turn signal with non-zero exit)',
118
- continuationPrompt: 'Your previous output was truncated due to output limits. Continue from where you stopped. Do not repeat what you already wrote.',
119
- };
120
- }
121
-
122
- // Also detect explicit truncation error messages
123
- if (matchesAny(text, TRUNCATED_PATTERNS)) {
124
- return {
125
- category: 'truncated',
126
- strategy: 'continue',
127
- delayMs: 0,
128
- reason: `Truncation pattern detected in output`,
129
- continuationPrompt: 'Your previous output was truncated due to output limits. Continue from where you stopped. Do not repeat what you already wrote.',
130
- };
131
- }
132
-
133
- // 2. Context overflow detection
134
- if (matchesAny(text, CONTEXT_OVERFLOW_PATTERNS)) {
135
- return {
136
- category: 'context_overflow',
137
- strategy: 'compact_and_retry',
138
- delayMs: 0,
139
- reason: 'Context overflow detected — prompt is too large for the model',
140
- };
141
- }
142
-
143
- // 3. Transient error detection
144
- if (matchesAny(text, TRANSIENT_PATTERNS)) {
145
- const attempt = taskId ? (backoffState.get(taskId) ?? 0) + 1 : 1;
146
- if (taskId) backoffState.set(taskId, attempt);
147
-
148
- // Exponential backoff: 2s, 4s, 8s, capped at 30s
149
- const baseDelay = 2000;
150
- const maxDelay = 30_000;
151
- const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
152
- // Add jitter: ±25%
153
- const jitter = delay * 0.25 * (Math.random() * 2 - 1);
154
-
155
- return {
156
- category: 'transient',
157
- strategy: 'backoff_and_retry',
158
- delayMs: Math.round(delay + jitter),
159
- reason: `Transient error detected (attempt ${attempt})`,
160
- backoffAttempt: attempt,
161
- };
162
- }
163
-
164
- // 4. Unknown — fall back to normal retry
165
- if (taskId) resetBackoff(taskId); // Non-transient failure resets backoff
166
-
167
- return {
168
- category: 'unknown',
169
- strategy: 'normal_retry',
170
- delayMs: 0,
171
- reason: killed
172
- ? 'Agent killed by timeout'
173
- : `Agent exited with code ${exitCode}`,
174
- };
175
- }
176
-
177
- /**
178
- * Check if the error is recoverable (not a permanent failure).
179
- * Truncated and transient errors are always recoverable.
180
- * Context overflow is recoverable if compaction is available.
181
- * Unknown errors may be recoverable via normal retry.
182
- */
183
- export function isRecoverable(action: RecoveryAction): boolean {
184
- return action.strategy !== 'normal_retry' || action.category === 'unknown';
185
- }
186
-
187
- // ── Helpers ────────────────────────────────────────────────────────
188
-
189
- function matchesAny(text: string, patterns: RegExp[]): boolean {
190
- return patterns.some(p => p.test(text));
191
- }
1
+ /**
2
+ * Error Recovery — Phase 7, Step 3: Layered error classification and recovery.
3
+ *
4
+ * Detects three failure modes from agent output/exit and recommends a
5
+ * recovery strategy. The coordinator uses this to decide between:
6
+ * - continuation (truncated output)
7
+ * - context compaction + re-dispatch (context overflow)
8
+ * - exponential backoff + retry (transient errors)
9
+ * - normal retry (unknown / unrecoverable)
10
+ *
11
+ * Design principle: detection is heuristic-based pattern matching on
12
+ * stderr/tailOutput. No false positive should crash the pipeline —
13
+ * the worst case of a wrong classification is a wasted retry attempt.
14
+ */
15
+
16
+ // ── Types ──────────────────────────────────────────────────────────
17
+
18
+ export type ErrorCategory = 'truncated' | 'context_overflow' | 'transient' | 'unknown';
19
+
20
+ export interface RecoveryAction {
21
+ category: ErrorCategory;
22
+ /** Recommended recovery strategy */
23
+ strategy: 'continue' | 'compact_and_retry' | 'backoff_and_retry' | 'normal_retry';
24
+ /** Suggested delay before retry in ms (0 = immediate) */
25
+ delayMs: number;
26
+ /** Human-readable explanation of the detection */
27
+ reason: string;
28
+ /** Suggested continuation prompt (for 'continue' strategy) */
29
+ continuationPrompt?: string;
30
+ /** Backoff attempt number (for 'backoff_and_retry') */
31
+ backoffAttempt?: number;
32
+ }
33
+
34
+ export interface ErrorContext {
35
+ exitCode: number | null;
36
+ killed: boolean;
37
+ tailOutput: string;
38
+ /** Whether the agent stream had an end_turn signal */
39
+ hasEndTurn?: boolean;
40
+ }
41
+
42
+ // ── Pattern Registry ───────────────────────────────────────────────
43
+
44
+ const TRUNCATED_PATTERNS = [
45
+ /max.?output/i,
46
+ /output.?limit/i,
47
+ /max.?tokens/i,
48
+ /response.?truncat/i,
49
+ /generation.?limit/i,
50
+ ];
51
+
52
+ const CONTEXT_OVERFLOW_PATTERNS = [
53
+ /overlong.?prompt/i,
54
+ /context.?length.?exceed/i,
55
+ /prompt.?too.?long/i,
56
+ /maximum.?context/i,
57
+ /token.?limit.?exceed/i,
58
+ /input.?too.?long/i,
59
+ /request.?too.?large/i,
60
+ /max.?input.?tokens/i,
61
+ /content.?too.?long/i,
62
+ /exceeds.?(?:the\s+)?(?:model'?s?\s+)?(?:maximum|max).?(?:context|token)/i,
63
+ ];
64
+
65
+ const TRANSIENT_PATTERNS = [
66
+ /rate.?limit/i,
67
+ /429/,
68
+ /too.?many.?requests/i,
69
+ /server.?error/i,
70
+ /502|503|504/,
71
+ /bad.?gateway/i,
72
+ /service.?unavailable/i,
73
+ /gateway.?timeout/i,
74
+ /ECONNRESET/i,
75
+ /ECONNREFUSED/i,
76
+ /ETIMEDOUT/i,
77
+ /socket.?hang.?up/i,
78
+ /network.?error/i,
79
+ /temporary.?failure/i,
80
+ /overloaded/i,
81
+ /capacity/i,
82
+ ];
83
+
84
+ // ── Backoff State ──────────────────────────────────────────────────
85
+
86
+ const backoffState = new Map<string, number>(); // taskId → consecutive transient failures
87
+
88
+ /** Reset backoff state for a task (call on success or non-transient failure) */
89
+ export function resetBackoff(taskId: string): void {
90
+ backoffState.delete(taskId);
91
+ }
92
+
93
+ /** Get current backoff attempt count for a task */
94
+ export function getBackoffAttempt(taskId: string): number {
95
+ return backoffState.get(taskId) ?? 0;
96
+ }
97
+
98
+ // ── Core Classification ────────────────────────────────────────────
99
+
100
+ /**
101
+ * Classify an agent failure and recommend a recovery action.
102
+ *
103
+ * Priority order: truncated > context_overflow > transient > unknown.
104
+ * This ensures the most specific recovery strategy is chosen.
105
+ */
106
+ export function classifyError(ctx: ErrorContext, taskId?: string): RecoveryAction {
107
+ const { exitCode, killed, tailOutput, hasEndTurn } = ctx;
108
+ const text = tailOutput.slice(-2000); // Only check last 2KB
109
+
110
+ // 1. Truncated output detection
111
+ // Agent was cut off mid-response (no end_turn + non-zero exit)
112
+ if (!killed && exitCode !== 0 && hasEndTurn === false) {
113
+ return {
114
+ category: 'truncated',
115
+ strategy: 'continue',
116
+ delayMs: 0,
117
+ reason: 'Agent output appears truncated (no end_turn signal with non-zero exit)',
118
+ continuationPrompt: 'Your previous output was truncated due to output limits. Continue from where you stopped. Do not repeat what you already wrote.',
119
+ };
120
+ }
121
+
122
+ // Also detect explicit truncation error messages
123
+ if (matchesAny(text, TRUNCATED_PATTERNS)) {
124
+ return {
125
+ category: 'truncated',
126
+ strategy: 'continue',
127
+ delayMs: 0,
128
+ reason: `Truncation pattern detected in output`,
129
+ continuationPrompt: 'Your previous output was truncated due to output limits. Continue from where you stopped. Do not repeat what you already wrote.',
130
+ };
131
+ }
132
+
133
+ // 2. Context overflow detection
134
+ if (matchesAny(text, CONTEXT_OVERFLOW_PATTERNS)) {
135
+ return {
136
+ category: 'context_overflow',
137
+ strategy: 'compact_and_retry',
138
+ delayMs: 0,
139
+ reason: 'Context overflow detected — prompt is too large for the model',
140
+ };
141
+ }
142
+
143
+ // 3. Transient error detection
144
+ if (matchesAny(text, TRANSIENT_PATTERNS)) {
145
+ const attempt = taskId ? (backoffState.get(taskId) ?? 0) + 1 : 1;
146
+ if (taskId) backoffState.set(taskId, attempt);
147
+
148
+ // Exponential backoff: 2s, 4s, 8s, capped at 30s
149
+ const baseDelay = 2000;
150
+ const maxDelay = 30_000;
151
+ const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
152
+ // Add jitter: ±25%
153
+ const jitter = delay * 0.25 * (Math.random() * 2 - 1);
154
+
155
+ return {
156
+ category: 'transient',
157
+ strategy: 'backoff_and_retry',
158
+ delayMs: Math.round(delay + jitter),
159
+ reason: `Transient error detected (attempt ${attempt})`,
160
+ backoffAttempt: attempt,
161
+ };
162
+ }
163
+
164
+ // 4. Unknown — fall back to normal retry
165
+ if (taskId) resetBackoff(taskId); // Non-transient failure resets backoff
166
+
167
+ return {
168
+ category: 'unknown',
169
+ strategy: 'normal_retry',
170
+ delayMs: 0,
171
+ reason: killed
172
+ ? 'Agent killed by timeout'
173
+ : `Agent exited with code ${exitCode}`,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Check if the error is recoverable (not a permanent failure).
179
+ * Truncated and transient errors are always recoverable.
180
+ * Context overflow is recoverable if compaction is available.
181
+ * Unknown errors may be recoverable via normal retry.
182
+ */
183
+ export function isRecoverable(action: RecoveryAction): boolean {
184
+ return action.strategy !== 'normal_retry' || action.category === 'unknown';
185
+ }
186
+
187
+ // ── Helpers ────────────────────────────────────────────────────────
188
+
189
+ function matchesAny(text: string, patterns: RegExp[]): boolean {
190
+ return patterns.some(p => p.test(text));
191
+ }
@@ -1,140 +1,140 @@
1
- /**
2
- * Evidence Directory — Phase 7, Step 9: Per-task evidence collection.
3
- *
4
- * Creates a structured evidence directory for each pipeline run:
5
- * .pipeline/<pipelineId>/evidence/<taskId>/
6
- * prompt.md, output.txt, compile.txt, test.txt, result.json
7
- *
8
- * Design principle: evidence collection is best-effort. Disk write failures
9
- * are logged and ignored — never crash the pipeline for evidence.
10
- */
11
-
12
- import { mkdirSync, writeFileSync } from 'node:fs';
13
- import { join } from 'node:path';
14
- import type { GateResult } from './verify-gate.js';
15
- import type { TokenUsage } from './adapters/types.js';
16
-
17
- // ── Types ──────────────────────────────────────────────────────────
18
-
19
- export interface TaskEvidence {
20
- taskId: string;
21
- taskDescription: string;
22
- agentName: string;
23
- status: 'completed' | 'failed';
24
- durationMs: number;
25
- prompt?: string;
26
- tailOutput?: string;
27
- gateResults?: GateResult[];
28
- tokenUsage?: Record<string, TokenUsage>;
29
- costUSD?: number | null;
30
- fixAttempts?: number;
31
- }
32
-
33
- export interface PipelineSummary {
34
- pipelineId: string;
35
- goal: string;
36
- totalTasks: number;
37
- completed: number;
38
- failed: number;
39
- elapsedMs: number;
40
- tokenUsage?: Record<string, TokenUsage>;
41
- costUSD?: number | null;
42
- tasks: TaskEvidence[];
43
- /** A4: Idle agents and why they weren't dispatched */
44
- idleAgents?: Array<{ name: string; reason: string }>;
45
- /** A2: Routing decisions for explainability */
46
- routingDecisions?: Array<{ role: string; selected: string; reason: string; available: string[] }>;
47
- }
48
-
49
- // ── Core ───────────────────────────────────────────────────────────
50
-
51
- /**
52
- * Write evidence for a single task. Best-effort — never throws.
53
- */
54
- export function writeTaskEvidence(
55
- projectDir: string,
56
- pipelineId: string,
57
- evidence: TaskEvidence,
58
- ): string | null {
59
- try {
60
- const dir = join(projectDir, '.pipeline', pipelineId, 'evidence', evidence.taskId.slice(0, 12));
61
- mkdirSync(dir, { recursive: true });
62
-
63
- if (evidence.prompt) {
64
- safeWrite(join(dir, 'prompt.md'), evidence.prompt);
65
- }
66
- if (evidence.tailOutput) {
67
- safeWrite(join(dir, 'output.txt'), evidence.tailOutput);
68
- }
69
- if (evidence.gateResults) {
70
- for (const g of evidence.gateResults) {
71
- const content = `Command: ${g.command}\nStatus: ${g.passed ? 'PASS' : 'FAIL'}\nDuration: ${(g.durationMs / 1000).toFixed(1)}s\n\n${g.output}`;
72
- safeWrite(join(dir, `${g.gate}.txt`), content);
73
- }
74
- }
75
-
76
- const resultJson = {
77
- taskId: evidence.taskId,
78
- description: evidence.taskDescription,
79
- agent: evidence.agentName,
80
- status: evidence.status,
81
- durationMs: evidence.durationMs,
82
- fixAttempts: evidence.fixAttempts ?? 0,
83
- gates: evidence.gateResults?.map(g => ({ gate: g.gate, passed: g.passed, durationMs: g.durationMs, command: g.command })),
84
- tokenUsage: evidence.tokenUsage,
85
- costUSD: evidence.costUSD,
86
- };
87
- safeWrite(join(dir, 'result.json'), JSON.stringify(resultJson, null, 2));
88
- return dir;
89
- } catch {
90
- return null;
91
- }
92
- }
93
-
94
- /**
95
- * Write pipeline summary markdown. Best-effort — never throws.
96
- */
97
- export function writePipelineSummary(
98
- projectDir: string,
99
- summary: PipelineSummary,
100
- ): string | null {
101
- try {
102
- const dir = join(projectDir, '.pipeline', summary.pipelineId);
103
- mkdirSync(dir, { recursive: true });
104
-
105
- const elapsed = (summary.elapsedMs / 1000).toFixed(0);
106
- const lines = [
107
- `# Pipeline: ${summary.goal.slice(0, 100)}`,
108
- '',
109
- `- **ID**: ${summary.pipelineId}`,
110
- `- **Tasks**: ${summary.completed}/${summary.totalTasks} completed, ${summary.failed} failed`,
111
- `- **Elapsed**: ${elapsed}s`,
112
- ];
113
-
114
- if (summary.costUSD != null) {
115
- lines.push(`- **Cost**: $${summary.costUSD.toFixed(4)}`);
116
- }
117
-
118
- lines.push('', '## Tasks', '');
119
- for (const t of summary.tasks) {
120
- const status = t.status === 'completed' ? 'PASS' : 'FAIL';
121
- const dur = (t.durationMs / 1000).toFixed(1);
122
- const fixes = t.fixAttempts ? ` (${t.fixAttempts} fix attempts)` : '';
123
- lines.push(`- [${status}] ${t.taskDescription.slice(0, 80)} — ${t.agentName}, ${dur}s${fixes}`);
124
- }
125
-
126
- const content = lines.join('\n') + '\n';
127
- safeWrite(join(dir, 'summary.md'), content);
128
- return join(dir, 'summary.md');
129
- } catch {
130
- return null;
131
- }
132
- }
133
-
134
- // ── Helpers ────────────────────────────────────────────────────────
135
-
136
- function safeWrite(path: string, content: string): void {
137
- try {
138
- writeFileSync(path, content, 'utf-8');
139
- } catch { /* best-effort */ }
140
- }
1
+ /**
2
+ * Evidence Directory — Phase 7, Step 9: Per-task evidence collection.
3
+ *
4
+ * Creates a structured evidence directory for each pipeline run:
5
+ * .pipeline/<pipelineId>/evidence/<taskId>/
6
+ * prompt.md, output.txt, compile.txt, test.txt, result.json
7
+ *
8
+ * Design principle: evidence collection is best-effort. Disk write failures
9
+ * are logged and ignored — never crash the pipeline for evidence.
10
+ */
11
+
12
+ import { mkdirSync, writeFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import type { GateResult } from './verify-gate.js';
15
+ import type { TokenUsage } from './adapters/types.js';
16
+
17
+ // ── Types ──────────────────────────────────────────────────────────
18
+
19
+ export interface TaskEvidence {
20
+ taskId: string;
21
+ taskDescription: string;
22
+ agentName: string;
23
+ status: 'completed' | 'failed';
24
+ durationMs: number;
25
+ prompt?: string;
26
+ tailOutput?: string;
27
+ gateResults?: GateResult[];
28
+ tokenUsage?: Record<string, TokenUsage>;
29
+ costUSD?: number | null;
30
+ fixAttempts?: number;
31
+ }
32
+
33
+ export interface PipelineSummary {
34
+ pipelineId: string;
35
+ goal: string;
36
+ totalTasks: number;
37
+ completed: number;
38
+ failed: number;
39
+ elapsedMs: number;
40
+ tokenUsage?: Record<string, TokenUsage>;
41
+ costUSD?: number | null;
42
+ tasks: TaskEvidence[];
43
+ /** A4: Idle agents and why they weren't dispatched */
44
+ idleAgents?: Array<{ name: string; reason: string }>;
45
+ /** A2: Routing decisions for explainability */
46
+ routingDecisions?: Array<{ role: string; selected: string; reason: string; available: string[] }>;
47
+ }
48
+
49
+ // ── Core ───────────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Write evidence for a single task. Best-effort — never throws.
53
+ */
54
+ export function writeTaskEvidence(
55
+ projectDir: string,
56
+ pipelineId: string,
57
+ evidence: TaskEvidence,
58
+ ): string | null {
59
+ try {
60
+ const dir = join(projectDir, '.pipeline', pipelineId, 'evidence', evidence.taskId.slice(0, 12));
61
+ mkdirSync(dir, { recursive: true });
62
+
63
+ if (evidence.prompt) {
64
+ safeWrite(join(dir, 'prompt.md'), evidence.prompt);
65
+ }
66
+ if (evidence.tailOutput) {
67
+ safeWrite(join(dir, 'output.txt'), evidence.tailOutput);
68
+ }
69
+ if (evidence.gateResults) {
70
+ for (const g of evidence.gateResults) {
71
+ const content = `Command: ${g.command}\nStatus: ${g.passed ? 'PASS' : 'FAIL'}\nDuration: ${(g.durationMs / 1000).toFixed(1)}s\n\n${g.output}`;
72
+ safeWrite(join(dir, `${g.gate}.txt`), content);
73
+ }
74
+ }
75
+
76
+ const resultJson = {
77
+ taskId: evidence.taskId,
78
+ description: evidence.taskDescription,
79
+ agent: evidence.agentName,
80
+ status: evidence.status,
81
+ durationMs: evidence.durationMs,
82
+ fixAttempts: evidence.fixAttempts ?? 0,
83
+ gates: evidence.gateResults?.map(g => ({ gate: g.gate, passed: g.passed, durationMs: g.durationMs, command: g.command })),
84
+ tokenUsage: evidence.tokenUsage,
85
+ costUSD: evidence.costUSD,
86
+ };
87
+ safeWrite(join(dir, 'result.json'), JSON.stringify(resultJson, null, 2));
88
+ return dir;
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Write pipeline summary markdown. Best-effort — never throws.
96
+ */
97
+ export function writePipelineSummary(
98
+ projectDir: string,
99
+ summary: PipelineSummary,
100
+ ): string | null {
101
+ try {
102
+ const dir = join(projectDir, '.pipeline', summary.pipelineId);
103
+ mkdirSync(dir, { recursive: true });
104
+
105
+ const elapsed = (summary.elapsedMs / 1000).toFixed(0);
106
+ const lines = [
107
+ `# Pipeline: ${summary.goal.slice(0, 100)}`,
108
+ '',
109
+ `- **ID**: ${summary.pipelineId}`,
110
+ `- **Tasks**: ${summary.completed}/${summary.totalTasks} completed, ${summary.failed} failed`,
111
+ `- **Elapsed**: ${elapsed}s`,
112
+ ];
113
+
114
+ if (summary.costUSD != null) {
115
+ lines.push(`- **Cost**: $${summary.costUSD.toFixed(4)}`);
116
+ }
117
+
118
+ lines.push('', '## Tasks', '');
119
+ for (const t of summary.tasks) {
120
+ const status = t.status === 'completed' ? 'PASS' : 'FAIL';
121
+ const dur = (t.durationMs / 1000).toFixed(1);
122
+ const fixes = t.fixAttempts ? ` (${t.fixAttempts} fix attempts)` : '';
123
+ lines.push(`- [${status}] ${t.taskDescription.slice(0, 80)} — ${t.agentName}, ${dur}s${fixes}`);
124
+ }
125
+
126
+ const content = lines.join('\n') + '\n';
127
+ safeWrite(join(dir, 'summary.md'), content);
128
+ return join(dir, 'summary.md');
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ // ── Helpers ────────────────────────────────────────────────────────
135
+
136
+ function safeWrite(path: string, content: string): void {
137
+ try {
138
+ writeFileSync(path, content, 'utf-8');
139
+ } catch { /* best-effort */ }
140
+ }