nightralph 0.0.52 → 0.0.54
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/README.md +145 -16
- package/dist/display.js +80 -62
- package/dist/display.js.map +2 -2
- package/dist/index.js +2099 -314
- package/dist/index.js.map +4 -4
- package/dist/learnings.js +356 -0
- package/dist/learnings.js.map +7 -0
- package/dist/meta.json +234 -14
- package/dist/orchestrator.js +528 -142
- package/dist/orchestrator.js.map +3 -3
- package/dist/preflight.js +34 -0
- package/dist/preflight.js.map +7 -0
- package/dist/progress.js +5 -4
- package/dist/progress.js.map +2 -2
- package/dist/retry.js +83 -0
- package/dist/retry.js.map +7 -0
- package/dist/review.js +662 -0
- package/dist/review.js.map +7 -0
- package/dist/rules.js +54 -0
- package/dist/rules.js.map +7 -0
- package/dist/src/display.d.ts +35 -33
- package/dist/src/display.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/learnings.d.ts +68 -0
- package/dist/src/learnings.d.ts.map +1 -0
- package/dist/src/orchestrator.d.ts +24 -11
- package/dist/src/orchestrator.d.ts.map +1 -1
- package/dist/src/preflight.d.ts +6 -0
- package/dist/src/preflight.d.ts.map +1 -0
- package/dist/src/progress.d.ts +1 -1
- package/dist/src/progress.d.ts.map +1 -1
- package/dist/src/retry.d.ts +26 -0
- package/dist/src/retry.d.ts.map +1 -0
- package/dist/src/review.d.ts +127 -0
- package/dist/src/review.d.ts.map +1 -0
- package/dist/src/rules.d.ts +10 -0
- package/dist/src/rules.d.ts.map +1 -0
- package/dist/src/testcmd.d.ts +1 -0
- package/dist/src/testcmd.d.ts.map +1 -1
- package/dist/src/worktree.d.ts +30 -1
- package/dist/src/worktree.d.ts.map +1 -1
- package/dist/testcmd.js +13 -0
- package/dist/testcmd.js.map +2 -2
- package/dist/worktree.js +236 -32
- package/dist/worktree.js.map +2 -2
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ so they can't drift as long as you install the skills with this package.
|
|
|
22
22
|
<!-- toc -->
|
|
23
23
|
|
|
24
24
|
- [Install](#install)
|
|
25
|
+
* [Requirements](#requirements)
|
|
25
26
|
* [Install Dependency Skills](#install-dependency-skills)
|
|
26
27
|
- [Workflow](#workflow)
|
|
27
28
|
* [Create Issues](#create-issues)
|
|
@@ -32,11 +33,15 @@ so they can't drift as long as you install the skills with this package.
|
|
|
32
33
|
* [Select a ticket](#select-a-ticket)
|
|
33
34
|
* [Worktrees](#worktrees)
|
|
34
35
|
* [Progress](#progress)
|
|
36
|
+
* [Integration branch](#integration-branch)
|
|
37
|
+
* [Review](#review)
|
|
38
|
+
* [Status display](#status-display)
|
|
35
39
|
- [Keep Your Machine Awake](#keep-your-machine-awake)
|
|
36
40
|
* [Mac OS](#mac-os)
|
|
37
41
|
+ [Claude Opus 4.6](#claude-opus-46)
|
|
38
42
|
+ [Claude Opus 5.5](#claude-opus-55)
|
|
39
|
-
+ [Claude Sonnet 5](#claude-sonnet-
|
|
43
|
+
+ [Claude Sonnet 5.5](#claude-sonnet-55)
|
|
44
|
+
+ [Claude Haiku 5.5](#claude-haiku-55)
|
|
40
45
|
+ [Claude Fable 5.1](#claude-fable-51)
|
|
41
46
|
+ [`pi` + OpenRouter model](#pi--openrouter-model)
|
|
42
47
|
+ [`codex` + Luna](#codex--luna)
|
|
@@ -51,6 +56,19 @@ so they can't drift as long as you install the skills with this package.
|
|
|
51
56
|
npm i -S nightralph
|
|
52
57
|
```
|
|
53
58
|
|
|
59
|
+
### Requirements
|
|
60
|
+
|
|
61
|
+
- Node 24 or newer (see `engines` in `package.json`) and git.
|
|
62
|
+
- The agent CLI you name on the command line (`claude`, `codex` or
|
|
63
|
+
`pi`) installed and on your `PATH`. After it finds tickets,
|
|
64
|
+
nightralph checks for this command before it touches git, and
|
|
65
|
+
exits with code 2 if the command is not found. A run with no
|
|
66
|
+
tickets stops before the check. With `--dry-run` it prints the
|
|
67
|
+
problem and keeps going.
|
|
68
|
+
- macOS or Linux. nightralph runs agents in their own process group
|
|
69
|
+
and uses `sh -c` for test and install commands, so it does not
|
|
70
|
+
run on Windows.
|
|
71
|
+
|
|
54
72
|
### Install Dependency Skills
|
|
55
73
|
|
|
56
74
|
```sh
|
|
@@ -99,8 +117,8 @@ a prose text description.
|
|
|
99
117
|
The `nightralph` CLI does the `tdd` part of the workflow: the agent
|
|
100
118
|
prompt tells the agent to write a failing test per acceptance
|
|
101
119
|
criterion at the seams named in the spec's Testing Decisions, and
|
|
102
|
-
the orchestrator runs the project's test command
|
|
103
|
-
before a ticket can be marked `done`.
|
|
120
|
+
the orchestrator runs the project's test command, when there is
|
|
121
|
+
one, in the worktree before a ticket can be marked `done`.
|
|
104
122
|
|
|
105
123
|
>
|
|
106
124
|
> [!NOTE]
|
|
@@ -156,14 +174,16 @@ Flags:
|
|
|
156
174
|
| Flag | Required | Default | Description |
|
|
157
175
|
|------|----------|---------|-------------|
|
|
158
176
|
| `<provider>` | yes | -- | Agent harness to spawn (e.g. `claude`, `codex`, `pi`) |
|
|
159
|
-
| `[turns]` | no | -- | Max agentic turns per
|
|
177
|
+
| `[turns]` | no | -- | Max agentic turns per agent run (positional shorthand for `--max-turns`) |
|
|
160
178
|
| `-m`, `--model` | no | -- | Model flag passed to the agent |
|
|
161
179
|
| `--spec <name>` | no | auto | Feature name (resolves `.scratch/<name>/`) |
|
|
162
180
|
| `--dry-run` | no | -- | Show wave order and prompt without running |
|
|
181
|
+
| `--no-review` | no | review on | Skip the review and fix pass at the end of a run |
|
|
163
182
|
| `-X` | no | -- | Stop on merge conflict instead of re-spawning |
|
|
164
183
|
| `--timeout` | no | `3600` | Kill the agent after N seconds. A killed agent is not a failure: its work is kept (see Notes) |
|
|
165
|
-
| `--test-cmd <cmd>` | no | auto | Run in each worktree after the agent exits; non-zero exit rejects the ticket.
|
|
166
|
-
| `--
|
|
184
|
+
| `--test-cmd <cmd>` | no | auto | Run in each worktree after the agent exits; non-zero exit rejects the ticket. Defaults to `npm test` when `package.json` has a `test` script. When nothing is detected the run has no test gate and says so at startup. Pass `""` to run without a gate. Shares the agent's `--timeout`. Dependencies are already installed by `--install-cmd` |
|
|
185
|
+
| `--install-cmd <cmd>` | no | auto | Run in each fresh worktree before the agent starts, and again after a merge-conflict rebase; non-zero exit fails the attempt (it is retried like any other failure). Auto-detects from the lockfile: `pnpm-lock.yaml` -> `pnpm install --prefer-offline --frozen-lockfile`, `package-lock.json` -> `npm ci --prefer-offline --no-audit --no-fund`, a `package.json` with no lockfile -> `npm install --prefer-offline --no-audit --no-fund --no-package-lock`, no `package.json` -> no install. Pass `""` to disable. Shares the agent's `--timeout`. Output goes to `<ticket>.install.log` |
|
|
186
|
+
| `--max-turns <n>` | no | -- | Max agentic turns per agent run. Forwarded as `--max-turns <n>` to `claude` only; for `codex` and `pi` the flag is skipped with a warning and the agent runs with no turn limit. The limit is per spawn, not a total for the ticket: every retry and every re-spawn after a merge conflict starts with a fresh budget of `n` turns, so with `--retries 3` one ticket can use up to 4 x `n`. When omitted (or `0`), no limit is passed, so `--timeout` is the only cap. Takes precedence over the `[turns]` positional when both are given. Reaching the limit is an error exit, so the attempt fails and is retried like any other failure |
|
|
167
187
|
| `--effort <level>` | no | -- | Starting effort/reasoning level. Each provider has its own ladder: `claude` supports `low`, `medium`, `high`, `xhigh`, `max` (forwarded as `--effort`); `codex` supports `low`, `medium`, `high` (forwarded as `-c model_reasoning_effort=<level>`); `pi` supports `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` (forwarded as `--thinking`). Omitted = provider default. Each retry steps up from here (see `--retries`) |
|
|
168
188
|
| `--retries <n>` | no | `3` | Extra attempts per ticket after a failed run. Each retry starts from a fresh worktree and raises the effort level one rung (see Notes). `0` disables retries |
|
|
169
189
|
| `--retry-delay <n>` | no | `30` | Seconds to wait before the first retry; doubles each further retry (30s, 60s, 120s, ...). `0` disables the wait |
|
|
@@ -182,14 +202,16 @@ Flags:
|
|
|
182
202
|
tickets start in the next wave.
|
|
183
203
|
* An agent run only counts as a success when the agent exits 0,
|
|
184
204
|
its worktree branch has new commits relative to the base branch,
|
|
185
|
-
and the test command (see `--test-cmd`)
|
|
205
|
+
and the test command (see `--test-cmd`), when there is one, exits
|
|
206
|
+
0 in that worktree.
|
|
186
207
|
Uncommitted changes left by the agent are auto-committed before
|
|
187
208
|
the check. On success, the ticket's status is updated to `done`
|
|
188
209
|
in-place. If the agent exits 0 but made no commits, or the test
|
|
189
210
|
command fails, the ticket is left `ready-for-agent` for a retry
|
|
190
211
|
and the test output is written to `<dir>/../logs/<ticket>.test.log`.
|
|
191
212
|
Failed tickets (non-zero exit) keep their status and block
|
|
192
|
-
dependents.
|
|
213
|
+
dependents. The status display marks those dependents as blocked
|
|
214
|
+
and names the failed ticket.
|
|
193
215
|
* A failed attempt is retried, up to `--retries` times, before the
|
|
194
216
|
ticket is recorded as failed. Non-zero exit, exit 0 with no new
|
|
195
217
|
commits, and a red test command all count as failures; a timeout
|
|
@@ -206,6 +228,20 @@ Flags:
|
|
|
206
228
|
logs are written to `<dir>/../logs/<ticket>.attempt<n>.log`
|
|
207
229
|
(and `.attempt<n>.test.log` for the test command) so the first
|
|
208
230
|
attempt's log is kept.
|
|
231
|
+
* Each attempt at a ticket appends an entry to
|
|
232
|
+
`.scratch/<feature>/learnings.md`, next to `progress.md`. The
|
|
233
|
+
entry records what the orchestrator saw (the outcome, the agent's
|
|
234
|
+
exit code, the checklist items it reported, and the last lines of
|
|
235
|
+
the test or install log when one of those failed) and, when an
|
|
236
|
+
agent ran, the agent's own notes. The file is committed along with
|
|
237
|
+
`progress.md` and persists across runs. Each agent's prompt
|
|
238
|
+
includes the most
|
|
239
|
+
recent entries for its own ticket and the last successful entry of
|
|
240
|
+
each ticket that directly blocks it, so a retry can see why the
|
|
241
|
+
previous attempt failed. Agents keep their notes in
|
|
242
|
+
`.nightralph/notes.md` inside the worktree. nightralph collects
|
|
243
|
+
that file when the agent exits, and a self-ignoring
|
|
244
|
+
`.nightralph/.gitignore` keeps it out of the agent's commits.
|
|
209
245
|
* A ticket that exhausts all its retry attempts is excluded from
|
|
210
246
|
later waves in the same run. It stays `ready-for-agent` on disk
|
|
211
247
|
so a future run can pick it up, but it will not be re-started
|
|
@@ -255,13 +291,18 @@ is `.scratch/<name>/issues/`.
|
|
|
255
291
|
|
|
256
292
|
## How It Works
|
|
257
293
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
ticket slug.
|
|
294
|
+
Tickets run in waves. A wave is a batch of tickets whose blockers are
|
|
295
|
+
all done. The tickets in a wave run in parallel, up to `--concurrency`
|
|
296
|
+
at once, and each ticket gets a worktree named after the ticket slug.
|
|
297
|
+
nightralph waits for the whole batch to finish, merges the branches of
|
|
298
|
+
the tickets that succeeded, and marks them `done`. Then it looks for
|
|
299
|
+
tickets that are newly unblocked. Those form the next wave. This
|
|
300
|
+
continues until no ticket is ready to run.
|
|
261
301
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
302
|
+
The wave total in the status display header (the `3` in `Wave 2/3`) is
|
|
303
|
+
a prediction made at startup. It assumes every ticket succeeds. When a
|
|
304
|
+
ticket fails, its dependents never become ready, so the run can end
|
|
305
|
+
before it reaches the predicted last wave.
|
|
265
306
|
|
|
266
307
|
|
|
267
308
|
### Select a ticket
|
|
@@ -279,6 +320,20 @@ completes, successful branches are merged back into the base branch.
|
|
|
279
320
|
The first branch merges directly, and subsequent branches rebase
|
|
280
321
|
onto the updated base before merging.
|
|
281
322
|
|
|
323
|
+
Worktrees are created next to the repository, not inside it: a
|
|
324
|
+
ticket's worktree is the sibling directory
|
|
325
|
+
`<repo>.<feature>-<num>-<slug>` on the branch
|
|
326
|
+
`<repo>--<feature>-<num>-<slug>`. A run that is interrupted leaves
|
|
327
|
+
them in place; the next run removes a stale worktree and branch of
|
|
328
|
+
the same name before creating a new one.
|
|
329
|
+
|
|
330
|
+
A fresh worktree has only tracked files, so there is no
|
|
331
|
+
`node_modules`. Before the agent starts, the orchestrator installs
|
|
332
|
+
dependencies with the command that matches the project's lockfile
|
|
333
|
+
(see `--install-cmd`), and the prompt tells the agent not to run
|
|
334
|
+
another installer. The install never writes a lockfile, so ticket
|
|
335
|
+
branches do not commit one by accident.
|
|
336
|
+
|
|
282
337
|
If a merge conflict occurs, the behavior depends on the `-X` flag: by
|
|
283
338
|
default the agent is re-spawned to resolve it; with `-X`,
|
|
284
339
|
the run stops immediately.
|
|
@@ -289,6 +344,74 @@ A file `progress.md` is written to the `.scratch/<feature>/` directory
|
|
|
289
344
|
and committed after each ticket completes, so you can monitor the run from
|
|
290
345
|
another terminal by reading the file.
|
|
291
346
|
|
|
347
|
+
### Integration branch
|
|
348
|
+
|
|
349
|
+
Launched from your default branch (`main`), nightralph does not merge
|
|
350
|
+
into it. It creates `nightralph--<feature>` (or `nightralph--all` for
|
|
351
|
+
several features), merges every ticket there, and leaves you on that
|
|
352
|
+
branch. Running again from `main` resumes the same branch, so finished
|
|
353
|
+
tickets stay finished. Launched from any other branch, it merges into
|
|
354
|
+
that branch.
|
|
355
|
+
|
|
356
|
+
After you merge the integration branch into `main`, the next run
|
|
357
|
+
fast-forwards it to `main` and resumes. If the branch has commits `main`
|
|
358
|
+
lacks, nightralph reports how far behind it is; merge `main` into it or
|
|
359
|
+
delete it with `git branch -D`.
|
|
360
|
+
|
|
361
|
+
If `.scratch/` is gitignored, the bookkeeping files (tickets, progress,
|
|
362
|
+
learnings, review) are force-added and live on the integration branch, so
|
|
363
|
+
they disappear from disk when you check out `main`.
|
|
364
|
+
|
|
365
|
+
### Review
|
|
366
|
+
|
|
367
|
+
After the last wave, an agent reviews everything merged since the last
|
|
368
|
+
review and writes its findings to `.scratch/<feature>/review.md` (`.scratch/review.md`
|
|
369
|
+
for a multi-feature run). A
|
|
370
|
+
second agent fixes them test-first; its work merges only if the test
|
|
371
|
+
command passes. Findings it could not fix stay unchecked in
|
|
372
|
+
`review.md`. Skip this with `--no-review`.
|
|
373
|
+
|
|
374
|
+
### Status display
|
|
375
|
+
|
|
376
|
+
In a terminal, nightralph shows a header line, one row per ticket, and
|
|
377
|
+
a log of recent messages. The ticket list holds every ticket that was
|
|
378
|
+
`ready-for-agent` when the run started, and it stays the same for the
|
|
379
|
+
whole run. Rows do not move, disappear, or reappear between waves.
|
|
380
|
+
|
|
381
|
+
Each row starts with a dot that shows the ticket's state:
|
|
382
|
+
|
|
383
|
+
* Dim empty dot: pending. The ticket has not started yet.
|
|
384
|
+
* Yellow dot: running. The agent is working, the test command is
|
|
385
|
+
running, or the ticket is waiting out a retry delay.
|
|
386
|
+
* Cyan dot: waiting to merge. The agent and the test command
|
|
387
|
+
succeeded, and the branch waits for the rest of the wave to finish.
|
|
388
|
+
The dot stays cyan while the branch is merged, including while a
|
|
389
|
+
re-spawned agent resolves a merge conflict.
|
|
390
|
+
* Green dot: done.
|
|
391
|
+
* Red dot: failed.
|
|
392
|
+
* Dim filled dot: blocked. A ticket upstream failed or is not agent
|
|
393
|
+
work (for example, it is `ready-for-human`), so this ticket cannot
|
|
394
|
+
run in this session. The row names the blocking tickets, as in
|
|
395
|
+
`auth/03 (blocked by auth/01)`.
|
|
396
|
+
|
|
397
|
+
The header looks like this:
|
|
398
|
+
|
|
399
|
+
```
|
|
400
|
+
Wave 2/3 - 5/8 done - 63%
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
The counts cover only this run's tickets, the ones in the list.
|
|
404
|
+
Tickets finished by an earlier run are not counted.
|
|
405
|
+
|
|
406
|
+
When stdout is not a terminal, for example when it is piped to a file,
|
|
407
|
+
nightralph prints one line per wave that names the wave's tickets,
|
|
408
|
+
followed by plain log lines.
|
|
409
|
+
|
|
410
|
+
Known limit: with `-X`, the run stops at the first merge conflict. A
|
|
411
|
+
ticket whose branch succeeded but came after the conflicting branch is
|
|
412
|
+
never merged, and its row stays on the cyan "waiting to merge" dot when
|
|
413
|
+
the run stops.
|
|
414
|
+
|
|
292
415
|
|
|
293
416
|
---
|
|
294
417
|
|
|
@@ -312,10 +435,16 @@ caffeinate -i npx nightralph claude -m claude-opus-4-6 --effort low
|
|
|
312
435
|
```sh
|
|
313
436
|
caffeinate -i npx nightralph claude -m claude-opus-5-5 --effort low
|
|
314
437
|
```
|
|
315
|
-
#### Claude Sonnet 5
|
|
438
|
+
#### Claude Sonnet 5.5
|
|
439
|
+
|
|
440
|
+
```sh
|
|
441
|
+
caffeinate -i npx nightralph claude -m claude-sonnet-5-5 --effort high
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
#### Claude Haiku 5.5
|
|
316
445
|
|
|
317
446
|
```sh
|
|
318
|
-
caffeinate -i npx nightralph claude -m claude-
|
|
447
|
+
caffeinate -i npx nightralph claude -m claude-haiku-5-5 --effort high
|
|
319
448
|
```
|
|
320
449
|
|
|
321
450
|
#### Claude Fable 5.1
|
package/dist/display.js
CHANGED
|
@@ -58,6 +58,14 @@ const DEFAULT_COLUMNS = 80;
|
|
|
58
58
|
const MIN_LOG_ROWS = 1;
|
|
59
59
|
const MAX_MESSAGE_BUFFER = 200;
|
|
60
60
|
const ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
61
|
+
function rowText(ticket) {
|
|
62
|
+
const label = ticketLabel(ticket);
|
|
63
|
+
if (ticket.status !== "blocked" || !ticket.blockedBy?.length) {
|
|
64
|
+
return label;
|
|
65
|
+
}
|
|
66
|
+
return `${label} (blocked by ${ticket.blockedBy.join(", ")})`;
|
|
67
|
+
}
|
|
68
|
+
__name(rowText, "rowText");
|
|
61
69
|
function fitLine(line, width) {
|
|
62
70
|
const visible = line.replace(ANSI_RE, "");
|
|
63
71
|
if (visible.length <= width) return line;
|
|
@@ -81,13 +89,16 @@ function renderDot(status) {
|
|
|
81
89
|
case "in-progress":
|
|
82
90
|
return color.yellow(FILLED_DOT);
|
|
83
91
|
// The agent and its test gate are finished; the branch
|
|
84
|
-
// waits for the rest of the wave before it is merged.
|
|
92
|
+
// waits for the rest of the wave before it is merged. It
|
|
93
|
+
// stays cyan through the merge and any conflict respawn.
|
|
85
94
|
case "awaiting-merge":
|
|
86
95
|
return color.cyan(FILLED_DOT);
|
|
87
96
|
case "done":
|
|
88
97
|
return color.green(FILLED_DOT);
|
|
89
98
|
case "failed":
|
|
90
99
|
return color.red(FILLED_DOT);
|
|
100
|
+
case "blocked":
|
|
101
|
+
return color.dim(FILLED_DOT);
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
104
|
__name(renderDot, "renderDot");
|
|
@@ -96,8 +107,8 @@ class StatusDisplay {
|
|
|
96
107
|
__name(this, "StatusDisplay");
|
|
97
108
|
}
|
|
98
109
|
wave = null;
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
tickets = [];
|
|
111
|
+
hasRun = false;
|
|
101
112
|
messages = [];
|
|
102
113
|
renderedLines = 0;
|
|
103
114
|
config;
|
|
@@ -114,20 +125,28 @@ class StatusDisplay {
|
|
|
114
125
|
const available = termHeight - overhead;
|
|
115
126
|
return Math.max(1, available);
|
|
116
127
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
};
|
|
126
|
-
this.remaining = (remaining ?? []).map((t) => ({
|
|
128
|
+
findTicket(num, feature) {
|
|
129
|
+
const found = feature ? this.tickets.find(
|
|
130
|
+
(t) => t.num === num && t.feature === feature
|
|
131
|
+
) : this.tickets.find((t) => t.num === num);
|
|
132
|
+
return found ?? null;
|
|
133
|
+
}
|
|
134
|
+
startRun(tickets) {
|
|
135
|
+
this.tickets = tickets.map((t) => ({
|
|
127
136
|
...t,
|
|
128
137
|
status: "pending"
|
|
129
138
|
}));
|
|
130
|
-
this.
|
|
139
|
+
this.hasRun = true;
|
|
140
|
+
this.viewportStart = 0;
|
|
141
|
+
}
|
|
142
|
+
startWave(waveNum, totalWaves, tickets, _progress) {
|
|
143
|
+
if (!this.hasRun) {
|
|
144
|
+
this.startRun(tickets);
|
|
145
|
+
}
|
|
146
|
+
this.wave = {
|
|
147
|
+
waveNum,
|
|
148
|
+
totalWaves
|
|
149
|
+
};
|
|
131
150
|
this.messages = [];
|
|
132
151
|
this.viewportStart = 0;
|
|
133
152
|
this.config.stream.write(ansi.cursorHide());
|
|
@@ -147,28 +166,31 @@ class StatusDisplay {
|
|
|
147
166
|
this.render();
|
|
148
167
|
}
|
|
149
168
|
setTicketStatus(num, status, feature) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
this.viewportStart = ticketIndex;
|
|
162
|
-
} else if (ticketIndex >= this.viewportStart + availableRows) {
|
|
163
|
-
this.viewportStart = ticketIndex - availableRows + 3;
|
|
164
|
-
}
|
|
165
|
-
this.viewportStart = Math.max(
|
|
166
|
-
0,
|
|
167
|
-
Math.min(this.viewportStart, totalTickets - 1)
|
|
168
|
-
);
|
|
169
|
+
const ticket = this.findTicket(num, feature);
|
|
170
|
+
if (!ticket) return;
|
|
171
|
+
ticket.status = status;
|
|
172
|
+
if (status === "in-progress") {
|
|
173
|
+
const ticketIndex = this.tickets.indexOf(ticket);
|
|
174
|
+
const availableRows = this.getAvailableRows();
|
|
175
|
+
const totalTickets = this.tickets.length;
|
|
176
|
+
if (ticketIndex < this.viewportStart) {
|
|
177
|
+
this.viewportStart = ticketIndex;
|
|
178
|
+
} else if (ticketIndex >= this.viewportStart + availableRows) {
|
|
179
|
+
this.viewportStart = ticketIndex - availableRows + 3;
|
|
169
180
|
}
|
|
170
|
-
this.
|
|
181
|
+
this.viewportStart = Math.max(
|
|
182
|
+
0,
|
|
183
|
+
Math.min(this.viewportStart, totalTickets - 1)
|
|
184
|
+
);
|
|
171
185
|
}
|
|
186
|
+
this.render();
|
|
187
|
+
}
|
|
188
|
+
setBlocked(num, feature, blockedBy) {
|
|
189
|
+
const ticket = this.findTicket(num, feature);
|
|
190
|
+
if (!ticket) return;
|
|
191
|
+
ticket.status = "blocked";
|
|
192
|
+
ticket.blockedBy = [...blockedBy];
|
|
193
|
+
this.render();
|
|
172
194
|
}
|
|
173
195
|
log(message) {
|
|
174
196
|
if (!this.wave) {
|
|
@@ -195,59 +217,52 @@ class StatusDisplay {
|
|
|
195
217
|
if (this.renderedLines > 0) {
|
|
196
218
|
output += ansi.eraseLines(this.renderedLines + 1);
|
|
197
219
|
}
|
|
198
|
-
const
|
|
220
|
+
const done = this.tickets.filter(
|
|
199
221
|
(t) => t.status === "done"
|
|
200
222
|
).length;
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const pct = total === 0 ? 0 : Math.round(completed / total * 100);
|
|
223
|
+
const total = this.tickets.length;
|
|
224
|
+
const pct = total === 0 ? 0 : Math.round(done / total * 100);
|
|
204
225
|
const width = Math.max(
|
|
205
226
|
1,
|
|
206
227
|
(this.config.stream.columns ?? DEFAULT_COLUMNS) - 1
|
|
207
228
|
);
|
|
208
|
-
|
|
209
|
-
|
|
229
|
+
const header = `Wave ${this.wave.waveNum}/${this.wave.totalWaves} - ${done}/${total} done - ${pct}%`;
|
|
230
|
+
output += fitLine(header, width) + "\n";
|
|
210
231
|
const availableRows = this.getAvailableRows();
|
|
211
|
-
const tickets = [
|
|
212
|
-
...this.wave.tickets,
|
|
213
|
-
...this.remaining
|
|
214
|
-
];
|
|
215
232
|
let renderedTicketCount = 0;
|
|
216
|
-
if (tickets.length <= availableRows) {
|
|
217
|
-
for (const ticket of tickets) {
|
|
233
|
+
if (this.tickets.length <= availableRows) {
|
|
234
|
+
for (const ticket of this.tickets) {
|
|
218
235
|
const dot = renderDot(ticket.status);
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
width - 2
|
|
222
|
-
);
|
|
236
|
+
const text = rowText(ticket);
|
|
237
|
+
const name = fitLine(text, width - 2);
|
|
223
238
|
output += `${dot} ${name}
|
|
224
239
|
`;
|
|
225
240
|
}
|
|
226
|
-
renderedTicketCount = tickets.length;
|
|
241
|
+
renderedTicketCount = this.tickets.length;
|
|
227
242
|
} else {
|
|
228
243
|
this.viewportStart = Math.max(
|
|
229
244
|
0,
|
|
230
245
|
Math.min(
|
|
231
246
|
this.viewportStart,
|
|
232
|
-
tickets.length - 1
|
|
247
|
+
this.tickets.length - 1
|
|
233
248
|
)
|
|
234
249
|
);
|
|
235
250
|
let hasMoreAbove = this.viewportStart > 0;
|
|
236
251
|
let tentativeCount = availableRows - (hasMoreAbove ? 1 : 0);
|
|
237
|
-
let hasMoreBelow = this.viewportStart + tentativeCount < tickets.length;
|
|
252
|
+
let hasMoreBelow = this.viewportStart + tentativeCount < this.tickets.length;
|
|
238
253
|
let ticketsToShow = Math.max(
|
|
239
254
|
0,
|
|
240
255
|
tentativeCount - (hasMoreBelow ? 1 : 0)
|
|
241
256
|
);
|
|
242
257
|
const maxStart = Math.max(
|
|
243
258
|
0,
|
|
244
|
-
tickets.length - Math.max(1, ticketsToShow)
|
|
259
|
+
this.tickets.length - Math.max(1, ticketsToShow)
|
|
245
260
|
);
|
|
246
261
|
if (this.viewportStart > maxStart) {
|
|
247
262
|
this.viewportStart = maxStart;
|
|
248
263
|
hasMoreAbove = this.viewportStart > 0;
|
|
249
264
|
tentativeCount = availableRows - (hasMoreAbove ? 1 : 0);
|
|
250
|
-
hasMoreBelow = this.viewportStart + tentativeCount < tickets.length;
|
|
265
|
+
hasMoreBelow = this.viewportStart + tentativeCount < this.tickets.length;
|
|
251
266
|
ticketsToShow = Math.max(
|
|
252
267
|
0,
|
|
253
268
|
tentativeCount - (hasMoreBelow ? 1 : 0)
|
|
@@ -258,21 +273,19 @@ class StatusDisplay {
|
|
|
258
273
|
output += ` ... ${moreCount} more above
|
|
259
274
|
`;
|
|
260
275
|
}
|
|
261
|
-
const visibleTickets = tickets.slice(
|
|
276
|
+
const visibleTickets = this.tickets.slice(
|
|
262
277
|
this.viewportStart,
|
|
263
278
|
this.viewportStart + ticketsToShow
|
|
264
279
|
);
|
|
265
280
|
for (const ticket of visibleTickets) {
|
|
266
281
|
const dot = renderDot(ticket.status);
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
width - 2
|
|
270
|
-
);
|
|
282
|
+
const text = rowText(ticket);
|
|
283
|
+
const name = fitLine(text, width - 2);
|
|
271
284
|
output += `${dot} ${name}
|
|
272
285
|
`;
|
|
273
286
|
}
|
|
274
287
|
if (hasMoreBelow) {
|
|
275
|
-
const moreCount = tickets.length - (this.viewportStart + ticketsToShow);
|
|
288
|
+
const moreCount = this.tickets.length - (this.viewportStart + ticketsToShow);
|
|
276
289
|
output += ` ... ${moreCount} more below
|
|
277
290
|
`;
|
|
278
291
|
}
|
|
@@ -304,7 +317,9 @@ class PlainDisplay {
|
|
|
304
317
|
static {
|
|
305
318
|
__name(this, "PlainDisplay");
|
|
306
319
|
}
|
|
307
|
-
|
|
320
|
+
startRun(_tickets) {
|
|
321
|
+
}
|
|
322
|
+
startWave(waveNum, _totalWaves, tickets, progress) {
|
|
308
323
|
const pct = progress && progress.total > 0 ? ` (${progress.completed}/${progress.total} done)` : "";
|
|
309
324
|
console.log(
|
|
310
325
|
`
|
|
@@ -313,6 +328,8 @@ Wave ${waveNum}${pct}: ` + tickets.map((t) => ticketLabel(t)).join(", ")
|
|
|
313
328
|
}
|
|
314
329
|
setTicketStatus(_num, _status, _feature) {
|
|
315
330
|
}
|
|
331
|
+
setBlocked(_num, _feature, _blockedBy) {
|
|
332
|
+
}
|
|
316
333
|
log(message) {
|
|
317
334
|
console.log(message);
|
|
318
335
|
}
|
|
@@ -335,6 +352,7 @@ export {
|
|
|
335
352
|
createDisplay,
|
|
336
353
|
fitLine,
|
|
337
354
|
renderDot,
|
|
355
|
+
ticketLabel,
|
|
338
356
|
wrapLine
|
|
339
357
|
};
|
|
340
358
|
//# sourceMappingURL=display.js.map
|
package/dist/display.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/display.ts"],
|
|
4
|
-
"sourcesContent": ["export type TicketStatus =\n | 'pending'\n | 'in-progress'\n | 'awaiting-merge'\n | 'done'\n | 'failed'\n\nexport type DisplayTicket = {\n readonly num:number\n readonly filename:string\n readonly feature?:string\n status:TicketStatus\n}\n\nexport type WaveState = {\n readonly waveNum:number\n readonly totalWaves:number\n readonly tickets:Array<DisplayTicket>\n}\n\nexport type DisplayConfig = {\n readonly stream:NodeJS.WriteStream\n}\n\n/**\n * Progress across the whole run, not just the current wave.\n * `completed` counts tickets done before the wave started;\n * tickets that finish during the wave are added on top.\n */\nexport type RunProgress = {\n readonly completed:number\n readonly total:number\n}\n\nexport type Display = {\n startWave(\n waveNum:number,\n totalWaves:number,\n tickets:ReadonlyArray<{\n num:number\n filename:string\n feature?:string\n }>,\n progress?:RunProgress,\n remaining?:ReadonlyArray<{\n num:number\n filename:string\n feature?:string\n }>,\n ):void\n setTicketStatus(\n num:number,\n status:TicketStatus,\n feature?:string,\n ):void\n log(message:string):void\n cleanup():void\n}\n\nconst ESC = '\\x1b['\n\nexport const ansi = {\n cursorUp (n:number):string {\n return `${ESC}${n}A`\n },\n cursorTo (col:number):string {\n return `${ESC}${col}G`\n },\n eraseLine ():string {\n return `${ESC}2K`\n },\n eraseLines (n:number):string {\n let out = ''\n for (let i = 0; i < n; i++) {\n out += ansi.eraseLine()\n if (i < n - 1) out += ansi.cursorUp(1)\n }\n return out\n },\n cursorHide ():string {\n return `${ESC}?25l`\n },\n cursorShow ():string {\n return `${ESC}?25h`\n },\n} as const\n\nexport const color = {\n green (text:string):string {\n return `${ESC}32m${text}${ESC}0m`\n },\n yellow (text:string):string {\n return `${ESC}33m${text}${ESC}0m`\n },\n red (text:string):string {\n return `${ESC}31m${text}${ESC}0m`\n },\n cyan (text:string):string {\n return `${ESC}36m${text}${ESC}0m`\n },\n dim (text:string):string {\n return `${ESC}2m${text}${ESC}0m`\n },\n} as const\n\nfunction ticketLabel (\n ticket:{ num:number; filename:string; feature?:string },\n):string {\n if (ticket.feature) {\n const padded = String(ticket.num).padStart(2, '0')\n return `${ticket.feature}/${padded}`\n }\n return ticket.filename\n}\n\nconst FILLED_DOT = '\u25CF'\nconst EMPTY_DOT = '\u25CB'\nconst SEPARATOR_WIDTH = 50\nconst DEFAULT_COLUMNS = 80\nconst MIN_LOG_ROWS = 1\nconst MAX_MESSAGE_BUFFER = 200\n\n// eslint-disable-next-line no-control-regex\nconst ANSI_RE = /\\x1b\\[[0-9;?]*[A-Za-z]/g\n\n/**\n * Clamp a line to `width` visible columns so it can never wrap.\n * A wrapped line occupies more terminal rows than `render`\n * counts, which leaves stale rows behind on the next erase.\n */\nexport function fitLine (line:string, width:number):string {\n const visible = line.replace(ANSI_RE, '')\n if (visible.length <= width) return line\n return visible.slice(0, width)\n}\n\n/**\n * Split a line into rows of at most `width` visible columns,\n * the way a terminal would wrap it. Short lines keep their\n * ANSI styling; long lines are wrapped on visible text only,\n * so `render` can count every row it emits.\n */\nexport function wrapLine (line:string, width:number):Array<string> {\n const visible = line.replace(ANSI_RE, '')\n if (visible.length <= width) return [line]\n const rows:Array<string> = []\n for (let i = 0; i < visible.length; i += width) {\n rows.push(visible.slice(i, i + width))\n }\n return rows\n}\n\nexport function renderDot (status:TicketStatus):string {\n switch (status) {\n case 'pending':\n return color.dim(EMPTY_DOT)\n case 'in-progress':\n return color.yellow(FILLED_DOT)\n // The agent and its test gate are finished; the branch\n // waits for the rest of the wave before it is merged.\n case 'awaiting-merge':\n return color.cyan(FILLED_DOT)\n case 'done':\n return color.green(FILLED_DOT)\n case 'failed':\n return color.red(FILLED_DOT)\n }\n}\n\nexport class StatusDisplay implements Display {\n private wave:WaveState|null = null\n private progress:RunProgress|null = null\n private remaining:Array<DisplayTicket> = []\n private messages:Array<string> = []\n private renderedLines:number = 0\n private config:DisplayConfig\n private viewportStart:number = 0\n private resizeListener:(() => void)|null = null\n\n constructor (config?:Readonly<Partial<DisplayConfig>>) {\n this.config = {\n stream:config?.stream ?? process.stdout,\n }\n }\n\n private getAvailableRows ():number {\n const termHeight = this.config.stream.rows ?? 24\n const overhead = 2 + MIN_LOG_ROWS\n const available = termHeight - overhead\n return Math.max(1, available)\n }\n\n startWave (\n waveNum:number,\n totalWaves:number,\n tickets:Array<Readonly<{\n num:number\n filename:string\n feature?:string\n }>>,\n progress?:RunProgress,\n remaining?:ReadonlyArray<{\n num:number\n filename:string\n feature?:string\n }>,\n ):void {\n this.wave = {\n waveNum,\n totalWaves,\n tickets:tickets.map(t => ({\n ...t,\n status:'pending' as const,\n })),\n }\n this.remaining = (remaining ?? []).map(t => ({\n ...t,\n status:'pending' as const,\n }))\n this.progress = progress ?? null\n this.messages = []\n this.viewportStart = 0\n this.config.stream.write(ansi.cursorHide())\n\n if (this.config.stream.isTTY) {\n if (this.resizeListener) {\n this.config.stream.removeListener(\n 'resize', this.resizeListener,\n )\n }\n this.resizeListener = () => this.render()\n this.config.stream.on(\n 'resize', this.resizeListener,\n )\n }\n\n this.render()\n }\n\n setTicketStatus (\n num:number,\n status:TicketStatus,\n feature?:string,\n ):void {\n if (!this.wave) return\n\n const ticket = feature ?\n this.wave.tickets.find(\n t => t.num === num && t.feature === feature,\n ) :\n this.wave.tickets.find(t => t.num === num)\n if (ticket) {\n ticket.status = status\n\n if (status === 'in-progress') {\n const ticketIndex = this.wave.tickets.indexOf(ticket)\n const availableRows = this.getAvailableRows()\n const totalTickets = this.wave.tickets.length\n\n if (ticketIndex < this.viewportStart) {\n this.viewportStart = ticketIndex\n } else if (ticketIndex >= this.viewportStart + availableRows) {\n // offset by 3 to keep context visible below\n // the active ticket\n this.viewportStart = ticketIndex - availableRows + 3\n }\n this.viewportStart = Math.max(\n 0,\n Math.min(this.viewportStart, totalTickets - 1),\n )\n }\n\n this.render()\n }\n }\n\n log (message:string):void {\n // Before the first wave there is no grid to repaint,\n // so a message would otherwise be dropped. Write it\n // straight to the stream instead. A later render\n // never erases these rows, since renderedLines is 0.\n if (!this.wave) {\n this.config.stream.write(message + '\\n')\n return\n }\n for (const line of message.split('\\n')) {\n this.messages.push(line)\n }\n while (this.messages.length > MAX_MESSAGE_BUFFER) {\n this.messages.shift()\n }\n this.render()\n }\n\n cleanup ():void {\n // Remove resize listener if it exists\n if (this.resizeListener && this.config.stream.isTTY) {\n this.config.stream.removeListener('resize', this.resizeListener)\n }\n this.config.stream.write(ansi.cursorShow())\n }\n\n private render ():void {\n if (!this.wave) return\n\n let output = ''\n\n if (this.renderedLines > 0) {\n output += ansi.eraseLines(this.renderedLines + 1)\n }\n\n const doneInWave = this.wave.tickets.filter(\n t => t.status === 'done',\n ).length\n const completed = doneInWave + (this.progress?.completed ?? 0)\n const total = this.progress?.total ?? this.wave.tickets.length\n const pct = total === 0 ? 0 : Math.round((completed / total) * 100)\n const width = Math.max(\n 1,\n (this.config.stream.columns ?? DEFAULT_COLUMNS) - 1,\n )\n\n output += `Wave ${this.wave.waveNum}/` +\n `${this.wave.totalWaves} - ` +\n `${pct}% complete\\n`\n\n // Pagination logic\n const availableRows = this.getAvailableRows()\n const tickets:ReadonlyArray<DisplayTicket> = [\n ...this.wave.tickets,\n ...this.remaining,\n ]\n let renderedTicketCount = 0\n\n if (tickets.length <= availableRows) {\n // All tickets fit, no pagination\n for (const ticket of tickets) {\n const dot = renderDot(ticket.status)\n const name = fitLine(\n ticketLabel(ticket), width - 2,\n )\n output += `${dot} ${name}\\n`\n }\n renderedTicketCount = tickets.length\n } else {\n // Need pagination -- iterative clamp\n this.viewportStart = Math.max(\n 0,\n Math.min(\n this.viewportStart,\n tickets.length - 1,\n ),\n )\n\n let hasMoreAbove = this.viewportStart > 0\n let tentativeCount =\n availableRows - (hasMoreAbove ? 1 : 0)\n let hasMoreBelow =\n this.viewportStart + tentativeCount <\n tickets.length\n let ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0),\n )\n\n const maxStart = Math.max(\n 0,\n tickets.length - Math.max(1, ticketsToShow),\n )\n if (this.viewportStart > maxStart) {\n this.viewportStart = maxStart\n hasMoreAbove = this.viewportStart > 0\n tentativeCount = availableRows - (hasMoreAbove ? 1 : 0)\n hasMoreBelow = (\n this.viewportStart +\n tentativeCount < tickets.length\n )\n ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0)\n )\n }\n\n // Show \"more above\" indicator if needed\n if (hasMoreAbove) {\n const moreCount = this.viewportStart\n output += ` ... ${moreCount} more above\\n`\n }\n\n // Get visible tickets and render them\n const visibleTickets = tickets.slice(\n this.viewportStart,\n this.viewportStart + ticketsToShow,\n )\n\n for (const ticket of visibleTickets) {\n const dot = renderDot(ticket.status)\n const name = fitLine(\n ticketLabel(ticket), width - 2,\n )\n output += `${dot} ${name}\\n`\n }\n\n // Show \"more below\" indicator if needed\n if (hasMoreBelow) {\n const moreCount = (\n tickets.length -\n (this.viewportStart + ticketsToShow)\n )\n output += ` ... ${moreCount} more below\\n`\n }\n\n renderedTicketCount = (hasMoreAbove ? 1 : 0) +\n visibleTickets.length +\n (hasMoreBelow ? 1 : 0)\n }\n\n output += '-'.repeat(Math.min(SEPARATOR_WIDTH, width)) + '\\n'\n\n const termHeight = this.config.stream.rows ?? 24\n const logRows = Math.max(\n 0,\n termHeight - 3 - renderedTicketCount,\n )\n const wrapped = this.messages.flatMap(\n msg => wrapLine(msg, width),\n )\n const visibleMessages = wrapped.slice(-logRows)\n for (const row of visibleMessages) {\n output += `${row}\\n`\n }\n const emptyRows = logRows - visibleMessages.length\n for (let i = 0; i < emptyRows; i++) {\n output += '\\n'\n }\n\n this.renderedLines = 2 + renderedTicketCount + logRows\n this.config.stream.write(output)\n }\n}\n\nexport class PlainDisplay implements Display {\n startWave (\n waveNum:number,\n _totalWaves:number,\n tickets:ReadonlyArray<{\n num:number\n filename:string\n feature?:string\n }>,\n progress?:RunProgress,\n _remaining?:ReadonlyArray<{\n num:number\n filename:string\n feature?:string\n }>,\n ):void {\n const pct = progress && progress.total > 0 ?\n ` (${progress.completed}/${progress.total} done)` :\n ''\n console.log(\n `\\nWave ${waveNum}${pct}: ` +\n tickets.map(t => ticketLabel(t)).join(', ')\n )\n }\n\n setTicketStatus (\n _num:number,\n _status:TicketStatus,\n _feature?:string,\n ):void {\n // no-op: plain mode reports via log()\n }\n\n log (message:string):void {\n console.log(message)\n }\n\n cleanup ():void {\n // no-op\n }\n}\n\nexport function createDisplay (stream?:NodeJS.WriteStream):Display {\n const s = stream ?? process.stdout\n if (s.isTTY) {\n return new StatusDisplay({ stream:s })\n }\n\n return new PlainDisplay()\n}\n"],
|
|
5
|
-
"mappings": ";;
|
|
4
|
+
"sourcesContent": ["export type TicketStatus =\n | 'pending'\n | 'in-progress'\n | 'awaiting-merge'\n | 'done'\n | 'failed'\n | 'blocked'\n\nexport type DisplayTicket = {\n readonly num:number\n readonly filename:string\n readonly feature?:string\n status:TicketStatus\n blockedBy?:ReadonlyArray<string>\n}\n\nexport type TicketRef = {\n readonly num:number\n readonly filename:string\n readonly feature?:string\n}\n\nexport type WaveState = {\n readonly waveNum:number\n readonly totalWaves:number\n}\n\nexport type DisplayConfig = {\n readonly stream:NodeJS.WriteStream\n}\n\n/**\n * Progress across the whole run, not just the current wave.\n * `completed` counts this run's tickets done before the wave\n * started; `total` is the size of this run's ticket list. Only\n * PlainDisplay prints it; StatusDisplay counts from its own list.\n */\nexport type RunProgress = {\n readonly completed:number\n readonly total:number\n}\n\nexport type Display = {\n /**\n * Called once at the start of the run, fixes the ticket list\n * for the entire session.\n */\n startRun(tickets:ReadonlyArray<TicketRef>):void\n startWave(\n waveNum:number,\n totalWaves:number,\n tickets:ReadonlyArray<TicketRef>,\n progress?:RunProgress,\n ):void\n setTicketStatus(\n num:number,\n status:TicketStatus,\n feature?:string,\n ):void\n /**\n * Mark a ticket as blocked; `blockedBy` holds display labels\n * of the upstream tickets that failed or need a human.\n */\n setBlocked(\n num:number,\n feature:string|undefined,\n blockedBy:ReadonlyArray<string>,\n ):void\n log(message:string):void\n cleanup():void\n}\n\nconst ESC = '\\x1b['\n\nexport const ansi = {\n cursorUp (n:number):string {\n return `${ESC}${n}A`\n },\n cursorTo (col:number):string {\n return `${ESC}${col}G`\n },\n eraseLine ():string {\n return `${ESC}2K`\n },\n eraseLines (n:number):string {\n let out = ''\n for (let i = 0; i < n; i++) {\n out += ansi.eraseLine()\n if (i < n - 1) out += ansi.cursorUp(1)\n }\n return out\n },\n cursorHide ():string {\n return `${ESC}?25l`\n },\n cursorShow ():string {\n return `${ESC}?25h`\n },\n} as const\n\nexport const color = {\n green (text:string):string {\n return `${ESC}32m${text}${ESC}0m`\n },\n yellow (text:string):string {\n return `${ESC}33m${text}${ESC}0m`\n },\n red (text:string):string {\n return `${ESC}31m${text}${ESC}0m`\n },\n cyan (text:string):string {\n return `${ESC}36m${text}${ESC}0m`\n },\n dim (text:string):string {\n return `${ESC}2m${text}${ESC}0m`\n },\n} as const\n\nexport function ticketLabel (\n ticket:{ num:number; filename:string; feature?:string },\n):string {\n if (ticket.feature) {\n const padded = String(ticket.num).padStart(2, '0')\n return `${ticket.feature}/${padded}`\n }\n return ticket.filename\n}\n\nconst FILLED_DOT = '\u25CF'\nconst EMPTY_DOT = '\u25CB'\nconst SEPARATOR_WIDTH = 50\nconst DEFAULT_COLUMNS = 80\nconst MIN_LOG_ROWS = 1\nconst MAX_MESSAGE_BUFFER = 200\n\n// eslint-disable-next-line no-control-regex\nconst ANSI_RE = /\\x1b\\[[0-9;?]*[A-Za-z]/g\n\nfunction rowText (ticket:DisplayTicket):string {\n const label = ticketLabel(ticket)\n if (ticket.status !== 'blocked' || !ticket.blockedBy?.length) {\n return label\n }\n return `${label} (blocked by ${ticket.blockedBy.join(', ')})`\n}\n\n/**\n * Clamp a line to `width` visible columns so it can never wrap.\n * A wrapped line occupies more terminal rows than `render`\n * counts, which leaves stale rows behind on the next erase.\n */\nexport function fitLine (line:string, width:number):string {\n const visible = line.replace(ANSI_RE, '')\n if (visible.length <= width) return line\n return visible.slice(0, width)\n}\n\n/**\n * Split a line into rows of at most `width` visible columns,\n * the way a terminal would wrap it. Short lines keep their\n * ANSI styling; long lines are wrapped on visible text only,\n * so `render` can count every row it emits.\n */\nexport function wrapLine (line:string, width:number):Array<string> {\n const visible = line.replace(ANSI_RE, '')\n if (visible.length <= width) return [line]\n const rows:Array<string> = []\n for (let i = 0; i < visible.length; i += width) {\n rows.push(visible.slice(i, i + width))\n }\n return rows\n}\n\nexport function renderDot (status:TicketStatus):string {\n switch (status) {\n case 'pending':\n return color.dim(EMPTY_DOT)\n case 'in-progress':\n return color.yellow(FILLED_DOT)\n // The agent and its test gate are finished; the branch\n // waits for the rest of the wave before it is merged. It\n // stays cyan through the merge and any conflict respawn.\n case 'awaiting-merge':\n return color.cyan(FILLED_DOT)\n case 'done':\n return color.green(FILLED_DOT)\n case 'failed':\n return color.red(FILLED_DOT)\n case 'blocked':\n // Ticket cannot run this session because an upstream\n // ticket failed or needs a human.\n return color.dim(FILLED_DOT)\n }\n}\n\nexport class StatusDisplay implements Display {\n private wave:WaveState|null = null\n private tickets:Array<DisplayTicket> = []\n private hasRun:boolean = false\n private messages:Array<string> = []\n private renderedLines:number = 0\n private config:DisplayConfig\n private viewportStart:number = 0\n private resizeListener:(() => void)|null = null\n\n constructor (config?:Readonly<Partial<DisplayConfig>>) {\n this.config = {\n stream:config?.stream ?? process.stdout,\n }\n }\n\n private getAvailableRows ():number {\n const termHeight = this.config.stream.rows ?? 24\n const overhead = 2 + MIN_LOG_ROWS\n const available = termHeight - overhead\n return Math.max(1, available)\n }\n\n private findTicket (\n num:number,\n feature?:string,\n ):DisplayTicket|null {\n const found = feature ?\n this.tickets.find(\n t => t.num === num && t.feature === feature,\n ) :\n this.tickets.find(t => t.num === num)\n return found ?? null\n }\n\n startRun (tickets:ReadonlyArray<TicketRef>):void {\n this.tickets = tickets.map(t => ({\n ...t,\n status:'pending' as const,\n }))\n this.hasRun = true\n this.viewportStart = 0\n }\n\n startWave (\n waveNum:number,\n totalWaves:number,\n tickets:ReadonlyArray<TicketRef>,\n _progress?:RunProgress,\n ):void {\n if (!this.hasRun) {\n this.startRun(tickets)\n }\n\n this.wave = {\n waveNum,\n totalWaves,\n }\n this.messages = []\n this.viewportStart = 0\n this.config.stream.write(ansi.cursorHide())\n\n if (this.config.stream.isTTY) {\n if (this.resizeListener) {\n this.config.stream.removeListener(\n 'resize', this.resizeListener,\n )\n }\n this.resizeListener = () => this.render()\n this.config.stream.on(\n 'resize', this.resizeListener,\n )\n }\n\n this.render()\n }\n\n setTicketStatus (\n num:number,\n status:TicketStatus,\n feature?:string,\n ):void {\n const ticket = this.findTicket(num, feature)\n if (!ticket) return\n\n ticket.status = status\n\n if (status === 'in-progress') {\n const ticketIndex = this.tickets.indexOf(ticket)\n const availableRows = this.getAvailableRows()\n const totalTickets = this.tickets.length\n\n if (ticketIndex < this.viewportStart) {\n this.viewportStart = ticketIndex\n } else if (ticketIndex >= this.viewportStart + availableRows) {\n // offset by 3 to keep context visible below\n // the active ticket\n this.viewportStart = ticketIndex - availableRows + 3\n }\n this.viewportStart = Math.max(\n 0,\n Math.min(this.viewportStart, totalTickets - 1),\n )\n }\n\n this.render()\n }\n\n setBlocked (\n num:number,\n feature:string|undefined,\n blockedBy:ReadonlyArray<string>,\n ):void {\n const ticket = this.findTicket(num, feature)\n if (!ticket) return\n\n ticket.status = 'blocked'\n ticket.blockedBy = [...blockedBy]\n this.render()\n }\n\n log (message:string):void {\n // Before the first wave there is no grid to repaint,\n // so a message would otherwise be dropped. Write it\n // straight to the stream instead. A later render\n // never erases these rows, since renderedLines is 0.\n if (!this.wave) {\n this.config.stream.write(message + '\\n')\n return\n }\n for (const line of message.split('\\n')) {\n this.messages.push(line)\n }\n while (this.messages.length > MAX_MESSAGE_BUFFER) {\n this.messages.shift()\n }\n this.render()\n }\n\n cleanup ():void {\n // Remove resize listener if it exists\n if (this.resizeListener && this.config.stream.isTTY) {\n this.config.stream.removeListener('resize', this.resizeListener)\n }\n this.config.stream.write(ansi.cursorShow())\n }\n\n private render ():void {\n if (!this.wave) return\n\n let output = ''\n\n if (this.renderedLines > 0) {\n output += ansi.eraseLines(this.renderedLines + 1)\n }\n\n const done = this.tickets.filter(\n t => t.status === 'done',\n ).length\n const total = this.tickets.length\n const pct = total === 0 ? 0 : Math.round((done / total) * 100)\n const width = Math.max(\n 1,\n (this.config.stream.columns ?? DEFAULT_COLUMNS) - 1,\n )\n\n const header = `Wave ${this.wave.waveNum}/` +\n `${this.wave.totalWaves} - ` +\n `${done}/${total} done - ${pct}%`\n output += fitLine(header, width) + '\\n'\n\n // Pagination logic\n const availableRows = this.getAvailableRows()\n let renderedTicketCount = 0\n\n if (this.tickets.length <= availableRows) {\n // All tickets fit, no pagination\n for (const ticket of this.tickets) {\n const dot = renderDot(ticket.status)\n const text = rowText(ticket)\n const name = fitLine(text, width - 2)\n output += `${dot} ${name}\\n`\n }\n renderedTicketCount = this.tickets.length\n } else {\n // Need pagination -- iterative clamp\n this.viewportStart = Math.max(\n 0,\n Math.min(\n this.viewportStart,\n this.tickets.length - 1,\n ),\n )\n\n let hasMoreAbove = this.viewportStart > 0\n let tentativeCount =\n availableRows - (hasMoreAbove ? 1 : 0)\n let hasMoreBelow =\n this.viewportStart + tentativeCount <\n this.tickets.length\n let ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0),\n )\n\n const maxStart = Math.max(\n 0,\n this.tickets.length - Math.max(1, ticketsToShow),\n )\n if (this.viewportStart > maxStart) {\n this.viewportStart = maxStart\n hasMoreAbove = this.viewportStart > 0\n tentativeCount = availableRows - (hasMoreAbove ? 1 : 0)\n hasMoreBelow = (\n this.viewportStart +\n tentativeCount < this.tickets.length\n )\n ticketsToShow = Math.max(\n 0,\n tentativeCount - (hasMoreBelow ? 1 : 0)\n )\n }\n\n // Show \"more above\" indicator if needed\n if (hasMoreAbove) {\n const moreCount = this.viewportStart\n output += ` ... ${moreCount} more above\\n`\n }\n\n // Get visible tickets and render them\n const visibleTickets = this.tickets.slice(\n this.viewportStart,\n this.viewportStart + ticketsToShow,\n )\n\n for (const ticket of visibleTickets) {\n const dot = renderDot(ticket.status)\n const text = rowText(ticket)\n const name = fitLine(text, width - 2)\n output += `${dot} ${name}\\n`\n }\n\n // Show \"more below\" indicator if needed\n if (hasMoreBelow) {\n const moreCount = (\n this.tickets.length -\n (this.viewportStart + ticketsToShow)\n )\n output += ` ... ${moreCount} more below\\n`\n }\n\n renderedTicketCount = (hasMoreAbove ? 1 : 0) +\n visibleTickets.length +\n (hasMoreBelow ? 1 : 0)\n }\n\n output += '-'.repeat(Math.min(SEPARATOR_WIDTH, width)) + '\\n'\n\n const termHeight = this.config.stream.rows ?? 24\n const logRows = Math.max(\n 0,\n termHeight - 3 - renderedTicketCount,\n )\n const wrapped = this.messages.flatMap(\n msg => wrapLine(msg, width),\n )\n const visibleMessages = wrapped.slice(-logRows)\n for (const row of visibleMessages) {\n output += `${row}\\n`\n }\n const emptyRows = logRows - visibleMessages.length\n for (let i = 0; i < emptyRows; i++) {\n output += '\\n'\n }\n\n this.renderedLines = 2 + renderedTicketCount + logRows\n this.config.stream.write(output)\n }\n}\n\nexport class PlainDisplay implements Display {\n startRun (_tickets:ReadonlyArray<TicketRef>):void {\n // no-op: plain mode has no fixed list\n }\n\n startWave (\n waveNum:number,\n _totalWaves:number,\n tickets:ReadonlyArray<TicketRef>,\n progress?:RunProgress,\n ):void {\n const pct = progress && progress.total > 0 ?\n ` (${progress.completed}/${progress.total} done)` :\n ''\n console.log(\n `\\nWave ${waveNum}${pct}: ` +\n tickets.map(t => ticketLabel(t)).join(', ')\n )\n }\n\n setTicketStatus (\n _num:number,\n _status:TicketStatus,\n _feature?:string,\n ):void {\n // no-op: plain mode reports via log()\n }\n\n setBlocked (\n _num:number,\n _feature:string|undefined,\n _blockedBy:ReadonlyArray<string>,\n ):void {\n // no-op: plain mode has no fixed list\n }\n\n log (message:string):void {\n console.log(message)\n }\n\n cleanup ():void {\n // no-op\n }\n}\n\nexport function createDisplay (stream?:NodeJS.WriteStream):Display {\n const s = stream ?? process.stdout\n if (s.isTTY) {\n return new StatusDisplay({ stream:s })\n }\n\n return new PlainDisplay()\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|