sequant 2.10.0 → 2.12.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 (108) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +19 -2
  4. package/dist/bin/cli.js +47 -2
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
  7. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +331 -12
  8. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +7 -18
  9. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +5 -1
  10. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +62 -8
  11. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +187 -28
  12. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +127 -23
  13. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +130 -13
  14. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +306 -8
  15. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +79 -0
  16. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +40 -20
  17. package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +14 -1
  18. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  19. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +23 -6
  20. package/dist/src/commands/doctor.js +20 -18
  21. package/dist/src/commands/locks.d.ts +20 -1
  22. package/dist/src/commands/locks.js +206 -4
  23. package/dist/src/commands/ready.d.ts +6 -0
  24. package/dist/src/commands/ready.js +19 -1
  25. package/dist/src/commands/run-display.js +1 -0
  26. package/dist/src/commands/worktree.d.ts +31 -0
  27. package/dist/src/commands/worktree.js +95 -0
  28. package/dist/src/lib/ac-linter.js +26 -0
  29. package/dist/src/lib/ac-parser.d.ts +40 -0
  30. package/dist/src/lib/ac-parser.js +202 -16
  31. package/dist/src/lib/cli-flags.d.ts +23 -0
  32. package/dist/src/lib/cli-flags.js +43 -0
  33. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +2 -0
  34. package/dist/src/lib/cli-ui/run-renderer.js +7 -1
  35. package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
  36. package/dist/src/lib/locks/checkout-lock.js +389 -0
  37. package/dist/src/lib/locks/index.d.ts +6 -3
  38. package/dist/src/lib/locks/index.js +4 -2
  39. package/dist/src/lib/locks/lock-manager.d.ts +81 -1
  40. package/dist/src/lib/locks/lock-manager.js +230 -5
  41. package/dist/src/lib/locks/types.d.ts +72 -0
  42. package/dist/src/lib/locks/types.js +28 -0
  43. package/dist/src/lib/markdown-fence.d.ts +24 -0
  44. package/dist/src/lib/markdown-fence.js +51 -0
  45. package/dist/src/lib/mcp-config.d.ts +24 -0
  46. package/dist/src/lib/mcp-config.js +51 -0
  47. package/dist/src/lib/scope/analyzer.d.ts +4 -0
  48. package/dist/src/lib/scope/analyzer.js +7 -1
  49. package/dist/src/lib/settings.d.ts +111 -1
  50. package/dist/src/lib/settings.js +59 -0
  51. package/dist/src/lib/system.d.ts +7 -3
  52. package/dist/src/lib/system.js +7 -3
  53. package/dist/src/lib/test-tautology-detector.d.ts +4 -3
  54. package/dist/src/lib/test-tautology-detector.js +147 -40
  55. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  56. package/dist/src/lib/workflow/batch-executor.js +154 -23
  57. package/dist/src/lib/workflow/config-resolver.d.ts +25 -0
  58. package/dist/src/lib/workflow/config-resolver.js +90 -0
  59. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
  60. package/dist/src/lib/workflow/drivers/claude-code.js +14 -3
  61. package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
  62. package/dist/src/lib/workflow/effort-escalation.js +82 -0
  63. package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
  64. package/dist/src/lib/workflow/error-classifier.js +4 -0
  65. package/dist/src/lib/workflow/log-writer.d.ts +10 -1
  66. package/dist/src/lib/workflow/log-writer.js +20 -0
  67. package/dist/src/lib/workflow/metrics-schema.d.ts +49 -6
  68. package/dist/src/lib/workflow/metrics-schema.js +33 -0
  69. package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
  70. package/dist/src/lib/workflow/mutation-marker.d.ts +86 -0
  71. package/dist/src/lib/workflow/mutation-marker.js +97 -0
  72. package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
  73. package/dist/src/lib/workflow/phase-detection.js +5 -1
  74. package/dist/src/lib/workflow/phase-executor.d.ts +17 -0
  75. package/dist/src/lib/workflow/phase-executor.js +60 -4
  76. package/dist/src/lib/workflow/qa-gaps-marker.d.ts +38 -0
  77. package/dist/src/lib/workflow/qa-gaps-marker.js +66 -0
  78. package/dist/src/lib/workflow/ready-gate.d.ts +53 -1
  79. package/dist/src/lib/workflow/ready-gate.js +105 -14
  80. package/dist/src/lib/workflow/run-log-schema.d.ts +175 -0
  81. package/dist/src/lib/workflow/run-log-schema.js +71 -1
  82. package/dist/src/lib/workflow/run-orchestrator.js +27 -0
  83. package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
  84. package/dist/src/lib/workflow/spec-recommendation.js +142 -0
  85. package/dist/src/lib/workflow/state-schema.d.ts +5 -1
  86. package/dist/src/lib/workflow/state-schema.js +8 -1
  87. package/dist/src/lib/workflow/types.d.ts +78 -0
  88. package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
  89. package/dist/src/lib/workflow/worktree-manager.js +9 -1
  90. package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
  91. package/dist/src/lib/workflow/worktree-resolver.js +126 -0
  92. package/package.json +4 -3
  93. package/templates/hooks/pre-tool.sh +331 -12
  94. package/templates/scripts/cleanup-worktree.sh +36 -15
  95. package/templates/scripts/new-feature.sh +25 -19
  96. package/templates/skills/_shared/references/subagent-types.md +7 -18
  97. package/templates/skills/assess/SKILL.md +5 -1
  98. package/templates/skills/exec/SKILL.md +62 -8
  99. package/templates/skills/fullsolve/SKILL.md +187 -28
  100. package/templates/skills/loop/SKILL.md +127 -23
  101. package/templates/skills/merger/SKILL.md +130 -13
  102. package/templates/skills/qa/SKILL.md +306 -8
  103. package/templates/skills/release/SKILL.md +79 -0
  104. package/templates/skills/spec/SKILL.md +40 -20
  105. package/templates/skills/spec/references/recommended-workflow.md +14 -1
  106. package/templates/skills/test/SKILL.md +1 -1
  107. package/templates/skills/testgen/SKILL.md +23 -6
  108. package/templates/agents/sequant-explorer.md +0 -24
@@ -11,9 +11,12 @@ import { SequantError } from "../errors.js";
11
11
  * `rate_limit` / `billing` (#761 AC-6) only arise from the driver's structured
12
12
  * errors — `classifyError` never produces them, since stderr text cannot
13
13
  * distinguish a window-exhausted limit from a transient 429 (`api_error`).
14
+ * `pr_creation` (#920) is assigned only at the `deriveFailureCategory` call
15
+ * site, not by `classifyError` — a failed `createPR` has no failing phase to
16
+ * classify, so it would otherwise leave `failureCategory` `undefined`.
14
17
  * Keep in sync with the inline category enum in `run-log-schema.ts`.
15
18
  */
16
- export declare const ERROR_CATEGORIES: readonly ["context_overflow", "api_error", "hook_failure", "build_error", "timeout", "rate_limit", "billing", "unknown"];
19
+ export declare const ERROR_CATEGORIES: readonly ["context_overflow", "api_error", "hook_failure", "build_error", "timeout", "rate_limit", "billing", "pr_creation", "unknown"];
17
20
  export type ErrorCategory = (typeof ERROR_CATEGORIES)[number];
18
21
  /**
19
22
  * Map from error type name to legacy category string.
@@ -11,6 +11,9 @@ import { ContextOverflowError, ApiError, HookFailureError, BuildError, TimeoutEr
11
11
  * `rate_limit` / `billing` (#761 AC-6) only arise from the driver's structured
12
12
  * errors — `classifyError` never produces them, since stderr text cannot
13
13
  * distinguish a window-exhausted limit from a transient 429 (`api_error`).
14
+ * `pr_creation` (#920) is assigned only at the `deriveFailureCategory` call
15
+ * site, not by `classifyError` — a failed `createPR` has no failing phase to
16
+ * classify, so it would otherwise leave `failureCategory` `undefined`.
14
17
  * Keep in sync with the inline category enum in `run-log-schema.ts`.
15
18
  */
16
19
  export const ERROR_CATEGORIES = [
@@ -21,6 +24,7 @@ export const ERROR_CATEGORIES = [
21
24
  "timeout",
22
25
  "rate_limit",
23
26
  "billing",
27
+ "pr_creation",
24
28
  "unknown",
25
29
  ];
26
30
  /**
@@ -13,7 +13,7 @@
13
13
  * await writer.finalize();
14
14
  * ```
15
15
  */
16
- import { type RunLog, type RunConfig, type PhaseLog, type Phase } from "./run-log-schema.js";
16
+ import { type RunLog, type RunConfig, type PhaseLog, type Phase, type SpecRecommendation } from "./run-log-schema.js";
17
17
  import { type RotationSettings } from "./log-rotation.js";
18
18
  export interface LogWriterOptions {
19
19
  /** Path to log directory (default: .sequant/logs in current directory) */
@@ -82,6 +82,15 @@ export declare class LogWriter {
82
82
  * Set PR info on the current issue (call before completeIssue)
83
83
  */
84
84
  setPRInfo(prNumber: number, prUrl: string, issueNumber?: number): void;
85
+ /**
86
+ * Record how the spec→run phase recommendation was resolved (#921 AC-4).
87
+ *
88
+ * Called right after `resolveSpecRecommendation` runs, before the next
89
+ * phase starts — mirrors {@link setPRInfo}'s post-hoc-setter shape because
90
+ * the spec `PhaseLog` is already written by the time resolution (which does
91
+ * its own comment-fetch I/O) completes.
92
+ */
93
+ setSpecRecommendation(recommendation: SpecRecommendation, issueNumber?: number): void;
85
94
  /**
86
95
  * Force the in-flight issue's status to `failure` (#879).
87
96
  *
@@ -134,6 +134,23 @@ export class LogWriter {
134
134
  issue.prNumber = prNumber;
135
135
  issue.prUrl = prUrl;
136
136
  }
137
+ /**
138
+ * Record how the spec→run phase recommendation was resolved (#921 AC-4).
139
+ *
140
+ * Called right after `resolveSpecRecommendation` runs, before the next
141
+ * phase starts — mirrors {@link setPRInfo}'s post-hoc-setter shape because
142
+ * the spec `PhaseLog` is already written by the time resolution (which does
143
+ * its own comment-fetch I/O) completes.
144
+ */
145
+ setSpecRecommendation(recommendation, issueNumber) {
146
+ const issue = issueNumber
147
+ ? (this.activeIssues.get(issueNumber) ?? this.currentIssue)
148
+ : this.currentIssue;
149
+ if (!issue) {
150
+ return;
151
+ }
152
+ issue.specRecommendation = recommendation;
153
+ }
137
154
  /**
138
155
  * Force the in-flight issue's status to `failure` (#879).
139
156
  *
@@ -209,6 +226,9 @@ export class LogWriter {
209
226
  ...(issue.prUrl != null && {
210
227
  prUrl: issue.prUrl,
211
228
  }),
229
+ ...(issue.specRecommendation != null && {
230
+ specRecommendation: issue.specRecommendation,
231
+ }),
212
232
  };
213
233
  this.runLog.issues.push(issueLog);
214
234
  // Clean up from activeIssues map
@@ -41,6 +41,7 @@ export declare const FailureCategorySchema: z.ZodEnum<{
41
41
  build_error: "build_error";
42
42
  rate_limit: "rate_limit";
43
43
  billing: "billing";
44
+ pr_creation: "pr_creation";
44
45
  }>;
45
46
  export type FailureCategory = z.infer<typeof FailureCategorySchema>;
46
47
  /**
@@ -48,12 +49,12 @@ export type FailureCategory = z.infer<typeof FailureCategorySchema>;
48
49
  */
49
50
  export declare const MetricPhaseSchema: z.ZodEnum<{
50
51
  exec: "exec";
51
- qa: "qa";
52
- loop: "loop";
53
52
  spec: "spec";
54
53
  "security-review": "security-review";
55
54
  testgen: "testgen";
56
55
  test: "test";
56
+ qa: "qa";
57
+ loop: "loop";
57
58
  }>;
58
59
  export type MetricPhase = z.infer<typeof MetricPhaseSchema>;
59
60
  /**
@@ -86,12 +87,12 @@ export declare const MetricRunSchema: z.ZodObject<{
86
87
  issues: z.ZodArray<z.ZodNumber>;
87
88
  phases: z.ZodArray<z.ZodEnum<{
88
89
  exec: "exec";
89
- qa: "qa";
90
- loop: "loop";
91
90
  spec: "spec";
92
91
  "security-review": "security-review";
93
92
  testgen: "testgen";
94
93
  test: "test";
94
+ qa: "qa";
95
+ loop: "loop";
95
96
  }>>;
96
97
  outcome: z.ZodEnum<{
97
98
  success: "success";
@@ -110,7 +111,17 @@ export declare const MetricRunSchema: z.ZodObject<{
110
111
  build_error: "build_error";
111
112
  rate_limit: "rate_limit";
112
113
  billing: "billing";
114
+ pr_creation: "pr_creation";
113
115
  }>>;
116
+ phasePolicies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
117
+ model: z.ZodOptional<z.ZodString>;
118
+ effort: z.ZodOptional<z.ZodString>;
119
+ }, z.core.$strip>>>;
120
+ effortEscalations: z.ZodOptional<z.ZodArray<z.ZodObject<{
121
+ phase: z.ZodString;
122
+ base: z.ZodString;
123
+ escalated: z.ZodString;
124
+ }, z.core.$strip>>>;
114
125
  metrics: z.ZodObject<{
115
126
  tokensUsed: z.ZodNumber;
116
127
  filesChanged: z.ZodNumber;
@@ -136,12 +147,12 @@ export declare const MetricsSchema: z.ZodObject<{
136
147
  issues: z.ZodArray<z.ZodNumber>;
137
148
  phases: z.ZodArray<z.ZodEnum<{
138
149
  exec: "exec";
139
- qa: "qa";
140
- loop: "loop";
141
150
  spec: "spec";
142
151
  "security-review": "security-review";
143
152
  testgen: "testgen";
144
153
  test: "test";
154
+ qa: "qa";
155
+ loop: "loop";
145
156
  }>>;
146
157
  outcome: z.ZodEnum<{
147
158
  success: "success";
@@ -160,7 +171,17 @@ export declare const MetricsSchema: z.ZodObject<{
160
171
  build_error: "build_error";
161
172
  rate_limit: "rate_limit";
162
173
  billing: "billing";
174
+ pr_creation: "pr_creation";
163
175
  }>>;
176
+ phasePolicies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
177
+ model: z.ZodOptional<z.ZodString>;
178
+ effort: z.ZodOptional<z.ZodString>;
179
+ }, z.core.$strip>>>;
180
+ effortEscalations: z.ZodOptional<z.ZodArray<z.ZodObject<{
181
+ phase: z.ZodString;
182
+ base: z.ZodString;
183
+ escalated: z.ZodString;
184
+ }, z.core.$strip>>>;
164
185
  metrics: z.ZodObject<{
165
186
  tokensUsed: z.ZodNumber;
166
187
  filesChanged: z.ZodNumber;
@@ -193,6 +214,28 @@ export declare function createMetricRun(options: {
193
214
  model?: string;
194
215
  flags?: string[];
195
216
  failureCategory?: FailureCategory;
217
+ /**
218
+ * Resolved per-phase model/effort overrides (#914), keyed by phase name.
219
+ * Pass only the phases that actually had a configured override — a phase
220
+ * that inherited the CLI default should not appear here at all. See
221
+ * `resolvePhasePolicies` in `config-resolver.ts`, which already produces
222
+ * a map shaped this way.
223
+ */
224
+ phasePolicies?: Record<string, {
225
+ model?: string;
226
+ effort?: string;
227
+ }>;
228
+ /**
229
+ * Effort escalations applied during this run (#915), one entry per
230
+ * escalated phase execution. Pass only executions that actually escalated
231
+ * — see `MetricRunSchema.effortEscalations`'s doc comment for why this is
232
+ * a sibling array rather than an extension of `phasePolicies`.
233
+ */
234
+ effortEscalations?: Array<{
235
+ phase: string;
236
+ base: string;
237
+ escalated: string;
238
+ }>;
196
239
  metrics?: Partial<RunMetrics>;
197
240
  }): MetricRun;
198
241
  /**
@@ -96,6 +96,33 @@ export const MetricRunSchema = z.object({
96
96
  * existed (additive — no `version` bump required).
97
97
  */
98
98
  failureCategory: FailureCategorySchema.optional(),
99
+ /**
100
+ * Resolved per-phase `model`/`effort` overrides (#914), keyed by phase
101
+ * name. Only phases with a configured override get an entry — a phase
102
+ * that inherited the CLI default is omitted entirely, not recorded with
103
+ * undefined fields. Enum/alias strings only, consistent with this
104
+ * schema's no-file-paths/no-content privacy contract. Optional and
105
+ * additive — absent on records written before this field existed.
106
+ */
107
+ phasePolicies: z
108
+ .record(z.string(), z.object({ model: z.string().optional(), effort: z.string().optional() }))
109
+ .optional(),
110
+ /**
111
+ * Effort escalations applied during this run (#915), one entry per
112
+ * escalated phase execution — distinct from `phasePolicies`, which is a
113
+ * flat phase→policy map recorded once per run and can't express a value
114
+ * that changes per retry. Only populated when at least one execution
115
+ * escalated; omitted entirely (not an empty array) otherwise, matching
116
+ * `phasePolicies`'s omit-when-empty convention. Phase names and enum
117
+ * effort strings only, consistent with this schema's privacy contract.
118
+ */
119
+ effortEscalations: z
120
+ .array(z.object({
121
+ phase: z.string(),
122
+ base: z.string(),
123
+ escalated: z.string(),
124
+ }))
125
+ .optional(),
99
126
  /** Aggregate metrics */
100
127
  metrics: RunMetricsSchema,
101
128
  });
@@ -137,6 +164,12 @@ export function createMetricRun(options) {
137
164
  model: options.model ?? "unknown",
138
165
  flags: options.flags ?? [],
139
166
  failureCategory: options.failureCategory,
167
+ ...(options.phasePolicies && Object.keys(options.phasePolicies).length > 0
168
+ ? { phasePolicies: options.phasePolicies }
169
+ : {}),
170
+ ...(options.effortEscalations && options.effortEscalations.length > 0
171
+ ? { effortEscalations: options.effortEscalations }
172
+ : {}),
140
173
  metrics: {
141
174
  tokensUsed: options.metrics?.tokensUsed ?? 0,
142
175
  filesChanged: options.metrics?.filesChanged ?? 0,
@@ -70,6 +70,17 @@ export declare class MetricsWriter {
70
70
  model?: string;
71
71
  flags?: string[];
72
72
  failureCategory?: FailureCategory;
73
+ /** Resolved per-phase model/effort overrides (#914). See `createMetricRun`. */
74
+ phasePolicies?: Record<string, {
75
+ model?: string;
76
+ effort?: string;
77
+ }>;
78
+ /** Effort escalations applied during this run (#915). See `createMetricRun`. */
79
+ effortEscalations?: Array<{
80
+ phase: string;
81
+ base: string;
82
+ escalated: string;
83
+ }>;
73
84
  metrics?: Partial<RunMetrics>;
74
85
  }): Promise<MetricRun>;
75
86
  /**
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Parseable mutation-verification markers for `/qa` §6i (#939).
3
+ *
4
+ * CLAUDE.md's testing rule ("Gate tests ship with a recorded mutation
5
+ * result") was honor-system prose — nothing parsed or checked the recorded
6
+ * result, so compliance was invisible (see #830, #834's "prose only, and
7
+ * therefore unenforceable" defect class). This module promotes that record
8
+ * to a parseable PR-body marker:
9
+ *
10
+ * ```
11
+ * <!-- SEQUANT_MUTATION: {"ac":"AC-3","mutation":"removed payload fixture
12
+ * block","failedTest":"injection.test.ts > rejects payload"} -->
13
+ * ```
14
+ *
15
+ * Same durable-marker idiom as `SEQUANT_SPEC` (spec-recommendation.ts) and
16
+ * `SEQUANT_PHASE` (phase-detection.ts): a flat-JSON HTML comment, matched
17
+ * with the `{[^}]+}` regex family. That regex stops at the FIRST `}`, so the
18
+ * payload must stay flat — a nested object would truncate mid-JSON and fail
19
+ * to parse. Unlike those two markers (one recommendation / one status per
20
+ * comment, latest-wins), a PR body carries one `SEQUANT_MUTATION` marker per
21
+ * AC, so all markers are collected, not just the latest.
22
+ */
23
+ import { z } from "zod";
24
+ declare const MutationMarkerSchema: z.ZodObject<{
25
+ ac: z.ZodString;
26
+ mutation: z.ZodString;
27
+ failedTest: z.ZodString;
28
+ }, z.core.$strip>;
29
+ export type MutationMarker = z.infer<typeof MutationMarkerSchema>;
30
+ export type MutationMarkerClassification = "valid" | "test_not_in_diff";
31
+ export interface ClassifiedMutationMarker extends MutationMarker {
32
+ classification: MutationMarkerClassification;
33
+ }
34
+ /**
35
+ * Parse every `SEQUANT_MUTATION` marker from a PR body.
36
+ *
37
+ * Markers inside fenced code blocks or inline code (e.g. a doc example
38
+ * showing the marker format) are ignored, matching `parsePhaseMarkers`.
39
+ * Malformed JSON or schema-invalid entries are skipped silently rather than
40
+ * thrown — one bad marker must not take down the rest of the PR body's
41
+ * markers.
42
+ *
43
+ * Called with just `prBody`, this only parses. Given a second argument —
44
+ * the diff's test-file paths — it also **rejects** markers naming a test
45
+ * absent from the diff, attaching a `classification` to each result rather
46
+ * than silently dropping the rejected ones (a fabricated marker is worse
47
+ * than a missing one; it must be visible, not discarded). The two-argument
48
+ * form is what makes this function itself the rejecting entry point AC-3
49
+ * names, while the one-argument form stays free of a git-diff dependency
50
+ * for parser-only tests.
51
+ *
52
+ * @param prBody - The full PR body text
53
+ * @param diffTestFiles - When provided, test file paths present in the PR's
54
+ * diff; each returned marker is classified against them
55
+ * @returns Every valid marker found, in document order — classified when
56
+ * `diffTestFiles` is provided
57
+ */
58
+ export declare function parseMutationMarkers(prBody: string): MutationMarker[];
59
+ export declare function parseMutationMarkers(prBody: string, diffTestFiles: string[]): ClassifiedMutationMarker[];
60
+ /**
61
+ * Reduce a marker list to one-per-AC, the later marker winning.
62
+ *
63
+ * A re-run's marker (e.g. after amending the PR body) supersedes the
64
+ * earlier one for the same AC — the same "latest wins" idiom
65
+ * `resolveSpecRecommendation` uses for `SEQUANT_SPEC`.
66
+ *
67
+ * @param markers - Markers as returned by {@link parseMutationMarkers}
68
+ * @returns Map keyed by AC id, one marker per key
69
+ */
70
+ export declare function latestMutationMarkerPerAc(markers: MutationMarker[]): Map<string, MutationMarker>;
71
+ /**
72
+ * Classify a marker's `failedTest` against the diff's actual test files.
73
+ *
74
+ * A fabricated marker (naming a test that doesn't exist in the diff) is
75
+ * worse than a missing one — it claims verification that never happened.
76
+ * `failedTest` follows the `<file> > <test name>` shape `/qa` §6i renders
77
+ * in its output table (e.g. `injection.test.ts > rejects payload`); only
78
+ * the file segment is checked, since the suite/test-name portion after
79
+ * ` > ` isn't independently verifiable without executing the test.
80
+ *
81
+ * @param marker - A single parsed marker
82
+ * @param diffTestFiles - Test file paths present in the PR's diff
83
+ * @returns `"valid"` when the named file is in the diff, `"test_not_in_diff"` otherwise
84
+ */
85
+ export declare function classifyMutationMarker(marker: MutationMarker, diffTestFiles: string[]): MutationMarkerClassification;
86
+ export {};
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Parseable mutation-verification markers for `/qa` §6i (#939).
3
+ *
4
+ * CLAUDE.md's testing rule ("Gate tests ship with a recorded mutation
5
+ * result") was honor-system prose — nothing parsed or checked the recorded
6
+ * result, so compliance was invisible (see #830, #834's "prose only, and
7
+ * therefore unenforceable" defect class). This module promotes that record
8
+ * to a parseable PR-body marker:
9
+ *
10
+ * ```
11
+ * <!-- SEQUANT_MUTATION: {"ac":"AC-3","mutation":"removed payload fixture
12
+ * block","failedTest":"injection.test.ts > rejects payload"} -->
13
+ * ```
14
+ *
15
+ * Same durable-marker idiom as `SEQUANT_SPEC` (spec-recommendation.ts) and
16
+ * `SEQUANT_PHASE` (phase-detection.ts): a flat-JSON HTML comment, matched
17
+ * with the `{[^}]+}` regex family. That regex stops at the FIRST `}`, so the
18
+ * payload must stay flat — a nested object would truncate mid-JSON and fail
19
+ * to parse. Unlike those two markers (one recommendation / one status per
20
+ * comment, latest-wins), a PR body carries one `SEQUANT_MUTATION` marker per
21
+ * AC, so all markers are collected, not just the latest.
22
+ */
23
+ import { z } from "zod";
24
+ import { stripMarkdownCode } from "./phase-detection.js";
25
+ /** Regex to extract mutation-verification marker JSON from HTML comments. */
26
+ const MUTATION_MARKER_REGEX = /<!-- SEQUANT_MUTATION: (\{[^}]+\}) -->/g;
27
+ const MutationMarkerSchema = z.object({
28
+ ac: z.string().min(1),
29
+ mutation: z.string().min(1),
30
+ failedTest: z.string().min(1),
31
+ });
32
+ function parseMarkersRaw(prBody) {
33
+ const markers = [];
34
+ const stripped = stripMarkdownCode(prBody);
35
+ MUTATION_MARKER_REGEX.lastIndex = 0;
36
+ let match;
37
+ while ((match = MUTATION_MARKER_REGEX.exec(stripped)) !== null) {
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(match[1]);
41
+ }
42
+ catch {
43
+ continue;
44
+ }
45
+ const result = MutationMarkerSchema.safeParse(parsed);
46
+ if (result.success) {
47
+ markers.push(result.data);
48
+ }
49
+ }
50
+ return markers;
51
+ }
52
+ export function parseMutationMarkers(prBody, diffTestFiles) {
53
+ const markers = parseMarkersRaw(prBody);
54
+ if (diffTestFiles === undefined) {
55
+ return markers;
56
+ }
57
+ return markers.map((marker) => ({
58
+ ...marker,
59
+ classification: classifyMutationMarker(marker, diffTestFiles),
60
+ }));
61
+ }
62
+ /**
63
+ * Reduce a marker list to one-per-AC, the later marker winning.
64
+ *
65
+ * A re-run's marker (e.g. after amending the PR body) supersedes the
66
+ * earlier one for the same AC — the same "latest wins" idiom
67
+ * `resolveSpecRecommendation` uses for `SEQUANT_SPEC`.
68
+ *
69
+ * @param markers - Markers as returned by {@link parseMutationMarkers}
70
+ * @returns Map keyed by AC id, one marker per key
71
+ */
72
+ export function latestMutationMarkerPerAc(markers) {
73
+ const byAc = new Map();
74
+ for (const marker of markers) {
75
+ byAc.set(marker.ac, marker);
76
+ }
77
+ return byAc;
78
+ }
79
+ /**
80
+ * Classify a marker's `failedTest` against the diff's actual test files.
81
+ *
82
+ * A fabricated marker (naming a test that doesn't exist in the diff) is
83
+ * worse than a missing one — it claims verification that never happened.
84
+ * `failedTest` follows the `<file> > <test name>` shape `/qa` §6i renders
85
+ * in its output table (e.g. `injection.test.ts > rejects payload`); only
86
+ * the file segment is checked, since the suite/test-name portion after
87
+ * ` > ` isn't independently verifiable without executing the test.
88
+ *
89
+ * @param marker - A single parsed marker
90
+ * @param diffTestFiles - Test file paths present in the PR's diff
91
+ * @returns `"valid"` when the named file is in the diff, `"test_not_in_diff"` otherwise
92
+ */
93
+ export function classifyMutationMarker(marker, diffTestFiles) {
94
+ const namedFile = marker.failedTest.split(">")[0]?.trim() ?? "";
95
+ const found = diffTestFiles.some((file) => file === namedFile || file.endsWith(`/${namedFile}`));
96
+ return found ? "valid" : "test_not_in_diff";
97
+ }
@@ -11,6 +11,18 @@
11
11
  * ```
12
12
  */
13
13
  import { type Phase, type PhaseMarker } from "./state-schema.js";
14
+ /**
15
+ * Strip markdown code blocks and inline code from text.
16
+ * This prevents phase markers inside code examples from being parsed.
17
+ *
18
+ * Exported for reuse by `spec-recommendation.ts` (#921), which applies the
19
+ * same code-fence stripping to the `SEQUANT_SPEC` marker so documentation
20
+ * examples embedding that marker can't false-positive either.
21
+ *
22
+ * @param text - The text to strip code from
23
+ * @returns Text with code blocks and inline code removed
24
+ */
25
+ export declare function stripMarkdownCode(text: string): string;
14
26
  /**
15
27
  * Format a phase marker as an HTML comment string for embedding in GitHub comments.
16
28
  *
@@ -25,10 +25,14 @@ const INLINE_CODE_REGEX = /`[^`\n]+`/g;
25
25
  * Strip markdown code blocks and inline code from text.
26
26
  * This prevents phase markers inside code examples from being parsed.
27
27
  *
28
+ * Exported for reuse by `spec-recommendation.ts` (#921), which applies the
29
+ * same code-fence stripping to the `SEQUANT_SPEC` marker so documentation
30
+ * examples embedding that marker can't false-positive either.
31
+ *
28
32
  * @param text - The text to strip code from
29
33
  * @returns Text with code blocks and inline code removed
30
34
  */
31
- function stripMarkdownCode(text) {
35
+ export function stripMarkdownCode(text) {
32
36
  // First remove fenced code blocks (multi-line)
33
37
  let result = text.replace(FENCED_CODE_BLOCK_REGEX, "");
34
38
  // Then remove inline code
@@ -225,6 +225,15 @@ export declare function endedWithoutVerdict(output: string | undefined): boolean
225
225
  * @internal Exported for testing only
226
226
  */
227
227
  export declare function parseQaSummary(output: string): QaSummary | null;
228
+ /**
229
+ * Select the gap descriptions from a QA summary appropriate to feed a fix
230
+ * loop (#937 AC-3): every gap, except one whose `SEQUANT_QA_GAPS` finding is
231
+ * explicitly marked `document` or `pause_for_human` — those are QA-real but
232
+ * not code-fixable (quality/polish, or a decision the loop can't make on its
233
+ * own). A gap with no matching finding (the legacy prose-scrape path) is
234
+ * always included, unchanged from pre-#937 behavior.
235
+ */
236
+ export declare function selectFixableGaps(summary: QaSummary | null | undefined): string[];
228
237
  /**
229
238
  * Format duration in human-readable format
230
239
  */
@@ -341,6 +350,14 @@ export declare function mapAgentFailureToPhaseResult(phase: Phase, agentResult:
341
350
  sessionId?: string;
342
351
  resumeHandle?: ResumeHandle;
343
352
  };
353
+ /**
354
+ * Marks the embedded `promptContext` block appended below so a phase's own
355
+ * skill (e.g. `/loop`) can detect it deterministically instead of pattern-
356
+ * matching prose that varies per producer (`ready-gate.ts` and
357
+ * `batch-executor.ts` build differently-worded context for the same slot).
358
+ * See #960.
359
+ */
360
+ export declare const PROMPT_CONTEXT_SENTINEL = "SEQUANT_PROMPT_CONTEXT";
344
361
  /**
345
362
  * Get the prompt for a phase with the issue number substituted.
346
363
  * Selects self-contained prompts for non-Claude agents.
@@ -10,6 +10,7 @@
10
10
  import chalk from "chalk";
11
11
  import { execFileSync } from "child_process";
12
12
  import { resolveDiffBase } from "./git-diff-utils.js";
13
+ import { parseQaGapsMarker } from "./qa-gaps-marker.js";
13
14
  import { readAgentsMd } from "../agents-md.js";
14
15
  import { getDriver } from "./drivers/index.js";
15
16
  import { classifyError } from "./error-classifier.js";
@@ -499,9 +500,43 @@ export function parseQaSummary(output) {
499
500
  }
500
501
  if (acTotal === 0)
501
502
  return null;
502
- const gaps = parseListSection(output, /\*\*(?:Issues|Gaps)/);
503
+ const proseGaps = parseListSection(output, /\*\*(?:Issues|Gaps)/);
503
504
  const suggestions = parseListSection(output, /\*\*Suggestions/);
504
- return { acMet, acTotal, gaps, suggestions };
505
+ // #937: prefer the structured marker, but UNION with the prose scrape
506
+ // rather than replace it — a marker-carrying comment can still contain a
507
+ // gap the model couldn't fit into the six categories (the fallback rule),
508
+ // and that prose-only finding must not be silently dropped (AC-5).
509
+ const findings = parseQaGapsMarker(output) ?? undefined;
510
+ const seen = new Set();
511
+ const gaps = [];
512
+ for (const gap of [
513
+ ...(findings?.map((f) => f.description) ?? []),
514
+ ...proseGaps,
515
+ ]) {
516
+ const key = gap.trim().toLowerCase();
517
+ if (seen.has(key))
518
+ continue;
519
+ seen.add(key);
520
+ gaps.push(gap);
521
+ }
522
+ return { acMet, acTotal, gaps, suggestions, ...(findings && { findings }) };
523
+ }
524
+ /**
525
+ * Select the gap descriptions from a QA summary appropriate to feed a fix
526
+ * loop (#937 AC-3): every gap, except one whose `SEQUANT_QA_GAPS` finding is
527
+ * explicitly marked `document` or `pause_for_human` — those are QA-real but
528
+ * not code-fixable (quality/polish, or a decision the loop can't make on its
529
+ * own). A gap with no matching finding (the legacy prose-scrape path) is
530
+ * always included, unchanged from pre-#937 behavior.
531
+ */
532
+ export function selectFixableGaps(summary) {
533
+ if (!summary)
534
+ return [];
535
+ const excluded = new Set((summary.findings ?? [])
536
+ .filter((f) => f.recommendedAction === "document" ||
537
+ f.recommendedAction === "pause_for_human")
538
+ .map((f) => f.description.trim().toLowerCase()));
539
+ return summary.gaps.filter((g) => !excluded.has(g.trim().toLowerCase()));
505
540
  }
506
541
  /**
507
542
  * Parse a markdown bullet list section, filtering out "None" variants.
@@ -850,6 +885,14 @@ export function mapAgentFailureToPhaseResult(phase, agentResult, durationSeconds
850
885
  exitCode: agentResult.exitCode,
851
886
  };
852
887
  }
888
+ /**
889
+ * Marks the embedded `promptContext` block appended below so a phase's own
890
+ * skill (e.g. `/loop`) can detect it deterministically instead of pattern-
891
+ * matching prose that varies per producer (`ready-gate.ts` and
892
+ * `batch-executor.ts` build differently-worded context for the same slot).
893
+ * See #960.
894
+ */
895
+ export const PROMPT_CONTEXT_SENTINEL = "SEQUANT_PROMPT_CONTEXT";
853
896
  /**
854
897
  * Get the prompt for a phase with the issue number substituted.
855
898
  * Selects self-contained prompts for non-Claude agents.
@@ -867,9 +910,11 @@ export async function getPhasePrompt(phase, issueNumber, agent, promptContext) {
867
910
  : undefined;
868
911
  const template = driverPrompt ?? definition.promptTemplate;
869
912
  let basePrompt = template.replace(/\{issue\}/g, String(issueNumber));
870
- // Append phase-specific context (e.g., QA findings for loop phase)
913
+ // Append phase-specific context (e.g., QA findings for loop phase),
914
+ // wrapped in a sentinel so the phase's own skill (e.g. /loop) can detect
915
+ // embedded orchestrator context without re-fetching it from GitHub (#960).
871
916
  if (promptContext) {
872
- basePrompt += `\n\n---\n\n${promptContext}`;
917
+ basePrompt += `\n\n---\n\n<!-- ${PROMPT_CONTEXT_SENTINEL} -->\n${promptContext}\n<!-- /${PROMPT_CONTEXT_SENTINEL} -->`;
873
918
  }
874
919
  // Include AGENTS.md content in the prompt context for non-Claude agent compatibility.
875
920
  // Claude reads CLAUDE.md natively, but other agents (Aider, Codex, Gemini CLI)
@@ -1032,6 +1077,12 @@ async function executePhase(issueNumber, phase, config, resumeHandle, worktreePa
1032
1077
  const eligibleHandle = resumeHandle && driver.canResume(resumeHandle, cwd)
1033
1078
  ? resumeHandle
1034
1079
  : undefined;
1080
+ // #914: resolved per-phase model/effort, if this phase has one. Both
1081
+ // ExecutionConfig producers (buildExecutionConfig, ready-gate.ts's
1082
+ // buildPhaseConfig) populate `phasePolicies` the same way, so this is the
1083
+ // single site that turns it into driver-facing fields — see the doc
1084
+ // comment on ExecutionConfig.phasePolicies.
1085
+ const phasePolicy = config.phasePolicies?.[phase];
1035
1086
  // Build AgentExecutionConfig for the driver
1036
1087
  const agentConfig = {
1037
1088
  cwd,
@@ -1040,9 +1091,14 @@ async function executePhase(issueNumber, phase, config, resumeHandle, worktreePa
1040
1091
  phaseTimeout: config.phaseTimeout,
1041
1092
  verbose: config.verbose,
1042
1093
  mcp: config.mcp,
1094
+ mcpAllowlist: config.mcpAllowlist,
1043
1095
  resumeHandle: eligibleHandle,
1044
1096
  sessionId: eligibleHandle?.token,
1045
1097
  files,
1098
+ ...(phasePolicy?.model ? { model: phasePolicy.model } : {}),
1099
+ ...(phasePolicy?.effort
1100
+ ? { effort: phasePolicy.effort }
1101
+ : {}),
1046
1102
  onOutput: config.verbose || reportActivity
1047
1103
  ? (text) => {
1048
1104
  if (config.verbose) {