sequant 2.1.1 → 2.2.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bin/cli.js +1 -0
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +122 -3
- package/dist/src/commands/run-compat.d.ts +14 -0
- package/dist/src/commands/run-compat.js +12 -0
- package/dist/src/commands/run-display.d.ts +17 -0
- package/dist/src/commands/run-display.js +116 -0
- package/dist/src/commands/run.d.ts +4 -26
- package/dist/src/commands/run.js +47 -772
- package/dist/src/commands/status.js +24 -1
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.js +9 -0
- package/dist/src/lib/errors.d.ts +93 -0
- package/dist/src/lib/errors.js +97 -0
- package/dist/src/lib/settings.d.ts +236 -0
- package/dist/src/lib/settings.js +482 -37
- package/dist/src/lib/skill-version.d.ts +19 -0
- package/dist/src/lib/skill-version.js +68 -0
- package/dist/src/lib/templates.d.ts +1 -0
- package/dist/src/lib/templates.js +1 -1
- package/dist/src/lib/workflow/batch-executor.js +13 -5
- package/dist/src/lib/workflow/config-resolver.d.ts +50 -0
- package/dist/src/lib/workflow/config-resolver.js +167 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +17 -7
- package/dist/src/lib/workflow/error-classifier.js +113 -15
- package/dist/src/lib/workflow/phase-executor.d.ts +31 -0
- package/dist/src/lib/workflow/phase-executor.js +143 -48
- package/dist/src/lib/workflow/run-log-schema.d.ts +12 -0
- package/dist/src/lib/workflow/run-log-schema.js +7 -1
- package/dist/src/lib/workflow/run-orchestrator.d.ts +161 -0
- package/dist/src/lib/workflow/run-orchestrator.js +510 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +4 -3
- package/dist/src/lib/workflow/worktree-manager.js +61 -11
- package/package.json +1 -1
- package/templates/skills/assess/SKILL.md +239 -77
- package/templates/skills/exec/SKILL.md +7 -68
- package/templates/skills/fullsolve/SKILL.md +303 -137
- package/templates/skills/qa/SKILL.md +42 -46
- package/templates/skills/qa/scripts/quality-checks.sh +47 -1
- package/templates/skills/spec/SKILL.md +183 -982
- package/templates/skills/spec/references/quality-checklist.md +75 -0
- package/templates/skills/test/SKILL.md +0 -27
- package/templates/skills/testgen/SKILL.md +0 -27
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import chalk from "chalk";
|
|
11
11
|
import { spawnSync } from "child_process";
|
|
12
12
|
import { createPhaseLogFromTiming } from "./log-writer.js";
|
|
13
|
-
import { classifyError } from "./error-classifier.js";
|
|
13
|
+
import { classifyError, errorTypeToCategory } from "./error-classifier.js";
|
|
14
14
|
import { PhaseSpinner } from "../phase-spinner.js";
|
|
15
15
|
import { getGitDiffStats, getCommitHash } from "./git-diff-utils.js";
|
|
16
16
|
import { createCheckpointCommit, rebaseBeforePR, createPR, readCacheMetrics, filterResumedPhases, } from "./worktree-manager.js";
|
|
@@ -399,11 +399,15 @@ export async function runIssueWithLogging(ctx) {
|
|
|
399
399
|
// Build errorContext from captured stderr/stdout tails (#447)
|
|
400
400
|
let specErrorContext;
|
|
401
401
|
if (!specResult.success && specResult.stderrTail) {
|
|
402
|
+
const specError = classifyError(specResult.stderrTail ?? [], specResult.exitCode);
|
|
402
403
|
specErrorContext = {
|
|
403
404
|
stderrTail: specResult.stderrTail ?? [],
|
|
404
405
|
stdoutTail: specResult.stdoutTail ?? [],
|
|
405
406
|
exitCode: specResult.exitCode,
|
|
406
|
-
category:
|
|
407
|
+
category: errorTypeToCategory(specError),
|
|
408
|
+
errorType: specError.name,
|
|
409
|
+
errorMetadata: specError.metadata,
|
|
410
|
+
isRetryable: specError.isRetryable,
|
|
407
411
|
};
|
|
408
412
|
}
|
|
409
413
|
const phaseLog = createPhaseLogFromTiming("spec", issueNumber, specStartTime, specEndTime, specResult.success
|
|
@@ -595,14 +599,18 @@ export async function runIssueWithLogging(ctx) {
|
|
|
595
599
|
: undefined;
|
|
596
600
|
// Read cache metrics for QA phase (AC-7)
|
|
597
601
|
const cacheMetrics = phase === "qa" ? readCacheMetrics(worktreePath) : undefined;
|
|
598
|
-
// Build errorContext from captured stderr/stdout tails (#447)
|
|
602
|
+
// Build errorContext from captured stderr/stdout tails (#447, AC-7/AC-8)
|
|
599
603
|
let errorContext;
|
|
600
604
|
if (!result.success && result.stderrTail) {
|
|
605
|
+
const typedError = classifyError(result.stderrTail ?? [], result.exitCode);
|
|
601
606
|
errorContext = {
|
|
602
607
|
stderrTail: result.stderrTail ?? [],
|
|
603
608
|
stdoutTail: result.stdoutTail ?? [],
|
|
604
609
|
exitCode: result.exitCode,
|
|
605
|
-
category:
|
|
610
|
+
category: errorTypeToCategory(typedError),
|
|
611
|
+
errorType: typedError.name,
|
|
612
|
+
errorMetadata: typedError.metadata,
|
|
613
|
+
isRetryable: typedError.isRetryable,
|
|
606
614
|
};
|
|
607
615
|
}
|
|
608
616
|
const phaseLog = createPhaseLogFromTiming(phase, issueNumber, phaseStartTime, phaseEndTime, result.success
|
|
@@ -739,7 +747,7 @@ export async function runIssueWithLogging(ctx) {
|
|
|
739
747
|
}
|
|
740
748
|
// Create checkpoint commit in chain mode after QA passes
|
|
741
749
|
if (success && chainMode && worktreePath) {
|
|
742
|
-
createCheckpointCommit(worktreePath, issueNumber, config.verbose);
|
|
750
|
+
createCheckpointCommit(worktreePath, issueNumber, config.verbose, baseBranch);
|
|
743
751
|
}
|
|
744
752
|
// Rebase onto the base branch before PR creation (unless --no-rebase)
|
|
745
753
|
// This ensures the branch is up-to-date and prevents lockfile drift
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigResolver — 4-layer configuration merge for sequant run.
|
|
3
|
+
*
|
|
4
|
+
* Priority: defaults < settings < env < explicit (CLI flags).
|
|
5
|
+
* Handles Commander.js --no-X boolean negation at the CLI boundary.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { type ExecutionConfig, type RunOptions } from "./types.js";
|
|
10
|
+
import type { SequantSettings } from "../settings.js";
|
|
11
|
+
/**
|
|
12
|
+
* Layers for config resolution.
|
|
13
|
+
* Each field is optional — only defined values participate in merging.
|
|
14
|
+
*/
|
|
15
|
+
export interface ConfigLayers {
|
|
16
|
+
defaults: Record<string, unknown>;
|
|
17
|
+
settings: Record<string, unknown>;
|
|
18
|
+
env: Record<string, unknown>;
|
|
19
|
+
explicit: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generic 4-layer priority merge.
|
|
23
|
+
* For each key across all layers: explicit > env > settings > defaults.
|
|
24
|
+
* Env strings are coerced to match the type of the default value.
|
|
25
|
+
*/
|
|
26
|
+
export declare class ConfigResolver {
|
|
27
|
+
private layers;
|
|
28
|
+
constructor(layers: ConfigLayers);
|
|
29
|
+
/**
|
|
30
|
+
* Resolve all layers into a single merged config object.
|
|
31
|
+
* Priority: explicit > env > settings > defaults.
|
|
32
|
+
*/
|
|
33
|
+
resolve(): Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Normalize Commander.js --no-X flags into RunOptions negation fields.
|
|
37
|
+
* This is a thin adapter at the CLI boundary — not used by programmatic callers.
|
|
38
|
+
*/
|
|
39
|
+
export declare function normalizeCommanderOptions(options: RunOptions): RunOptions;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve RunOptions + settings + env into a fully merged RunOptions.
|
|
42
|
+
* This replaces the inline merging logic previously in run.ts.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveRunOptions(cliOptions: RunOptions, settings: SequantSettings): RunOptions;
|
|
45
|
+
/**
|
|
46
|
+
* Build an ExecutionConfig from merged RunOptions and settings.
|
|
47
|
+
* Extracts the phase-timeout, MCP, retry, and mode resolution logic
|
|
48
|
+
* that was previously inline in run.ts.
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildExecutionConfig(mergedOptions: RunOptions, settings: SequantSettings, issueCount: number): ExecutionConfig;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigResolver — 4-layer configuration merge for sequant run.
|
|
3
|
+
*
|
|
4
|
+
* Priority: defaults < settings < env < explicit (CLI flags).
|
|
5
|
+
* Handles Commander.js --no-X boolean negation at the CLI boundary.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { DEFAULT_CONFIG, DEFAULT_PHASES, } from "./types.js";
|
|
10
|
+
import { getEnvConfig } from "./batch-executor.js";
|
|
11
|
+
/**
|
|
12
|
+
* Coerce an env-var string to the type of the default value.
|
|
13
|
+
* Returns the string as-is if no default exists for type inference.
|
|
14
|
+
*/
|
|
15
|
+
function coerceEnvValue(value, defaultValue) {
|
|
16
|
+
if (typeof value !== "string")
|
|
17
|
+
return value;
|
|
18
|
+
if (typeof defaultValue === "number") {
|
|
19
|
+
const n = Number(value);
|
|
20
|
+
return isNaN(n) ? value : n;
|
|
21
|
+
}
|
|
22
|
+
if (typeof defaultValue === "boolean") {
|
|
23
|
+
return value === "true";
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generic 4-layer priority merge.
|
|
29
|
+
* For each key across all layers: explicit > env > settings > defaults.
|
|
30
|
+
* Env strings are coerced to match the type of the default value.
|
|
31
|
+
*/
|
|
32
|
+
export class ConfigResolver {
|
|
33
|
+
layers;
|
|
34
|
+
constructor(layers) {
|
|
35
|
+
this.layers = layers;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve all layers into a single merged config object.
|
|
39
|
+
* Priority: explicit > env > settings > defaults.
|
|
40
|
+
*/
|
|
41
|
+
resolve() {
|
|
42
|
+
const { defaults, settings, env, explicit } = this.layers;
|
|
43
|
+
// Collect all keys across layers
|
|
44
|
+
const allKeys = new Set([
|
|
45
|
+
...Object.keys(defaults),
|
|
46
|
+
...Object.keys(settings),
|
|
47
|
+
...Object.keys(env),
|
|
48
|
+
...Object.keys(explicit),
|
|
49
|
+
]);
|
|
50
|
+
const result = {};
|
|
51
|
+
for (const key of allKeys) {
|
|
52
|
+
// Check each layer in reverse priority (lowest first)
|
|
53
|
+
const layers = [
|
|
54
|
+
{ value: defaults[key] },
|
|
55
|
+
{ value: settings[key] },
|
|
56
|
+
{ value: env[key] },
|
|
57
|
+
{ value: explicit[key] },
|
|
58
|
+
];
|
|
59
|
+
// Walk from highest to lowest priority, take first defined value
|
|
60
|
+
let resolved = undefined;
|
|
61
|
+
const defaultVal = defaults[key];
|
|
62
|
+
for (const layer of layers) {
|
|
63
|
+
if (layer.value !== undefined) {
|
|
64
|
+
resolved = layer.value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Coerce env values if the winning value came from env layer
|
|
68
|
+
if (resolved !== undefined &&
|
|
69
|
+
explicit[key] === undefined &&
|
|
70
|
+
env[key] !== undefined &&
|
|
71
|
+
settings[key] === undefined) {
|
|
72
|
+
// Only env contributed — coerce
|
|
73
|
+
resolved = coerceEnvValue(resolved, defaultVal);
|
|
74
|
+
}
|
|
75
|
+
else if (resolved !== undefined &&
|
|
76
|
+
explicit[key] === undefined &&
|
|
77
|
+
env[key] !== undefined) {
|
|
78
|
+
// env is present and wins over settings — coerce the env value
|
|
79
|
+
resolved = coerceEnvValue(env[key], defaultVal);
|
|
80
|
+
}
|
|
81
|
+
result[key] = resolved;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Normalize Commander.js --no-X flags into RunOptions negation fields.
|
|
88
|
+
* This is a thin adapter at the CLI boundary — not used by programmatic callers.
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeCommanderOptions(options) {
|
|
91
|
+
const raw = options;
|
|
92
|
+
return {
|
|
93
|
+
...options,
|
|
94
|
+
...(raw.log === false && { noLog: true }),
|
|
95
|
+
...(raw.smartTests === false && { noSmartTests: true }),
|
|
96
|
+
...(raw.mcp === false && { noMcp: true }),
|
|
97
|
+
...(raw.retry === false && { noRetry: true }),
|
|
98
|
+
...(raw.rebase === false && { noRebase: true }),
|
|
99
|
+
...(raw.pr === false && { noPr: true }),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolve RunOptions + settings + env into a fully merged RunOptions.
|
|
104
|
+
* This replaces the inline merging logic previously in run.ts.
|
|
105
|
+
*/
|
|
106
|
+
export function resolveRunOptions(cliOptions, settings) {
|
|
107
|
+
const normalized = normalizeCommanderOptions(cliOptions);
|
|
108
|
+
const envConfig = getEnvConfig();
|
|
109
|
+
// Strip undefined keys so programmatic callers don't clobber env/settings values
|
|
110
|
+
const defined = Object.fromEntries(Object.entries(normalized).filter(([, v]) => v !== undefined));
|
|
111
|
+
const merged = {
|
|
112
|
+
// Settings defaults
|
|
113
|
+
sequential: defined.sequential ?? settings.run.sequential,
|
|
114
|
+
concurrency: defined.concurrency ?? settings.run.concurrency,
|
|
115
|
+
timeout: defined.timeout ?? settings.run.timeout,
|
|
116
|
+
logPath: defined.logPath ?? settings.run.logPath,
|
|
117
|
+
qualityLoop: defined.qualityLoop ?? settings.run.qualityLoop,
|
|
118
|
+
maxIterations: defined.maxIterations ?? settings.run.maxIterations,
|
|
119
|
+
noSmartTests: defined.noSmartTests ?? !settings.run.smartTests,
|
|
120
|
+
// Agent settings
|
|
121
|
+
isolateParallel: defined.isolateParallel ?? settings.agents.isolateParallel,
|
|
122
|
+
// Env overrides
|
|
123
|
+
...envConfig,
|
|
124
|
+
// CLI explicit options override all
|
|
125
|
+
...defined,
|
|
126
|
+
};
|
|
127
|
+
// Auto-detect phases from labels unless --phases explicitly set
|
|
128
|
+
const autoDetectPhases = !cliOptions.phases && settings.run.autoDetectPhases;
|
|
129
|
+
merged.autoDetectPhases = autoDetectPhases;
|
|
130
|
+
return merged;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Build an ExecutionConfig from merged RunOptions and settings.
|
|
134
|
+
* Extracts the phase-timeout, MCP, retry, and mode resolution logic
|
|
135
|
+
* that was previously inline in run.ts.
|
|
136
|
+
*/
|
|
137
|
+
export function buildExecutionConfig(mergedOptions, settings, issueCount) {
|
|
138
|
+
const explicitPhases = mergedOptions.phases
|
|
139
|
+
? mergedOptions.phases.split(",").map((p) => p.trim())
|
|
140
|
+
: null;
|
|
141
|
+
const mcpEnabled = mergedOptions.noMcp
|
|
142
|
+
? false
|
|
143
|
+
: (settings.run.mcp ?? DEFAULT_CONFIG.mcp);
|
|
144
|
+
const retryEnabled = mergedOptions.noRetry
|
|
145
|
+
? false
|
|
146
|
+
: (settings.run.retry ?? true);
|
|
147
|
+
const isSequential = mergedOptions.sequential ?? false;
|
|
148
|
+
const isParallel = !isSequential && issueCount > 1;
|
|
149
|
+
return {
|
|
150
|
+
...DEFAULT_CONFIG,
|
|
151
|
+
phases: explicitPhases ?? DEFAULT_PHASES,
|
|
152
|
+
sequential: isSequential,
|
|
153
|
+
concurrency: mergedOptions.concurrency ?? DEFAULT_CONFIG.concurrency,
|
|
154
|
+
parallel: isParallel,
|
|
155
|
+
dryRun: mergedOptions.dryRun ?? false,
|
|
156
|
+
verbose: mergedOptions.verbose ?? false,
|
|
157
|
+
phaseTimeout: mergedOptions.timeout ?? DEFAULT_CONFIG.phaseTimeout,
|
|
158
|
+
qualityLoop: mergedOptions.qualityLoop ?? false,
|
|
159
|
+
maxIterations: mergedOptions.maxIterations ?? DEFAULT_CONFIG.maxIterations,
|
|
160
|
+
noSmartTests: mergedOptions.noSmartTests ?? false,
|
|
161
|
+
mcp: mcpEnabled,
|
|
162
|
+
retry: retryEnabled,
|
|
163
|
+
agent: mergedOptions.agent ?? settings.run.agent,
|
|
164
|
+
aiderSettings: settings.run.aider,
|
|
165
|
+
isolateParallel: mergedOptions.isolateParallel,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Error classifier — categorizes phase failures from stderr content.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Refactored (AC-7): Returns typed SequantError instances instead of string
|
|
5
|
+
* categories. Exit codes are the primary signal; stderr patterns are secondary.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
import { SequantError } from "../errors.js";
|
|
8
|
+
/** All recognized error categories (kept for backwards compatibility). */
|
|
8
9
|
export declare const ERROR_CATEGORIES: readonly ["context_overflow", "api_error", "hook_failure", "build_error", "timeout", "unknown"];
|
|
9
10
|
export type ErrorCategory = (typeof ERROR_CATEGORIES)[number];
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
+
* Map from error type name to legacy category string.
|
|
13
|
+
* Used for backwards-compatible log storage (AC-8).
|
|
14
|
+
*/
|
|
15
|
+
export declare function errorTypeToCategory(error: SequantError): ErrorCategory;
|
|
16
|
+
/**
|
|
17
|
+
* Classify stderr lines into a typed SequantError instance (AC-7).
|
|
18
|
+
*
|
|
19
|
+
* Exit codes are the primary signal; stderr patterns are secondary.
|
|
20
|
+
* Returns a typed error instance with structured metadata.
|
|
12
21
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
22
|
+
* @param stderrLines - Lines from stderr
|
|
23
|
+
* @param exitCode - Process exit code (primary signal)
|
|
24
|
+
* @returns Typed SequantError subclass instance
|
|
15
25
|
*/
|
|
16
|
-
export declare function classifyError(stderrLines: string[]):
|
|
26
|
+
export declare function classifyError(stderrLines: string[], exitCode?: number): SequantError;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Error classifier — categorizes phase failures from stderr content.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Refactored (AC-7): Returns typed SequantError instances instead of string
|
|
5
|
+
* categories. Exit codes are the primary signal; stderr patterns are secondary.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
import { ContextOverflowError, ApiError, HookFailureError, BuildError, TimeoutError, SubprocessError, } from "../errors.js";
|
|
8
|
+
/** All recognized error categories (kept for backwards compatibility). */
|
|
8
9
|
export const ERROR_CATEGORIES = [
|
|
9
10
|
"context_overflow",
|
|
10
11
|
"api_error",
|
|
@@ -13,6 +14,26 @@ export const ERROR_CATEGORIES = [
|
|
|
13
14
|
"timeout",
|
|
14
15
|
"unknown",
|
|
15
16
|
];
|
|
17
|
+
/**
|
|
18
|
+
* Map from error type name to legacy category string.
|
|
19
|
+
* Used for backwards-compatible log storage (AC-8).
|
|
20
|
+
*/
|
|
21
|
+
export function errorTypeToCategory(error) {
|
|
22
|
+
switch (error.name) {
|
|
23
|
+
case "ContextOverflowError":
|
|
24
|
+
return "context_overflow";
|
|
25
|
+
case "ApiError":
|
|
26
|
+
return "api_error";
|
|
27
|
+
case "HookFailureError":
|
|
28
|
+
return "hook_failure";
|
|
29
|
+
case "BuildError":
|
|
30
|
+
return "build_error";
|
|
31
|
+
case "TimeoutError":
|
|
32
|
+
return "timeout";
|
|
33
|
+
default:
|
|
34
|
+
return "unknown";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
16
37
|
/**
|
|
17
38
|
* Ordered list of classifiers. First match wins (highest priority first).
|
|
18
39
|
*/
|
|
@@ -30,6 +51,16 @@ const CLASSIFIERS = [
|
|
|
30
51
|
{
|
|
31
52
|
category: "timeout",
|
|
32
53
|
patterns: [/timeout/i, /timed?\s*out/i, /SIGTERM/, /deadline exceeded/i],
|
|
54
|
+
extract: (line) => {
|
|
55
|
+
const match = line.match(/(\d+)\s*(?:s|ms|seconds?|milliseconds?)/i);
|
|
56
|
+
if (match) {
|
|
57
|
+
const value = parseInt(match[1], 10);
|
|
58
|
+
// If the unit looks like seconds (or no unit after number), convert to ms
|
|
59
|
+
const isMs = /ms|milliseconds?/i.test(match[0]);
|
|
60
|
+
return { timeoutMs: isMs ? value : value * 1000 };
|
|
61
|
+
}
|
|
62
|
+
return {};
|
|
63
|
+
},
|
|
33
64
|
},
|
|
34
65
|
{
|
|
35
66
|
category: "api_error",
|
|
@@ -43,6 +74,10 @@ const CLASSIFIERS = [
|
|
|
43
74
|
/\b502\b/,
|
|
44
75
|
/overloaded/i,
|
|
45
76
|
],
|
|
77
|
+
extract: (line) => {
|
|
78
|
+
const statusMatch = line.match(/\b(429|502|503|401|403)\b/);
|
|
79
|
+
return statusMatch ? { statusCode: parseInt(statusMatch[1], 10) } : {};
|
|
80
|
+
},
|
|
46
81
|
},
|
|
47
82
|
{
|
|
48
83
|
category: "hook_failure",
|
|
@@ -52,6 +87,10 @@ const CLASSIFIERS = [
|
|
|
52
87
|
/HOOK_BLOCKED/i,
|
|
53
88
|
/blocked by hook/i,
|
|
54
89
|
],
|
|
90
|
+
extract: (line) => {
|
|
91
|
+
const hookMatch = line.match(/(?:hook|pre-?commit|HOOK_BLOCKED)[:\s]*(.{0,50})/i);
|
|
92
|
+
return hookMatch ? { hook: hookMatch[1]?.trim() || "unknown" } : {};
|
|
93
|
+
},
|
|
55
94
|
},
|
|
56
95
|
{
|
|
57
96
|
category: "build_error",
|
|
@@ -65,26 +104,85 @@ const CLASSIFIERS = [
|
|
|
65
104
|
/eslint/i,
|
|
66
105
|
/npm ERR!/,
|
|
67
106
|
],
|
|
107
|
+
extract: (line) => {
|
|
108
|
+
if (/TS\d{4,5}:/.test(line)) {
|
|
109
|
+
const codeMatch = line.match(/(TS\d{4,5}):/);
|
|
110
|
+
return { toolchain: "tsc", errorCode: codeMatch?.[1] };
|
|
111
|
+
}
|
|
112
|
+
if (/eslint/i.test(line))
|
|
113
|
+
return { toolchain: "eslint" };
|
|
114
|
+
if (/npm ERR!/.test(line))
|
|
115
|
+
return { toolchain: "npm" };
|
|
116
|
+
return { toolchain: "unknown" };
|
|
117
|
+
},
|
|
68
118
|
},
|
|
69
119
|
];
|
|
70
120
|
/**
|
|
71
|
-
* Classify stderr lines into
|
|
121
|
+
* Classify stderr lines into a typed SequantError instance (AC-7).
|
|
72
122
|
*
|
|
73
|
-
*
|
|
74
|
-
* Returns
|
|
123
|
+
* Exit codes are the primary signal; stderr patterns are secondary.
|
|
124
|
+
* Returns a typed error instance with structured metadata.
|
|
125
|
+
*
|
|
126
|
+
* @param stderrLines - Lines from stderr
|
|
127
|
+
* @param exitCode - Process exit code (primary signal)
|
|
128
|
+
* @returns Typed SequantError subclass instance
|
|
75
129
|
*/
|
|
76
|
-
export function classifyError(stderrLines) {
|
|
77
|
-
|
|
78
|
-
|
|
130
|
+
export function classifyError(stderrLines, exitCode) {
|
|
131
|
+
const combinedStderr = stderrLines?.join(" ") ?? "";
|
|
132
|
+
// Primary signal: exit code (AC-7)
|
|
133
|
+
if (exitCode !== undefined) {
|
|
134
|
+
// 143 = SIGTERM, often timeout
|
|
135
|
+
if (exitCode === 143 || exitCode === 137) {
|
|
136
|
+
return new TimeoutError(`Process killed with signal (exit code ${exitCode})`, { timeoutMs: undefined, phase: undefined });
|
|
137
|
+
}
|
|
79
138
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
139
|
+
// Secondary signal: stderr pattern matching
|
|
140
|
+
if (stderrLines && stderrLines.length > 0) {
|
|
141
|
+
for (const { category, patterns, extract } of CLASSIFIERS) {
|
|
142
|
+
for (const line of stderrLines) {
|
|
143
|
+
for (const pattern of patterns) {
|
|
144
|
+
if (pattern.test(line)) {
|
|
145
|
+
const metadata = extract?.(line) ?? {};
|
|
146
|
+
return createErrorForCategory(category, line, metadata, exitCode);
|
|
147
|
+
}
|
|
85
148
|
}
|
|
86
149
|
}
|
|
87
150
|
}
|
|
88
151
|
}
|
|
89
|
-
|
|
152
|
+
// Fallback: SubprocessError with exit code
|
|
153
|
+
return new SubprocessError(combinedStderr || "Unknown error", {
|
|
154
|
+
exitCode,
|
|
155
|
+
command: undefined,
|
|
156
|
+
stderr: combinedStderr || undefined,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Create a typed error instance from a legacy category string.
|
|
161
|
+
*/
|
|
162
|
+
function createErrorForCategory(category, message, metadata, exitCode) {
|
|
163
|
+
switch (category) {
|
|
164
|
+
case "context_overflow":
|
|
165
|
+
return new ContextOverflowError(message, metadata);
|
|
166
|
+
case "api_error":
|
|
167
|
+
return new ApiError(message, {
|
|
168
|
+
statusCode: metadata.statusCode,
|
|
169
|
+
endpoint: metadata.endpoint,
|
|
170
|
+
});
|
|
171
|
+
case "hook_failure":
|
|
172
|
+
return new HookFailureError(message, {
|
|
173
|
+
hook: metadata.hook,
|
|
174
|
+
reason: metadata.reason,
|
|
175
|
+
});
|
|
176
|
+
case "build_error":
|
|
177
|
+
return new BuildError(message, {
|
|
178
|
+
toolchain: metadata.toolchain,
|
|
179
|
+
errorCode: metadata.errorCode,
|
|
180
|
+
});
|
|
181
|
+
case "timeout":
|
|
182
|
+
return new TimeoutError(message, {
|
|
183
|
+
timeoutMs: metadata.timeoutMs,
|
|
184
|
+
});
|
|
185
|
+
default:
|
|
186
|
+
return new SubprocessError(message, { exitCode });
|
|
187
|
+
}
|
|
90
188
|
}
|
|
@@ -11,6 +11,7 @@ import { ShutdownManager } from "../shutdown.js";
|
|
|
11
11
|
import { PhaseSpinner } from "../phase-spinner.js";
|
|
12
12
|
import { Phase, ExecutionConfig, PhaseResult, QaVerdict } from "./types.js";
|
|
13
13
|
import type { QaSummary } from "./run-log-schema.js";
|
|
14
|
+
import type { AgentPhaseResult } from "./drivers/index.js";
|
|
14
15
|
/**
|
|
15
16
|
* Spec-specific retry configuration.
|
|
16
17
|
* Spec failures have a higher failure rate (~8.6%) than other phases due to
|
|
@@ -40,6 +41,36 @@ export declare function parseQaSummary(output: string): QaSummary | null;
|
|
|
40
41
|
* Format duration in human-readable format
|
|
41
42
|
*/
|
|
42
43
|
export declare function formatDuration(seconds: number): string;
|
|
44
|
+
/**
|
|
45
|
+
* Check whether the exec phase produced any changes in the worktree.
|
|
46
|
+
* Returns true if HEAD has commits unique to it relative to origin/main
|
|
47
|
+
* OR uncommitted work is present.
|
|
48
|
+
*
|
|
49
|
+
* Uses `git rev-list --count origin/main..HEAD` (commits reachable from HEAD
|
|
50
|
+
* but not origin/main) instead of `git diff origin/main..HEAD`, because the
|
|
51
|
+
* two-dot diff also fires in reverse when origin/main has advanced past HEAD
|
|
52
|
+
* — on stale branches that would falsely report "has commits" even when the
|
|
53
|
+
* exec phase produced nothing, reintroducing the bug #534 is fixing.
|
|
54
|
+
*
|
|
55
|
+
* Fails open (returns true) on git errors — a missing origin ref is better
|
|
56
|
+
* diagnosed as a real zero-diff run than as a false phase failure.
|
|
57
|
+
*
|
|
58
|
+
* @internal Exported for testing only.
|
|
59
|
+
*/
|
|
60
|
+
export declare function hasExecChanges(cwd: string): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Map a successful AgentPhaseResult to a PhaseResult, applying phase-specific
|
|
63
|
+
* guards that catch agent sessions which returned success without producing
|
|
64
|
+
* usable work (#534):
|
|
65
|
+
*
|
|
66
|
+
* - `qa`: fails when no parseable verdict is found (empty or malformed output).
|
|
67
|
+
* - `exec`: fails when no commits and no uncommitted changes exist.
|
|
68
|
+
*
|
|
69
|
+
* @internal Exported for testing only.
|
|
70
|
+
*/
|
|
71
|
+
export declare function mapAgentSuccessToPhaseResult(phase: Phase, agentResult: AgentPhaseResult, durationSeconds: number, cwd: string): PhaseResult & {
|
|
72
|
+
sessionId?: string;
|
|
73
|
+
};
|
|
43
74
|
/**
|
|
44
75
|
* Get the prompt for a phase with the issue number substituted.
|
|
45
76
|
* Selects self-contained prompts for non-Claude agents.
|