@getpipher/armory-todo 0.5.2 → 0.5.3
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 +2 -2
- package/docs/superpowers/plans/2026-07-21-caps-release.md +4 -4
- package/docs/superpowers/plans/2026-07-21-project-scope-management.md +22 -22
- package/docs/superpowers/plans/2026-07-21-title-notes-split.md +1 -1
- package/docs/superpowers/specs/2026-07-21-caps-release-design.md +2 -2
- package/docs/superpowers/specs/2026-07-21-project-scope-management-design.md +8 -8
- package/extensions/todo.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,7 +105,7 @@ irreversible escape hatch, always user-confirmed.
|
|
|
105
105
|
|
|
106
106
|
## Project-scope management (v0.4.0)
|
|
107
107
|
|
|
108
|
-
A project registry (`~/.pi/agent/todo/projects.json`, lazy-synced on read) tracks known projects + an advisory per-project **`maxOpen`** cap slot. The `todo` tool gains two actions: **`projects`** (per-project scope overview — open/in_progress/parked/done counts + `maxOpen` + `OVER`/`?typo` markers + last-updated) and **`project_rename`** (rename or merge a project; rewrites live + archive + registry — the typo-cleanup path, e.g. `
|
|
108
|
+
A project registry (`~/.pi/agent/todo/projects.json`, lazy-synced on read) tracks known projects + an advisory per-project **`maxOpen`** cap slot. The `todo` tool gains two actions: **`projects`** (per-project scope overview — open/in_progress/parked/done counts + `maxOpen` + `OVER`/`?typo` markers + last-updated) and **`project_rename`** (rename or merge a project; rewrites live + archive + registry — the typo-cleanup path, e.g. `foo-bat` → `foo-bar`).
|
|
109
109
|
|
|
110
110
|
`health` gains four per-project flags: **`PROJECT_OVER`** (open > a project's `maxOpen` slot), **`PROJECT_LARGE`** (open > `health.perProjectDefaultMax`, default 8 — fires out-of-the-box, no per-project config needed), **`PROJECT_STALE`** (project untouched > `activeStaleDays`), **`PROJECT_TYPO`** (1-todo project with a near-named sibling, Levenshtein ≤ 2). The `/todo health` report + the `todo` `health` action both show a `projects:` section.
|
|
111
111
|
|
|
@@ -115,7 +115,7 @@ The interactive `/todo` panel gains a 6th tab — **Projects** — listing the o
|
|
|
115
115
|
|
|
116
116
|
## Caps enforcement (v0.5.0)
|
|
117
117
|
|
|
118
|
-
Three caps keep the store (and its auto-injected prompt block) from bloating silently — the forcing-function half of [issue #1](https://github.com/
|
|
118
|
+
Three caps keep the store (and its auto-injected prompt block) from bloating silently — the forcing-function half of [issue #1](https://github.com/getpipher/armory-todo/issues/1):
|
|
119
119
|
|
|
120
120
|
1. **Count cap (per-project `maxOpen`, enforced).** A project's `maxOpen` slot (set via the Projects tab → Set maxOpen, or `setProjectMaxOpen`) **blocks `add`** when the project is at its cap, and **blocks a project-move** of an `open`/`in_progress` todo into a capped project. The cap is on the `open` count (matches the `PROJECT_OVER` health flag); `in_progress` doesn't count. Un-park (`parked→open`) is intentionally **not** blocked — reactivating deferred work isn't adding new work. The block message tells you how to raise/clear the cap. `maxOpen: null` (default) = uncapped.
|
|
121
121
|
|
|
@@ -178,7 +178,7 @@ notThrows("zero open under cap ok", () => checkProjectCap({ project: "pi", curre
|
|
|
178
178
|
throws("at-cap add throws (currentOpen == maxOpen)", () => checkProjectCap({ project: "pi", currentOpen: 8, maxOpen: 8 }));
|
|
179
179
|
throws("over-cap add throws", () => checkProjectCap({ project: "pi", currentOpen: 12, maxOpen: 8 }));
|
|
180
180
|
throws("project cap message has raise hint", () => checkProjectCap({ project: "pi", currentOpen: 8, maxOpen: 8 }), "raise maxOpen");
|
|
181
|
-
throws("project cap message has project name", () => checkProjectCap({ project: "
|
|
181
|
+
throws("project cap message has project name", () => checkProjectCap({ project: "foo-bar", currentOpen: 8, maxOpen: 8 }), "foo-bar");
|
|
182
182
|
// maxOpen 0 = no open todos allowed
|
|
183
183
|
throws("maxOpen 0 + any add throws", () => checkProjectCap({ project: "pi", currentOpen: 0, maxOpen: 0 }));
|
|
184
184
|
|
|
@@ -953,13 +953,13 @@ with:
|
|
|
953
953
|
Replace:
|
|
954
954
|
|
|
955
955
|
```ts
|
|
956
|
-
"Use todo (action:'project_rename', oldName, newName) to rename or merge a project (rewrites live + archive + registry). Use it to fix typo'd project strings (e.g.
|
|
956
|
+
"Use todo (action:'project_rename', oldName, newName) to rename or merge a project (rewrites live + archive + registry). Use it to fix typo'd project strings (e.g. foo-bat → foo-bar). Rename onto an existing name merges (consolidates the old project into the new). Advisory maxOpen caps are NOT enforced in v0.4.0 — they only drive a health flag; enforcement lands in v0.5.0.",
|
|
957
957
|
```
|
|
958
958
|
|
|
959
959
|
with:
|
|
960
960
|
|
|
961
961
|
```ts
|
|
962
|
-
"Use todo (action:'project_rename', oldName, newName) to rename or merge a project (rewrites live + archive + registry). Use it to fix typo'd project strings (e.g.
|
|
962
|
+
"Use todo (action:'project_rename', oldName, newName) to rename or merge a project (rewrites live + archive + registry). Use it to fix typo'd project strings (e.g. foo-bat → foo-bar). Rename onto an existing name merges (consolidates the old project into the new). Per-project maxOpen caps are ENFORCED (block-on-add); they also drive a PROJECT_OVER health flag when breached.",
|
|
963
963
|
```
|
|
964
964
|
|
|
965
965
|
- [ ] **Step 3: Verify syntax**
|
|
@@ -1036,7 +1036,7 @@ Insert a new section immediately after the "Project-scope management (v0.4.0)" s
|
|
|
1036
1036
|
```md
|
|
1037
1037
|
## Caps enforcement (v0.5.0)
|
|
1038
1038
|
|
|
1039
|
-
Three caps keep the store (and its auto-injected prompt block) from bloating silently — the forcing-function half of [issue #1](https://github.com/
|
|
1039
|
+
Three caps keep the store (and its auto-injected prompt block) from bloating silently — the forcing-function half of [issue #1](https://github.com/getpipher/armory-todo/issues/1):
|
|
1040
1040
|
|
|
1041
1041
|
1. **Count cap (per-project `maxOpen`, enforced).** A project's `maxOpen` slot (set via the Projects tab → Set maxOpen, or `setProjectMaxOpen`) **blocks `add`** when the project is at its cap, and **blocks a project-move** of an `open`/`in_progress` todo into a capped project. The cap is on the `open` count (matches the `PROJECT_OVER` health flag); `in_progress` doesn't count. Un-park (`parked→open`) is intentionally **not** blocked — reactivating deferred work isn't adding new work. The block message tells you how to raise/clear the cap. `maxOpen: null` (default) = uncapped.
|
|
1042
1042
|
|
|
@@ -301,7 +301,7 @@ const mode = statSync(join(tmp, "projects.json")).mode & 0o777;
|
|
|
301
301
|
ok("registry file mode 0600", mode === 0o600, `(mode ${mode.toString(8)})`);
|
|
302
302
|
|
|
303
303
|
// --- reconcileRegistry: appends unknown from live + archive, idempotent ---
|
|
304
|
-
const a = addTodo({ title: "T1", project: "
|
|
304
|
+
const a = addTodo({ title: "T1", project: "foo-bar" });
|
|
305
305
|
const arch0: ArchiveStore = { version: 3, updatedAt: "x", todos: [{ ...a, project: "bug-bounty", status: "done", closedAt: "x" }] };
|
|
306
306
|
saveArchive(arch0);
|
|
307
307
|
let reg = loadRegistry();
|
|
@@ -316,15 +316,15 @@ const res3 = reconcileRegistry(res1.reg, listTodos({ status: "all", limit: 200 }
|
|
|
316
316
|
ok("reconcile no (no project) entry", !res3.reg.projects.some((p) => p.name === ""));
|
|
317
317
|
|
|
318
318
|
// --- getProjectEntry hit/miss ---
|
|
319
|
-
ok("getProjectEntry hit", getProjectEntry(res3.reg, "
|
|
319
|
+
ok("getProjectEntry hit", getProjectEntry(res3.reg, "foo-bar") !== undefined);
|
|
320
320
|
ok("getProjectEntry miss", getProjectEntry(res3.reg, "nope") === undefined);
|
|
321
321
|
|
|
322
322
|
// --- setProjectMaxOpen: create-if-unknown, set number, null clears ---
|
|
323
|
-
const e1 = setProjectMaxOpen(res3.reg, "
|
|
323
|
+
const e1 = setProjectMaxOpen(res3.reg, "foo-bar", 8);
|
|
324
324
|
eq("setMaxOpen sets 8", e1.maxOpen, 8);
|
|
325
325
|
const e2 = setProjectMaxOpen(res3.reg, "brand-new", 3);
|
|
326
326
|
ok("setMaxOpen creates unknown", getProjectEntry(res3.reg, "brand-new") !== undefined && e2.maxOpen === 3);
|
|
327
|
-
const e3 = setProjectMaxOpen(res3.reg, "
|
|
327
|
+
const e3 = setProjectMaxOpen(res3.reg, "foo-bar", null);
|
|
328
328
|
eq("setMaxOpen null clears", e3.maxOpen, null);
|
|
329
329
|
let threw = false;
|
|
330
330
|
try { setProjectMaxOpen(res3.reg, "", 5); } catch { threw = true; }
|
|
@@ -336,23 +336,23 @@ ok("setMaxOpen negative throws", threw2);
|
|
|
336
336
|
// --- renameProject: rewrites live + archive + registry; merge; self no-op ---
|
|
337
337
|
// (fresh isolated registry for rename tests)
|
|
338
338
|
rmSync(join(tmp, "projects.json"), { force: true });
|
|
339
|
-
addTodo({ title: "R1", project: "
|
|
340
|
-
addTodo({ title: "R2", project: "
|
|
341
|
-
const arch1: ArchiveStore = { version: 3, updatedAt: "x", todos: [{ ...a, project: "
|
|
339
|
+
addTodo({ title: "R1", project: "foo-bat" });
|
|
340
|
+
addTodo({ title: "R2", project: "foo-bat" });
|
|
341
|
+
const arch1: ArchiveStore = { version: 3, updatedAt: "x", todos: [{ ...a, project: "foo-bat", status: "done", closedAt: "x" }] };
|
|
342
342
|
saveArchive(arch1);
|
|
343
343
|
let regR = loadRegistry();
|
|
344
344
|
reconcileRegistry(regR, listTodos({ status: "all", limit: 200 }), loadArchiveAsync().todos);
|
|
345
345
|
saveRegistry(regR);
|
|
346
|
-
// merge:
|
|
347
|
-
const rr = renameProject("
|
|
346
|
+
// merge: foo-bat → foo-bar (foo-bar already exists from earlier? no — fresh. so create)
|
|
347
|
+
const rr = renameProject("foo-bat", "foo-bar");
|
|
348
348
|
eq("rename liveRenamed", rr.liveRenamed, 2);
|
|
349
349
|
eq("rename archivedRenamed", rr.archivedRenamed, 1);
|
|
350
350
|
ok("rename to-new → merged=false", !rr.merged);
|
|
351
|
-
ok("rename removed old entry", getProjectEntry(loadRegistry(), "
|
|
352
|
-
ok("rename created new entry", getProjectEntry(loadRegistry(), "
|
|
351
|
+
ok("rename removed old entry", getProjectEntry(loadRegistry(), "foo-bat") === undefined);
|
|
352
|
+
ok("rename created new entry", getProjectEntry(loadRegistry(), "foo-bar") !== undefined);
|
|
353
353
|
|
|
354
354
|
// self-rename no-op
|
|
355
|
-
const self = renameProject("
|
|
355
|
+
const self = renameProject("foo-bar", "foo-bar");
|
|
356
356
|
eq("self-rename liveRenamed 0", self.liveRenamed, 0);
|
|
357
357
|
eq("self-rename merged false", self.merged, false);
|
|
358
358
|
|
|
@@ -656,20 +656,20 @@ eq("sip maxOpen 0", sip3.maxOpen, 0);
|
|
|
656
656
|
ok("sip over (1 > 0)", sip3.over);
|
|
657
657
|
|
|
658
658
|
// --- typo: 1-todo project with near-sibling ---
|
|
659
|
-
addTodo({ title: "z", project: "
|
|
660
|
-
addTodo({ title: "y", project: "
|
|
659
|
+
addTodo({ title: "z", project: "foo-bat" }); // 1 todo, near "foo-bar"?
|
|
660
|
+
addTodo({ title: "y", project: "foo-bar" }); // sibling
|
|
661
661
|
const o4 = projectsOverview();
|
|
662
|
-
const typo = o4.rows.find((r) => r.name === "
|
|
663
|
-
ok("
|
|
664
|
-
const notTypo = o4.rows.find((r) => r.name === "
|
|
665
|
-
ok("
|
|
662
|
+
const typo = o4.rows.find((r) => r.name === "foo-bat")!;
|
|
663
|
+
ok("foo-bat typo (near foo-bar)", typo.typo);
|
|
664
|
+
const notTypo = o4.rows.find((r) => r.name === "foo-bar")!;
|
|
665
|
+
ok("foo-bar not typo (>1 todo)", !notTypo.typo);
|
|
666
666
|
|
|
667
667
|
// --- lastUpdated = max live updatedAt, "" if no live todos ---
|
|
668
668
|
const o5 = projectsOverview();
|
|
669
669
|
ok("pi lastUpdated non-empty", o5.rows.find((r) => r.name === "pi")!.lastUpdated.length > 0);
|
|
670
670
|
|
|
671
671
|
// --- registry seeded via reconcile (projectsOverview persists) ---
|
|
672
|
-
ok("registry has
|
|
672
|
+
ok("registry has foo-bat", loadRegistry().projects.some((p) => p.name === "foo-bat"));
|
|
673
673
|
|
|
674
674
|
rmSync(tmp, { recursive: true, force: true });
|
|
675
675
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
@@ -826,8 +826,8 @@ saveRegistry(reg);
|
|
|
826
826
|
for (let i = 0; i < 9; i++) addTodo({ title: `large-${i}`, project: "large-proj" });
|
|
827
827
|
|
|
828
828
|
// PROJECT_TYPO: 1 todo + near-sibling
|
|
829
|
-
addTodo({ title: "typo", project: "
|
|
830
|
-
addTodo({ title: "sib", project: "
|
|
829
|
+
addTodo({ title: "typo", project: "foo-bat" });
|
|
830
|
+
addTodo({ title: "sib", project: "foo-bar" });
|
|
831
831
|
|
|
832
832
|
// PROJECT_STALE: lastUpdated > activeStaleDays (30)
|
|
833
833
|
const stale = addTodo({ title: "stale", project: "stale-proj" });
|
|
@@ -989,7 +989,7 @@ import type { ProjectsOverview } from "../src/projects.ts";
|
|
|
989
989
|
const overview: ProjectsOverview = {
|
|
990
990
|
rows: [
|
|
991
991
|
{ name: "pi", open: 3, in_progress: 0, parked: 0, done: 1, total: 4, maxOpen: 2, over: true, typo: false, lastUpdated: "2026-07-21T00:00:00.000Z" },
|
|
992
|
-
{ name: "
|
|
992
|
+
{ name: "foo-bat", open: 0, in_progress: 0, parked: 0, done: 0, total: 1, maxOpen: null, over: false, typo: true, lastUpdated: "" },
|
|
993
993
|
],
|
|
994
994
|
totalTodos: 5,
|
|
995
995
|
noProject: { count: 2, open: 1 },
|
|
@@ -702,7 +702,7 @@ const { migrateV2ToV3 } = await import("../src/migrate.ts");
|
|
|
702
702
|
}
|
|
703
703
|
```
|
|
704
704
|
|
|
705
|
-
Note: in Case 4, the curated armory todo's project is `"getpipher"` — fix the assertion `v3.todos[1]!.project
|
|
705
|
+
Note: in Case 4, the curated armory todo's project is `"getpipher"` — fix the assertion `v3.todos[1]!.project` to match the input `"getpipher"` (the v2 input uses `"getpipher"`). Make sure the assertion matches the input.
|
|
706
706
|
|
|
707
707
|
- [ ] **Step 6: Run the migrate suite**
|
|
708
708
|
|
|
@@ -95,7 +95,7 @@ Error messages (actionable, surfaced verbatim to the agent/user via the existing
|
|
|
95
95
|
```
|
|
96
96
|
## Open TODOs (23) — ⚠ over budget (cap 15)
|
|
97
97
|
23 open+in_progress across 5 projects
|
|
98
|
-
over-budget:
|
|
98
|
+
over-budget: getpipher 9/8, sip-protocol 6/5
|
|
99
99
|
run `todo list` or `/todo` to see the full list
|
|
100
100
|
```
|
|
101
101
|
|
|
@@ -219,4 +219,4 @@ brainstorm ✅ → spec (this doc) → RECTOR reviews → writing-plans → exec
|
|
|
219
219
|
- Zero runtime deps (node:fs only). 2-space indent. No TODO/FIXME. No AI attribution.
|
|
220
220
|
- Tests: node:test via tsx. `npm test` must stay green (331 baseline + new).
|
|
221
221
|
- Commits: `feat(scope): …` per task. PR → `--merge --delete-branch`.
|
|
222
|
-
-
|
|
222
|
+
- getpipher UX mental model (in `~/local-dev/getpipher/AGENTS.md`): interactive first (panel) for humans, CLI-style (tool actions) for the agent. New enforcement surfaces (block-on-add error, over-cap injection summary) follow this — the error is the agent's programmatic surface; the panel needs no add flow.
|
|
@@ -18,7 +18,7 @@ The injected `## Open TODOs (N)` block is already bounded (v0.3.0 title-only inj
|
|
|
18
18
|
1. **No forcing function on the *count*.** Open can grow to 30/50/100 with no pushback; the injection *hides* the rot behind `+N more`. A cap is a triage signal, not a bytes fix.
|
|
19
19
|
2. **No scope overview.** `project` is free-text, un-aggregated. Working across ZeroClaw, getpipher, vision, etc., you can't see "this project owns 18 of my 22 open" — can't triage *where* to cut. The bigger daily pain than the cap.
|
|
20
20
|
3. **No per-project bloat signal in `health`.** `ACTIVE_LARGE` fires globally at >15; can't say *which* project is over budget.
|
|
21
|
-
4. **No curation surface.** A typo'd project string (`
|
|
21
|
+
4. **No curation surface.** A typo'd project string (`foo-bat` vs `foo-bar`) silently becomes its own "project" with no way to merge.
|
|
22
22
|
|
|
23
23
|
v0.4.0 is the **triage visibility** release: see the backlog by project + know when one's over budget + fix typos. **No enforcement** — that's v0.5.0's "caps release" (count + notes + injection, all block-on-add).
|
|
24
24
|
|
|
@@ -39,8 +39,8 @@ v0.4.0 is the **triage visibility** release: see the backlog by project + know w
|
|
|
39
39
|
| **`projects` output** (Q9) | B — counts + cap signal + typo marker | One-stop triage; staleness/notesBytes stay in `health` (whole-store concerns). |
|
|
40
40
|
| **`health` per-project flags** (Q10) | C — `PROJECT_OVER` + `PROJECT_TYPO` + `PROJECT_LARGE` + `PROJECT_STALE` | `PROJECT_LARGE` (global default threshold) makes the per-project signal useful on day 1, not gated behind per-project config. All advisory. |
|
|
41
41
|
| **Action API + slash naming** (Q11) | A — tool `action:'projects'` + `action:'project_rename'`; slash `/todo projects` (thin mirror) | Plural=list, singular=action namespace. No `/todo project rename` slash — rename is panel-only (Q12). |
|
|
42
|
-
| **Panel surface** (Q12) | A — 6th tab `projects` + per-project action submenu | Panel is the primary human surface (
|
|
43
|
-
| **UX mental model** | Documented in `~/local-dev/getpipher/AGENTS.md` (cross-cutting, all
|
|
42
|
+
| **Panel surface** (Q12) | A — 6th tab `projects` + per-project action submenu | Panel is the primary human surface (getpipher UX mental model: interactive first, CLI-style for the agent). |
|
|
43
|
+
| **UX mental model** | Documented in `~/local-dev/getpipher/AGENTS.md` (cross-cutting, all getpipher extensions) | Panel-first design order; tool action + slash are secondary/derived. |
|
|
44
44
|
|
|
45
45
|
**Defaults (Q13):** `health.perProjectDefaultMax = 8`; `maxOpen` per-project default `null`; `PROJECT_STALE` reuses `activeStaleDays` (30d); `PROJECT_TYPO` = exactly 1 todo in project (live + archived done); seed scope = live + archive; empty-project group = `(no project)` (excluded from typo detection, no `maxOpen`); rename is best-effort multi-file (live → archive → registry, backup-on-corrupt per file, no cross-file WAL).
|
|
46
46
|
|
|
@@ -58,7 +58,7 @@ v0.4.0 is the **triage visibility** release: see the backlog by project + know w
|
|
|
58
58
|
"updatedAt": "2026-07-21T12:00:00.000Z",
|
|
59
59
|
"projects": [
|
|
60
60
|
{
|
|
61
|
-
"name": "
|
|
61
|
+
"name": "foo-bar",
|
|
62
62
|
"maxOpen": 5,
|
|
63
63
|
"createdAt": "2026-07-21T12:00:00.000Z",
|
|
64
64
|
"updatedAt": "2026-07-21T12:00:00.000Z"
|
|
@@ -158,7 +158,7 @@ Flag logic per project (live open count is the `open` figure):
|
|
|
158
158
|
- `PROJECT_TYPO` — total todos (live + archived done) in the project === 1 AND a near-named sibling (Levenshtein ≤ 2) exists in the registry
|
|
159
159
|
|
|
160
160
|
`healthReport()` calls `reconcileRegistry` first (so the registry is current), persists iff changed, then computes. Suggestions gain per-project actionable lines, e.g.:
|
|
161
|
-
- `project '
|
|
161
|
+
- `project 'foo-bat' has 1 todo — possible typo of 'foo-bar'? → todo project rename foo-bat foo-bar`
|
|
162
162
|
- `project 'getpipher' 12 open (maxOpen 5) → close/park some, or raise maxOpen`
|
|
163
163
|
- `project 'bug-bounty' 9 open (per-project default max 8) → over budget`
|
|
164
164
|
- `project 'vision' untouched 45d → stale, park or close`
|
|
@@ -171,7 +171,7 @@ Flag logic per project (live open count is the `open` figure):
|
|
|
171
171
|
|
|
172
172
|
#### Tool actions (agent surface)
|
|
173
173
|
- `action: 'projects'` → returns `ProjectsOverview` (structured). No params.
|
|
174
|
-
- `action: 'project_rename'` → params `oldName: string`, `newName: string`. Returns `{ liveRenamed: number, archivedRenamed: number, merged: boolean, newName: string }`. Throws `TodoError` if `oldName` not in registry. `newName` may equal an existing different project — rename-onto-existing is a **merge** (consolidates `oldName` todos into `newName`, removes the `oldName` registry entry, keeps `newName`'s entry; `merged: true`). This is the typo-cleanup path (`
|
|
174
|
+
- `action: 'project_rename'` → params `oldName: string`, `newName: string`. Returns `{ liveRenamed: number, archivedRenamed: number, merged: boolean, newName: string }`. Throws `TodoError` if `oldName` not in registry. `newName` may equal an existing different project — rename-onto-existing is a **merge** (consolidates `oldName` todos into `newName`, removes the `oldName` registry entry, keeps `newName`'s entry; `merged: true`). This is the typo-cleanup path (`foo-bat` → `foo-bar` where `getpipher` already exists). Self-rename (`newName === oldName`) is a no-op success (`{ liveRenamed: 0, archivedRenamed: 0, merged: false, newName }`).
|
|
175
175
|
|
|
176
176
|
#### Panel (human surface — primary)
|
|
177
177
|
New 6th tab `projects` in the `/todo` panel (existing 5: active/parked/done/archive/config → now 6). Tab label `Projects`.
|
|
@@ -179,13 +179,13 @@ New 6th tab `projects` in the `/todo` panel (existing 5: active/parked/done/arch
|
|
|
179
179
|
- **Rows:** `ProjectOverviewRow` rendered as: `name open/in_progress/parked/done (total) [max:N or —] OVER? ?typo · lastUpdated`. Box-draw to match existing tab style.
|
|
180
180
|
- **Sort:** open desc → total desc → name asc (Q9).
|
|
181
181
|
- **Action submenu** (per project, via `openActionSubmenu`): `Rename` / `Set maxOpen` / `Filter active to project`.
|
|
182
|
-
- **Rename** — inline `Input` (single-line; pi-tui can't nest `ctx.ui.editor()` inside `ctx.ui.custom()`). Validates `newName` non-empty + not equal to current. Calls `renameProject`. Confirms merge if target exists. Notify on success (`Renamed
|
|
182
|
+
- **Rename** — inline `Input` (single-line; pi-tui can't nest `ctx.ui.editor()` inside `ctx.ui.custom()`). Validates `newName` non-empty + not equal to current. Calls `renameProject`. Confirms merge if target exists. Notify on success (`Renamed foo-bat → foo-bar (3 live + 1 archived)`).
|
|
183
183
|
- **Set maxOpen** — inline `Input`, accepts a positive integer or `clear` (→ `null`). Calls `setProjectMaxOpen`. Notify (`getpipher maxOpen = 5` or `getpipher maxOpen cleared`).
|
|
184
184
|
- **Filter active to project** — jumps to the `active` tab with a `project` filter applied (existing list filter already supports `project`).
|
|
185
185
|
- **`(no project)` bucket** — shown as a non-selectable summary row at the top or footer (`(no project): N open`), no submenu.
|
|
186
186
|
|
|
187
187
|
#### Slash (thin mirror only)
|
|
188
|
-
- `/todo projects` — prints the `ProjectsOverview` as text (mirrors `/todo health`'s text-report style). One new sub. **No** `/todo project rename` slash — rename is panel-only (Q12=A +
|
|
188
|
+
- `/todo projects` — prints the `ProjectsOverview` as text (mirrors `/todo health`'s text-report style). One new sub. **No** `/todo project rename` slash — rename is panel-only (Q12=A + getpipher UX mental model).
|
|
189
189
|
|
|
190
190
|
### 3.7 `src/config.ts` — one new field, no schema bump
|
|
191
191
|
|
package/extensions/todo.ts
CHANGED
|
@@ -157,7 +157,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
157
157
|
"Use todo (action:'health') to check bloat across all boxes (counts + flags + suggestions). Run when the user asks about hygiene/bloat or before any hard-prune.",
|
|
158
158
|
"Use todo (action:'prune', hard:true, confirm:true, box?, olderThan?) for PERMANENT deletion (the only irreversible action). ALWAYS run health first, show the user the report + the exact proposed command, and wait for an explicit yes before passing confirm:true. Never hard-prune without explicit user confirmation.",
|
|
159
159
|
"Use todo (action:'projects') for a per-project scope overview (open/in_progress/parked/done counts + maxOpen + OVER/?typo markers). Run when the user asks 'which projects have open work' or to see backlog shape by project.",
|
|
160
|
-
"Use todo (action:'project_rename', oldName, newName) to rename or merge a project (rewrites live + archive + registry). Use it to fix typo'd project strings (e.g.
|
|
160
|
+
"Use todo (action:'project_rename', oldName, newName) to rename or merge a project (rewrites live + archive + registry). Use it to fix typo'd project strings (e.g. foo-bat → foo-bar). Rename onto an existing name merges (consolidates the old project into the new). Per-project maxOpen caps are ENFORCED (block-on-add); they also drive a PROJECT_OVER health flag when breached.",
|
|
161
161
|
],
|
|
162
162
|
parameters: Type.Object({
|
|
163
163
|
action: StringEnum(ACTIONS),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpipher/armory-todo",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Global, cross-session TODO for pi \u2014 persists across all sessions and is auto-injected into every prompt. The disk-backed counterpart to branch-scoped pi todo extensions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|