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
@@ -7,61 +7,69 @@
7
7
  *
8
8
  * @module phase-mapper
9
9
  */
10
+ import { phaseRegistry } from "./phase-registry.js";
10
11
  /**
11
- * UI-related labels that trigger automatic test phase
12
- */
13
- export const UI_LABELS = ["ui", "frontend", "admin", "web", "browser"];
14
- /**
15
- * Bug-related labels that skip spec phase
12
+ * Bug-related labels (used by downstream metadata consumers).
13
+ *
14
+ * Issue-type metadata NOT phase-trigger rules. The registry-driven
15
+ * `detectPhasesFromLabels` below does not consult this list. It stays
16
+ * here because `batch-executor.ts` and other modules read it for
17
+ * `issueType` propagation and similar non-phase concerns.
16
18
  */
17
19
  export const BUG_LABELS = ["bug", "fix", "hotfix", "patch"];
18
20
  /**
19
- * Documentation labels that skip spec phase
21
+ * Documentation labels (used for issueType propagation and downstream metadata).
22
+ *
23
+ * Issue-type metadata — NOT phase-trigger rules. See BUG_LABELS comment.
20
24
  */
21
25
  export const DOCS_LABELS = ["docs", "documentation", "readme"];
22
26
  /**
23
- * Complex labels that enable quality loop
27
+ * Complex labels that enable quality loop.
28
+ *
29
+ * Quality-loop trigger — NOT a phase-trigger rule (does not add the loop
30
+ * *phase*; only flips the `qualityLoop` flag on the run config). Kept
31
+ * out of the phase registry by design.
24
32
  */
25
33
  export const COMPLEX_LABELS = ["complex", "refactor", "breaking", "major"];
26
34
  /**
27
- * Security-related labels that trigger security-review phase
35
+ * Look up label-based detect rules from the registry, returning the set
36
+ * of phases whose `detect.labels` intersect the issue's labels. Comparison
37
+ * is case-insensitive (labels lowercased at the call site).
28
38
  */
29
- export const SECURITY_LABELS = [
30
- "security",
31
- "auth",
32
- "authentication",
33
- "permissions",
34
- "admin",
35
- ];
39
+ function detectPhasesFromRegistry(lowerLabels) {
40
+ const matched = new Set();
41
+ for (const def of phaseRegistry.list()) {
42
+ const triggers = def.detect?.labels;
43
+ if (!triggers || triggers.length === 0)
44
+ continue;
45
+ const hit = triggers.some((t) => lowerLabels.includes(t.toLowerCase()));
46
+ if (hit)
47
+ matched.add(def.name);
48
+ }
49
+ return matched;
50
+ }
36
51
  /**
37
- * Detect phases based on issue labels (like /assess logic)
52
+ * Detect phases based on issue labels (like /assess logic).
53
+ *
54
+ * Label → phase mapping now lives in `PhaseDefinition.detect.labels`. Only
55
+ * the *insertion position* of detected phases remains baked in here, because
56
+ * pipeline ordering depends on the phase's role (security-review goes after
57
+ * spec; test goes before qa).
38
58
  */
39
59
  export function detectPhasesFromLabels(labels) {
40
60
  const lowerLabels = labels.map((l) => l.toLowerCase());
41
- // Check for bug/fix labels exec qa (skip spec)
42
- const isBugFix = lowerLabels.some((label) => BUG_LABELS.some((bugLabel) => label === bugLabel));
43
- // Check for docs labels → exec → qa (skip spec)
44
- const isDocs = lowerLabels.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
45
- // Check for UI labels → add test phase
46
- const isUI = lowerLabels.some((label) => UI_LABELS.some((uiLabel) => label === uiLabel));
47
- // Check for complex labels → enable quality loop
61
+ // Quality loop is a registry-independent label trigger (see COMPLEX_LABELS).
48
62
  const isComplex = lowerLabels.some((label) => COMPLEX_LABELS.some((complexLabel) => label === complexLabel));
49
- // Check for security labels → add security-review phase
50
- const isSecurity = lowerLabels.some((label) => SECURITY_LABELS.some((secLabel) => label === secLabel));
51
- // Build phase list
52
- let phases;
53
- if (isBugFix || isDocs) {
54
- // Simple workflow: exec qa
55
- phases = ["exec", "qa"];
56
- }
57
- else if (isUI) {
58
- // UI workflow: spec exec → test → qa
59
- phases = ["spec", "exec", "test", "qa"];
60
- }
61
- else {
62
- // Standard workflow: spec → exec → qa
63
- phases = ["spec", "exec", "qa"];
64
- }
63
+ const matched = detectPhasesFromRegistry(lowerLabels);
64
+ const isUI = matched.has("test");
65
+ const isSecurity = matched.has("security-review");
66
+ // Build phase list — spec is always included by default (#533).
67
+ // Bug/docs labels no longer short-circuit spec; downstream consumers
68
+ // (e.g. `issueType: "docs"` propagation) still use DOCS_LABELS for
69
+ // metadata purposes, not for phase selection.
70
+ const phases = isUI
71
+ ? ["spec", "exec", "test", "qa"]
72
+ : ["spec", "exec", "qa"];
65
73
  // Add security-review phase after spec if security labels detected
66
74
  if (isSecurity && phases.includes("spec")) {
67
75
  const specIndex = phases.indexOf("spec");
@@ -89,18 +97,10 @@ export function parseRecommendedWorkflow(output) {
89
97
  .split(/\s*→\s*|\s*->\s*|\s*,\s*/)
90
98
  .map((p) => p.trim().toLowerCase())
91
99
  .filter((p) => p.length > 0);
92
- // Validate and convert to Phase type
100
+ // Validate against the registry accepts any registered phase.
93
101
  const validPhases = [];
94
102
  for (const name of phaseNames) {
95
- if ([
96
- "spec",
97
- "security-review",
98
- "testgen",
99
- "exec",
100
- "test",
101
- "qa",
102
- "loop",
103
- ].includes(name)) {
103
+ if (phaseRegistry.has(name)) {
104
104
  validPhases.push(name);
105
105
  }
106
106
  }
@@ -115,10 +115,21 @@ export function parseRecommendedWorkflow(output) {
115
115
  return { phases: validPhases, qualityLoop };
116
116
  }
117
117
  /**
118
- * Check if an issue has UI-related labels
118
+ * Check if an issue has UI-related labels.
119
+ *
120
+ * Sources the label list from the `test` phase's `detect.labels` entry in
121
+ * the registry — same data as `detectPhasesFromLabels` consults, just
122
+ * exposed as a boolean for callers that only need the yes/no answer
123
+ * (e.g. test phase insertion in `determinePhasesForIssue`).
119
124
  */
120
125
  export function hasUILabels(labels) {
121
- return labels.some((label) => UI_LABELS.some((uiLabel) => label.toLowerCase() === uiLabel));
126
+ const testTriggers = phaseRegistry.has("test")
127
+ ? (phaseRegistry.get("test").detect?.labels ?? [])
128
+ : [];
129
+ if (testTriggers.length === 0)
130
+ return false;
131
+ const lowered = new Set(testTriggers.map((t) => t.toLowerCase()));
132
+ return labels.some((label) => lowered.has(label.toLowerCase()));
122
133
  }
123
134
  /**
124
135
  * Determine phases to run based on options and issue labels
@@ -132,6 +143,14 @@ export function determinePhasesForIssue(basePhases, labels, options) {
132
143
  phases.splice(specIndex + 1, 0, "testgen");
133
144
  }
134
145
  }
146
+ // Add security-review phase after spec if requested.
147
+ // Idempotent vs label-based auto-detection in detectPhasesFromLabels.
148
+ if (options.securityReview && phases.includes("spec")) {
149
+ const specIndex = phases.indexOf("spec");
150
+ if (!phases.includes("security-review")) {
151
+ phases.splice(specIndex + 1, 0, "security-review");
152
+ }
153
+ }
135
154
  // Auto-detect UI issues and add test phase
136
155
  if (hasUILabels(labels) && !phases.includes("test")) {
137
156
  // Add test phase before qa if present, otherwise at the end
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Phase registry — single source of truth for workflow phase definitions.
3
+ *
4
+ * Replaces scattered constants (`PHASE_PROMPTS`, `AIDER_PHASE_PROMPTS`,
5
+ * `ISOLATED_PHASES`, `UI_LABELS`, `SECURITY_LABELS`) with a uniform record
6
+ * per phase. All 9 built-in phases register here at module load — no
7
+ * special-casing inside consumer code.
8
+ *
9
+ * Built-in registrations live at the bottom of this file rather than in a
10
+ * separate `built-in-phases.ts` module. The colocated layout follows the
11
+ * existing `drivers/index.ts` pattern and avoids the ESM-cycle pitfall of
12
+ * a separate bootstrap module that re-imports the registry singleton
13
+ * before the singleton is fully initialized.
14
+ *
15
+ * User-extensibility (filesystem discovery of `.sequant/phases/`) is
16
+ * deliberately deferred — see issue #505 descoping comment.
17
+ */
18
+ /**
19
+ * Per-driver overrides for a phase. Today only `promptTemplate` is supported;
20
+ * extend the inner record when additional fields need per-driver values.
21
+ */
22
+ export interface DriverOverride {
23
+ promptTemplate?: string;
24
+ }
25
+ /**
26
+ * Retry policy for a single phase. Phases without this field fall back to
27
+ * the global cold-start retry defaults in `phase-executor.ts`. The fields
28
+ * are deliberately optional — a phase only needs to specify what differs.
29
+ */
30
+ export interface RetryStrategy {
31
+ /** Override the cold-start retry attempt count for this phase. */
32
+ maxRetries?: number;
33
+ /** Initial backoff in ms before the first retry. */
34
+ backoffMs?: number;
35
+ /** Override the cold-start threshold (seconds). */
36
+ coldStartThreshold?: number;
37
+ /** Extra retries beyond cold-start (e.g. for transient API errors). */
38
+ extraRetries?: number;
39
+ }
40
+ /**
41
+ * Auto-detection rules consumed by phase-mapper.ts.
42
+ * `labels` is an exact-match list (case-insensitive at the call site).
43
+ */
44
+ export interface DetectRules {
45
+ labels?: string[];
46
+ }
47
+ /**
48
+ * Definition of a workflow phase. Registered at startup via
49
+ * `phaseRegistry.register(...)` and consumed by phase-executor, phase-mapper,
50
+ * and the CLI validator.
51
+ */
52
+ export interface PhaseDefinition {
53
+ /** Phase name (matches the skill template directory + CLI `--phases` token). */
54
+ name: string;
55
+ /** Skill template directory under `templates/skills/<skill>/SKILL.md`. */
56
+ skill: string;
57
+ /**
58
+ * Natural-language prompt for the default (Claude Code) driver. The token
59
+ * `{issue}` is substituted with the GitHub issue number at execution time.
60
+ */
61
+ promptTemplate: string;
62
+ /**
63
+ * When true, phase-executor runs this phase inside the issue worktree.
64
+ * `spec`, `verify`, and `merger` run in the main repo (no worktree).
65
+ */
66
+ requiresWorktree: boolean;
67
+ /** Optional per-phase retry overrides. */
68
+ retryStrategy?: RetryStrategy;
69
+ /** Optional auto-detection rules. */
70
+ detect?: DetectRules;
71
+ /**
72
+ * Per-driver overrides. Keyed by agent name (e.g. `"aider"`). When the
73
+ * orchestrator runs a non-Claude driver, the corresponding override's
74
+ * `promptTemplate` (if present) replaces the default.
75
+ */
76
+ driverOverrides?: Record<string, DriverOverride>;
77
+ /**
78
+ * Insertion order hint. Used by phase-mapper to sort label-detected phases
79
+ * into a deterministic pipeline position. Defaults to 0.
80
+ */
81
+ order?: number;
82
+ }
83
+ /**
84
+ * In-memory registry of phase definitions. Single mutable instance lives
85
+ * in this module — see the exported `phaseRegistry` constant.
86
+ *
87
+ * The class is intentionally minimal (no lifecycle hooks, no async). All
88
+ * mutations happen synchronously at module load by the built-in registrations
89
+ * at the bottom of this file.
90
+ */
91
+ export declare class PhaseRegistry {
92
+ private readonly definitions;
93
+ /**
94
+ * Register a phase definition. Throws on duplicate names so misconfigured
95
+ * bootstrap modules surface immediately instead of silently overwriting.
96
+ */
97
+ register(definition: PhaseDefinition): void;
98
+ /**
99
+ * Retrieve a phase by name. Throws with a "did you mean" list when the
100
+ * lookup fails — clearer than a downstream "undefined.promptTemplate".
101
+ */
102
+ get(name: string): PhaseDefinition;
103
+ /** True when a phase with this name is registered. */
104
+ has(name: string): boolean;
105
+ /**
106
+ * All registered phase definitions in insertion order. Insertion order
107
+ * is also the canonical pipeline order (see registrations below).
108
+ */
109
+ list(): PhaseDefinition[];
110
+ /**
111
+ * All registered phase names in insertion order. Replaces the
112
+ * `PhaseSchema.options` array literal exposed by the previous typed-enum
113
+ * `PhaseSchema`.
114
+ */
115
+ names(): string[];
116
+ }
117
+ /**
118
+ * Singleton registry instance. All consumer modules (phase-executor,
119
+ * phase-mapper, types.ts, CLI) read from this same instance — there is
120
+ * no second registry.
121
+ */
122
+ export declare const phaseRegistry: PhaseRegistry;
123
+ /**
124
+ * Convenience accessor for the registered phase names. Used by Zod refines,
125
+ * tests, and CLI validation in place of the removed `PhaseSchema.options`.
126
+ */
127
+ export declare function getPhaseNames(): string[];
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Phase registry — single source of truth for workflow phase definitions.
3
+ *
4
+ * Replaces scattered constants (`PHASE_PROMPTS`, `AIDER_PHASE_PROMPTS`,
5
+ * `ISOLATED_PHASES`, `UI_LABELS`, `SECURITY_LABELS`) with a uniform record
6
+ * per phase. All 9 built-in phases register here at module load — no
7
+ * special-casing inside consumer code.
8
+ *
9
+ * Built-in registrations live at the bottom of this file rather than in a
10
+ * separate `built-in-phases.ts` module. The colocated layout follows the
11
+ * existing `drivers/index.ts` pattern and avoids the ESM-cycle pitfall of
12
+ * a separate bootstrap module that re-imports the registry singleton
13
+ * before the singleton is fully initialized.
14
+ *
15
+ * User-extensibility (filesystem discovery of `.sequant/phases/`) is
16
+ * deliberately deferred — see issue #505 descoping comment.
17
+ */
18
+ /**
19
+ * In-memory registry of phase definitions. Single mutable instance lives
20
+ * in this module — see the exported `phaseRegistry` constant.
21
+ *
22
+ * The class is intentionally minimal (no lifecycle hooks, no async). All
23
+ * mutations happen synchronously at module load by the built-in registrations
24
+ * at the bottom of this file.
25
+ */
26
+ export class PhaseRegistry {
27
+ definitions = new Map();
28
+ /**
29
+ * Register a phase definition. Throws on duplicate names so misconfigured
30
+ * bootstrap modules surface immediately instead of silently overwriting.
31
+ */
32
+ register(definition) {
33
+ if (this.definitions.has(definition.name)) {
34
+ throw new Error(`PhaseRegistry: phase "${definition.name}" is already registered`);
35
+ }
36
+ this.definitions.set(definition.name, definition);
37
+ }
38
+ /**
39
+ * Retrieve a phase by name. Throws with a "did you mean" list when the
40
+ * lookup fails — clearer than a downstream "undefined.promptTemplate".
41
+ */
42
+ get(name) {
43
+ const def = this.definitions.get(name);
44
+ if (!def) {
45
+ const available = this.names().join(", ");
46
+ throw new Error(`PhaseRegistry: unknown phase "${name}". Available: ${available}`);
47
+ }
48
+ return def;
49
+ }
50
+ /** True when a phase with this name is registered. */
51
+ has(name) {
52
+ return this.definitions.has(name);
53
+ }
54
+ /**
55
+ * All registered phase definitions in insertion order. Insertion order
56
+ * is also the canonical pipeline order (see registrations below).
57
+ */
58
+ list() {
59
+ return [...this.definitions.values()];
60
+ }
61
+ /**
62
+ * All registered phase names in insertion order. Replaces the
63
+ * `PhaseSchema.options` array literal exposed by the previous typed-enum
64
+ * `PhaseSchema`.
65
+ */
66
+ names() {
67
+ return [...this.definitions.keys()];
68
+ }
69
+ }
70
+ /**
71
+ * Singleton registry instance. All consumer modules (phase-executor,
72
+ * phase-mapper, types.ts, CLI) read from this same instance — there is
73
+ * no second registry.
74
+ */
75
+ export const phaseRegistry = new PhaseRegistry();
76
+ /**
77
+ * Convenience accessor for the registered phase names. Used by Zod refines,
78
+ * tests, and CLI validation in place of the removed `PhaseSchema.options`.
79
+ */
80
+ export function getPhaseNames() {
81
+ return phaseRegistry.names();
82
+ }
83
+ // ─── Built-in phase registrations ────────────────────────────────────────
84
+ //
85
+ // Insertion order below IS the canonical pipeline order — preserved from the
86
+ // pre-registry `PhaseSchema` literal (`spec, security-review, exec, testgen,
87
+ // test, verify, qa, loop, merger`). Reordering these entries changes the
88
+ // order returned by `phaseRegistry.list()` / `getPhaseNames()` and the
89
+ // downstream `WORKFLOW_PHASES` constant in `state-schema.ts`.
90
+ // Spec — runs in the main repo (planning only, no worktree mutation)
91
+ phaseRegistry.register({
92
+ name: "spec",
93
+ skill: "spec",
94
+ promptTemplate: "Review GitHub issue #{issue} and create an implementation plan with verification criteria. Run the /spec {issue} workflow.",
95
+ requiresWorktree: false,
96
+ // Spec has a higher transient failure rate (~8.6%) than other phases due
97
+ // to GitHub API issues and rate limits. phase-executor.ts reads these
98
+ // values directly from the registry at module load (see
99
+ // SPEC_RETRY_BACKOFF_MS / SPEC_EXTRA_RETRIES).
100
+ retryStrategy: { extraRetries: 1, backoffMs: 5000 },
101
+ driverOverrides: {
102
+ aider: {
103
+ promptTemplate: `Read GitHub issue #{issue} using 'gh issue view #{issue}'.
104
+ Create a spec comment on the issue with:
105
+ 1. Implementation plan
106
+ 2. Acceptance criteria as a checklist
107
+ 3. Risk assessment
108
+ Post the comment using 'gh issue comment #{issue} --body "<comment>"'.`,
109
+ },
110
+ },
111
+ });
112
+ // Security review — worktree-isolated, label-triggered
113
+ phaseRegistry.register({
114
+ name: "security-review",
115
+ skill: "security-review",
116
+ promptTemplate: "Perform a deep security analysis for GitHub issue #{issue} focusing on auth, permissions, and sensitive operations. Run the /security-review {issue} workflow.",
117
+ requiresWorktree: true,
118
+ detect: {
119
+ labels: ["security", "auth", "authentication", "permissions", "admin"],
120
+ },
121
+ driverOverrides: {
122
+ aider: {
123
+ promptTemplate: `Perform a security review for GitHub issue #{issue}.
124
+ Read the issue with 'gh issue view #{issue}'.
125
+ Check for auth, permissions, injection, and sensitive data issues.
126
+ Post findings as a comment on the issue.`,
127
+ },
128
+ },
129
+ });
130
+ // Exec — worktree-isolated
131
+ phaseRegistry.register({
132
+ name: "exec",
133
+ skill: "exec",
134
+ promptTemplate: "Implement the feature for GitHub issue #{issue} following the spec. Run the /exec {issue} workflow.",
135
+ requiresWorktree: true,
136
+ driverOverrides: {
137
+ aider: {
138
+ promptTemplate: `Implement the feature described in GitHub issue #{issue}.
139
+ Read the issue and any spec comments with 'gh issue view #{issue} --comments'.
140
+ Follow the implementation plan from the spec.
141
+ Write tests for new functionality.
142
+ Ensure the build passes with 'npm test' and 'npm run build'.`,
143
+ },
144
+ },
145
+ });
146
+ // Testgen — worktree-isolated
147
+ phaseRegistry.register({
148
+ name: "testgen",
149
+ skill: "testgen",
150
+ promptTemplate: "Generate test stubs for GitHub issue #{issue} based on the specification. Run the /testgen {issue} workflow.",
151
+ requiresWorktree: true,
152
+ driverOverrides: {
153
+ aider: {
154
+ promptTemplate: `Generate test stubs for GitHub issue #{issue}.
155
+ Read the spec comments on the issue with 'gh issue view #{issue} --comments'.
156
+ Create test files with describe/it blocks covering the acceptance criteria.
157
+ Use the project's existing test framework.`,
158
+ },
159
+ },
160
+ });
161
+ // Test — worktree-isolated, label-triggered (UI/frontend issues)
162
+ phaseRegistry.register({
163
+ name: "test",
164
+ skill: "test",
165
+ promptTemplate: "Execute structured browser-based testing for GitHub issue #{issue}. Run the /test {issue} workflow.",
166
+ requiresWorktree: true,
167
+ detect: { labels: ["ui", "frontend", "admin", "web", "browser"] },
168
+ driverOverrides: {
169
+ aider: {
170
+ promptTemplate: `Test the implementation for GitHub issue #{issue}.
171
+ Run 'npm test' and verify all tests pass.
172
+ Check for edge cases and error handling.`,
173
+ },
174
+ },
175
+ });
176
+ // Verify — runs in main repo (CLI-only feature verification)
177
+ phaseRegistry.register({
178
+ name: "verify",
179
+ skill: "verify",
180
+ promptTemplate: "Verify the implementation for GitHub issue #{issue} by running commands and capturing output. Run the /verify {issue} workflow.",
181
+ requiresWorktree: false,
182
+ driverOverrides: {
183
+ aider: {
184
+ promptTemplate: `Verify the implementation for GitHub issue #{issue}.
185
+ Run relevant commands and capture their output for review.`,
186
+ },
187
+ },
188
+ });
189
+ // QA — worktree-isolated
190
+ phaseRegistry.register({
191
+ name: "qa",
192
+ skill: "qa",
193
+ promptTemplate: "Review the implementation for GitHub issue #{issue} against acceptance criteria. Run the /qa {issue} workflow.",
194
+ requiresWorktree: true,
195
+ driverOverrides: {
196
+ aider: {
197
+ promptTemplate: `Review the changes for GitHub issue #{issue}.
198
+ Run 'npm test' and 'npm run build' to verify everything works.
199
+ Check each acceptance criterion from the issue comments.
200
+ Output a verdict: READY_FOR_MERGE, AC_MET_BUT_NOT_A_PLUS, or AC_NOT_MET
201
+ with format "### Verdict: <VERDICT>" followed by an explanation.`,
202
+ },
203
+ },
204
+ });
205
+ // Loop — worktree-isolated. `maxRetries: 0` encodes the
206
+ // "skip cold-start retries" rule consumed by phase-executor.ts (#488).
207
+ phaseRegistry.register({
208
+ name: "loop",
209
+ skill: "loop",
210
+ promptTemplate: "Parse test/QA findings for GitHub issue #{issue} and iterate until quality gates pass. Run the /loop {issue} workflow.",
211
+ requiresWorktree: true,
212
+ retryStrategy: { maxRetries: 0 },
213
+ driverOverrides: {
214
+ aider: {
215
+ promptTemplate: `Review test and QA findings for GitHub issue #{issue}.
216
+ Fix any issues identified in the QA feedback.
217
+ Re-run 'npm test' and 'npm run build' until all quality gates pass.`,
218
+ },
219
+ },
220
+ });
221
+ // Merger — runs in main repo (multi-worktree integration)
222
+ phaseRegistry.register({
223
+ name: "merger",
224
+ skill: "merger",
225
+ promptTemplate: "Integrate and merge completed worktrees for GitHub issue #{issue}. Run the /merger {issue} workflow.",
226
+ requiresWorktree: false,
227
+ driverOverrides: {
228
+ aider: {
229
+ promptTemplate: `Integrate and merge completed worktrees for GitHub issue #{issue}.
230
+ Ensure all branches are up to date and merge cleanly.`,
231
+ },
232
+ },
233
+ });
@@ -94,7 +94,7 @@ export declare class GitHubProvider implements PlatformProvider {
94
94
  * Create a PR via `gh pr create` CLI, returning raw result.
95
95
  * Used by worktree-manager.ts which needs access to stdout for URL extraction.
96
96
  */
97
- createPRCliSync(title: string, body: string, head: string, cwd?: string): CreatePRCliResult;
97
+ createPRCliSync(title: string, body: string, head: string, cwd?: string, base?: string): CreatePRCliResult;
98
98
  /**
99
99
  * Batch fetch issue and PR status in a single GraphQL call.
100
100
  * Returns a map keyed by issue/PR number.
@@ -137,8 +137,25 @@ export class GitHubProvider {
137
137
  * Create a PR via `gh pr create` CLI, returning raw result.
138
138
  * Used by worktree-manager.ts which needs access to stdout for URL extraction.
139
139
  */
140
- createPRCliSync(title, body, head, cwd) {
141
- const result = spawnSync("gh", ["pr", "create", "--title", title, "--body", body, "--head", head], { stdio: "pipe", cwd, timeout: 30000 });
140
+ createPRCliSync(title, body, head, cwd, base) {
141
+ const args = [
142
+ "pr",
143
+ "create",
144
+ "--title",
145
+ title,
146
+ "--body",
147
+ body,
148
+ "--head",
149
+ head,
150
+ ];
151
+ if (base) {
152
+ args.push("--base", base);
153
+ }
154
+ const result = spawnSync("gh", args, {
155
+ stdio: "pipe",
156
+ cwd,
157
+ timeout: 30000,
158
+ });
142
159
  return {
143
160
  stdout: result.stdout?.toString() ?? "",
144
161
  stderr: result.stderr?.toString() ?? "",
@@ -415,7 +432,7 @@ export class GitHubProvider {
415
432
  });
416
433
  }
417
434
  async createPR(opts) {
418
- const result = this.createPRCliSync(opts.title, opts.body, opts.head);
435
+ const result = this.createPRCliSync(opts.title, opts.body, opts.head, undefined, opts.base);
419
436
  if (result.exitCode !== 0) {
420
437
  const error = result.stderr.trim() || "Unknown error";
421
438
  throw new Error(`gh pr create failed: ${error}`);
@@ -28,16 +28,32 @@ export declare function checkPRMergeStatus(prNumber: number): PRMergeStatus;
28
28
  /**
29
29
  * Check if a branch has been merged into a base branch using git
30
30
  *
31
+ * "Merged" here means the branch was the source of an actual merge commit on
32
+ * the base branch — i.e., the branch tip appears as a non-first parent of some
33
+ * merge commit reachable from baseBranch. This deliberately excludes the case
34
+ * where the branch tip is just an ancestor of baseBranch with no commits ever
35
+ * added (e.g., a worktree branch created from main that was abandoned before
36
+ * any commits were made). Those branches are reachable from main but were
37
+ * never merged in any meaningful sense; the older `git branch --merged` check
38
+ * misclassified them as merged and caused subsequent runs to skip the still-
39
+ * open issue.
40
+ *
41
+ * Squash-merged branches do not satisfy this check (their tip is not on main
42
+ * after squash) — callers that need to detect squash merges should rely on
43
+ * commit-message detection (see {@link isIssueMergedIntoMain}'s `--grep` path)
44
+ * or a PR API check.
45
+ *
31
46
  * @param branchName - The branch name to check (e.g., "feature/33-some-title")
32
47
  * @param baseBranch - The base branch to check against (default: "main")
33
- * @returns true if the branch is merged into the base branch, false otherwise
48
+ * @returns true if a merge commit on baseBranch records branchName's tip as a
49
+ * non-first parent, false otherwise
34
50
  */
35
51
  export declare function isBranchMergedIntoMain(branchName: string, baseBranch?: string): boolean;
36
52
  /**
37
53
  * Check if a feature branch for an issue is merged into a base branch
38
54
  *
39
55
  * Tries multiple detection methods:
40
- * 1. Check if branch exists and is merged via `git branch --merged <baseBranch>`
56
+ * 1. Find `feature/<N>-*` branches with `git branch -a` and check via {@link isBranchMergedIntoMain}
41
57
  * 2. Check for merge commits mentioning the issue
42
58
  *
43
59
  * @param issueNumber - The issue number to check