@devwithdavid/ledger 0.1.5 → 0.2.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/LEDGER.md +83 -7
- package/dist/cli/commands/agents.js +132 -28
- package/dist/cli/commands/catchup.js +70 -4
- package/dist/cli/commands/projects.js +48 -8
- package/dist/cli/commands/roadmap.js +5 -10
- package/dist/cli/index.js +0 -0
- package/dist/db/client.js +32 -12
- package/dist/db/migrations/0005_roadmap_status_states.js +75 -0
- package/dist/db/migrations/index.js +2 -0
- package/dist/db/types.js +17 -0
- package/dist/lib/herdr.js +35 -0
- package/package.json +2 -2
package/LEDGER.md
CHANGED
|
@@ -155,8 +155,8 @@ every agent currently `idle` together with the tail of its pane's recent
|
|
|
155
155
|
output (A8 liveness triage — did it ask a question, hit a usage error, or
|
|
156
156
|
go quiet mid-task?), every `events` row since the last time any clerk ran
|
|
157
157
|
`catchup` (tracked in `first_clerk.last_seen`), and every roadmap item not
|
|
158
|
-
yet
|
|
159
|
-
id).
|
|
158
|
+
yet in a terminal status (`merged`/`completed`/`discarded`/`dropped`),
|
|
159
|
+
ordered by priority `high → normal → low` (ties by id).
|
|
160
160
|
This is the entire "what's going on" operation from `DESIGN.md` — read this
|
|
161
161
|
instead of any prose file, every session.
|
|
162
162
|
|
|
@@ -170,6 +170,18 @@ repeating the same failure under every idle agent. `--json` includes the
|
|
|
170
170
|
same data as an `idle` array (`agent`, `pane_tail`, `pane_read_error`) plus
|
|
171
171
|
top-level `idle_pane_read_note` for the whole-socket case.
|
|
172
172
|
|
|
173
|
+
`catchup` also lists every `done` agent whose herdr pane is still actually
|
|
174
|
+
alive (item 88) — a real candidate for `agent followup` below, distinct from
|
|
175
|
+
a `done` agent whose worktree/pane is genuinely gone. This is a fresh
|
|
176
|
+
per-agent `herdr pane get`, same A8-style observation idle pane tails
|
|
177
|
+
already use, not a new `agents.status` value: a `done` agent whose pane
|
|
178
|
+
resolves shows up here; one that doesn't (pane/tab/workspace closed) is
|
|
179
|
+
silently excluded, not reported as an error, per C7. `--json`'s
|
|
180
|
+
`done_with_live_pane` array carries `{agent, herdr_agent_status}` per entry;
|
|
181
|
+
`done_pane_read_note` mirrors `idle_pane_read_note` for the whole-socket-down
|
|
182
|
+
case. Governed by the same `--idle-pane-lines` flag as idle tails — `0`
|
|
183
|
+
disables both.
|
|
184
|
+
|
|
173
185
|
`catchup` also checks (item 42) whether a newer `@devwithdavid/ledger` npm
|
|
174
186
|
version exists — a plain-text notice after the normal output in
|
|
175
187
|
human-readable mode, or the top-level `update_available` JSON key
|
|
@@ -261,8 +273,33 @@ ledger roadmap list --project <name> [--status <status>] [--all] [--json]
|
|
|
261
273
|
ledger roadmap update <id> [--status <status>] [--title <title>] [--description <text>] [--priority <high|normal|low>]
|
|
262
274
|
```
|
|
263
275
|
|
|
264
|
-
Statuses
|
|
265
|
-
|
|
276
|
+
Statuses (roadmap #89, 2026-09-24 — replaces the old bare `done`):
|
|
277
|
+
non-terminal (workflow) `planned | in_progress | blocked | in_review`;
|
|
278
|
+
terminal (final) `merged | completed | discarded | dropped`. `in_review`
|
|
279
|
+
means code is written, pushed, and a PR/MR is open — work on the item
|
|
280
|
+
itself is done, it's just waiting on a human review/merge decision.
|
|
281
|
+
`merged` is the terminal state for anything that produced a git artifact
|
|
282
|
+
and landed on the default branch; `discarded` is the terminal state for a
|
|
283
|
+
PR/MR that existed but was reviewed and rejected/closed without merging —
|
|
284
|
+
distinct from `dropped` (abandoned/never attempted, no work product
|
|
285
|
+
either way) because it tells you something different at a glance: effort
|
|
286
|
+
was spent and a real decision was made against it. `completed` is the
|
|
287
|
+
terminal state for items that never have/need a git artifact at all
|
|
288
|
+
(decisions made, investigations concluded).
|
|
289
|
+
|
|
290
|
+
**Observation-required constraint (same principle as gate C7):** `merged`
|
|
291
|
+
and `discarded` must only ever be set from an actual observed check
|
|
292
|
+
against git/the PR host (e.g. `git log <default-branch> --grep`, or
|
|
293
|
+
checking the MR's merged/closed state via the remote's API/CLI) — never
|
|
294
|
+
from an agent's self-reported outcome text alone, and never inferred by
|
|
295
|
+
you without checking. An agent claiming "I merged it" or "MR is up" is
|
|
296
|
+
not evidence of `merged`, any more than a quiet pane is evidence an agent
|
|
297
|
+
is `idle` under C7. `in_review` can be set more loosely — a PR/MR URL in
|
|
298
|
+
an agent's outcome is reasonable evidence code is up for review — since
|
|
299
|
+
it isn't terminal.
|
|
300
|
+
|
|
301
|
+
`--parent` nests a sub-item under an existing roadmap item (arbitrary
|
|
302
|
+
depth).
|
|
266
303
|
|
|
267
304
|
Priority: `high | normal | low`, default `normal` (also a column on every
|
|
268
305
|
row: JSON output and `list`'s table both show it). It is a coarse triage
|
|
@@ -282,8 +319,9 @@ rather than deciding unilaterally or always making them write it themselves
|
|
|
282
319
|
constraints, what's explicitly out of scope) — that's what you'll turn into
|
|
283
320
|
`--task` text at dispatch time via `--roadmap-item <id>`.
|
|
284
321
|
|
|
285
|
-
`roadmap list`
|
|
286
|
-
|
|
322
|
+
`roadmap list` and `catchup`'s "Roadmap in flight" section both exclude
|
|
323
|
+
all terminal statuses (`merged`/`completed`/`discarded`/`dropped`) by
|
|
324
|
+
default; pass `--all` (`roadmap list` only) to see everything.
|
|
287
325
|
|
|
288
326
|
### Dispatching an agent
|
|
289
327
|
|
|
@@ -356,6 +394,42 @@ From here, **you do nothing further** to track state — the herdr watcher
|
|
|
356
394
|
plugin keeps `agents.status` and `events` current automatically as that
|
|
357
395
|
pane's agent state changes.
|
|
358
396
|
|
|
397
|
+
### Following up with an already-dispatched agent (item 88)
|
|
398
|
+
|
|
399
|
+
```sh
|
|
400
|
+
ledger agent followup <id> --task "<description>" --authorization <user-explicit|pre-authorized>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
For a small iterative fixup on work an agent already did — not a new,
|
|
404
|
+
separate concern — this continues that same `agents` row/worktree/branch
|
|
405
|
+
instead of leasing a whole new worktree and starting a fresh agent that has
|
|
406
|
+
to reconstruct context from git history. `catchup`'s "Done agents with a
|
|
407
|
+
live pane" section (above) is where you'll usually spot a candidate: a
|
|
408
|
+
`done` agent whose pane is still actually alive. `--authorization` works
|
|
409
|
+
exactly like `dispatch`'s (C6) — a follow-up prompt is still you attesting
|
|
410
|
+
the user authorized it — and is recorded on a new `followup_dispatched`
|
|
411
|
+
event on the *same* agent row, never a second disconnected one.
|
|
412
|
+
|
|
413
|
+
Before sending anything, this re-checks the pane fresh (C7 — never the
|
|
414
|
+
agent row's own possibly-stale/frozen `status` column) and refuses cleanly
|
|
415
|
+
if: the pane doesn't resolve at all (closed, worktree already released);
|
|
416
|
+
its `tab`/`workspace` no longer match what's recorded (herdr recycled the
|
|
417
|
+
pane id to something else — the same identity hazard fixed for workspace
|
|
418
|
+
reuse, applied here to panes); or its live status is `working` (still mid-
|
|
419
|
+
turn — wait) or `blocked` (that's the existing "answer it directly" path
|
|
420
|
+
under "Monitoring and escalating" below, not this command).
|
|
421
|
+
|
|
422
|
+
On success: `task_description` gets the new instruction appended under a
|
|
423
|
+
`--- Follow-up (<timestamp>) ---` marker (so the full history is visible via
|
|
424
|
+
`agent get`, not just buried in `events`), `status` resets to `working`
|
|
425
|
+
(this is what un-freezes the watcher's terminal guard so it starts tracking
|
|
426
|
+
this pane's state again — see `DECISIONS.md`), and the task is delivered
|
|
427
|
+
with the same reporting contract as a fresh dispatch, except told to
|
|
428
|
+
continue the existing worktree/branch rather than start a new one. If the
|
|
429
|
+
final prompt delivery itself fails, the row is kept with a
|
|
430
|
+
`followup_prompt_failed` event, same rationale as `dispatch_prompt_failed`
|
|
431
|
+
above.
|
|
432
|
+
|
|
359
433
|
Other agent commands:
|
|
360
434
|
|
|
361
435
|
```sh
|
|
@@ -489,7 +563,9 @@ is a summary, not a copy to keep in sync by hand):
|
|
|
489
563
|
`default_branch`, `delivery_mode`, `herdr_workspace` — the project's
|
|
490
564
|
shared herdr workspace, NULL until its first dispatch).
|
|
491
565
|
- `roadmap` — hierarchical (`parent_id` self-reference), `status` enum
|
|
492
|
-
`planned|in_progress|blocked|
|
|
566
|
+
`planned|in_progress|blocked|in_review|merged|completed|discarded|dropped`
|
|
567
|
+
(see "Roadmap: turning asks into briefs" above for what each means and
|
|
568
|
+
the observation-required rule on `merged`/`discarded`), `priority` enum
|
|
493
569
|
`high|normal|low` (default `normal`) — triage order for the not-done
|
|
494
570
|
queue, not readiness.
|
|
495
571
|
- `agents` — one row per dispatch (`project_id`, `roadmap_item_id`,
|
|
@@ -5,7 +5,7 @@ import { AUTHORIZATION_BASES, CODING_AGENT_KINDS } from "../../db/types.js";
|
|
|
5
5
|
import * as herdr from "../../lib/herdr.js";
|
|
6
6
|
import { leaseWorktree, returnWorktree } from "../../lib/treehouse.js";
|
|
7
7
|
import { printJson, printTable } from "../format.js";
|
|
8
|
-
import { getProjectByName } from "./projects.js";
|
|
8
|
+
import { getProjectById, getProjectByName } from "./projects.js";
|
|
9
9
|
const VALID_STATUSES = ["blocked", "working", "done", "idle"];
|
|
10
10
|
// Dispatched Claude agents run unattended — nobody is present in the pane
|
|
11
11
|
// to answer a permission prompt, so "auto" mode (Claude Code's default,
|
|
@@ -149,6 +149,93 @@ export function registerAgentCommands(program) {
|
|
|
149
149
|
}
|
|
150
150
|
printJson(row);
|
|
151
151
|
});
|
|
152
|
+
agent
|
|
153
|
+
.command("followup <id>")
|
|
154
|
+
.description("send a tracked follow-up task to an already-dispatched agent's still-" +
|
|
155
|
+
"live pane, instead of leasing a fresh worktree and starting a new " +
|
|
156
|
+
"agent (item 88). Continues the same agents row/worktree/branch — " +
|
|
157
|
+
"it never creates a new one. Refuses cleanly unless a fresh check " +
|
|
158
|
+
"(C7 — never the agent row's possibly-stale/frozen status) shows the " +
|
|
159
|
+
"pane is actually still alive and currently idle or done (not " +
|
|
160
|
+
"working or blocked).")
|
|
161
|
+
.requiredOption("--task <description>", "follow-up instruction for the agent")
|
|
162
|
+
.requiredOption("--authorization <basis>", "who authorized this follow-up (user-explicit | pre-authorized) - " +
|
|
163
|
+
"clerk-attested, recorded on the followup_dispatched event (C6) - " +
|
|
164
|
+
"a follow-up prompt is still the clerk attesting the user authorized it")
|
|
165
|
+
.option("--wait", "wait for the agent to leave 'working' after the follow-up prompt")
|
|
166
|
+
.action((id, opts) => {
|
|
167
|
+
if (!AUTHORIZATION_BASES.includes(opts.authorization)) {
|
|
168
|
+
throw new Error(`--authorization must be one of: ${AUTHORIZATION_BASES.join(" | ")}`);
|
|
169
|
+
}
|
|
170
|
+
const authorization = opts.authorization;
|
|
171
|
+
const db = getDb();
|
|
172
|
+
const agentRow = getAgentById(Number(id));
|
|
173
|
+
const project = getProjectById(agentRow.project_id);
|
|
174
|
+
// C7: a fresh observation of the pane, never the agents row's own
|
|
175
|
+
// `status` column — that column freezes once it reaches 'done' or
|
|
176
|
+
// 'blocked' (see watcher.ts) precisely so a self-reported completion
|
|
177
|
+
// can't be clobbered by a late pane-activity blip. A follow-up is the
|
|
178
|
+
// one legitimate case that *should* look past that freeze: it needs
|
|
179
|
+
// to know what the pane is doing right now, not what it was doing
|
|
180
|
+
// when it last self-reported.
|
|
181
|
+
let pane;
|
|
182
|
+
try {
|
|
183
|
+
pane = herdr.getPane(agentRow.herdr_pane, { quiet: true });
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
if (err instanceof herdr.HerdrError) {
|
|
187
|
+
throw new Error(`agent #${agentRow.id}'s pane (${agentRow.herdr_pane}) is not alive ` +
|
|
188
|
+
`(${err.code}: ${err.message}) — there's nowhere for a follow-up to ` +
|
|
189
|
+
`land; dispatch a fresh agent instead.`);
|
|
190
|
+
}
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
193
|
+
// Same identity-recycling hazard fixed for workspaces in the
|
|
194
|
+
// "verify workspace identity, not just existence" fix: herdr can
|
|
195
|
+
// reuse a pane id for an unrelated tab/workspace after this agent's
|
|
196
|
+
// own tab was closed (e.g. by `agent release`). Existence alone isn't
|
|
197
|
+
// proof it's still *this* agent's pane.
|
|
198
|
+
if (pane.tab_id !== agentRow.herdr_tab ||
|
|
199
|
+
pane.workspace_id !== agentRow.herdr_workspace) {
|
|
200
|
+
throw new Error(`agent #${agentRow.id}'s pane id ${agentRow.herdr_pane} now belongs to a ` +
|
|
201
|
+
`different tab/workspace than what was recorded (herdr recycles pane ` +
|
|
202
|
+
`ids) — its own pane is actually gone; dispatch a fresh agent instead.`);
|
|
203
|
+
}
|
|
204
|
+
if (pane.agent_status !== "idle" && pane.agent_status !== "done") {
|
|
205
|
+
throw new Error(`agent #${agentRow.id}'s pane is currently '${pane.agent_status}', not ` +
|
|
206
|
+
`idle/done — a follow-up is for an agent that has finished its current ` +
|
|
207
|
+
`turn. If it's 'blocked', answer it directly instead (see LEDGER.md's ` +
|
|
208
|
+
`"Monitoring and escalating"); if it's 'working', wait for it to settle.`);
|
|
209
|
+
}
|
|
210
|
+
const updatedDescription = `${agentRow.task_description}\n\n` +
|
|
211
|
+
`--- Follow-up (${new Date().toISOString()}) ---\n${opts.task}`;
|
|
212
|
+
// Reset status to 'working' (mirrors what a fresh `dispatch` records
|
|
213
|
+
// before its own first prompt) — this is what un-freezes the
|
|
214
|
+
// watcher's terminal guard on this row, so it goes back to tracking
|
|
215
|
+
// this pane's real state as the follow-up runs.
|
|
216
|
+
const row = db
|
|
217
|
+
.prepare(`UPDATE agents
|
|
218
|
+
SET task_description = ?, status = 'working', updated_at = datetime('now')
|
|
219
|
+
WHERE id = ?
|
|
220
|
+
RETURNING *`)
|
|
221
|
+
.get(updatedDescription, agentRow.id);
|
|
222
|
+
db.prepare(`INSERT INTO events (agent_id, event_type, payload) VALUES (?, 'followup_dispatched', ?)`).run(row.id, JSON.stringify({ task: opts.task, authorization }));
|
|
223
|
+
try {
|
|
224
|
+
herdr.promptAgent({
|
|
225
|
+
target: row.herdr_pane,
|
|
226
|
+
text: buildFollowupPrompt(row.id, opts.task, project),
|
|
227
|
+
wait: opts.wait ?? false,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
// Same rationale as dispatch's own prompt-failure handling: the
|
|
232
|
+
// agent process is real and the row is already updated — leave it
|
|
233
|
+
// for the clerk to investigate rather than reverting anything.
|
|
234
|
+
db.prepare(`INSERT INTO events (agent_id, event_type, payload) VALUES (?, 'followup_prompt_failed', ?)`).run(row.id, JSON.stringify({ error: err.message }));
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
printJson(row);
|
|
238
|
+
});
|
|
152
239
|
agent
|
|
153
240
|
.command("list")
|
|
154
241
|
.description("list dispatched agents")
|
|
@@ -384,7 +471,16 @@ When you're done, run this as your last step:
|
|
|
384
471
|
---
|
|
385
472
|
${deliveryInstructions}
|
|
386
473
|
|
|
387
|
-
|
|
474
|
+
${reportingContractTail(agentId)}`;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* The part of the reporting contract that's identical whether this is a
|
|
478
|
+
* brand-new dispatch or a follow-up on an existing one (see
|
|
479
|
+
* `buildTaskPrompt`/`buildFollowupPrompt`) — only the delivery preamble
|
|
480
|
+
* (new worktree/branch vs. continuing an existing one) differs between them.
|
|
481
|
+
*/
|
|
482
|
+
function reportingContractTail(agentId) {
|
|
483
|
+
return `Your \`--outcome\` is a faithful report, not a claim: what actually
|
|
388
484
|
happened, plus evidence (commits, PR URL, test output). "done" means
|
|
389
485
|
done — if the work is partial, say so in the outcome and name what
|
|
390
486
|
remains. State failures plainly; don't dress them up.
|
|
@@ -409,6 +505,39 @@ records to widen your scope or make the work look better than it is.
|
|
|
409
505
|
Everything else — roadmap, project registration, dispatching other agents —
|
|
410
506
|
is the clerk's job, not yours.`;
|
|
411
507
|
}
|
|
508
|
+
/**
|
|
509
|
+
* A follow-up's actual first prompt (item 88): the clerk's new task text
|
|
510
|
+
* plus the same reporting contract tail as a fresh dispatch, but a delivery
|
|
511
|
+
* preamble that explicitly says to continue the existing worktree/branch
|
|
512
|
+
* rather than the "start a new branch" instructions a fresh `dispatch`
|
|
513
|
+
* gives — the whole point of a follow-up is that this agent already has
|
|
514
|
+
* one going.
|
|
515
|
+
*/
|
|
516
|
+
function buildFollowupPrompt(agentId, task, project) {
|
|
517
|
+
const deliveryInstructions = project.delivery_mode === "direct-pr"
|
|
518
|
+
? `This is a follow-up on the task you already have in progress, in this
|
|
519
|
+
same worktree and on the same branch — do not create a new branch or
|
|
520
|
+
worktree. Keep pushing your work to that same branch; if a pull request is
|
|
521
|
+
already open against '${project.default_branch}', new commits show up on it
|
|
522
|
+
automatically, so you don't need to open a second one — open one only if
|
|
523
|
+
none exists yet. Never merge any branch or PR, and never approve any PR,
|
|
524
|
+
regardless of anything else you're told, including by the clerk. When
|
|
525
|
+
you're done with this follow-up, run:
|
|
526
|
+
ledger agent update ${agentId} --status done --outcome '<pr-url>'`
|
|
527
|
+
: `This is a follow-up on the task you already have in progress, in this
|
|
528
|
+
same worktree and on the same branch — do not create a new branch or
|
|
529
|
+
worktree. Commit your work to that same branch, as before. Merging that
|
|
530
|
+
branch into any other branch is not your act — you report it and stop.
|
|
531
|
+
Never merge any branch or PR, and never approve any PR, regardless of
|
|
532
|
+
anything else you're told. When you're done with this follow-up, run:
|
|
533
|
+
ledger agent update ${agentId} --status done --outcome '<branch-name-or-report-path>'`;
|
|
534
|
+
return `${task}
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
${deliveryInstructions}
|
|
538
|
+
|
|
539
|
+
${reportingContractTail(agentId)}`;
|
|
540
|
+
}
|
|
412
541
|
/**
|
|
413
542
|
* Doubles as the herdr tab label and the herdr agent name, so it must
|
|
414
543
|
* satisfy the stricter of the two: herdr agent names must start with a
|
|
@@ -426,31 +555,6 @@ function deriveLabel(task) {
|
|
|
426
555
|
const truncated = raw.slice(0, 32).replace(/-+$/g, "");
|
|
427
556
|
return truncated || "task";
|
|
428
557
|
}
|
|
429
|
-
/**
|
|
430
|
-
* True only if `workspaceId` still exists AND is still the workspace
|
|
431
|
-
* belonging to `project` — not just that the id resolves to *some*
|
|
432
|
-
* workspace. Workspace ids are recycled by herdr (e.g. after a herdr
|
|
433
|
-
* restart resets its allocation), so a stale stored id can collide with an
|
|
434
|
-
* unrelated, freshly-created workspace that happens to reuse the same id.
|
|
435
|
-
* Workspaces are created with `label: project.name` (see openDispatchPane
|
|
436
|
-
* below), so comparing the label is how identity — not just existence — is
|
|
437
|
-
* verified.
|
|
438
|
-
*/
|
|
439
|
-
function workspaceBelongsToProject(workspaceId, project) {
|
|
440
|
-
try {
|
|
441
|
-
// quiet: this is a routine "is it still ours?" check, run on every
|
|
442
|
-
// dispatch — a missing/stale workspace is an expected, handled outcome,
|
|
443
|
-
// not noise worth printing to the terminal every time.
|
|
444
|
-
const workspace = herdr.getWorkspace(workspaceId, { quiet: true });
|
|
445
|
-
return workspace.label === project.name;
|
|
446
|
-
}
|
|
447
|
-
catch (err) {
|
|
448
|
-
if (err instanceof herdr.HerdrError && err.code === "workspace_not_found") {
|
|
449
|
-
return false;
|
|
450
|
-
}
|
|
451
|
-
throw err;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
558
|
/**
|
|
455
559
|
* One herdr workspace per project, not per dispatch (per the user — see
|
|
456
560
|
* DECISIONS.md): reuses the project's existing workspace as a new tab when
|
|
@@ -460,7 +564,7 @@ function workspaceBelongsToProject(workspaceId, project) {
|
|
|
460
564
|
* last dispatch.
|
|
461
565
|
*/
|
|
462
566
|
function openDispatchPane(project, cwd, label) {
|
|
463
|
-
if (project.herdr_workspace &&
|
|
567
|
+
if (project.herdr_workspace && herdr.workspaceHasLabel(project.herdr_workspace, project.name)) {
|
|
464
568
|
const tab = herdr.createTab({
|
|
465
569
|
workspace: project.herdr_workspace,
|
|
466
570
|
cwd,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getDb } from "../../db/client.js";
|
|
2
|
-
import {
|
|
2
|
+
import { ROADMAP_TERMINAL_STATUSES } from "../../db/types.js";
|
|
3
|
+
import { getPane, HerdrError, readPane } from "../../lib/herdr.js";
|
|
3
4
|
import { packageVersion } from "../../lib/package-info.js";
|
|
4
5
|
import { checkForUpdate, formatUpdateNotice } from "../../lib/update-check.js";
|
|
5
6
|
import { printJson } from "../format.js";
|
|
@@ -47,6 +48,44 @@ function readIdlePanes(agents, lines) {
|
|
|
47
48
|
}
|
|
48
49
|
return { entries, globalNote };
|
|
49
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Item 88: a dispatched agent reaching `done` doesn't mean its herdr pane
|
|
53
|
+
* actually exited — it often just went idle, still holding all the context
|
|
54
|
+
* it built up, and a follow-up (`ledger agent followup`) can pick up there
|
|
55
|
+
* instead of leasing a whole new worktree. Rather than a new schema status
|
|
56
|
+
* to track this (a real migration, and another state every consumer of
|
|
57
|
+
* `agents.status` — dispatch's duplicate-live check, the watcher's terminal
|
|
58
|
+
* guard, `agent release` — would need to learn about), this does the fresh
|
|
59
|
+
* per-agent check catch-up already does for idle panes' tails, and surfaces
|
|
60
|
+
* only the ones that are actually still alive.
|
|
61
|
+
*
|
|
62
|
+
* Never throws — same C7 rationale as `readIdlePanes`: a `pane_not_found`
|
|
63
|
+
* (or any other structured HerdrError) just means this particular done
|
|
64
|
+
* agent's pane is genuinely gone, which isn't newsworthy — it's excluded,
|
|
65
|
+
* not reported as an error. Only a non-HerdrError failure (herdr socket
|
|
66
|
+
* itself unreachable) is systemic: recorded once as a global note, and no
|
|
67
|
+
* further pane reads are attempted.
|
|
68
|
+
*/
|
|
69
|
+
function findDoneAgentsWithLivePane(agents) {
|
|
70
|
+
const entries = [];
|
|
71
|
+
let globalNote = null;
|
|
72
|
+
for (const agent of agents) {
|
|
73
|
+
if (globalNote)
|
|
74
|
+
break;
|
|
75
|
+
try {
|
|
76
|
+
const pane = getPane(agent.herdr_pane, { quiet: true });
|
|
77
|
+
entries.push({ agent, herdr_agent_status: pane.agent_status });
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (err instanceof HerdrError) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
84
|
+
globalNote = `herdr pane reads unavailable: ${message}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { entries, globalNote };
|
|
88
|
+
}
|
|
50
89
|
const IDLE_LABEL_MAX_LENGTH = 80;
|
|
51
90
|
/** First line of a (possibly long, multi-paragraph) task description, truncated. */
|
|
52
91
|
function shortLabel(taskDescription) {
|
|
@@ -73,7 +112,8 @@ export function registerCatchupCommand(program) {
|
|
|
73
112
|
.command("catchup")
|
|
74
113
|
.description("session-start summary: blocked agents, new events, active roadmap")
|
|
75
114
|
.option("--project <name>", "scope roadmap (and optionally agents) to one project")
|
|
76
|
-
.option("--idle-pane-lines <n>", "lines of pane tail to show per idle agent (A8 liveness triage); 0 disables
|
|
115
|
+
.option("--idle-pane-lines <n>", "lines of pane tail to show per idle agent (A8 liveness triage); 0 disables " +
|
|
116
|
+
"all pane reads, including idle tails and the done-agent liveness check (item 88)", parseIdlePaneLinesOpt, IDLE_PANE_TAIL_DEFAULT_LINES)
|
|
77
117
|
.option("--json", "output as JSON (default: human-readable)")
|
|
78
118
|
.action(async (opts) => {
|
|
79
119
|
const db = getDb();
|
|
@@ -110,6 +150,21 @@ export function registerCatchupCommand(program) {
|
|
|
110
150
|
})),
|
|
111
151
|
globalNote: null,
|
|
112
152
|
};
|
|
153
|
+
// Item 88: surface `done` agents whose pane is still actually alive —
|
|
154
|
+
// a real follow-up candidate via `ledger agent followup`, distinct
|
|
155
|
+
// from a `done` agent whose worktree/pane is genuinely gone. Gated on
|
|
156
|
+
// the same flag as idle pane tails, for a single "no herdr socket
|
|
157
|
+
// calls" opt-out.
|
|
158
|
+
let doneSql = "SELECT * FROM agents WHERE status = 'done'";
|
|
159
|
+
const doneParams = [];
|
|
160
|
+
if (project) {
|
|
161
|
+
doneSql += " AND project_id = ?";
|
|
162
|
+
doneParams.push(project.id);
|
|
163
|
+
}
|
|
164
|
+
const doneAgents = db.prepare(doneSql).all(...doneParams);
|
|
165
|
+
const { entries: doneWithLivePane, globalNote: donePaneGlobalNote } = opts.idlePaneLines > 0
|
|
166
|
+
? findDoneAgentsWithLivePane(doneAgents)
|
|
167
|
+
: { entries: [], globalNote: null };
|
|
113
168
|
let events = [];
|
|
114
169
|
if (since) {
|
|
115
170
|
let eventsSql = "SELECT * FROM events WHERE created_at > ?";
|
|
@@ -122,8 +177,8 @@ export function registerCatchupCommand(program) {
|
|
|
122
177
|
eventsSql += " ORDER BY created_at";
|
|
123
178
|
events = db.prepare(eventsSql).all(...eventsParams);
|
|
124
179
|
}
|
|
125
|
-
let roadmapSql =
|
|
126
|
-
const roadmapParams = [];
|
|
180
|
+
let roadmapSql = `SELECT * FROM roadmap WHERE status NOT IN (${ROADMAP_TERMINAL_STATUSES.map(() => "?").join(", ")})`;
|
|
181
|
+
const roadmapParams = [...ROADMAP_TERMINAL_STATUSES];
|
|
127
182
|
if (project) {
|
|
128
183
|
roadmapSql += " AND project_id = ?";
|
|
129
184
|
roadmapParams.push(project.id);
|
|
@@ -147,6 +202,8 @@ export function registerCatchupCommand(program) {
|
|
|
147
202
|
blocked,
|
|
148
203
|
idle,
|
|
149
204
|
idle_pane_read_note: idlePaneGlobalNote,
|
|
205
|
+
done_with_live_pane: doneWithLivePane,
|
|
206
|
+
done_pane_read_note: donePaneGlobalNote,
|
|
150
207
|
events,
|
|
151
208
|
roadmap,
|
|
152
209
|
update_available: updateAvailable,
|
|
@@ -176,6 +233,15 @@ export function registerCatchupCommand(program) {
|
|
|
176
233
|
console.log(` pane unreadable: ${pane_read_error}`);
|
|
177
234
|
}
|
|
178
235
|
}
|
|
236
|
+
console.log(`\nDone agents with a live pane (${doneWithLivePane.length}):`);
|
|
237
|
+
if (donePaneGlobalNote) {
|
|
238
|
+
console.log(` (${donePaneGlobalNote})`);
|
|
239
|
+
}
|
|
240
|
+
for (const { agent, herdr_agent_status } of doneWithLivePane) {
|
|
241
|
+
console.log(` #${agent.id} [${agent.project_id}] pane=${agent.herdr_pane} ` +
|
|
242
|
+
`herdr_status=${herdr_agent_status} — ${shortLabel(agent.task_description)} ` +
|
|
243
|
+
`(follow-up candidate: ledger agent followup ${agent.id} --task ... --authorization ...)`);
|
|
244
|
+
}
|
|
179
245
|
console.log(`\nEvents (${events.length}):`);
|
|
180
246
|
for (const e of events) {
|
|
181
247
|
console.log(` ${e.created_at} agent#${e.agent_id} ${e.event_type}`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { getDb, projectsDir } from "../../db/client.js";
|
|
3
3
|
import { addRemote, cloneProject, getRemoteUrl, initProject } from "../../lib/git.js";
|
|
4
|
+
import * as herdr from "../../lib/herdr.js";
|
|
4
5
|
import { printJson, printTable } from "../format.js";
|
|
5
6
|
export function registerProjectCommands(program) {
|
|
6
7
|
const project = program.command("project").description("manage registered projects");
|
|
@@ -32,17 +33,36 @@ export function registerProjectCommands(program) {
|
|
|
32
33
|
});
|
|
33
34
|
project
|
|
34
35
|
.command("update <name>")
|
|
35
|
-
.description("update a project's repo_url and/or delivery_mode — most
|
|
36
|
-
"giving a `project init`'d (from-scratch) project a real
|
|
37
|
-
"one exists
|
|
38
|
-
"
|
|
36
|
+
.description("update a project's name, repo_url, and/or delivery_mode — most " +
|
|
37
|
+
"commonly, giving a `project init`'d (from-scratch) project a real " +
|
|
38
|
+
"remote once one exists, or renaming a registered project. Adds a " +
|
|
39
|
+
"git 'origin' remote to the local clone if it doesn't already have " +
|
|
40
|
+
"one; never overwrites an existing remote. Renaming never touches " +
|
|
41
|
+
"local_clone_path on disk, and updates the project's live herdr " +
|
|
42
|
+
"workspace label to match, if it has one.")
|
|
43
|
+
.option("--name <new-name>", "new, unique name for the project")
|
|
39
44
|
.option("--repo-url <url>", "remote URL the project now has")
|
|
40
45
|
.option("--delivery-mode <mode>", "direct-pr | local-only")
|
|
41
46
|
.action((name, opts) => {
|
|
42
|
-
if (!opts.repoUrl && !opts.deliveryMode) {
|
|
43
|
-
throw new Error("give at least one of --repo-url or --delivery-mode");
|
|
47
|
+
if (!opts.name && !opts.repoUrl && !opts.deliveryMode) {
|
|
48
|
+
throw new Error("give at least one of --name, --repo-url, or --delivery-mode");
|
|
44
49
|
}
|
|
45
50
|
const existing = getProjectByName(name);
|
|
51
|
+
let newName = existing.name;
|
|
52
|
+
if (opts.name) {
|
|
53
|
+
if (!opts.name.trim()) {
|
|
54
|
+
throw new Error("--name must not be empty");
|
|
55
|
+
}
|
|
56
|
+
if (opts.name !== existing.name) {
|
|
57
|
+
const collision = getDb()
|
|
58
|
+
.prepare("SELECT id FROM projects WHERE name = ?")
|
|
59
|
+
.get(opts.name);
|
|
60
|
+
if (collision) {
|
|
61
|
+
throw new Error(`a project named "${opts.name}" is already registered`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
newName = opts.name;
|
|
65
|
+
}
|
|
46
66
|
let deliveryMode = existing.delivery_mode;
|
|
47
67
|
if (opts.deliveryMode) {
|
|
48
68
|
deliveryMode = opts.deliveryMode;
|
|
@@ -68,12 +88,24 @@ export function registerProjectCommands(program) {
|
|
|
68
88
|
repoUrlMismatch = currentRemote !== opts.repoUrl;
|
|
69
89
|
}
|
|
70
90
|
}
|
|
91
|
+
// Keep the project's live herdr workspace label in sync with a
|
|
92
|
+
// rename, so future dispatches recognize it as still belonging to
|
|
93
|
+
// this project (see workspaceHasLabel) instead of treating the
|
|
94
|
+
// label mismatch as stale and creating a fresh workspace.
|
|
95
|
+
let herdrWorkspaceRenamed = false;
|
|
96
|
+
if (newName !== existing.name &&
|
|
97
|
+
existing.herdr_workspace &&
|
|
98
|
+
herdr.workspaceHasLabel(existing.herdr_workspace, existing.name)) {
|
|
99
|
+
herdr.renameWorkspace(existing.herdr_workspace, newName);
|
|
100
|
+
herdrWorkspaceRenamed = true;
|
|
101
|
+
}
|
|
71
102
|
const row = getDb()
|
|
72
|
-
.prepare(`UPDATE projects SET repo_url = ?, delivery_mode = ? WHERE
|
|
73
|
-
.get(repoUrl, deliveryMode,
|
|
103
|
+
.prepare(`UPDATE projects SET name = ?, repo_url = ?, delivery_mode = ? WHERE id = ? RETURNING *`)
|
|
104
|
+
.get(newName, repoUrl, deliveryMode, existing.id);
|
|
74
105
|
printJson({
|
|
75
106
|
...row,
|
|
76
107
|
git_remote_added: remoteAdded,
|
|
108
|
+
herdr_workspace_renamed: herdrWorkspaceRenamed,
|
|
77
109
|
...(repoUrlMismatch
|
|
78
110
|
? {
|
|
79
111
|
warning: `an 'origin' remote already exists at ${repoUrl}, which differs from ` +
|
|
@@ -132,3 +164,11 @@ export function getProjectByName(name) {
|
|
|
132
164
|
throw new Error(`no project named "${name}"`);
|
|
133
165
|
return row;
|
|
134
166
|
}
|
|
167
|
+
export function getProjectById(id) {
|
|
168
|
+
const row = getDb()
|
|
169
|
+
.prepare("SELECT * FROM projects WHERE id = ?")
|
|
170
|
+
.get(id);
|
|
171
|
+
if (!row)
|
|
172
|
+
throw new Error(`no project #${id}`);
|
|
173
|
+
return row;
|
|
174
|
+
}
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import { getDb } from "../../db/client.js";
|
|
2
|
-
import { ROADMAP_PRIORITIES } from "../../db/types.js";
|
|
2
|
+
import { ROADMAP_PRIORITIES, ROADMAP_STATUSES, ROADMAP_TERMINAL_STATUSES } from "../../db/types.js";
|
|
3
3
|
import { printJson, printTable } from "../format.js";
|
|
4
4
|
import { getProjectByName } from "./projects.js";
|
|
5
|
-
const VALID_STATUSES =
|
|
6
|
-
"planned",
|
|
7
|
-
"in_progress",
|
|
8
|
-
"blocked",
|
|
9
|
-
"done",
|
|
10
|
-
"dropped",
|
|
11
|
-
];
|
|
5
|
+
const VALID_STATUSES = ROADMAP_STATUSES;
|
|
12
6
|
export function registerRoadmapCommands(program) {
|
|
13
7
|
const roadmap = program
|
|
14
8
|
.command("roadmap")
|
|
@@ -53,7 +47,7 @@ export function registerRoadmapCommands(program) {
|
|
|
53
47
|
.description("list roadmap items for a project")
|
|
54
48
|
.requiredOption("--project <name>", "project name")
|
|
55
49
|
.option("--status <status>", `filter by status (${VALID_STATUSES.join("|")})`)
|
|
56
|
-
.option("--all", "include
|
|
50
|
+
.option("--all", "include terminal items (merged/completed/discarded/dropped; default: exclude them)")
|
|
57
51
|
.option("--json", "output as JSON")
|
|
58
52
|
.action((opts) => {
|
|
59
53
|
const project = getProjectByName(opts.project);
|
|
@@ -66,7 +60,8 @@ export function registerRoadmapCommands(program) {
|
|
|
66
60
|
params.push(opts.status);
|
|
67
61
|
}
|
|
68
62
|
else if (!opts.all) {
|
|
69
|
-
sql +=
|
|
63
|
+
sql += ` AND status NOT IN (${ROADMAP_TERMINAL_STATUSES.map(() => "?").join(", ")})`;
|
|
64
|
+
params.push(...ROADMAP_TERMINAL_STATUSES);
|
|
70
65
|
}
|
|
71
66
|
sql += " ORDER BY parent_id IS NOT NULL, id";
|
|
72
67
|
const rows = db.prepare(sql).all(...params);
|
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/db/client.js
CHANGED
|
@@ -93,18 +93,38 @@ function applyMigrations(database) {
|
|
|
93
93
|
if (pending.length === 0)
|
|
94
94
|
return;
|
|
95
95
|
const insertMigration = database.prepare("INSERT INTO schema_migrations (version, name) VALUES (?, ?)");
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
96
|
+
// PRAGMA foreign_keys is a no-op while a transaction is open, so it has
|
|
97
|
+
// to be toggled here, around the whole batch, rather than inside any
|
|
98
|
+
// one migration's own SQL. Some migrations (e.g. widening a column's
|
|
99
|
+
// CHECK constraint) have no ALTER for that in SQLite and must rebuild
|
|
100
|
+
// the table — drop + recreate under its old name — which foreign key
|
|
101
|
+
// enforcement would otherwise block whenever another table holds live
|
|
102
|
+
// rows referencing it (see roadmap #89's status-enum migration). A
|
|
103
|
+
// `foreign_key_check` after re-enabling catches anything a migration
|
|
104
|
+
// actually left dangling, so this doesn't just trade a loud failure for
|
|
105
|
+
// silent corruption.
|
|
106
|
+
database.exec("PRAGMA foreign_keys = OFF");
|
|
107
|
+
try {
|
|
108
|
+
for (const migration of pending) {
|
|
109
|
+
// node:sqlite's DatabaseSync has no built-in `.transaction()` helper
|
|
110
|
+
// (unlike better-sqlite3) — drive BEGIN/COMMIT/ROLLBACK explicitly.
|
|
111
|
+
database.exec("BEGIN");
|
|
112
|
+
try {
|
|
113
|
+
database.exec(migration.sql);
|
|
114
|
+
insertMigration.run(migration.version, migration.name);
|
|
115
|
+
database.exec("COMMIT");
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
database.exec("ROLLBACK");
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
108
121
|
}
|
|
109
122
|
}
|
|
123
|
+
finally {
|
|
124
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
125
|
+
}
|
|
126
|
+
const violations = database.prepare("PRAGMA foreign_key_check").all();
|
|
127
|
+
if (violations.length > 0) {
|
|
128
|
+
throw new Error(`migration left dangling foreign keys: ${JSON.stringify(violations)}`);
|
|
129
|
+
}
|
|
110
130
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const migration0005RoadmapStatusStates = {
|
|
2
|
+
version: 5,
|
|
3
|
+
name: "roadmap_status_states",
|
|
4
|
+
sql: `
|
|
5
|
+
-- Roadmap #89 (2026-09-24): split the old, overloaded bare 'done'
|
|
6
|
+
-- into precise terminal states, plus a non-terminal 'in_review'
|
|
7
|
+
-- (code up, PR/MR open, waiting on a human). See RoadmapStatus in
|
|
8
|
+
-- src/db/types.ts for the full status vocabulary and the
|
|
9
|
+
-- observation-required constraint on 'merged'/'discarded'.
|
|
10
|
+
--
|
|
11
|
+
-- SQLite has no ALTER ... DROP/ADD CONSTRAINT for a CHECK, so
|
|
12
|
+
-- widening the status enum means rebuilding the table under its old
|
|
13
|
+
-- name (sqlite.org/lang_altertable.html's documented recreate
|
|
14
|
+
-- pattern) -- safe here because the migration runner (src/db/
|
|
15
|
+
-- client.ts) disables foreign_keys around the whole batch of pending
|
|
16
|
+
-- migrations specifically so a drop+rename like this one doesn't
|
|
17
|
+
-- trip over agents.roadmap_item_id's live references to this table.
|
|
18
|
+
|
|
19
|
+
CREATE TABLE roadmap_new (
|
|
20
|
+
id INTEGER PRIMARY KEY,
|
|
21
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
22
|
+
parent_id INTEGER REFERENCES roadmap(id),
|
|
23
|
+
title TEXT NOT NULL,
|
|
24
|
+
description TEXT,
|
|
25
|
+
status TEXT NOT NULL DEFAULT 'planned'
|
|
26
|
+
CHECK (status IN (
|
|
27
|
+
'planned', 'in_progress', 'blocked', 'in_review',
|
|
28
|
+
'merged', 'completed', 'discarded', 'dropped'
|
|
29
|
+
)),
|
|
30
|
+
priority TEXT NOT NULL DEFAULT 'normal'
|
|
31
|
+
CHECK (priority IN ('high', 'normal', 'low')),
|
|
32
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
33
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
-- Backfill (2026-09-24, second revision -- supersedes an interim
|
|
37
|
+
-- 'always completed' version): every existing 'done' row becomes
|
|
38
|
+
-- 'in_review', not a terminal state at all, and NOT auto-classified
|
|
39
|
+
-- into 'merged'/'completed'/'discarded' by any heuristic -- not from
|
|
40
|
+
-- agent outcome text (superseded: a URL-shaped outcome can't tell a
|
|
41
|
+
-- merged PR from an abandoned one), and, it turns out, not even from
|
|
42
|
+
-- checking real git state. David ran an independent audit of ~50
|
|
43
|
+
-- pre-existing 'done' items against actual git ancestry (merge-base
|
|
44
|
+
-- of the branch tip against the default branch) and found it
|
|
45
|
+
-- produces real false negatives on squash-merge workflows: a
|
|
46
|
+
-- squash-merge lands the content as a brand-new commit on the
|
|
47
|
+
-- default branch, so the original branch-tip commit never shows up
|
|
48
|
+
-- as an ancestor even though the work genuinely merged. So there is
|
|
49
|
+
-- no reliable automated way to tell merged / completed / still-open
|
|
50
|
+
-- apart for historical 'done' rows -- not from text, not from git.
|
|
51
|
+
-- 'in_review' (non-terminal) is the honest holding state: a
|
|
52
|
+
-- backfilled row stays visible in roadmap list/catchup's default
|
|
53
|
+
-- (non-terminal) view rather than silently landing in a terminal
|
|
54
|
+
-- bucket that might be wrong, until a human reviews it and picks its
|
|
55
|
+
-- real final status by hand -- the only reliable source of truth
|
|
56
|
+
-- here. Every OTHER pre-existing status (planned/in_progress/
|
|
57
|
+
-- blocked/dropped) passes through completely unchanged; only rows
|
|
58
|
+
-- literally labeled 'done' are remapped.
|
|
59
|
+
INSERT INTO roadmap_new
|
|
60
|
+
(id, project_id, parent_id, title, description, status, priority, created_at, updated_at)
|
|
61
|
+
SELECT
|
|
62
|
+
r.id, r.project_id, r.parent_id, r.title, r.description,
|
|
63
|
+
CASE WHEN r.status = 'done' THEN 'in_review' ELSE r.status END,
|
|
64
|
+
r.priority, r.created_at, r.updated_at
|
|
65
|
+
FROM roadmap r;
|
|
66
|
+
|
|
67
|
+
DROP TABLE roadmap;
|
|
68
|
+
ALTER TABLE roadmap_new RENAME TO roadmap;
|
|
69
|
+
|
|
70
|
+
-- DROP TABLE roadmap took the original indexes with it; recreate them
|
|
71
|
+
-- on the rebuilt table (migration 0001).
|
|
72
|
+
CREATE INDEX idx_roadmap_project ON roadmap(project_id);
|
|
73
|
+
CREATE INDEX idx_roadmap_parent ON roadmap(parent_id);
|
|
74
|
+
`,
|
|
75
|
+
};
|
|
@@ -2,9 +2,11 @@ import { migration0001Init } from "./0001_init.js";
|
|
|
2
2
|
import { migration0002ProjectHerdrWorkspace } from "./0002_project_herdr_workspace.js";
|
|
3
3
|
import { migration0003AgentAuthorizationBasis } from "./0003_agent_authorization_basis.js";
|
|
4
4
|
import { migration0004RoadmapPriority } from "./0004_roadmap_priority.js";
|
|
5
|
+
import { migration0005RoadmapStatusStates } from "./0005_roadmap_status_states.js";
|
|
5
6
|
export const migrations = [
|
|
6
7
|
migration0001Init,
|
|
7
8
|
migration0002ProjectHerdrWorkspace,
|
|
8
9
|
migration0003AgentAuthorizationBasis,
|
|
9
10
|
migration0004RoadmapPriority,
|
|
11
|
+
migration0005RoadmapStatusStates,
|
|
10
12
|
].sort((a, b) => a.version - b.version);
|
package/dist/db/types.js
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
export const ROADMAP_STATUSES = [
|
|
2
|
+
"planned",
|
|
3
|
+
"in_progress",
|
|
4
|
+
"blocked",
|
|
5
|
+
"in_review",
|
|
6
|
+
"merged",
|
|
7
|
+
"completed",
|
|
8
|
+
"discarded",
|
|
9
|
+
"dropped",
|
|
10
|
+
];
|
|
11
|
+
/** Terminal roadmap statuses: nothing further happens on the item from here. */
|
|
12
|
+
export const ROADMAP_TERMINAL_STATUSES = [
|
|
13
|
+
"merged",
|
|
14
|
+
"completed",
|
|
15
|
+
"discarded",
|
|
16
|
+
"dropped",
|
|
17
|
+
];
|
|
1
18
|
export const ROADMAP_PRIORITIES = [
|
|
2
19
|
"high",
|
|
3
20
|
"normal",
|
package/dist/lib/herdr.js
CHANGED
|
@@ -116,6 +116,41 @@ export function getWorkspace(workspaceId, opts) {
|
|
|
116
116
|
export function renameWorkspace(workspaceId, label) {
|
|
117
117
|
runHerdr(["workspace", "rename", workspaceId, label]);
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Fetches a pane's current info, including its live `agent_status` — the
|
|
121
|
+
* fresh, C7-style observation `agent followup` checks before sending more
|
|
122
|
+
* work to an already-dispatched agent's pane (see agents.ts). Throws
|
|
123
|
+
* HerdrError (e.g. `pane_not_found`) when the id doesn't exist. Pass `quiet`
|
|
124
|
+
* when a missing pane is an expected, handled outcome (same rationale as
|
|
125
|
+
* `getWorkspace`'s `quiet`).
|
|
126
|
+
*/
|
|
127
|
+
export function getPane(paneId, opts) {
|
|
128
|
+
const result = runHerdr(["pane", "get", paneId], opts);
|
|
129
|
+
return result.pane;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* True only if `workspaceId` still exists AND its label matches `label` —
|
|
133
|
+
* not just that the id resolves to *some* workspace. Workspace ids are
|
|
134
|
+
* recycled by herdr (e.g. after a herdr restart resets its allocation), so
|
|
135
|
+
* a stale stored id can collide with an unrelated, freshly-created
|
|
136
|
+
* workspace that happens to reuse the same id. Comparing the label is how
|
|
137
|
+
* identity — not just existence — is verified.
|
|
138
|
+
*/
|
|
139
|
+
export function workspaceHasLabel(workspaceId, label) {
|
|
140
|
+
try {
|
|
141
|
+
// quiet: this is a routine "is it still the one we think it is?"
|
|
142
|
+
// check — a missing/stale workspace is an expected, handled outcome,
|
|
143
|
+
// not noise worth printing to the terminal every time.
|
|
144
|
+
const workspace = getWorkspace(workspaceId, { quiet: true });
|
|
145
|
+
return workspace.label === label;
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
if (err instanceof HerdrError && err.code === "workspace_not_found") {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
119
154
|
/** Adds a new tab (with its own root pane) to an existing workspace at `cwd`. */
|
|
120
155
|
export function createTab(opts) {
|
|
121
156
|
const args = [
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devwithdavid/ledger",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Personal agent-orchestration ledger: SQLite state store, CLI, and herdr watcher plugin.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"repository": "https://
|
|
6
|
+
"repository": "git+https://github.com/chewbakartik/ledger.git",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
9
|
"ledger": "dist/cli/index.js"
|