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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +73 -0
- package/dist/bin/cli.js +95 -9
- package/dist/src/commands/doctor.d.ts +25 -0
- package/dist/src/commands/doctor.js +36 -1
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +118 -0
- package/dist/src/commands/locks.d.ts +67 -0
- package/dist/src/commands/locks.js +290 -0
- package/dist/src/commands/merge.js +11 -0
- package/dist/src/commands/prompt.d.ts +39 -0
- package/dist/src/commands/prompt.js +179 -0
- package/dist/src/commands/run-display.d.ts +26 -0
- package/dist/src/commands/run-display.js +150 -0
- package/dist/src/commands/run-progress.d.ts +32 -0
- package/dist/src/commands/run-progress.js +76 -0
- package/dist/src/commands/run.js +83 -73
- package/dist/src/commands/stats.d.ts +2 -0
- package/dist/src/commands/stats.js +94 -8
- package/dist/src/commands/status.js +27 -1
- package/dist/src/commands/watch.d.ts +16 -0
- package/dist/src/commands/watch.js +147 -0
- package/dist/src/lib/ac-linter.d.ts +1 -1
- package/dist/src/lib/ac-linter.js +81 -0
- package/dist/src/lib/assess-collision-detect.d.ts +91 -0
- package/dist/src/lib/assess-collision-detect.js +217 -0
- package/dist/src/lib/assess-comment-parser.d.ts +59 -1
- package/dist/src/lib/assess-comment-parser.js +124 -2
- package/dist/src/lib/cli-ui/format.d.ts +19 -0
- package/dist/src/lib/cli-ui/format.js +34 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +181 -0
- package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +239 -0
- package/dist/src/lib/cli-ui/run-renderer.js +1173 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
- package/dist/src/lib/locks/index.d.ts +7 -0
- package/dist/src/lib/locks/index.js +5 -0
- package/dist/src/lib/locks/lock-manager.d.ts +168 -0
- package/dist/src/lib/locks/lock-manager.js +433 -0
- package/dist/src/lib/locks/types.d.ts +59 -0
- package/dist/src/lib/locks/types.js +31 -0
- package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
- package/dist/src/lib/qa/markdown-only-ci.js +74 -0
- package/dist/src/lib/relay/activation.d.ts +60 -0
- package/dist/src/lib/relay/activation.js +122 -0
- package/dist/src/lib/relay/archive.d.ts +34 -0
- package/dist/src/lib/relay/archive.js +106 -0
- package/dist/src/lib/relay/frame.d.ts +20 -0
- package/dist/src/lib/relay/frame.js +76 -0
- package/dist/src/lib/relay/index.d.ts +13 -0
- package/dist/src/lib/relay/index.js +13 -0
- package/dist/src/lib/relay/paths.d.ts +43 -0
- package/dist/src/lib/relay/paths.js +59 -0
- package/dist/src/lib/relay/pid.d.ts +34 -0
- package/dist/src/lib/relay/pid.js +72 -0
- package/dist/src/lib/relay/reader.d.ts +35 -0
- package/dist/src/lib/relay/reader.js +115 -0
- package/dist/src/lib/relay/types.d.ts +68 -0
- package/dist/src/lib/relay/types.js +76 -0
- package/dist/src/lib/relay/writer.d.ts +48 -0
- package/dist/src/lib/relay/writer.js +113 -0
- package/dist/src/lib/settings.d.ts +31 -1
- package/dist/src/lib/settings.js +18 -3
- package/dist/src/lib/skill-version.d.ts +19 -0
- package/dist/src/lib/skill-version.js +68 -0
- package/dist/src/lib/templates.d.ts +1 -0
- package/dist/src/lib/templates.js +1 -1
- package/dist/src/lib/version-check.d.ts +60 -5
- package/dist/src/lib/version-check.js +97 -9
- package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
- package/dist/src/lib/workflow/batch-executor.js +249 -176
- package/dist/src/lib/workflow/config-resolver.js +4 -0
- package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
- package/dist/src/lib/workflow/heartbeat.js +194 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +88 -3
- package/dist/src/lib/workflow/phase-executor.js +276 -52
- package/dist/src/lib/workflow/phase-mapper.d.ts +3 -2
- package/dist/src/lib/workflow/phase-mapper.js +17 -20
- package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
- package/dist/src/lib/workflow/platforms/github.js +20 -3
- package/dist/src/lib/workflow/pr-status.d.ts +18 -2
- package/dist/src/lib/workflow/pr-status.js +41 -9
- package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
- package/dist/src/lib/workflow/qa-stagnation.js +179 -0
- package/dist/src/lib/workflow/run-orchestrator.d.ts +76 -0
- package/dist/src/lib/workflow/run-orchestrator.js +382 -29
- package/dist/src/lib/workflow/run-reflect.js +1 -1
- package/dist/src/lib/workflow/run-state.d.ts +71 -0
- package/dist/src/lib/workflow/run-state.js +14 -0
- package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
- package/dist/src/lib/workflow/state-cleanup.js +17 -5
- package/dist/src/lib/workflow/state-manager.d.ts +12 -1
- package/dist/src/lib/workflow/state-manager.js +37 -0
- package/dist/src/lib/workflow/state-schema.d.ts +62 -0
- package/dist/src/lib/workflow/state-schema.js +35 -1
- package/dist/src/lib/workflow/types.d.ts +74 -1
- package/dist/src/lib/workflow/worktree-manager.d.ts +12 -4
- package/dist/src/lib/workflow/worktree-manager.js +76 -17
- package/dist/src/mcp/tools/run.d.ts +44 -0
- package/dist/src/mcp/tools/run.js +104 -13
- package/dist/src/ui/tui/App.d.ts +14 -0
- package/dist/src/ui/tui/App.js +41 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
- package/dist/src/ui/tui/ElapsedTimer.js +31 -0
- package/dist/src/ui/tui/Header.d.ts +6 -0
- package/dist/src/ui/tui/Header.js +15 -0
- package/dist/src/ui/tui/IssueBox.d.ts +16 -0
- package/dist/src/ui/tui/IssueBox.js +68 -0
- package/dist/src/ui/tui/Spinner.d.ts +9 -0
- package/dist/src/ui/tui/Spinner.js +18 -0
- package/dist/src/ui/tui/index.d.ts +15 -0
- package/dist/src/ui/tui/index.js +29 -0
- package/dist/src/ui/tui/theme.d.ts +29 -0
- package/dist/src/ui/tui/theme.js +52 -0
- package/dist/src/ui/tui/truncate.d.ts +11 -0
- package/dist/src/ui/tui/truncate.js +31 -0
- package/package.json +10 -3
- package/templates/agents/sequant-explorer.md +1 -0
- package/templates/agents/sequant-qa-checker.md +2 -1
- package/templates/agents/sequant-testgen.md +1 -0
- package/templates/hooks/post-tool.sh +11 -0
- package/templates/hooks/pre-tool.sh +18 -9
- package/templates/hooks/relay-check.sh +107 -0
- package/templates/relay/frame.txt +11 -0
- package/templates/scripts/cleanup-worktree.sh +25 -3
- package/templates/scripts/new-feature.sh +6 -0
- package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
- package/templates/skills/_shared/references/subagent-types.md +21 -8
- package/templates/skills/assess/SKILL.md +261 -94
- package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
- package/templates/skills/docs/SKILL.md +141 -22
- package/templates/skills/exec/SKILL.md +10 -49
- package/templates/skills/fullsolve/SKILL.md +80 -32
- package/templates/skills/loop/SKILL.md +28 -0
- package/templates/skills/merger/SKILL.md +621 -0
- package/templates/skills/qa/SKILL.md +746 -8
- package/templates/skills/qa/scripts/quality-checks.sh +47 -1
- package/templates/skills/setup/SKILL.md +6 -0
- package/templates/skills/spec/SKILL.md +217 -964
- package/templates/skills/spec/references/parallel-groups.md +7 -0
- package/templates/skills/spec/references/quality-checklist.md +75 -0
- package/templates/skills/spec/references/recommended-workflow.md +4 -2
- package/templates/skills/test/SKILL.md +0 -27
- 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
|
-
|
|
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";
|