sequant 1.20.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.claude-plugin/marketplace.json +2 -4
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +29 -9
  4. package/dist/bin/cli.js +25 -2
  5. package/dist/src/commands/doctor.js +42 -9
  6. package/dist/src/commands/init.d.ts +1 -0
  7. package/dist/src/commands/init.js +52 -0
  8. package/dist/src/commands/logs.d.ts +1 -0
  9. package/dist/src/commands/logs.js +18 -2
  10. package/dist/src/commands/run.d.ts +7 -0
  11. package/dist/src/commands/run.js +235 -68
  12. package/dist/src/commands/serve.d.ts +13 -0
  13. package/dist/src/commands/serve.js +131 -0
  14. package/dist/src/commands/stats.d.ts +1 -0
  15. package/dist/src/commands/stats.js +185 -26
  16. package/dist/src/commands/status.d.ts +2 -0
  17. package/dist/src/commands/status.js +99 -50
  18. package/dist/src/index.d.ts +2 -2
  19. package/dist/src/index.js +4 -1
  20. package/dist/src/lib/ac-parser.d.ts +2 -0
  21. package/dist/src/lib/ac-parser.js +12 -2
  22. package/dist/src/lib/assess-comment-parser.d.ts +137 -0
  23. package/dist/src/lib/assess-comment-parser.js +344 -0
  24. package/dist/src/lib/ci/config.d.ts +22 -0
  25. package/dist/src/lib/ci/config.js +134 -0
  26. package/dist/src/lib/ci/index.d.ts +12 -0
  27. package/dist/src/lib/ci/index.js +10 -0
  28. package/dist/src/lib/ci/inputs.d.ts +29 -0
  29. package/dist/src/lib/ci/inputs.js +103 -0
  30. package/dist/src/lib/ci/labels.d.ts +34 -0
  31. package/dist/src/lib/ci/labels.js +101 -0
  32. package/dist/src/lib/ci/outputs.d.ts +25 -0
  33. package/dist/src/lib/ci/outputs.js +84 -0
  34. package/dist/src/lib/ci/triggers.d.ts +9 -0
  35. package/dist/src/lib/ci/triggers.js +86 -0
  36. package/dist/src/lib/ci/types.d.ts +131 -0
  37. package/dist/src/lib/ci/types.js +47 -0
  38. package/dist/src/lib/mcp-config.d.ts +54 -0
  39. package/dist/src/lib/mcp-config.js +172 -0
  40. package/dist/src/lib/merge-check/index.js +6 -12
  41. package/dist/src/lib/merge-check/types.d.ts +20 -7
  42. package/dist/src/lib/merge-check/types.js +11 -0
  43. package/dist/src/lib/phase-signal.d.ts +3 -3
  44. package/dist/src/lib/phase-signal.js +5 -3
  45. package/dist/src/lib/settings.d.ts +52 -0
  46. package/dist/src/lib/settings.js +41 -0
  47. package/dist/src/lib/shutdown.d.ts +16 -5
  48. package/dist/src/lib/shutdown.js +32 -12
  49. package/dist/src/lib/solve-comment-parser.d.ts +9 -102
  50. package/dist/src/lib/solve-comment-parser.js +13 -248
  51. package/dist/src/lib/stacks.d.ts +8 -0
  52. package/dist/src/lib/stacks.js +34 -0
  53. package/dist/src/lib/system.js +3 -7
  54. package/dist/src/lib/test-tautology-detector.d.ts +10 -0
  55. package/dist/src/lib/test-tautology-detector.js +43 -4
  56. package/dist/src/lib/upstream/assessment.js +9 -59
  57. package/dist/src/lib/upstream/issues.js +12 -75
  58. package/dist/src/lib/version-check.d.ts +2 -2
  59. package/dist/src/lib/version-check.js +6 -3
  60. package/dist/src/lib/version.d.ts +4 -0
  61. package/dist/src/lib/version.js +25 -0
  62. package/dist/src/lib/workflow/batch-executor.d.ts +18 -86
  63. package/dist/src/lib/workflow/batch-executor.js +232 -55
  64. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +56 -0
  65. package/dist/src/lib/workflow/drivers/agent-driver.js +8 -0
  66. package/dist/src/lib/workflow/drivers/aider.d.ts +18 -0
  67. package/dist/src/lib/workflow/drivers/aider.js +160 -0
  68. package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -0
  69. package/dist/src/lib/workflow/drivers/claude-code.js +165 -0
  70. package/dist/src/lib/workflow/drivers/index.d.ts +20 -0
  71. package/dist/src/lib/workflow/drivers/index.js +27 -0
  72. package/dist/src/lib/workflow/error-classifier.d.ts +16 -0
  73. package/dist/src/lib/workflow/error-classifier.js +90 -0
  74. package/dist/src/lib/workflow/log-writer.d.ts +6 -3
  75. package/dist/src/lib/workflow/log-writer.js +57 -27
  76. package/dist/src/lib/workflow/metrics-schema.d.ts +9 -9
  77. package/dist/src/lib/workflow/phase-detection.d.ts +23 -0
  78. package/dist/src/lib/workflow/phase-detection.js +45 -29
  79. package/dist/src/lib/workflow/phase-executor.d.ts +42 -3
  80. package/dist/src/lib/workflow/phase-executor.js +340 -220
  81. package/dist/src/lib/workflow/phase-mapper.d.ts +1 -1
  82. package/dist/src/lib/workflow/phase-mapper.js +7 -7
  83. package/dist/src/lib/workflow/platforms/github.d.ts +157 -0
  84. package/dist/src/lib/workflow/platforms/github.js +466 -0
  85. package/dist/src/lib/workflow/platforms/index.d.ts +17 -0
  86. package/dist/src/lib/workflow/platforms/index.js +25 -0
  87. package/dist/src/lib/workflow/platforms/platform-provider.d.ts +67 -0
  88. package/dist/src/lib/workflow/platforms/platform-provider.js +8 -0
  89. package/dist/src/lib/workflow/pr-status.d.ts +2 -4
  90. package/dist/src/lib/workflow/pr-status.js +3 -16
  91. package/dist/src/lib/workflow/qa-cache.d.ts +58 -0
  92. package/dist/src/lib/workflow/qa-cache.js +88 -0
  93. package/dist/src/lib/workflow/reconcile.d.ts +69 -0
  94. package/dist/src/lib/workflow/reconcile.js +290 -0
  95. package/dist/src/lib/workflow/ring-buffer.d.ts +17 -0
  96. package/dist/src/lib/workflow/ring-buffer.js +37 -0
  97. package/dist/src/lib/workflow/run-log-schema.d.ts +115 -24
  98. package/dist/src/lib/workflow/run-log-schema.js +47 -12
  99. package/dist/src/lib/workflow/run-reflect.js +1 -1
  100. package/dist/src/lib/workflow/state-cleanup.js +21 -0
  101. package/dist/src/lib/workflow/state-manager.d.ts +34 -3
  102. package/dist/src/lib/workflow/state-manager.js +278 -126
  103. package/dist/src/lib/workflow/state-schema.d.ts +34 -30
  104. package/dist/src/lib/workflow/state-schema.js +35 -25
  105. package/dist/src/lib/workflow/state-utils.d.ts +3 -1
  106. package/dist/src/lib/workflow/state-utils.js +1 -0
  107. package/dist/src/lib/workflow/types.d.ts +208 -6
  108. package/dist/src/lib/workflow/types.js +20 -1
  109. package/dist/src/lib/workflow/worktree-discovery.d.ts +1 -1
  110. package/dist/src/lib/workflow/worktree-discovery.js +6 -14
  111. package/dist/src/lib/workflow/worktree-manager.js +33 -51
  112. package/dist/src/mcp/index.d.ts +4 -0
  113. package/dist/src/mcp/index.js +4 -0
  114. package/dist/src/mcp/resources.d.ts +7 -0
  115. package/dist/src/mcp/resources.js +111 -0
  116. package/dist/src/mcp/run-registry.d.ts +34 -0
  117. package/dist/src/mcp/run-registry.js +42 -0
  118. package/dist/src/mcp/server.d.ts +12 -0
  119. package/dist/src/mcp/server.js +50 -0
  120. package/dist/src/mcp/tools/logs.d.ts +7 -0
  121. package/dist/src/mcp/tools/logs.js +149 -0
  122. package/dist/src/mcp/tools/run.d.ts +121 -0
  123. package/dist/src/mcp/tools/run.js +591 -0
  124. package/dist/src/mcp/tools/status.d.ts +7 -0
  125. package/dist/src/mcp/tools/status.js +127 -0
  126. package/package.json +10 -1
  127. package/templates/hooks/post-tool.sh +19 -8
  128. package/templates/hooks/pre-tool.sh +36 -49
  129. package/templates/mcp.json +6 -0
  130. package/templates/skills/assess/SKILL.md +354 -352
  131. package/templates/skills/exec/SKILL.md +64 -1
  132. package/templates/skills/fullsolve/SKILL.md +35 -4
  133. package/templates/skills/qa/SKILL.md +486 -9
  134. package/templates/skills/qa/scripts/quality-checks.sh +1 -1
  135. package/templates/skills/setup/SKILL.md +386 -0
  136. package/templates/skills/solve/SKILL.md +38 -664
  137. package/templates/skills/spec/SKILL.md +90 -31
@@ -0,0 +1,160 @@
1
+ /**
2
+ * AiderDriver — AgentDriver implementation wrapping the Aider CLI.
3
+ *
4
+ * Shells out to `aider --yes --no-auto-commits --message "<prompt>"`
5
+ * for fully non-interactive phase execution. Sequant manages git,
6
+ * not Aider.
7
+ */
8
+ import { spawn } from "child_process";
9
+ import { execSync } from "child_process";
10
+ import { RingBuffer } from "../ring-buffer.js";
11
+ export class AiderDriver {
12
+ name = "aider";
13
+ settings;
14
+ constructor(settings) {
15
+ this.settings = settings;
16
+ }
17
+ async executePhase(prompt, config) {
18
+ const args = this.buildArgs(prompt, config.files);
19
+ return new Promise((resolve) => {
20
+ let capturedOutput = "";
21
+ let capturedStderr = "";
22
+ const stderrBuffer = new RingBuffer(50);
23
+ const stdoutBuffer = new RingBuffer(50);
24
+ const proc = spawn("aider", args, {
25
+ cwd: config.cwd,
26
+ env: { ...process.env, ...config.env },
27
+ stdio: ["ignore", "pipe", "pipe"],
28
+ });
29
+ // Set up timeout
30
+ const timeoutId = setTimeout(() => {
31
+ proc.kill("SIGTERM");
32
+ }, config.phaseTimeout * 1000);
33
+ // Wire external abort signal
34
+ if (config.abortSignal) {
35
+ const onAbort = () => {
36
+ proc.kill("SIGTERM");
37
+ };
38
+ config.abortSignal.addEventListener("abort", onAbort);
39
+ proc.on("close", () => {
40
+ config.abortSignal?.removeEventListener("abort", onAbort);
41
+ });
42
+ }
43
+ proc.stdout.on("data", (data) => {
44
+ const text = data.toString();
45
+ capturedOutput += text;
46
+ const lines = text.split("\n").filter((l) => l.length > 0);
47
+ for (const line of lines) {
48
+ stdoutBuffer.push(line);
49
+ }
50
+ if (config.verbose) {
51
+ config.onOutput?.(text);
52
+ }
53
+ });
54
+ proc.stderr.on("data", (data) => {
55
+ const text = data.toString();
56
+ capturedStderr += text;
57
+ const lines = text.split("\n").filter((l) => l.length > 0);
58
+ for (const line of lines) {
59
+ stderrBuffer.push(line);
60
+ }
61
+ config.onStderr?.(text);
62
+ });
63
+ proc.on("error", (err) => {
64
+ clearTimeout(timeoutId);
65
+ if (err.code === "ENOENT") {
66
+ resolve({
67
+ success: false,
68
+ output: capturedOutput,
69
+ error: "Aider CLI not found. Install it with: pip install aider-chat",
70
+ stderrTail: stderrBuffer.getLines(),
71
+ stdoutTail: stdoutBuffer.getLines(),
72
+ });
73
+ }
74
+ else {
75
+ resolve({
76
+ success: false,
77
+ output: capturedOutput,
78
+ error: `Failed to start aider: ${err.message}`,
79
+ stderrTail: stderrBuffer.getLines(),
80
+ stdoutTail: stdoutBuffer.getLines(),
81
+ });
82
+ }
83
+ });
84
+ proc.on("close", (code, signal) => {
85
+ clearTimeout(timeoutId);
86
+ if (signal) {
87
+ const isTimeout = signal === "SIGTERM";
88
+ resolve({
89
+ success: false,
90
+ output: capturedOutput,
91
+ error: isTimeout
92
+ ? `Timeout after ${config.phaseTimeout}s`
93
+ : `Process killed by signal: ${signal}`,
94
+ stderrTail: stderrBuffer.getLines(),
95
+ stdoutTail: stdoutBuffer.getLines(),
96
+ exitCode: code ?? undefined,
97
+ });
98
+ return;
99
+ }
100
+ if (code === 0) {
101
+ resolve({
102
+ success: true,
103
+ output: capturedOutput,
104
+ stderrTail: stderrBuffer.getLines(),
105
+ stdoutTail: stdoutBuffer.getLines(),
106
+ exitCode: 0,
107
+ });
108
+ }
109
+ else {
110
+ const stderrSuffix = capturedStderr
111
+ ? `\nStderr: ${capturedStderr.slice(0, 500)}`
112
+ : "";
113
+ resolve({
114
+ success: false,
115
+ output: capturedOutput,
116
+ error: `Aider exited with code ${code}${stderrSuffix}`,
117
+ stderrTail: stderrBuffer.getLines(),
118
+ stdoutTail: stdoutBuffer.getLines(),
119
+ exitCode: code ?? undefined,
120
+ });
121
+ }
122
+ });
123
+ });
124
+ }
125
+ async isAvailable() {
126
+ try {
127
+ execSync("which aider", { stdio: "pipe" });
128
+ return true;
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ }
134
+ /** Build the CLI argument list for aider. */
135
+ buildArgs(prompt, files) {
136
+ const args = [
137
+ "--yes",
138
+ "--no-auto-commits",
139
+ "--no-pretty",
140
+ "--message",
141
+ prompt,
142
+ ];
143
+ if (this.settings?.model) {
144
+ args.push("--model", this.settings.model);
145
+ }
146
+ if (this.settings?.editFormat) {
147
+ args.push("--edit-format", this.settings.editFormat);
148
+ }
149
+ if (this.settings?.extraArgs) {
150
+ args.push(...this.settings.extraArgs);
151
+ }
152
+ // Pass relevant files for context (e.g., changed files from git diff)
153
+ if (files?.length) {
154
+ for (const file of files) {
155
+ args.push("--file", file);
156
+ }
157
+ }
158
+ return args;
159
+ }
160
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * ClaudeCodeDriver — AgentDriver implementation wrapping the Claude Agent SDK.
3
+ *
4
+ * Owns the `@anthropic-ai/claude-agent-sdk` import. No other file in the
5
+ * orchestration layer should import the SDK directly.
6
+ */
7
+ import type { AgentDriver, AgentExecutionConfig, AgentPhaseResult } from "./agent-driver.js";
8
+ export declare class ClaudeCodeDriver implements AgentDriver {
9
+ name: string;
10
+ /**
11
+ * Track session ID across calls so callers can implement resume.
12
+ * Set after each executePhase() call.
13
+ */
14
+ private lastSessionId?;
15
+ executePhase(prompt: string, config: AgentExecutionConfig): Promise<AgentPhaseResult>;
16
+ isAvailable(): Promise<boolean>;
17
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * ClaudeCodeDriver — AgentDriver implementation wrapping the Claude Agent SDK.
3
+ *
4
+ * Owns the `@anthropic-ai/claude-agent-sdk` import. No other file in the
5
+ * orchestration layer should import the SDK directly.
6
+ */
7
+ import { query } from "@anthropic-ai/claude-agent-sdk";
8
+ import { getMcpServersConfig } from "../../system.js";
9
+ import { RingBuffer } from "../ring-buffer.js";
10
+ export class ClaudeCodeDriver {
11
+ name = "claude-code";
12
+ /**
13
+ * Track session ID across calls so callers can implement resume.
14
+ * Set after each executePhase() call.
15
+ */
16
+ lastSessionId;
17
+ async executePhase(prompt, config) {
18
+ const abortController = new AbortController();
19
+ // Wire external abort signal
20
+ if (config.abortSignal) {
21
+ config.abortSignal.addEventListener("abort", () => abortController.abort());
22
+ }
23
+ // Set up timeout
24
+ const timeoutId = setTimeout(() => {
25
+ abortController.abort();
26
+ }, config.phaseTimeout * 1000);
27
+ let resultSessionId;
28
+ let resultMessage;
29
+ let capturedOutput = "";
30
+ let capturedStderr = "";
31
+ const stderrBuffer = new RingBuffer(50);
32
+ const stdoutBuffer = new RingBuffer(50);
33
+ try {
34
+ // Get MCP servers config if enabled
35
+ const mcpServers = config.mcp ? getMcpServersConfig() : undefined;
36
+ const queryInstance = query({
37
+ prompt,
38
+ options: {
39
+ abortController,
40
+ cwd: config.cwd,
41
+ settingSources: ["project"],
42
+ systemPrompt: { type: "preset", preset: "claude_code" },
43
+ tools: { type: "preset", preset: "claude_code" },
44
+ permissionMode: "bypassPermissions",
45
+ allowDangerouslySkipPermissions: true,
46
+ // Resume from previous session if provided
47
+ ...(config.sessionId ? { resume: config.sessionId } : {}),
48
+ env: config.env,
49
+ ...(mcpServers ? { mcpServers } : {}),
50
+ stderr: (data) => {
51
+ capturedStderr += data;
52
+ // Split on newlines and push each line to the ring buffer
53
+ const lines = data.split("\n").filter((l) => l.length > 0);
54
+ for (const line of lines) {
55
+ stderrBuffer.push(line);
56
+ }
57
+ config.onStderr?.(data);
58
+ },
59
+ },
60
+ });
61
+ // Stream and process messages
62
+ for await (const message of queryInstance) {
63
+ if (message.type === "system" && message.subtype === "init") {
64
+ resultSessionId = message.session_id;
65
+ }
66
+ if (message.type === "assistant") {
67
+ const content = message.message.content;
68
+ const textContent = content
69
+ .filter((c) => c.type === "text" && c.text)
70
+ .map((c) => c.text)
71
+ .join("");
72
+ if (textContent) {
73
+ capturedOutput += textContent;
74
+ const lines = textContent.split("\n").filter((l) => l.length > 0);
75
+ for (const line of lines) {
76
+ stdoutBuffer.push(line);
77
+ }
78
+ config.onOutput?.(textContent);
79
+ }
80
+ }
81
+ if (message.type === "result") {
82
+ resultMessage = message;
83
+ }
84
+ }
85
+ clearTimeout(timeoutId);
86
+ this.lastSessionId = resultSessionId;
87
+ if (resultMessage) {
88
+ if (resultMessage.subtype === "success") {
89
+ return {
90
+ success: true,
91
+ output: capturedOutput,
92
+ sessionId: resultSessionId,
93
+ stderrTail: stderrBuffer.getLines(),
94
+ stdoutTail: stdoutBuffer.getLines(),
95
+ };
96
+ }
97
+ // Handle error subtypes
98
+ let error;
99
+ const errorSubtype = resultMessage.subtype;
100
+ if (errorSubtype === "error_max_turns") {
101
+ error = "Max turns reached";
102
+ }
103
+ else if (errorSubtype === "error_during_execution") {
104
+ error = resultMessage.errors?.join(", ") || "Error during execution";
105
+ }
106
+ else if (errorSubtype === "error_max_budget_usd") {
107
+ error = "Budget limit exceeded";
108
+ }
109
+ else {
110
+ error = `Error: ${errorSubtype}`;
111
+ }
112
+ return {
113
+ success: false,
114
+ output: capturedOutput,
115
+ sessionId: resultSessionId,
116
+ error,
117
+ stderrTail: stderrBuffer.getLines(),
118
+ stdoutTail: stdoutBuffer.getLines(),
119
+ };
120
+ }
121
+ return {
122
+ success: false,
123
+ output: capturedOutput,
124
+ sessionId: resultSessionId,
125
+ error: "No result received from Claude",
126
+ stderrTail: stderrBuffer.getLines(),
127
+ stdoutTail: stdoutBuffer.getLines(),
128
+ };
129
+ }
130
+ catch (err) {
131
+ clearTimeout(timeoutId);
132
+ const error = err instanceof Error ? err.message : String(err);
133
+ if (error.includes("abort") || error.includes("AbortError")) {
134
+ return {
135
+ success: false,
136
+ output: capturedOutput,
137
+ error: `Timeout after ${config.phaseTimeout}s`,
138
+ stderrTail: stderrBuffer.getLines(),
139
+ stdoutTail: stdoutBuffer.getLines(),
140
+ };
141
+ }
142
+ const stderrSuffix = capturedStderr
143
+ ? `\nStderr: ${capturedStderr.slice(0, 500)}`
144
+ : "";
145
+ return {
146
+ success: false,
147
+ output: capturedOutput,
148
+ sessionId: resultSessionId,
149
+ error: error + stderrSuffix,
150
+ stderrTail: stderrBuffer.getLines(),
151
+ stdoutTail: stdoutBuffer.getLines(),
152
+ };
153
+ }
154
+ }
155
+ async isAvailable() {
156
+ try {
157
+ // If we can import the SDK, it's available
158
+ await import("@anthropic-ai/claude-agent-sdk");
159
+ return true;
160
+ }
161
+ catch {
162
+ return false;
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Agent driver registry.
3
+ *
4
+ * Simple map-based registry — not a plugin system.
5
+ * New drivers are registered by adding entries to DRIVERS.
6
+ */
7
+ import type { AgentDriver } from "./agent-driver.js";
8
+ import type { AiderSettings } from "../../settings.js";
9
+ export type { AgentDriver, AgentExecutionConfig, AgentPhaseResult, } from "./agent-driver.js";
10
+ export interface DriverOptions {
11
+ aiderSettings?: AiderSettings;
12
+ }
13
+ /**
14
+ * Get an agent driver by name.
15
+ *
16
+ * @param name - Driver name (default: "claude-code")
17
+ * @param options - Optional driver-specific settings
18
+ * @throws Error if driver name is unknown
19
+ */
20
+ export declare function getDriver(name?: string, options?: DriverOptions): AgentDriver;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Agent driver registry.
3
+ *
4
+ * Simple map-based registry — not a plugin system.
5
+ * New drivers are registered by adding entries to DRIVERS.
6
+ */
7
+ import { ClaudeCodeDriver } from "./claude-code.js";
8
+ import { AiderDriver } from "./aider.js";
9
+ const DRIVERS = {
10
+ "claude-code": () => new ClaudeCodeDriver(),
11
+ aider: (opts) => new AiderDriver(opts?.aiderSettings),
12
+ };
13
+ /**
14
+ * Get an agent driver by name.
15
+ *
16
+ * @param name - Driver name (default: "claude-code")
17
+ * @param options - Optional driver-specific settings
18
+ * @throws Error if driver name is unknown
19
+ */
20
+ export function getDriver(name = "claude-code", options) {
21
+ const factory = DRIVERS[name];
22
+ if (!factory) {
23
+ const available = Object.keys(DRIVERS).join(", ");
24
+ throw new Error(`Unknown agent driver "${name}". Available drivers: ${available}`);
25
+ }
26
+ return factory(options);
27
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Error classifier — categorizes phase failures from stderr content.
3
+ *
4
+ * Pattern-matches stderr lines against known error signatures to produce
5
+ * a structured category for analytics and debugging.
6
+ */
7
+ /** All recognized error categories (AC-7: defined as constants). */
8
+ export declare const ERROR_CATEGORIES: readonly ["context_overflow", "api_error", "hook_failure", "build_error", "timeout", "unknown"];
9
+ export type ErrorCategory = (typeof ERROR_CATEGORIES)[number];
10
+ /**
11
+ * Classify stderr lines into an error category.
12
+ *
13
+ * Scans lines in order; the first classifier whose pattern matches any line wins.
14
+ * Returns "unknown" if no patterns match.
15
+ */
16
+ export declare function classifyError(stderrLines: string[]): ErrorCategory;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Error classifier — categorizes phase failures from stderr content.
3
+ *
4
+ * Pattern-matches stderr lines against known error signatures to produce
5
+ * a structured category for analytics and debugging.
6
+ */
7
+ /** All recognized error categories (AC-7: defined as constants). */
8
+ export const ERROR_CATEGORIES = [
9
+ "context_overflow",
10
+ "api_error",
11
+ "hook_failure",
12
+ "build_error",
13
+ "timeout",
14
+ "unknown",
15
+ ];
16
+ /**
17
+ * Ordered list of classifiers. First match wins (highest priority first).
18
+ */
19
+ const CLASSIFIERS = [
20
+ {
21
+ category: "context_overflow",
22
+ patterns: [
23
+ /context window/i,
24
+ /token limit/i,
25
+ /context length/i,
26
+ /max.?context/i,
27
+ /exceeded.*context/i,
28
+ ],
29
+ },
30
+ {
31
+ category: "timeout",
32
+ patterns: [/timeout/i, /timed?\s*out/i, /SIGTERM/, /deadline exceeded/i],
33
+ },
34
+ {
35
+ category: "api_error",
36
+ patterns: [
37
+ /rate.?limit/i,
38
+ /\b429\b/,
39
+ /api.*error/i,
40
+ /auth.*fail/i,
41
+ /unauthorized/i,
42
+ /\b503\b/,
43
+ /\b502\b/,
44
+ /overloaded/i,
45
+ ],
46
+ },
47
+ {
48
+ category: "hook_failure",
49
+ patterns: [
50
+ /hook.*fail/i,
51
+ /pre-?commit/i,
52
+ /HOOK_BLOCKED/i,
53
+ /blocked by hook/i,
54
+ ],
55
+ },
56
+ {
57
+ category: "build_error",
58
+ patterns: [
59
+ /typescript.*error/i,
60
+ /TS\d{4,5}:/,
61
+ /syntax\s*error/i,
62
+ /cannot find module/i,
63
+ /compilation.*fail/i,
64
+ /build.*fail/i,
65
+ /eslint/i,
66
+ /npm ERR!/,
67
+ ],
68
+ },
69
+ ];
70
+ /**
71
+ * Classify stderr lines into an error category.
72
+ *
73
+ * Scans lines in order; the first classifier whose pattern matches any line wins.
74
+ * Returns "unknown" if no patterns match.
75
+ */
76
+ export function classifyError(stderrLines) {
77
+ if (!stderrLines || stderrLines.length === 0) {
78
+ return "unknown";
79
+ }
80
+ for (const { category, patterns } of CLASSIFIERS) {
81
+ for (const line of stderrLines) {
82
+ for (const pattern of patterns) {
83
+ if (pattern.test(line)) {
84
+ return category;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ return "unknown";
90
+ }
@@ -32,6 +32,9 @@ export interface LogWriterOptions {
32
32
  */
33
33
  export declare class LogWriter {
34
34
  private runLog;
35
+ /** Active issues being tracked concurrently, keyed by issue number */
36
+ private activeIssues;
37
+ /** @deprecated Single-issue slot for backwards compatibility — use activeIssues */
35
38
  private currentIssue;
36
39
  private logPath;
37
40
  private writeToUserLogs;
@@ -62,11 +65,11 @@ export declare class LogWriter {
62
65
  /**
63
66
  * Set PR info on the current issue (call before completeIssue)
64
67
  */
65
- setPRInfo(prNumber: number, prUrl: string): void;
68
+ setPRInfo(prNumber: number, prUrl: string, issueNumber?: number): void;
66
69
  /**
67
70
  * Complete the current issue and add it to the run log
68
71
  */
69
- completeIssue(): void;
72
+ completeIssue(issueNumber?: number): void;
70
73
  /**
71
74
  * Finalize the run log and write to disk
72
75
  *
@@ -96,4 +99,4 @@ export declare class LogWriter {
96
99
  *
97
100
  * Utility function for creating phase logs when you have start/end times.
98
101
  */
99
- export declare function createPhaseLogFromTiming(phase: Phase, issueNumber: number, startTime: Date, endTime: Date, status: PhaseLog["status"], options?: Partial<Pick<PhaseLog, "error" | "iterations" | "filesModified" | "testsRun" | "testsPassed" | "verdict" | "commitHash" | "fileDiffStats" | "cacheMetrics">>): PhaseLog;
102
+ export declare function createPhaseLogFromTiming(phase: Phase, issueNumber: number, startTime: Date, endTime: Date, status: PhaseLog["status"], options?: Partial<Pick<PhaseLog, "error" | "iterations" | "filesModified" | "testsRun" | "testsPassed" | "verdict" | "summary" | "commitHash" | "fileDiffStats" | "cacheMetrics" | "errorContext">>): PhaseLog;
@@ -23,6 +23,9 @@ import { rotateIfNeeded, DEFAULT_ROTATION_SETTINGS, } from "./log-rotation.js";
23
23
  */
24
24
  export class LogWriter {
25
25
  runLog = null;
26
+ /** Active issues being tracked concurrently, keyed by issue number */
27
+ activeIssues = new Map();
28
+ /** @deprecated Single-issue slot for backwards compatibility — use activeIssues */
26
29
  currentIssue = null;
27
30
  logPath;
28
31
  writeToUserLogs;
@@ -64,7 +67,7 @@ export class LogWriter {
64
67
  if (!this.runLog) {
65
68
  throw new Error("LogWriter not initialized. Call initialize() first.");
66
69
  }
67
- this.currentIssue = {
70
+ const issueData = {
68
71
  issueNumber,
69
72
  title,
70
73
  labels,
@@ -72,6 +75,9 @@ export class LogWriter {
72
75
  status: "success",
73
76
  totalDurationSeconds: 0,
74
77
  };
78
+ this.activeIssues.set(issueNumber, issueData);
79
+ // Keep currentIssue in sync for callers that don't pass issueNumber
80
+ this.currentIssue = issueData;
75
81
  if (this.verbose) {
76
82
  console.log(`📝 Started logging issue #${issueNumber}`);
77
83
  }
@@ -82,17 +88,18 @@ export class LogWriter {
82
88
  * @param phaseLog - Complete phase log entry
83
89
  */
84
90
  logPhase(phaseLog) {
85
- if (!this.currentIssue) {
86
- throw new Error("No current issue. Call startIssue() first.");
91
+ // Route to the correct issue by issueNumber, falling back to currentIssue
92
+ const issue = this.activeIssues.get(phaseLog.issueNumber) ?? this.currentIssue;
93
+ if (!issue) {
94
+ throw new Error(`No active issue #${phaseLog.issueNumber}. Call startIssue() first.`);
87
95
  }
88
- this.currentIssue.phases = [...(this.currentIssue.phases ?? []), phaseLog];
96
+ issue.phases = [...(issue.phases ?? []), phaseLog];
89
97
  // Update issue status based on phase result
90
98
  if (phaseLog.status === "failure") {
91
- this.currentIssue.status = "failure";
99
+ issue.status = "failure";
92
100
  }
93
- else if (phaseLog.status === "timeout" &&
94
- this.currentIssue.status !== "failure") {
95
- this.currentIssue.status = "partial";
101
+ else if (phaseLog.status === "timeout" && issue.status !== "failure") {
102
+ issue.status = "partial";
96
103
  }
97
104
  if (this.verbose) {
98
105
  console.log(`📝 Logged phase: ${phaseLog.phase} (${phaseLog.status}) - ${phaseLog.durationSeconds.toFixed(1)}s`);
@@ -101,38 +108,57 @@ export class LogWriter {
101
108
  /**
102
109
  * Set PR info on the current issue (call before completeIssue)
103
110
  */
104
- setPRInfo(prNumber, prUrl) {
105
- if (!this.currentIssue) {
111
+ setPRInfo(prNumber, prUrl, issueNumber) {
112
+ const issue = issueNumber
113
+ ? (this.activeIssues.get(issueNumber) ?? this.currentIssue)
114
+ : this.currentIssue;
115
+ if (!issue) {
106
116
  return;
107
117
  }
108
- this.currentIssue.prNumber = prNumber;
109
- this.currentIssue.prUrl = prUrl;
118
+ issue.prNumber = prNumber;
119
+ issue.prUrl = prUrl;
110
120
  }
111
121
  /**
112
122
  * Complete the current issue and add it to the run log
113
123
  */
114
- completeIssue() {
115
- if (!this.runLog || !this.currentIssue) {
116
- throw new Error("No current issue to complete.");
124
+ completeIssue(issueNumber) {
125
+ if (!this.runLog) {
126
+ throw new Error("No run log. Call initialize() first.");
127
+ }
128
+ // Resolve the issue to complete
129
+ const issue = issueNumber
130
+ ? this.activeIssues.get(issueNumber)
131
+ : this.currentIssue;
132
+ if (!issue) {
133
+ throw new Error(issueNumber
134
+ ? `No active issue #${issueNumber} to complete.`
135
+ : "No current issue to complete.");
117
136
  }
118
137
  // Calculate total duration from phases
119
- const totalDurationSeconds = this.currentIssue.phases?.reduce((sum, p) => sum + p.durationSeconds, 0) ?? 0;
138
+ const totalDurationSeconds = issue.phases?.reduce((sum, p) => sum + p.durationSeconds, 0) ?? 0;
120
139
  const issueLog = {
121
- issueNumber: this.currentIssue.issueNumber,
122
- title: this.currentIssue.title,
123
- labels: this.currentIssue.labels,
124
- status: this.currentIssue.status,
125
- phases: this.currentIssue.phases,
140
+ issueNumber: issue.issueNumber,
141
+ title: issue.title,
142
+ labels: issue.labels,
143
+ status: issue.status,
144
+ phases: issue.phases,
126
145
  totalDurationSeconds,
127
- ...(this.currentIssue.prNumber != null && {
128
- prNumber: this.currentIssue.prNumber,
146
+ ...(issue.prNumber != null && {
147
+ prNumber: issue.prNumber,
129
148
  }),
130
- ...(this.currentIssue.prUrl != null && {
131
- prUrl: this.currentIssue.prUrl,
149
+ ...(issue.prUrl != null && {
150
+ prUrl: issue.prUrl,
132
151
  }),
133
152
  };
134
153
  this.runLog.issues.push(issueLog);
135
- this.currentIssue = null;
154
+ // Clean up from activeIssues map
155
+ if (issue.issueNumber != null) {
156
+ this.activeIssues.delete(issue.issueNumber);
157
+ }
158
+ // Clear currentIssue if it was the one completed
159
+ if (this.currentIssue === issue) {
160
+ this.currentIssue = null;
161
+ }
136
162
  if (this.verbose) {
137
163
  console.log(`📝 Completed issue #${issueLog.issueNumber} (${issueLog.status})`);
138
164
  }
@@ -150,7 +176,11 @@ export class LogWriter {
150
176
  if (!this.runLog) {
151
177
  throw new Error("LogWriter not initialized.");
152
178
  }
153
- // Complete any pending issue
179
+ // Complete any pending issues (Map-based concurrent tracking)
180
+ for (const issueNum of [...this.activeIssues.keys()]) {
181
+ this.completeIssue(issueNum);
182
+ }
183
+ // Fallback: complete legacy currentIssue if not already handled
154
184
  if (this.currentIssue) {
155
185
  this.completeIssue();
156
186
  }