sequant 2.1.2 → 2.3.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 (146) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +73 -0
  4. package/dist/bin/cli.js +95 -9
  5. package/dist/src/commands/doctor.d.ts +25 -0
  6. package/dist/src/commands/doctor.js +36 -1
  7. package/dist/src/commands/init.d.ts +1 -0
  8. package/dist/src/commands/init.js +118 -0
  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 +39 -0
  13. package/dist/src/commands/prompt.js +179 -0
  14. package/dist/src/commands/run-display.d.ts +26 -0
  15. package/dist/src/commands/run-display.js +150 -0
  16. package/dist/src/commands/run-progress.d.ts +32 -0
  17. package/dist/src/commands/run-progress.js +76 -0
  18. package/dist/src/commands/run.js +83 -73
  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 +27 -1
  22. package/dist/src/commands/watch.d.ts +16 -0
  23. package/dist/src/commands/watch.js +147 -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 +181 -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 +239 -0
  35. package/dist/src/lib/cli-ui/run-renderer.js +1173 -0
  36. package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
  37. package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
  38. package/dist/src/lib/locks/index.d.ts +7 -0
  39. package/dist/src/lib/locks/index.js +5 -0
  40. package/dist/src/lib/locks/lock-manager.d.ts +168 -0
  41. package/dist/src/lib/locks/lock-manager.js +433 -0
  42. package/dist/src/lib/locks/types.d.ts +59 -0
  43. package/dist/src/lib/locks/types.js +31 -0
  44. package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
  45. package/dist/src/lib/qa/markdown-only-ci.js +74 -0
  46. package/dist/src/lib/relay/activation.d.ts +60 -0
  47. package/dist/src/lib/relay/activation.js +122 -0
  48. package/dist/src/lib/relay/archive.d.ts +34 -0
  49. package/dist/src/lib/relay/archive.js +106 -0
  50. package/dist/src/lib/relay/frame.d.ts +20 -0
  51. package/dist/src/lib/relay/frame.js +76 -0
  52. package/dist/src/lib/relay/index.d.ts +13 -0
  53. package/dist/src/lib/relay/index.js +13 -0
  54. package/dist/src/lib/relay/paths.d.ts +43 -0
  55. package/dist/src/lib/relay/paths.js +59 -0
  56. package/dist/src/lib/relay/pid.d.ts +34 -0
  57. package/dist/src/lib/relay/pid.js +72 -0
  58. package/dist/src/lib/relay/reader.d.ts +35 -0
  59. package/dist/src/lib/relay/reader.js +115 -0
  60. package/dist/src/lib/relay/types.d.ts +68 -0
  61. package/dist/src/lib/relay/types.js +76 -0
  62. package/dist/src/lib/relay/writer.d.ts +48 -0
  63. package/dist/src/lib/relay/writer.js +113 -0
  64. package/dist/src/lib/settings.d.ts +31 -1
  65. package/dist/src/lib/settings.js +18 -3
  66. package/dist/src/lib/skill-version.d.ts +19 -0
  67. package/dist/src/lib/skill-version.js +68 -0
  68. package/dist/src/lib/templates.d.ts +1 -0
  69. package/dist/src/lib/templates.js +1 -1
  70. package/dist/src/lib/version-check.d.ts +60 -5
  71. package/dist/src/lib/version-check.js +97 -9
  72. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  73. package/dist/src/lib/workflow/batch-executor.js +249 -176
  74. package/dist/src/lib/workflow/config-resolver.js +4 -0
  75. package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
  76. package/dist/src/lib/workflow/heartbeat.js +194 -0
  77. package/dist/src/lib/workflow/phase-executor.d.ts +88 -3
  78. package/dist/src/lib/workflow/phase-executor.js +276 -52
  79. package/dist/src/lib/workflow/phase-mapper.d.ts +3 -2
  80. package/dist/src/lib/workflow/phase-mapper.js +17 -20
  81. package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
  82. package/dist/src/lib/workflow/platforms/github.js +20 -3
  83. package/dist/src/lib/workflow/pr-status.d.ts +18 -2
  84. package/dist/src/lib/workflow/pr-status.js +41 -9
  85. package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
  86. package/dist/src/lib/workflow/qa-stagnation.js +179 -0
  87. package/dist/src/lib/workflow/run-orchestrator.d.ts +76 -0
  88. package/dist/src/lib/workflow/run-orchestrator.js +382 -29
  89. package/dist/src/lib/workflow/run-reflect.js +1 -1
  90. package/dist/src/lib/workflow/run-state.d.ts +71 -0
  91. package/dist/src/lib/workflow/run-state.js +14 -0
  92. package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
  93. package/dist/src/lib/workflow/state-cleanup.js +17 -5
  94. package/dist/src/lib/workflow/state-manager.d.ts +12 -1
  95. package/dist/src/lib/workflow/state-manager.js +37 -0
  96. package/dist/src/lib/workflow/state-schema.d.ts +62 -0
  97. package/dist/src/lib/workflow/state-schema.js +35 -1
  98. package/dist/src/lib/workflow/types.d.ts +74 -1
  99. package/dist/src/lib/workflow/worktree-manager.d.ts +12 -4
  100. package/dist/src/lib/workflow/worktree-manager.js +76 -17
  101. package/dist/src/mcp/tools/run.d.ts +44 -0
  102. package/dist/src/mcp/tools/run.js +104 -13
  103. package/dist/src/ui/tui/App.d.ts +14 -0
  104. package/dist/src/ui/tui/App.js +41 -0
  105. package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
  106. package/dist/src/ui/tui/ElapsedTimer.js +31 -0
  107. package/dist/src/ui/tui/Header.d.ts +6 -0
  108. package/dist/src/ui/tui/Header.js +15 -0
  109. package/dist/src/ui/tui/IssueBox.d.ts +16 -0
  110. package/dist/src/ui/tui/IssueBox.js +68 -0
  111. package/dist/src/ui/tui/Spinner.d.ts +9 -0
  112. package/dist/src/ui/tui/Spinner.js +18 -0
  113. package/dist/src/ui/tui/index.d.ts +15 -0
  114. package/dist/src/ui/tui/index.js +29 -0
  115. package/dist/src/ui/tui/theme.d.ts +29 -0
  116. package/dist/src/ui/tui/theme.js +52 -0
  117. package/dist/src/ui/tui/truncate.d.ts +11 -0
  118. package/dist/src/ui/tui/truncate.js +31 -0
  119. package/package.json +10 -3
  120. package/templates/agents/sequant-explorer.md +1 -0
  121. package/templates/agents/sequant-qa-checker.md +2 -1
  122. package/templates/agents/sequant-testgen.md +1 -0
  123. package/templates/hooks/post-tool.sh +11 -0
  124. package/templates/hooks/pre-tool.sh +18 -9
  125. package/templates/hooks/relay-check.sh +107 -0
  126. package/templates/relay/frame.txt +11 -0
  127. package/templates/scripts/cleanup-worktree.sh +25 -3
  128. package/templates/scripts/new-feature.sh +6 -0
  129. package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
  130. package/templates/skills/_shared/references/subagent-types.md +21 -8
  131. package/templates/skills/assess/SKILL.md +261 -94
  132. package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
  133. package/templates/skills/docs/SKILL.md +141 -22
  134. package/templates/skills/exec/SKILL.md +10 -49
  135. package/templates/skills/fullsolve/SKILL.md +80 -32
  136. package/templates/skills/loop/SKILL.md +28 -0
  137. package/templates/skills/merger/SKILL.md +621 -0
  138. package/templates/skills/qa/SKILL.md +746 -8
  139. package/templates/skills/qa/scripts/quality-checks.sh +47 -1
  140. package/templates/skills/setup/SKILL.md +6 -0
  141. package/templates/skills/spec/SKILL.md +217 -964
  142. package/templates/skills/spec/references/parallel-groups.md +7 -0
  143. package/templates/skills/spec/references/quality-checklist.md +75 -0
  144. package/templates/skills/spec/references/recommended-workflow.md +4 -2
  145. package/templates/skills/test/SKILL.md +0 -27
  146. package/templates/skills/testgen/SKILL.md +24 -44
@@ -75,7 +75,12 @@ export interface AssessMarkers {
75
75
  */
76
76
  export type SolveMarkers = AssessMarkers;
77
77
  /**
78
- * Check if a comment is an assess command output (new format)
78
+ * Check if a comment is an assess command output (new format).
79
+ *
80
+ * Matches either prose markers (e.g., "## Assess Analysis") or the durable
81
+ * HTML action marker `<!-- assess:action=... -->` written by every /assess
82
+ * comment regardless of prose format. The HTML marker is the contract;
83
+ * prose can change.
79
84
  */
80
85
  export declare function isAssessComment(body: string): boolean;
81
86
  /**
@@ -87,6 +92,14 @@ export declare function isSolveComment(body: string): boolean;
87
92
  * Find the most recent assess/solve comment from a list of comments
88
93
  */
89
94
  export declare function findAssessComment(comments: IssueComment[]): IssueComment | null;
95
+ /**
96
+ * Find every assess/solve comment in chronological order (oldest first).
97
+ *
98
+ * Preserves the input order of `comments`, which is the order GitHub returns
99
+ * them (chronological). Callers that need the most recent comment should
100
+ * read the last element.
101
+ */
102
+ export declare function findAllAssessComments(comments: IssueComment[]): IssueComment[];
90
103
  /**
91
104
  * Find the most recent solve comment from a list of comments
92
105
  * @deprecated Use findAssessComment instead
@@ -135,3 +148,48 @@ export declare function assessCoversIssue(workflow: AssessWorkflowResult, issueN
135
148
  * @deprecated Use assessCoversIssue instead
136
149
  */
137
150
  export declare function solveCoversIssue(workflow: AssessWorkflowResult, issueNumber: number): boolean;
151
+ /**
152
+ * Build a one-line supersession header for a new assess comment.
153
+ *
154
+ * Format:
155
+ * - 0 priors → null (caller omits the header entirely)
156
+ * - 1 prior → `Supersedes prior assess from <date> (<action>)`
157
+ * - ≥2 priors → `Supersedes N prior assessments (most recent: <date>)`
158
+ *
159
+ * Priors must be passed in chronological order (oldest first), matching
160
+ * the output of findAllAssessComments.
161
+ */
162
+ export declare function buildSupersessionHeader(priors: IssueComment[]): string | null;
163
+ /**
164
+ * Result of churn detection: whether to fire the warning, the count
165
+ * of prior assessments, and the date of the first one.
166
+ */
167
+ export interface ChurnResult {
168
+ isChurn: boolean;
169
+ count: number;
170
+ firstDate?: string;
171
+ }
172
+ /**
173
+ * Detect re-assessment churn — repeated /assess invocations without
174
+ * an intervening exec phase. Fires only when ≥3 priors exist and no
175
+ * exec phase marker appears in any comment dated after the first prior.
176
+ *
177
+ * `allComments` should be the full comment list from the issue
178
+ * (including the prior assess comments themselves) so we can search
179
+ * for SEQUANT_PHASE exec markers in non-assess comments.
180
+ */
181
+ export declare function detectChurn(priors: IssueComment[], allComments: IssueComment[]): ChurnResult;
182
+ /**
183
+ * Decide whether to prompt the user before posting a new assess comment
184
+ * that conflicts with the most recent prior recommendation.
185
+ *
186
+ * Prompts only when:
187
+ * - A prior action exists, AND
188
+ * - The prior action is PROCEED or REWRITE (the "do work" actions
189
+ * where flipping to PARK/CLOSE/etc. needs explicit confirmation), AND
190
+ * - The new action differs from the prior.
191
+ *
192
+ * Skips the prompt for prior CLOSE/PARK/CLARIFY/MERGE — flipping
193
+ * away from those is rarely "wrong" and the prompt would just be noise.
194
+ */
195
+ export declare function shouldPromptOnConflict(priorAction: AssessAction | undefined, newAction: AssessAction | undefined): boolean;
@@ -70,6 +70,11 @@ const HTML_MARKER_PATTERN = /<!--\s*(?:assess|solve):(\w[\w-]*)=([\w,.-]*)\s*-->
70
70
  * Pattern to extract assess-specific HTML markers (takes precedence)
71
71
  */
72
72
  const ASSESS_HTML_MARKER_PATTERN = /<!--\s*assess:(\w[\w-]*)=([\w,.-]*)\s*-->/g;
73
+ /**
74
+ * Non-global pattern for `.test()` checks. Matches any assess/solve action
75
+ * marker — the durable contract written by /assess regardless of prose format.
76
+ */
77
+ const ASSESS_ACTION_MARKER_PATTERN = /<!--\s*(?:assess|solve):action=[\w-]+\s*-->/;
73
78
  /**
74
79
  * Pattern to extract issue numbers from header
75
80
  * Matches: "## Solve Workflow for Issues: 123, 456" or "#123"
@@ -88,10 +93,17 @@ const VALID_ACTIONS = [
88
93
  ];
89
94
  // ─── Detection Functions ────────────────────────────────────────────────────
90
95
  /**
91
- * Check if a comment is an assess command output (new format)
96
+ * Check if a comment is an assess command output (new format).
97
+ *
98
+ * Matches either prose markers (e.g., "## Assess Analysis") or the durable
99
+ * HTML action marker `<!-- assess:action=... -->` written by every /assess
100
+ * comment regardless of prose format. The HTML marker is the contract;
101
+ * prose can change.
92
102
  */
93
103
  export function isAssessComment(body) {
94
- return ALL_MARKERS.some((marker) => body.includes(marker));
104
+ if (ALL_MARKERS.some((marker) => body.includes(marker)))
105
+ return true;
106
+ return ASSESS_ACTION_MARKER_PATTERN.test(body);
95
107
  }
96
108
  /**
97
109
  * Check if a comment is a solve command output (legacy format)
@@ -111,6 +123,16 @@ export function findAssessComment(comments) {
111
123
  }
112
124
  return null;
113
125
  }
126
+ /**
127
+ * Find every assess/solve comment in chronological order (oldest first).
128
+ *
129
+ * Preserves the input order of `comments`, which is the order GitHub returns
130
+ * them (chronological). Callers that need the most recent comment should
131
+ * read the last element.
132
+ */
133
+ export function findAllAssessComments(comments) {
134
+ return comments.filter((c) => isAssessComment(c.body));
135
+ }
114
136
  /**
115
137
  * Find the most recent solve comment from a list of comments
116
138
  * @deprecated Use findAssessComment instead
@@ -342,3 +364,103 @@ export function assessCoversIssue(workflow, issueNumber) {
342
364
  export function solveCoversIssue(workflow, issueNumber) {
343
365
  return assessCoversIssue(workflow, issueNumber);
344
366
  }
367
+ // ─── Prior-Assessment Detection ─────────────────────────────────────────────
368
+ /**
369
+ * Pattern matching exec phase markers in any comment.
370
+ * Used by detectChurn to differentiate "no execution" churn from
371
+ * legitimate re-assessment after exec.
372
+ */
373
+ const EXEC_PHASE_MARKER_PATTERN = /<!--\s*SEQUANT_PHASE:\s*\{[^}]*"phase"\s*:\s*"exec"[^}]*\}\s*-->/;
374
+ /**
375
+ * Format a comment's createdAt timestamp as YYYY-MM-DD.
376
+ * Falls back to the raw value when missing or unparseable so output
377
+ * still says something useful.
378
+ */
379
+ function formatAssessDate(createdAt) {
380
+ if (!createdAt)
381
+ return "unknown date";
382
+ const parsed = new Date(createdAt);
383
+ if (isNaN(parsed.getTime()))
384
+ return createdAt;
385
+ return parsed.toISOString().slice(0, 10);
386
+ }
387
+ /**
388
+ * Build a one-line supersession header for a new assess comment.
389
+ *
390
+ * Format:
391
+ * - 0 priors → null (caller omits the header entirely)
392
+ * - 1 prior → `Supersedes prior assess from <date> (<action>)`
393
+ * - ≥2 priors → `Supersedes N prior assessments (most recent: <date>)`
394
+ *
395
+ * Priors must be passed in chronological order (oldest first), matching
396
+ * the output of findAllAssessComments.
397
+ */
398
+ export function buildSupersessionHeader(priors) {
399
+ if (priors.length === 0)
400
+ return null;
401
+ const mostRecent = priors[priors.length - 1];
402
+ const date = formatAssessDate(mostRecent.createdAt);
403
+ if (priors.length === 1) {
404
+ const workflow = parseAssessWorkflow(mostRecent.body);
405
+ const action = workflow.action ?? "unknown";
406
+ return `Supersedes prior assess from ${date} (${action})`;
407
+ }
408
+ return `Supersedes ${priors.length} prior assessments (most recent: ${date})`;
409
+ }
410
+ /**
411
+ * Detect re-assessment churn — repeated /assess invocations without
412
+ * an intervening exec phase. Fires only when ≥3 priors exist and no
413
+ * exec phase marker appears in any comment dated after the first prior.
414
+ *
415
+ * `allComments` should be the full comment list from the issue
416
+ * (including the prior assess comments themselves) so we can search
417
+ * for SEQUANT_PHASE exec markers in non-assess comments.
418
+ */
419
+ export function detectChurn(priors, allComments) {
420
+ const count = priors.length;
421
+ if (count < 3) {
422
+ return { isChurn: false, count };
423
+ }
424
+ const firstPrior = priors[0];
425
+ const firstDate = formatAssessDate(firstPrior.createdAt);
426
+ const firstTimestamp = firstPrior.createdAt
427
+ ? new Date(firstPrior.createdAt).getTime()
428
+ : NaN;
429
+ const hasExecAfterFirst = allComments.some((c) => {
430
+ if (!EXEC_PHASE_MARKER_PATTERN.test(c.body))
431
+ return false;
432
+ if (!Number.isFinite(firstTimestamp))
433
+ return true;
434
+ if (!c.createdAt)
435
+ return true;
436
+ const ts = new Date(c.createdAt).getTime();
437
+ if (isNaN(ts))
438
+ return true;
439
+ return ts >= firstTimestamp;
440
+ });
441
+ return {
442
+ isChurn: !hasExecAfterFirst,
443
+ count,
444
+ firstDate,
445
+ };
446
+ }
447
+ /**
448
+ * Decide whether to prompt the user before posting a new assess comment
449
+ * that conflicts with the most recent prior recommendation.
450
+ *
451
+ * Prompts only when:
452
+ * - A prior action exists, AND
453
+ * - The prior action is PROCEED or REWRITE (the "do work" actions
454
+ * where flipping to PARK/CLOSE/etc. needs explicit confirmation), AND
455
+ * - The new action differs from the prior.
456
+ *
457
+ * Skips the prompt for prior CLOSE/PARK/CLARIFY/MERGE — flipping
458
+ * away from those is rarely "wrong" and the prompt would just be noise.
459
+ */
460
+ export function shouldPromptOnConflict(priorAction, newAction) {
461
+ if (!priorAction || !newAction)
462
+ return false;
463
+ if (priorAction === newAction)
464
+ return false;
465
+ return priorAction === "PROCEED" || priorAction === "REWRITE";
466
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared formatting helpers used by run-renderer and friends.
3
+ *
4
+ * Lives in cli-ui/ so renderer and heartbeat can share without depending on
5
+ * the legacy phase-spinner module.
6
+ */
7
+ /**
8
+ * Format elapsed time in human-readable form.
9
+ *
10
+ * @example
11
+ * formatElapsedTime(5) // "5s"
12
+ * formatElapsedTime(75) // "1m 15s"
13
+ * formatElapsedTime(3725) // "1h 2m"
14
+ */
15
+ export declare function formatElapsedTime(seconds: number): string;
16
+ /**
17
+ * Format a wall-clock timestamp as `HH:MM:SS`. Used by the non-TTY renderer.
18
+ */
19
+ export declare function formatTimestamp(date: Date): string;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared formatting helpers used by run-renderer and friends.
3
+ *
4
+ * Lives in cli-ui/ so renderer and heartbeat can share without depending on
5
+ * the legacy phase-spinner module.
6
+ */
7
+ /**
8
+ * Format elapsed time in human-readable form.
9
+ *
10
+ * @example
11
+ * formatElapsedTime(5) // "5s"
12
+ * formatElapsedTime(75) // "1m 15s"
13
+ * formatElapsedTime(3725) // "1h 2m"
14
+ */
15
+ export function formatElapsedTime(seconds) {
16
+ const s = Math.max(0, Math.floor(seconds));
17
+ if (s < 60)
18
+ return `${s}s`;
19
+ if (s < 3600) {
20
+ const m = Math.floor(s / 60);
21
+ const r = s % 60;
22
+ return r > 0 ? `${m}m ${r}s` : `${m}m`;
23
+ }
24
+ const h = Math.floor(s / 3600);
25
+ const r = Math.floor((s % 3600) / 60);
26
+ return r > 0 ? `${h}h ${r}m` : `${h}h`;
27
+ }
28
+ /**
29
+ * Format a wall-clock timestamp as `HH:MM:SS`. Used by the non-TTY renderer.
30
+ */
31
+ export function formatTimestamp(date) {
32
+ const pad = (n) => String(n).padStart(2, "0");
33
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
34
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Types shared between RunRenderer modes (TTY, non-TTY, orchestrator).
3
+ *
4
+ * The renderer is event-driven: a `ProgressEvent` flows in and the renderer
5
+ * decides whether to update the live zone, append an event line, or both.
6
+ */
7
+ export type ProgressEventKind = "start" | "complete" | "failed";
8
+ /** Raw event from batch-executor `emitProgressLine` / `onProgress` callbacks. */
9
+ export interface ProgressEvent {
10
+ issue: number;
11
+ phase: string;
12
+ event: ProgressEventKind;
13
+ durationSeconds?: number;
14
+ error?: string;
15
+ /**
16
+ * Quality-loop iteration (1-based). Set on the `loop` phase event and on
17
+ * retried phase events (exec, qa, ...) once the outer loop iterates past 1.
18
+ * Surfaced in the events log as `(attempt N/M)` and in the live-zone status
19
+ * cell as `loop N/M` (#624 Item 3).
20
+ */
21
+ iteration?: number;
22
+ }
23
+ /** Per-phase status tracked inside the renderer state machine. */
24
+ export interface PhaseState {
25
+ name: string;
26
+ status: "pending" | "running" | "done" | "failed";
27
+ startedAt?: number;
28
+ durationMs?: number;
29
+ /** Loop iteration label (e.g. "loop 2/3"). */
30
+ loopIteration?: number;
31
+ /**
32
+ * #624 Item 4: normalized signature of the most recent failure for THIS
33
+ * phase (ANSI-stripped, lowercased, first 80 chars, trimmed). Per-phase so
34
+ * "same failure as attempt N" never references a different phase's attempt.
35
+ */
36
+ lastFailureSignature?: string;
37
+ /**
38
+ * #624 Item 4: 1-based attempt number for this phase when
39
+ * `lastFailureSignature` was first observed. Referenced in the abbreviated
40
+ * form `(attempt N/M, same failure as attempt K)`.
41
+ */
42
+ firstAttemptForSignature?: number;
43
+ }
44
+ /** Per-issue status tracked inside the renderer state machine. */
45
+ export interface IssueState {
46
+ issueNumber: number;
47
+ title?: string;
48
+ worktreePath?: string;
49
+ branch?: string;
50
+ status: "queued" | "running" | "done" | "failed";
51
+ phases: PhaseState[];
52
+ currentPhase?: string;
53
+ startedAt?: number;
54
+ completedAt?: number;
55
+ prNumber?: number;
56
+ prUrl?: string;
57
+ /** Optional sub-status line (e.g. "claude streaming · editing src/cli.ts"). */
58
+ subStatus?: string;
59
+ /** Last QA verdict / error reason for failed issues. */
60
+ failureReason?: string;
61
+ /**
62
+ * AC-23: auto-detect mode — render `Phase: detecting…` until spec finishes
63
+ * and the resolved plan is known.
64
+ */
65
+ autoDetect?: boolean;
66
+ }
67
+ /** Initial registration payload — fed at runner start so queued rows render. */
68
+ export interface IssueRegistration {
69
+ issueNumber: number;
70
+ title?: string;
71
+ worktreePath?: string;
72
+ branch?: string;
73
+ /**
74
+ * AC-23: when true, the issue runs in auto-detect mode. The renderer shows
75
+ * `Phase: detecting…` while spec is running (before the resolved phase plan
76
+ * is known) and switches to the normal phase header once spec completes.
77
+ */
78
+ autoDetect?: boolean;
79
+ }
80
+ /** Per-issue summary fields used by the final summary table. */
81
+ export interface IssueSummary {
82
+ issueNumber: number;
83
+ success: boolean;
84
+ durationSeconds?: number;
85
+ phases: Array<{
86
+ name: string;
87
+ success: boolean;
88
+ }>;
89
+ loopTriggered?: boolean;
90
+ prNumber?: number;
91
+ prUrl?: string;
92
+ failureReason?: string;
93
+ qaVerdict?: string;
94
+ unmetCount?: number;
95
+ }
96
+ /** Inputs needed to render the final summary block. */
97
+ export interface SummaryRenderInput {
98
+ issues: IssueSummary[];
99
+ totalDurationSeconds?: number;
100
+ logPath?: string | null;
101
+ dryRun?: boolean;
102
+ }
103
+ /** Public renderer interface — all modes implement this. */
104
+ export interface RunRenderer {
105
+ /** Register an issue so it shows as `queued` before the first phase event. */
106
+ registerIssue(reg: IssueRegistration): void;
107
+ /** Feed a progress event from batch-executor. */
108
+ onEvent(event: ProgressEvent): void;
109
+ /** Mark an issue as completed with PR info. Called by orchestrator. */
110
+ setPullRequest(issue: number, prNumber: number, prUrl: string): void;
111
+ /** Pause live updates so verbose streaming can write through. */
112
+ pause(): void;
113
+ /** Resume live updates after streaming ends. */
114
+ resume(): void;
115
+ /** Render the final summary block. */
116
+ renderSummary(input: SummaryRenderInput): void;
117
+ /** Tear down timers, cursor state, signal listeners. */
118
+ dispose(): void;
119
+ }
120
+ /**
121
+ * Options shared by every renderer mode. Most are optional; defaults match
122
+ * production behaviour (real stdout, real timers, real signals).
123
+ */
124
+ export interface RenderOptions {
125
+ /** Override stdout writer. Defaults to `process.stdout.write`. */
126
+ stdoutWrite?: (s: string) => void;
127
+ /** Override stderr writer. Defaults to `process.stderr.write`. */
128
+ stderrWrite?: (s: string) => void;
129
+ /** Override clock. Defaults to `Date.now`. */
130
+ now?: () => number;
131
+ /** Override "wall clock" used for non-TTY timestamps. Defaults to `() => new Date()`. */
132
+ wallClock?: () => Date;
133
+ /** Override TTY detection. Defaults to `process.stdout.isTTY`. */
134
+ isTTY?: boolean;
135
+ /** Override terminal column count. Defaults to `process.stdout.columns`. */
136
+ columns?: number;
137
+ /** Disable color output (NO_COLOR). */
138
+ noColor?: boolean;
139
+ /** Heartbeat tick interval for the live zone (ms). Defaults to 1000. */
140
+ liveTickMs?: number;
141
+ /** Idle heartbeat interval for non-TTY mode (ms). Defaults to 60_000. */
142
+ nonTtyHeartbeatMs?: number;
143
+ /** Don't subscribe to SIGWINCH (used in tests). */
144
+ noSignalListeners?: boolean;
145
+ /**
146
+ * AC-26: when a running phase has been active for longer than this many ms
147
+ * with no completion event, the status header flips to `⚠ stalled · …`.
148
+ * Defaults to half the phase timeout when wired from settings; effectively
149
+ * disabled (10× the default heartbeat) when omitted.
150
+ */
151
+ stallThresholdMs?: number;
152
+ /**
153
+ * AC-28: cap visible per-issue rows in the multi-issue live grid. When the
154
+ * total issue count exceeds this, the oldest done rows roll up into a
155
+ * single `✔ {N} done` summary row at the top. Defaults to 10.
156
+ */
157
+ multiIssueRowCap?: number;
158
+ /**
159
+ * #624 Item 1: terminal row count. The TTY live zone caps its frame height
160
+ * at `max(8, rows - 5)` so `log-update` never deals with a frame taller than
161
+ * the terminal — otherwise it loses cursor tracking and appends fresh frames
162
+ * instead of replacing them. Defaults to `process.stdout.rows` or 24.
163
+ */
164
+ rows?: number;
165
+ /**
166
+ * #624 Item 3 / D2: total allowed quality-loop iterations. Used by every
167
+ * retry-suffix site (`(attempt N/M)` / `loop N/M`) so the M denominator
168
+ * tracks the configured maximum instead of being hardcoded. Defaults to 3.
169
+ */
170
+ maxLoopIterations?: number;
171
+ /**
172
+ * When true, `renderSummary` is rendered even if no issues were registered.
173
+ * Default: false (matches existing displaySummary behaviour).
174
+ */
175
+ alwaysRenderSummary?: boolean;
176
+ }
177
+ /**
178
+ * Mode the renderer should run in. Auto-detected by `createRunRenderer` from
179
+ * env + TTY, but explicit selection is supported for tests.
180
+ */
181
+ export type RendererMode = "tty" | "non-tty" | "orchestrator";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Types shared between RunRenderer modes (TTY, non-TTY, orchestrator).
3
+ *
4
+ * The renderer is event-driven: a `ProgressEvent` flows in and the renderer
5
+ * decides whether to update the live zone, append an event line, or both.
6
+ */
7
+ export {};