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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +81 -5
- package/dist/bin/cli.js +140 -13
- package/dist/src/commands/abort.d.ts +36 -0
- package/dist/src/commands/abort.js +138 -0
- package/dist/src/commands/doctor.d.ts +25 -0
- package/dist/src/commands/doctor.js +36 -1
- 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 +46 -0
- package/dist/src/commands/prompt.js +273 -0
- package/dist/src/commands/run-display.d.ts +11 -2
- package/dist/src/commands/run-display.js +62 -28
- package/dist/src/commands/run-progress.d.ts +42 -0
- package/dist/src/commands/run-progress.js +93 -0
- package/dist/src/commands/run.js +90 -18
- package/dist/src/commands/stats.d.ts +2 -0
- package/dist/src/commands/stats.js +94 -8
- package/dist/src/commands/status.js +12 -0
- package/dist/src/commands/watch.d.ts +18 -0
- package/dist/src/commands/watch.js +211 -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 +220 -0
- package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +265 -0
- package/dist/src/lib/cli-ui/run-renderer.js +1390 -0
- package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
- package/dist/src/lib/cli-ui/scrollback-harness.js +294 -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/merge-check/types.js +1 -1
- 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 +112 -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 +70 -0
- package/dist/src/lib/relay/types.js +85 -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/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 +274 -185
- package/dist/src/lib/workflow/config-resolver.js +4 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
- package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
- package/dist/src/lib/workflow/drivers/aider.js +9 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
- package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
- package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
- package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
- package/dist/src/lib/workflow/event-emitter.js +102 -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/notice.d.ts +32 -0
- package/dist/src/lib/workflow/notice.js +38 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +58 -16
- package/dist/src/lib/workflow/phase-executor.js +244 -130
- package/dist/src/lib/workflow/phase-mapper.d.ts +27 -13
- package/dist/src/lib/workflow/phase-mapper.js +70 -51
- package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
- package/dist/src/lib/workflow/phase-registry.js +233 -0
- 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-log-schema.d.ts +5 -55
- package/dist/src/lib/workflow/run-orchestrator.d.ts +70 -1
- package/dist/src/lib/workflow/run-orchestrator.js +464 -25
- 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 +31 -2
- package/dist/src/lib/workflow/state-manager.js +64 -1
- package/dist/src/lib/workflow/state-schema.d.ts +82 -35
- package/dist/src/lib/workflow/state-schema.js +63 -4
- package/dist/src/lib/workflow/types.d.ts +139 -16
- package/dist/src/lib/workflow/types.js +18 -13
- package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
- package/dist/src/lib/workflow/worktree-manager.js +15 -6
- 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 +14 -6
- 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 +92 -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 +122 -68
- 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 -8
- package/templates/skills/fullsolve/SKILL.md +79 -5
- package/templates/skills/loop/SKILL.md +28 -0
- package/templates/skills/merger/SKILL.md +621 -0
- package/templates/skills/qa/SKILL.md +727 -8
- package/templates/skills/setup/SKILL.md +12 -6
- package/templates/skills/spec/SKILL.md +52 -0
- package/templates/skills/spec/references/parallel-groups.md +7 -0
- package/templates/skills/spec/references/recommended-workflow.md +4 -2
- package/templates/skills/testgen/SKILL.md +24 -17
|
@@ -7,27 +7,47 @@ import type { LogWriter } from "./log-writer.js";
|
|
|
7
7
|
import type { StateManager } from "./state-manager.js";
|
|
8
8
|
import type { ShutdownManager } from "../shutdown.js";
|
|
9
9
|
import type { WorktreeInfo } from "./worktree-manager.js";
|
|
10
|
+
export type { WorkflowEventEmitter, WorkflowEvents, WorkflowEventListener, IssueEventStatus, BaseEventPayload, RunEventPayload, PhaseStartedPayload, PhaseCompletedPayload, PhaseFailedPayload, IssueStatusChangedPayload, QaVerdictPayload, ProgressPayload, } from "./event-emitter.js";
|
|
10
11
|
/**
|
|
11
12
|
* Canonical Zod schema for all workflow phases.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* Backed by the phase registry. `PhaseSchema.parse(name)` succeeds iff
|
|
15
|
+
* `phaseRegistry.has(name)`. The set of valid phases is the registry's
|
|
16
|
+
* keys at the time of parsing — registration happens at module load,
|
|
17
|
+
* so for normal runtime use the set is fixed by the time any code parses.
|
|
18
|
+
*
|
|
19
|
+
* This replaces the prior `z.enum([...])` literal. The set of valid names
|
|
20
|
+
* is identical for the 9 built-in phases; the only observable behavior
|
|
21
|
+
* change is that `PhaseSchema.options` is no longer available — use
|
|
22
|
+
* `getPhaseNames()` from `phase-registry.ts` instead.
|
|
23
|
+
*/
|
|
24
|
+
export declare const PhaseSchema: z.ZodString;
|
|
25
|
+
/**
|
|
26
|
+
* Available workflow phases. Widened from a string-literal union to `string`
|
|
27
|
+
* after the registry migration — exhaustiveness checking on `switch (phase)`
|
|
28
|
+
* is now a runtime concern (see the comment in phase-executor.ts where the
|
|
29
|
+
* only relevant switch lives).
|
|
15
30
|
*/
|
|
16
|
-
export
|
|
17
|
-
qa: "qa";
|
|
18
|
-
loop: "loop";
|
|
19
|
-
verify: "verify";
|
|
20
|
-
spec: "spec";
|
|
21
|
-
"security-review": "security-review";
|
|
22
|
-
exec: "exec";
|
|
23
|
-
testgen: "testgen";
|
|
24
|
-
test: "test";
|
|
25
|
-
merger: "merger";
|
|
26
|
-
}>;
|
|
31
|
+
export type Phase = string;
|
|
27
32
|
/**
|
|
28
|
-
*
|
|
33
|
+
* Lifecycle hook for pausing the run renderer's live zone while verbose
|
|
34
|
+
* Claude streaming writes through stdout, then resuming after the agent
|
|
35
|
+
* call completes. Replaces the legacy `PhaseSpinner` argument (#618).
|
|
36
|
+
*
|
|
37
|
+
* Lives in the workflow types barrel so the cli-ui layer can implement it
|
|
38
|
+
* without the workflow layer reaching back into cli-ui (#656).
|
|
29
39
|
*/
|
|
30
|
-
export
|
|
40
|
+
export interface PhasePauseHandle {
|
|
41
|
+
pause(): void;
|
|
42
|
+
resume(): void;
|
|
43
|
+
/**
|
|
44
|
+
* #647 AC-3: print a notice line (e.g., retry/fallback message) without
|
|
45
|
+
* breaking log-update's cursor model. Implementations clear the live zone,
|
|
46
|
+
* write the line through the renderer's own stdout channel, then redraw.
|
|
47
|
+
* In quiet / non-TTY paths this degrades to a plain write.
|
|
48
|
+
*/
|
|
49
|
+
appendNotice(message: string): void;
|
|
50
|
+
}
|
|
31
51
|
/**
|
|
32
52
|
* Default phases for workflow execution
|
|
33
53
|
*/
|
|
@@ -101,6 +121,21 @@ export interface ExecutionConfig {
|
|
|
101
121
|
* Propagated as SEQUANT_FAILED_ACS env var to skills.
|
|
102
122
|
*/
|
|
103
123
|
failedAcs?: string;
|
|
124
|
+
/**
|
|
125
|
+
* Runtime callback invoked when the agent driver emits a chunk of output
|
|
126
|
+
* during phase execution (#543). Used by the multi-issue TUI to enrich
|
|
127
|
+
* `nowLine` with sub-phase activity. Not serialized — set per-call by the
|
|
128
|
+
* orchestrator. The phase executor throttles calls to ~10 Hz so the TUI's
|
|
129
|
+
* poll budget is preserved.
|
|
130
|
+
*/
|
|
131
|
+
onActivity?: (text: string) => void;
|
|
132
|
+
/**
|
|
133
|
+
* Enable interactive relay (#383). When true, phase-executor sets
|
|
134
|
+
* `SEQUANT_RELAY=true` in the agent environment so the PostToolUse hook
|
|
135
|
+
* starts polling `<worktree>/.sequant/relay/inbox.jsonl` for user messages.
|
|
136
|
+
* Default: false (opt-in for the initial rollout).
|
|
137
|
+
*/
|
|
138
|
+
relayEnabled?: boolean;
|
|
104
139
|
}
|
|
105
140
|
/**
|
|
106
141
|
* Default execution configuration
|
|
@@ -143,6 +178,18 @@ export interface IssueResult {
|
|
|
143
178
|
prNumber?: number;
|
|
144
179
|
/** PR URL if created after successful QA */
|
|
145
180
|
prUrl?: string;
|
|
181
|
+
/**
|
|
182
|
+
* Set when the issue was skipped because another sequant session holds
|
|
183
|
+
* the per-issue lock (#625). Surfaced in the summary as
|
|
184
|
+
* `locked by PID <n>`. When present, `success` is false and the issue
|
|
185
|
+
* was not executed.
|
|
186
|
+
*/
|
|
187
|
+
locked?: {
|
|
188
|
+
pid: number;
|
|
189
|
+
hostname: string;
|
|
190
|
+
startedAt: string;
|
|
191
|
+
command: string;
|
|
192
|
+
};
|
|
146
193
|
}
|
|
147
194
|
/**
|
|
148
195
|
* CLI options for the run command, merged with settings and env config.
|
|
@@ -163,6 +210,7 @@ export interface RunOptions {
|
|
|
163
210
|
smartTests?: boolean;
|
|
164
211
|
noSmartTests?: boolean;
|
|
165
212
|
testgen?: boolean;
|
|
213
|
+
securityReview?: boolean;
|
|
166
214
|
autoDetectPhases?: boolean;
|
|
167
215
|
/** Enable automatic worktree creation for issue isolation */
|
|
168
216
|
worktreeIsolation?: boolean;
|
|
@@ -172,6 +220,12 @@ export interface RunOptions {
|
|
|
172
220
|
quiet?: boolean;
|
|
173
221
|
/** Chain issues: each branches from previous (requires --sequential) */
|
|
174
222
|
chain?: boolean;
|
|
223
|
+
/**
|
|
224
|
+
* Stacked PRs: each non-first PR targets its predecessor branch instead of
|
|
225
|
+
* `main`. Implies --chain. The final PR still targets `main` so partial
|
|
226
|
+
* progress can land without the whole stack. (#605)
|
|
227
|
+
*/
|
|
228
|
+
stacked?: boolean;
|
|
175
229
|
/**
|
|
176
230
|
* Wait for QA pass before starting next issue in chain mode.
|
|
177
231
|
* When enabled, the chain pauses if QA fails, preventing downstream issues
|
|
@@ -239,6 +293,24 @@ export interface RunOptions {
|
|
|
239
293
|
* Resolution priority: CLI flag → settings.agents.isolateParallel → false
|
|
240
294
|
*/
|
|
241
295
|
isolateParallel?: boolean;
|
|
296
|
+
/**
|
|
297
|
+
* Render a live multi-issue dashboard during the run.
|
|
298
|
+
* Requires a TTY; auto-falls back to linear output when stdout is piped.
|
|
299
|
+
* Experimental — surface and behavior may change.
|
|
300
|
+
*/
|
|
301
|
+
experimentalTui?: boolean;
|
|
302
|
+
/**
|
|
303
|
+
* With `--force`, SIGTERM the prior PID holding the per-issue lock
|
|
304
|
+
* before claiming it. Only acts on same-host alive PIDs. (#625)
|
|
305
|
+
*/
|
|
306
|
+
signalOther?: boolean;
|
|
307
|
+
/**
|
|
308
|
+
* Interactive relay (#383). Set via `--no-relay`, which Commander surfaces as
|
|
309
|
+
* `options.relay = false`. When `false`, the PostToolUse hook is not
|
|
310
|
+
* activated and `sequant prompt` cannot reach this run.
|
|
311
|
+
* Resolution priority: this CLI flag → settings.run.relay → default (true).
|
|
312
|
+
*/
|
|
313
|
+
relay?: boolean;
|
|
242
314
|
}
|
|
243
315
|
/**
|
|
244
316
|
* CLI arguments for run command
|
|
@@ -271,11 +343,32 @@ export interface BatchResult {
|
|
|
271
343
|
/**
|
|
272
344
|
* Callback type for per-phase progress updates.
|
|
273
345
|
* Used by parallel mode in run.ts to render phase status to the terminal.
|
|
346
|
+
*
|
|
347
|
+
* `extra.iteration` (#624 Item 3): outer quality-loop iteration. Threaded
|
|
348
|
+
* through to the renderer as `(attempt N/M)` on retried phase events and
|
|
349
|
+
* `loop N/M` on loop-phase live-zone status cells.
|
|
350
|
+
*
|
|
351
|
+
* `"activity"` (#543): sub-phase activity ping. `extra.text` carries a short
|
|
352
|
+
* one-line snippet (e.g. last line of agent output) for the dashboard's
|
|
353
|
+
* `nowLine`. Fires at most ~10 Hz from the phase executor.
|
|
274
354
|
*/
|
|
275
|
-
export type ProgressCallback = (issue: number, phase: string, event: "start" | "complete" | "failed", extra?: {
|
|
355
|
+
export type ProgressCallback = (issue: number, phase: string, event: "start" | "complete" | "failed" | "activity", extra?: {
|
|
276
356
|
durationSeconds?: number;
|
|
277
357
|
error?: string;
|
|
358
|
+
iteration?: number;
|
|
359
|
+
text?: string;
|
|
278
360
|
}) => void;
|
|
361
|
+
/**
|
|
362
|
+
* #672 AC-2: fired once per issue after the executor has resolved the final
|
|
363
|
+
* phase pipeline (post auto-detect, post resume filter, post testgen /
|
|
364
|
+
* security-review insertion). Lets the run renderer seed pending cells for
|
|
365
|
+
* the full roadmap before any phase fires, so users see what is about to run
|
|
366
|
+
* instead of phases appearing one at a time as they stream.
|
|
367
|
+
*
|
|
368
|
+
* Empty `phases` means "no plan known" — the renderer should fall back to
|
|
369
|
+
* streaming-only display.
|
|
370
|
+
*/
|
|
371
|
+
export type PhasePlanCallback = (issue: number, phases: string[]) => void;
|
|
279
372
|
/**
|
|
280
373
|
* Shared context for executing a batch of issues.
|
|
281
374
|
* Replaces 11 positional parameters in executeBatch (#402).
|
|
@@ -294,6 +387,17 @@ export interface BatchExecutionContext {
|
|
|
294
387
|
packageManager?: string;
|
|
295
388
|
baseBranch?: string;
|
|
296
389
|
onProgress?: ProgressCallback;
|
|
390
|
+
/** #672 AC-2: forwarded to per-issue context so batch-executor can fire it
|
|
391
|
+
* once the final phase pipeline is known. */
|
|
392
|
+
onPhasePlan?: PhasePlanCallback;
|
|
393
|
+
/**
|
|
394
|
+
* Optional live-zone pause handle (#656). When set, the phase executor calls
|
|
395
|
+
* `pause()` before forwarding verbose Claude SDK output to stdout and
|
|
396
|
+
* `resume()` after the agent call completes — so the 1Hz live grid does not
|
|
397
|
+
* collide with streaming text. Wired from the active `RunRenderer` at the
|
|
398
|
+
* composition root in `run.ts`; left undefined for quiet/TUI modes.
|
|
399
|
+
*/
|
|
400
|
+
phasePauseHandle?: PhasePauseHandle;
|
|
297
401
|
}
|
|
298
402
|
/**
|
|
299
403
|
* Context object for executing a single issue through the workflow.
|
|
@@ -325,6 +429,17 @@ export interface IssueExecutionContext {
|
|
|
325
429
|
chain?: {
|
|
326
430
|
enabled: boolean;
|
|
327
431
|
isLast: boolean;
|
|
432
|
+
/**
|
|
433
|
+
* Stacked-PR base branch for this issue. Set only when --stacked is active
|
|
434
|
+
* and this issue has a predecessor in the chain. When set, createPR targets
|
|
435
|
+
* this branch instead of `main`. (#605)
|
|
436
|
+
*/
|
|
437
|
+
predecessorBranch?: string;
|
|
438
|
+
/**
|
|
439
|
+
* Pre-rendered stack manifest line for the PR body, e.g.
|
|
440
|
+
* `Part of stack: #100 → #101 (this) → #102`. Set only under --stacked.
|
|
441
|
+
*/
|
|
442
|
+
stackManifest?: string;
|
|
328
443
|
};
|
|
329
444
|
/** Package manager name (e.g., "npm", "pnpm") */
|
|
330
445
|
packageManager?: string;
|
|
@@ -332,4 +447,12 @@ export interface IssueExecutionContext {
|
|
|
332
447
|
baseBranch?: string;
|
|
333
448
|
/** Per-phase progress callback (used in parallel mode) */
|
|
334
449
|
onProgress?: ProgressCallback;
|
|
450
|
+
/** #672 AC-2: invoked once after the per-issue phase plan resolves. */
|
|
451
|
+
onPhasePlan?: PhasePlanCallback;
|
|
452
|
+
/**
|
|
453
|
+
* Optional live-zone pause handle (#656). Forwarded to
|
|
454
|
+
* `executePhaseWithRetry` so the renderer's `pause`/`resume` hooks fire
|
|
455
|
+
* around verbose Claude streaming.
|
|
456
|
+
*/
|
|
457
|
+
phasePauseHandle?: PhasePauseHandle;
|
|
335
458
|
}
|
|
@@ -2,23 +2,28 @@
|
|
|
2
2
|
* Core types for workflow execution
|
|
3
3
|
*/
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
// Importing the registry triggers its side-effect registrations (built-ins
|
|
6
|
+
// live at the bottom of phase-registry.ts), guaranteeing the registry is
|
|
7
|
+
// populated before any PhaseSchema parse runs.
|
|
8
|
+
import { phaseRegistry, getPhaseNames } from "./phase-registry.js";
|
|
5
9
|
/**
|
|
6
10
|
* Canonical Zod schema for all workflow phases.
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
12
|
+
* Backed by the phase registry. `PhaseSchema.parse(name)` succeeds iff
|
|
13
|
+
* `phaseRegistry.has(name)`. The set of valid phases is the registry's
|
|
14
|
+
* keys at the time of parsing — registration happens at module load,
|
|
15
|
+
* so for normal runtime use the set is fixed by the time any code parses.
|
|
16
|
+
*
|
|
17
|
+
* This replaces the prior `z.enum([...])` literal. The set of valid names
|
|
18
|
+
* is identical for the 9 built-in phases; the only observable behavior
|
|
19
|
+
* change is that `PhaseSchema.options` is no longer available — use
|
|
20
|
+
* `getPhaseNames()` from `phase-registry.ts` instead.
|
|
10
21
|
*/
|
|
11
|
-
export const PhaseSchema = z
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
"test",
|
|
17
|
-
"verify",
|
|
18
|
-
"qa",
|
|
19
|
-
"loop",
|
|
20
|
-
"merger",
|
|
21
|
-
]);
|
|
22
|
+
export const PhaseSchema = z
|
|
23
|
+
.string()
|
|
24
|
+
.refine((name) => phaseRegistry.has(name), {
|
|
25
|
+
error: (issue) => `Unknown phase "${String(issue.input)}". Available: ${getPhaseNames().join(", ")}`,
|
|
26
|
+
});
|
|
22
27
|
/**
|
|
23
28
|
* Default phases for workflow execution
|
|
24
29
|
*/
|
|
@@ -200,7 +200,14 @@ export declare function rebaseBeforePR(worktreePath: string, issueNumber: number
|
|
|
200
200
|
* @param issueTitle Issue title (for PR title)
|
|
201
201
|
* @param branch Branch name
|
|
202
202
|
* @param verbose Whether to show verbose output
|
|
203
|
+
* @param labels Issue labels (used to pick `fix(...)` vs `feat(...)` prefix)
|
|
204
|
+
* @param stackOptions When set under --stacked, `prBase` overrides the default
|
|
205
|
+
* PR target (otherwise gh defaults to the repo's default branch) and
|
|
206
|
+
* `stackManifest` is appended to the PR body. (#605)
|
|
203
207
|
* @returns PRCreationResult with PR info or error
|
|
204
208
|
* @internal Exported for testing
|
|
205
209
|
*/
|
|
206
|
-
export declare function createPR(worktreePath: string, issueNumber: number, issueTitle: string, branch: string, verbose: boolean, labels?: string[]
|
|
210
|
+
export declare function createPR(worktreePath: string, issueNumber: number, issueTitle: string, branch: string, verbose: boolean, labels?: string[], stackOptions?: {
|
|
211
|
+
prBase?: string;
|
|
212
|
+
stackManifest?: string;
|
|
213
|
+
}): PRCreationResult;
|
|
@@ -843,10 +843,14 @@ export function rebaseBeforePR(worktreePath, issueNumber, packageManager, verbos
|
|
|
843
843
|
* @param issueTitle Issue title (for PR title)
|
|
844
844
|
* @param branch Branch name
|
|
845
845
|
* @param verbose Whether to show verbose output
|
|
846
|
+
* @param labels Issue labels (used to pick `fix(...)` vs `feat(...)` prefix)
|
|
847
|
+
* @param stackOptions When set under --stacked, `prBase` overrides the default
|
|
848
|
+
* PR target (otherwise gh defaults to the repo's default branch) and
|
|
849
|
+
* `stackManifest` is appended to the PR body. (#605)
|
|
846
850
|
* @returns PRCreationResult with PR info or error
|
|
847
851
|
* @internal Exported for testing
|
|
848
852
|
*/
|
|
849
|
-
export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose, labels) {
|
|
853
|
+
export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose, labels, stackOptions) {
|
|
850
854
|
const github = new GitHubProvider();
|
|
851
855
|
// Step 1: Check for existing PR on this branch
|
|
852
856
|
const existingPRInfo = github.viewPRByBranchSync(branch, worktreePath);
|
|
@@ -882,17 +886,22 @@ export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose,
|
|
|
882
886
|
const isBug = labels?.some((l) => /^bug/i.test(l));
|
|
883
887
|
const prefix = isBug ? "fix" : "feat";
|
|
884
888
|
const prTitle = `${prefix}(#${issueNumber}): ${issueTitle}`;
|
|
885
|
-
const
|
|
889
|
+
const bodyLines = [
|
|
886
890
|
`## Summary`,
|
|
887
891
|
``,
|
|
888
892
|
`Automated PR for issue #${issueNumber}.`,
|
|
889
893
|
``,
|
|
890
894
|
`Fixes #${issueNumber}`,
|
|
891
895
|
``,
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
+
];
|
|
897
|
+
// #605 AC-4: emit stack manifest before the trailer so reviewers see the
|
|
898
|
+
// chain at the top of the body. Manifest is only set under --stacked.
|
|
899
|
+
if (stackOptions?.stackManifest) {
|
|
900
|
+
bodyLines.push(stackOptions.stackManifest, ``);
|
|
901
|
+
}
|
|
902
|
+
bodyLines.push(`---`, `🤖 Generated by \`sequant run\``);
|
|
903
|
+
const prBody = bodyLines.join("\n");
|
|
904
|
+
const prResult = github.createPRCliSync(prTitle, prBody, branch, worktreePath, stackOptions?.prBase);
|
|
896
905
|
if (prResult.exitCode !== 0) {
|
|
897
906
|
const prError = prResult.stderr.trim() || "Unknown error";
|
|
898
907
|
// Check if PR already exists (race condition or push-before-PR scenarios)
|
|
@@ -50,6 +50,16 @@ interface RunToolResponse {
|
|
|
50
50
|
* spawnAsync(command, [...prefixArgs, "run", ...userArgs])
|
|
51
51
|
*/
|
|
52
52
|
export declare function resolveCliBinary(): [string, string[]];
|
|
53
|
+
/**
|
|
54
|
+
* Find and parse a run log file by its exact runId suffix (#631).
|
|
55
|
+
*
|
|
56
|
+
* Avoids the recency-window heuristic in `readLatestRunLog`, which can
|
|
57
|
+
* return another concurrent run's log or a stale same-issue log when
|
|
58
|
+
* filesystem ordering doesn't favor the current run.
|
|
59
|
+
*
|
|
60
|
+
* Returns null on miss, parse failure, or I/O error (no new failure mode).
|
|
61
|
+
*/
|
|
62
|
+
export declare function readRunLogById(runId: string): Promise<RunLog | null>;
|
|
53
63
|
/**
|
|
54
64
|
* Find and parse the most recent run log file.
|
|
55
65
|
*
|
|
@@ -58,10 +68,26 @@ export declare function resolveCliBinary(): [string, string[]];
|
|
|
58
68
|
* stale logs from a previous run being returned.
|
|
59
69
|
*/
|
|
60
70
|
export declare function readLatestRunLog(runStartTime?: Date): Promise<RunLog | null>;
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the run log for an MCP `sequant_run` invocation (#631).
|
|
73
|
+
*
|
|
74
|
+
* Prefers exact-filename lookup by captured runId; falls back to the
|
|
75
|
+
* time-window heuristic when no runId was captured (older CLI, startup
|
|
76
|
+
* race) or when the runId lookup returned null (corrupted file, slow
|
|
77
|
+
* fsync). On lookup-miss with a captured runId, emit a debug line on
|
|
78
|
+
* the MCP server's own stderr so the silent fallback is observable.
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveRunLog(capturedRunId: string | null, runStartTime: Date): Promise<RunLog | null>;
|
|
61
81
|
/**
|
|
62
82
|
* Build a structured response from a parsed RunLog
|
|
63
83
|
*/
|
|
64
84
|
export declare function buildStructuredResponse(runLog: RunLog, rawOutput: string, overallStatus: "success" | "failure", exitCode?: number | null, errorOutput?: string): RunToolResponse;
|
|
85
|
+
/**
|
|
86
|
+
* Parse a SEQUANT_RUN_ID line emitted by the batch executor (#631).
|
|
87
|
+
* Returns the runId UUID or null if the line isn't a runId line or the
|
|
88
|
+
* payload isn't a well-formed UUID.
|
|
89
|
+
*/
|
|
90
|
+
export declare function parseRunIdLine(line: string): string | null;
|
|
65
91
|
/** Parsed progress event from a SEQUANT_PROGRESS line. */
|
|
66
92
|
export interface ProgressEvent {
|
|
67
93
|
issue: number;
|
|
@@ -75,6 +101,24 @@ export interface ProgressEvent {
|
|
|
75
101
|
* Returns the parsed event or null if the line isn't a progress line.
|
|
76
102
|
*/
|
|
77
103
|
export declare function parseProgressLine(line: string): ProgressEvent | null;
|
|
104
|
+
/**
|
|
105
|
+
* Stateful capture of the per-run UUID emitted on stderr by the spawned
|
|
106
|
+
* CLI (#631). Each MCP request creates its own capture instance.
|
|
107
|
+
*
|
|
108
|
+
* `routeLine` consumes a complete stderr line:
|
|
109
|
+
* - Until a `SEQUANT_RUN_ID:` line is seen, attempts to capture it.
|
|
110
|
+
* - After capture (or for non-runId lines), delegates to `parseProgressLine`
|
|
111
|
+
* and returns the parsed `ProgressEvent`, if any.
|
|
112
|
+
*
|
|
113
|
+
* Returning `ProgressEvent | null` (instead of side-effecting) keeps the
|
|
114
|
+
* factory pure and lets callers wire emission (`emitProgress`) at the
|
|
115
|
+
* outer layer. This separation also makes the capture logic directly
|
|
116
|
+
* testable without driving the full MCP request handler.
|
|
117
|
+
*/
|
|
118
|
+
export declare function createRunIdCapture(): {
|
|
119
|
+
routeLine: (line: string) => ProgressEvent | null;
|
|
120
|
+
getCapturedRunId: () => string | null;
|
|
121
|
+
};
|
|
78
122
|
/**
|
|
79
123
|
* Build a human-readable message for a progress notification (AC-3).
|
|
80
124
|
* @internal Exported for testing only.
|
|
@@ -71,6 +71,30 @@ function resolveLogDir() {
|
|
|
71
71
|
}
|
|
72
72
|
return projectPath;
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Find and parse a run log file by its exact runId suffix (#631).
|
|
76
|
+
*
|
|
77
|
+
* Avoids the recency-window heuristic in `readLatestRunLog`, which can
|
|
78
|
+
* return another concurrent run's log or a stale same-issue log when
|
|
79
|
+
* filesystem ordering doesn't favor the current run.
|
|
80
|
+
*
|
|
81
|
+
* Returns null on miss, parse failure, or I/O error (no new failure mode).
|
|
82
|
+
*/
|
|
83
|
+
export async function readRunLogById(runId) {
|
|
84
|
+
try {
|
|
85
|
+
const logDir = resolveLogDir();
|
|
86
|
+
const entries = await readdir(logDir);
|
|
87
|
+
// Filename format: run-<timestamp>-<runId>.json — match by exact suffix.
|
|
88
|
+
const match = entries.find((f) => f.endsWith(`-${runId}.json`));
|
|
89
|
+
if (!match)
|
|
90
|
+
return null;
|
|
91
|
+
const content = await readFile(join(logDir, match), "utf-8");
|
|
92
|
+
return RunLogSchema.parse(JSON.parse(content));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
74
98
|
/**
|
|
75
99
|
* Find and parse the most recent run log file.
|
|
76
100
|
*
|
|
@@ -109,6 +133,24 @@ export async function readLatestRunLog(runStartTime) {
|
|
|
109
133
|
return null;
|
|
110
134
|
}
|
|
111
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Resolve the run log for an MCP `sequant_run` invocation (#631).
|
|
138
|
+
*
|
|
139
|
+
* Prefers exact-filename lookup by captured runId; falls back to the
|
|
140
|
+
* time-window heuristic when no runId was captured (older CLI, startup
|
|
141
|
+
* race) or when the runId lookup returned null (corrupted file, slow
|
|
142
|
+
* fsync). On lookup-miss with a captured runId, emit a debug line on
|
|
143
|
+
* the MCP server's own stderr so the silent fallback is observable.
|
|
144
|
+
*/
|
|
145
|
+
export async function resolveRunLog(capturedRunId, runStartTime) {
|
|
146
|
+
if (capturedRunId !== null) {
|
|
147
|
+
const byId = await readRunLogById(capturedRunId);
|
|
148
|
+
if (byId)
|
|
149
|
+
return byId;
|
|
150
|
+
console.error(`[mcp:run] runId ${capturedRunId} lookup miss — falling back to readLatestRunLog`);
|
|
151
|
+
}
|
|
152
|
+
return readLatestRunLog(runStartTime);
|
|
153
|
+
}
|
|
112
154
|
/**
|
|
113
155
|
* Build a structured response from a parsed RunLog
|
|
114
156
|
*/
|
|
@@ -199,6 +241,21 @@ function buildFallbackResponse(stdout, issueNumbers, overallStatus, phases, exit
|
|
|
199
241
|
}
|
|
200
242
|
/** Prefix used by the batch executor to emit structured progress lines. */
|
|
201
243
|
const PROGRESS_LINE_PREFIX = "SEQUANT_PROGRESS:";
|
|
244
|
+
/** Prefix used by the batch executor to emit the current run's UUID (#631). */
|
|
245
|
+
const RUN_ID_LINE_PREFIX = "SEQUANT_RUN_ID:";
|
|
246
|
+
/** UUID v4 pattern produced by `crypto.randomUUID()`. */
|
|
247
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
248
|
+
/**
|
|
249
|
+
* Parse a SEQUANT_RUN_ID line emitted by the batch executor (#631).
|
|
250
|
+
* Returns the runId UUID or null if the line isn't a runId line or the
|
|
251
|
+
* payload isn't a well-formed UUID.
|
|
252
|
+
*/
|
|
253
|
+
export function parseRunIdLine(line) {
|
|
254
|
+
if (!line.startsWith(RUN_ID_LINE_PREFIX))
|
|
255
|
+
return null;
|
|
256
|
+
const id = line.slice(RUN_ID_LINE_PREFIX.length).trim();
|
|
257
|
+
return UUID_RE.test(id) ? id : null;
|
|
258
|
+
}
|
|
202
259
|
/**
|
|
203
260
|
* Parse a SEQUANT_PROGRESS line emitted by the batch executor.
|
|
204
261
|
* Returns the parsed event or null if the line isn't a progress line.
|
|
@@ -233,6 +290,38 @@ export function parseProgressLine(line) {
|
|
|
233
290
|
return null;
|
|
234
291
|
}
|
|
235
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Stateful capture of the per-run UUID emitted on stderr by the spawned
|
|
295
|
+
* CLI (#631). Each MCP request creates its own capture instance.
|
|
296
|
+
*
|
|
297
|
+
* `routeLine` consumes a complete stderr line:
|
|
298
|
+
* - Until a `SEQUANT_RUN_ID:` line is seen, attempts to capture it.
|
|
299
|
+
* - After capture (or for non-runId lines), delegates to `parseProgressLine`
|
|
300
|
+
* and returns the parsed `ProgressEvent`, if any.
|
|
301
|
+
*
|
|
302
|
+
* Returning `ProgressEvent | null` (instead of side-effecting) keeps the
|
|
303
|
+
* factory pure and lets callers wire emission (`emitProgress`) at the
|
|
304
|
+
* outer layer. This separation also makes the capture logic directly
|
|
305
|
+
* testable without driving the full MCP request handler.
|
|
306
|
+
*/
|
|
307
|
+
export function createRunIdCapture() {
|
|
308
|
+
let capturedRunId = null;
|
|
309
|
+
return {
|
|
310
|
+
routeLine(line) {
|
|
311
|
+
if (capturedRunId === null) {
|
|
312
|
+
const id = parseRunIdLine(line);
|
|
313
|
+
if (id) {
|
|
314
|
+
capturedRunId = id;
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return parseProgressLine(line);
|
|
319
|
+
},
|
|
320
|
+
getCapturedRunId() {
|
|
321
|
+
return capturedRunId;
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
236
325
|
/**
|
|
237
326
|
* Build a human-readable message for a progress notification (AC-3).
|
|
238
327
|
* @internal Exported for testing only.
|
|
@@ -368,22 +457,21 @@ export function registerRunTool(server) {
|
|
|
368
457
|
// Swallow synchronous errors (AC-6)
|
|
369
458
|
}
|
|
370
459
|
};
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
460
|
+
// Per-request capture of the runId line emitted by the spawned CLI
|
|
461
|
+
// (#631). `routeLine` also parses progress events; we wire emission
|
|
462
|
+
// at this layer so the capture factory stays pure / testable.
|
|
463
|
+
const { routeLine, getCapturedRunId } = createRunIdCapture();
|
|
375
464
|
const handleLine = (line) => {
|
|
376
|
-
const event =
|
|
465
|
+
const event = routeLine(line);
|
|
377
466
|
if (event)
|
|
378
467
|
emitProgress(event);
|
|
379
468
|
};
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
469
|
+
// `hasProgressToken` no longer gates the line buffer (always-on so
|
|
470
|
+
// runId capture works without a subscriber); it still gates the
|
|
471
|
+
// `onProgress` callback below, which controls spawnAsync's
|
|
472
|
+
// timeout-reset behavior on progress events.
|
|
383
473
|
const hasProgressToken = progressToken !== undefined;
|
|
384
|
-
const stderrLineBuffer =
|
|
385
|
-
? createLineBuffer(handleLine)
|
|
386
|
-
: undefined;
|
|
474
|
+
const stderrLineBuffer = createLineBuffer(handleLine);
|
|
387
475
|
// Register all issues as active runs for real-time status polling
|
|
388
476
|
for (const issue of issues) {
|
|
389
477
|
registerRun(issue);
|
|
@@ -405,8 +493,11 @@ export function registerRunTool(server) {
|
|
|
405
493
|
const stdout = result.stdout || "";
|
|
406
494
|
const stderr = result.stderr || "";
|
|
407
495
|
const overallStatus = result.exitCode === 0 ? "success" : "failure";
|
|
408
|
-
// Try to read structured log file for rich per-issue data
|
|
409
|
-
|
|
496
|
+
// Try to read structured log file for rich per-issue data.
|
|
497
|
+
// Prefer exact-filename lookup by captured runId (#631); fall back to
|
|
498
|
+
// the time-window heuristic when the CLI didn't emit a runId (older
|
|
499
|
+
// CLI, startup race) or the file is not yet visible on disk.
|
|
500
|
+
const runLog = await resolveRunLog(getCapturedRunId(), runStartTime);
|
|
410
501
|
let response;
|
|
411
502
|
if (runLog) {
|
|
412
503
|
response = buildStructuredResponse(runLog, stdout, overallStatus, result.exitCode, stderr || undefined);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type JSX } from "react";
|
|
2
|
+
import type { RunSnapshot } from "../../lib/workflow/run-state.js";
|
|
3
|
+
/**
|
|
4
|
+
* Root TUI component.
|
|
5
|
+
*
|
|
6
|
+
* Polls `getSnapshot` at 10 Hz. A 1 Hz "now" tick keeps the
|
|
7
|
+
* last-activity stamp moving even when the snapshot itself is unchanged.
|
|
8
|
+
* When the snapshot reports `done`, the component stops polling and
|
|
9
|
+
* invokes `onDone` so the caller can `unmount` the ink instance.
|
|
10
|
+
*/
|
|
11
|
+
export declare function App({ getSnapshot, onDone, }: {
|
|
12
|
+
getSnapshot: () => RunSnapshot;
|
|
13
|
+
onDone?: () => void;
|
|
14
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Box, useStdout } from "ink";
|
|
4
|
+
import { Header } from "./Header.js";
|
|
5
|
+
import { IssueBox } from "./IssueBox.js";
|
|
6
|
+
const POLL_MS = 100; // 10 Hz
|
|
7
|
+
/**
|
|
8
|
+
* Root TUI component.
|
|
9
|
+
*
|
|
10
|
+
* Polls `getSnapshot` at 10 Hz. A 1 Hz "now" tick keeps the
|
|
11
|
+
* last-activity stamp moving even when the snapshot itself is unchanged.
|
|
12
|
+
* When the snapshot reports `done`, the component stops polling and
|
|
13
|
+
* invokes `onDone` so the caller can `unmount` the ink instance.
|
|
14
|
+
*/
|
|
15
|
+
export function App({ getSnapshot, onDone, }) {
|
|
16
|
+
const [snapshot, setSnapshot] = useState(() => getSnapshot());
|
|
17
|
+
const [now, setNow] = useState(() => Date.now());
|
|
18
|
+
const doneFired = useRef(false);
|
|
19
|
+
const { stdout } = useStdout();
|
|
20
|
+
// Snapshot poller (drives all state transitions).
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const id = setInterval(() => {
|
|
23
|
+
const next = getSnapshot();
|
|
24
|
+
setSnapshot(next);
|
|
25
|
+
if (next.done && !doneFired.current) {
|
|
26
|
+
doneFired.current = true;
|
|
27
|
+
clearInterval(id);
|
|
28
|
+
onDone?.();
|
|
29
|
+
}
|
|
30
|
+
}, POLL_MS);
|
|
31
|
+
return () => clearInterval(id);
|
|
32
|
+
}, [getSnapshot, onDone]);
|
|
33
|
+
// Coarse 1 Hz tick for the last-activity stamp.
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
36
|
+
return () => clearInterval(id);
|
|
37
|
+
}, []);
|
|
38
|
+
const columns = stdout?.columns ?? 80;
|
|
39
|
+
const boxWidth = Math.min(columns - 2, 100);
|
|
40
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { snapshot: snapshot }), snapshot.issues.map((issue, i) => (_jsx(IssueBox, { state: issue, slot: i, width: boxWidth, now: now }, issue.number)))] }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type JSX } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Per-issue elapsed timer. Owns its own interval so tick-driven re-renders
|
|
4
|
+
* are scoped to this leaf component and do not propagate to `IssueBox`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function ElapsedTimer({ startedAt }: {
|
|
7
|
+
startedAt?: Date;
|
|
8
|
+
}): JSX.Element;
|
|
9
|
+
/** Format an absolute timestamp as the "last activity Xs ago" stamp. */
|
|
10
|
+
export declare function formatSinceActivity(now: number, activityAt: Date): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Text } from "ink";
|
|
4
|
+
/**
|
|
5
|
+
* Per-issue elapsed timer. Owns its own interval so tick-driven re-renders
|
|
6
|
+
* are scoped to this leaf component and do not propagate to `IssueBox`.
|
|
7
|
+
*/
|
|
8
|
+
export function ElapsedTimer({ startedAt }) {
|
|
9
|
+
const [now, setNow] = useState(() => Date.now());
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
12
|
+
return () => clearInterval(id);
|
|
13
|
+
}, []);
|
|
14
|
+
if (!startedAt)
|
|
15
|
+
return _jsx(Text, { children: "--:--" });
|
|
16
|
+
const secs = Math.max(0, Math.floor((now - startedAt.getTime()) / 1000));
|
|
17
|
+
const mm = Math.floor(secs / 60)
|
|
18
|
+
.toString()
|
|
19
|
+
.padStart(2, "0");
|
|
20
|
+
const ss = (secs % 60).toString().padStart(2, "0");
|
|
21
|
+
return _jsx(Text, { children: `${mm}:${ss}` });
|
|
22
|
+
}
|
|
23
|
+
/** Format an absolute timestamp as the "last activity Xs ago" stamp. */
|
|
24
|
+
export function formatSinceActivity(now, activityAt) {
|
|
25
|
+
const secs = Math.max(0, Math.floor((now - activityAt.getTime()) / 1000));
|
|
26
|
+
if (secs < 60)
|
|
27
|
+
return `${secs}s ago`;
|
|
28
|
+
const mm = Math.floor(secs / 60);
|
|
29
|
+
const ss = secs % 60;
|
|
30
|
+
return `${mm}m ${ss}s ago`;
|
|
31
|
+
}
|