sequant 2.3.0 → 2.5.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 (101) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/README.md +125 -160
  4. package/dist/bin/cli.js +59 -4
  5. package/dist/dashboard/server.js +1 -0
  6. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +2 -2
  7. package/dist/marketplace/external_plugins/sequant/README.md +6 -3
  8. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +92 -0
  9. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +18 -9
  10. package/dist/marketplace/external_plugins/sequant/hooks/relay-check.sh +107 -0
  11. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/behavior-rule-detection.md +205 -0
  12. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +21 -8
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +302 -86
  14. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +109 -0
  15. package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +141 -22
  16. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +83 -78
  17. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +377 -137
  18. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +28 -0
  19. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +621 -0
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +741 -232
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +47 -1
  22. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +12 -6
  23. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +217 -964
  24. package/dist/marketplace/external_plugins/sequant/skills/spec/references/parallel-groups.md +7 -0
  25. package/dist/marketplace/external_plugins/sequant/skills/spec/references/quality-checklist.md +75 -0
  26. package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +4 -2
  27. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +0 -27
  28. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +24 -44
  29. package/dist/src/commands/abort.d.ts +36 -0
  30. package/dist/src/commands/abort.js +138 -0
  31. package/dist/src/commands/prompt.d.ts +7 -0
  32. package/dist/src/commands/prompt.js +101 -7
  33. package/dist/src/commands/ready-tui-adapter.d.ts +59 -0
  34. package/dist/src/commands/ready-tui-adapter.js +130 -0
  35. package/dist/src/commands/ready.d.ts +49 -0
  36. package/dist/src/commands/ready.js +243 -0
  37. package/dist/src/commands/run-progress.d.ts +11 -1
  38. package/dist/src/commands/run-progress.js +20 -3
  39. package/dist/src/commands/run.js +12 -2
  40. package/dist/src/commands/status.js +4 -0
  41. package/dist/src/commands/watch.d.ts +2 -0
  42. package/dist/src/commands/watch.js +67 -3
  43. package/dist/src/lib/assess-collision-detect.js +1 -1
  44. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +39 -0
  45. package/dist/src/lib/cli-ui/run-renderer.d.ts +34 -2
  46. package/dist/src/lib/cli-ui/run-renderer.js +250 -33
  47. package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
  48. package/dist/src/lib/cli-ui/scrollback-harness.js +294 -0
  49. package/dist/src/lib/merge-check/types.js +1 -1
  50. package/dist/src/lib/relay/archive.js +6 -0
  51. package/dist/src/lib/relay/types.d.ts +2 -0
  52. package/dist/src/lib/relay/types.js +9 -0
  53. package/dist/src/lib/settings.d.ts +34 -0
  54. package/dist/src/lib/settings.js +23 -1
  55. package/dist/src/lib/workflow/batch-executor.js +34 -18
  56. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
  57. package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
  58. package/dist/src/lib/workflow/drivers/aider.js +9 -0
  59. package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
  60. package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
  61. package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
  62. package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
  63. package/dist/src/lib/workflow/event-emitter.js +102 -0
  64. package/dist/src/lib/workflow/notice.d.ts +32 -0
  65. package/dist/src/lib/workflow/notice.js +38 -0
  66. package/dist/src/lib/workflow/phase-executor.d.ts +9 -21
  67. package/dist/src/lib/workflow/phase-executor.js +105 -117
  68. package/dist/src/lib/workflow/phase-mapper.d.ts +26 -13
  69. package/dist/src/lib/workflow/phase-mapper.js +55 -33
  70. package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
  71. package/dist/src/lib/workflow/phase-registry.js +233 -0
  72. package/dist/src/lib/workflow/platforms/github.d.ts +6 -0
  73. package/dist/src/lib/workflow/platforms/github.js +17 -0
  74. package/dist/src/lib/workflow/ready-gate.d.ts +155 -0
  75. package/dist/src/lib/workflow/ready-gate.js +374 -0
  76. package/dist/src/lib/workflow/reconcile.js +6 -0
  77. package/dist/src/lib/workflow/run-log-schema.d.ts +5 -55
  78. package/dist/src/lib/workflow/run-orchestrator.d.ts +32 -2
  79. package/dist/src/lib/workflow/run-orchestrator.js +125 -11
  80. package/dist/src/lib/workflow/state-manager.d.ts +19 -1
  81. package/dist/src/lib/workflow/state-manager.js +27 -1
  82. package/dist/src/lib/workflow/state-schema.d.ts +23 -35
  83. package/dist/src/lib/workflow/state-schema.js +29 -3
  84. package/dist/src/lib/workflow/types.d.ts +74 -15
  85. package/dist/src/lib/workflow/types.js +18 -13
  86. package/dist/src/ui/tui/App.js +8 -2
  87. package/dist/src/ui/tui/IssueBox.js +3 -4
  88. package/dist/src/ui/tui/index.d.ts +13 -4
  89. package/dist/src/ui/tui/index.js +19 -5
  90. package/dist/src/ui/tui/row-cap.d.ts +51 -0
  91. package/dist/src/ui/tui/row-cap.js +76 -0
  92. package/dist/src/ui/tui/teardown.d.ts +20 -0
  93. package/dist/src/ui/tui/teardown.js +29 -0
  94. package/dist/src/ui/tui/theme.d.ts +3 -0
  95. package/dist/src/ui/tui/theme.js +3 -0
  96. package/package.json +23 -11
  97. package/templates/hooks/post-tool.sh +81 -0
  98. package/templates/skills/assess/SKILL.md +28 -28
  99. package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
  100. package/templates/skills/qa/SKILL.md +5 -2
  101. package/templates/skills/setup/SKILL.md +6 -6
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Phase registry — single source of truth for workflow phase definitions.
3
+ *
4
+ * Replaces scattered constants (`PHASE_PROMPTS`, `AIDER_PHASE_PROMPTS`,
5
+ * `ISOLATED_PHASES`, `UI_LABELS`, `SECURITY_LABELS`) with a uniform record
6
+ * per phase. All 9 built-in phases register here at module load — no
7
+ * special-casing inside consumer code.
8
+ *
9
+ * Built-in registrations live at the bottom of this file rather than in a
10
+ * separate `built-in-phases.ts` module. The colocated layout follows the
11
+ * existing `drivers/index.ts` pattern and avoids the ESM-cycle pitfall of
12
+ * a separate bootstrap module that re-imports the registry singleton
13
+ * before the singleton is fully initialized.
14
+ *
15
+ * User-extensibility (filesystem discovery of `.sequant/phases/`) is
16
+ * deliberately deferred — see issue #505 descoping comment.
17
+ */
18
+ /**
19
+ * Per-driver overrides for a phase. Today only `promptTemplate` is supported;
20
+ * extend the inner record when additional fields need per-driver values.
21
+ */
22
+ export interface DriverOverride {
23
+ promptTemplate?: string;
24
+ }
25
+ /**
26
+ * Retry policy for a single phase. Phases without this field fall back to
27
+ * the global cold-start retry defaults in `phase-executor.ts`. The fields
28
+ * are deliberately optional — a phase only needs to specify what differs.
29
+ */
30
+ export interface RetryStrategy {
31
+ /** Override the cold-start retry attempt count for this phase. */
32
+ maxRetries?: number;
33
+ /** Initial backoff in ms before the first retry. */
34
+ backoffMs?: number;
35
+ /** Override the cold-start threshold (seconds). */
36
+ coldStartThreshold?: number;
37
+ /** Extra retries beyond cold-start (e.g. for transient API errors). */
38
+ extraRetries?: number;
39
+ }
40
+ /**
41
+ * Auto-detection rules consumed by phase-mapper.ts.
42
+ * `labels` is an exact-match list (case-insensitive at the call site).
43
+ */
44
+ export interface DetectRules {
45
+ labels?: string[];
46
+ }
47
+ /**
48
+ * Definition of a workflow phase. Registered at startup via
49
+ * `phaseRegistry.register(...)` and consumed by phase-executor, phase-mapper,
50
+ * and the CLI validator.
51
+ */
52
+ export interface PhaseDefinition {
53
+ /** Phase name (matches the skill template directory + CLI `--phases` token). */
54
+ name: string;
55
+ /** Skill template directory under `templates/skills/<skill>/SKILL.md`. */
56
+ skill: string;
57
+ /**
58
+ * Natural-language prompt for the default (Claude Code) driver. The token
59
+ * `{issue}` is substituted with the GitHub issue number at execution time.
60
+ */
61
+ promptTemplate: string;
62
+ /**
63
+ * When true, phase-executor runs this phase inside the issue worktree.
64
+ * `spec`, `verify`, and `merger` run in the main repo (no worktree).
65
+ */
66
+ requiresWorktree: boolean;
67
+ /** Optional per-phase retry overrides. */
68
+ retryStrategy?: RetryStrategy;
69
+ /** Optional auto-detection rules. */
70
+ detect?: DetectRules;
71
+ /**
72
+ * Per-driver overrides. Keyed by agent name (e.g. `"aider"`). When the
73
+ * orchestrator runs a non-Claude driver, the corresponding override's
74
+ * `promptTemplate` (if present) replaces the default.
75
+ */
76
+ driverOverrides?: Record<string, DriverOverride>;
77
+ /**
78
+ * Insertion order hint. Used by phase-mapper to sort label-detected phases
79
+ * into a deterministic pipeline position. Defaults to 0.
80
+ */
81
+ order?: number;
82
+ }
83
+ /**
84
+ * In-memory registry of phase definitions. Single mutable instance lives
85
+ * in this module — see the exported `phaseRegistry` constant.
86
+ *
87
+ * The class is intentionally minimal (no lifecycle hooks, no async). All
88
+ * mutations happen synchronously at module load by the built-in registrations
89
+ * at the bottom of this file.
90
+ */
91
+ export declare class PhaseRegistry {
92
+ private readonly definitions;
93
+ /**
94
+ * Register a phase definition. Throws on duplicate names so misconfigured
95
+ * bootstrap modules surface immediately instead of silently overwriting.
96
+ */
97
+ register(definition: PhaseDefinition): void;
98
+ /**
99
+ * Retrieve a phase by name. Throws with a "did you mean" list when the
100
+ * lookup fails — clearer than a downstream "undefined.promptTemplate".
101
+ */
102
+ get(name: string): PhaseDefinition;
103
+ /** True when a phase with this name is registered. */
104
+ has(name: string): boolean;
105
+ /**
106
+ * All registered phase definitions in insertion order. Insertion order
107
+ * is also the canonical pipeline order (see registrations below).
108
+ */
109
+ list(): PhaseDefinition[];
110
+ /**
111
+ * All registered phase names in insertion order. Replaces the
112
+ * `PhaseSchema.options` array literal exposed by the previous typed-enum
113
+ * `PhaseSchema`.
114
+ */
115
+ names(): string[];
116
+ }
117
+ /**
118
+ * Singleton registry instance. All consumer modules (phase-executor,
119
+ * phase-mapper, types.ts, CLI) read from this same instance — there is
120
+ * no second registry.
121
+ */
122
+ export declare const phaseRegistry: PhaseRegistry;
123
+ /**
124
+ * Convenience accessor for the registered phase names. Used by Zod refines,
125
+ * tests, and CLI validation in place of the removed `PhaseSchema.options`.
126
+ */
127
+ export declare function getPhaseNames(): string[];
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Phase registry — single source of truth for workflow phase definitions.
3
+ *
4
+ * Replaces scattered constants (`PHASE_PROMPTS`, `AIDER_PHASE_PROMPTS`,
5
+ * `ISOLATED_PHASES`, `UI_LABELS`, `SECURITY_LABELS`) with a uniform record
6
+ * per phase. All 9 built-in phases register here at module load — no
7
+ * special-casing inside consumer code.
8
+ *
9
+ * Built-in registrations live at the bottom of this file rather than in a
10
+ * separate `built-in-phases.ts` module. The colocated layout follows the
11
+ * existing `drivers/index.ts` pattern and avoids the ESM-cycle pitfall of
12
+ * a separate bootstrap module that re-imports the registry singleton
13
+ * before the singleton is fully initialized.
14
+ *
15
+ * User-extensibility (filesystem discovery of `.sequant/phases/`) is
16
+ * deliberately deferred — see issue #505 descoping comment.
17
+ */
18
+ /**
19
+ * In-memory registry of phase definitions. Single mutable instance lives
20
+ * in this module — see the exported `phaseRegistry` constant.
21
+ *
22
+ * The class is intentionally minimal (no lifecycle hooks, no async). All
23
+ * mutations happen synchronously at module load by the built-in registrations
24
+ * at the bottom of this file.
25
+ */
26
+ export class PhaseRegistry {
27
+ definitions = new Map();
28
+ /**
29
+ * Register a phase definition. Throws on duplicate names so misconfigured
30
+ * bootstrap modules surface immediately instead of silently overwriting.
31
+ */
32
+ register(definition) {
33
+ if (this.definitions.has(definition.name)) {
34
+ throw new Error(`PhaseRegistry: phase "${definition.name}" is already registered`);
35
+ }
36
+ this.definitions.set(definition.name, definition);
37
+ }
38
+ /**
39
+ * Retrieve a phase by name. Throws with a "did you mean" list when the
40
+ * lookup fails — clearer than a downstream "undefined.promptTemplate".
41
+ */
42
+ get(name) {
43
+ const def = this.definitions.get(name);
44
+ if (!def) {
45
+ const available = this.names().join(", ");
46
+ throw new Error(`PhaseRegistry: unknown phase "${name}". Available: ${available}`);
47
+ }
48
+ return def;
49
+ }
50
+ /** True when a phase with this name is registered. */
51
+ has(name) {
52
+ return this.definitions.has(name);
53
+ }
54
+ /**
55
+ * All registered phase definitions in insertion order. Insertion order
56
+ * is also the canonical pipeline order (see registrations below).
57
+ */
58
+ list() {
59
+ return [...this.definitions.values()];
60
+ }
61
+ /**
62
+ * All registered phase names in insertion order. Replaces the
63
+ * `PhaseSchema.options` array literal exposed by the previous typed-enum
64
+ * `PhaseSchema`.
65
+ */
66
+ names() {
67
+ return [...this.definitions.keys()];
68
+ }
69
+ }
70
+ /**
71
+ * Singleton registry instance. All consumer modules (phase-executor,
72
+ * phase-mapper, types.ts, CLI) read from this same instance — there is
73
+ * no second registry.
74
+ */
75
+ export const phaseRegistry = new PhaseRegistry();
76
+ /**
77
+ * Convenience accessor for the registered phase names. Used by Zod refines,
78
+ * tests, and CLI validation in place of the removed `PhaseSchema.options`.
79
+ */
80
+ export function getPhaseNames() {
81
+ return phaseRegistry.names();
82
+ }
83
+ // ─── Built-in phase registrations ────────────────────────────────────────
84
+ //
85
+ // Insertion order below IS the canonical pipeline order — preserved from the
86
+ // pre-registry `PhaseSchema` literal (`spec, security-review, exec, testgen,
87
+ // test, verify, qa, loop, merger`). Reordering these entries changes the
88
+ // order returned by `phaseRegistry.list()` / `getPhaseNames()` and the
89
+ // downstream `WORKFLOW_PHASES` constant in `state-schema.ts`.
90
+ // Spec — runs in the main repo (planning only, no worktree mutation)
91
+ phaseRegistry.register({
92
+ name: "spec",
93
+ skill: "spec",
94
+ promptTemplate: "Review GitHub issue #{issue} and create an implementation plan with verification criteria. Run the /spec {issue} workflow.",
95
+ requiresWorktree: false,
96
+ // Spec has a higher transient failure rate (~8.6%) than other phases due
97
+ // to GitHub API issues and rate limits. phase-executor.ts reads these
98
+ // values directly from the registry at module load (see
99
+ // SPEC_RETRY_BACKOFF_MS / SPEC_EXTRA_RETRIES).
100
+ retryStrategy: { extraRetries: 1, backoffMs: 5000 },
101
+ driverOverrides: {
102
+ aider: {
103
+ promptTemplate: `Read GitHub issue #{issue} using 'gh issue view #{issue}'.
104
+ Create a spec comment on the issue with:
105
+ 1. Implementation plan
106
+ 2. Acceptance criteria as a checklist
107
+ 3. Risk assessment
108
+ Post the comment using 'gh issue comment #{issue} --body "<comment>"'.`,
109
+ },
110
+ },
111
+ });
112
+ // Security review — worktree-isolated, label-triggered
113
+ phaseRegistry.register({
114
+ name: "security-review",
115
+ skill: "security-review",
116
+ promptTemplate: "Perform a deep security analysis for GitHub issue #{issue} focusing on auth, permissions, and sensitive operations. Run the /security-review {issue} workflow.",
117
+ requiresWorktree: true,
118
+ detect: {
119
+ labels: ["security", "auth", "authentication", "permissions", "admin"],
120
+ },
121
+ driverOverrides: {
122
+ aider: {
123
+ promptTemplate: `Perform a security review for GitHub issue #{issue}.
124
+ Read the issue with 'gh issue view #{issue}'.
125
+ Check for auth, permissions, injection, and sensitive data issues.
126
+ Post findings as a comment on the issue.`,
127
+ },
128
+ },
129
+ });
130
+ // Exec — worktree-isolated
131
+ phaseRegistry.register({
132
+ name: "exec",
133
+ skill: "exec",
134
+ promptTemplate: "Implement the feature for GitHub issue #{issue} following the spec. Run the /exec {issue} workflow.",
135
+ requiresWorktree: true,
136
+ driverOverrides: {
137
+ aider: {
138
+ promptTemplate: `Implement the feature described in GitHub issue #{issue}.
139
+ Read the issue and any spec comments with 'gh issue view #{issue} --comments'.
140
+ Follow the implementation plan from the spec.
141
+ Write tests for new functionality.
142
+ Ensure the build passes with 'npm test' and 'npm run build'.`,
143
+ },
144
+ },
145
+ });
146
+ // Testgen — worktree-isolated
147
+ phaseRegistry.register({
148
+ name: "testgen",
149
+ skill: "testgen",
150
+ promptTemplate: "Generate test stubs for GitHub issue #{issue} based on the specification. Run the /testgen {issue} workflow.",
151
+ requiresWorktree: true,
152
+ driverOverrides: {
153
+ aider: {
154
+ promptTemplate: `Generate test stubs for GitHub issue #{issue}.
155
+ Read the spec comments on the issue with 'gh issue view #{issue} --comments'.
156
+ Create test files with describe/it blocks covering the acceptance criteria.
157
+ Use the project's existing test framework.`,
158
+ },
159
+ },
160
+ });
161
+ // Test — worktree-isolated, label-triggered (UI/frontend issues)
162
+ phaseRegistry.register({
163
+ name: "test",
164
+ skill: "test",
165
+ promptTemplate: "Execute structured browser-based testing for GitHub issue #{issue}. Run the /test {issue} workflow.",
166
+ requiresWorktree: true,
167
+ detect: { labels: ["ui", "frontend", "admin", "web", "browser"] },
168
+ driverOverrides: {
169
+ aider: {
170
+ promptTemplate: `Test the implementation for GitHub issue #{issue}.
171
+ Run 'npm test' and verify all tests pass.
172
+ Check for edge cases and error handling.`,
173
+ },
174
+ },
175
+ });
176
+ // Verify — runs in main repo (CLI-only feature verification)
177
+ phaseRegistry.register({
178
+ name: "verify",
179
+ skill: "verify",
180
+ promptTemplate: "Verify the implementation for GitHub issue #{issue} by running commands and capturing output. Run the /verify {issue} workflow.",
181
+ requiresWorktree: false,
182
+ driverOverrides: {
183
+ aider: {
184
+ promptTemplate: `Verify the implementation for GitHub issue #{issue}.
185
+ Run relevant commands and capture their output for review.`,
186
+ },
187
+ },
188
+ });
189
+ // QA — worktree-isolated
190
+ phaseRegistry.register({
191
+ name: "qa",
192
+ skill: "qa",
193
+ promptTemplate: "Review the implementation for GitHub issue #{issue} against acceptance criteria. Run the /qa {issue} workflow.",
194
+ requiresWorktree: true,
195
+ driverOverrides: {
196
+ aider: {
197
+ promptTemplate: `Review the changes for GitHub issue #{issue}.
198
+ Run 'npm test' and 'npm run build' to verify everything works.
199
+ Check each acceptance criterion from the issue comments.
200
+ Output a verdict: READY_FOR_MERGE, AC_MET_BUT_NOT_A_PLUS, or AC_NOT_MET
201
+ with format "### Verdict: <VERDICT>" followed by an explanation.`,
202
+ },
203
+ },
204
+ });
205
+ // Loop — worktree-isolated. `maxRetries: 0` encodes the
206
+ // "skip cold-start retries" rule consumed by phase-executor.ts (#488).
207
+ phaseRegistry.register({
208
+ name: "loop",
209
+ skill: "loop",
210
+ promptTemplate: "Parse test/QA findings for GitHub issue #{issue} and iterate until quality gates pass. Run the /loop {issue} workflow.",
211
+ requiresWorktree: true,
212
+ retryStrategy: { maxRetries: 0 },
213
+ driverOverrides: {
214
+ aider: {
215
+ promptTemplate: `Review test and QA findings for GitHub issue #{issue}.
216
+ Fix any issues identified in the QA feedback.
217
+ Re-run 'npm test' and 'npm run build' until all quality gates pass.`,
218
+ },
219
+ },
220
+ });
221
+ // Merger — runs in main repo (multi-worktree integration)
222
+ phaseRegistry.register({
223
+ name: "merger",
224
+ skill: "merger",
225
+ promptTemplate: "Integrate and merge completed worktrees for GitHub issue #{issue}. Run the /merger {issue} workflow.",
226
+ requiresWorktree: false,
227
+ driverOverrides: {
228
+ aider: {
229
+ promptTemplate: `Integrate and merge completed worktrees for GitHub issue #{issue}.
230
+ Ensure all branches are up to date and merge cleanly.`,
231
+ },
232
+ },
233
+ });
@@ -105,6 +105,12 @@ export declare class GitHubProvider implements PlatformProvider {
105
105
  * Used by merge-check and worktree-discovery.
106
106
  */
107
107
  fetchIssueTitleSync(issueId: string): string | null;
108
+ /**
109
+ * Fetch an issue's raw body markdown. Used by `sequant ready` (#683) to parse
110
+ * the Non-Goals section for report-only gap classification. Returns null when
111
+ * gh is unavailable, the issue can't be fetched, or the body is empty.
112
+ */
113
+ fetchIssueBodySync(issueId: string): string | null;
108
114
  /**
109
115
  * Check if the `gh` CLI binary is installed (not auth, just available).
110
116
  * Used by upstream/assessment.ts for pre-flight checks.
@@ -249,6 +249,23 @@ export class GitHubProvider {
249
249
  return null;
250
250
  }
251
251
  }
252
+ /**
253
+ * Fetch an issue's raw body markdown. Used by `sequant ready` (#683) to parse
254
+ * the Non-Goals section for report-only gap classification. Returns null when
255
+ * gh is unavailable, the issue can't be fetched, or the body is empty.
256
+ */
257
+ fetchIssueBodySync(issueId) {
258
+ try {
259
+ const result = spawnSync("gh", ["issue", "view", issueId, "--json", "body", "--jq", ".body"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10000 });
260
+ if (result.status !== 0)
261
+ return null;
262
+ const body = result.stdout ?? "";
263
+ return body.trim() ? body : null;
264
+ }
265
+ catch {
266
+ return null;
267
+ }
268
+ }
252
269
  /**
253
270
  * Check if the `gh` CLI binary is installed (not auth, just available).
254
271
  * Used by upstream/assessment.ts for pre-flight checks.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Ready gate engine (#683).
3
+ *
4
+ * Drives the `sequant ready <issue>` pipeline: a full-weight `qa → loop → qa`
5
+ * loop that reproduces the maintainer's manual fresh-session A+ pass
6
+ * deterministically, then STOPS at a human merge gate — it never merges.
7
+ *
8
+ * The loop's exit threshold is set by a **gate policy**:
9
+ *
10
+ * - `ac` (default): stop once no `AC_NOT_MET` verdict remains. Remaining
11
+ * quality/polish gaps are surfaced in the report but NOT auto-fixed. Findings
12
+ * that touch the issue's Non-Goals are report-only. Predictable, scope-
13
+ * respecting behavior for a team engineer with a fixed agenda.
14
+ * - `a-plus` (opt-in): loop toward `READY_FOR_MERGE`, auto-fixing quality gaps.
15
+ *
16
+ * Both policies are additionally bounded by `maxIterations`, an optional token
17
+ * budget, and the `LOOP_NO_DIFF` stagnation guard. The #534 class (zero-diff
18
+ * exec / null QA verdict) is never reported as ready.
19
+ *
20
+ * This module is the reusable engine — a future `sequant run --ready-gate`
21
+ * (out of scope for #683) can reuse `runReadyGate` directly. The command shell
22
+ * lives in `src/commands/ready.ts`.
23
+ */
24
+ import type { ExecutionConfig, PhaseResult, ProgressCallback } from "./types.js";
25
+ import type { QaVerdict } from "./run-log-schema.js";
26
+ import type { ReadyPolicy } from "../settings.js";
27
+ import type { IssueStatus } from "./state-schema.js";
28
+ import { type LoopProgressSnapshot } from "./qa-stagnation.js";
29
+ export type { ReadyPolicy } from "../settings.js";
30
+ /**
31
+ * Why the gate stopped. Drives `ready`, the persisted issue status, and the
32
+ * human-facing report headline.
33
+ */
34
+ export type ReadyTerminalReason =
35
+ /** `ac`: ACs objectively met (no AC_NOT_MET). Quality gaps reported, not fixed. */
36
+ "AC_MET"
37
+ /** Either policy: QA returned READY_FOR_MERGE. */
38
+ | "READY_FOR_MERGE"
39
+ /** Guard: hit the iteration cap before the threshold. Needs human. */
40
+ | "MAX_ITERATIONS"
41
+ /** Guard: token budget exhausted before the threshold. Needs human. */
42
+ | "TOKEN_BUDGET"
43
+ /** Guard: `/loop` produced no diff — can't make progress. Needs human. */
44
+ | "LOOP_NO_DIFF"
45
+ /** Guard: `/loop` phase itself failed. Needs human. */
46
+ | "LOOP_FAILED"
47
+ /** #534: zero-diff exec or null/unparseable QA verdict. Not ready. */
48
+ | "NO_IMPLEMENTATION";
49
+ /** A single gap surfaced by QA, classified for the report. */
50
+ export interface ReadyGapItem {
51
+ /** Gap description as surfaced by QA. */
52
+ description: string;
53
+ /**
54
+ * True when this finding overlaps one of the issue's Non-Goals. In `ac`
55
+ * mode these are explicitly report-only (never fed to the fix loop).
56
+ */
57
+ nonGoal: boolean;
58
+ }
59
+ /** Structured outcome of a ready-gate run. */
60
+ export interface ReadyResult {
61
+ issueNumber: number;
62
+ policy: ReadyPolicy;
63
+ /** True only when the gate certifies the work as merge-ready for a human. */
64
+ ready: boolean;
65
+ reason: ReadyTerminalReason;
66
+ /** Issue status to persist (`waiting_for_human_merge` iff ready). */
67
+ issueStatus: IssueStatus;
68
+ /** Number of QA passes executed. */
69
+ iterations: number;
70
+ /** Last parsed QA verdict (null if QA never produced one). */
71
+ finalVerdict: QaVerdict | null;
72
+ /** Gap descriptions the fix loop was asked to address across iterations. */
73
+ autoFixed: string[];
74
+ /** Gaps still present / accepted at exit (quality gaps, Non-Goal items). */
75
+ remaining: ReadyGapItem[];
76
+ /** Total tokens consumed across all phases (best-effort from token files). */
77
+ tokensUsed: number;
78
+ /** Human-readable markdown gap report (AC-4). */
79
+ report: string;
80
+ }
81
+ /**
82
+ * Thin phase-runner abstraction so the engine can be unit-tested without the
83
+ * full `executePhaseWithRetry` positional signature or a live agent driver.
84
+ */
85
+ export type ReadyPhaseRunner = (phase: "qa" | "loop", config: ExecutionConfig, worktreePath: string) => Promise<PhaseResult>;
86
+ export interface RunReadyGateOptions {
87
+ issueNumber: number;
88
+ worktreePath: string;
89
+ policy: ReadyPolicy;
90
+ /** Hard iteration cap on QA passes (AC-6). */
91
+ maxIterations: number;
92
+ /** Optional token budget; 0/undefined disables the token cap (AC-6). */
93
+ tokenBudget?: number;
94
+ /** Non-Goals parsed from the issue body, for report-only classification. */
95
+ nonGoals?: string[];
96
+ /** Per-phase timeout in seconds. */
97
+ phaseTimeout: number;
98
+ /** Whether MCP servers are enabled for phase execution. */
99
+ mcp: boolean;
100
+ verbose?: boolean;
101
+ /** Injectable phase runner — defaults to the real executePhaseWithRetry wrapper. */
102
+ runPhase: ReadyPhaseRunner;
103
+ /**
104
+ * #697: optional live-progress sink. The gate owns the qa→loop→qa loop, so it
105
+ * is the natural emit site (the `run` path emits from RunOrchestrator/batch-
106
+ * executor). Fires `start` before each phase and `complete`/`failed` after,
107
+ * carrying the 1-based QA-pass `iteration` so the renderer shows `loop N/M`.
108
+ * Optional — injected unit tests that omit it stay unaffected.
109
+ */
110
+ onProgress?: ProgressCallback;
111
+ /** Injectable token reader — defaults to reading `<worktree>/.sequant`. */
112
+ readTokensUsed?: (worktreePath: string) => number;
113
+ /** Injectable change detector — defaults to {@link hasExecChanges}. */
114
+ hasChangesFn?: (cwd: string) => boolean;
115
+ /** Injectable loop-progress snapshot — defaults to {@link snapshotLoopProgress}. */
116
+ snapshotFn?: (cwd: string) => LoopProgressSnapshot;
117
+ }
118
+ /**
119
+ * Pure exit predicate. Given a policy and a QA verdict, has the loop reached
120
+ * its stopping threshold?
121
+ *
122
+ * - `READY_FOR_MERGE` always stops (both policies).
123
+ * - `ac`: `AC_MET_BUT_NOT_A_PLUS` also stops — ACs are objectively met; the
124
+ * remaining gaps are quality-only and `ac` reports rather than fixes them.
125
+ * - `a-plus`: only `READY_FOR_MERGE` stops.
126
+ * - `AC_NOT_MET` / `NEEDS_VERIFICATION` never stop in either policy.
127
+ */
128
+ export declare function isAtThreshold(policy: ReadyPolicy, verdict: QaVerdict): boolean;
129
+ /**
130
+ * Does a gap description overlap a Non-Goal? Conservative token-overlap
131
+ * heuristic: ≥2 shared significant words marks the gap as Non-Goal-touching.
132
+ *
133
+ * @internal Exported for testing only.
134
+ */
135
+ export declare function gapTouchesNonGoals(gap: string, nonGoals: string[]): boolean;
136
+ /**
137
+ * Parse the issue body's Non-Goals section into a list of bullet items.
138
+ *
139
+ * Recognizes `## Non-goals`, `## Non-Goals`, `### Out of scope`, etc. Captures
140
+ * markdown bullet items until the next heading. Returns `[]` when no section is
141
+ * present.
142
+ *
143
+ * @internal Exported for testing only.
144
+ */
145
+ export declare function parseNonGoals(issueBody: string): string[];
146
+ /**
147
+ * Render the structured gap report (AC-4).
148
+ *
149
+ * @internal Exported for testing only.
150
+ */
151
+ export declare function formatReadyReport(result: ReadyResult): string;
152
+ /**
153
+ * Drive the policy-bounded `qa → loop → qa` ready gate.
154
+ */
155
+ export declare function runReadyGate(opts: RunReadyGateOptions): Promise<ReadyResult>;