nightralph 0.0.18 → 0.0.21
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 +74 -10
- package/dist/display.js +12 -20
- package/dist/display.js.map +2 -2
- package/dist/docs-templates/issue-tracker-github.md +15 -85
- package/dist/docs-templates/issue-tracker.md +16 -10
- package/dist/index.js +691 -198
- package/dist/index.js.map +4 -4
- package/dist/meta.json +66 -12
- package/dist/orchestrator.js +517 -142
- package/dist/orchestrator.js.map +3 -3
- package/dist/progress.js +10 -0
- package/dist/progress.js.map +2 -2
- package/dist/skills/to-spec/SKILL.md +1 -1
- package/dist/skills/to-tickets/SKILL.md +2 -2
- package/dist/src/display.d.ts +0 -2
- package/dist/src/display.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/orchestrator.d.ts +25 -2
- package/dist/src/orchestrator.d.ts.map +1 -1
- package/dist/src/progress.d.ts +1 -1
- package/dist/src/progress.d.ts.map +1 -1
- package/dist/src/testcmd.d.ts +11 -0
- package/dist/src/testcmd.d.ts.map +1 -0
- package/dist/src/worktree.d.ts +3 -1
- package/dist/src/worktree.d.ts.map +1 -1
- package/dist/testcmd.js +67 -0
- package/dist/testcmd.js.map +7 -0
- package/dist/worktree.js +19 -6
- package/dist/worktree.js.map +2 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -86,7 +86,11 @@ to add the required skills to your local `.agents/skills/` folder.
|
|
|
86
86
|
Use the `to-tickets` skill to create issues in local markdown files from
|
|
87
87
|
a prose text description.
|
|
88
88
|
|
|
89
|
-
The `nightralph` CLI does the `tdd` part of the workflow
|
|
89
|
+
The `nightralph` CLI does the `tdd` part of the workflow: the agent
|
|
90
|
+
prompt tells the agent to write a failing test per acceptance
|
|
91
|
+
criterion at the seams named in the spec's Testing Decisions, and
|
|
92
|
+
the orchestrator runs the project's test command in the worktree
|
|
93
|
+
before a ticket can be marked `done`.
|
|
90
94
|
|
|
91
95
|
>
|
|
92
96
|
> [!NOTE]
|
|
@@ -122,6 +126,12 @@ npx nightralph codex --dry-run
|
|
|
122
126
|
|
|
123
127
|
# Specify a feature by name
|
|
124
128
|
npx nightralph claude -m claude-opus-4-6 --spec my-feature
|
|
129
|
+
|
|
130
|
+
# Cap each agent at 30 agentic turns
|
|
131
|
+
npx nightralph claude -m claude-opus-4-6 --max-turns 30
|
|
132
|
+
|
|
133
|
+
# Start pi at medium thinking and allow one escalated retry
|
|
134
|
+
npx nightralph pi -m openrouter/z-ai/glm-5.3-flash --thinking medium --retries 1
|
|
125
135
|
```
|
|
126
136
|
|
|
127
137
|
#### `--spec <name>` option
|
|
@@ -139,7 +149,12 @@ Flags:
|
|
|
139
149
|
| `--spec <name>` | no | auto | Feature name (resolves `.scratch/<name>/`) |
|
|
140
150
|
| `--dry-run` | no | -- | Show wave order and prompt without running |
|
|
141
151
|
| `-X` | no | -- | Stop on merge conflict instead of re-spawning |
|
|
142
|
-
| `--timeout` | no | `
|
|
152
|
+
| `--timeout` | no | `3600` | Kill the agent after N seconds. A killed agent is not a failure: its work is kept (see Notes) |
|
|
153
|
+
| `--test-cmd <cmd>` | no | auto | Run in each worktree after the agent exits; non-zero exit rejects the ticket. Auto-detects `npm test`. Pass `""` to disable. Shares the agent's `--timeout`. The command runs in a fresh git worktree, which contains only tracked files, so a project that needs an install step should include it, for example `--test-cmd 'npm ci && npm test'` |
|
|
154
|
+
| `--max-turns <n>` | no | -- | Max agentic turns per ticket. Forwarded verbatim as `--max-turns <n>` to the provider CLI. Only `claude` accepts it; `codex` and `pi` reject it as an unknown option and the agent exits immediately. Applies to every agent spawn, including a re-spawn after a merge conflict. When omitted, `claude` applies no turn limit, so `--timeout` is the only cap. Reaching the limit is an error exit, so the ticket is recorded as failed |
|
|
155
|
+
| `--thinking <level>` | no | -- | Starting thinking level for `pi`, one of `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Forwarded as `--thinking <level>`. Omitted = pi's own default. Ignored for other providers. Each retry steps up from here (see `--retries`) |
|
|
156
|
+
| `--retries <n>` | no | `2` | Extra attempts per ticket after a failed run. Each retry starts from a fresh worktree; with `pi` it also raises the thinking level (see Notes). `0` disables retries |
|
|
157
|
+
| `--retry-delay <n>` | no | `30` | Seconds to wait before the first retry; doubles each further retry (30s, 60s, 120s, ...). `0` disables the wait |
|
|
143
158
|
|
|
144
159
|
|
|
145
160
|
**Notes**:
|
|
@@ -154,19 +169,68 @@ Flags:
|
|
|
154
169
|
tickets start in the next wave.
|
|
155
170
|
* An agent run only counts as a success when the agent exits 0,
|
|
156
171
|
its worktree branch has new commits relative to the base branch,
|
|
157
|
-
and the
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
and the test command (see `--test-cmd`) exits 0 in that worktree.
|
|
173
|
+
Uncommitted changes left by the agent are auto-committed before
|
|
174
|
+
the check. On success, the ticket's status is updated to `done`
|
|
175
|
+
in-place. If the agent exits 0 but made no commits, or the test
|
|
176
|
+
command fails, the ticket is left `ready-for-agent` for a retry
|
|
177
|
+
and the test output is written to `<dir>/../logs/<ticket>.test.log`.
|
|
178
|
+
Failed tickets (non-zero exit) keep their status and block
|
|
179
|
+
dependents.
|
|
180
|
+
* A failed attempt is retried, up to `--retries` times, before the
|
|
181
|
+
ticket is recorded as failed. Non-zero exit, exit 0 with no new
|
|
182
|
+
commits, and a red test command all count as failures; a timeout
|
|
183
|
+
does not (see below). Each retry discards the previous worktree
|
|
184
|
+
and starts from a fresh one. With `pi`, each retry raises the
|
|
185
|
+
thinking level: without `--thinking` the first retry uses `high`
|
|
186
|
+
and the second `max`; with `--thinking <level>` each retry steps
|
|
187
|
+
one level up from the previous attempt, stopping at `max`.
|
|
188
|
+
`claude` and `codex` are retried at the same settings. Retries
|
|
189
|
+
are spaced with exponential backoff controlled by `--retry-delay`:
|
|
190
|
+
the first retry waits `--retry-delay` seconds, the second waits
|
|
191
|
+
double that, and so on. This lets provider outages clear before
|
|
192
|
+
burning the next attempt. Retry
|
|
193
|
+
logs are written to `<dir>/../logs/<ticket>.attempt<n>.log`
|
|
194
|
+
(and `.attempt<n>.test.log` for the test command) so the first
|
|
195
|
+
attempt's log is kept.
|
|
196
|
+
* A ticket that exhausts all its retry attempts is excluded from
|
|
197
|
+
later waves in the same run. It stays `ready-for-agent` on disk
|
|
198
|
+
so a future run can pick it up, but it will not be re-started
|
|
199
|
+
within the current invocation. The final summary line includes
|
|
200
|
+
the count of failed tickets when any failed.
|
|
201
|
+
* An agent still running at `--timeout` is killed. The kill is
|
|
202
|
+
reported in the status display and in the ticket log. It is not
|
|
203
|
+
treated as an agent failure: uncommitted changes in the worktree
|
|
204
|
+
are auto-committed, and the branch goes through the same
|
|
205
|
+
new-commits check, test gate, and merge as an agent that exited 0.
|
|
206
|
+
Opus tickets have taken over 12 minutes of API time, which is why
|
|
207
|
+
the default is an hour rather than the old 15 minutes.
|
|
208
|
+
* Agents are spawned in their own process group (`detached:true`).
|
|
209
|
+
When the timeout fires or when you press Ctrl-C (SIGINT) or send
|
|
210
|
+
SIGTERM, the orchestrator kills the entire process group -- not
|
|
211
|
+
just the agent process -- so dev servers, watchers, and other
|
|
212
|
+
child processes are cleaned up. SIGINT exits 130 and SIGTERM
|
|
213
|
+
exits 143 after killing all running agents and restoring the
|
|
214
|
+
terminal.
|
|
215
|
+
* For fast models that reliably finish tickets in a few minutes,
|
|
216
|
+
`--timeout 900` is a reasonable cap that catches stuck agents
|
|
217
|
+
sooner than the one-hour default.
|
|
218
|
+
* `.scratch/` should be listed in your project's `.gitignore`.
|
|
219
|
+
The orchestrator force-adds `progress.md` so it is tracked despite
|
|
220
|
+
the ignore rule, but ticket files, logs, and spec files are
|
|
221
|
+
working state and should not be committed to the project repo.
|
|
162
222
|
* Agent logs are written to `<dir>/../logs/<ticket>.log`.
|
|
163
223
|
* Provider commands are launched non-interactively with default flags
|
|
164
224
|
that grant the agent write access to the worktree, so tickets can
|
|
165
225
|
actually be implemented without a permission prompt:
|
|
166
226
|
* `claude`: `--print --dangerously-skip-permissions`
|
|
167
|
-
* `codex`: `exec --
|
|
168
|
-
|
|
169
|
-
|
|
227
|
+
* `codex`: `exec --dangerously-bypass-approvals-and-sandbox`
|
|
228
|
+
(codex removed `--full-auto`; the workspace-write sandbox
|
|
229
|
+
cannot commit in a worktree because its `.git` link points
|
|
230
|
+
outside the tree)
|
|
231
|
+
* `pi`: `-p --verbose --approve --no-session --mode json`
|
|
232
|
+
(prompt delivered via temp file; `--approve` auto-accepts
|
|
233
|
+
tool-use proposals so the agent runs non-interactively)
|
|
170
234
|
|
|
171
235
|
|
|
172
236
|
## How It Works
|
package/dist/display.js
CHANGED
|
@@ -44,6 +44,8 @@ const FILLED_DOT = "\u25CF";
|
|
|
44
44
|
const EMPTY_DOT = "\u25CB";
|
|
45
45
|
const SEPARATOR_WIDTH = 50;
|
|
46
46
|
const DEFAULT_COLUMNS = 80;
|
|
47
|
+
const MIN_LOG_ROWS = 1;
|
|
48
|
+
const MAX_MESSAGE_BUFFER = 200;
|
|
47
49
|
const ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
48
50
|
function fitLine(line, width) {
|
|
49
51
|
const visible = line.replace(ANSI_RE, "");
|
|
@@ -76,16 +78,14 @@ class StatusDisplay {
|
|
|
76
78
|
config;
|
|
77
79
|
viewportStart = 0;
|
|
78
80
|
resizeListener = null;
|
|
79
|
-
signalHandler = null;
|
|
80
81
|
constructor(config) {
|
|
81
82
|
this.config = {
|
|
82
|
-
messageLines: config?.messageLines ?? 10,
|
|
83
83
|
stream: config?.stream ?? process.stdout
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
getAvailableRows() {
|
|
87
87
|
const termHeight = this.config.stream.rows ?? 24;
|
|
88
|
-
const overhead = 2 +
|
|
88
|
+
const overhead = 2 + MIN_LOG_ROWS;
|
|
89
89
|
const available = termHeight - overhead;
|
|
90
90
|
return Math.max(1, available);
|
|
91
91
|
}
|
|
@@ -119,13 +119,6 @@ class StatusDisplay {
|
|
|
119
119
|
this.resizeListener
|
|
120
120
|
);
|
|
121
121
|
}
|
|
122
|
-
if (this.signalHandler) {
|
|
123
|
-
process.removeListener("SIGINT", this.signalHandler);
|
|
124
|
-
process.removeListener("SIGTERM", this.signalHandler);
|
|
125
|
-
}
|
|
126
|
-
this.signalHandler = () => this.cleanup();
|
|
127
|
-
process.on("SIGINT", this.signalHandler);
|
|
128
|
-
process.on("SIGTERM", this.signalHandler);
|
|
129
122
|
this.render();
|
|
130
123
|
}
|
|
131
124
|
setTicketStatus(num, status) {
|
|
@@ -152,7 +145,7 @@ class StatusDisplay {
|
|
|
152
145
|
}
|
|
153
146
|
log(message) {
|
|
154
147
|
this.messages.push(message);
|
|
155
|
-
if (this.messages.length >
|
|
148
|
+
if (this.messages.length > MAX_MESSAGE_BUFFER) {
|
|
156
149
|
this.messages.shift();
|
|
157
150
|
}
|
|
158
151
|
this.render();
|
|
@@ -161,10 +154,6 @@ class StatusDisplay {
|
|
|
161
154
|
if (this.resizeListener && this.config.stream.isTTY) {
|
|
162
155
|
this.config.stream.removeListener("resize", this.resizeListener);
|
|
163
156
|
}
|
|
164
|
-
if (this.signalHandler) {
|
|
165
|
-
process.removeListener("SIGINT", this.signalHandler);
|
|
166
|
-
process.removeListener("SIGTERM", this.signalHandler);
|
|
167
|
-
}
|
|
168
157
|
this.config.stream.write(ansi.cursorShow());
|
|
169
158
|
}
|
|
170
159
|
render() {
|
|
@@ -251,18 +240,21 @@ class StatusDisplay {
|
|
|
251
240
|
renderedTicketCount = (hasMoreAbove ? 1 : 0) + visibleTickets.length + (hasMoreBelow ? 1 : 0);
|
|
252
241
|
}
|
|
253
242
|
output += "-".repeat(Math.min(SEPARATOR_WIDTH, width)) + "\n";
|
|
254
|
-
const
|
|
243
|
+
const termHeight = this.config.stream.rows ?? 24;
|
|
244
|
+
const logRows = Math.max(
|
|
255
245
|
0,
|
|
256
|
-
|
|
246
|
+
termHeight - 2 - renderedTicketCount
|
|
257
247
|
);
|
|
258
|
-
|
|
248
|
+
const visibleMessages = this.messages.slice(-logRows);
|
|
249
|
+
for (const msg of visibleMessages) {
|
|
259
250
|
output += `${fitLine(msg, width)}
|
|
260
251
|
`;
|
|
261
252
|
}
|
|
262
|
-
|
|
253
|
+
const emptyRows = logRows - visibleMessages.length;
|
|
254
|
+
for (let i = 0; i < emptyRows; i++) {
|
|
263
255
|
output += "\n";
|
|
264
256
|
}
|
|
265
|
-
this.renderedLines = 2 + renderedTicketCount +
|
|
257
|
+
this.renderedLines = 2 + renderedTicketCount + logRows;
|
|
266
258
|
this.config.stream.write(output);
|
|
267
259
|
}
|
|
268
260
|
}
|
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 | 'done'\n | 'failed'\n\nexport type DisplayTicket = {\n readonly num:number\n readonly filename: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 messageLines:number\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 }>,\n progress?:RunProgress,\n remaining?:ReadonlyArray<{\n num:number\n filename:string\n }>,\n ):void\n setTicketStatus(num:number, status:TicketStatus):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 dim (text:string):string {\n return `${ESC}2m${text}${ESC}0m`\n },\n} as const\n\nconst FILLED_DOT = '\u25CF'\nconst EMPTY_DOT = '\u25CB'\nconst SEPARATOR_WIDTH = 50\nconst DEFAULT_COLUMNS = 80\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\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 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 private signalHandler:(() => void)|null = null\n\n constructor (config?:Readonly<Partial<DisplayConfig>>) {\n this.config = {\n messageLines:config?.messageLines ?? 10,\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 + this.config.messageLines\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<{ num:number; filename:string }>>,\n progress?:RunProgress,\n remaining?:ReadonlyArray<{\n num:number\n filename: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 // Register signal handler to restore cursor on interruption\n if (this.signalHandler) {\n process.removeListener('SIGINT', this.signalHandler)\n process.removeListener('SIGTERM', this.signalHandler)\n }\n this.signalHandler = () => this.cleanup()\n process.on('SIGINT', this.signalHandler)\n process.on('SIGTERM', this.signalHandler)\n\n this.render()\n }\n\n setTicketStatus (num:number, status:TicketStatus):void {\n if (!this.wave) return\n\n const ticket = 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 this.messages.push(message)\n if (this.messages.length > this.config.messageLines) {\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 // Remove signal handlers\n if (this.signalHandler) {\n process.removeListener('SIGINT', this.signalHandler)\n process.removeListener('SIGTERM', this.signalHandler)\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(ticket.filename, width - 2)\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(ticket.filename, width - 2)\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 msgLines = Math.max(\n 0,\n this.config.messageLines - this.messages.length,\n )\n for (const msg of this.messages) {\n output += `${fitLine(msg, width)}\\n`\n }\n for (let i = 0; i < msgLines; i++) {\n output += '\\n'\n }\n\n this.renderedLines = 2 + renderedTicketCount + this.config.messageLines\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<{ num:number; filename:string }>,\n progress?:RunProgress,\n _remaining?:ReadonlyArray<{\n num:number\n filename: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 => t.filename).join(', ')\n )\n }\n\n setTicketStatus (_num:number, _status:TicketStatus):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 | 'done'\n | 'failed'\n\nexport type DisplayTicket = {\n readonly num:number\n readonly filename: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 }>,\n progress?:RunProgress,\n remaining?:ReadonlyArray<{\n num:number\n filename:string\n }>,\n ):void\n setTicketStatus(num:number, status:TicketStatus):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 dim (text:string):string {\n return `${ESC}2m${text}${ESC}0m`\n },\n} as const\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\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 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<{ num:number; filename:string }>>,\n progress?:RunProgress,\n remaining?:ReadonlyArray<{\n num:number\n filename: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 (num:number, status:TicketStatus):void {\n if (!this.wave) return\n\n const ticket = 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 this.messages.push(message)\n if (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(ticket.filename, width - 2)\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(ticket.filename, width - 2)\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 - 2 - renderedTicketCount,\n )\n const visibleMessages = this.messages.slice(-logRows)\n for (const msg of visibleMessages) {\n output += `${fitLine(msg, width)}\\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<{ num:number; filename:string }>,\n progress?:RunProgress,\n _remaining?:ReadonlyArray<{\n num:number\n filename: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 => t.filename).join(', ')\n )\n }\n\n setTicketStatus (_num:number, _status:TicketStatus):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": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -141,88 +141,18 @@ gh issue view <number>
|
|
|
141
141
|
|
|
142
142
|
The user will normally pass the issue number.
|
|
143
143
|
|
|
144
|
-
##
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
Add the `blocked` label and optionally a comment referencing the blocking issue:
|
|
160
|
-
|
|
161
|
-
```bash
|
|
162
|
-
gh issue edit <number> --add-label "blocked"
|
|
163
|
-
gh issue comment <number> --body "Blocked by #42"
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### Link related issues
|
|
167
|
-
|
|
168
|
-
Use issue references in comments and body text. GitHub auto-links:
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
# In an issue comment or body, write:
|
|
172
|
-
Relates to #123
|
|
173
|
-
Depends on #456
|
|
174
|
-
Blocks #789
|
|
175
|
-
Fixed in #999
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
These create bidirectional links visible in the issue UI.
|
|
179
|
-
|
|
180
|
-
### Check if unblocked
|
|
181
|
-
|
|
182
|
-
Query the blocking issues referenced in the issue's comments or description. An issue is unblocked when no blocking issues remain open.
|
|
183
|
-
|
|
184
|
-
## Frontier Query and Workflow
|
|
185
|
-
|
|
186
|
-
### Find available issues
|
|
187
|
-
|
|
188
|
-
Query for open, unassigned issues:
|
|
189
|
-
|
|
190
|
-
```bash
|
|
191
|
-
gh issue list --state open --assignee "" --label "available,p0" --json number,title
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### Claim an issue
|
|
195
|
-
|
|
196
|
-
Assign to yourself when starting work:
|
|
197
|
-
|
|
198
|
-
```bash
|
|
199
|
-
gh issue edit <number> --add-assignee "@me"
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
Optionally add `in-progress` label:
|
|
203
|
-
|
|
204
|
-
```bash
|
|
205
|
-
gh issue edit <number> --add-label "in-progress"
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
### Resolve an issue
|
|
209
|
-
|
|
210
|
-
Work through the issue in a PR. When done, close the issue and link the PR:
|
|
211
|
-
|
|
212
|
-
```bash
|
|
213
|
-
gh issue close <number> --comment "Resolved in PR #999"
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
Alternatively, create a closing comment with a reference:
|
|
217
|
-
|
|
218
|
-
```bash
|
|
219
|
-
gh issue comment <number> --body "Fixed in PR #999"
|
|
220
|
-
gh issue close <number>
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## Tips
|
|
224
|
-
|
|
225
|
-
- Use GitHub milestones to group related features
|
|
226
|
-
- Use `gh issue create --web` to open the issue creation form in your browser
|
|
227
|
-
- Use `gh issue edit <number> --web` to open an issue in the browser for complex edits
|
|
228
|
-
- Save frequently-used queries as saved views in the GitHub web UI
|
|
144
|
+
## Tickets are executed within hours, not weeks
|
|
145
|
+
|
|
146
|
+
Tickets in this tracker are run by an autonomous agent shortly
|
|
147
|
+
after they are written, with a fixed time budget and no
|
|
148
|
+
interview. This changes one upstream rule:
|
|
149
|
+
|
|
150
|
+
- Do include the file paths, module names, and exported symbols
|
|
151
|
+
the ticket touches. Put the list under a `## Touches` heading
|
|
152
|
+
in the issue body. The "avoid specific file paths" guidance in
|
|
153
|
+
`to-tickets` exists for long-lived human trackers and does not
|
|
154
|
+
apply here.
|
|
155
|
+
- Keep the `- [ ]` acceptance criteria phrased as observable
|
|
156
|
+
behavior. The agent writes one failing test per criterion, so
|
|
157
|
+
each criterion should be testable at a seam named in the spec's
|
|
158
|
+
Testing Decisions.
|
|
@@ -8,6 +8,7 @@ Issues and specs for this repo live as markdown files in `.scratch/`.
|
|
|
8
8
|
- The spec is `.scratch/<feature-slug>/spec.md`
|
|
9
9
|
- Implementation issues are one file per ticket at `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01`, never a single combined tickets file
|
|
10
10
|
- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings)
|
|
11
|
+
- Dependencies are recorded as a `**Blocked by:**` line near the top of each issue file, listing the numbers of the tickets that must complete first, or `None`
|
|
11
12
|
- Comments and conversation history append to the bottom of the file under a `## Comments` heading
|
|
12
13
|
|
|
13
14
|
## When a skill says "publish to the issue tracker"
|
|
@@ -18,13 +19,18 @@ Create a new file under `.scratch/<feature-slug>/` (creating the directory if ne
|
|
|
18
19
|
|
|
19
20
|
Read the file at the referenced path. The user will normally pass the path or the issue number directly.
|
|
20
21
|
|
|
21
|
-
##
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
## Tickets are executed within hours, not weeks
|
|
23
|
+
|
|
24
|
+
Tickets in this tracker are run by an autonomous agent shortly
|
|
25
|
+
after they are written, with a fixed time budget and no
|
|
26
|
+
interview. This changes one upstream rule:
|
|
27
|
+
|
|
28
|
+
- Do include the file paths, module names, and exported symbols
|
|
29
|
+
the ticket touches. Put them on a `**Touches:**` line directly
|
|
30
|
+
under `**Blocked by:**`. The "avoid specific file paths" guidance
|
|
31
|
+
in `to-tickets` exists for long-lived human trackers and does not
|
|
32
|
+
apply here.
|
|
33
|
+
- Keep the `- [ ]` acceptance criteria phrased as observable
|
|
34
|
+
behavior. The agent writes one failing test per criterion, so
|
|
35
|
+
each criterion should be testable at a seam named in the spec's
|
|
36
|
+
Testing Decisions.
|