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
@@ -31,22 +31,54 @@ export function checkPRMergeStatus(prNumber) {
31
31
  /**
32
32
  * Check if a branch has been merged into a base branch using git
33
33
  *
34
+ * "Merged" here means the branch was the source of an actual merge commit on
35
+ * the base branch — i.e., the branch tip appears as a non-first parent of some
36
+ * merge commit reachable from baseBranch. This deliberately excludes the case
37
+ * where the branch tip is just an ancestor of baseBranch with no commits ever
38
+ * added (e.g., a worktree branch created from main that was abandoned before
39
+ * any commits were made). Those branches are reachable from main but were
40
+ * never merged in any meaningful sense; the older `git branch --merged` check
41
+ * misclassified them as merged and caused subsequent runs to skip the still-
42
+ * open issue.
43
+ *
44
+ * Squash-merged branches do not satisfy this check (their tip is not on main
45
+ * after squash) — callers that need to detect squash merges should rely on
46
+ * commit-message detection (see {@link isIssueMergedIntoMain}'s `--grep` path)
47
+ * or a PR API check.
48
+ *
34
49
  * @param branchName - The branch name to check (e.g., "feature/33-some-title")
35
50
  * @param baseBranch - The base branch to check against (default: "main")
36
- * @returns true if the branch is merged into the base branch, false otherwise
51
+ * @returns true if a merge commit on baseBranch records branchName's tip as a
52
+ * non-first parent, false otherwise
37
53
  */
38
54
  export function isBranchMergedIntoMain(branchName, baseBranch = "main") {
39
55
  try {
40
- // Get branches merged into the base branch
41
- const result = spawnSync("git", ["branch", "--merged", baseBranch], {
56
+ // Resolve the branch tip SHA. If the branch can't be resolved (deleted,
57
+ // typo'd, etc.), it can't be "merged" by any definition.
58
+ const tipResult = spawnSync("git", ["rev-parse", branchName], {
42
59
  stdio: "pipe",
43
60
  timeout: 10000,
44
61
  });
45
- if (result.status === 0 && result.stdout) {
46
- const mergedBranches = result.stdout.toString();
47
- // Check if our branch is in the list (handle both local and remote refs)
48
- return (mergedBranches.includes(branchName) ||
49
- mergedBranches.includes(`remotes/origin/${branchName}`));
62
+ if (tipResult.status !== 0)
63
+ return false;
64
+ const branchTip = tipResult.stdout.toString().trim();
65
+ if (!branchTip)
66
+ return false;
67
+ // Walk recent merge commits on baseBranch and check whether any records
68
+ // the branch tip as a non-first parent. The first parent of a merge
69
+ // commit is the prior tip of baseBranch; non-first parents are the
70
+ // sources being merged in.
71
+ const mergesResult = spawnSync("git", ["rev-list", "--merges", "--parents", "-200", baseBranch], { stdio: "pipe", timeout: 10000 });
72
+ if (mergesResult.status === 0 && mergesResult.stdout) {
73
+ const lines = mergesResult.stdout.toString().trim().split("\n");
74
+ for (const line of lines) {
75
+ if (!line)
76
+ continue;
77
+ const parts = line.split(" ");
78
+ if (parts.length > 2 && parts.slice(2).includes(branchTip)) {
79
+ return true;
80
+ }
81
+ }
50
82
  }
51
83
  }
52
84
  catch {
@@ -58,7 +90,7 @@ export function isBranchMergedIntoMain(branchName, baseBranch = "main") {
58
90
  * Check if a feature branch for an issue is merged into a base branch
59
91
  *
60
92
  * Tries multiple detection methods:
61
- * 1. Check if branch exists and is merged via `git branch --merged <baseBranch>`
93
+ * 1. Find `feature/<N>-*` branches with `git branch -a` and check via {@link isBranchMergedIntoMain}
62
94
  * 2. Check for merge commits mentioning the issue
63
95
  *
64
96
  * @param issueNumber - The issue number to check
@@ -0,0 +1,117 @@
1
+ /**
2
+ * QA stagnation detection — early-exit guard for fullsolve's QA loop.
3
+ *
4
+ * Background (issue #581): when fullsolve's QA loop sees an `AC_NOT_MET`
5
+ * verdict it invokes `/loop` to apply fixes, then re-runs `/qa`. If `/loop`
6
+ * silently no-ops (no diff, no commit), the re-run produces the same verdict
7
+ * at the same SHA — wasting iterations without adding signal.
8
+ *
9
+ * This module exposes:
10
+ *
11
+ * - `detectStagnation()` — pure decision function. Given the latest qa marker,
12
+ * the current HEAD SHA, and whether the worktree is dirty, returns whether
13
+ * the next QA invocation would be wasted.
14
+ * - `recordStagnation()` — appends a stagnation entry to the per-issue
15
+ * record in `.sequant/state.json` so successive fullsolve runs can see
16
+ * the history.
17
+ *
18
+ * The fullsolve and loop SKILL.md files invoke a thin CLI shim that wraps
19
+ * these functions so the same code paths are exercised by tests and the
20
+ * orchestrated runtime.
21
+ */
22
+ import { type StateManagerOptions } from "./state-manager.js";
23
+ import type { PhaseMarker } from "./state-schema.js";
24
+ /**
25
+ * Reason codes for halting the QA loop.
26
+ */
27
+ export type StagnationReason = "SAME_SHA_NO_PROGRESS" | "LOOP_NO_DIFF";
28
+ /**
29
+ * Decision returned by `detectStagnation`.
30
+ *
31
+ * `stagnant === true` means the orchestrator should NOT re-invoke `/qa` at
32
+ * the current state — either escalate or hand off to `/loop` for one more
33
+ * fix attempt.
34
+ */
35
+ export interface StagnationDecision {
36
+ stagnant: boolean;
37
+ reason?: StagnationReason;
38
+ /** Human-readable explanation suitable for logs / GitHub comments. */
39
+ message: string;
40
+ /** SHA the prior QA was recorded at, if any. */
41
+ priorSha?: string;
42
+ /** Verdict from the prior QA, if any. */
43
+ priorVerdict?: string;
44
+ }
45
+ export interface DetectStagnationInput {
46
+ /** Current `git rev-parse HEAD` for the worktree. */
47
+ currentSha: string;
48
+ /** Whether `git status --porcelain` is non-empty. */
49
+ isDirty: boolean;
50
+ /**
51
+ * Most recent qa phase marker (any status). Pass `null` when no marker has
52
+ * been recorded yet — that always means "fresh, run qa".
53
+ */
54
+ lastMarker: PhaseMarker | null;
55
+ }
56
+ /**
57
+ * Pure detection function: would invoking `/qa` again be a wasted cycle?
58
+ *
59
+ * The contract from issue #581 AC-1: if the prior qa marker is `failed`,
60
+ * its `commitSHA` matches HEAD, AND the worktree is clean, the next `/qa`
61
+ * call will produce the same verdict — so the orchestrator should escalate
62
+ * (or run `/loop` once) instead.
63
+ */
64
+ export declare function detectStagnation(input: DetectStagnationInput): StagnationDecision;
65
+ /**
66
+ * Append a stagnation entry to the per-issue record. Schema-additive — older
67
+ * state files without the field will simply gain it on next write.
68
+ */
69
+ export declare function recordStagnation(issueNumber: number, entry: {
70
+ sha: string;
71
+ verdict: string;
72
+ iteration: number;
73
+ reason: StagnationReason;
74
+ }, options?: StateManagerOptions): Promise<void>;
75
+ /**
76
+ * Inputs required for the CLI shim. Loaded from git + GitHub at the call site.
77
+ */
78
+ export interface CLIDetectInput {
79
+ currentSha: string;
80
+ isDirty: boolean;
81
+ lastMarker: PhaseMarker | null;
82
+ }
83
+ /**
84
+ * Read the worktree's HEAD SHA. Pure wrapper around `git rev-parse HEAD`.
85
+ */
86
+ export declare function readHeadSha(cwd?: string): string;
87
+ /**
88
+ * Returns true when `git status --porcelain` reports any uncommitted change.
89
+ */
90
+ export declare function readIsDirty(cwd?: string): boolean;
91
+ /**
92
+ * Snapshot a worktree's HEAD SHA and dirty bit. Used by `/loop` to detect
93
+ * whether a fix attempt actually produced a diff. We deliberately exclude
94
+ * `.sequant/state.json` writes from the dirty check — the helper itself
95
+ * writes there, and per issue #581 those writes do NOT count as progress.
96
+ */
97
+ export interface LoopProgressSnapshot {
98
+ sha: string;
99
+ /** Path-relative dirty entries, excluding `.sequant/` state writes. */
100
+ dirty: string[];
101
+ }
102
+ export declare function snapshotLoopProgress(cwd?: string): LoopProgressSnapshot;
103
+ export interface LoopProgressDecision {
104
+ /** True when the snapshot pair shows a real diff (commit or working-tree change). */
105
+ progressed: boolean;
106
+ reason?: "LOOP_NO_DIFF";
107
+ message: string;
108
+ }
109
+ /**
110
+ * Compare two `LoopProgressSnapshot`s. Returns `progressed: false` ONLY when
111
+ * both the SHA and the (state-excluded) dirty set are unchanged.
112
+ *
113
+ * State-file / settings writes are excluded from the dirty comparison per
114
+ * issue #581's open question — `/loop` may legitimately touch
115
+ * `.sequant/state.json` without that counting as a fix.
116
+ */
117
+ export declare function compareLoopProgress(before: LoopProgressSnapshot, after: LoopProgressSnapshot): LoopProgressDecision;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * QA stagnation detection — early-exit guard for fullsolve's QA loop.
3
+ *
4
+ * Background (issue #581): when fullsolve's QA loop sees an `AC_NOT_MET`
5
+ * verdict it invokes `/loop` to apply fixes, then re-runs `/qa`. If `/loop`
6
+ * silently no-ops (no diff, no commit), the re-run produces the same verdict
7
+ * at the same SHA — wasting iterations without adding signal.
8
+ *
9
+ * This module exposes:
10
+ *
11
+ * - `detectStagnation()` — pure decision function. Given the latest qa marker,
12
+ * the current HEAD SHA, and whether the worktree is dirty, returns whether
13
+ * the next QA invocation would be wasted.
14
+ * - `recordStagnation()` — appends a stagnation entry to the per-issue
15
+ * record in `.sequant/state.json` so successive fullsolve runs can see
16
+ * the history.
17
+ *
18
+ * The fullsolve and loop SKILL.md files invoke a thin CLI shim that wraps
19
+ * these functions so the same code paths are exercised by tests and the
20
+ * orchestrated runtime.
21
+ */
22
+ import { execSync } from "child_process";
23
+ import { StateManager, getStateManager, } from "./state-manager.js";
24
+ /**
25
+ * Pure detection function: would invoking `/qa` again be a wasted cycle?
26
+ *
27
+ * The contract from issue #581 AC-1: if the prior qa marker is `failed`,
28
+ * its `commitSHA` matches HEAD, AND the worktree is clean, the next `/qa`
29
+ * call will produce the same verdict — so the orchestrator should escalate
30
+ * (or run `/loop` once) instead.
31
+ */
32
+ export function detectStagnation(input) {
33
+ const { currentSha, isDirty, lastMarker } = input;
34
+ if (!lastMarker) {
35
+ return { stagnant: false, message: "No prior qa marker — fresh run." };
36
+ }
37
+ if (lastMarker.phase !== "qa") {
38
+ return {
39
+ stagnant: false,
40
+ message: `Latest marker is for phase '${lastMarker.phase}', not qa.`,
41
+ };
42
+ }
43
+ if (lastMarker.status !== "failed") {
44
+ return {
45
+ stagnant: false,
46
+ message: `Prior qa marker status is '${lastMarker.status}', not 'failed'.`,
47
+ };
48
+ }
49
+ if (!lastMarker.commitSHA) {
50
+ return {
51
+ stagnant: false,
52
+ message: "Prior qa marker has no commitSHA — cannot compare; fall through.",
53
+ };
54
+ }
55
+ if (lastMarker.commitSHA !== currentSha) {
56
+ return {
57
+ stagnant: false,
58
+ message: `Prior qa SHA ${lastMarker.commitSHA} ≠ HEAD ${currentSha}; new commits since last run.`,
59
+ priorSha: lastMarker.commitSHA,
60
+ };
61
+ }
62
+ if (isDirty) {
63
+ return {
64
+ stagnant: false,
65
+ message: "Worktree dirty since last qa — uncommitted changes will produce different output.",
66
+ priorSha: lastMarker.commitSHA,
67
+ };
68
+ }
69
+ return {
70
+ stagnant: true,
71
+ reason: "SAME_SHA_NO_PROGRESS",
72
+ message: `Prior qa failed at HEAD ${currentSha} and worktree is clean — ` +
73
+ `re-running /qa would produce the same verdict.`,
74
+ priorSha: lastMarker.commitSHA,
75
+ priorVerdict: lastMarker.error,
76
+ };
77
+ }
78
+ /**
79
+ * Append a stagnation entry to the per-issue record. Schema-additive — older
80
+ * state files without the field will simply gain it on next write.
81
+ */
82
+ export async function recordStagnation(issueNumber, entry, options = {}) {
83
+ const manager = options.statePath !== undefined
84
+ ? new StateManager(options)
85
+ : getStateManager(options);
86
+ await manager.withLock(async () => {
87
+ const state = await manager.getState();
88
+ const issueState = state.issues[String(issueNumber)];
89
+ if (!issueState) {
90
+ throw new Error(`Cannot record stagnation: issue #${issueNumber} not found in state`);
91
+ }
92
+ const existing = issueState.qaStagnation ?? [];
93
+ existing.push({
94
+ sha: entry.sha,
95
+ verdict: entry.verdict,
96
+ iteration: entry.iteration,
97
+ reason: entry.reason,
98
+ detectedAt: new Date().toISOString(),
99
+ });
100
+ issueState.qaStagnation = existing;
101
+ issueState.lastActivity = new Date().toISOString();
102
+ await manager.saveState(state);
103
+ });
104
+ }
105
+ /**
106
+ * Read the worktree's HEAD SHA. Pure wrapper around `git rev-parse HEAD`.
107
+ */
108
+ export function readHeadSha(cwd = process.cwd()) {
109
+ return execSync("git rev-parse HEAD", { cwd, encoding: "utf-8" }).trim();
110
+ }
111
+ /**
112
+ * Returns true when `git status --porcelain` reports any uncommitted change.
113
+ */
114
+ export function readIsDirty(cwd = process.cwd()) {
115
+ return (execSync("git status --porcelain", { cwd, encoding: "utf-8" }).trim()
116
+ .length > 0);
117
+ }
118
+ const STATE_DIR_PREFIX = ".sequant/";
119
+ function readDirtyExcludingState(cwd = process.cwd()) {
120
+ // `git status --porcelain` (v1) always prefixes each line with exactly
121
+ // `XY ` (two status bytes + a space) before the path. We must slice on the
122
+ // RAW line — trimming first strips the leading space of unstaged-only
123
+ // entries (` M file.ts`) and a subsequent `.{1,3}` regex would chop real
124
+ // path bytes. See issue #581 QA review.
125
+ const out = execSync("git status --porcelain", {
126
+ cwd,
127
+ encoding: "utf-8",
128
+ });
129
+ return out
130
+ .split("\n")
131
+ .filter((line) => line.length > 0)
132
+ .map((line) => {
133
+ // Renamed entries use `R old -> new`; we only care about the new path.
134
+ const path = line.slice(3);
135
+ const arrow = path.indexOf(" -> ");
136
+ return arrow >= 0 ? path.slice(arrow + 4) : path;
137
+ })
138
+ .filter((path) => !path.startsWith(STATE_DIR_PREFIX));
139
+ }
140
+ export function snapshotLoopProgress(cwd = process.cwd()) {
141
+ return { sha: readHeadSha(cwd), dirty: readDirtyExcludingState(cwd) };
142
+ }
143
+ /**
144
+ * Compare two `LoopProgressSnapshot`s. Returns `progressed: false` ONLY when
145
+ * both the SHA and the (state-excluded) dirty set are unchanged.
146
+ *
147
+ * State-file / settings writes are excluded from the dirty comparison per
148
+ * issue #581's open question — `/loop` may legitimately touch
149
+ * `.sequant/state.json` without that counting as a fix.
150
+ */
151
+ export function compareLoopProgress(before, after) {
152
+ if (before.sha !== after.sha) {
153
+ return {
154
+ progressed: true,
155
+ message: `HEAD advanced ${before.sha} → ${after.sha}.`,
156
+ };
157
+ }
158
+ const beforeSet = new Set(before.dirty);
159
+ const afterSet = new Set(after.dirty);
160
+ if (beforeSet.size !== afterSet.size) {
161
+ return {
162
+ progressed: true,
163
+ message: `Working-tree dirty count changed (${beforeSet.size} → ${afterSet.size}).`,
164
+ };
165
+ }
166
+ for (const path of afterSet) {
167
+ if (!beforeSet.has(path)) {
168
+ return {
169
+ progressed: true,
170
+ message: `New dirty path detected: ${path}.`,
171
+ };
172
+ }
173
+ }
174
+ return {
175
+ progressed: false,
176
+ reason: "LOOP_NO_DIFF",
177
+ message: "/loop made no commit and no working-tree changes (excluding .sequant/ state writes) — manual intervention required.",
178
+ };
179
+ }
@@ -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.ZodEnum<{
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.ZodEnum<{
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.ZodEnum<{
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.ZodEnum<{
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.ZodEnum<{
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,12 +6,24 @@
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
+ import type { RunSnapshot } from "./run-state.js";
10
11
  import type { WorktreeInfo } from "./worktree-manager.js";
11
12
  import { LogWriter } from "./log-writer.js";
12
13
  import { StateManager } from "./state-manager.js";
13
14
  import { ShutdownManager } from "../shutdown.js";
15
+ import type { LockFile } from "../locks/index.js";
16
+ import { WorkflowEventEmitter } from "./event-emitter.js";
14
17
  import type { SequantSettings } from "../settings.js";
18
+ /**
19
+ * Build the stack-manifest line emitted into PR bodies under --stacked.
20
+ *
21
+ * Example for issues `[100, 101, 102]` at `currentIndex=1`:
22
+ * `Part of stack: #100 → #101 (this) → #102`
23
+ *
24
+ * @internal Exported for testing.
25
+ */
26
+ export declare function buildStackManifest(issueNumbers: number[], currentIndex: number): string;
15
27
  /**
16
28
  * Injectable services for RunOrchestrator.
17
29
  * All optional — orchestrator degrades gracefully when services are absent.
@@ -45,6 +57,14 @@ export interface OrchestratorConfig {
45
57
  baseBranch?: string;
46
58
  /** Per-phase progress callback (parallel mode) */
47
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;
48
68
  }
49
69
  /**
50
70
  * High-level init config for full lifecycle execution.
@@ -64,6 +84,21 @@ export interface RunInit {
64
84
  baseBranch?: string;
65
85
  /** Per-phase progress callback */
66
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;
96
+ /**
97
+ * Invoked once the orchestrator is constructed but before execution begins.
98
+ * Used by the experimental TUI to attach a snapshot poller to the active
99
+ * orchestrator instance created inside `run()`.
100
+ */
101
+ onOrchestratorReady?: (orchestrator: RunOrchestrator) => void;
67
102
  }
68
103
  /**
69
104
  * Pure result of config resolution — no side effects, no services.
@@ -127,7 +162,36 @@ export interface RunResult {
127
162
  */
128
163
  export declare class RunOrchestrator {
129
164
  private readonly cfg;
165
+ private readonly issueStates;
166
+ private readonly phaseStartTimes;
167
+ private readonly emitter;
168
+ private done;
130
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;
177
+ /**
178
+ * Point-in-time view of the entire run.
179
+ *
180
+ * Safe under concurrent reads: the returned object contains only freshly
181
+ * allocated arrays and plain records; no internal Map or mutable state
182
+ * reference is leaked. Callers may hold snapshots across awaits without
183
+ * observing torn writes.
184
+ */
185
+ getSnapshot(): RunSnapshot;
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
+ */
191
+ markDone(): void;
192
+ private initIssueStates;
193
+ private wrapProgress;
194
+ private applyProgressEvent;
131
195
  /**
132
196
  * Pure config resolution — no side effects.
133
197
  *
@@ -157,5 +221,10 @@ export declare class RunOrchestrator {
157
221
  private executeOneIssue;
158
222
  private static recordMetrics;
159
223
  }
224
+ /**
225
+ * Build the synthetic `IssueResult` returned for an issue that was skipped
226
+ * because another sequant session holds its lock (#625).
227
+ */
228
+ export declare function buildLockedResult(issueNumber: number, holder: LockFile): IssueResult;
160
229
  /** Log a non-fatal warning: one-line summary always, detail in verbose. */
161
230
  export declare function logNonFatalWarning(message: string, error: unknown, verbose: boolean): void;