sequant 2.10.0 → 2.11.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 (71) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +6 -2
  3. package/dist/bin/cli.js +47 -2
  4. package/dist/src/commands/locks.d.ts +20 -1
  5. package/dist/src/commands/locks.js +206 -4
  6. package/dist/src/commands/ready.d.ts +6 -0
  7. package/dist/src/commands/ready.js +15 -1
  8. package/dist/src/commands/run-display.js +1 -0
  9. package/dist/src/commands/worktree.d.ts +31 -0
  10. package/dist/src/commands/worktree.js +95 -0
  11. package/dist/src/lib/cli-flags.d.ts +23 -0
  12. package/dist/src/lib/cli-flags.js +43 -0
  13. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +2 -0
  14. package/dist/src/lib/cli-ui/run-renderer.js +7 -1
  15. package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
  16. package/dist/src/lib/locks/checkout-lock.js +389 -0
  17. package/dist/src/lib/locks/index.d.ts +6 -3
  18. package/dist/src/lib/locks/index.js +4 -2
  19. package/dist/src/lib/locks/lock-manager.d.ts +81 -1
  20. package/dist/src/lib/locks/lock-manager.js +230 -5
  21. package/dist/src/lib/locks/types.d.ts +72 -0
  22. package/dist/src/lib/locks/types.js +28 -0
  23. package/dist/src/lib/settings.d.ts +73 -0
  24. package/dist/src/lib/settings.js +45 -0
  25. package/dist/src/lib/test-tautology-detector.d.ts +4 -3
  26. package/dist/src/lib/test-tautology-detector.js +101 -41
  27. package/dist/src/lib/workflow/batch-executor.js +78 -19
  28. package/dist/src/lib/workflow/config-resolver.d.ts +25 -0
  29. package/dist/src/lib/workflow/config-resolver.js +89 -0
  30. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +15 -0
  31. package/dist/src/lib/workflow/drivers/claude-code.js +5 -0
  32. package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
  33. package/dist/src/lib/workflow/effort-escalation.js +82 -0
  34. package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
  35. package/dist/src/lib/workflow/error-classifier.js +4 -0
  36. package/dist/src/lib/workflow/log-writer.d.ts +10 -1
  37. package/dist/src/lib/workflow/log-writer.js +20 -0
  38. package/dist/src/lib/workflow/metrics-schema.d.ts +49 -6
  39. package/dist/src/lib/workflow/metrics-schema.js +33 -0
  40. package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
  41. package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
  42. package/dist/src/lib/workflow/phase-detection.js +5 -1
  43. package/dist/src/lib/workflow/phase-executor.js +10 -0
  44. package/dist/src/lib/workflow/ready-gate.d.ts +28 -0
  45. package/dist/src/lib/workflow/ready-gate.js +24 -3
  46. package/dist/src/lib/workflow/run-log-schema.d.ts +55 -0
  47. package/dist/src/lib/workflow/run-log-schema.js +31 -1
  48. package/dist/src/lib/workflow/run-orchestrator.js +27 -0
  49. package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
  50. package/dist/src/lib/workflow/spec-recommendation.js +142 -0
  51. package/dist/src/lib/workflow/types.d.ts +64 -0
  52. package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
  53. package/dist/src/lib/workflow/worktree-manager.js +9 -1
  54. package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
  55. package/dist/src/lib/workflow/worktree-resolver.js +126 -0
  56. package/package.json +3 -2
  57. package/templates/hooks/pre-tool.sh +228 -0
  58. package/templates/scripts/cleanup-worktree.sh +36 -15
  59. package/templates/scripts/new-feature.sh +25 -19
  60. package/templates/skills/_shared/references/subagent-types.md +7 -18
  61. package/templates/skills/assess/SKILL.md +5 -1
  62. package/templates/skills/exec/SKILL.md +61 -7
  63. package/templates/skills/fullsolve/SKILL.md +127 -21
  64. package/templates/skills/loop/SKILL.md +56 -11
  65. package/templates/skills/merger/SKILL.md +98 -10
  66. package/templates/skills/qa/SKILL.md +59 -6
  67. package/templates/skills/release/SKILL.md +79 -0
  68. package/templates/skills/spec/SKILL.md +31 -15
  69. package/templates/skills/spec/references/recommended-workflow.md +14 -1
  70. package/templates/skills/testgen/SKILL.md +23 -6
  71. package/templates/agents/sequant-explorer.md +0 -24
@@ -42,6 +42,33 @@ export const QaVerdictSchema = z.enum([
42
42
  "AC_NOT_MET",
43
43
  "NEEDS_VERIFICATION",
44
44
  ]);
45
+ /**
46
+ * Source that produced the resolved spec→run phase recommendation (#921).
47
+ *
48
+ * Ordered by resolution priority: a durable structured marker in the spec's
49
+ * GitHub comment beats the same comment's prose section, which beats the
50
+ * spec agent's ephemeral chat text, which beats label-based guessing.
51
+ */
52
+ export const SpecRecommendationSourceSchema = z.enum([
53
+ "marker",
54
+ "comment-prose",
55
+ "chat",
56
+ "label-fallback",
57
+ ]);
58
+ /**
59
+ * Resolved spec→run phase recommendation, recorded on the issue log so
60
+ * fallback frequency is auditable (#921 AC-4). Additive/optional — absent on
61
+ * runs that never reached spec resolution (e.g. spec failed) or predate this
62
+ * field, keeping the persisted-log schema stable at `version: 1`.
63
+ */
64
+ export const SpecRecommendationSchema = z.object({
65
+ /** Which step in the resolution chain produced this result */
66
+ source: SpecRecommendationSourceSchema,
67
+ /** Resolved phases, spec excluded (spec already ran) */
68
+ phases: z.array(PhaseSchema),
69
+ /** Whether the quality loop should be enabled */
70
+ qualityLoop: z.boolean(),
71
+ });
45
72
  /**
46
73
  * File diff statistics for a single file (AC-3)
47
74
  */
@@ -82,7 +109,7 @@ export const ErrorContextSchema = z.object({
82
109
  /**
83
110
  * Classified error category (legacy, kept for backwards compatibility).
84
111
  * Keep in sync with `ERROR_CATEGORIES` in `error-classifier.ts` —
85
- * `rate_limit` / `billing` added by #761 AC-6.
112
+ * `rate_limit` / `billing` added by #761 AC-6, `pr_creation` by #920.
86
113
  */
87
114
  category: z.enum([
88
115
  "context_overflow",
@@ -92,6 +119,7 @@ export const ErrorContextSchema = z.object({
92
119
  "timeout",
93
120
  "rate_limit",
94
121
  "billing",
122
+ "pr_creation",
95
123
  "unknown",
96
124
  ]),
97
125
  /** Typed error class name (AC-8), e.g. "ApiError", "BuildError" */
@@ -198,6 +226,8 @@ export const IssueLogSchema = z.object({
198
226
  prNumber: z.number().int().positive().optional(),
199
227
  /** PR URL if created after successful QA */
200
228
  prUrl: z.string().optional(),
229
+ /** How the spec→run phase recommendation was resolved (#921 AC-4) */
230
+ specRecommendation: SpecRecommendationSchema.optional(),
201
231
  });
202
232
  /**
203
233
  * Run configuration
@@ -1207,6 +1207,29 @@ export class RunOrchestrator {
1207
1207
  // issue is the halting one; in parallel runs it is the earliest-listed
1208
1208
  // failure. Undefined on success — the field is omitted from the record.
1209
1209
  const failureCategory = results.find((r) => !r.success && r.failureCategory)?.failureCategory;
1210
+ // #915: one entry per escalated phase execution, from both retry sites —
1211
+ // the outer quality loop (`phaseResults[].escalatedEffort`) and the
1212
+ // `--ready-gate` QA-pass loop (`readyGate.effortEscalations`).
1213
+ const effortEscalations = [
1214
+ ...results.flatMap((r) => r.phaseResults
1215
+ .filter((p) => p.escalatedEffort)
1216
+ .map((p) => ({
1217
+ phase: p.phase,
1218
+ base: p.escalatedEffort.base,
1219
+ escalated: p.escalatedEffort.escalated,
1220
+ }))),
1221
+ ...results.flatMap((r) => r.readyGate?.effortEscalations ?? []),
1222
+ ];
1223
+ // #915: the outer quality loop already logs its own escalations live at
1224
+ // the batch-executor dispatch site — only the `--ready-gate` QA-pass loop
1225
+ // has no equivalent live print, so surface those here.
1226
+ if (config.verbose) {
1227
+ for (const r of results) {
1228
+ for (const e of r.readyGate?.effortEscalations ?? []) {
1229
+ console.log(chalk.gray(` effort: ${e.base} → ${e.escalated} (ready-gate retry, #${r.issueNumber})`));
1230
+ }
1231
+ }
1232
+ }
1210
1233
  await metricsWriter.recordRun({
1211
1234
  issues: issueNumbers,
1212
1235
  phases: Array.from(allPhases),
@@ -1215,6 +1238,10 @@ export class RunOrchestrator {
1215
1238
  model: process.env.ANTHROPIC_MODEL ?? "opus",
1216
1239
  flags: cliFlags,
1217
1240
  failureCategory,
1241
+ // #914: resolved per-phase model/effort, when any phase had one.
1242
+ phasePolicies: config.phasePolicies,
1243
+ // #915: escalated tiers, when any phase execution escalated.
1244
+ effortEscalations,
1218
1245
  metrics: {
1219
1246
  tokensUsed: tokenUsage.tokensUsed,
1220
1247
  filesChanged: totalFilesChanged,
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Structured resolution of the spec→run phase recommendation (#921).
3
+ *
4
+ * `parseRecommendedWorkflow` (phase-mapper.ts) regexes the spec agent's
5
+ * *ephemeral chat text* for a `## Recommended Workflow` section. When the
6
+ * spec agent posts its plan via a body file instead of restating it in
7
+ * chat, that regex has nothing to match and the run silently falls back to
8
+ * label-based phase detection — dropping any recommended phase the label
9
+ * fallback can never produce (e.g. `testgen`). See #814.
10
+ *
11
+ * This module resolves the recommendation through an ordered chain, each
12
+ * step falling through loudly to the next on failure:
13
+ *
14
+ * 1. `marker` — the durable `<!-- SEQUANT_SPEC: {json} --> `
15
+ * comment marker (this issue's fix)
16
+ * 2. `comment-prose` — the same regex as `chat`, applied to the spec
17
+ * plan's GitHub comment body instead of chat text
18
+ * 3. `chat` — `parseRecommendedWorkflow` over the agent's
19
+ * captured chat output (existing behavior)
20
+ * 4. `label-fallback` — `detectPhasesFromLabels` (existing behavior)
21
+ *
22
+ * The durable comment marker is the system's existing idiom — see
23
+ * `SEQUANT_PHASE` in `phase-detection.ts` and the `/assess` HTML markers in
24
+ * `assess-comment-parser.ts`.
25
+ */
26
+ import type { Phase } from "./types.js";
27
+ import { GitHubProvider } from "./platforms/github.js";
28
+ import type { SpecRecommendationSource } from "./run-log-schema.js";
29
+ export interface ResolvedSpecRecommendation {
30
+ phases: Phase[];
31
+ qualityLoop: boolean;
32
+ source: SpecRecommendationSource;
33
+ }
34
+ /**
35
+ * Extract and validate the latest `SEQUANT_SPEC` marker across a set of
36
+ * comment bodies (oldest-to-newest order, matching `gh`'s natural order).
37
+ *
38
+ * Returns `null` when no marker is present at all. Returns `null` and logs a
39
+ * visible warning when the latest marker is malformed JSON, fails schema
40
+ * validation, or names a phase the registry doesn't recognize — per AC-2,
41
+ * an invalid marker falls through to the next resolution step rather than
42
+ * silently dropping the recommendation or silently ignoring the bad phase.
43
+ *
44
+ * Only the *latest* marker is considered (last comment, last match within
45
+ * it) — a re-spec's marker supersedes an earlier one, matching how a human
46
+ * reader would interpret the thread.
47
+ */
48
+ export declare function parseSpecMarker(commentBodies: string[]): {
49
+ phases: Phase[];
50
+ qualityLoop: boolean;
51
+ } | null;
52
+ export interface ResolveSpecRecommendationInput {
53
+ /** The spec agent's captured chat output (existing `chat` fallback). */
54
+ chatOutput: string;
55
+ /** GitHub issue number, used to fetch its comments for the marker/prose steps. */
56
+ issueNumber: number;
57
+ /** Issue labels, used for the final label-based fallback. */
58
+ labels: string[];
59
+ /**
60
+ * Injectable for tests — defaults to a real `GitHubProvider`. Comment
61
+ * fetch failures are already handled by `fetchIssueCommentBodiesSync`
62
+ * (returns `[]`), so the chain degrades to `chat`/`label-fallback`
63
+ * gracefully when GitHub is unreachable.
64
+ */
65
+ githubProvider?: Pick<GitHubProvider, "fetchIssueCommentBodiesSync">;
66
+ }
67
+ /**
68
+ * Resolve the spec→run phase recommendation through the ordered chain:
69
+ * comment-marker → comment-prose → chat-text → label-fallback.
70
+ */
71
+ export declare function resolveSpecRecommendation(input: ResolveSpecRecommendationInput): ResolvedSpecRecommendation;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Structured resolution of the spec→run phase recommendation (#921).
3
+ *
4
+ * `parseRecommendedWorkflow` (phase-mapper.ts) regexes the spec agent's
5
+ * *ephemeral chat text* for a `## Recommended Workflow` section. When the
6
+ * spec agent posts its plan via a body file instead of restating it in
7
+ * chat, that regex has nothing to match and the run silently falls back to
8
+ * label-based phase detection — dropping any recommended phase the label
9
+ * fallback can never produce (e.g. `testgen`). See #814.
10
+ *
11
+ * This module resolves the recommendation through an ordered chain, each
12
+ * step falling through loudly to the next on failure:
13
+ *
14
+ * 1. `marker` — the durable `<!-- SEQUANT_SPEC: {json} --> `
15
+ * comment marker (this issue's fix)
16
+ * 2. `comment-prose` — the same regex as `chat`, applied to the spec
17
+ * plan's GitHub comment body instead of chat text
18
+ * 3. `chat` — `parseRecommendedWorkflow` over the agent's
19
+ * captured chat output (existing behavior)
20
+ * 4. `label-fallback` — `detectPhasesFromLabels` (existing behavior)
21
+ *
22
+ * The durable comment marker is the system's existing idiom — see
23
+ * `SEQUANT_PHASE` in `phase-detection.ts` and the `/assess` HTML markers in
24
+ * `assess-comment-parser.ts`.
25
+ */
26
+ import chalk from "chalk";
27
+ import { z } from "zod";
28
+ import { phaseRegistry } from "./phase-registry.js";
29
+ import { GitHubProvider } from "./platforms/github.js";
30
+ import { stripMarkdownCode } from "./phase-detection.js";
31
+ import { parseRecommendedWorkflow, detectPhasesFromLabels, } from "./phase-mapper.js";
32
+ /** Regex to extract the SEQUANT_SPEC marker JSON from an HTML comment. */
33
+ const SPEC_MARKER_REGEX = /<!-- SEQUANT_SPEC: (\{[^}]+\}) -->/g;
34
+ /**
35
+ * Structural shape of the marker JSON, before phase names are checked
36
+ * against the phase registry. `qualityLoop` defaults to `false` when
37
+ * omitted, matching `parseRecommendedWorkflow`'s prose-parsing default.
38
+ */
39
+ const SpecMarkerJsonSchema = z.object({
40
+ phases: z.array(z.string()).min(1),
41
+ qualityLoop: z.boolean().optional().default(false),
42
+ });
43
+ /**
44
+ * Extract and validate the latest `SEQUANT_SPEC` marker across a set of
45
+ * comment bodies (oldest-to-newest order, matching `gh`'s natural order).
46
+ *
47
+ * Returns `null` when no marker is present at all. Returns `null` and logs a
48
+ * visible warning when the latest marker is malformed JSON, fails schema
49
+ * validation, or names a phase the registry doesn't recognize — per AC-2,
50
+ * an invalid marker falls through to the next resolution step rather than
51
+ * silently dropping the recommendation or silently ignoring the bad phase.
52
+ *
53
+ * Only the *latest* marker is considered (last comment, last match within
54
+ * it) — a re-spec's marker supersedes an earlier one, matching how a human
55
+ * reader would interpret the thread.
56
+ */
57
+ export function parseSpecMarker(commentBodies) {
58
+ let latestRaw = null;
59
+ for (const body of commentBodies) {
60
+ const stripped = stripMarkdownCode(body);
61
+ SPEC_MARKER_REGEX.lastIndex = 0;
62
+ let match;
63
+ while ((match = SPEC_MARKER_REGEX.exec(stripped)) !== null) {
64
+ latestRaw = match[1];
65
+ }
66
+ }
67
+ if (latestRaw === null) {
68
+ return null;
69
+ }
70
+ let parsedJson;
71
+ try {
72
+ parsedJson = JSON.parse(latestRaw);
73
+ }
74
+ catch {
75
+ console.log(chalk.yellow(` ⚠ SEQUANT_SPEC marker is not valid JSON — falling through to comment prose`));
76
+ return null;
77
+ }
78
+ const result = SpecMarkerJsonSchema.safeParse(parsedJson);
79
+ if (!result.success) {
80
+ console.log(chalk.yellow(` ⚠ SEQUANT_SPEC marker failed schema validation — falling through to comment prose`));
81
+ return null;
82
+ }
83
+ const unknownPhases = result.data.phases.filter((name) => !phaseRegistry.has(name));
84
+ if (unknownPhases.length > 0) {
85
+ console.log(chalk.yellow(` ⚠ SEQUANT_SPEC marker names unknown phase(s): ${unknownPhases.join(", ")} — falling through to comment prose`));
86
+ return null;
87
+ }
88
+ return { phases: result.data.phases, qualityLoop: result.data.qualityLoop };
89
+ }
90
+ /**
91
+ * Find the most recently posted comment containing a `## Recommended
92
+ * Workflow` section and parse it with the existing prose regex.
93
+ *
94
+ * Scans newest-to-oldest so a later spec re-run's comment takes precedence
95
+ * over an earlier one — same intuition as the marker step.
96
+ */
97
+ function parseCommentProse(commentBodies) {
98
+ for (let i = commentBodies.length - 1; i >= 0; i--) {
99
+ const parsed = parseRecommendedWorkflow(commentBodies[i]);
100
+ if (parsed) {
101
+ return parsed;
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+ /**
107
+ * Resolve the spec→run phase recommendation through the ordered chain:
108
+ * comment-marker → comment-prose → chat-text → label-fallback.
109
+ */
110
+ export function resolveSpecRecommendation(input) {
111
+ const github = input.githubProvider ?? new GitHubProvider();
112
+ const commentBodies = github.fetchIssueCommentBodiesSync(String(input.issueNumber));
113
+ let phases;
114
+ let qualityLoop;
115
+ let source;
116
+ const marker = parseSpecMarker(commentBodies);
117
+ const prose = marker ? null : parseCommentProse(commentBodies);
118
+ const chat = marker || prose || !input.chatOutput
119
+ ? null
120
+ : parseRecommendedWorkflow(input.chatOutput);
121
+ if (marker) {
122
+ ({ phases, qualityLoop } = marker);
123
+ source = "marker";
124
+ }
125
+ else if (prose) {
126
+ ({ phases, qualityLoop } = prose);
127
+ source = "comment-prose";
128
+ }
129
+ else if (chat) {
130
+ ({ phases, qualityLoop } = chat);
131
+ source = "chat";
132
+ }
133
+ else {
134
+ const fallback = detectPhasesFromLabels(input.labels);
135
+ phases = fallback.phases;
136
+ qualityLoop = fallback.qualityLoop;
137
+ source = "label-fallback";
138
+ }
139
+ // Spec already ran by the time this resolves — never re-include it,
140
+ // regardless of which step in the chain produced the result.
141
+ return { phases: phases.filter((p) => p !== "spec"), qualityLoop, source };
142
+ }
@@ -182,6 +182,28 @@ export interface ExecutionConfig {
182
182
  * see {@link AutoWaitLedger}.
183
183
  */
184
184
  autoWaitMinutes?: number;
185
+ /**
186
+ * Resolved per-phase `model`/`effort` overrides (#914), keyed by phase
187
+ * name. Merged from `settings.run.phases` and the CLI's `--models`/
188
+ * `--efforts` flags via `resolvePhasePolicies` (CLI > settings > absent) —
189
+ * see `config-resolver.ts`. Absent/empty by default: `phase-executor.ts`
190
+ * only sets `AgentExecutionConfig.model`/`.effort` when a phase has an
191
+ * entry here, so an unconfigured run reaches the SDK unchanged.
192
+ */
193
+ phasePolicies?: Record<string, {
194
+ model?: string;
195
+ effort?: string;
196
+ }>;
197
+ /**
198
+ * Evidence-based effort escalation on quality-loop retries (#915). CLI >
199
+ * settings > absent (`false`), resolved by `buildExecutionConfig`
200
+ * (`config-resolver.ts`) and `ready-gate.ts`'s `buildPhaseConfig` — the same
201
+ * two producers `phasePolicies` uses, so this cannot drift from that one
202
+ * (#833 class). Consumed only at dispatch time by
203
+ * `effort-escalation.ts`'s `withEscalatedEffort`, never baked statically
204
+ * into `phasePolicies` here — escalation is per-execution, not per-run.
205
+ */
206
+ effortEscalation?: boolean;
185
207
  }
186
208
  /**
187
209
  * A single liveness notice emitted during an auto-wait (#804 AC-7).
@@ -246,6 +268,16 @@ export interface PhaseResult {
246
268
  stdoutTail?: string[];
247
269
  /** Process exit code from the agent driver (#447) */
248
270
  exitCode?: number;
271
+ /**
272
+ * Set when this execution's effort was escalated one tier above its
273
+ * resolved base (#915) — a quality-loop retry with `effortEscalation`
274
+ * enabled. Additive/optional, same shape as `capped?`/`structuredError?`;
275
+ * absent on every non-escalated execution.
276
+ */
277
+ escalatedEffort?: {
278
+ base: string;
279
+ escalated: string;
280
+ };
249
281
  }
250
282
  /**
251
283
  * Result of executing all phases for an issue
@@ -269,6 +301,15 @@ export interface IssueResult {
269
301
  * with this string as the reason.
270
302
  */
271
303
  prCreationError?: string;
304
+ /**
305
+ * Set when PR creation was skipped because the branch carried zero commits
306
+ * ahead of its base (#920) — a phase-restricted run (e.g. `--phases spec`)
307
+ * that implemented nothing has no deliverable to open a PR for, and
308
+ * attempting one would only fail with GitHub's "No commits between main and
309
+ * …" error. Distinct from {@link IssueResult.prCreationError}: this is not
310
+ * a failure — `success` stays whatever the phases produced.
311
+ */
312
+ prSkippedReason?: string;
272
313
  /**
273
314
  * Set when the issue was skipped because another sequant session holds
274
315
  * the per-issue lock (#625). Surfaced in the summary as
@@ -486,6 +527,29 @@ export interface RunOptions {
486
527
  * and bounds wholesale — no new settings accompany this flag.
487
528
  */
488
529
  readyGate?: boolean;
530
+ /**
531
+ * Per-phase model override (#914). Either a bare value applied to every
532
+ * phase (`--models sonnet`) or a comma list of `phase=model` pairs
533
+ * (`--models spec=fable,exec=sonnet`). Parsed and merged with
534
+ * `settings.run.phases` by `resolvePhasePolicies` (CLI > settings >
535
+ * absent) into `ExecutionConfig.phasePolicies`. Malformed specs fail fast
536
+ * at the Commander option boundary via `cli-flags.ts`.
537
+ */
538
+ models?: string;
539
+ /**
540
+ * Per-phase reasoning-effort override (#914). Same grammar as {@link
541
+ * models} (bare value or comma list of `phase=effort` pairs); each value
542
+ * validates against the SDK's closed `low|medium|high|xhigh|max` enum.
543
+ */
544
+ efforts?: string;
545
+ /**
546
+ * Evidence-based effort escalation on quality-loop retries (#915). Set via
547
+ * `--escalate-effort`. When true, a retried phase execution resolves one
548
+ * effort tier above its configured/inherited base for that execution only
549
+ * — see `effort-escalation.ts`. Default `false`: escalation raises token
550
+ * spend, so an unset flag leaves every run byte-identical to pre-#915.
551
+ */
552
+ escalateEffort?: boolean;
489
553
  }
490
554
  /**
491
555
  * CLI arguments for run command
@@ -104,8 +104,15 @@ export declare function checkWorktreeFreshness(worktreePath: string, verbose: bo
104
104
  export declare function removeStaleWorktree(existingPath: string, branch: string, verbose: boolean): boolean;
105
105
  /**
106
106
  * List all active worktrees with their branches
107
+ *
108
+ * `git worktree list` only ever reports worktrees belonging to the repository
109
+ * containing `cwd`, which is what makes this a repo-scoped lookup: a sibling
110
+ * project's worktree can never appear here, even though `../worktrees/` is a
111
+ * single directory shared by every repo under the same parent (#899).
112
+ *
113
+ * @param cwd - Directory to run git in. Defaults to the current process cwd.
107
114
  */
108
- export declare function listWorktrees(): Array<{
115
+ export declare function listWorktrees(cwd?: string): Array<{
109
116
  path: string;
110
117
  branch: string;
111
118
  issue: number | null;
@@ -205,10 +205,18 @@ export function removeStaleWorktree(existingPath, branch, verbose) {
205
205
  }
206
206
  /**
207
207
  * List all active worktrees with their branches
208
+ *
209
+ * `git worktree list` only ever reports worktrees belonging to the repository
210
+ * containing `cwd`, which is what makes this a repo-scoped lookup: a sibling
211
+ * project's worktree can never appear here, even though `../worktrees/` is a
212
+ * single directory shared by every repo under the same parent (#899).
213
+ *
214
+ * @param cwd - Directory to run git in. Defaults to the current process cwd.
208
215
  */
209
- export function listWorktrees() {
216
+ export function listWorktrees(cwd) {
210
217
  const result = spawnSync("git", ["worktree", "list", "--porcelain"], {
211
218
  stdio: "pipe",
219
+ ...(cwd ? { cwd } : {}),
212
220
  });
213
221
  if (result.status !== 0)
214
222
  return [];
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Repo-scoped worktree resolution and verification (#899).
3
+ *
4
+ * Skill-driven `/fullsolve` used to hand `/exec` an unexpanded glob
5
+ * (`../worktrees/feature/<issue>-` followed by a star and a slash) and `/exec`
6
+ * was instructed to trust it without checking. Two things went wrong:
7
+ *
8
+ * 1. When nothing had created the worktree, the `cd` failed and the agent
9
+ * silently kept working in the main checkout.
10
+ * 2. `../worktrees/` is one flat namespace shared by every repo under the same
11
+ * parent directory, and issue numbers are per-repo — so where the glob *did*
12
+ * match, it could match a sibling project's worktree.
13
+ *
14
+ * Both are fixed the same way: never resolve worktrees through the filesystem.
15
+ * `git worktree list` reports only the worktrees of the repository containing
16
+ * `cwd`, so scoping is structural rather than a filter that can be forgotten,
17
+ * and it reports the *branch*, which is the real identity — a worktree
18
+ * directory slug can drift from its branch after a rename.
19
+ */
20
+ /** Named failure modes for `resolveIssueWorktree`. */
21
+ export type ResolveErrorCode = "WORKTREE_NOT_FOUND" | "WORKTREE_AMBIGUOUS";
22
+ /** Named failure modes for `verifyWorktreePath`. */
23
+ export type VerifyErrorCode = "SEQUANT_WORKTREE_NOT_FOUND" | "SEQUANT_WORKTREE_FOREIGN" | "SEQUANT_WORKTREE_ISSUE_MISMATCH";
24
+ /** A worktree of the current repository, as reported by git. */
25
+ export interface ResolvedWorktree {
26
+ /** Absolute path, exactly as git reports it. */
27
+ path: string;
28
+ /** Branch checked out there, e.g. `feature/899-some-slug`. */
29
+ branch: string;
30
+ }
31
+ export type ResolveResult = ({
32
+ ok: true;
33
+ } & ResolvedWorktree) | {
34
+ ok: false;
35
+ error: ResolveErrorCode;
36
+ message: string;
37
+ /** Populated for WORKTREE_AMBIGUOUS so the caller can report candidates. */
38
+ candidates: ResolvedWorktree[];
39
+ };
40
+ export type VerifyResult = ({
41
+ ok: true;
42
+ } & ResolvedWorktree) | {
43
+ ok: false;
44
+ error: VerifyErrorCode;
45
+ message: string;
46
+ };
47
+ /**
48
+ * Find the worktree of the *current* repository that holds issue `issue`.
49
+ *
50
+ * Selection keys on the branch git reports, never on the directory name, so a
51
+ * worktree whose slug has drifted from its branch still resolves — and a
52
+ * directory that merely looks like a match (a sibling repo's worktree, or a
53
+ * stray directory) never does.
54
+ *
55
+ * @param issue - Issue number to look up.
56
+ * @param cwd - Directory inside the repository to search. Defaults to process cwd.
57
+ */
58
+ export declare function resolveIssueWorktree(issue: number, cwd?: string): ResolveResult;
59
+ /**
60
+ * Verify that a caller-supplied worktree path is safe to work in.
61
+ *
62
+ * This is the guard `/exec` runs before trusting `SEQUANT_WORKTREE`. It fails
63
+ * closed on every uncertain case — a bad path must halt the run, never degrade
64
+ * into "keep going in whatever directory we happen to be in".
65
+ *
66
+ * @param worktreePath - The path to check (typically `$SEQUANT_WORKTREE`).
67
+ * @param options.issue - When set, the branch must belong to this issue.
68
+ * @param options.cwd - Directory inside the repository to check against.
69
+ */
70
+ export declare function verifyWorktreePath(worktreePath: string, options?: {
71
+ issue?: number;
72
+ cwd?: string;
73
+ }): VerifyResult;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Repo-scoped worktree resolution and verification (#899).
3
+ *
4
+ * Skill-driven `/fullsolve` used to hand `/exec` an unexpanded glob
5
+ * (`../worktrees/feature/<issue>-` followed by a star and a slash) and `/exec`
6
+ * was instructed to trust it without checking. Two things went wrong:
7
+ *
8
+ * 1. When nothing had created the worktree, the `cd` failed and the agent
9
+ * silently kept working in the main checkout.
10
+ * 2. `../worktrees/` is one flat namespace shared by every repo under the same
11
+ * parent directory, and issue numbers are per-repo — so where the glob *did*
12
+ * match, it could match a sibling project's worktree.
13
+ *
14
+ * Both are fixed the same way: never resolve worktrees through the filesystem.
15
+ * `git worktree list` reports only the worktrees of the repository containing
16
+ * `cwd`, so scoping is structural rather than a filter that can be forgotten,
17
+ * and it reports the *branch*, which is the real identity — a worktree
18
+ * directory slug can drift from its branch after a rename.
19
+ */
20
+ import { existsSync, realpathSync, statSync } from "fs";
21
+ import path from "path";
22
+ import { listWorktrees } from "./worktree-manager.js";
23
+ /**
24
+ * Find the worktree of the *current* repository that holds issue `issue`.
25
+ *
26
+ * Selection keys on the branch git reports, never on the directory name, so a
27
+ * worktree whose slug has drifted from its branch still resolves — and a
28
+ * directory that merely looks like a match (a sibling repo's worktree, or a
29
+ * stray directory) never does.
30
+ *
31
+ * @param issue - Issue number to look up.
32
+ * @param cwd - Directory inside the repository to search. Defaults to process cwd.
33
+ */
34
+ export function resolveIssueWorktree(issue, cwd) {
35
+ const matches = listWorktrees(cwd)
36
+ .filter((w) => w.issue === issue)
37
+ .map(({ path: p, branch }) => ({ path: p, branch }));
38
+ if (matches.length === 0) {
39
+ return {
40
+ ok: false,
41
+ error: "WORKTREE_NOT_FOUND",
42
+ message: `WORKTREE_NOT_FOUND: no worktree of this repository has a branch for issue #${issue}. ` +
43
+ `Create one with \`./scripts/new-feature.sh ${issue}\`.`,
44
+ candidates: [],
45
+ };
46
+ }
47
+ if (matches.length > 1) {
48
+ const list = matches.map((m) => `${m.branch} -> ${m.path}`).join("; ");
49
+ return {
50
+ ok: false,
51
+ error: "WORKTREE_AMBIGUOUS",
52
+ message: `WORKTREE_AMBIGUOUS: ${matches.length} worktrees claim issue #${issue} (${list}). ` +
53
+ `Remove the stale one, or set SEQUANT_WORKTREE explicitly.`,
54
+ candidates: matches,
55
+ };
56
+ }
57
+ return { ok: true, ...matches[0] };
58
+ }
59
+ /**
60
+ * Canonicalize a path for comparison, tolerating paths that do not exist.
61
+ *
62
+ * `realpathSync` throws on a missing path, so fall back to resolving the
63
+ * deepest existing ancestor and re-appending the remainder — enough to make
64
+ * `/tmp/...` and `/private/tmp/...` compare equal on macOS.
65
+ */
66
+ function canonicalize(target) {
67
+ const absolute = path.resolve(target);
68
+ try {
69
+ return realpathSync(absolute);
70
+ }
71
+ catch {
72
+ const parent = path.dirname(absolute);
73
+ if (parent === absolute)
74
+ return absolute;
75
+ return path.join(canonicalize(parent), path.basename(absolute));
76
+ }
77
+ }
78
+ /**
79
+ * Verify that a caller-supplied worktree path is safe to work in.
80
+ *
81
+ * This is the guard `/exec` runs before trusting `SEQUANT_WORKTREE`. It fails
82
+ * closed on every uncertain case — a bad path must halt the run, never degrade
83
+ * into "keep going in whatever directory we happen to be in".
84
+ *
85
+ * @param worktreePath - The path to check (typically `$SEQUANT_WORKTREE`).
86
+ * @param options.issue - When set, the branch must belong to this issue.
87
+ * @param options.cwd - Directory inside the repository to check against.
88
+ */
89
+ export function verifyWorktreePath(worktreePath, options = {}) {
90
+ const raw = worktreePath.trim();
91
+ if (raw.length === 0 || raw.includes("*")) {
92
+ return {
93
+ ok: false,
94
+ error: "SEQUANT_WORKTREE_NOT_FOUND",
95
+ message: `SEQUANT_WORKTREE_NOT_FOUND: ${raw.length === 0 ? "empty path" : `unexpanded glob "${raw}"`}. Expected a resolved absolute directory.`,
96
+ };
97
+ }
98
+ const absolute = path.resolve(raw);
99
+ if (!existsSync(absolute) || !statSync(absolute).isDirectory()) {
100
+ return {
101
+ ok: false,
102
+ error: "SEQUANT_WORKTREE_NOT_FOUND",
103
+ message: `SEQUANT_WORKTREE_NOT_FOUND: "${raw}" is not an existing directory.`,
104
+ };
105
+ }
106
+ const canonical = canonicalize(absolute);
107
+ const known = listWorktrees(options.cwd);
108
+ const match = known.find((w) => canonicalize(w.path) === canonical);
109
+ if (!match) {
110
+ return {
111
+ ok: false,
112
+ error: "SEQUANT_WORKTREE_FOREIGN",
113
+ message: `SEQUANT_WORKTREE_FOREIGN: "${raw}" is not a worktree of this repository. ` +
114
+ `It belongs to another project or is stale; \`../worktrees/\` is shared across repos.`,
115
+ };
116
+ }
117
+ if (options.issue !== undefined && match.issue !== options.issue) {
118
+ return {
119
+ ok: false,
120
+ error: "SEQUANT_WORKTREE_ISSUE_MISMATCH",
121
+ message: `SEQUANT_WORKTREE_ISSUE_MISMATCH: "${raw}" has branch "${match.branch}", ` +
122
+ `which is not issue #${options.issue}.`,
123
+ };
124
+ }
125
+ return { ok: true, path: match.path, branch: match.branch };
126
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sequant",
3
- "version": "2.10.0",
3
+ "version": "2.11.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": {
@@ -24,10 +24,11 @@
24
24
  "build": "rm -rf dist/src dist/bin dist/dashboard && tsc && chmod +x dist/bin/cli.js",
25
25
  "dev": "tsx bin/cli.ts",
26
26
  "test": "vitest run",
27
- "lint": "eslint src/ bin/ --max-warnings 0",
27
+ "lint": "eslint src/ bin/ scripts/ --max-warnings 0",
28
28
  "sync:skills": "npx tsx scripts/check-skill-sync.ts --fix",
29
29
  "sync:hooks": "bash scripts/sync-hooks.sh",
30
30
  "validate:skills": "for skill in templates/skills/*/; do case \"$skill\" in *_shared*|*/references/*) continue;; esac; npx skills-ref validate \"$skill\"; done",
31
+ "typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json",
31
32
  "lint:skill-calls": "npx tsx scripts/lint-skill-calls.ts",
32
33
  "lint:skill-gates": "npx tsx scripts/lint-skill-gates.ts",
33
34
  "lint:skill-sync": "npx tsx scripts/check-skill-sync.ts",