project-mgr 0.4.0 → 0.5.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1 - 2026-11-02
4
+
5
+ ### Added
6
+
7
+ - Export `openWork(db, project?)` and expose it through `pmgr next` / `pmgr status next`, combining open pins, unpinned Todos, and unconverted legacy steps.
8
+ - Add live `openWork` to CLI `current --json` and show open work with pin markers in text mode. Historical reads retain their previous shape.
9
+ - Add repeatable `add --done-todo` / `--cancel-todo` for atomic closures and `todo-convert` by entry/index, project, or all current legacy sources.
10
+
11
+ ### Changed
12
+
13
+ - **CLI behavior change:** plain `add` now keeps open pins; `--next` and `--next-todo` append (first-occurrence uid order). Use `--replace-next` to replace or clear pins without changing Todo status. Domain `addStatus` still defaults to replace; imports/exports stay unchanged. Fixed-uid re-pin of an already-carried pin remains a documented L2 limitation for direct API callers.
14
+ - Reuse prior open Todos for exact trimmed `--next` titles, including unassigned Todos already pinned on the project, preferring pinned work, then the oldest Todo, while preserving provenance. Duplicate new titles in one write still create separate Todos.
15
+ - Make `pmgr next --json` the daily checklist and guide agents to keep, finish, or explicitly retire work before adding new actions. Legacy carry preserves unconverted strings until explicit conversion. No automatic cancellation.
16
+ - CLI `current` builds open work in one scan and groups it per project instead of re-querying each project.
17
+ - Translate the new Todo/open-work section of `README_CN.md` into Chinese and document the L2 fixed-UID retry limitation in the architecture notes.
18
+
19
+ ## 0.5.0 - 2026-10-29
20
+
21
+ ### Added
22
+
23
+ - Store new snapshot next work as `nextTodoUids` references. `add --next` creates Todos and those refs in one transaction; `add --next-todo` pins existing UIDs.
24
+ - Reject mixed prose and refs, duplicate UIDs, combining `--next` with `--next-todo`, cross-project refs, and project changes that conflict with any historical reference.
25
+ - Export version 4 carries refs and every Todo they need, including unassigned Todos referenced by a project export.
26
+ - `todos --status open` lists open work. With `--project`, current snapshot pins are ordered first.
27
+ - Dashboard activity history and project cards show referenced Todos with live status.
28
+
29
+ ### Changed
30
+
31
+ - New snapshots no longer persist next-step prose. Legacy string `nextSteps` stay readable. Per-index conversion does not rewrite history; completion copies the full string list until every source index has a Todo, then materializes refs without creating Todos or changing their status.
32
+
33
+ ### Fixed
34
+
35
+ - Detach the dashboard browser launch so a long-lived browser does not keep the process alive.
36
+ - Reject dangling and cross-project `nextTodoUids` from `importStatuses`, and require imported refs to name Todos in the same payload. Entry-only imports declare no Todos, so they cannot reference existing rows.
37
+ - Treat a fixed-UID retry of `--next` text as the same write after it has been stored as refs. Portable imports still require an exact snapshot match.
38
+
3
39
  ## 0.4.0 - 2026-10-28
4
40
 
5
41
  ### Added
package/README.md CHANGED
@@ -225,7 +225,7 @@ pnpm start projects --query "mvp" --json
225
225
 
226
226
  ## Bulk import
227
227
 
228
- `import` accepts a JSON object, a JSON array, or a JSONL file containing one object per line. Every record is validated before the whole batch is written in one SQLite transaction; any failure rolls back the entire batch.
228
+ `import` accepts a JSON object, a JSON array, or a JSONL file containing one object per line. Every record is validated before the whole batch is written in one SQLite transaction; any failure rolls back the entire batch. The example below is a legacy entry: string `nextSteps` are kept and do not create Todos. New snapshots created with `add` store `nextTodoUids` instead.
229
229
 
230
230
  ```json
231
231
  {
@@ -336,22 +336,72 @@ Independently migrated copies of legacy data receive different UUIDs. Cross-impo
336
336
 
337
337
  ### Todos and dashboard actions
338
338
 
339
- New `add --next` values create independent Todos linked to the new activity.
340
- Historical next steps stay as text until explicitly converted in the dashboard.
341
- The Todos tab supports standalone creation, optional project assignment, source
342
- links, completion, cancellation, and reopening. Project cards filter this list.
343
- Activity cards provide **Mark completed**, recording a follow-up with the current
344
- project status; this neither completes the project nor its Todos.
339
+ Todos own actionable work. A new status snapshot stores ordered `nextTodoUids`
340
+ and does not persist next-step prose. CLI `add` now carries open pins by default;
341
+ plain `add` no longer clears them, and `--next` appends new work. An exact,
342
+ case-sensitive title (after trimming) reuses a prior open Todo in that project
343
+ or an unassigned Todo already pinned on it, preferring a current pin, then the oldest Todo. Provenance stays unchanged.
344
+ Duplicate new titles within one write still create separate Todos.
345
+ Use `--next-todo` to pin an existing UID explicitly.
346
+ Do not combine `--next` with `--next-todo`. Duplicate UIDs in one list are rejected.
345
347
 
346
348
  ```sh
347
- pmgr todo-add --title "Review results" --project demo --json
349
+ pmgr add --project demo --status active --summary "Review queued" \
350
+ --next "Collect logs" --next "Write the note" --source agent --json
348
351
  pmgr todo-add --title "Unassigned idea" --json
349
- pmgr todos --project demo --json
352
+ pmgr add --project demo --status active --summary "Pin the idea" \
353
+ --next-todo <todo-uuid> --source agent --json
354
+ pmgr next --project demo --json
350
355
  pmgr todo-update --uid <todo-uuid> --status completed --json
356
+ pmgr todo-update --uid <todo-uuid> --status cancelled --json
351
357
  ```
352
358
 
353
- Exports with Todos use version 3 and preserve their identities and state;
354
- version-2 and legacy imports remain supported without creating new Todos from
355
- old next-step text. Conflicting Todo snapshots are rejected atomically on import.
356
- Dashboard writes are restricted to same-origin localhost requests with JSON bodies;
357
- remote-host dashboards remain read-only.
359
+ The canonical daily checklist is `pmgr next --json` (also `pmgr status next`).
360
+ Do not combine `nextSteps`, `nextTodos`, and `todos` by hand. Before every `add`,
361
+ run `pmgr next --project P --json` and decide each item: keep it (do nothing;
362
+ existing pins carry), finish it with `--done-todo <uid>`, or retire superseded
363
+ work with `--cancel-todo <uid>`. Both close flags are repeatable and run in the
364
+ same transaction as the snapshot. Unpinned work remains visible in the backlog.
365
+ Use `--next` only for new actions, never status descriptions; put blockers in
366
+ `--blocker`. No Todo is automatically cancelled by age or similarity.
367
+
368
+ `--replace-next` restores replacement behavior: without next work it unpins
369
+ everything; with next work it pins only those UIDs. It never closes Todos.
370
+ The domain `addStatus` API still defaults to `nextMode: "replace"`; CLI `add`
371
+ passes `"carry"` unless `--replace-next` is present. Imports stay strict.
372
+
373
+ Legacy snapshots retain strings until explicitly converted. A plain carry-mode
374
+ `add` preserves the full legacy list and `nextStepsSourceUid` while any index is
375
+ unconverted; adding new next work then requires conversion or `--replace-next`.
376
+ Conversion is idempotent and appends no snapshot:
377
+
378
+ ```sh
379
+ pmgr todo-convert --entry <id-or-uid> --index 0 --json
380
+ pmgr todo-convert --project demo --json
381
+ # Back up before an explicit, user-run conversion of every current legacy source.
382
+ pmgr export --format json > backup.json
383
+ pmgr todo-convert --all --json
384
+ ```
385
+
386
+ Once all source indices have Todos, the next carry-mode `add` materializes refs
387
+ in index order, filtering out closed Todos. Historical snapshots stay unchanged.
388
+ `next` combines converted Todos and unconverted strings without duplicates.
389
+
390
+ `current --json` adds live `openWork` for each project's checklist, while its
391
+ stored `nextSteps`, `nextTodoUids`, and live `nextTodos` retain their previous
392
+ meaning, including closed pins. Text `current` prints open work with `[pinned]`
393
+ markers. `history`, `at`, and `getEntry` do not gain `openWork`; `history` text
394
+ and `completeActivity` follow-up copying stay unchanged. `at` returns stored
395
+ snapshots without live Todo resolution.
396
+
397
+ A snapshot may reference Todos that are unassigned or already in that project.
398
+ If a Todo appears in any `nextTodoUids`, or is linked to a source entry, its
399
+ project cannot be changed. Unpinning the current snapshot does not lift that lock.
400
+
401
+ Exports that contain refs use version 4 and include every referenced Todo.
402
+ Version 3 remains the Todo bundle without refs. Version 2 and legacy imports
403
+ do not invent open work. Dangling, duplicate, mixed, or cross-project refs fail
404
+ the whole import. Dashboard writes stay same-origin localhost JSON. Activity
405
+ cards still offer **Mark completed**, which appends a follow-up and does not
406
+ complete the project or its Todos. Adding a Todo from the dashboard does not
407
+ pin it; pinning requires a new snapshot.
package/README_CN.md CHANGED
@@ -223,7 +223,7 @@ pnpm start projects --query "mvp" --json
223
223
 
224
224
  ## 批量导入
225
225
 
226
- `import` 接受一个 JSON 对象、JSON 数组,或每行一个对象的 JSONL 文件。所有记录先校验,再在同一个 SQLite 事务中写入;任一记录失败时整批回滚。
226
+ `import` 接受一个 JSON 对象、JSON 数组,或每行一个对象的 JSONL 文件。所有记录先校验,再在同一个 SQLite 事务中写入;任一记录失败时整批回滚。下面的示例是旧格式条目:字符串 `nextSteps` 会保留,且不会创建待办。用 `add` 写入的新快照会改为存储 `nextTodoUids`。
227
227
 
228
228
  ```json
229
229
  {
@@ -334,20 +334,60 @@ pmgr revoke-completion --entry 12 --by 18 --reason "验收仍未完成" --source
334
334
 
335
335
  ### 待办与仪表板操作
336
336
 
337
- 新的 `add --next` 会创建独立待办,并关联到这条新动态。
338
- 历史中的下一步仍只是文本,直到在仪表板里显式转为待办。
339
- 待办页支持单独创建、可选关联项目、查看来源动态、标为已完成、取消和重新打开。项目卡片可以筛选这份列表。
340
- 动态卡片提供 **标为已完成**,会用当前项目状态记一条后续动态;这既不会把项目标成完成,也不会完成它的待办。
337
+ 待办是具体工作的来源。新的状态快照按顺序存储 `nextTodoUids`,不再把下一步原文写入快照。
338
+ CLI `add` 现在默认沿用仍处于 open 的钉住项:不带下一步的 `add` 不再清空它们,`--next` 会追加新工作。
339
+ 去掉首尾空白后,标题与该项目已有的 open 待办(或已钉在该项目上的未归属待办)完全一致(区分大小写)时,会复用那个待办:优先已钉住的,其次最早创建的。来源信息保持不变。
340
+ 同一次写入里重复的新标题仍会各自创建待办。要显式钉住已有 UID,请传 `--next-todo`。
341
+ 不要把 `--next` 和 `--next-todo` 写在同一次 `add` 里。同一列表里的重复 UID 会被拒绝。
341
342
 
342
343
  ```sh
343
- pmgr todo-add --title "复查结果" --project demo --json
344
+ pmgr add --project demo --status active --summary "已排队复查" \
345
+ --next "收集日志" --next "写说明" --source agent --json
344
346
  pmgr todo-add --title "未归属的想法" --json
345
- pmgr todos --project demo --json
347
+ pmgr add --project demo --status active --summary "钉住这个想法" \
348
+ --next-todo <todo-uuid> --source agent --json
349
+ pmgr next --project demo --json
346
350
  pmgr todo-update --uid <todo-uuid> --status completed --json
351
+ pmgr todo-update --uid <todo-uuid> --status cancelled --json
347
352
  ```
348
353
 
349
- 带待办的导出使用 version 3,并保留待办的身份与状态;
350
- version 2 和旧版导入仍然可用,且不会把旧的下一步文本自动变成新待办。
351
- 冲突的待办快照会在导入时整批拒绝。
352
- 仪表板写入仅接受同源的 localhost JSON 请求;
353
- 绑定到远程主机时,仪表板保持只读。
354
+ 每日清单统一使用 `pmgr next --json`(也可写作 `pmgr status next`),不要手动拼接 `nextSteps`、`nextTodos` 和 `todos`。
355
+ 每次 `add` 之前先运行 `pmgr next --project P --json`,逐条决定:保留(什么都不做,已钉住的会自动沿用)、用 `--done-todo <uid>` 完成,或用 `--cancel-todo <uid>` 退役被取代的工作。
356
+ 这两个关闭参数可重复,并与快照在同一事务中执行。未钉住的工作仍会作为积压项显示。
357
+ `--next` 只写新的行动,不写状态描述;阻塞写进 `--blocker`。系统不会按时间或相似度自动取消任何待办。
358
+
359
+ `--replace-next` 恢复替换语义:不带下一步时取消全部钉住,带下一步时只钉住这些 UID。它不会关闭待办。
360
+ 领域层 `addStatus` 默认仍是 `nextMode: "replace"`;CLI `add` 除非带 `--replace-next`,否则传 `"carry"`。导入仍保持严格校验。
361
+ 已知限制:领域 API 用固定 UID 重新钉住一个已沿用的待办时,原样重试会报 `conflicting entry uid`。CLI 不传固定 UID,`--external-ref` 重放不受影响。
362
+
363
+ 旧快照保留字符串 `nextSteps`,并省略 `nextTodoUids`,直到显式转换。
364
+ 只要还有来源下标未转换,沿用模式的 `add`(以及完成活动时的后续快照)会复制完整字符串列表和 `nextStepsSourceUid`;此时要写入新的下一步,需先转换或使用 `--replace-next`。
365
+ 转换是幂等的,不会追加快照:
366
+
367
+ ```sh
368
+ pmgr todo-convert --entry <id-or-uid> --index 0 --json
369
+ pmgr todo-convert --project demo --json
370
+ # 对所有当前旧快照做显式转换前,先备份;仅在用户要求时执行。
371
+ pmgr export --format json > backup.json
372
+ pmgr todo-convert --all --json
373
+ ```
374
+
375
+ 每个下标都已有待办之后,下一次沿用模式的 `add` 会按原下标顺序写入引用,并过滤掉已关闭的待办。历史快照保持不变。
376
+ `next` 会合并已转换的待办和未转换的字符串,不会重复。
377
+
378
+ `current --json` 为每个项目增加实时的 `openWork` 清单;存储的 `nextSteps`、`nextTodoUids` 以及实时的 `nextTodos` 含义不变,仍包含已关闭的钉住项。
379
+ 文本模式的 `current` 会列出未完成工作,并用 `[pinned]` 标出钉住项。
380
+ `history`、`at` 和 `getEntry` 不带 `openWork`;`history` 文本输出和完成活动的后续快照复制规则不变。
381
+ `at` 返回当时存储的快照,不附加实时待办状态。
382
+
383
+ 快照只能引用未归属的待办,或已经属于该项目的待办。
384
+ 待办一旦出现在任意 `nextTodoUids` 中,或关联了来源动态,就不能再改项目。
385
+ 从当前快照拿掉引用,也不会解除这个限制。
386
+
387
+ 带引用的导出使用 version 4,并包含被引用的待办。
388
+ 没有引用、但有待办的导出仍是 version 3。
389
+ version 2 和旧版导入不会凭空创建待办。
390
+ 悬空、重复、混用或跨项目引用会使整批导入失败。
391
+ 仪表板写入仍只接受同源的 localhost JSON 请求。
392
+ 动态卡片上的 **标为已完成** 会追加一条后续动态,既不完成项目,也不完成待办。
393
+ 在仪表板里新增待办不会把它钉进当前下一步;钉住需要再写一条快照。
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { readFileSync, realpathSync } from "node:fs";
3
3
  import { basename } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
- import { addStatus, createTodo, listTodos, updateTodo, at, current, history, importLedger, exportLedger, changeCompletion, listProjects, openLedger, parseTime, setProjectMetadata, startServer, ValidationError, } from "./index.js";
5
+ import { addStatus, createTodo, convertNextStep, getEntry, openWork, openWorkByProject, listTodos, updateTodo, at, current, history, importLedger, exportLedger, changeCompletion, listProjects, openLedger, parseTime, setProjectMetadata, startServer, ValidationError, } from "./index.js";
6
6
  const booleanFlags = new Set([
7
7
  "json",
8
8
  "clear-description",
@@ -11,8 +11,19 @@ const booleanFlags = new Set([
11
11
  "no-open",
12
12
  "help",
13
13
  "version",
14
+ "replace-next",
15
+ "all",
16
+ ]);
17
+ const repeats = new Set([
18
+ "next",
19
+ "next-todo",
20
+ "blocker",
21
+ "decision",
22
+ "tag",
23
+ "completes",
24
+ "done-todo",
25
+ "cancel-todo",
14
26
  ]);
15
- const repeats = new Set(["next", "blocker", "decision", "tag", "completes"]);
16
27
  function parse(argv) {
17
28
  const command = [];
18
29
  const flags = Object.create(null);
@@ -58,6 +69,10 @@ const allowedFlags = new Map([
58
69
  "progress",
59
70
  "blocker",
60
71
  "next",
72
+ "next-todo",
73
+ "replace-next",
74
+ "done-todo",
75
+ "cancel-todo",
61
76
  "decision",
62
77
  "tag",
63
78
  "at",
@@ -68,7 +83,9 @@ const allowedFlags = new Map([
68
83
  ]),
69
84
  ],
70
85
  ["todo-add", new Set(["title", "project", "entry", "json"])],
71
- ["todos", new Set(["project", "json"])],
86
+ ["todos", new Set(["project", "status", "json"])],
87
+ ["next", new Set(["project", "json"])],
88
+ ["todo-convert", new Set(["entry", "index", "project", "all", "json"])],
72
89
  ["todo-update", new Set(["uid", "status", "project", "json"])],
73
90
  ["complete", new Set(["entry", "by", "reason", "source", "json"])],
74
91
  ["revoke-completion", new Set(["entry", "by", "reason", "source", "json"])],
@@ -97,18 +114,26 @@ const allowedFlags = new Map([
97
114
  ["serve", new Set(["port", "host", "open", "no-open", "json"])],
98
115
  ]);
99
116
  const commandHelp = {
117
+ next: ["List all open work, pinned first", "[--project <name>] [--json]"],
118
+ "todo-convert": [
119
+ "Convert legacy next steps without appending a snapshot",
120
+ "--entry <id-or-uid> --index <n> | --project <name> | --all [--json]",
121
+ ],
100
122
  "todo-add": [
101
123
  "Create an independent or linked Todo",
102
124
  "--title <text> [--project <name>] [--entry <uid>] [--json]",
103
125
  ],
104
- todos: ["List Todos", "[--project <name>] [--json]"],
126
+ todos: [
127
+ "List Todos",
128
+ "[--project <name>] [--status open|completed|cancelled] [--json]",
129
+ ],
105
130
  "todo-update": [
106
131
  "Change Todo status or assign a project",
107
132
  "--uid <uuid> [--status open|completed|cancelled] [--project <name>] [--json]",
108
133
  ],
109
134
  add: [
110
135
  "Append a status snapshot",
111
- "--project <name> --summary <text> [options]",
136
+ "--project <name> --summary <text> [--next <text> | --next-todo <uid>] [--replace-next] [--done-todo <uid>] [--cancel-todo <uid>] [options]",
112
137
  ],
113
138
  complete: [
114
139
  "Link a completed activity to its follow-up",
@@ -182,7 +207,7 @@ function validateInvocation(command, flags) {
182
207
  const action = command[statusCommand ? 1 : 0];
183
208
  const allowed = typeof action === "string" ? allowedFlags.get(action) : undefined;
184
209
  if (typeof action !== "string" || !allowed)
185
- throw new ValidationError("usage: pmgr <add|complete|revoke-completion|import|current|projects|list|set-project|history|at|export|serve> (or: pmgr status <action>)");
210
+ throw new ValidationError("usage: pmgr <add|next|todo-add|todos|todo-update|todo-convert|complete|revoke-completion|import|current|projects|list|set-project|history|at|export|serve> (or: pmgr status <action>)");
186
211
  const positional = command.slice(statusCommand ? 2 : 1);
187
212
  if (action === "import") {
188
213
  if (positional.length > 1)
@@ -221,7 +246,7 @@ function output(value, json, write) {
221
246
  write(`${JSON.stringify(value)}\n`);
222
247
  else if (Array.isArray(value)) {
223
248
  const rows = value
224
- .map((entry) => `${entry.project}\t${entry.effectiveStatus ?? entry.status}\t${entry.occurredAt}\t${entry.summary}${entry.completedBy ? ` (completed by #${entry.completedBy.id}; original: ${entry.status})` : ""}`)
249
+ .map((entry) => `${entry.project}\t${entry.effectiveStatus ?? entry.status}\t${entry.occurredAt}\t${entry.summary}${entry.completedBy ? ` (completed by #${entry.completedBy.id}; original: ${entry.status})` : ""}${entry.openWork ? entry.openWork.map((item) => `\n - ${item.title}${item.pinned ? " [pinned]" : ""}`).join("") : ""}`)
225
250
  .join("\n");
226
251
  write(`${rows || "No entries found."}\n`);
227
252
  }
@@ -301,6 +326,50 @@ export function run(argv, write = process.stdout.write.bind(process.stdout)) {
301
326
  const json = flags.json === true;
302
327
  try {
303
328
  switch (action) {
329
+ case "next": {
330
+ const items = openWork(db, stringFlag(flags, "project"));
331
+ if (json)
332
+ output(items, true, write);
333
+ else
334
+ write(items.length
335
+ ? items
336
+ .map((item) => `${item.project ?? "-"}\t${item.kind === "todo" ? item.uid : `${item.entryUid}#${item.stepIndex}`}\t${item.pinned ? "pinned" : "-"}\t${item.title}`)
337
+ .join("\n") + "\n"
338
+ : "No open work.\n");
339
+ break;
340
+ }
341
+ case "todo-convert": {
342
+ const ref = stringFlag(flags, "entry");
343
+ const project = stringFlag(flags, "project");
344
+ if (Number(ref !== undefined) +
345
+ Number(project !== undefined) +
346
+ Number(flags.all === true) !==
347
+ 1)
348
+ throw new ValidationError("use exactly one of --entry, --project, or --all");
349
+ if ((ref === undefined) !== (flags.index === undefined))
350
+ throw new ValidationError("--entry requires --index; --index requires --entry");
351
+ const todos = db
352
+ .transaction(() => {
353
+ if (ref !== undefined)
354
+ return [convertNextStep(db, ref, numberFlag(flags, "index", 0))];
355
+ return current(db, project).flatMap((entry) => {
356
+ if (entry.nextTodoUids !== undefined)
357
+ return [];
358
+ const source = entry.nextStepsSourceUid
359
+ ? getEntry(db, entry.nextStepsSourceUid)
360
+ : entry;
361
+ return source.nextSteps.map((_, index) => convertNextStep(db, source.uid, index));
362
+ });
363
+ })
364
+ .immediate();
365
+ if (json)
366
+ output(todos, true, write);
367
+ else
368
+ write(todos
369
+ .map((todo) => `${todo.uid}\t${todo.status}\t${todo.project}\t${todo.title}`)
370
+ .join("\n") + "\n");
371
+ break;
372
+ }
304
373
  case "todo-add":
305
374
  output(createTodo(db, {
306
375
  title: stringFlag(flags, "title", true),
@@ -309,7 +378,15 @@ export function run(argv, write = process.stdout.write.bind(process.stdout)) {
309
378
  }), json, write);
310
379
  break;
311
380
  case "todos": {
312
- const todos = listTodos(db, stringFlag(flags, "project"));
381
+ const status = stringFlag(flags, "status");
382
+ if (status !== undefined &&
383
+ status !== "open" &&
384
+ status !== "completed" &&
385
+ status !== "cancelled")
386
+ throw new ValidationError("--status must be open, completed, or cancelled");
387
+ const todos = listTodos(db, stringFlag(flags, "project"), status === undefined
388
+ ? {}
389
+ : { status: status });
313
390
  if (json)
314
391
  output(todos, true, write);
315
392
  else
@@ -328,6 +405,10 @@ export function run(argv, write = process.stdout.write.bind(process.stdout)) {
328
405
  const project = stringFlag(flags, "project", true);
329
406
  const description = stringFlag(flags, "description");
330
407
  const notes = stringFlag(flags, "notes");
408
+ const next = listFlag(flags, "next");
409
+ const nextTodo = listFlag(flags, "next-todo");
410
+ if (next.length > 0 && nextTodo.length > 0)
411
+ throw new ValidationError("cannot combine --next and --next-todo");
331
412
  const result = db
332
413
  .transaction(() => {
333
414
  if (description !== undefined || notes !== undefined) {
@@ -339,7 +420,19 @@ export function run(argv, write = process.stdout.write.bind(process.stdout)) {
339
420
  summary: stringFlag(flags, "summary", true),
340
421
  progress: stringFlag(flags, "progress"),
341
422
  blockers: listFlag(flags, "blocker"),
342
- nextSteps: listFlag(flags, "next"),
423
+ nextSteps: next,
424
+ nextMode: flags["replace-next"] === true ? "replace" : "carry",
425
+ closeTodos: [
426
+ ...listFlag(flags, "done-todo").map((uid) => ({
427
+ uid,
428
+ status: "completed",
429
+ })),
430
+ ...listFlag(flags, "cancel-todo").map((uid) => ({
431
+ uid,
432
+ status: "cancelled",
433
+ })),
434
+ ],
435
+ ...(nextTodo.length > 0 ? { nextTodoUids: nextTodo } : {}),
343
436
  decisions: listFlag(flags, "decision"),
344
437
  tags: listFlag(flags, "tag"),
345
438
  occurredAt: stringFlag(flags, "at"),
@@ -360,9 +453,16 @@ export function run(argv, write = process.stdout.write.bind(process.stdout)) {
360
453
  command[command[0] === "status" ? 2 : 1] ??
361
454
  "")).map((result) => result.entry), json, write);
362
455
  break;
363
- case "current":
364
- output(current(db, stringFlag(flags, "project")), json, write);
456
+ case "current": {
457
+ const project = stringFlag(flags, "project");
458
+ // One open-work scan for all projects, grouped per project.
459
+ const { groups } = openWorkByProject(db, project);
460
+ output(current(db, project).map((entry) => ({
461
+ ...entry,
462
+ openWork: groups.get(entry.project),
463
+ })), json, write);
365
464
  break;
465
+ }
366
466
  case "projects":
367
467
  case "list":
368
468
  outputProjects(listProjects(db, { query: stringFlag(flags, "query") }), json, write);
package/dist/db.d.ts CHANGED
@@ -1,10 +1,35 @@
1
1
  import Database from "better-sqlite3";
2
- import { type ProjectMetadata, type ProjectOverview, type Source, type StatusEntry } from "./domain.js";
2
+ import { type ProjectMetadata, type ProjectOverview, type Source, type StatusEntry, type StatusEntryInput } from "./domain.js";
3
3
  export type Ledger = Database.Database;
4
4
  export type InsertResult = {
5
5
  entry: StatusEntry;
6
6
  inserted: boolean;
7
7
  };
8
+ export type AddStatusInput = StatusEntryInput & {
9
+ nextMode?: "carry" | "replace";
10
+ closeTodos?: {
11
+ uid: string;
12
+ status: "completed" | "cancelled";
13
+ }[];
14
+ };
15
+ export type OpenWorkItem = {
16
+ kind: "todo";
17
+ uid: string;
18
+ title: string;
19
+ project: string | null;
20
+ pinned: boolean;
21
+ pinIndex: number | null;
22
+ createdAt: string;
23
+ entryUid: string | null;
24
+ } | {
25
+ kind: "legacy-step";
26
+ project: string;
27
+ entryUid: string;
28
+ stepIndex: number;
29
+ title: string;
30
+ pinned: true;
31
+ pinIndex: number;
32
+ };
8
33
  export type HistoryOptions = {
9
34
  since?: string;
10
35
  until?: string;
@@ -20,6 +45,12 @@ export declare function listProjects(db: Ledger, options?: {
20
45
  export declare function addStatus(db: Ledger, value: unknown, defaultSource?: Source, completes?: string[]): InsertResult;
21
46
  export declare function importStatuses(db: Ledger, values: unknown[]): InsertResult[];
22
47
  export declare function current(db: Ledger, project?: string): StatusEntry[];
48
+ /** Per-project open work from one current() and one Todo scan; no cross-project dedupe. */
49
+ export declare function openWorkByProject(db: Ledger, project?: string): {
50
+ groups: Map<string, OpenWorkItem[]>;
51
+ unassigned: Todo[];
52
+ };
53
+ export declare function openWork(db: Ledger, project?: string): OpenWorkItem[];
23
54
  export declare function history(db: Ledger, project?: string, options?: HistoryOptions): StatusEntry[];
24
55
  export declare function at(db: Ledger, project: string, occurredAt: string): StatusEntry | undefined;
25
56
  export declare function countEntries(db: Ledger, project?: string): number;
@@ -37,12 +68,13 @@ export declare function getEntry(db: Ledger, ref: string): StatusEntry | undefin
37
68
  export declare function changeCompletion(db: Ledger, targetRef: string, byRef: string, action?: CompletionEvent["action"], reason?: string, source?: Source): CompletionEvent;
38
69
  export declare function exportLedger(db: Ledger, project?: string): {
39
70
  todos?: Todo[] | undefined;
40
- version: number;
71
+ version: 3 | 2 | 4;
41
72
  entries: {
42
73
  recordedAt: string;
43
74
  progress?: string | undefined;
44
75
  externalRef?: string | undefined;
45
76
  nextStepsSourceUid?: string | undefined;
77
+ nextTodoUids?: string[] | undefined;
46
78
  uid: string;
47
79
  project: string;
48
80
  status: import("./domain.js").ProjectStatus;
@@ -67,7 +99,9 @@ export type Todo = {
67
99
  createdAt: string;
68
100
  completedAt: string | null;
69
101
  };
70
- export declare function listTodos(db: Ledger, project?: string): Todo[];
102
+ export declare function listTodos(db: Ledger, project?: string, options?: {
103
+ status?: Todo["status"];
104
+ }): Todo[];
71
105
  export declare function createTodo(db: Ledger, value: unknown): Todo;
72
106
  export declare function convertNextStep(db: Ledger, ref: string, index: number): Todo;
73
107
  export declare function updateTodo(db: Ledger, uid: string, value: unknown): Todo;