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.
Files changed (45) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/bin/cli.js +1 -0
  4. package/dist/src/commands/init.d.ts +1 -0
  5. package/dist/src/commands/init.js +122 -3
  6. package/dist/src/commands/run-compat.d.ts +14 -0
  7. package/dist/src/commands/run-compat.js +12 -0
  8. package/dist/src/commands/run-display.d.ts +17 -0
  9. package/dist/src/commands/run-display.js +116 -0
  10. package/dist/src/commands/run.d.ts +4 -26
  11. package/dist/src/commands/run.js +47 -772
  12. package/dist/src/commands/status.js +24 -1
  13. package/dist/src/index.d.ts +11 -0
  14. package/dist/src/index.js +9 -0
  15. package/dist/src/lib/errors.d.ts +93 -0
  16. package/dist/src/lib/errors.js +97 -0
  17. package/dist/src/lib/settings.d.ts +236 -0
  18. package/dist/src/lib/settings.js +482 -37
  19. package/dist/src/lib/skill-version.d.ts +19 -0
  20. package/dist/src/lib/skill-version.js +68 -0
  21. package/dist/src/lib/templates.d.ts +1 -0
  22. package/dist/src/lib/templates.js +1 -1
  23. package/dist/src/lib/workflow/batch-executor.js +13 -5
  24. package/dist/src/lib/workflow/config-resolver.d.ts +50 -0
  25. package/dist/src/lib/workflow/config-resolver.js +167 -0
  26. package/dist/src/lib/workflow/error-classifier.d.ts +17 -7
  27. package/dist/src/lib/workflow/error-classifier.js +113 -15
  28. package/dist/src/lib/workflow/phase-executor.d.ts +31 -0
  29. package/dist/src/lib/workflow/phase-executor.js +143 -48
  30. package/dist/src/lib/workflow/run-log-schema.d.ts +12 -0
  31. package/dist/src/lib/workflow/run-log-schema.js +7 -1
  32. package/dist/src/lib/workflow/run-orchestrator.d.ts +161 -0
  33. package/dist/src/lib/workflow/run-orchestrator.js +510 -0
  34. package/dist/src/lib/workflow/worktree-manager.d.ts +4 -3
  35. package/dist/src/lib/workflow/worktree-manager.js +61 -11
  36. package/package.json +1 -1
  37. package/templates/skills/assess/SKILL.md +239 -77
  38. package/templates/skills/exec/SKILL.md +7 -68
  39. package/templates/skills/fullsolve/SKILL.md +303 -137
  40. package/templates/skills/qa/SKILL.md +42 -46
  41. package/templates/skills/qa/scripts/quality-checks.sh +47 -1
  42. package/templates/skills/spec/SKILL.md +183 -982
  43. package/templates/skills/spec/references/quality-checklist.md +75 -0
  44. package/templates/skills/test/SKILL.md +0 -27
  45. package/templates/skills/testgen/SKILL.md +0 -27
@@ -11,6 +11,8 @@ import chalk from "chalk";
11
11
  import { execSync } from "child_process";
12
12
  import { readAgentsMd } from "../agents-md.js";
13
13
  import { getDriver } from "./drivers/index.js";
14
+ import { classifyError } from "./error-classifier.js";
15
+ import { ApiError } from "../errors.js";
14
16
  /**
15
17
  * Natural language prompts for each phase.
16
18
  * Claude Code invokes the corresponding skills via natural language.
@@ -216,6 +218,131 @@ export function formatDuration(seconds) {
216
218
  const secs = seconds % 60;
217
219
  return `${mins}m ${secs.toFixed(0)}s`;
218
220
  }
221
+ /**
222
+ * Check whether the exec phase produced any changes in the worktree.
223
+ * Returns true if HEAD has commits unique to it relative to origin/main
224
+ * OR uncommitted work is present.
225
+ *
226
+ * Uses `git rev-list --count origin/main..HEAD` (commits reachable from HEAD
227
+ * but not origin/main) instead of `git diff origin/main..HEAD`, because the
228
+ * two-dot diff also fires in reverse when origin/main has advanced past HEAD
229
+ * — on stale branches that would falsely report "has commits" even when the
230
+ * exec phase produced nothing, reintroducing the bug #534 is fixing.
231
+ *
232
+ * Fails open (returns true) on git errors — a missing origin ref is better
233
+ * diagnosed as a real zero-diff run than as a false phase failure.
234
+ *
235
+ * @internal Exported for testing only.
236
+ */
237
+ export function hasExecChanges(cwd) {
238
+ let commitsAhead;
239
+ try {
240
+ const count = execSync("git rev-list --count origin/main..HEAD", {
241
+ cwd,
242
+ stdio: "pipe",
243
+ })
244
+ .toString()
245
+ .trim();
246
+ commitsAhead = Number.parseInt(count, 10) > 0;
247
+ }
248
+ catch {
249
+ return true;
250
+ }
251
+ if (commitsAhead)
252
+ return true;
253
+ try {
254
+ const porcelain = execSync("git status --porcelain", { cwd, stdio: "pipe" })
255
+ .toString()
256
+ .trim();
257
+ return porcelain.length > 0;
258
+ }
259
+ catch {
260
+ return true;
261
+ }
262
+ }
263
+ /**
264
+ * Map a successful AgentPhaseResult to a PhaseResult, applying phase-specific
265
+ * guards that catch agent sessions which returned success without producing
266
+ * usable work (#534):
267
+ *
268
+ * - `qa`: fails when no parseable verdict is found (empty or malformed output).
269
+ * - `exec`: fails when no commits and no uncommitted changes exist.
270
+ *
271
+ * @internal Exported for testing only.
272
+ */
273
+ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds, cwd) {
274
+ const tails = {
275
+ stderrTail: agentResult.stderrTail,
276
+ stdoutTail: agentResult.stdoutTail,
277
+ exitCode: agentResult.exitCode,
278
+ };
279
+ if (phase === "qa") {
280
+ const verdict = agentResult.output
281
+ ? parseQaVerdict(agentResult.output)
282
+ : null;
283
+ const summary = agentResult.output
284
+ ? (parseQaSummary(agentResult.output) ?? undefined)
285
+ : undefined;
286
+ if (verdict &&
287
+ verdict !== "READY_FOR_MERGE" &&
288
+ verdict !== "NEEDS_VERIFICATION") {
289
+ return {
290
+ phase,
291
+ success: false,
292
+ durationSeconds,
293
+ error: `QA verdict: ${verdict}`,
294
+ sessionId: agentResult.sessionId,
295
+ output: agentResult.output,
296
+ verdict,
297
+ summary,
298
+ ...tails,
299
+ };
300
+ }
301
+ if (!verdict) {
302
+ // #534: a null verdict (empty or unparseable output) is not success.
303
+ return {
304
+ phase,
305
+ success: false,
306
+ durationSeconds,
307
+ error: "QA completed without a parseable verdict",
308
+ sessionId: agentResult.sessionId,
309
+ output: agentResult.output,
310
+ summary,
311
+ ...tails,
312
+ };
313
+ }
314
+ return {
315
+ phase,
316
+ success: true,
317
+ durationSeconds,
318
+ sessionId: agentResult.sessionId,
319
+ output: agentResult.output,
320
+ verdict,
321
+ summary,
322
+ ...tails,
323
+ };
324
+ }
325
+ if (phase === "exec" && !hasExecChanges(cwd)) {
326
+ // #534: an exec phase that produced nothing is not success.
327
+ return {
328
+ phase,
329
+ success: false,
330
+ durationSeconds,
331
+ error: "exec produced no changes (no commits, no uncommitted work)",
332
+ sessionId: agentResult.sessionId,
333
+ output: agentResult.output,
334
+ ...tails,
335
+ };
336
+ }
337
+ return {
338
+ phase,
339
+ success: true,
340
+ durationSeconds,
341
+ sessionId: agentResult.sessionId,
342
+ output: agentResult.output,
343
+ ...tails,
344
+ };
345
+ }
219
346
  /**
220
347
  * Get the prompt for a phase with the issue number substituted.
221
348
  * Selects self-contained prompts for non-Claude agents.
@@ -388,52 +515,8 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
388
515
  shutdownManager.removeAbortController(abortController);
389
516
  }
390
517
  const durationSeconds = (Date.now() - startTime) / 1000;
391
- // Map AgentPhaseResult to PhaseResult
392
- const tails = {
393
- stderrTail: agentResult.stderrTail,
394
- stdoutTail: agentResult.stdoutTail,
395
- exitCode: agentResult.exitCode,
396
- };
397
518
  if (agentResult.success) {
398
- // For QA phase, check the verdict to determine actual success
399
- // Agent "success" just means the execution completed — we need to parse the verdict
400
- if (phase === "qa" && agentResult.output) {
401
- const verdict = parseQaVerdict(agentResult.output);
402
- const summary = parseQaSummary(agentResult.output) ?? undefined;
403
- if (verdict &&
404
- verdict !== "READY_FOR_MERGE" &&
405
- verdict !== "NEEDS_VERIFICATION") {
406
- return {
407
- phase,
408
- success: false,
409
- durationSeconds,
410
- error: `QA verdict: ${verdict}`,
411
- sessionId: agentResult.sessionId,
412
- output: agentResult.output,
413
- verdict,
414
- summary,
415
- ...tails,
416
- };
417
- }
418
- return {
419
- phase,
420
- success: true,
421
- durationSeconds,
422
- sessionId: agentResult.sessionId,
423
- output: agentResult.output,
424
- verdict: verdict ?? undefined,
425
- summary,
426
- ...tails,
427
- };
428
- }
429
- return {
430
- phase,
431
- success: true,
432
- durationSeconds,
433
- sessionId: agentResult.sessionId,
434
- output: agentResult.output,
435
- ...tails,
436
- };
519
+ return mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds, cwd);
437
520
  }
438
521
  return {
439
522
  phase,
@@ -441,7 +524,9 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
441
524
  durationSeconds,
442
525
  error: agentResult.error,
443
526
  sessionId: agentResult.sessionId,
444
- ...tails,
527
+ stderrTail: agentResult.stderrTail,
528
+ stdoutTail: agentResult.stdoutTail,
529
+ exitCode: agentResult.exitCode,
445
530
  };
446
531
  }
447
532
  /**
@@ -490,9 +575,19 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
490
575
  return lastResult;
491
576
  }
492
577
  // Genuine failure (took long enough to be real work) → skip cold-start retries.
493
- // For spec phase, break to allow Phase 3 (spec-specific retry) to run.
494
- // For other phases, return immediately no further retries.
578
+ // Use error classification (AC-9): if the error is retryable (e.g., API
579
+ // rate limit, transient 503), allow one more attempt even for genuine failures.
495
580
  if (duration >= COLD_START_THRESHOLD_SECONDS) {
581
+ const typedError = classifyError(lastResult.stderrTail ?? [], lastResult.exitCode);
582
+ if (typedError.isRetryable && attempt < COLD_START_MAX_RETRIES) {
583
+ if (config.verbose) {
584
+ const label = typedError instanceof ApiError
585
+ ? `API error (status ${typedError.metadata.statusCode ?? "unknown"})`
586
+ : typedError.name;
587
+ console.log(chalk.yellow(`\n ⟳ Retryable error: ${label}, retrying... (attempt ${attempt + 2}/${COLD_START_MAX_RETRIES + 1})`));
588
+ }
589
+ continue;
590
+ }
496
591
  if (phase === "spec") {
497
592
  break;
498
593
  }
@@ -90,6 +90,9 @@ export declare const ErrorContextSchema: z.ZodObject<{
90
90
  hook_failure: "hook_failure";
91
91
  build_error: "build_error";
92
92
  }>;
93
+ errorType: z.ZodOptional<z.ZodString>;
94
+ errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
95
+ isRetryable: z.ZodOptional<z.ZodBoolean>;
93
96
  }, z.core.$strip>;
94
97
  export type ErrorContext = z.infer<typeof ErrorContextSchema>;
95
98
  /**
@@ -177,6 +180,9 @@ export declare const PhaseLogSchema: z.ZodObject<{
177
180
  hook_failure: "hook_failure";
178
181
  build_error: "build_error";
179
182
  }>;
183
+ errorType: z.ZodOptional<z.ZodString>;
184
+ errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
185
+ isRetryable: z.ZodOptional<z.ZodBoolean>;
180
186
  }, z.core.$strip>>;
181
187
  }, z.core.$strip>;
182
188
  export type PhaseLog = z.infer<typeof PhaseLogSchema>;
@@ -260,6 +266,9 @@ export declare const IssueLogSchema: z.ZodObject<{
260
266
  hook_failure: "hook_failure";
261
267
  build_error: "build_error";
262
268
  }>;
269
+ errorType: z.ZodOptional<z.ZodString>;
270
+ errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
271
+ isRetryable: z.ZodOptional<z.ZodBoolean>;
263
272
  }, z.core.$strip>>;
264
273
  }, z.core.$strip>>;
265
274
  totalDurationSeconds: z.ZodNumber;
@@ -404,6 +413,9 @@ export declare const RunLogSchema: z.ZodObject<{
404
413
  hook_failure: "hook_failure";
405
414
  build_error: "build_error";
406
415
  }>;
416
+ errorType: z.ZodOptional<z.ZodString>;
417
+ errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
418
+ isRetryable: z.ZodOptional<z.ZodBoolean>;
407
419
  }, z.core.$strip>>;
408
420
  }, z.core.$strip>>;
409
421
  totalDurationSeconds: z.ZodNumber;
@@ -79,7 +79,7 @@ export const ErrorContextSchema = z.object({
79
79
  stdoutTail: z.array(z.string()),
80
80
  /** Process exit code */
81
81
  exitCode: z.number().int().optional(),
82
- /** Classified error category */
82
+ /** Classified error category (legacy, kept for backwards compatibility) */
83
83
  category: z.enum([
84
84
  "context_overflow",
85
85
  "api_error",
@@ -88,6 +88,12 @@ export const ErrorContextSchema = z.object({
88
88
  "timeout",
89
89
  "unknown",
90
90
  ]),
91
+ /** Typed error class name (AC-8), e.g. "ApiError", "BuildError" */
92
+ errorType: z.string().optional(),
93
+ /** Structured error metadata (AC-8) */
94
+ errorMetadata: z.record(z.string(), z.unknown()).optional(),
95
+ /** Whether this error type is retryable (AC-9) */
96
+ isRetryable: z.boolean().optional(),
91
97
  });
92
98
  /**
93
99
  * Condensed QA verdict summary for structured log output (#434).
@@ -0,0 +1,161 @@
1
+ /**
2
+ * RunOrchestrator — CLI-free execution engine for sequant workflows.
3
+ *
4
+ * Owns the full lifecycle: config → issue discovery → dispatch → results.
5
+ * Importable and usable without Commander.js or CLI context.
6
+ *
7
+ * @module
8
+ */
9
+ import type { ExecutionConfig, IssueResult, RunOptions, ProgressCallback } from "./types.js";
10
+ import type { WorktreeInfo } from "./worktree-manager.js";
11
+ import { LogWriter } from "./log-writer.js";
12
+ import { StateManager } from "./state-manager.js";
13
+ import { ShutdownManager } from "../shutdown.js";
14
+ import type { SequantSettings } from "../settings.js";
15
+ /**
16
+ * Injectable services for RunOrchestrator.
17
+ * All optional — orchestrator degrades gracefully when services are absent.
18
+ */
19
+ export interface OrchestratorServices {
20
+ logWriter?: LogWriter | null;
21
+ stateManager?: StateManager | null;
22
+ shutdownManager?: ShutdownManager;
23
+ }
24
+ /**
25
+ * CLI-free configuration for RunOrchestrator.
26
+ * No Commander.js types leak into this interface.
27
+ */
28
+ export interface OrchestratorConfig {
29
+ /** Execution settings (phases, timeouts, mode flags) */
30
+ config: ExecutionConfig;
31
+ /** Merged run options (post-resolution, no raw CLI types) */
32
+ options: RunOptions;
33
+ /** Issue metadata keyed by issue number */
34
+ issueInfoMap: Map<number, {
35
+ title: string;
36
+ labels: string[];
37
+ }>;
38
+ /** Worktree paths keyed by issue number */
39
+ worktreeMap: Map<number, WorktreeInfo>;
40
+ /** Injectable services */
41
+ services: OrchestratorServices;
42
+ /** Package manager name (e.g. "npm", "pnpm") */
43
+ packageManager?: string;
44
+ /** Base branch for rebase/PR targets */
45
+ baseBranch?: string;
46
+ /** Per-phase progress callback (parallel mode) */
47
+ onProgress?: ProgressCallback;
48
+ }
49
+ /**
50
+ * High-level init config for full lifecycle execution.
51
+ * Used by RunOrchestrator.run() — the entry point for programmatic callers.
52
+ */
53
+ export interface RunInit {
54
+ /** Raw CLI options (pre-merge) */
55
+ options: RunOptions;
56
+ /** Resolved settings */
57
+ settings: SequantSettings;
58
+ /** Manifest metadata */
59
+ manifest: {
60
+ stack: string;
61
+ packageManager: string;
62
+ };
63
+ /** Explicit base branch override */
64
+ baseBranch?: string;
65
+ /** Per-phase progress callback */
66
+ onProgress?: ProgressCallback;
67
+ }
68
+ /**
69
+ * Pure result of config resolution — no side effects, no services.
70
+ * Produced by `RunOrchestrator.resolveConfig()` and consumed by both
71
+ * `run()` (internally) and the CLI (for pre-run display).
72
+ */
73
+ export interface ResolvedRun {
74
+ /** Post-merge run options (defaults < settings < env < explicit) */
75
+ mergedOptions: RunOptions;
76
+ /** Execution config derived from mergedOptions */
77
+ config: ExecutionConfig;
78
+ /** Parsed + dep-sorted issue numbers (pre-state-guard) */
79
+ issueNumbers: number[];
80
+ /** Resolved batches if --batch specified, else null */
81
+ batches: number[][] | null;
82
+ /** Resolved base branch (CLI → settings → auto-detect → "main") */
83
+ baseBranch: string;
84
+ /** Stack from manifest */
85
+ stack: string;
86
+ /** True when phases will be auto-detected from issue labels */
87
+ autoDetectPhases: boolean;
88
+ /** True when worktree isolation is enabled */
89
+ worktreeIsolationEnabled: boolean;
90
+ /** True when JSON logging will be initialized */
91
+ logEnabled: boolean;
92
+ /** True when state tracking will be enabled */
93
+ stateEnabled: boolean;
94
+ }
95
+ /**
96
+ * Structured result of a full orchestrator run.
97
+ */
98
+ export interface RunResult {
99
+ /** Per-issue results */
100
+ results: IssueResult[];
101
+ /** Log file path (if logging enabled) */
102
+ logPath: string | null;
103
+ /** Non-zero if any issue failed */
104
+ exitCode: number;
105
+ /** Worktree map (for summary display) */
106
+ worktreeMap: Map<number, WorktreeInfo>;
107
+ /** Issue info map (for summary display) */
108
+ issueInfoMap: Map<number, {
109
+ title: string;
110
+ labels: string[];
111
+ }>;
112
+ /** Resolved execution config */
113
+ config: ExecutionConfig;
114
+ /** Resolved merged options */
115
+ mergedOptions: RunOptions;
116
+ /** Log writer (for reflection access) */
117
+ logWriter: LogWriter | null;
118
+ }
119
+ /**
120
+ * CLI-free workflow execution engine.
121
+ *
122
+ * Two usage modes:
123
+ * 1. Full lifecycle: `RunOrchestrator.run(init, issueNumbers)` — handles
124
+ * services, worktrees, state guard, execution, and metrics.
125
+ * 2. Low-level: `new RunOrchestrator(config).execute(issueNumbers)` — caller
126
+ * manages setup/teardown.
127
+ */
128
+ export declare class RunOrchestrator {
129
+ private readonly cfg;
130
+ constructor(config: OrchestratorConfig);
131
+ /**
132
+ * Pure config resolution — no side effects.
133
+ *
134
+ * Produces a `ResolvedRun` containing merged options, execution config,
135
+ * parsed/sorted issue numbers, base branch, and display-only flags. Safe
136
+ * to call for preview purposes (e.g. CLI config display before run).
137
+ *
138
+ * `run()` uses this internally to avoid duplicating resolution logic.
139
+ */
140
+ static resolveConfig(init: RunInit, issueArgs: string[], batches?: number[][] | null): ResolvedRun;
141
+ /**
142
+ * Full lifecycle execution — the primary entry point for programmatic use.
143
+ *
144
+ * Handles: config resolution → services setup → state guard →
145
+ * issue discovery → worktree creation → execution → metrics → cleanup.
146
+ */
147
+ static run(init: RunInit, issueArgs: string[], batches?: number[][] | null): Promise<RunResult>;
148
+ /**
149
+ * Execute workflow for the given issue numbers.
150
+ * Returns one IssueResult per issue.
151
+ */
152
+ execute(issueNumbers: number[]): Promise<IssueResult[]>;
153
+ private validate;
154
+ private buildBatchContext;
155
+ private executeSequential;
156
+ private executeParallel;
157
+ private executeOneIssue;
158
+ private static recordMetrics;
159
+ }
160
+ /** Log a non-fatal warning: one-line summary always, detail in verbose. */
161
+ export declare function logNonFatalWarning(message: string, error: unknown, verbose: boolean): void;