sequant 2.2.0 → 2.4.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 (156) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +81 -5
  4. package/dist/bin/cli.js +140 -13
  5. package/dist/src/commands/abort.d.ts +36 -0
  6. package/dist/src/commands/abort.js +138 -0
  7. package/dist/src/commands/doctor.d.ts +25 -0
  8. package/dist/src/commands/doctor.js +36 -1
  9. package/dist/src/commands/locks.d.ts +67 -0
  10. package/dist/src/commands/locks.js +290 -0
  11. package/dist/src/commands/merge.js +11 -0
  12. package/dist/src/commands/prompt.d.ts +46 -0
  13. package/dist/src/commands/prompt.js +273 -0
  14. package/dist/src/commands/run-display.d.ts +11 -2
  15. package/dist/src/commands/run-display.js +62 -28
  16. package/dist/src/commands/run-progress.d.ts +42 -0
  17. package/dist/src/commands/run-progress.js +93 -0
  18. package/dist/src/commands/run.js +90 -18
  19. package/dist/src/commands/stats.d.ts +2 -0
  20. package/dist/src/commands/stats.js +94 -8
  21. package/dist/src/commands/status.js +12 -0
  22. package/dist/src/commands/watch.d.ts +18 -0
  23. package/dist/src/commands/watch.js +211 -0
  24. package/dist/src/lib/ac-linter.d.ts +1 -1
  25. package/dist/src/lib/ac-linter.js +81 -0
  26. package/dist/src/lib/assess-collision-detect.d.ts +91 -0
  27. package/dist/src/lib/assess-collision-detect.js +217 -0
  28. package/dist/src/lib/assess-comment-parser.d.ts +59 -1
  29. package/dist/src/lib/assess-comment-parser.js +124 -2
  30. package/dist/src/lib/cli-ui/format.d.ts +19 -0
  31. package/dist/src/lib/cli-ui/format.js +34 -0
  32. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +220 -0
  33. package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
  34. package/dist/src/lib/cli-ui/run-renderer.d.ts +265 -0
  35. package/dist/src/lib/cli-ui/run-renderer.js +1390 -0
  36. package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
  37. package/dist/src/lib/cli-ui/scrollback-harness.js +294 -0
  38. package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
  39. package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
  40. package/dist/src/lib/locks/index.d.ts +7 -0
  41. package/dist/src/lib/locks/index.js +5 -0
  42. package/dist/src/lib/locks/lock-manager.d.ts +168 -0
  43. package/dist/src/lib/locks/lock-manager.js +433 -0
  44. package/dist/src/lib/locks/types.d.ts +59 -0
  45. package/dist/src/lib/locks/types.js +31 -0
  46. package/dist/src/lib/merge-check/types.js +1 -1
  47. package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
  48. package/dist/src/lib/qa/markdown-only-ci.js +74 -0
  49. package/dist/src/lib/relay/activation.d.ts +60 -0
  50. package/dist/src/lib/relay/activation.js +122 -0
  51. package/dist/src/lib/relay/archive.d.ts +34 -0
  52. package/dist/src/lib/relay/archive.js +112 -0
  53. package/dist/src/lib/relay/frame.d.ts +20 -0
  54. package/dist/src/lib/relay/frame.js +76 -0
  55. package/dist/src/lib/relay/index.d.ts +13 -0
  56. package/dist/src/lib/relay/index.js +13 -0
  57. package/dist/src/lib/relay/paths.d.ts +43 -0
  58. package/dist/src/lib/relay/paths.js +59 -0
  59. package/dist/src/lib/relay/pid.d.ts +34 -0
  60. package/dist/src/lib/relay/pid.js +72 -0
  61. package/dist/src/lib/relay/reader.d.ts +35 -0
  62. package/dist/src/lib/relay/reader.js +115 -0
  63. package/dist/src/lib/relay/types.d.ts +70 -0
  64. package/dist/src/lib/relay/types.js +85 -0
  65. package/dist/src/lib/relay/writer.d.ts +48 -0
  66. package/dist/src/lib/relay/writer.js +113 -0
  67. package/dist/src/lib/settings.d.ts +31 -1
  68. package/dist/src/lib/settings.js +18 -3
  69. package/dist/src/lib/version-check.d.ts +60 -5
  70. package/dist/src/lib/version-check.js +97 -9
  71. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  72. package/dist/src/lib/workflow/batch-executor.js +274 -185
  73. package/dist/src/lib/workflow/config-resolver.js +4 -0
  74. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
  75. package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
  76. package/dist/src/lib/workflow/drivers/aider.js +9 -0
  77. package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
  78. package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
  79. package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
  80. package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
  81. package/dist/src/lib/workflow/event-emitter.js +102 -0
  82. package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
  83. package/dist/src/lib/workflow/heartbeat.js +194 -0
  84. package/dist/src/lib/workflow/notice.d.ts +32 -0
  85. package/dist/src/lib/workflow/notice.js +38 -0
  86. package/dist/src/lib/workflow/phase-executor.d.ts +58 -16
  87. package/dist/src/lib/workflow/phase-executor.js +244 -130
  88. package/dist/src/lib/workflow/phase-mapper.d.ts +27 -13
  89. package/dist/src/lib/workflow/phase-mapper.js +70 -51
  90. package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
  91. package/dist/src/lib/workflow/phase-registry.js +233 -0
  92. package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
  93. package/dist/src/lib/workflow/platforms/github.js +20 -3
  94. package/dist/src/lib/workflow/pr-status.d.ts +18 -2
  95. package/dist/src/lib/workflow/pr-status.js +41 -9
  96. package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
  97. package/dist/src/lib/workflow/qa-stagnation.js +179 -0
  98. package/dist/src/lib/workflow/run-log-schema.d.ts +5 -55
  99. package/dist/src/lib/workflow/run-orchestrator.d.ts +70 -1
  100. package/dist/src/lib/workflow/run-orchestrator.js +464 -25
  101. package/dist/src/lib/workflow/run-reflect.js +1 -1
  102. package/dist/src/lib/workflow/run-state.d.ts +71 -0
  103. package/dist/src/lib/workflow/run-state.js +14 -0
  104. package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
  105. package/dist/src/lib/workflow/state-cleanup.js +17 -5
  106. package/dist/src/lib/workflow/state-manager.d.ts +31 -2
  107. package/dist/src/lib/workflow/state-manager.js +64 -1
  108. package/dist/src/lib/workflow/state-schema.d.ts +82 -35
  109. package/dist/src/lib/workflow/state-schema.js +63 -4
  110. package/dist/src/lib/workflow/types.d.ts +139 -16
  111. package/dist/src/lib/workflow/types.js +18 -13
  112. package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
  113. package/dist/src/lib/workflow/worktree-manager.js +15 -6
  114. package/dist/src/mcp/tools/run.d.ts +44 -0
  115. package/dist/src/mcp/tools/run.js +104 -13
  116. package/dist/src/ui/tui/App.d.ts +14 -0
  117. package/dist/src/ui/tui/App.js +41 -0
  118. package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
  119. package/dist/src/ui/tui/ElapsedTimer.js +31 -0
  120. package/dist/src/ui/tui/Header.d.ts +6 -0
  121. package/dist/src/ui/tui/Header.js +15 -0
  122. package/dist/src/ui/tui/IssueBox.d.ts +16 -0
  123. package/dist/src/ui/tui/IssueBox.js +68 -0
  124. package/dist/src/ui/tui/Spinner.d.ts +9 -0
  125. package/dist/src/ui/tui/Spinner.js +18 -0
  126. package/dist/src/ui/tui/index.d.ts +15 -0
  127. package/dist/src/ui/tui/index.js +29 -0
  128. package/dist/src/ui/tui/theme.d.ts +29 -0
  129. package/dist/src/ui/tui/theme.js +52 -0
  130. package/dist/src/ui/tui/truncate.d.ts +11 -0
  131. package/dist/src/ui/tui/truncate.js +31 -0
  132. package/package.json +14 -6
  133. package/templates/agents/sequant-explorer.md +1 -0
  134. package/templates/agents/sequant-qa-checker.md +2 -1
  135. package/templates/agents/sequant-testgen.md +1 -0
  136. package/templates/hooks/post-tool.sh +92 -0
  137. package/templates/hooks/pre-tool.sh +18 -9
  138. package/templates/hooks/relay-check.sh +107 -0
  139. package/templates/relay/frame.txt +11 -0
  140. package/templates/scripts/cleanup-worktree.sh +25 -3
  141. package/templates/scripts/new-feature.sh +6 -0
  142. package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
  143. package/templates/skills/_shared/references/subagent-types.md +21 -8
  144. package/templates/skills/assess/SKILL.md +122 -68
  145. package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
  146. package/templates/skills/docs/SKILL.md +141 -22
  147. package/templates/skills/exec/SKILL.md +10 -8
  148. package/templates/skills/fullsolve/SKILL.md +79 -5
  149. package/templates/skills/loop/SKILL.md +28 -0
  150. package/templates/skills/merger/SKILL.md +621 -0
  151. package/templates/skills/qa/SKILL.md +727 -8
  152. package/templates/skills/setup/SKILL.md +12 -6
  153. package/templates/skills/spec/SKILL.md +52 -0
  154. package/templates/skills/spec/references/parallel-groups.md +7 -0
  155. package/templates/skills/spec/references/recommended-workflow.md +4 -2
  156. package/templates/skills/testgen/SKILL.md +24 -17
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Runtime state snapshots for multi-issue dashboard rendering.
3
+ *
4
+ * Decouples the TUI from the orchestrator: `getSnapshot()` returns an
5
+ * immutable, plain-object view of current run state that can be safely
6
+ * read from a render loop without holding locks.
7
+ */
8
+ /**
9
+ * Format a coarse "now" line for a phase transition.
10
+ * Used as the M1 default when no finer activity signal exists.
11
+ */
12
+ export function formatCoarseNowLine(phase) {
13
+ return `running ${phase}`;
14
+ }
@@ -58,9 +58,9 @@ export interface ReconcileOptions {
58
58
  export interface ReconcileResult {
59
59
  /** Whether reconciliation was successful */
60
60
  success: boolean;
61
- /** Issues that were advanced from ready_for_merge to merged */
61
+ /** Issues advanced to `merged` (from `ready_for_merge`, `in_progress`, or `waiting_for_qa_gate`) */
62
62
  advanced: number[];
63
- /** Issues checked but still ready_for_merge */
63
+ /** Issues checked but not yet merged (status unchanged) */
64
64
  stillPending: number[];
65
65
  /** Error message if failed */
66
66
  error?: string;
@@ -68,10 +68,18 @@ export interface ReconcileResult {
68
68
  /**
69
69
  * Lightweight state reconciliation at run start
70
70
  *
71
- * Checks issues in `ready_for_merge` state and advances them to `merged`
72
- * if their PRs are merged or their branches are in main.
71
+ * Checks issues in `ready_for_merge`, `in_progress`, or `waiting_for_qa_gate`
72
+ * state and advances them to `merged` if their PRs are merged or their branches
73
+ * are in main.
73
74
  *
74
- * This prevents re-running already completed issues.
75
+ * Including `in_progress` covers the case where a PR was merged outside
76
+ * this sequant session (separate process, `gh pr merge`, web UI) — without
77
+ * it, the run command would re-execute already-merged issues. See #592.
78
+ *
79
+ * Including `waiting_for_qa_gate` covers the symmetric case where a PR
80
+ * awaiting human QA-gate approval is merged externally before the next
81
+ * sequant session — without it, `sequant run <N>` re-executes the QA phase
82
+ * against already-merged work. See #606.
75
83
  *
76
84
  * @param options - Reconciliation options
77
85
  * @returns Result with lists of advanced and still-pending issues
@@ -188,10 +188,18 @@ export async function cleanupStaleEntries(options = {}) {
188
188
  /**
189
189
  * Lightweight state reconciliation at run start
190
190
  *
191
- * Checks issues in `ready_for_merge` state and advances them to `merged`
192
- * if their PRs are merged or their branches are in main.
191
+ * Checks issues in `ready_for_merge`, `in_progress`, or `waiting_for_qa_gate`
192
+ * state and advances them to `merged` if their PRs are merged or their branches
193
+ * are in main.
193
194
  *
194
- * This prevents re-running already completed issues.
195
+ * Including `in_progress` covers the case where a PR was merged outside
196
+ * this sequant session (separate process, `gh pr merge`, web UI) — without
197
+ * it, the run command would re-execute already-merged issues. See #592.
198
+ *
199
+ * Including `waiting_for_qa_gate` covers the symmetric case where a PR
200
+ * awaiting human QA-gate approval is merged externally before the next
201
+ * sequant session — without it, `sequant run <N>` re-executes the QA phase
202
+ * against already-merged work. See #606.
195
203
  *
196
204
  * @param options - Reconciliation options
197
205
  * @returns Result with lists of advanced and still-pending issues
@@ -213,9 +221,13 @@ export async function reconcileStateAtStartup(options = {}) {
213
221
  const state = await manager.getState();
214
222
  const advanced = [];
215
223
  const stillPending = [];
216
- // Find issues in ready_for_merge state
224
+ // Find issues in ready_for_merge, in_progress, or waiting_for_qa_gate state.
225
+ // in_progress covers PRs merged outside this session (#592).
226
+ // waiting_for_qa_gate covers PRs merged before the next QA-gate run (#606).
217
227
  for (const [issueNumStr, issueState] of Object.entries(state.issues)) {
218
- if (issueState.status !== "ready_for_merge") {
228
+ if (issueState.status !== "ready_for_merge" &&
229
+ issueState.status !== "in_progress" &&
230
+ issueState.status !== "waiting_for_qa_gate") {
219
231
  continue;
220
232
  }
221
233
  const issueNum = parseInt(issueNumStr, 10);
@@ -20,7 +20,7 @@
20
20
  * const state = await manager.getIssueState(42);
21
21
  * ```
22
22
  */
23
- import { type WorkflowState, type IssueState, type Phase, type PhaseStatus, type IssueStatus, type PRInfo, type AcceptanceCriteria, type ACStatus } from "./state-schema.js";
23
+ import { type WorkflowState, type IssueState, type Phase, type PhaseStatus, type IssueStatus, type PRInfo, type AcceptanceCriteria, type ACStatus, type RelayState } from "./state-schema.js";
24
24
  import type { ScopeAssessment } from "../scope/types.js";
25
25
  export interface StateManagerOptions {
26
26
  /** Path to state file (default: .sequant/state.json) */
@@ -130,9 +130,38 @@ 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>;
154
+ /**
155
+ * Set or clear the relay state for an issue (#383).
156
+ *
157
+ * Pass `null` to remove the relay field entirely (deactivation). Pass an
158
+ * object to overwrite the current relay state (activation or refresh).
159
+ */
160
+ setRelayState(issueNumber: number, relay: RelayState | null): Promise<void>;
161
+ /**
162
+ * Increment the relay message counter. No-op when relay isn't active.
163
+ */
164
+ incrementRelayMessageCount(issueNumber: number, delta?: number): Promise<void>;
136
165
  /**
137
166
  * Update loop iteration for an issue
138
167
  */
@@ -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,63 @@ 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
+ }
421
+ /**
422
+ * Set or clear the relay state for an issue (#383).
423
+ *
424
+ * Pass `null` to remove the relay field entirely (deactivation). Pass an
425
+ * object to overwrite the current relay state (activation or refresh).
426
+ */
427
+ async setRelayState(issueNumber, relay) {
428
+ await this.withLock(async () => {
429
+ const state = await this.getState();
430
+ const issueState = state.issues[String(issueNumber)];
431
+ if (!issueState) {
432
+ throw new Error(`Issue #${issueNumber} not found in state`);
433
+ }
434
+ if (relay === null) {
435
+ delete issueState.relay;
436
+ }
437
+ else {
438
+ issueState.relay = relay;
439
+ }
440
+ issueState.lastActivity = new Date().toISOString();
441
+ await this.saveState(state);
442
+ });
443
+ }
444
+ /**
445
+ * Increment the relay message counter. No-op when relay isn't active.
446
+ */
447
+ async incrementRelayMessageCount(issueNumber, delta = 1) {
448
+ await this.withLock(async () => {
449
+ const state = await this.getState();
450
+ const issueState = state.issues[String(issueNumber)];
451
+ if (!issueState || !issueState.relay)
452
+ return;
453
+ issueState.relay.messageCount += delta;
454
+ issueState.lastActivity = new Date().toISOString();
455
+ await this.saveState(state);
456
+ });
457
+ }
395
458
  /**
396
459
  * Update loop iteration for an issue
397
460
  */
@@ -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
  */
@@ -54,17 +59,7 @@ export type { Phase } from "./types.js";
54
59
  * Embedded as HTML comments: `<!-- SEQUANT_PHASE: {...} -->`
55
60
  */
56
61
  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
- }>;
62
+ phase: z.ZodString;
68
63
  status: z.ZodEnum<{
69
64
  pending: "pending";
70
65
  skipped: "skipped";
@@ -186,6 +181,36 @@ export declare const AcceptanceCriteriaSchema: z.ZodObject<{
186
181
  }, z.core.$strip>;
187
182
  }, z.core.$strip>;
188
183
  export type AcceptanceCriteria = z.infer<typeof AcceptanceCriteriaSchema>;
184
+ /**
185
+ * Single QA stagnation observation.
186
+ *
187
+ * Recorded when fullsolve detects a same-SHA same-verdict cycle so future
188
+ * runs can spot "we've been here before" without re-deriving it from comments.
189
+ */
190
+ export declare const QAStagnationEntrySchema: z.ZodObject<{
191
+ sha: z.ZodString;
192
+ verdict: z.ZodString;
193
+ detectedAt: z.ZodString;
194
+ iteration: z.ZodNumber;
195
+ reason: z.ZodEnum<{
196
+ SAME_SHA_NO_PROGRESS: "SAME_SHA_NO_PROGRESS";
197
+ LOOP_NO_DIFF: "LOOP_NO_DIFF";
198
+ }>;
199
+ }, z.core.$strip>;
200
+ export type QAStagnationEntry = z.infer<typeof QAStagnationEntrySchema>;
201
+ /**
202
+ * Optional relay state for the interactive bidirectional channel (#383).
203
+ *
204
+ * Present when `sequant run` has activated the relay for an issue, absent
205
+ * otherwise. Legacy state files (no relay field) still parse successfully.
206
+ */
207
+ export declare const RelayStateSchema: z.ZodObject<{
208
+ enabled: z.ZodBoolean;
209
+ pid: z.ZodNumber;
210
+ startedAt: z.ZodString;
211
+ messageCount: z.ZodNumber;
212
+ }, z.core.$strip>;
213
+ export type RelayState = z.infer<typeof RelayStateSchema>;
189
214
  /**
190
215
  * Complete state for a single issue
191
216
  */
@@ -203,17 +228,7 @@ export declare const IssueStateSchema: z.ZodObject<{
203
228
  }>;
204
229
  worktree: z.ZodOptional<z.ZodString>;
205
230
  branch: z.ZodOptional<z.ZodString>;
206
- currentPhase: z.ZodOptional<z.ZodEnum<{
207
- qa: "qa";
208
- loop: "loop";
209
- verify: "verify";
210
- spec: "spec";
211
- "security-review": "security-review";
212
- exec: "exec";
213
- testgen: "testgen";
214
- test: "test";
215
- merger: "merger";
216
- }>>;
231
+ currentPhase: z.ZodOptional<z.ZodString>;
217
232
  phases: z.ZodRecord<z.ZodString, z.ZodObject<{
218
233
  status: z.ZodEnum<{
219
234
  pending: "pending";
@@ -301,7 +316,28 @@ export declare const IssueStateSchema: z.ZodObject<{
301
316
  }, z.core.$strip>;
302
317
  recommendation: z.ZodString;
303
318
  }, z.core.$strip>>;
319
+ qaStagnation: z.ZodOptional<z.ZodArray<z.ZodObject<{
320
+ sha: z.ZodString;
321
+ verdict: z.ZodString;
322
+ detectedAt: z.ZodString;
323
+ iteration: z.ZodNumber;
324
+ reason: z.ZodEnum<{
325
+ SAME_SHA_NO_PROGRESS: "SAME_SHA_NO_PROGRESS";
326
+ LOOP_NO_DIFF: "LOOP_NO_DIFF";
327
+ }>;
328
+ }, z.core.$strip>>>;
329
+ relay: z.ZodOptional<z.ZodObject<{
330
+ enabled: z.ZodBoolean;
331
+ pid: z.ZodNumber;
332
+ startedAt: z.ZodString;
333
+ messageCount: z.ZodNumber;
334
+ }, z.core.$strip>>;
304
335
  sessionId: z.ZodOptional<z.ZodString>;
336
+ resumeHandle: z.ZodOptional<z.ZodObject<{
337
+ driver: z.ZodString;
338
+ token: z.ZodString;
339
+ originCwd: z.ZodString;
340
+ }, z.core.$strip>>;
305
341
  resolvedAt: z.ZodOptional<z.ZodString>;
306
342
  lastActivity: z.ZodString;
307
343
  createdAt: z.ZodString;
@@ -330,17 +366,7 @@ export declare const WorkflowStateSchema: z.ZodObject<{
330
366
  }>;
331
367
  worktree: z.ZodOptional<z.ZodString>;
332
368
  branch: z.ZodOptional<z.ZodString>;
333
- currentPhase: z.ZodOptional<z.ZodEnum<{
334
- qa: "qa";
335
- loop: "loop";
336
- verify: "verify";
337
- spec: "spec";
338
- "security-review": "security-review";
339
- exec: "exec";
340
- testgen: "testgen";
341
- test: "test";
342
- merger: "merger";
343
- }>>;
369
+ currentPhase: z.ZodOptional<z.ZodString>;
344
370
  phases: z.ZodRecord<z.ZodString, z.ZodObject<{
345
371
  status: z.ZodEnum<{
346
372
  pending: "pending";
@@ -428,7 +454,28 @@ export declare const WorkflowStateSchema: z.ZodObject<{
428
454
  }, z.core.$strip>;
429
455
  recommendation: z.ZodString;
430
456
  }, z.core.$strip>>;
457
+ qaStagnation: z.ZodOptional<z.ZodArray<z.ZodObject<{
458
+ sha: z.ZodString;
459
+ verdict: z.ZodString;
460
+ detectedAt: z.ZodString;
461
+ iteration: z.ZodNumber;
462
+ reason: z.ZodEnum<{
463
+ SAME_SHA_NO_PROGRESS: "SAME_SHA_NO_PROGRESS";
464
+ LOOP_NO_DIFF: "LOOP_NO_DIFF";
465
+ }>;
466
+ }, z.core.$strip>>>;
467
+ relay: z.ZodOptional<z.ZodObject<{
468
+ enabled: z.ZodBoolean;
469
+ pid: z.ZodNumber;
470
+ startedAt: z.ZodString;
471
+ messageCount: z.ZodNumber;
472
+ }, z.core.$strip>>;
431
473
  sessionId: z.ZodOptional<z.ZodString>;
474
+ resumeHandle: z.ZodOptional<z.ZodObject<{
475
+ driver: z.ZodString;
476
+ token: z.ZodString;
477
+ originCwd: z.ZodString;
478
+ }, z.core.$strip>>;
432
479
  resolvedAt: z.ZodOptional<z.ZodString>;
433
480
  lastActivity: z.ZodString;
434
481
  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
  */
@@ -31,7 +37,7 @@ export const PhaseStatusSchema = z.enum([
31
37
  "in_progress", // Phase currently executing
32
38
  "completed", // Phase finished successfully
33
39
  "failed", // Phase finished with errors
34
- "skipped", // Phase intentionally skipped (e.g., bug labels skip spec)
40
+ "skipped", // Phase intentionally skipped (e.g., prior phase marker already exists)
35
41
  ]);
36
42
  /**
37
43
  * Issue status - tracks overall issue progress
@@ -153,6 +159,36 @@ export const AcceptanceCriteriaSchema = z.object({
153
159
  blocked: z.number().int().nonnegative(),
154
160
  }),
155
161
  });
162
+ /**
163
+ * Single QA stagnation observation.
164
+ *
165
+ * Recorded when fullsolve detects a same-SHA same-verdict cycle so future
166
+ * runs can spot "we've been here before" without re-deriving it from comments.
167
+ */
168
+ export const QAStagnationEntrySchema = z.object({
169
+ /** HEAD SHA at the time of detection */
170
+ sha: z.string(),
171
+ /** QA verdict that repeated (e.g., AC_NOT_MET) */
172
+ verdict: z.string(),
173
+ /** ISO 8601 timestamp of detection */
174
+ detectedAt: z.string().datetime(),
175
+ /** QA loop iteration number where stagnation was detected */
176
+ iteration: z.number().int().nonnegative(),
177
+ /** Reason code: SAME_SHA_NO_PROGRESS or LOOP_NO_DIFF */
178
+ reason: z.enum(["SAME_SHA_NO_PROGRESS", "LOOP_NO_DIFF"]),
179
+ });
180
+ /**
181
+ * Optional relay state for the interactive bidirectional channel (#383).
182
+ *
183
+ * Present when `sequant run` has activated the relay for an issue, absent
184
+ * otherwise. Legacy state files (no relay field) still parse successfully.
185
+ */
186
+ export const RelayStateSchema = z.object({
187
+ enabled: z.boolean(),
188
+ pid: z.number().int().positive(),
189
+ startedAt: z.string().datetime(),
190
+ messageCount: z.number().int().nonnegative(),
191
+ });
156
192
  /**
157
193
  * Complete state for a single issue
158
194
  */
@@ -179,8 +215,31 @@ export const IssueStateSchema = z.object({
179
215
  acceptanceCriteria: AcceptanceCriteriaSchema.optional(),
180
216
  /** Scope assessment result (if performed by /spec) */
181
217
  scopeAssessment: ScopeAssessmentSchema.optional(),
182
- /** Claude session ID (for resume) */
218
+ /** QA stagnation log: same-SHA same-verdict cycles detected during fullsolve */
219
+ qaStagnation: z.array(QAStagnationEntrySchema).optional(),
220
+ /** Relay state (#383); present when bidirectional relay is active */
221
+ relay: RelayStateSchema.optional(),
222
+ /**
223
+ * Claude session ID (for resume).
224
+ * @deprecated Use {@link resumeHandle} (#674). Retained as a read-path
225
+ * mirror of `resumeHandle.token` for one release so state files written by
226
+ * prior sequant builds load cleanly. Legacy entries (token without
227
+ * `originCwd`) intentionally do NOT resume — the driver-owned `canResume`
228
+ * fail-safe disables them.
229
+ */
183
230
  sessionId: z.string().optional(),
231
+ /**
232
+ * Driver-tagged resume handle (#674). Stores the driver name, the
233
+ * resume token, and the cwd the session was created in so the next phase
234
+ * can prove cwd-equality before reattaching.
235
+ */
236
+ resumeHandle: z
237
+ .object({
238
+ driver: z.string(),
239
+ token: z.string(),
240
+ originCwd: z.string(),
241
+ })
242
+ .optional(),
184
243
  /** When the issue transitioned to a terminal status (merged/abandoned/closed) */
185
244
  resolvedAt: z.string().datetime().optional(),
186
245
  /** Most recent activity timestamp */