sequant 2.6.2 → 2.8.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 (62) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +13 -1
  4. package/dist/bin/cli.d.ts +1 -1
  5. package/dist/bin/cli.js +11 -1
  6. package/dist/bin/preflight.d.ts +21 -0
  7. package/dist/bin/preflight.js +45 -0
  8. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  9. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/force-push.md +34 -0
  10. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +24 -7
  11. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +29 -0
  12. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +100 -2
  13. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +24 -0
  14. package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +285 -0
  15. package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +202 -0
  16. package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +287 -0
  17. package/dist/marketplace/external_plugins/sequant/skills/qa/references/test-quality-checklist.md +272 -0
  18. package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +40 -0
  19. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +95 -11
  20. package/dist/marketplace/external_plugins/sequant/skills/references/shared/framework-gotchas.md +186 -0
  21. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +661 -0
  22. package/dist/marketplace/external_plugins/sequant/skills/test/references/browser-testing-patterns.md +423 -0
  23. package/dist/marketplace/external_plugins/sequant/skills/upstream/SKILL.md +419 -0
  24. package/dist/src/commands/sync.d.ts +1 -0
  25. package/dist/src/commands/sync.js +56 -1
  26. package/dist/src/commands/update.js +7 -0
  27. package/dist/src/lib/errors.d.ts +85 -0
  28. package/dist/src/lib/errors.js +111 -0
  29. package/dist/src/lib/version-check.d.ts +19 -0
  30. package/dist/src/lib/version-check.js +44 -0
  31. package/dist/src/lib/workflow/batch-executor.js +61 -6
  32. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +17 -0
  33. package/dist/src/lib/workflow/drivers/claude-code.d.ts +22 -0
  34. package/dist/src/lib/workflow/drivers/claude-code.js +111 -7
  35. package/dist/src/lib/workflow/log-writer.d.ts +1 -1
  36. package/dist/src/lib/workflow/phase-executor.d.ts +18 -0
  37. package/dist/src/lib/workflow/phase-executor.js +76 -14
  38. package/dist/src/lib/workflow/run-log-schema.d.ts +3 -0
  39. package/dist/src/lib/workflow/run-log-schema.js +7 -0
  40. package/dist/src/lib/workflow/state-manager.d.ts +1 -0
  41. package/dist/src/lib/workflow/state-manager.js +6 -0
  42. package/dist/src/lib/workflow/state-schema.d.ts +3 -0
  43. package/dist/src/lib/workflow/state-schema.js +7 -0
  44. package/dist/src/lib/workflow/types.d.ts +17 -0
  45. package/dist/src/ui/tui/theme.d.ts +18 -4
  46. package/dist/src/ui/tui/theme.js +18 -4
  47. package/package.json +4 -3
  48. package/templates/skills/_shared/references/force-push.md +34 -0
  49. package/templates/skills/assess/SKILL.md +24 -7
  50. package/templates/skills/exec/SKILL.md +29 -0
  51. package/templates/skills/loop/SKILL.md +100 -2
  52. package/templates/skills/qa/SKILL.md +24 -0
  53. package/templates/skills/qa/references/anti-pattern-detection.md +285 -0
  54. package/templates/skills/qa/references/call-site-review.md +202 -0
  55. package/templates/skills/qa/references/quality-gates.md +287 -0
  56. package/templates/skills/qa/references/test-quality-checklist.md +272 -0
  57. package/templates/skills/qa/references/testing-requirements.md +40 -0
  58. package/templates/skills/qa/scripts/quality-checks.sh +95 -11
  59. package/templates/skills/references/shared/framework-gotchas.md +186 -0
  60. package/templates/skills/release/SKILL.md +661 -0
  61. package/templates/skills/test/references/browser-testing-patterns.md +423 -0
  62. package/templates/skills/upstream/SKILL.md +419 -0
@@ -12,7 +12,7 @@ import { execSync, execFileSync } from "child_process";
12
12
  import { readAgentsMd } from "../agents-md.js";
13
13
  import { getDriver } from "./drivers/index.js";
14
14
  import { classifyError } from "./error-classifier.js";
15
- import { ApiError } from "../errors.js";
15
+ import { ApiError, BillingError } from "../errors.js";
16
16
  import { phaseRegistry } from "./phase-registry.js";
17
17
  import { bracketedConsoleLog } from "./notice.js";
18
18
  /**
@@ -408,6 +408,43 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
408
408
  ...tails,
409
409
  };
410
410
  }
411
+ /**
412
+ * Map a failed driver result to a `PhaseResult`.
413
+ *
414
+ * Symmetric to {@link mapAgentSuccessToPhaseResult}; extracted so the
415
+ * failure-path mapping (notably the #739 capped/output gating) is unit-testable
416
+ * without spawning a driver.
417
+ *
418
+ * `output` is propagated **only** for a capped phase (#739): a capped result is
419
+ * incomplete-but-not-hard-failed, so its partial work must survive downstream.
420
+ * A genuine (non-capped) failure keeps the historical behaviour of dropping
421
+ * `output`, leaving the `/loop` fix-context (`formatFailureContext`) unchanged.
422
+ *
423
+ * @internal Exported for testing only
424
+ */
425
+ export function mapAgentFailureToPhaseResult(phase, agentResult, durationSeconds) {
426
+ return {
427
+ phase,
428
+ success: false,
429
+ durationSeconds,
430
+ error: agentResult.error,
431
+ // Propagate the driver's typed cause (#732) so the retry logic can prefer
432
+ // it over stderr-regex classification and gate the MCP fallback.
433
+ structuredError: agentResult.structuredError,
434
+ // Propagate the turn-cap flag and the partial output (#739). On the failure
435
+ // path `output` was previously dropped entirely — for a capped phase the
436
+ // partial work is usable and must be preserved, mirroring the driver/skill
437
+ // slice from #733. Gating `output` on `capped` keeps non-capped failures
438
+ // byte-for-byte identical to pre-#739 behaviour.
439
+ capped: agentResult.capped,
440
+ output: agentResult.capped ? agentResult.output : undefined,
441
+ sessionId: agentResult.sessionId,
442
+ resumeHandle: agentResult.resumeHandle,
443
+ stderrTail: agentResult.stderrTail,
444
+ stdoutTail: agentResult.stdoutTail,
445
+ exitCode: agentResult.exitCode,
446
+ };
447
+ }
411
448
  /**
412
449
  * Get the prompt for a phase with the issue number substituted.
413
450
  * Selects self-contained prompts for non-Claude agents.
@@ -642,17 +679,7 @@ async function executePhase(issueNumber, phase, config, resumeHandle, worktreePa
642
679
  if (agentResult.success) {
643
680
  return mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds, cwd);
644
681
  }
645
- return {
646
- phase,
647
- success: false,
648
- durationSeconds,
649
- error: agentResult.error,
650
- sessionId: agentResult.sessionId,
651
- resumeHandle: agentResult.resumeHandle,
652
- stderrTail: agentResult.stderrTail,
653
- stdoutTail: agentResult.stdoutTail,
654
- exitCode: agentResult.exitCode,
655
- };
682
+ return mapAgentFailureToPhaseResult(phase, agentResult, durationSeconds);
656
683
  }
657
684
  /**
658
685
  * Execute a phase with automatic retry for cold-start failures and MCP fallback.
@@ -693,6 +720,14 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
693
720
  if (lastResult.success) {
694
721
  return lastResult;
695
722
  }
723
+ // Turn-capped phase (#739): incomplete-but-not-hard-failed. A retry cannot
724
+ // un-cap a turn limit, so short-circuit before any fallback — same rationale
725
+ // as the billing skip (#732), but capped must skip *all* retries (incl.
726
+ // cold-start), so an explicit early return is required, not just a guard
727
+ // flag at the MCP gate.
728
+ if (lastResult.capped) {
729
+ return lastResult;
730
+ }
696
731
  }
697
732
  else {
698
733
  // Phase 1: Cold-start retry attempts (with MCP enabled if configured)
@@ -703,11 +738,23 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
703
738
  if (lastResult.success) {
704
739
  return lastResult;
705
740
  }
741
+ // Turn-capped phase (#739): short-circuit before cold-start retries, the
742
+ // MCP fallback, and the spec-extra retry — a retry cannot un-cap a turn
743
+ // limit. The early return here (rather than a guard at the MCP gate alone)
744
+ // is what skips the cold-start re-spawns, unlike the billing case which
745
+ // still cold-start-retries in the <60s window.
746
+ if (lastResult.capped) {
747
+ return lastResult;
748
+ }
706
749
  // Genuine failure (took long enough to be real work) → skip cold-start retries.
707
750
  // Use error classification (AC-9): if the error is retryable (e.g., API
708
751
  // rate limit, transient 503), allow one more attempt even for genuine failures.
709
752
  if (duration >= COLD_START_THRESHOLD_SECONDS) {
710
- const typedError = classifyError(lastResult.stderrTail ?? [], lastResult.exitCode);
753
+ // Prefer the driver's structured cause (#732) it reflects the real
754
+ // SDK rate-limit/billing signal — over stderr-regex classification,
755
+ // which only sees text and never the structured data.
756
+ const typedError = lastResult.structuredError ??
757
+ classifyError(lastResult.stderrTail ?? [], lastResult.exitCode);
711
758
  if (typedError.isRetryable && attempt < COLD_START_MAX_RETRIES) {
712
759
  if (config.verbose) {
713
760
  const label = typedError instanceof ApiError
@@ -735,7 +782,22 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
735
782
  // Phase 2: MCP fallback - if MCP is enabled and we're still failing, try without MCP
736
783
  // This handles npx-based MCP servers that fail on first run due to cold-cache issues.
737
784
  // Skip for `loop` phase — MCP is never the cause of loop failures (#488).
738
- if (config.mcp && !lastResult.success && !skipColdStartRetry) {
785
+ //
786
+ // Also skip when the failure is a billing/credits error (#732): a no-MCP
787
+ // retry cannot refill credits, so the misleading "retrying without MCP"
788
+ // noise (#592) would only mask the real cause. The accurate structured
789
+ // message (e.g. "Out of credits") is surfaced instead.
790
+ const failureIsBilling = lastResult.structuredError instanceof BillingError;
791
+ // Belt-and-suspenders (#739): the capped early-returns above already exit
792
+ // before reaching here, but gate the MCP fallback on `!failureIsCapped` too so
793
+ // intent is documented and future code paths can't accidentally re-spawn a
794
+ // capped phase without MCP.
795
+ const failureIsCapped = lastResult.capped === true;
796
+ if (config.mcp &&
797
+ !lastResult.success &&
798
+ !skipColdStartRetry &&
799
+ !failureIsBilling &&
800
+ !failureIsCapped) {
739
801
  bracketedConsoleLog(spinner, chalk.yellow(`\n ! Phase failed with MCP enabled, retrying without MCP...`));
740
802
  // Create config copy with MCP disabled
741
803
  const configWithoutMcp = {
@@ -125,6 +125,7 @@ export declare const PhaseLogSchema: z.ZodObject<{
125
125
  timeout: "timeout";
126
126
  }>;
127
127
  error: z.ZodOptional<z.ZodString>;
128
+ capped: z.ZodOptional<z.ZodBoolean>;
128
129
  iterations: z.ZodOptional<z.ZodNumber>;
129
130
  filesModified: z.ZodOptional<z.ZodArray<z.ZodString>>;
130
131
  testsRun: z.ZodOptional<z.ZodNumber>;
@@ -201,6 +202,7 @@ export declare const IssueLogSchema: z.ZodObject<{
201
202
  timeout: "timeout";
202
203
  }>;
203
204
  error: z.ZodOptional<z.ZodString>;
205
+ capped: z.ZodOptional<z.ZodBoolean>;
204
206
  iterations: z.ZodOptional<z.ZodNumber>;
205
207
  filesModified: z.ZodOptional<z.ZodArray<z.ZodString>>;
206
208
  testsRun: z.ZodOptional<z.ZodNumber>;
@@ -318,6 +320,7 @@ export declare const RunLogSchema: z.ZodObject<{
318
320
  timeout: "timeout";
319
321
  }>;
320
322
  error: z.ZodOptional<z.ZodString>;
323
+ capped: z.ZodOptional<z.ZodBoolean>;
321
324
  iterations: z.ZodOptional<z.ZodNumber>;
322
325
  filesModified: z.ZodOptional<z.ZodArray<z.ZodString>>;
323
326
  testsRun: z.ZodOptional<z.ZodNumber>;
@@ -130,6 +130,13 @@ export const PhaseLogSchema = z.object({
130
130
  status: PhaseStatusSchema,
131
131
  /** Error message if failed */
132
132
  error: z.string().optional(),
133
+ /**
134
+ * Set when the phase hit its turn cap (`error_max_turns`) (#739). Distinguishes
135
+ * an incomplete-but-not-hard-failed phase (partial output preserved) from a
136
+ * genuine failure. Reuses the `"failure"` status — additive boolean rather than
137
+ * a new `PhaseStatus` enum value, to keep the persisted-log schema stable.
138
+ */
139
+ capped: z.boolean().optional(),
133
140
  /** Number of iterations (for loop phase) */
134
141
  iterations: z.number().int().nonnegative().optional(),
135
142
  /** Files modified during this phase */
@@ -116,6 +116,7 @@ export declare class StateManager {
116
116
  updatePhaseStatus(issueNumber: number, phase: Phase, status: PhaseStatus, options?: {
117
117
  error?: string;
118
118
  iteration?: number;
119
+ capped?: boolean;
119
120
  }): Promise<void>;
120
121
  /**
121
122
  * Update the overall issue status
@@ -300,6 +300,12 @@ export class StateManager {
300
300
  if (options?.iteration !== undefined) {
301
301
  phaseState.iteration = options.iteration;
302
302
  }
303
+ // Persist the turn-cap marker (#739) so a halted-on-cap phase is
304
+ // distinguishable from a genuine failure in state, not just the run-log —
305
+ // this is what makes the "reversible later" resume path first-class.
306
+ if (options?.capped !== undefined) {
307
+ phaseState.capped = options.capped;
308
+ }
303
309
  // Preserve startedAt if already set
304
310
  const existingPhase = issueState.phases[phase];
305
311
  if (existingPhase?.startedAt && status !== "pending") {
@@ -88,6 +88,7 @@ export declare const PhaseStateSchema: z.ZodObject<{
88
88
  startedAt: z.ZodOptional<z.ZodString>;
89
89
  completedAt: z.ZodOptional<z.ZodString>;
90
90
  error: z.ZodOptional<z.ZodString>;
91
+ capped: z.ZodOptional<z.ZodBoolean>;
91
92
  iteration: z.ZodOptional<z.ZodNumber>;
92
93
  }, z.core.$strip>;
93
94
  export type PhaseState = z.infer<typeof PhaseStateSchema>;
@@ -242,6 +243,7 @@ export declare const IssueStateSchema: z.ZodObject<{
242
243
  startedAt: z.ZodOptional<z.ZodString>;
243
244
  completedAt: z.ZodOptional<z.ZodString>;
244
245
  error: z.ZodOptional<z.ZodString>;
246
+ capped: z.ZodOptional<z.ZodBoolean>;
245
247
  iteration: z.ZodOptional<z.ZodNumber>;
246
248
  }, z.core.$strip>>;
247
249
  pr: z.ZodOptional<z.ZodObject<{
@@ -381,6 +383,7 @@ export declare const WorkflowStateSchema: z.ZodObject<{
381
383
  startedAt: z.ZodOptional<z.ZodString>;
382
384
  completedAt: z.ZodOptional<z.ZodString>;
383
385
  error: z.ZodOptional<z.ZodString>;
386
+ capped: z.ZodOptional<z.ZodBoolean>;
384
387
  iteration: z.ZodOptional<z.ZodNumber>;
385
388
  }, z.core.$strip>>;
386
389
  pr: z.ZodOptional<z.ZodObject<{
@@ -85,6 +85,13 @@ export const PhaseStateSchema = z.object({
85
85
  completedAt: z.string().datetime().optional(),
86
86
  /** Error message if phase failed */
87
87
  error: z.string().optional(),
88
+ /**
89
+ * Set when the phase halted on its turn cap (`error_max_turns`) (#739).
90
+ * Distinguishes a recoverable cap (partial output preserved, resume to
91
+ * continue) from a genuine failure while keeping `status: "failed"`.
92
+ * Additive/optional — existing persisted state is unaffected.
93
+ */
94
+ capped: z.boolean().optional(),
88
95
  /** Number of loop iterations (for loop phase) */
89
96
  iteration: z.number().int().nonnegative().optional(),
90
97
  });
@@ -7,6 +7,7 @@ import type { LogWriter } from "./log-writer.js";
7
7
  import type { StateManager } from "./state-manager.js";
8
8
  import type { ShutdownManager } from "../shutdown.js";
9
9
  import type { WorktreeInfo } from "./worktree-manager.js";
10
+ import type { SequantError } from "../errors.js";
10
11
  export type { WorkflowEventEmitter, WorkflowEvents, WorkflowEventListener, IssueEventStatus, BaseEventPayload, RunEventPayload, PhaseStartedPayload, PhaseCompletedPayload, PhaseFailedPayload, IssueStatusChangedPayload, QaVerdictPayload, ProgressPayload, } from "./event-emitter.js";
11
12
  /**
12
13
  * Canonical Zod schema for all workflow phases.
@@ -160,6 +161,22 @@ export interface PhaseResult {
160
161
  success: boolean;
161
162
  durationSeconds?: number;
162
163
  error?: string;
164
+ /**
165
+ * Typed error with structured cause data, propagated from the driver's
166
+ * `AgentPhaseResult.structuredError` (#732). When present, the retry logic
167
+ * prefers it over stderr-regex classification and uses its type to gate the
168
+ * MCP fallback (a `BillingError` skips the misleading retry — #592).
169
+ */
170
+ structuredError?: SequantError;
171
+ /**
172
+ * Set when the phase hit its turn cap (`error_max_turns`), propagated from the
173
+ * driver's `AgentPhaseResult.capped` (#733/#739). A capped phase is
174
+ * incomplete-but-not-hard-failed: the partial work in `output` is preserved
175
+ * rather than discarded, and the retry logic treats it like the `BillingError`
176
+ * skip (#732) — a retry cannot un-cap a turn limit. Additive/optional, same
177
+ * shape as `structuredError?`; existing consumers are unaffected.
178
+ */
179
+ capped?: boolean;
163
180
  /** Captured output from the phase (used for parsing spec recommendations) */
164
181
  output?: string;
165
182
  /** Parsed QA verdict (only for qa phase) */
@@ -12,8 +12,10 @@ import type { IssueStatus, PhaseStatus } from "../../lib/workflow/run-state.js";
12
12
  * - `BRAND_GREEN` is the accent/success green (`--color-accent`).
13
13
  *
14
14
  * Used to brand the two color signals that matter most at a glance — the
15
- * live/active phase and success — while issue-distinction (border rotation),
16
- * failure (red), and dividers (gray) stay on robust named ANSI colors.
15
+ * live/active phase and success — while issue-distinction (border rotation)
16
+ * and failure (red) stay on robust named ANSI colors. The muted gray for
17
+ * secondary chrome is a fixed mid-gray (`DIVIDER_COLOR`) chosen for WCAG
18
+ * contrast rather than the too-dim ANSI bright-black.
17
19
  *
18
20
  * Ink/chalk auto-downsamples hex to the nearest ANSI color on terminals
19
21
  * without truecolor, and `NO_COLOR` still strips all color, so these degrade
@@ -24,8 +26,20 @@ export declare const BRAND_GREEN: "#10b981";
24
26
  /** Border-color palette rotated by issue start order. */
25
27
  export declare const BORDER_ROTATION: readonly ["cyan", "magenta", "blue", "yellow"];
26
28
  export type BorderColor = (typeof BORDER_ROTATION)[number] | typeof BRAND_GREEN | typeof BRAND_ORANGE | "red" | "gray";
27
- /** Gray used for horizontal dividers inside each box. */
28
- export declare const DIVIDER_COLOR: "gray";
29
+ /**
30
+ * Muted gray for secondary chrome: dividers, field labels (`branch`/`now`/
31
+ * `log`), the `phase N/total` + elapsed line, the `last activity` stamp, and
32
+ * the terminal status line.
33
+ *
34
+ * Lifted off ANSI `"gray"` (bright-black), which the brand's own dark theme
35
+ * renders at ~2.4:1 — below WCAG AA (4.5) and even the 3.0 large-text/UI floor.
36
+ * This fixed mid-gray clears AA (~4.9:1) on the dark theme while staying above
37
+ * 3.0 on light terminals (~3.4:1). It still degrades gracefully: chalk
38
+ * downsamples the hex to the nearest ANSI color on non-truecolor terminals, and
39
+ * `NO_COLOR` strips it entirely. The not-yet-started phase glyph stays on ANSI
40
+ * `"gray"` (see `phaseStatusColor`) so pending work remains the most recessed.
41
+ */
42
+ export declare const DIVIDER_COLOR: "#8B8B9A";
29
43
  /** Brand orange for the live/active phase spinner — the one element the eye
30
44
  * tracks. Border rotation still distinguishes concurrent issues. */
31
45
  export declare const ACTIVE_PHASE_COLOR: "#FF8012";
@@ -11,8 +11,10 @@
11
11
  * - `BRAND_GREEN` is the accent/success green (`--color-accent`).
12
12
  *
13
13
  * Used to brand the two color signals that matter most at a glance — the
14
- * live/active phase and success — while issue-distinction (border rotation),
15
- * failure (red), and dividers (gray) stay on robust named ANSI colors.
14
+ * live/active phase and success — while issue-distinction (border rotation)
15
+ * and failure (red) stay on robust named ANSI colors. The muted gray for
16
+ * secondary chrome is a fixed mid-gray (`DIVIDER_COLOR`) chosen for WCAG
17
+ * contrast rather than the too-dim ANSI bright-black.
16
18
  *
17
19
  * Ink/chalk auto-downsamples hex to the nearest ANSI color on terminals
18
20
  * without truecolor, and `NO_COLOR` still strips all color, so these degrade
@@ -22,8 +24,20 @@ export const BRAND_ORANGE = "#FF8012";
22
24
  export const BRAND_GREEN = "#10b981";
23
25
  /** Border-color palette rotated by issue start order. */
24
26
  export const BORDER_ROTATION = ["cyan", "magenta", "blue", "yellow"];
25
- /** Gray used for horizontal dividers inside each box. */
26
- export const DIVIDER_COLOR = "gray";
27
+ /**
28
+ * Muted gray for secondary chrome: dividers, field labels (`branch`/`now`/
29
+ * `log`), the `phase N/total` + elapsed line, the `last activity` stamp, and
30
+ * the terminal status line.
31
+ *
32
+ * Lifted off ANSI `"gray"` (bright-black), which the brand's own dark theme
33
+ * renders at ~2.4:1 — below WCAG AA (4.5) and even the 3.0 large-text/UI floor.
34
+ * This fixed mid-gray clears AA (~4.9:1) on the dark theme while staying above
35
+ * 3.0 on light terminals (~3.4:1). It still degrades gracefully: chalk
36
+ * downsamples the hex to the nearest ANSI color on non-truecolor terminals, and
37
+ * `NO_COLOR` strips it entirely. The not-yet-started phase glyph stays on ANSI
38
+ * `"gray"` (see `phaseStatusColor`) so pending work remains the most recessed.
39
+ */
40
+ export const DIVIDER_COLOR = "#8B8B9A";
27
41
  /** Brand orange for the live/active phase spinner — the one element the eye
28
42
  * tracks. Border rotation still distinguishes concurrent issues. */
29
43
  export const ACTIVE_PHASE_COLOR = BRAND_ORANGE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sequant",
3
- "version": "2.6.2",
3
+ "version": "2.8.0",
4
4
  "description": "AI coding agent orchestrator — resolve GitHub issues end-to-end with isolated git worktrees, quality gates, and an MCP server. Works with Claude Code or Aider.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,10 +25,11 @@
25
25
  "dev": "tsx bin/cli.ts",
26
26
  "test": "vitest run",
27
27
  "lint": "eslint src/ bin/ --max-warnings 0",
28
- "sync:skills": "cp -r templates/skills/* .claude/skills/",
28
+ "sync:skills": "npx tsx scripts/check-skill-sync.ts --fix",
29
29
  "sync:hooks": "bash scripts/sync-hooks.sh",
30
- "validate:skills": "for skill in templates/skills/*/; do npx skills-ref validate \"$skill\"; done",
30
+ "validate:skills": "for skill in templates/skills/*/; do case \"$skill\" in *_shared*|*/references/*) continue;; esac; npx skills-ref validate \"$skill\"; done",
31
31
  "lint:skill-calls": "npx tsx scripts/lint-skill-calls.ts",
32
+ "lint:skill-sync": "npx tsx scripts/check-skill-sync.ts",
32
33
  "prepare:marketplace": "npx tsx scripts/prepare-marketplace.ts",
33
34
  "validate:marketplace": "npx tsx scripts/prepare-marketplace.ts --validate-only",
34
35
  "prepublishOnly": "npm run build"
@@ -0,0 +1,34 @@
1
+ # Force-push handoff pattern
2
+
3
+ When you encounter `HOOK_BLOCKED: Force push` from `.claude/hooks/pre-tool.sh:106-111`, **do not attempt to bypass the hook**. The block is intentional — force-pushing rewrites history and can destroy work for others sharing the branch.
4
+
5
+ ## The pattern: hand the command to the user
6
+
7
+ When a force push is genuinely required (e.g., cleaning contamination off a feature branch after a clean rebase), present the exact command to the user prefixed with `!` so they execute it in-session:
8
+
9
+ ```
10
+ ! git push --force-with-lease origin feature/<branch>
11
+ ```
12
+
13
+ The user pastes that into the prompt; the `!` runs it in their shell, output streams back into the conversation, and you continue from there.
14
+
15
+ **Always prefer `--force-with-lease` over raw `--force`.** `--force-with-lease` refuses to overwrite the remote ref if someone else pushed in the meantime, turning a silent stomp into a clean error.
16
+
17
+ ## Why bypass attempts fail
18
+
19
+ `CLAUDE_HOOKS_DISABLED=true git push --force ...` does **not** work. The hook reads `CLAUDE_HOOKS_DISABLED` at the harness level *before* Bash executes the command, so prefixing the env var inside the command line has no effect. Setting it via `export` in a prior tool call doesn't help either — each Bash tool call is a fresh subprocess.
20
+
21
+ ## When force push is legitimate vs. not
22
+
23
+ | Legitimate | Not legitimate |
24
+ |------------|----------------|
25
+ | Cleaning rebase contamination off your own feature branch before PR | Rewriting history on `main`/`master` |
26
+ | Removing accidentally-committed secrets after rotation | "Squashing for cleanliness" on a shared branch |
27
+ | Recovering from a mistakenly-pushed merge commit | Force-pushing over someone else's work |
28
+
29
+ For shared branches, prefer `git revert` over force push.
30
+
31
+ ## Reference
32
+
33
+ - Block definition: `.claude/hooks/pre-tool.sh:106-111`
34
+ - Regex: `git push.*(--force| -f($| ))` — note this can also match the literal strings inside quoted `gh issue/pr` bodies (workaround: write the body to a file first)
@@ -69,6 +69,20 @@ If the output is non-empty, paste every line verbatim above the dashboard table
69
69
 
70
70
  The orchestrator/MCP mode (`SEQUANT_ORCHESTRATOR` set) returns no output, so the call is safe to make unconditionally.
71
71
 
72
+ **Command prefix (#740, read-only):**
73
+
74
+ Probe once here for a global/PATH `sequant`, and reuse the result for every emitted run command below. `npx sequant` is the invocation most prone to version skew (a dual node prefix plus npx cache reuse can silently run a *stale* binary while a directly-installed `sequant` on PATH is current), so prefer a resolvable global install when one exists.
75
+
76
+ ```bash
77
+ # Resolve CMD_PREFIX once here; reuse it for every emitted run command below.
78
+ command -v sequant >/dev/null 2>&1 && CMD_PREFIX="sequant" || CMD_PREFIX="npx sequant"
79
+ ```
80
+
81
+ - Global install on PATH → `CMD_PREFIX="sequant"` → emit `sequant run …`
82
+ - No global install (npx-only) → `CMD_PREFIX="npx sequant"` → emit `npx sequant run …` (unchanged default — zero behavior change for npx-only users)
83
+
84
+ The probe is read-only and side-effect-free, so it runs unconditionally, including in orchestrator/MCP mode (`SEQUANT_ORCHESTRATOR` set).
85
+
72
86
  **From GitHub (parallel for all issues):**
73
87
 
74
88
  ```bash
@@ -182,7 +196,7 @@ Triggers (any one):
182
196
  - Issue body or comments mention `"depends on #N"`, `"blocked by #N"`, or `"after #N"`
183
197
  - One issue's described output is another issue's input (e.g., A changes a function signature that B consumes)
184
198
 
185
- Format: `Chain: npx sequant run <N1> <N2> --chain --qa-gate -Q <phases> # alternative — <one-line reason>`
199
+ Format: `Chain: <CMD_PREFIX> run <N1> <N2> --chain --qa-gate -Q <phases> # alternative — <one-line reason>` (`<CMD_PREFIX>` resolved in Step 1)
186
200
 
187
201
  Flag references:
188
202
  - `--chain` chains issues (each branches from previous; implies `--sequential`)
@@ -233,15 +247,15 @@ False-positive guards and tunables (excluded paths, the path regex, the slash-co
233
247
  ...
234
248
  ────────────────────────────────────────────────────────────────
235
249
  Commands:
236
- npx sequant run <N1> <N2> <flags>
237
- npx sequant run <N3> <flags> # resume
250
+ <CMD_PREFIX> run <N1> <N2> <flags>
251
+ <CMD_PREFIX> run <N3> <flags> # resume
238
252
  ────────────────────────────────────────────────────────────────
239
253
  Order: <N> → <N> (<dependency reason>)
240
254
 
241
255
  ⚠ #<N> <warning>
242
256
  ⚠ #<N> <warning>
243
257
 
244
- Chain: npx sequant run <N1> <N2> --chain --qa-gate -Q <phases> # alternative — <reason>
258
+ Chain: <CMD_PREFIX> run <N1> <N2> --chain --qa-gate -Q <phases> # alternative — <reason>
245
259
 
246
260
  Flags:
247
261
  <flag> <one-line reason>
@@ -286,6 +300,7 @@ The commands block is headed by `Commands:` — no box-drawing, no character cou
286
300
  6. If ALL issues share the same workflow, emit a single command
287
301
  7. **Line splitting:** When a single command would contain more than 6 issue numbers, split into multiple commands of at most 6 issues each, grouped by compatible workflow. Example: 11 issues → two commands (6 + 5)
288
302
  8. **Minimal flags:** Omit `--phases` when the resulting workflow equals the CLI default (registered at `bin/cli.ts:186`, defined as `DEFAULT_PHASES` in `src/lib/workflow/types.ts`). Prefer additive flags over restating phases — additive flags: `--testgen` and `--security-review` (`bin/cli.ts:208-209`). Use `--testgen` instead of `--phases spec,testgen,exec,qa` (or `…,testgen,…,test,qa` for ui-labelled issues, since `phase-mapper.determinePhasesForIssue` auto-adds `test` from the ui label). Use `--security-review` instead of `--phases spec,security-review,exec,qa`. The posted marker (`<!-- assess:phases=… -->`) records the full resolved workflow regardless — markers are machine-readable, displayed commands are human shorthand. This intentional divergence is fine: parsers consume markers, humans copy commands.
303
+ 9. **Command prefix:** Substitute the Step-1 `CMD_PREFIX` for **every** emitted `sequant run` command — the Commands block, the `Chain:` line, and both single-issue detail-mode commands (PROCEED and the REWRITE "fresh start"). `Cleanup:` commands are `git`/`gh`, not `sequant`, so they are unaffected. A resolvable global `sequant` on PATH yields `sequant run …`; npx-only yields `npx sequant run …` (the default). Never mix prefixes within a single assessment.
289
304
 
290
305
  #### Annotation Rules
291
306
 
@@ -303,7 +318,7 @@ Emit annotations in this order between the separators that follow `Commands:`:
303
318
  - `⚠ #412 bug + auth labels — domain label (auth) takes priority over bug`
304
319
 
305
320
  - **`Chain:`** — Only when 2+ PROCEED issues have a detected dependency (see "Chain detection" in Step 4). Suggests an alternative execution topology. Does not replace the default per-issue commands. Format:
306
- `Chain: npx sequant run <N1> <N2> --chain --qa-gate -Q <phases> # alternative — <one-line reason>`
321
+ `Chain: <CMD_PREFIX> run <N1> <N2> --chain --qa-gate -Q <phases> # alternative — <one-line reason>` (`<CMD_PREFIX>` resolved in Step 1)
307
322
 
308
323
  - **`Flags:`** — Only when non-default flags appear in the commands and the reason isn't obvious. One line per **distinct** flag used across all commands. Omit entire section when `-Q` is the only non-default flag AND its reason is obvious (e.g., all issues are enhancements). Format:
309
324
  ```
@@ -321,6 +336,8 @@ Emit annotations in this order between the separators that follow `Commands:`:
321
336
 
322
337
  Not all issues have explicit `- [ ]` checkboxes, so the `ACs` column is omitted.
323
338
 
339
+ > **Prefix in examples:** The worked examples in this doc show the `npx sequant` default (the zero-install path). When the Step-1 probe resolves a global `sequant` on PATH, `CMD_PREFIX="sequant"` and every emitted command uses `sequant run …` instead — consistently within one assessment (see Commands Block Rule #9).
340
+
324
341
  ```
325
342
  # Action Reason Run
326
343
  462 PARK Manual measurement task ‖
@@ -485,7 +502,7 @@ More context since you're focused on one issue. Separators between every section
485
502
  → PROCEED — <one-line reason>
486
503
 
487
504
  Commands:
488
- npx sequant run <N> <flags>
505
+ <CMD_PREFIX> run <N> <flags>
489
506
 
490
507
  <phases> · <N> ACs
491
508
 
@@ -617,7 +634,7 @@ Need: <specific information required>
617
634
  → REWRITE — <reason>
618
635
 
619
636
  Commands:
620
- npx sequant run <N> <flags> # fresh start
637
+ <CMD_PREFIX> run <N> <flags> # fresh start
621
638
 
622
639
  <phases> · <N> ACs
623
640
  ────────────────────────────────────────────────────────────────
@@ -1657,6 +1657,35 @@ Parse the agent's output text for these patterns to detect failures:
1657
1657
  | `blocked by hook` | Operation was blocked by pre-tool hook |
1658
1658
  | `I'm unable to` | Agent hit a blocking constraint |
1659
1659
 
1660
+ ### 4b2. Detecting Turn-Capped Implementers (Incomplete, not Failed)
1661
+
1662
+ Every spawned implementer runs under a `maxTurns` cap (live since #484). Hitting the cap is **not** a failure — the agent did real, partial work before running out of turns, and that work is preserved (driver returns it flagged `capped: true` and warns rather than erroring, #733). Treat a capped implementer as **incomplete**, distinct from the hook-block failures in Section 4b.
1663
+
1664
+ **Turn-cap detection keywords** (parse the agent's output text):
1665
+
1666
+ | Pattern | Meaning |
1667
+ |---------|---------|
1668
+ | `error_max_turns` | Agent hit its turn cap |
1669
+ | `turn cap` / `Returning partial results` | Driver-emitted turn-cap warning |
1670
+ | Output ends mid-task with no completion report | Likely capped |
1671
+
1672
+ **How to handle a capped implementer:**
1673
+
1674
+ - Do **not** discard its changes or roll back the group — keep the partial work it committed.
1675
+ - Record, per task, **which tasks finished vs. which were capped**. A capped task is incomplete, not done.
1676
+ - Continue with the remaining (non-capped) tasks normally; one cap does not abort the whole `/exec` run.
1677
+ - The next `/exec` iteration (or a resumed session) picks up the capped task to finish it — note it explicitly so it is not mistaken for complete.
1678
+ - Reflect capped tasks honestly in the AC verification table (⚠️ Partial) and the progress update, rather than reporting the AC as fully satisfied.
1679
+
1680
+ ```markdown
1681
+ ### Parallel Group Results
1682
+
1683
+ | Task | Status |
1684
+ |------|--------|
1685
+ | Create types/metrics.ts | ✅ Finished |
1686
+ | Refactor batch-executor | ⚠️ Capped (incomplete — resume next iteration) |
1687
+ ```
1688
+
1660
1689
  ### 4c. Prompt Templates for Sub-Agents
1661
1690
 
1662
1691
  When spawning sub-agents for implementation tasks, use task-specific prompt templates for better results. See [prompt-templates.md](../_shared/references/prompt-templates.md) for the full reference.