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
@@ -15,6 +15,7 @@ import { LogWriter } from "./log-writer.js";
15
15
  import { StateManager } from "./state-manager.js";
16
16
  import { ShutdownManager } from "../shutdown.js";
17
17
  import { LockManager, formatLockedMessage } from "../locks/index.js";
18
+ import { bracketedConsoleLog } from "./notice.js";
18
19
  /** Human-readable line for the run-orchestrator's `--signal-other` log (#637). */
19
20
  function formatSignalLine(issue, pid, result) {
20
21
  switch (result.reason) {
@@ -36,6 +37,7 @@ import { getIssueInfo, sortByDependencies, parseBatches, runIssueWithLogging, em
36
37
  import { reconcileStateAtStartup } from "./state-utils.js";
37
38
  import { getCommitHash } from "./git-diff-utils.js";
38
39
  import { MetricsWriter } from "./metrics-writer.js";
40
+ import { WorkflowEventEmitter } from "./event-emitter.js";
39
41
  import { determineOutcome } from "./metrics-schema.js";
40
42
  import { getTokenUsageForRun } from "./token-utils.js";
41
43
  import { resolveRunOptions, buildExecutionConfig } from "./config-resolver.js";
@@ -65,12 +67,31 @@ export class RunOrchestrator {
65
67
  cfg;
66
68
  issueStates = new Map();
67
69
  phaseStartTimes = new Map();
70
+ emitter;
68
71
  done = false;
69
72
  constructor(config) {
70
73
  this.validate(config);
74
+ // Build the event emitter before wrapProgress so the wrapper can route
75
+ // status transitions through `issue_status_changed` events (AC-3).
76
+ this.emitter = new WorkflowEventEmitter({
77
+ onListenerError: (event, error) => {
78
+ // Mirror the orchestrator's verbose-gated non-fatal warning style.
79
+ // Listener failures must never propagate to the run.
80
+ logNonFatalWarning(` ! Event listener for "${event}" threw, ignoring`, error, config.config?.verbose ?? false);
81
+ },
82
+ });
71
83
  this.cfg = { ...config, onProgress: this.wrapProgress(config.onProgress) };
72
84
  this.initIssueStates();
73
85
  }
86
+ /**
87
+ * Returns the workflow event emitter. External consumers (TUI, MCP server,
88
+ * future webhooks) call `getEmitter().on(...)` to subscribe to lifecycle
89
+ * events. Subscribing is opt-in — the orchestrator runs unaware of who is
90
+ * listening (#504, AC-3).
91
+ */
92
+ getEmitter() {
93
+ return this.emitter;
94
+ }
74
95
  /**
75
96
  * Point-in-time view of the entire run.
76
97
  *
@@ -97,9 +118,14 @@ export class RunOrchestrator {
97
118
  capturedAt: new Date(),
98
119
  };
99
120
  }
100
- /** Mark the run as completed so the dashboard can unmount. */
121
+ /**
122
+ * Mark the run as completed so the dashboard can unmount and drop event
123
+ * subscribers. Drains the emitter to prevent leaks across multiple
124
+ * `run()` invocations in the same process (e.g. the MCP server).
125
+ */
101
126
  markDone() {
102
127
  this.done = true;
128
+ this.emitter.removeAllListeners();
103
129
  }
104
130
  initIssueStates() {
105
131
  const { issueInfoMap, worktreeMap, config } = this.cfg;
@@ -129,6 +155,7 @@ export class RunOrchestrator {
129
155
  if (!state)
130
156
  return;
131
157
  if (event === "start") {
158
+ const wasStatus = state.status;
132
159
  if (!state.startedAt)
133
160
  state.startedAt = new Date();
134
161
  state.status = "running";
@@ -143,6 +170,19 @@ export class RunOrchestrator {
143
170
  const p = findOrAppendPhase(state, phase);
144
171
  p.status = "running";
145
172
  p.startedAt = now;
173
+ // Fire-and-forget — listener safety guaranteed by the emitter (AC-5).
174
+ void this.emitter.emit("phase_started", {
175
+ issueNumber: issue,
176
+ phase,
177
+ iteration: extra?.iteration,
178
+ });
179
+ if (wasStatus !== "running") {
180
+ void this.emitter.emit("issue_status_changed", {
181
+ issueNumber: issue,
182
+ from: wasStatus,
183
+ to: "running",
184
+ });
185
+ }
146
186
  return;
147
187
  }
148
188
  if (event === "activity") {
@@ -155,6 +195,11 @@ export class RunOrchestrator {
155
195
  return;
156
196
  state.currentPhase.nowLine = line;
157
197
  state.currentPhase.lastActivityAt = new Date();
198
+ void this.emitter.emit("progress", {
199
+ issueNumber: issue,
200
+ phase,
201
+ text: line,
202
+ });
158
203
  return;
159
204
  }
160
205
  // complete / failed
@@ -170,18 +215,48 @@ export class RunOrchestrator {
170
215
  p.status = event === "complete" ? "done" : "failed";
171
216
  p.elapsedMs = elapsedMs;
172
217
  state.currentPhase = undefined;
218
+ const durationSec = elapsedMs !== undefined ? Math.round(elapsedMs / 1000) : undefined;
173
219
  if (event === "failed") {
220
+ const prev = state.status;
174
221
  state.status = "failed";
175
222
  state.completedAt = new Date();
223
+ void this.emitter.emit("phase_failed", {
224
+ issueNumber: issue,
225
+ phase,
226
+ duration: durationSec,
227
+ error: extra?.error ?? "unknown",
228
+ iteration: extra?.iteration,
229
+ });
230
+ if (prev !== "failed") {
231
+ void this.emitter.emit("issue_status_changed", {
232
+ issueNumber: issue,
233
+ from: prev,
234
+ to: "failed",
235
+ });
236
+ }
176
237
  return;
177
238
  }
239
+ void this.emitter.emit("phase_completed", {
240
+ issueNumber: issue,
241
+ phase,
242
+ duration: durationSec ?? 0,
243
+ iteration: extra?.iteration,
244
+ });
178
245
  // Completed phase: if it's the last phase in the plan, mark issue passed.
179
246
  const allDone = state.phases.every((ph) => ph.status === "done" || ph.status === "failed");
180
247
  if (allDone) {
248
+ const prev = state.status;
181
249
  state.status = state.phases.some((ph) => ph.status === "failed")
182
250
  ? "failed"
183
251
  : "passed";
184
252
  state.completedAt = new Date();
253
+ if (prev !== state.status) {
254
+ void this.emitter.emit("issue_status_changed", {
255
+ issueNumber: issue,
256
+ from: prev,
257
+ to: state.status,
258
+ });
259
+ }
185
260
  }
186
261
  }
187
262
  /**
@@ -243,7 +318,7 @@ export class RunOrchestrator {
243
318
  * issue discovery → worktree creation → execution → metrics → cleanup.
244
319
  */
245
320
  static async run(init, issueArgs, batches) {
246
- const { manifest, onProgress, settings } = init;
321
+ const { manifest, onProgress, phasePauseHandle, settings } = init;
247
322
  // ── Config resolution ──────────────────────────────────────────────
248
323
  const resolved = RunOrchestrator.resolveConfig(init, issueArgs, batches);
249
324
  const { mergedOptions, config, baseBranch } = resolved;
@@ -450,18 +525,24 @@ export class RunOrchestrator {
450
525
  packageManager: manifest.packageManager,
451
526
  baseBranch,
452
527
  onProgress,
528
+ onPhasePlan: init.onPhasePlan,
529
+ phasePauseHandle,
453
530
  });
454
531
  init.onOrchestratorReady?.(orchestrator);
455
532
  try {
456
533
  if (resolvedBatches) {
457
534
  for (let batchIdx = 0; batchIdx < resolvedBatches.length; batchIdx++) {
458
535
  const batch = resolvedBatches[batchIdx];
459
- console.log(chalk.blue(`\n Batch ${batchIdx + 1}/${resolvedBatches.length}: Issues ${batch.map((n) => `#${n}`).join(", ")}`));
536
+ // #647 AC-3: between-batches in a multi-batch run, the renderer is
537
+ // still alive and may have a populated live zone from the previous
538
+ // batch. Route through `bracketedConsoleLog` so log-update's cursor
539
+ // model stays consistent.
540
+ bracketedConsoleLog(phasePauseHandle, chalk.blue(`\n Batch ${batchIdx + 1}/${resolvedBatches.length}: Issues ${batch.map((n) => `#${n}`).join(", ")}`));
460
541
  const batchResults = await orchestrator.execute(batch);
461
542
  results.push(...batchResults);
462
543
  const batchFailed = batchResults.some((r) => !r.success);
463
544
  if (batchFailed && config.sequential) {
464
- console.log(chalk.yellow(`\n ! Batch ${batchIdx + 1} failed, stopping batch execution`));
545
+ bracketedConsoleLog(phasePauseHandle, chalk.yellow(`\n ! Batch ${batchIdx + 1} failed, stopping batch execution`));
465
546
  break;
466
547
  }
467
548
  }
@@ -546,6 +627,8 @@ export class RunOrchestrator {
546
627
  packageManager: this.cfg.packageManager,
547
628
  baseBranch: this.cfg.baseBranch,
548
629
  onProgress: this.cfg.onProgress,
630
+ onPhasePlan: this.cfg.onPhasePlan,
631
+ phasePauseHandle: this.cfg.phasePauseHandle,
549
632
  };
550
633
  }
551
634
  async executeSequential(issueNumbers, batchCtx, options) {
@@ -632,7 +715,7 @@ export class RunOrchestrator {
632
715
  }
633
716
  async executeOneIssue(args) {
634
717
  const { issueNumber, batchCtx, chain, parallelIssueNumber } = args;
635
- const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, } = batchCtx;
718
+ const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, } = batchCtx;
636
719
  const issueInfo = issueInfoMap.get(issueNumber) ?? {
637
720
  title: `Issue #${issueNumber}`,
638
721
  labels: [],
@@ -655,15 +738,46 @@ export class RunOrchestrator {
655
738
  packageManager,
656
739
  baseBranch,
657
740
  onProgress,
741
+ onPhasePlan,
742
+ phasePauseHandle,
658
743
  };
659
- const result = await runIssueWithLogging(ctx);
660
- if (logWriter && result.prNumber && result.prUrl) {
661
- logWriter.setPRInfo(result.prNumber, result.prUrl, parallelIssueNumber);
744
+ // Fire-and-forget orchestrator does not await listener completion on
745
+ // the lifecycle bracket events. Listener safety is the emitter's job (AC-5).
746
+ void this.emitter.emit("run_started", { issueNumber });
747
+ const issueStartedAt = Date.now();
748
+ // `run_completed` is emitted in the finally so the bracket stays
749
+ // symmetric with `run_started` even if `runIssueWithLogging` throws —
750
+ // subscribers (MCP, dashboard) can rely on every started run ending.
751
+ let result;
752
+ try {
753
+ result = await runIssueWithLogging(ctx);
754
+ // Surface QA verdicts as a dedicated event so consumers don't have to
755
+ // re-parse phase output. Emits at most once per QA phase result.
756
+ for (const pr of result.phaseResults) {
757
+ if (pr.phase === "qa" && pr.verdict) {
758
+ void this.emitter.emit("qa_verdict", {
759
+ issueNumber,
760
+ phase: "qa",
761
+ verdict: pr.verdict,
762
+ });
763
+ }
764
+ }
765
+ if (logWriter && result.prNumber && result.prUrl) {
766
+ logWriter.setPRInfo(result.prNumber, result.prUrl, parallelIssueNumber);
767
+ }
768
+ if (logWriter) {
769
+ logWriter.completeIssue(parallelIssueNumber);
770
+ }
771
+ return result;
662
772
  }
663
- if (logWriter) {
664
- logWriter.completeIssue(parallelIssueNumber);
773
+ finally {
774
+ const durationSec = Math.round((Date.now() - issueStartedAt) / 1000);
775
+ void this.emitter.emit("run_completed", {
776
+ issueNumber,
777
+ duration: durationSec,
778
+ success: result?.success ?? false,
779
+ });
665
780
  }
666
- return result;
667
781
  }
668
782
  static async recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers) {
669
783
  const metricsWriter = new MetricsWriter({ verbose: config.verbose });
@@ -130,9 +130,27 @@ export declare class StateManager {
130
130
  */
131
131
  updateWorktreeInfo(issueNumber: number, worktree: string, branch: string): Promise<void>;
132
132
  /**
133
- * Update session ID for an issue (for resume)
133
+ * Update session ID for an issue (for resume).
134
+ *
135
+ * @deprecated Use {@link updateResumeHandle} (#674). This entry point only
136
+ * writes the legacy `sessionId` field — without an `originCwd`, the next
137
+ * phase's driver-owned `canResume()` fail-safe will decline resume, so the
138
+ * data is effectively inert. Retained for one release to keep callers from
139
+ * older sequant builds compiling.
134
140
  */
135
141
  updateSessionId(issueNumber: number, sessionId: string): Promise<void>;
142
+ /**
143
+ * Update the driver-tagged resume handle for an issue (#674).
144
+ *
145
+ * Writes both the new `resumeHandle` field and mirrors the token into
146
+ * the deprecated `sessionId` field so state files round-trip through
147
+ * older sequant readers for one release.
148
+ */
149
+ updateResumeHandle(issueNumber: number, handle: {
150
+ driver: string;
151
+ token: string;
152
+ originCwd: string;
153
+ }): Promise<void>;
136
154
  /**
137
155
  * Set or clear the relay state for an issue (#383).
138
156
  *
@@ -378,7 +378,13 @@ export class StateManager {
378
378
  }
379
379
  }
380
380
  /**
381
- * Update session ID for an issue (for resume)
381
+ * Update session ID for an issue (for resume).
382
+ *
383
+ * @deprecated Use {@link updateResumeHandle} (#674). This entry point only
384
+ * writes the legacy `sessionId` field — without an `originCwd`, the next
385
+ * phase's driver-owned `canResume()` fail-safe will decline resume, so the
386
+ * data is effectively inert. Retained for one release to keep callers from
387
+ * older sequant builds compiling.
382
388
  */
383
389
  async updateSessionId(issueNumber, sessionId) {
384
390
  await this.withLock(async () => {
@@ -392,6 +398,26 @@ export class StateManager {
392
398
  await this.saveState(state);
393
399
  });
394
400
  }
401
+ /**
402
+ * Update the driver-tagged resume handle for an issue (#674).
403
+ *
404
+ * Writes both the new `resumeHandle` field and mirrors the token into
405
+ * the deprecated `sessionId` field so state files round-trip through
406
+ * older sequant readers for one release.
407
+ */
408
+ async updateResumeHandle(issueNumber, handle) {
409
+ await this.withLock(async () => {
410
+ const state = await this.getState();
411
+ const issueState = state.issues[String(issueNumber)];
412
+ if (!issueState) {
413
+ throw new Error(`Issue #${issueNumber} not found in state`);
414
+ }
415
+ issueState.resumeHandle = handle;
416
+ issueState.sessionId = handle.token;
417
+ issueState.lastActivity = new Date().toISOString();
418
+ await this.saveState(state);
419
+ });
420
+ }
395
421
  /**
396
422
  * Set or clear the relay state for an issue (#383).
397
423
  *
@@ -19,9 +19,14 @@
19
19
  import { z } from "zod";
20
20
  import { PhaseSchema } from "./types.js";
21
21
  /**
22
- * Workflow phases in order of execution
22
+ * Workflow phases in order of execution.
23
+ *
24
+ * Sourced from the phase registry — replaces the previous `PhaseSchema.options`
25
+ * literal that existed when `PhaseSchema` was a `z.enum`. Computed at module
26
+ * load (after `built-in-phases.ts` has run); insertion order is the canonical
27
+ * pipeline order.
23
28
  */
24
- export declare const WORKFLOW_PHASES: ("qa" | "loop" | "verify" | "spec" | "security-review" | "exec" | "testgen" | "test" | "merger")[];
29
+ export declare const WORKFLOW_PHASES: readonly string[];
25
30
  /**
26
31
  * Phase status - tracks individual phase progress
27
32
  */
@@ -41,6 +46,7 @@ export declare const IssueStatusSchema: z.ZodEnum<{
41
46
  in_progress: "in_progress";
42
47
  not_started: "not_started";
43
48
  waiting_for_qa_gate: "waiting_for_qa_gate";
49
+ waiting_for_human_merge: "waiting_for_human_merge";
44
50
  ready_for_merge: "ready_for_merge";
45
51
  blocked: "blocked";
46
52
  abandoned: "abandoned";
@@ -54,17 +60,7 @@ export type { Phase } from "./types.js";
54
60
  * Embedded as HTML comments: `<!-- SEQUANT_PHASE: {...} -->`
55
61
  */
56
62
  export declare const PhaseMarkerSchema: z.ZodObject<{
57
- phase: z.ZodEnum<{
58
- qa: "qa";
59
- loop: "loop";
60
- verify: "verify";
61
- spec: "spec";
62
- "security-review": "security-review";
63
- exec: "exec";
64
- testgen: "testgen";
65
- test: "test";
66
- merger: "merger";
67
- }>;
63
+ phase: z.ZodString;
68
64
  status: z.ZodEnum<{
69
65
  pending: "pending";
70
66
  skipped: "skipped";
@@ -227,23 +223,14 @@ export declare const IssueStateSchema: z.ZodObject<{
227
223
  in_progress: "in_progress";
228
224
  not_started: "not_started";
229
225
  waiting_for_qa_gate: "waiting_for_qa_gate";
226
+ waiting_for_human_merge: "waiting_for_human_merge";
230
227
  ready_for_merge: "ready_for_merge";
231
228
  blocked: "blocked";
232
229
  abandoned: "abandoned";
233
230
  }>;
234
231
  worktree: z.ZodOptional<z.ZodString>;
235
232
  branch: z.ZodOptional<z.ZodString>;
236
- currentPhase: z.ZodOptional<z.ZodEnum<{
237
- qa: "qa";
238
- loop: "loop";
239
- verify: "verify";
240
- spec: "spec";
241
- "security-review": "security-review";
242
- exec: "exec";
243
- testgen: "testgen";
244
- test: "test";
245
- merger: "merger";
246
- }>>;
233
+ currentPhase: z.ZodOptional<z.ZodString>;
247
234
  phases: z.ZodRecord<z.ZodString, z.ZodObject<{
248
235
  status: z.ZodEnum<{
249
236
  pending: "pending";
@@ -348,6 +335,11 @@ export declare const IssueStateSchema: z.ZodObject<{
348
335
  messageCount: z.ZodNumber;
349
336
  }, z.core.$strip>>;
350
337
  sessionId: z.ZodOptional<z.ZodString>;
338
+ resumeHandle: z.ZodOptional<z.ZodObject<{
339
+ driver: z.ZodString;
340
+ token: z.ZodString;
341
+ originCwd: z.ZodString;
342
+ }, z.core.$strip>>;
351
343
  resolvedAt: z.ZodOptional<z.ZodString>;
352
344
  lastActivity: z.ZodString;
353
345
  createdAt: z.ZodString;
@@ -370,23 +362,14 @@ export declare const WorkflowStateSchema: z.ZodObject<{
370
362
  in_progress: "in_progress";
371
363
  not_started: "not_started";
372
364
  waiting_for_qa_gate: "waiting_for_qa_gate";
365
+ waiting_for_human_merge: "waiting_for_human_merge";
373
366
  ready_for_merge: "ready_for_merge";
374
367
  blocked: "blocked";
375
368
  abandoned: "abandoned";
376
369
  }>;
377
370
  worktree: z.ZodOptional<z.ZodString>;
378
371
  branch: z.ZodOptional<z.ZodString>;
379
- currentPhase: z.ZodOptional<z.ZodEnum<{
380
- qa: "qa";
381
- loop: "loop";
382
- verify: "verify";
383
- spec: "spec";
384
- "security-review": "security-review";
385
- exec: "exec";
386
- testgen: "testgen";
387
- test: "test";
388
- merger: "merger";
389
- }>>;
372
+ currentPhase: z.ZodOptional<z.ZodString>;
390
373
  phases: z.ZodRecord<z.ZodString, z.ZodObject<{
391
374
  status: z.ZodEnum<{
392
375
  pending: "pending";
@@ -491,6 +474,11 @@ export declare const WorkflowStateSchema: z.ZodObject<{
491
474
  messageCount: z.ZodNumber;
492
475
  }, z.core.$strip>>;
493
476
  sessionId: z.ZodOptional<z.ZodString>;
477
+ resumeHandle: z.ZodOptional<z.ZodObject<{
478
+ driver: z.ZodString;
479
+ token: z.ZodString;
480
+ originCwd: z.ZodString;
481
+ }, z.core.$strip>>;
494
482
  resolvedAt: z.ZodOptional<z.ZodString>;
495
483
  lastActivity: z.ZodString;
496
484
  createdAt: z.ZodString;
@@ -19,10 +19,16 @@
19
19
  import { z } from "zod";
20
20
  import { ScopeAssessmentSchema } from "../scope/types.js";
21
21
  import { PhaseSchema } from "./types.js";
22
+ import { getPhaseNames } from "./phase-registry.js";
22
23
  /**
23
- * Workflow phases in order of execution
24
+ * Workflow phases in order of execution.
25
+ *
26
+ * Sourced from the phase registry — replaces the previous `PhaseSchema.options`
27
+ * literal that existed when `PhaseSchema` was a `z.enum`. Computed at module
28
+ * load (after `built-in-phases.ts` has run); insertion order is the canonical
29
+ * pipeline order.
24
30
  */
25
- export const WORKFLOW_PHASES = PhaseSchema.options;
31
+ export const WORKFLOW_PHASES = getPhaseNames();
26
32
  /**
27
33
  * Phase status - tracks individual phase progress
28
34
  */
@@ -40,6 +46,7 @@ export const IssueStatusSchema = z.enum([
40
46
  "not_started", // Issue tracked but no work begun
41
47
  "in_progress", // Actively being worked on
42
48
  "waiting_for_qa_gate", // QA completed, waiting for gate approval in chain mode
49
+ "waiting_for_human_merge", // `sequant ready` (#683) finished its A+ gate; awaiting human merge decision (never auto-merges)
43
50
  "ready_for_merge", // All phases passed, PR ready for review
44
51
  "merged", // PR merged, work complete
45
52
  "blocked", // Waiting on external input or dependency
@@ -213,8 +220,27 @@ export const IssueStateSchema = z.object({
213
220
  qaStagnation: z.array(QAStagnationEntrySchema).optional(),
214
221
  /** Relay state (#383); present when bidirectional relay is active */
215
222
  relay: RelayStateSchema.optional(),
216
- /** Claude session ID (for resume) */
223
+ /**
224
+ * Claude session ID (for resume).
225
+ * @deprecated Use {@link resumeHandle} (#674). Retained as a read-path
226
+ * mirror of `resumeHandle.token` for one release so state files written by
227
+ * prior sequant builds load cleanly. Legacy entries (token without
228
+ * `originCwd`) intentionally do NOT resume — the driver-owned `canResume`
229
+ * fail-safe disables them.
230
+ */
217
231
  sessionId: z.string().optional(),
232
+ /**
233
+ * Driver-tagged resume handle (#674). Stores the driver name, the
234
+ * resume token, and the cwd the session was created in so the next phase
235
+ * can prove cwd-equality before reattaching.
236
+ */
237
+ resumeHandle: z
238
+ .object({
239
+ driver: z.string(),
240
+ token: z.string(),
241
+ originCwd: z.string(),
242
+ })
243
+ .optional(),
218
244
  /** When the issue transitioned to a terminal status (merged/abandoned/closed) */
219
245
  resolvedAt: z.string().datetime().optional(),
220
246
  /** Most recent activity timestamp */
@@ -7,27 +7,47 @@ import type { LogWriter } from "./log-writer.js";
7
7
  import type { StateManager } from "./state-manager.js";
8
8
  import type { ShutdownManager } from "../shutdown.js";
9
9
  import type { WorktreeInfo } from "./worktree-manager.js";
10
+ export type { WorkflowEventEmitter, WorkflowEvents, WorkflowEventListener, IssueEventStatus, BaseEventPayload, RunEventPayload, PhaseStartedPayload, PhaseCompletedPayload, PhaseFailedPayload, IssueStatusChangedPayload, QaVerdictPayload, ProgressPayload, } from "./event-emitter.js";
10
11
  /**
11
12
  * Canonical Zod schema for all workflow phases.
12
13
  *
13
- * This is the single source of truth — state-schema.ts and run-log-schema.ts
14
- * both reference this definition. Add new phases here only.
14
+ * Backed by the phase registry. `PhaseSchema.parse(name)` succeeds iff
15
+ * `phaseRegistry.has(name)`. The set of valid phases is the registry's
16
+ * keys at the time of parsing — registration happens at module load,
17
+ * so for normal runtime use the set is fixed by the time any code parses.
18
+ *
19
+ * This replaces the prior `z.enum([...])` literal. The set of valid names
20
+ * is identical for the 9 built-in phases; the only observable behavior
21
+ * change is that `PhaseSchema.options` is no longer available — use
22
+ * `getPhaseNames()` from `phase-registry.ts` instead.
23
+ */
24
+ export declare const PhaseSchema: z.ZodString;
25
+ /**
26
+ * Available workflow phases. Widened from a string-literal union to `string`
27
+ * after the registry migration — exhaustiveness checking on `switch (phase)`
28
+ * is now a runtime concern (see the comment in phase-executor.ts where the
29
+ * only relevant switch lives).
15
30
  */
16
- export declare const PhaseSchema: z.ZodEnum<{
17
- qa: "qa";
18
- loop: "loop";
19
- verify: "verify";
20
- spec: "spec";
21
- "security-review": "security-review";
22
- exec: "exec";
23
- testgen: "testgen";
24
- test: "test";
25
- merger: "merger";
26
- }>;
31
+ export type Phase = string;
27
32
  /**
28
- * Available workflow phases (inferred from PhaseSchema)
33
+ * Lifecycle hook for pausing the run renderer's live zone while verbose
34
+ * Claude streaming writes through stdout, then resuming after the agent
35
+ * call completes. Replaces the legacy `PhaseSpinner` argument (#618).
36
+ *
37
+ * Lives in the workflow types barrel so the cli-ui layer can implement it
38
+ * without the workflow layer reaching back into cli-ui (#656).
29
39
  */
30
- export type Phase = z.infer<typeof PhaseSchema>;
40
+ export interface PhasePauseHandle {
41
+ pause(): void;
42
+ resume(): void;
43
+ /**
44
+ * #647 AC-3: print a notice line (e.g., retry/fallback message) without
45
+ * breaking log-update's cursor model. Implementations clear the live zone,
46
+ * write the line through the renderer's own stdout channel, then redraw.
47
+ * In quiet / non-TTY paths this degrades to a plain write.
48
+ */
49
+ appendNotice(message: string): void;
50
+ }
31
51
  /**
32
52
  * Default phases for workflow execution
33
53
  */
@@ -116,6 +136,15 @@ export interface ExecutionConfig {
116
136
  * Default: false (opt-in for the initial rollout).
117
137
  */
118
138
  relayEnabled?: boolean;
139
+ /**
140
+ * Force full-weight (standalone) QA even under an orchestrator (#683).
141
+ * When true, the phase executor sets `SEQUANT_FULL_QA=1` in the agent
142
+ * environment for the `qa` phase. The QA skill honors this flag by running
143
+ * its standalone branch-freshness / process-state pre-flight checks even
144
+ * though `SEQUANT_ORCHESTRATOR` is also set. Used by `sequant ready` so its
145
+ * QA pass does NOT skip the checks that catch the #318/#529/#570 class.
146
+ */
147
+ fullQa?: boolean;
119
148
  }
120
149
  /**
121
150
  * Default execution configuration
@@ -338,6 +367,17 @@ export type ProgressCallback = (issue: number, phase: string, event: "start" | "
338
367
  iteration?: number;
339
368
  text?: string;
340
369
  }) => void;
370
+ /**
371
+ * #672 AC-2: fired once per issue after the executor has resolved the final
372
+ * phase pipeline (post auto-detect, post resume filter, post testgen /
373
+ * security-review insertion). Lets the run renderer seed pending cells for
374
+ * the full roadmap before any phase fires, so users see what is about to run
375
+ * instead of phases appearing one at a time as they stream.
376
+ *
377
+ * Empty `phases` means "no plan known" — the renderer should fall back to
378
+ * streaming-only display.
379
+ */
380
+ export type PhasePlanCallback = (issue: number, phases: string[]) => void;
341
381
  /**
342
382
  * Shared context for executing a batch of issues.
343
383
  * Replaces 11 positional parameters in executeBatch (#402).
@@ -356,6 +396,17 @@ export interface BatchExecutionContext {
356
396
  packageManager?: string;
357
397
  baseBranch?: string;
358
398
  onProgress?: ProgressCallback;
399
+ /** #672 AC-2: forwarded to per-issue context so batch-executor can fire it
400
+ * once the final phase pipeline is known. */
401
+ onPhasePlan?: PhasePlanCallback;
402
+ /**
403
+ * Optional live-zone pause handle (#656). When set, the phase executor calls
404
+ * `pause()` before forwarding verbose Claude SDK output to stdout and
405
+ * `resume()` after the agent call completes — so the 1Hz live grid does not
406
+ * collide with streaming text. Wired from the active `RunRenderer` at the
407
+ * composition root in `run.ts`; left undefined for quiet/TUI modes.
408
+ */
409
+ phasePauseHandle?: PhasePauseHandle;
359
410
  }
360
411
  /**
361
412
  * Context object for executing a single issue through the workflow.
@@ -405,4 +456,12 @@ export interface IssueExecutionContext {
405
456
  baseBranch?: string;
406
457
  /** Per-phase progress callback (used in parallel mode) */
407
458
  onProgress?: ProgressCallback;
459
+ /** #672 AC-2: invoked once after the per-issue phase plan resolves. */
460
+ onPhasePlan?: PhasePlanCallback;
461
+ /**
462
+ * Optional live-zone pause handle (#656). Forwarded to
463
+ * `executePhaseWithRetry` so the renderer's `pause`/`resume` hooks fire
464
+ * around verbose Claude streaming.
465
+ */
466
+ phasePauseHandle?: PhasePauseHandle;
408
467
  }
@@ -2,23 +2,28 @@
2
2
  * Core types for workflow execution
3
3
  */
4
4
  import { z } from "zod";
5
+ // Importing the registry triggers its side-effect registrations (built-ins
6
+ // live at the bottom of phase-registry.ts), guaranteeing the registry is
7
+ // populated before any PhaseSchema parse runs.
8
+ import { phaseRegistry, getPhaseNames } from "./phase-registry.js";
5
9
  /**
6
10
  * Canonical Zod schema for all workflow phases.
7
11
  *
8
- * This is the single source of truth — state-schema.ts and run-log-schema.ts
9
- * both reference this definition. Add new phases here only.
12
+ * Backed by the phase registry. `PhaseSchema.parse(name)` succeeds iff
13
+ * `phaseRegistry.has(name)`. The set of valid phases is the registry's
14
+ * keys at the time of parsing — registration happens at module load,
15
+ * so for normal runtime use the set is fixed by the time any code parses.
16
+ *
17
+ * This replaces the prior `z.enum([...])` literal. The set of valid names
18
+ * is identical for the 9 built-in phases; the only observable behavior
19
+ * change is that `PhaseSchema.options` is no longer available — use
20
+ * `getPhaseNames()` from `phase-registry.ts` instead.
10
21
  */
11
- export const PhaseSchema = z.enum([
12
- "spec",
13
- "security-review",
14
- "exec",
15
- "testgen",
16
- "test",
17
- "verify",
18
- "qa",
19
- "loop",
20
- "merger",
21
- ]);
22
+ export const PhaseSchema = z
23
+ .string()
24
+ .refine((name) => phaseRegistry.has(name), {
25
+ error: (issue) => `Unknown phase "${String(issue.input)}". Available: ${getPhaseNames().join(", ")}`,
26
+ });
22
27
  /**
23
28
  * Default phases for workflow execution
24
29
  */