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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +125 -160
- package/dist/bin/cli.js +59 -4
- package/dist/dashboard/server.js +1 -0
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +2 -2
- package/dist/marketplace/external_plugins/sequant/README.md +6 -3
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +92 -0
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +18 -9
- package/dist/marketplace/external_plugins/sequant/hooks/relay-check.sh +107 -0
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/behavior-rule-detection.md +205 -0
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +21 -8
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +302 -86
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +109 -0
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +141 -22
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +83 -78
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +377 -137
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +28 -0
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +621 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +741 -232
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +47 -1
- package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +12 -6
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +217 -964
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/parallel-groups.md +7 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/quality-checklist.md +75 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +4 -2
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +0 -27
- package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +24 -44
- package/dist/src/commands/abort.d.ts +36 -0
- package/dist/src/commands/abort.js +138 -0
- package/dist/src/commands/prompt.d.ts +7 -0
- package/dist/src/commands/prompt.js +101 -7
- package/dist/src/commands/ready-tui-adapter.d.ts +59 -0
- package/dist/src/commands/ready-tui-adapter.js +130 -0
- package/dist/src/commands/ready.d.ts +49 -0
- package/dist/src/commands/ready.js +243 -0
- package/dist/src/commands/run-progress.d.ts +11 -1
- package/dist/src/commands/run-progress.js +20 -3
- package/dist/src/commands/run.js +12 -2
- package/dist/src/commands/status.js +4 -0
- package/dist/src/commands/watch.d.ts +2 -0
- package/dist/src/commands/watch.js +67 -3
- package/dist/src/lib/assess-collision-detect.js +1 -1
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +39 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +34 -2
- package/dist/src/lib/cli-ui/run-renderer.js +250 -33
- package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
- package/dist/src/lib/cli-ui/scrollback-harness.js +294 -0
- package/dist/src/lib/merge-check/types.js +1 -1
- package/dist/src/lib/relay/archive.js +6 -0
- package/dist/src/lib/relay/types.d.ts +2 -0
- package/dist/src/lib/relay/types.js +9 -0
- package/dist/src/lib/settings.d.ts +34 -0
- package/dist/src/lib/settings.js +23 -1
- package/dist/src/lib/workflow/batch-executor.js +34 -18
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
- package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
- package/dist/src/lib/workflow/drivers/aider.js +9 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
- package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
- package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
- package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
- package/dist/src/lib/workflow/event-emitter.js +102 -0
- package/dist/src/lib/workflow/notice.d.ts +32 -0
- package/dist/src/lib/workflow/notice.js +38 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +9 -21
- package/dist/src/lib/workflow/phase-executor.js +105 -117
- package/dist/src/lib/workflow/phase-mapper.d.ts +26 -13
- package/dist/src/lib/workflow/phase-mapper.js +55 -33
- package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
- package/dist/src/lib/workflow/phase-registry.js +233 -0
- package/dist/src/lib/workflow/platforms/github.d.ts +6 -0
- package/dist/src/lib/workflow/platforms/github.js +17 -0
- package/dist/src/lib/workflow/ready-gate.d.ts +155 -0
- package/dist/src/lib/workflow/ready-gate.js +374 -0
- package/dist/src/lib/workflow/reconcile.js +6 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts +5 -55
- package/dist/src/lib/workflow/run-orchestrator.d.ts +32 -2
- package/dist/src/lib/workflow/run-orchestrator.js +125 -11
- package/dist/src/lib/workflow/state-manager.d.ts +19 -1
- package/dist/src/lib/workflow/state-manager.js +27 -1
- package/dist/src/lib/workflow/state-schema.d.ts +23 -35
- package/dist/src/lib/workflow/state-schema.js +29 -3
- package/dist/src/lib/workflow/types.d.ts +74 -15
- package/dist/src/lib/workflow/types.js +18 -13
- package/dist/src/ui/tui/App.js +8 -2
- package/dist/src/ui/tui/IssueBox.js +3 -4
- package/dist/src/ui/tui/index.d.ts +13 -4
- package/dist/src/ui/tui/index.js +19 -5
- package/dist/src/ui/tui/row-cap.d.ts +51 -0
- package/dist/src/ui/tui/row-cap.js +76 -0
- package/dist/src/ui/tui/teardown.d.ts +20 -0
- package/dist/src/ui/tui/teardown.js +29 -0
- package/dist/src/ui/tui/theme.d.ts +3 -0
- package/dist/src/ui/tui/theme.js +3 -0
- package/package.json +23 -11
- package/templates/hooks/post-tool.sh +81 -0
- package/templates/skills/assess/SKILL.md +28 -28
- package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
- package/templates/skills/qa/SKILL.md +5 -2
- package/templates/skills/setup/SKILL.md +6 -6
|
@@ -0,0 +1,374 @@
|
|
|
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 { snapshotLoopProgress, compareLoopProgress, } from "./qa-stagnation.js";
|
|
25
|
+
import { hasExecChanges } from "./phase-executor.js";
|
|
26
|
+
import { readTokenUsageFiles, aggregateTokenUsage, TOKEN_USAGE_DIR, } from "./token-utils.js";
|
|
27
|
+
import * as path from "path";
|
|
28
|
+
/**
|
|
29
|
+
* Pure exit predicate. Given a policy and a QA verdict, has the loop reached
|
|
30
|
+
* its stopping threshold?
|
|
31
|
+
*
|
|
32
|
+
* - `READY_FOR_MERGE` always stops (both policies).
|
|
33
|
+
* - `ac`: `AC_MET_BUT_NOT_A_PLUS` also stops — ACs are objectively met; the
|
|
34
|
+
* remaining gaps are quality-only and `ac` reports rather than fixes them.
|
|
35
|
+
* - `a-plus`: only `READY_FOR_MERGE` stops.
|
|
36
|
+
* - `AC_NOT_MET` / `NEEDS_VERIFICATION` never stop in either policy.
|
|
37
|
+
*/
|
|
38
|
+
export function isAtThreshold(policy, verdict) {
|
|
39
|
+
if (verdict === "READY_FOR_MERGE")
|
|
40
|
+
return true;
|
|
41
|
+
if (policy === "ac")
|
|
42
|
+
return verdict === "AC_MET_BUT_NOT_A_PLUS";
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const STOPWORDS = new Set([
|
|
46
|
+
"the",
|
|
47
|
+
"and",
|
|
48
|
+
"for",
|
|
49
|
+
"with",
|
|
50
|
+
"that",
|
|
51
|
+
"this",
|
|
52
|
+
"from",
|
|
53
|
+
"into",
|
|
54
|
+
"are",
|
|
55
|
+
"was",
|
|
56
|
+
"has",
|
|
57
|
+
"have",
|
|
58
|
+
"not",
|
|
59
|
+
"but",
|
|
60
|
+
"its",
|
|
61
|
+
"via",
|
|
62
|
+
"any",
|
|
63
|
+
"all",
|
|
64
|
+
"out",
|
|
65
|
+
"should",
|
|
66
|
+
"would",
|
|
67
|
+
"could",
|
|
68
|
+
"when",
|
|
69
|
+
"then",
|
|
70
|
+
"than",
|
|
71
|
+
"must",
|
|
72
|
+
"will",
|
|
73
|
+
]);
|
|
74
|
+
function significantTokens(text) {
|
|
75
|
+
return new Set(text
|
|
76
|
+
.toLowerCase()
|
|
77
|
+
.replace(/[^a-z0-9\s-]/g, " ")
|
|
78
|
+
.split(/[\s-]+/)
|
|
79
|
+
.filter((w) => w.length > 3 && !STOPWORDS.has(w)));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Does a gap description overlap a Non-Goal? Conservative token-overlap
|
|
83
|
+
* heuristic: ≥2 shared significant words marks the gap as Non-Goal-touching.
|
|
84
|
+
*
|
|
85
|
+
* @internal Exported for testing only.
|
|
86
|
+
*/
|
|
87
|
+
export function gapTouchesNonGoals(gap, nonGoals) {
|
|
88
|
+
if (nonGoals.length === 0)
|
|
89
|
+
return false;
|
|
90
|
+
const gapTokens = significantTokens(gap);
|
|
91
|
+
if (gapTokens.size === 0)
|
|
92
|
+
return false;
|
|
93
|
+
for (const ng of nonGoals) {
|
|
94
|
+
const ngTokens = significantTokens(ng);
|
|
95
|
+
let overlap = 0;
|
|
96
|
+
for (const t of ngTokens) {
|
|
97
|
+
if (gapTokens.has(t))
|
|
98
|
+
overlap++;
|
|
99
|
+
if (overlap >= 2)
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Parse the issue body's Non-Goals section into a list of bullet items.
|
|
107
|
+
*
|
|
108
|
+
* Recognizes `## Non-goals`, `## Non-Goals`, `### Out of scope`, etc. Captures
|
|
109
|
+
* markdown bullet items until the next heading. Returns `[]` when no section is
|
|
110
|
+
* present.
|
|
111
|
+
*
|
|
112
|
+
* @internal Exported for testing only.
|
|
113
|
+
*/
|
|
114
|
+
export function parseNonGoals(issueBody) {
|
|
115
|
+
if (!issueBody)
|
|
116
|
+
return [];
|
|
117
|
+
const lines = issueBody.split("\n");
|
|
118
|
+
const items = [];
|
|
119
|
+
let inSection = false;
|
|
120
|
+
const headingRe = /^#{1,6}\s+(.*)$/;
|
|
121
|
+
const nonGoalHeadingRe = /^(non-?goals?|out[ -]of[ -]scope)\b/i;
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
const heading = line.match(headingRe);
|
|
124
|
+
if (heading) {
|
|
125
|
+
inSection = nonGoalHeadingRe.test(heading[1].trim());
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (!inSection)
|
|
129
|
+
continue;
|
|
130
|
+
const bullet = line.match(/^\s*[-*]\s+(.+)$/);
|
|
131
|
+
if (bullet) {
|
|
132
|
+
// Strip surrounding markdown emphasis/backticks for cleaner matching.
|
|
133
|
+
const text = bullet[1].replace(/[`*]/g, "").trim();
|
|
134
|
+
if (text)
|
|
135
|
+
items.push(text);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return items;
|
|
139
|
+
}
|
|
140
|
+
function classifyGaps(gaps, nonGoals) {
|
|
141
|
+
return gaps.map((g) => ({
|
|
142
|
+
description: g,
|
|
143
|
+
nonGoal: gapTouchesNonGoals(g, nonGoals),
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
function defaultReadTokensUsed(worktreePath) {
|
|
147
|
+
const dir = path.join(worktreePath, TOKEN_USAGE_DIR);
|
|
148
|
+
// Read without cleanup — the engine polls cumulatively across phases.
|
|
149
|
+
const files = readTokenUsageFiles(dir);
|
|
150
|
+
return aggregateTokenUsage(files).tokensUsed;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Build a minimal ExecutionConfig for a single ready-gate phase.
|
|
154
|
+
*/
|
|
155
|
+
function buildPhaseConfig(opts, extra) {
|
|
156
|
+
return {
|
|
157
|
+
phases: [],
|
|
158
|
+
phaseTimeout: opts.phaseTimeout,
|
|
159
|
+
qualityLoop: false,
|
|
160
|
+
maxIterations: opts.maxIterations,
|
|
161
|
+
skipVerification: false,
|
|
162
|
+
sequential: true,
|
|
163
|
+
concurrency: 1,
|
|
164
|
+
parallel: false,
|
|
165
|
+
verbose: opts.verbose ?? false,
|
|
166
|
+
noSmartTests: false,
|
|
167
|
+
dryRun: false,
|
|
168
|
+
mcp: opts.mcp,
|
|
169
|
+
retry: true,
|
|
170
|
+
...extra,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Render the structured gap report (AC-4).
|
|
175
|
+
*
|
|
176
|
+
* @internal Exported for testing only.
|
|
177
|
+
*/
|
|
178
|
+
export function formatReadyReport(result) {
|
|
179
|
+
const headline = result.ready
|
|
180
|
+
? "✅ READY — awaiting human merge decision"
|
|
181
|
+
: result.reason === "NO_IMPLEMENTATION"
|
|
182
|
+
? "⛔ NOT READY — no implementation detected"
|
|
183
|
+
: "⚠️ NOT READY — needs human intervention";
|
|
184
|
+
const reasonText = {
|
|
185
|
+
AC_MET: "Acceptance Criteria are objectively met (no AC_NOT_MET). Remaining gaps are quality-only and reported below (policy `ac` does not auto-fix them).",
|
|
186
|
+
READY_FOR_MERGE: "QA returned READY_FOR_MERGE.",
|
|
187
|
+
MAX_ITERATIONS: "Hit the iteration cap before reaching the policy threshold. A human should review the remaining gaps.",
|
|
188
|
+
TOKEN_BUDGET: "Token budget exhausted before reaching the policy threshold. A human should review the remaining gaps.",
|
|
189
|
+
LOOP_NO_DIFF: "The fix loop made no diff (stagnation guard). Manual intervention required.",
|
|
190
|
+
LOOP_FAILED: "The fix loop phase failed. A human should investigate before merging.",
|
|
191
|
+
NO_IMPLEMENTATION: "Zero-diff worktree or null/unparseable QA verdict — there is nothing to certify (#534 guard).",
|
|
192
|
+
};
|
|
193
|
+
const lines = [];
|
|
194
|
+
lines.push(`## sequant ready — Issue #${result.issueNumber}`);
|
|
195
|
+
lines.push("");
|
|
196
|
+
lines.push(`**${headline}**`);
|
|
197
|
+
lines.push("");
|
|
198
|
+
lines.push(`- **Policy:** \`${result.policy}\``);
|
|
199
|
+
lines.push(`- **Final verdict:** ${result.finalVerdict ?? "(none)"}`);
|
|
200
|
+
lines.push(`- **Stop reason:** ${result.reason} — ${reasonText[result.reason]}`);
|
|
201
|
+
lines.push(`- **QA passes:** ${result.iterations}`);
|
|
202
|
+
lines.push(`- **Tokens used:** ${result.tokensUsed.toLocaleString()}`);
|
|
203
|
+
lines.push(`- **Final state:** \`${result.issueStatus}\` (never auto-merged)`);
|
|
204
|
+
lines.push("");
|
|
205
|
+
lines.push("### Auto-fixed");
|
|
206
|
+
if (result.autoFixed.length === 0) {
|
|
207
|
+
lines.push("- None");
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
for (const item of result.autoFixed)
|
|
211
|
+
lines.push(`- ${item}`);
|
|
212
|
+
}
|
|
213
|
+
lines.push("");
|
|
214
|
+
lines.push("### Remaining / accepted gaps");
|
|
215
|
+
if (result.remaining.length === 0) {
|
|
216
|
+
lines.push("- None");
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
for (const item of result.remaining) {
|
|
220
|
+
const tag = item.nonGoal ? " _(Non-Goal — report-only)_" : "";
|
|
221
|
+
lines.push(`- ${item.description}${tag}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
lines.push("");
|
|
225
|
+
lines.push("> The human merge gate is intentional: `sequant ready` never merges. Review the gaps above, then merge manually when satisfied.");
|
|
226
|
+
return lines.join("\n");
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Drive the policy-bounded `qa → loop → qa` ready gate.
|
|
230
|
+
*/
|
|
231
|
+
export async function runReadyGate(opts) {
|
|
232
|
+
const { issueNumber, worktreePath, policy, maxIterations, tokenBudget, nonGoals = [], } = opts;
|
|
233
|
+
const readTokensUsed = opts.readTokensUsed ?? defaultReadTokensUsed;
|
|
234
|
+
const hasChangesFn = opts.hasChangesFn ?? hasExecChanges;
|
|
235
|
+
const snapshotFn = opts.snapshotFn ?? snapshotLoopProgress;
|
|
236
|
+
// #697: run a phase while emitting live-progress events around it. `iteration`
|
|
237
|
+
// is the 1-based QA-pass index so the renderer can render `loop N/M`. Emits
|
|
238
|
+
// `failed` (and re-throws) if the runner itself throws so the catch path in
|
|
239
|
+
// ready.ts disposes a renderer that already reflects the failed cell.
|
|
240
|
+
const runPhaseTracked = async (phase, config, iteration) => {
|
|
241
|
+
opts.onProgress?.(opts.issueNumber, phase, "start", { iteration });
|
|
242
|
+
let phaseResult;
|
|
243
|
+
try {
|
|
244
|
+
phaseResult = await opts.runPhase(phase, config, worktreePath);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
opts.onProgress?.(opts.issueNumber, phase, "failed", {
|
|
248
|
+
iteration,
|
|
249
|
+
error: err instanceof Error ? err.message : String(err),
|
|
250
|
+
});
|
|
251
|
+
throw err;
|
|
252
|
+
}
|
|
253
|
+
opts.onProgress?.(opts.issueNumber, phase, phaseResult.success === false ? "failed" : "complete", {
|
|
254
|
+
iteration,
|
|
255
|
+
durationSeconds: phaseResult.durationSeconds,
|
|
256
|
+
error: phaseResult.success === false ? phaseResult.error : undefined,
|
|
257
|
+
});
|
|
258
|
+
return phaseResult;
|
|
259
|
+
};
|
|
260
|
+
let iterations = 0;
|
|
261
|
+
let finalVerdict = null;
|
|
262
|
+
const autoFixed = [];
|
|
263
|
+
let remaining = [];
|
|
264
|
+
let tokensUsed = 0;
|
|
265
|
+
const finish = (reason) => {
|
|
266
|
+
const ready = reason === "AC_MET" || reason === "READY_FOR_MERGE";
|
|
267
|
+
const issueStatus = ready
|
|
268
|
+
? "waiting_for_human_merge"
|
|
269
|
+
: "blocked";
|
|
270
|
+
const result = {
|
|
271
|
+
issueNumber,
|
|
272
|
+
policy,
|
|
273
|
+
ready,
|
|
274
|
+
reason,
|
|
275
|
+
issueStatus,
|
|
276
|
+
iterations,
|
|
277
|
+
finalVerdict,
|
|
278
|
+
autoFixed,
|
|
279
|
+
remaining,
|
|
280
|
+
tokensUsed,
|
|
281
|
+
report: "",
|
|
282
|
+
};
|
|
283
|
+
result.report = formatReadyReport(result);
|
|
284
|
+
return result;
|
|
285
|
+
};
|
|
286
|
+
const budgetExceeded = () => typeof tokenBudget === "number" &&
|
|
287
|
+
tokenBudget > 0 &&
|
|
288
|
+
tokensUsed >= tokenBudget;
|
|
289
|
+
// Loop is bounded by maxIterations QA passes. Each iteration: run QA, check
|
|
290
|
+
// the policy threshold + #534 guards, and (if not stopping) run one fix loop.
|
|
291
|
+
while (iterations < maxIterations) {
|
|
292
|
+
if (budgetExceeded()) {
|
|
293
|
+
return finish("TOKEN_BUDGET");
|
|
294
|
+
}
|
|
295
|
+
iterations++;
|
|
296
|
+
const qaResult = await runPhaseTracked("qa", buildPhaseConfig(opts, { fullQa: true }), iterations);
|
|
297
|
+
tokensUsed = readTokensUsed(worktreePath);
|
|
298
|
+
const verdict = qaResult.verdict ?? null;
|
|
299
|
+
// #534 guard: a null/unparseable verdict is never "ready".
|
|
300
|
+
if (!verdict) {
|
|
301
|
+
return finish("NO_IMPLEMENTATION");
|
|
302
|
+
}
|
|
303
|
+
// #534 guard: an empty worktree (no commits, no uncommitted work) is never
|
|
304
|
+
// "ready" — replays the #529/#570 empty-branch class.
|
|
305
|
+
if (!hasChangesFn(worktreePath)) {
|
|
306
|
+
return finish("NO_IMPLEMENTATION");
|
|
307
|
+
}
|
|
308
|
+
finalVerdict = verdict;
|
|
309
|
+
const gaps = qaResult.summary?.gaps ?? [];
|
|
310
|
+
remaining = classifyGaps(gaps, nonGoals);
|
|
311
|
+
// Policy threshold reached → stop at the human merge gate.
|
|
312
|
+
if (isAtThreshold(policy, verdict)) {
|
|
313
|
+
return finish(verdict === "READY_FOR_MERGE" ? "READY_FOR_MERGE" : "AC_MET");
|
|
314
|
+
}
|
|
315
|
+
// Not at threshold and out of iterations → clean halt, needs human.
|
|
316
|
+
if (iterations >= maxIterations) {
|
|
317
|
+
return finish("MAX_ITERATIONS");
|
|
318
|
+
}
|
|
319
|
+
if (budgetExceeded()) {
|
|
320
|
+
return finish("TOKEN_BUDGET");
|
|
321
|
+
}
|
|
322
|
+
// Run one fix loop. In `ac` mode we only reach here on AC_NOT_MET, so the
|
|
323
|
+
// gaps are AC gaps — feeding them via failedAcs keeps the loop scoped to
|
|
324
|
+
// the AC boundary (quality gaps are never fixed under `ac`). Non-Goal-
|
|
325
|
+
// touching findings are excluded from what we ask the loop to fix.
|
|
326
|
+
const fixableGaps = remaining
|
|
327
|
+
.filter((g) => !g.nonGoal)
|
|
328
|
+
.map((g) => g.description);
|
|
329
|
+
const before = snapshotFn(worktreePath);
|
|
330
|
+
const loopResult = await runPhaseTracked("loop", buildPhaseConfig(opts, {
|
|
331
|
+
lastVerdict: verdict,
|
|
332
|
+
failedAcs: fixableGaps.join("; ") || undefined,
|
|
333
|
+
promptContext: buildLoopContext(policy, verdict, fixableGaps),
|
|
334
|
+
}), iterations);
|
|
335
|
+
tokensUsed = readTokensUsed(worktreePath);
|
|
336
|
+
if (!loopResult.success) {
|
|
337
|
+
return finish("LOOP_FAILED");
|
|
338
|
+
}
|
|
339
|
+
const after = snapshotFn(worktreePath);
|
|
340
|
+
const progress = compareLoopProgress(before, after);
|
|
341
|
+
if (!progress.progressed) {
|
|
342
|
+
return finish("LOOP_NO_DIFF");
|
|
343
|
+
}
|
|
344
|
+
// The loop produced a diff — record what it was asked to fix and re-QA.
|
|
345
|
+
for (const g of fixableGaps) {
|
|
346
|
+
if (!autoFixed.includes(g))
|
|
347
|
+
autoFixed.push(g);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Iteration cap reached without an explicit stop above (defensive).
|
|
351
|
+
return finish("MAX_ITERATIONS");
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Build the prompt context handed to the `/loop` phase. Mirrors the
|
|
355
|
+
* batch-executor's `buildLoopContext` shape but scopes the instruction to the
|
|
356
|
+
* gate policy so `ac` runs do not chase quality-only gaps.
|
|
357
|
+
*/
|
|
358
|
+
function buildLoopContext(policy, verdict, fixableGaps) {
|
|
359
|
+
const parts = [];
|
|
360
|
+
parts.push(`Ready gate (#683) — policy: ${policy}`);
|
|
361
|
+
parts.push(`QA Verdict: ${verdict}`);
|
|
362
|
+
if (policy === "ac") {
|
|
363
|
+
parts.push("Scope: fix ONLY the unmet Acceptance Criteria below. Do NOT address quality/polish gaps or anything touching the issue's Non-Goals — those are deliberately deferred under the `ac` policy.");
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
parts.push("Scope: drive the work toward READY_FOR_MERGE by addressing the gaps below.");
|
|
367
|
+
}
|
|
368
|
+
if (fixableGaps.length > 0) {
|
|
369
|
+
parts.push("Gaps to address:");
|
|
370
|
+
for (const g of fixableGaps)
|
|
371
|
+
parts.push(`- ${g}`);
|
|
372
|
+
}
|
|
373
|
+
return parts.join("\n");
|
|
374
|
+
}
|
|
@@ -95,6 +95,12 @@ export function getNextActionHint(issue) {
|
|
|
95
95
|
}
|
|
96
96
|
case "waiting_for_qa_gate":
|
|
97
97
|
return `sequant run ${issue.number} --phase qa`;
|
|
98
|
+
case "waiting_for_human_merge":
|
|
99
|
+
// `sequant ready` certified the work; a human reviews + merges manually.
|
|
100
|
+
if (issue.pr?.number) {
|
|
101
|
+
return `review gaps, then gh pr merge ${issue.pr.number}`;
|
|
102
|
+
}
|
|
103
|
+
return `review gaps, then merge manually`;
|
|
98
104
|
case "ready_for_merge":
|
|
99
105
|
if (issue.pr?.number) {
|
|
100
106
|
return `gh pr merge ${issue.pr.number}`;
|
|
@@ -113,17 +113,7 @@ export type QaSummary = z.infer<typeof QaSummarySchema>;
|
|
|
113
113
|
* Log entry for a single phase execution
|
|
114
114
|
*/
|
|
115
115
|
export declare const PhaseLogSchema: z.ZodObject<{
|
|
116
|
-
phase: z.
|
|
117
|
-
qa: "qa";
|
|
118
|
-
loop: "loop";
|
|
119
|
-
verify: "verify";
|
|
120
|
-
spec: "spec";
|
|
121
|
-
"security-review": "security-review";
|
|
122
|
-
exec: "exec";
|
|
123
|
-
testgen: "testgen";
|
|
124
|
-
test: "test";
|
|
125
|
-
merger: "merger";
|
|
126
|
-
}>;
|
|
116
|
+
phase: z.ZodString;
|
|
127
117
|
issueNumber: z.ZodNumber;
|
|
128
118
|
startTime: z.ZodString;
|
|
129
119
|
endTime: z.ZodString;
|
|
@@ -199,17 +189,7 @@ export declare const IssueLogSchema: z.ZodObject<{
|
|
|
199
189
|
partial: "partial";
|
|
200
190
|
}>;
|
|
201
191
|
phases: z.ZodArray<z.ZodObject<{
|
|
202
|
-
phase: z.
|
|
203
|
-
qa: "qa";
|
|
204
|
-
loop: "loop";
|
|
205
|
-
verify: "verify";
|
|
206
|
-
spec: "spec";
|
|
207
|
-
"security-review": "security-review";
|
|
208
|
-
exec: "exec";
|
|
209
|
-
testgen: "testgen";
|
|
210
|
-
test: "test";
|
|
211
|
-
merger: "merger";
|
|
212
|
-
}>;
|
|
192
|
+
phase: z.ZodString;
|
|
213
193
|
issueNumber: z.ZodNumber;
|
|
214
194
|
startTime: z.ZodString;
|
|
215
195
|
endTime: z.ZodString;
|
|
@@ -280,17 +260,7 @@ export type IssueLog = z.infer<typeof IssueLogSchema>;
|
|
|
280
260
|
* Run configuration
|
|
281
261
|
*/
|
|
282
262
|
export declare const RunConfigSchema: z.ZodObject<{
|
|
283
|
-
phases: z.ZodArray<z.
|
|
284
|
-
qa: "qa";
|
|
285
|
-
loop: "loop";
|
|
286
|
-
verify: "verify";
|
|
287
|
-
spec: "spec";
|
|
288
|
-
"security-review": "security-review";
|
|
289
|
-
exec: "exec";
|
|
290
|
-
testgen: "testgen";
|
|
291
|
-
test: "test";
|
|
292
|
-
merger: "merger";
|
|
293
|
-
}>>;
|
|
263
|
+
phases: z.ZodArray<z.ZodString>;
|
|
294
264
|
sequential: z.ZodBoolean;
|
|
295
265
|
qualityLoop: z.ZodBoolean;
|
|
296
266
|
maxIterations: z.ZodNumber;
|
|
@@ -319,17 +289,7 @@ export declare const RunLogSchema: z.ZodObject<{
|
|
|
319
289
|
startTime: z.ZodString;
|
|
320
290
|
endTime: z.ZodString;
|
|
321
291
|
config: z.ZodObject<{
|
|
322
|
-
phases: z.ZodArray<z.
|
|
323
|
-
qa: "qa";
|
|
324
|
-
loop: "loop";
|
|
325
|
-
verify: "verify";
|
|
326
|
-
spec: "spec";
|
|
327
|
-
"security-review": "security-review";
|
|
328
|
-
exec: "exec";
|
|
329
|
-
testgen: "testgen";
|
|
330
|
-
test: "test";
|
|
331
|
-
merger: "merger";
|
|
332
|
-
}>>;
|
|
292
|
+
phases: z.ZodArray<z.ZodString>;
|
|
333
293
|
sequential: z.ZodBoolean;
|
|
334
294
|
qualityLoop: z.ZodBoolean;
|
|
335
295
|
maxIterations: z.ZodNumber;
|
|
@@ -346,17 +306,7 @@ export declare const RunLogSchema: z.ZodObject<{
|
|
|
346
306
|
partial: "partial";
|
|
347
307
|
}>;
|
|
348
308
|
phases: z.ZodArray<z.ZodObject<{
|
|
349
|
-
phase: z.
|
|
350
|
-
qa: "qa";
|
|
351
|
-
loop: "loop";
|
|
352
|
-
verify: "verify";
|
|
353
|
-
spec: "spec";
|
|
354
|
-
"security-review": "security-review";
|
|
355
|
-
exec: "exec";
|
|
356
|
-
testgen: "testgen";
|
|
357
|
-
test: "test";
|
|
358
|
-
merger: "merger";
|
|
359
|
-
}>;
|
|
309
|
+
phase: z.ZodString;
|
|
360
310
|
issueNumber: z.ZodNumber;
|
|
361
311
|
startTime: z.ZodString;
|
|
362
312
|
endTime: z.ZodString;
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import type { ExecutionConfig, IssueResult, RunOptions, ProgressCallback } from "./types.js";
|
|
9
|
+
import type { ExecutionConfig, IssueResult, RunOptions, ProgressCallback, PhasePlanCallback, PhasePauseHandle } from "./types.js";
|
|
10
10
|
import type { RunSnapshot } from "./run-state.js";
|
|
11
11
|
import type { WorktreeInfo } from "./worktree-manager.js";
|
|
12
12
|
import { LogWriter } from "./log-writer.js";
|
|
13
13
|
import { StateManager } from "./state-manager.js";
|
|
14
14
|
import { ShutdownManager } from "../shutdown.js";
|
|
15
15
|
import type { LockFile } from "../locks/index.js";
|
|
16
|
+
import { WorkflowEventEmitter } from "./event-emitter.js";
|
|
16
17
|
import type { SequantSettings } from "../settings.js";
|
|
17
18
|
/**
|
|
18
19
|
* Build the stack-manifest line emitted into PR bodies under --stacked.
|
|
@@ -56,6 +57,14 @@ export interface OrchestratorConfig {
|
|
|
56
57
|
baseBranch?: string;
|
|
57
58
|
/** Per-phase progress callback (parallel mode) */
|
|
58
59
|
onProgress?: ProgressCallback;
|
|
60
|
+
/** #672 AC-2: phase-plan callback forwarded into per-issue contexts. */
|
|
61
|
+
onPhasePlan?: PhasePlanCallback;
|
|
62
|
+
/**
|
|
63
|
+
* Optional live-zone pause handle (#656). Forwarded to every issue's
|
|
64
|
+
* batch context so `executePhaseWithRetry` can quiesce the renderer
|
|
65
|
+
* around verbose Claude streaming.
|
|
66
|
+
*/
|
|
67
|
+
phasePauseHandle?: PhasePauseHandle;
|
|
59
68
|
}
|
|
60
69
|
/**
|
|
61
70
|
* High-level init config for full lifecycle execution.
|
|
@@ -75,6 +84,15 @@ export interface RunInit {
|
|
|
75
84
|
baseBranch?: string;
|
|
76
85
|
/** Per-phase progress callback */
|
|
77
86
|
onProgress?: ProgressCallback;
|
|
87
|
+
/** #672 AC-2: phase-plan callback. Fired once per issue once the executor
|
|
88
|
+
* has resolved the final phase pipeline. */
|
|
89
|
+
onPhasePlan?: PhasePlanCallback;
|
|
90
|
+
/**
|
|
91
|
+
* Optional live-zone pause handle (#656). Threaded through to the
|
|
92
|
+
* `OrchestratorConfig` so verbose Claude streaming pauses the renderer's
|
|
93
|
+
* live zone instead of redrawing over it.
|
|
94
|
+
*/
|
|
95
|
+
phasePauseHandle?: PhasePauseHandle;
|
|
78
96
|
/**
|
|
79
97
|
* Invoked once the orchestrator is constructed but before execution begins.
|
|
80
98
|
* Used by the experimental TUI to attach a snapshot poller to the active
|
|
@@ -146,8 +164,16 @@ export declare class RunOrchestrator {
|
|
|
146
164
|
private readonly cfg;
|
|
147
165
|
private readonly issueStates;
|
|
148
166
|
private readonly phaseStartTimes;
|
|
167
|
+
private readonly emitter;
|
|
149
168
|
private done;
|
|
150
169
|
constructor(config: OrchestratorConfig);
|
|
170
|
+
/**
|
|
171
|
+
* Returns the workflow event emitter. External consumers (TUI, MCP server,
|
|
172
|
+
* future webhooks) call `getEmitter().on(...)` to subscribe to lifecycle
|
|
173
|
+
* events. Subscribing is opt-in — the orchestrator runs unaware of who is
|
|
174
|
+
* listening (#504, AC-3).
|
|
175
|
+
*/
|
|
176
|
+
getEmitter(): WorkflowEventEmitter;
|
|
151
177
|
/**
|
|
152
178
|
* Point-in-time view of the entire run.
|
|
153
179
|
*
|
|
@@ -157,7 +183,11 @@ export declare class RunOrchestrator {
|
|
|
157
183
|
* observing torn writes.
|
|
158
184
|
*/
|
|
159
185
|
getSnapshot(): RunSnapshot;
|
|
160
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* Mark the run as completed so the dashboard can unmount and drop event
|
|
188
|
+
* subscribers. Drains the emitter to prevent leaks across multiple
|
|
189
|
+
* `run()` invocations in the same process (e.g. the MCP server).
|
|
190
|
+
*/
|
|
161
191
|
markDone(): void;
|
|
162
192
|
private initIssueStates;
|
|
163
193
|
private wrapProgress;
|