@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 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. `getpither` → `getpipher`).
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/getpither/armory-todo/issues/1):
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: "getpipher", currentOpen: 8, maxOpen: 8 }), "getpipher");
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. getpithergetpipher). 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.",
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-batfoo-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. getpithergetpipher). 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.",
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-batfoo-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/getpither/armory-todo/issues/1):
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: "getpipher" });
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, "getpipher") !== undefined);
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, "getpipher", 8);
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, "getpipher", null);
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: "getpither" });
340
- addTodo({ title: "R2", project: "getpither" });
341
- const arch1: ArchiveStore = { version: 3, updatedAt: "x", todos: [{ ...a, project: "getpither", status: "done", closedAt: "x" }] };
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: getpithergetpipher (getpipher already exists from earlier? no — fresh. so create)
347
- const rr = renameProject("getpither", "getpipher");
346
+ // merge: foo-batfoo-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(), "getpither") === undefined);
352
- ok("rename created new entry", getProjectEntry(loadRegistry(), "getpipher") !== undefined);
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("getpipher", "getpipher");
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: "getpither" }); // 1 todo, near "getpipher"?
660
- addTodo({ title: "y", project: "getpipher" }); // sibling
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 === "getpither")!;
663
- ok("getpither typo (near getpipher)", typo.typo);
664
- const notTypo = o4.rows.find((r) => r.name === "getpipher")!;
665
- ok("getpipher not typo (>1 todo)", !notTypo.typo);
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 getpither", loadRegistry().projects.some((p) => p.name === "getpither"));
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: "getpither" });
830
- addTodo({ title: "sib", project: "getpipher" });
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: "getpither", open: 0, in_progress: 0, parked: 0, done: 0, total: 1, maxOpen: null, over: false, typo: true, lastUpdated: "" },
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 === "getpither"` to `"getpipher"` (the v2 input uses `"getpipher"`). Make sure the assertion matches the input.
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: getpither 9/8, sip-protocol 6/5
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
- - getpither 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.
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 (`getpither` vs `getpipher`) silently becomes its own "project" with no way to merge.
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 (getpither UX mental model: interactive first, CLI-style for the agent). |
43
- | **UX mental model** | Documented in `~/local-dev/getpipher/AGENTS.md` (cross-cutting, all getpither extensions) | Panel-first design order; tool action + slash are secondary/derived. |
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": "getpipher",
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 'getpither' has 1 todo — possible typo of 'getpipher'? → todo project rename getpither getpipher`
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 (`getpither` → `getpipher` where `getpipher` already exists). Self-rename (`newName === oldName`) is a no-op success (`{ liveRenamed: 0, archivedRenamed: 0, merged: false, newName }`).
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 getpithergetpipher (3 live + 1 archived)`).
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-batfoo-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 + getpither UX mental model).
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
 
@@ -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. getpithergetpipher). 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.",
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-batfoo-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.2",
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",