From b2d4863bbdf801d70d9d6e05b0516d983b11bdf0 Mon Sep 17 00:00:00 2001 From: Christoph Maier Date: Fri, 17 Jul 2026 15:52:37 +0200 Subject: [PATCH 1/5] feature(consume-accepted-backlog-ideas): Consume selected backlog ideas on plan approval Feature: consume-accepted-backlog-ideas --- lib/views/planning.mjs | 2 +- lib/write.mjs | 24 ++++++++- memory/backlog/index.md | 13 ++--- .../consume-accepted-backlog-ideas.md | 47 ++++++++++++++++++ memory/features/index.md | 3 ++ memory/index.md | 2 + memory/log.md | 10 ++++ memory/plan.md | 36 ++++++++++++++ memory/usage.md | 12 +++-- skills/iterator/lib/views/planning.mjs | 2 +- skills/iterator/lib/write.mjs | 24 ++++++++- test/write.test.mjs | 49 +++++++++++++++++++ 12 files changed, 209 insertions(+), 15 deletions(-) create mode 100644 memory/features/consume-accepted-backlog-ideas.md create mode 100644 memory/features/index.md create mode 100644 memory/plan.md diff --git a/lib/views/planning.mjs b/lib/views/planning.mjs index b1828dc..ef8ca04 100644 --- a/lib/views/planning.mjs +++ b/lib/views/planning.mjs @@ -303,7 +303,7 @@ function renderBacklog(w){ if(goal){ const handoff = document.createElement('button'); handoff.className = 'act primary-act'; handoff.type = 'button'; handoff.textContent = D.plan ? 'Selected candidates saved' : 'Plan selected candidates'; - handoff.title = D.plan ? 'Retire or finish the active plan before starting a new one.' : 'Start a new plan with the selected candidates as its initial goal.'; + handoff.title = D.plan ? 'Retire or finish the active plan before starting a new one.' : 'Start a new plan with the selected candidates as its initial goal. They are removed from the backlog only after the plan is approved.'; handoff.disabled = Boolean(D.plan); handoff.addEventListener('click', () => action('plan', null, 'Starting /iterator-plan from backlog', goal)); section.appendChild(handoff); diff --git a/lib/write.mjs b/lib/write.mjs index e6cffe4..e2860c7 100644 --- a/lib/write.mjs +++ b/lib/write.mjs @@ -450,11 +450,25 @@ ${featuresSection} `.replace(/\n{3,}/g, "\n\n"); writeFileSync(join(b.memDir, "plan.md"), joinDoc(fm, bodyText)); + // A selected candidate is explicitly being handed to plan creation. Consume + // it only after the approved plan has been written; drafts, feedback, and + // cancelled review rounds never invoke this deterministic operation. + const backlog = loadBacklogForWrite(b); + const consumedBacklog = + (payload.status || "approved") === "approved" + ? backlog.filter((item) => item.selected === true) + : []; + if (consumedBacklog.length) { + writeFileSync( + backlogPath(b), + backlogIndex(backlog.filter((item) => item.selected !== true)), + ); + } regenerate(root); prependLog( b.memDir, payload.log || - `**${b.plan ? "Update" : "Creation"}**: Plan "${title}" approved on branch ${payload.branch || b.branch}.`, + `**${b.plan ? "Update" : "Creation"}**: Plan "${title}" approved on branch ${payload.branch || b.branch}.${consumedBacklog.length ? ` Consumed ${consumedBacklog.length} selected backlog candidate(s).` : ""}`, ); // Soft memory-init gate: planning without the knowledge side means features // get no relevant memories — surface it, never block. @@ -536,7 +550,13 @@ ${featuresSection} return { op: "plan", - written: ["plan.md", "index.md", "log.md"], + written: [ + "plan.md", + "index.md", + "log.md", + ...(consumedBacklog.length ? ["backlog/index.md"] : []), + ], + consumedBacklog: consumedBacklog.map((item) => item.id), memoryDir: b.memDir, ...(branchResult ? branchResult : {}), ...(branchResult?.worktree diff --git a/memory/backlog/index.md b/memory/backlog/index.md index 6ad867a..8d446a1 100644 --- a/memory/backlog/index.md +++ b/memory/backlog/index.md @@ -2,13 +2,14 @@ type: Backlog title: Iterator backlog description: Saved ideas and bugs kept separate from active plan features. -items: "[{\"id\":\"improve-overall-styling\",\"title\":\"improve overall styling\",\"details\":\"In the center on top where the tabs are add iterator as title like iterator./planning in the head line. In the headlline replace iterator with the current work dir folder name\",\"kind\":\"idea\",\"selected\":true,\"created\":\"2026-07-16T14:46:33.605Z\",\"updated\":\"2026-07-16T14:50:15.493Z\"},{\"id\":\"show-in-settings-for-models-only-the-scoped-models\",\"title\":\"Show in settings for models only the scoped models\",\"details\":\"In the settings the selection of a model should only show the scoped models and not all models.\",\"kind\":\"idea\",\"selected\":true,\"created\":\"2026-07-16T14:47:34.877Z\",\"updated\":\"2026-07-16T14:50:14.167Z\"},{\"id\":\"improve-the-interaction-with-claude-code\",\"title\":\"Improve the interaction with claude code\",\"details\":\"THe server is only really usable by pi so claude relies on the skills. We need to improve the skills so that claude loads them correctly and follows the overall plan. So that it can use the ouput of plan or featuers for usage. Claude always implements then all like auto mode. And at the end the worktree is commited so that the user can see the changes.This is different from the pi workflow where the ui is the center piece.\",\"kind\":\"idea\",\"selected\":true,\"created\":\"2026-07-16T14:50:12.210Z\",\"updated\":\"2026-07-16T14:50:13.588Z\"},{\"id\":\"idea-backlog-layout\",\"title\":\"Idea backlog layout\",\"details\":\"The idea backlog needs better spacing and layout. also make the idead baclog list and retired plan list with finite size and vertical scrolling so its not a gigantic page.\",\"kind\":\"bug\",\"selected\":true,\"created\":\"2026-07-16T14:51:23.470Z\",\"updated\":\"2026-07-16T14:51:24.883Z\"}]" -timestamp: 2026-07-16T14:51:24.883Z +items: "[{\"id\":\"improve-overall-styling\",\"title\":\"improve overall styling\",\"details\":\"In the center on top where the tabs are add iterator as title like iterator./planning in the head line. In the headlline replace iterator with the current work dir folder name\",\"kind\":\"idea\",\"selected\":false,\"created\":\"2026-07-16T14:46:33.605Z\",\"updated\":\"2026-07-17T13:46:36.058Z\"},{\"id\":\"show-in-settings-for-models-only-the-scoped-models\",\"title\":\"Show in settings for models only the scoped models\",\"details\":\"In the settings the selection of a model should only show the scoped models and not all models.\",\"kind\":\"idea\",\"selected\":false,\"created\":\"2026-07-16T14:47:34.877Z\",\"updated\":\"2026-07-17T13:46:36.796Z\"},{\"id\":\"improve-the-interaction-with-claude-code\",\"title\":\"Improve the interaction with claude code\",\"details\":\"THe server is only really usable by pi so claude relies on the skills. We need to improve the skills so that claude loads them correctly and follows the overall plan. So that it can use the ouput of plan or featuers for usage. Claude always implements then all like auto mode. And at the end the worktree is commited so that the user can see the changes.This is different from the pi workflow where the ui is the center piece.\",\"kind\":\"idea\",\"selected\":false,\"created\":\"2026-07-16T14:50:12.210Z\",\"updated\":\"2026-07-17T13:46:37.399Z\"},{\"id\":\"idea-backlog-layout\",\"title\":\"Idea backlog layout\",\"details\":\"The idea backlog needs better spacing and layout. also make the idead baclog list and retired plan list with finite size and vertical scrolling so its not a gigantic page.\",\"kind\":\"bug\",\"selected\":false,\"created\":\"2026-07-16T14:51:23.470Z\",\"updated\":\"2026-07-17T13:46:38.156Z\"},{\"id\":\"remove-elements-from-idea-backlog-when-the-plan-and-feature-is-created\",\"title\":\"remove elements from idea backlog when the plan and feature is created.\",\"details\":\"elements part of an accepted plan and feature should be removed so no old one stay.\",\"kind\":\"idea\",\"selected\":true,\"created\":\"2026-07-17T13:46:33.926Z\",\"updated\":\"2026-07-17T13:46:38.906Z\"}]" +timestamp: 2026-07-17T13:46:38.906Z --- # Backlog -* [idea] improve overall styling — selected -* [idea] Show in settings for models only the scoped models — selected -* [idea] Improve the interaction with claude code — selected -* [bug] Idea backlog layout — selected +* [idea] improve overall styling +* [idea] Show in settings for models only the scoped models +* [idea] Improve the interaction with claude code +* [bug] Idea backlog layout +* [idea] remove elements from idea backlog when the plan and feature is created. — selected diff --git a/memory/features/consume-accepted-backlog-ideas.md b/memory/features/consume-accepted-backlog-ideas.md new file mode 100644 index 0000000..05e7735 --- /dev/null +++ b/memory/features/consume-accepted-backlog-ideas.md @@ -0,0 +1,47 @@ +--- +type: Feature +title: Consume accepted backlog ideas +description: Remove selected backlog candidates atomically when their plan and resulting feature set are accepted. +status: implemented +size: medium +depends_on: [] +files: ["lib/write.mjs", "lib/gather.mjs", "lib/bundle.mjs", "lib/views/planning.mjs", "extensions/iterator.js", "test/write.test.mjs", "test/gather.test.mjs", "test/ui.test.mjs", "test/client-js-parse.test.mjs", "test/sync.test.mjs", "skills/iterator/lib/write.mjs", "skills/iterator/lib/gather.mjs", "skills/iterator/lib/bundle.mjs", "skills/iterator/lib/views/planning.mjs"] +memories: [pitfalls/client-js-template-literal-escaping, architecture/package-and-skill-layout, architecture/workflow-state-ownership, decisions/iterator-dashboard-feature-workflow, decisions/polish-dashboard-and-multi-agent-workflows, decisions/powerline-shows-sandbox-ui-port, decisions/settings-close-returns-to-work, decisions/synced-droppable-skill-libs] +timestamp: "2026-07-17T13:52:37.777Z" +tags: [] +--- + +# Implementation notes + +Define a validated selected-backlog identifier handoff from Planning to plan acceptance, then consume only those records after the plan write succeeds. Preserve selections on non-approval outcomes. Carry the accepted candidate identities into the feature-set flow so the same accepted work cannot leave stale backlog entries. Refresh the gathered Planning payload after successful writes; update client UI wording/interaction without deriving lifecycle state there. Add writer, gather/UI, and browser-script coverage; run npm run sync for canonical lib changes. + +# Snippets + +```js +function selectedBacklogGoal(){ + const selected = (D.backlog || []).filter(item => item.selected); + if(!selected.length) return null; + return 'Create a plan from these saved backlog candidates:\\n\\n' + selected.map(item => + '[' + item.kind + '] ' + item.title + (item.details ? '\\n' + item.details : '')).join('\\n\\n'); +} +``` + +```js +function writeBacklog(payload, root) { + const b = loadBundle(root); + const action = String(payload.action || ''); + const items = loadBacklogForWrite(b); + // create, edit, select, delete +} +``` + +```js +function writePlan(payload, root) { + const b = loadBundle(root); + // validate and write plan, regenerate and log +} +``` + +# Blast radius + +Plan acceptance, feature slicing acceptance, the Planning backlog, deterministic bundle writes, and synchronized skill copies. diff --git a/memory/features/index.md b/memory/features/index.md new file mode 100644 index 0000000..c6923c2 --- /dev/null +++ b/memory/features/index.md @@ -0,0 +1,3 @@ +# Features + +* [Consume accepted backlog ideas](consume-accepted-backlog-ideas.md) - ⬜ pending · medium · Remove selected backlog candidates atomically when their plan and resulting feature set are accepted. diff --git a/memory/index.md b/memory/index.md index 1f5f167..6188d3c 100644 --- a/memory/index.md +++ b/memory/index.md @@ -10,6 +10,8 @@ last_memorized_commit: a5d59c50453e568ec486a3c9c017c2506898700e * [Design](design.md) - A compact dark developer control plane with clear workflow state. * [Backlog](backlog/index.md) - Saved ideas and bugs outside active plan features. * [Settings](settings.md) - Project settings (auto mode, models, git flow). +* [Features](features/) - One document per implementation feature. +* [Plan](plan.md) - Keep the backlog limited to ideas that have not entered accepted planning work. * [Architecture](/architecture/) - How the system is structured. * [Decisions](/decisions/) - Durable product and implementation choices agents should preserve. * [Patterns & Conventions](/patterns/) - How code and workflows are written in this repo. diff --git a/memory/log.md b/memory/log.md index 0a46998..e82b8bf 100644 --- a/memory/log.md +++ b/memory/log.md @@ -1,6 +1,16 @@ # iterator update log ## 2026-07-17 +* **Implementation**: Committed feature(consume-accepted-backlog-ideas) on branch iterator/consume-accepted-backlog-ideas; awaiting review. +* **Update**: Applied 1 feature adjustment(s). +* **Creation**: 1 feature(s) written. +* **Creation**: Plan "Remove accepted backlog ideas" approved on branch main. +* **Backlog**: select remove-elements-from-idea-backlog-when-the-plan-and-feature-is-created (selected). +* **Backlog**: select idea-backlog-layout (deselected). +* **Backlog**: select improve-the-interaction-with-claude-code (deselected). +* **Backlog**: select show-in-settings-for-models-only-the-scoped-models (deselected). +* **Backlog**: select improve-overall-styling (deselected). +* **Backlog**: create remove-elements-from-idea-backlog-when-the-plan-and-feature-is-created. * **Update**: Memorized [Remote browser access](/setup/remote-browser-access.md). * **Creation**: Memorized [An IPv4-only bind breaks localhost behind a sandbox forward](/pitfalls/ipv4-only-bind-breaks-localhost.md). * **Update**: Memorized [Powerline shows the sandbox-published UI port](/decisions/powerline-shows-sandbox-ui-port.md). diff --git a/memory/plan.md b/memory/plan.md new file mode 100644 index 0000000..a0bd76f --- /dev/null +++ b/memory/plan.md @@ -0,0 +1,36 @@ +--- +type: Plan +title: Remove accepted backlog ideas +description: Keep the backlog limited to ideas that have not entered accepted planning work. +status: approved +branch: iterator/remove-accepted-backlog-ideas +worktree: /Volumes/Extern/Projects/iterator-iterator-remove-accepted-backlog-ideas +created: 2026-07-17 +timestamp: 2026-07-17T13:48:31.340Z +--- + +# Goal + +When a saved backlog idea is incorporated into an accepted plan or its resulting accepted feature set, remove that idea from the backlog so the Planning view shows only unplanned work and does not retain stale candidates. + +# Architecture + +- Extend the existing deterministic bundle write and gather contracts to identify the backlog ideas selected for a plan and persist their removal atomically with accepted plan or feature mutations, preserving browser views as render-only consumers (architecture/workflow-state-ownership). +- Carry explicit backlog-idea identity through the plan and feature review/action payloads so only accepted, incorporated ideas are removed; unrelated and unselected candidates remain intact. +- Update Planning dashboard interactions and payload rendering to reflect the refreshed backlog after successful acceptance, following the scoped dashboard action protocol (decisions/iterator-dashboard-feature-workflow). +- Implement canonical behavior in `lib/` and synchronize shipped skill-library copies via `npm run sync` (architecture/package-and-skill-layout). + +# Dependencies + +(none) + +# Key decisions + +- Delete only ideas explicitly selected and incorporated by an accepted plan or accepted feature set; do not remove ideas on drafts, feedback, cancellation, timeout, or failed writes. +- Make backlog removal part of the same deterministic writer operation as the corresponding accepted state change, preventing stale items from surviving a successful acceptance or disappearing after a failed one. +- Retain the current backlog representation and dashboard workflow rather than introducing a second lifecycle state for consumed ideas. +- Keep client-side changes compatible with the template-literal escaping rule in pitfalls/client-js-template-literal-escaping and cover the served script path. + +# Features + +* [Consume accepted backlog ideas](/features/consume-accepted-backlog-ideas.md) - Remove selected backlog candidates atomically when their plan and resulting feature set are accepted. diff --git a/memory/usage.md b/memory/usage.md index 5068645..46bf28b 100644 --- a/memory/usage.md +++ b/memory/usage.md @@ -2,8 +2,8 @@ type: Usage title: Token usage description: Per-step model/token ledger for the active plan — written only by the usage op. -totals: "{\"steps\":{\"hub\":{\"openai-codex/gpt-5.6-terra\":{\"input\":4812,\"output\":587,\"cacheRead\":46080,\"cacheWrite\":0,\"turns\":4}}},\"features\":{\"retire-plan\":{\"input\":4812,\"output\":587,\"cacheRead\":46080,\"cacheWrite\":0,\"turns\":4}}}" -timestamp: 2026-07-17T08:53:03.973Z +totals: "{\"steps\":{\"hub\":{\"openai-codex/gpt-5.6-terra\":{\"input\":4812,\"output\":587,\"cacheRead\":46080,\"cacheWrite\":0,\"turns\":4}},\"plan\":{\"openai-codex/gpt-5.6-terra\":{\"input\":49508,\"output\":2430,\"cacheRead\":403456,\"cacheWrite\":0,\"turns\":17}}},\"features\":{\"retire-plan\":{\"input\":4812,\"output\":587,\"cacheRead\":46080,\"cacheWrite\":0,\"turns\":4}}}" +timestamp: 2026-07-17T13:49:44.849Z --- # Usage @@ -14,10 +14,16 @@ timestamp: 2026-07-17T08:53:03.973Z | --- | ---: | ---: | ---: | ---: | ---: | | openai-codex/gpt-5.6-terra | 4812 | 587 | 46080 | 0 | 4 | +## plan + +| model | input | output | cache read | cache write | turns | +| --- | ---: | ---: | ---: | ---: | ---: | +| openai-codex/gpt-5.6-terra | 49508 | 2430 | 403456 | 0 | 17 | + ## Per feature | feature | input | output | cache read | cache write | turns | | --- | ---: | ---: | ---: | ---: | ---: | | retire-plan | 4812 | 587 | 46080 | 0 | 4 | -Total: 4812 in / 587 out / 46080 cache-read / 0 cache-write over 4 turns. +Total: 54320 in / 3017 out / 449536 cache-read / 0 cache-write over 21 turns. diff --git a/skills/iterator/lib/views/planning.mjs b/skills/iterator/lib/views/planning.mjs index b1828dc..ef8ca04 100644 --- a/skills/iterator/lib/views/planning.mjs +++ b/skills/iterator/lib/views/planning.mjs @@ -303,7 +303,7 @@ function renderBacklog(w){ if(goal){ const handoff = document.createElement('button'); handoff.className = 'act primary-act'; handoff.type = 'button'; handoff.textContent = D.plan ? 'Selected candidates saved' : 'Plan selected candidates'; - handoff.title = D.plan ? 'Retire or finish the active plan before starting a new one.' : 'Start a new plan with the selected candidates as its initial goal.'; + handoff.title = D.plan ? 'Retire or finish the active plan before starting a new one.' : 'Start a new plan with the selected candidates as its initial goal. They are removed from the backlog only after the plan is approved.'; handoff.disabled = Boolean(D.plan); handoff.addEventListener('click', () => action('plan', null, 'Starting /iterator-plan from backlog', goal)); section.appendChild(handoff); diff --git a/skills/iterator/lib/write.mjs b/skills/iterator/lib/write.mjs index e6cffe4..e2860c7 100644 --- a/skills/iterator/lib/write.mjs +++ b/skills/iterator/lib/write.mjs @@ -450,11 +450,25 @@ ${featuresSection} `.replace(/\n{3,}/g, "\n\n"); writeFileSync(join(b.memDir, "plan.md"), joinDoc(fm, bodyText)); + // A selected candidate is explicitly being handed to plan creation. Consume + // it only after the approved plan has been written; drafts, feedback, and + // cancelled review rounds never invoke this deterministic operation. + const backlog = loadBacklogForWrite(b); + const consumedBacklog = + (payload.status || "approved") === "approved" + ? backlog.filter((item) => item.selected === true) + : []; + if (consumedBacklog.length) { + writeFileSync( + backlogPath(b), + backlogIndex(backlog.filter((item) => item.selected !== true)), + ); + } regenerate(root); prependLog( b.memDir, payload.log || - `**${b.plan ? "Update" : "Creation"}**: Plan "${title}" approved on branch ${payload.branch || b.branch}.`, + `**${b.plan ? "Update" : "Creation"}**: Plan "${title}" approved on branch ${payload.branch || b.branch}.${consumedBacklog.length ? ` Consumed ${consumedBacklog.length} selected backlog candidate(s).` : ""}`, ); // Soft memory-init gate: planning without the knowledge side means features // get no relevant memories — surface it, never block. @@ -536,7 +550,13 @@ ${featuresSection} return { op: "plan", - written: ["plan.md", "index.md", "log.md"], + written: [ + "plan.md", + "index.md", + "log.md", + ...(consumedBacklog.length ? ["backlog/index.md"] : []), + ], + consumedBacklog: consumedBacklog.map((item) => item.id), memoryDir: b.memDir, ...(branchResult ? branchResult : {}), ...(branchResult?.worktree diff --git a/test/write.test.mjs b/test/write.test.mjs index 2a1f61d..abb3f66 100644 --- a/test/write.test.mjs +++ b/test/write.test.mjs @@ -13,6 +13,7 @@ import { tmpdir } from "node:os"; import { join, resolve as resolvePath } from "node:path"; import { applyOp, topoSort, setFmKeys } from "../lib/write.mjs"; import { frontmatter, gather } from "../lib/gather.mjs"; +import { backlogItems } from "../lib/bundle.mjs"; process.env.ITERATOR_NOW = "2026-07-06T12:00:00Z"; @@ -108,6 +109,54 @@ test("plan op writes a conformant bundle and log entry", () => { } }); +test("plan approval consumes selected backlog candidates only", () => { + const root = makeRepo(); + try { + const selected = applyOp( + { + op: "backlog", + action: "create", + kind: "idea", + title: "Selected candidate", + details: "Turn this into a plan.", + }, + root, + ); + const retained = applyOp( + { + op: "backlog", + action: "create", + kind: "bug", + title: "Retained candidate", + details: "Leave this in the backlog.", + }, + root, + ); + applyOp( + { op: "backlog", action: "select", id: selected.item.id, selected: true }, + root, + ); + const draft = applyOp({ ...PLAN_OP, status: "draft" }, root); + assert.deepEqual(draft.consumedBacklog, []); + assert.deepEqual( + backlogItems(read(root, "backlog", "index.md")).map((item) => item.id), + [selected.item.id, retained.item.id], + "draft plan writes leave selected candidates available", + ); + + const result = applyOp(PLAN_OP, root); + assert.deepEqual(result.consumedBacklog, [selected.item.id]); + assert.ok(result.written.includes("backlog/index.md")); + assert.deepEqual( + backlogItems(read(root, "backlog", "index.md")).map((item) => item.id), + [retained.item.id], + ); + assert.match(read(root, "log.md"), /Consumed 1 selected backlog candidate/); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test("features op writes files, topo-orders the index, regenerates plan # Features", () => { const root = makeRepo(); try { From 7392151a6e3d37b32ad98dc2b5fc234a3c1b0c96 Mon Sep 17 00:00:00 2001 From: Christoph Maier Date: Fri, 17 Jul 2026 15:52:37 +0200 Subject: [PATCH 2/5] chore(iterator): record feature commit --- memory/features/consume-accepted-backlog-ideas.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/memory/features/consume-accepted-backlog-ideas.md b/memory/features/consume-accepted-backlog-ideas.md index 05e7735..f753a1c 100644 --- a/memory/features/consume-accepted-backlog-ideas.md +++ b/memory/features/consume-accepted-backlog-ideas.md @@ -7,8 +7,12 @@ size: medium depends_on: [] files: ["lib/write.mjs", "lib/gather.mjs", "lib/bundle.mjs", "lib/views/planning.mjs", "extensions/iterator.js", "test/write.test.mjs", "test/gather.test.mjs", "test/ui.test.mjs", "test/client-js-parse.test.mjs", "test/sync.test.mjs", "skills/iterator/lib/write.mjs", "skills/iterator/lib/gather.mjs", "skills/iterator/lib/bundle.mjs", "skills/iterator/lib/views/planning.mjs"] memories: [pitfalls/client-js-template-literal-escaping, architecture/package-and-skill-layout, architecture/workflow-state-ownership, decisions/iterator-dashboard-feature-workflow, decisions/polish-dashboard-and-multi-agent-workflows, decisions/powerline-shows-sandbox-ui-port, decisions/settings-close-returns-to-work, decisions/synced-droppable-skill-libs] -timestamp: "2026-07-17T13:52:37.777Z" +timestamp: "2026-07-17T13:52:37.857Z" tags: [] +commits: + - sha: b2d4863bbdf801d70d9d6e05b0516d983b11bdf0 + kind: implement + date: 2026-07-17 --- # Implementation notes From 30d015e987934f8487b01395f31893fe7d7ee16e Mon Sep 17 00:00:00 2001 From: Christoph Maier Date: Fri, 17 Jul 2026 15:53:47 +0200 Subject: [PATCH 3/5] chore(iterator): record feature commits and memory updates --- memory/decisions/index.md | 2 +- .../decisions/iterator-dashboard-feature-workflow.md | 12 +++++------- memory/features/consume-accepted-backlog-ideas.md | 5 +++-- memory/features/index.md | 2 +- memory/log.md | 2 ++ 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/memory/decisions/index.md b/memory/decisions/index.md index 6497c33..f9abb2b 100644 --- a/memory/decisions/index.md +++ b/memory/decisions/index.md @@ -6,5 +6,5 @@ Durable product and implementation choices agents should preserve. * [Powerline shows the sandbox-published UI port](/decisions/powerline-shows-sandbox-ui-port.md) - The footer trails a ui:PORT segment resolved from ITERATOR_DISPLAY_PORT, falling back to ~/.pisbx-env because sbx run never sources it into pi's environment. * [Return to Work when Settings closes](/decisions/settings-close-returns-to-work.md) - Idle Settings close events restore the refreshed Work hub without changing the settings persistence path. * [Sync shared libs into droppable skills](/decisions/synced-droppable-skill-libs.md) - Shared code is developed in root lib/ and copied into skill folders so skills work when installed together or copied manually. -* [Unify Iterator dashboard and feature workflow](/decisions/iterator-dashboard-feature-workflow.md) - Iterator’s dashboard uses explicit operation state and consistent iterator/features terminology so active work and navigation remain unambiguous. +* [Unify Iterator dashboard and feature workflow](/decisions/iterator-dashboard-feature-workflow.md) - Dashboard workflows keep backlog candidates separate from active work until selected candidates are consumed by approved plan creation. * [Use an OKF markdown bundle in target repos](/decisions/okf-markdown-bundle.md) - Project memory is stored as markdown plus YAML frontmatter in each target repo instead of in an external database. diff --git a/memory/decisions/iterator-dashboard-feature-workflow.md b/memory/decisions/iterator-dashboard-feature-workflow.md index fb8ddec..e5bfcb0 100644 --- a/memory/decisions/iterator-dashboard-feature-workflow.md +++ b/memory/decisions/iterator-dashboard-feature-workflow.md @@ -1,7 +1,7 @@ --- type: Decision title: Unify Iterator dashboard and feature workflow -description: Iterator’s dashboard uses explicit operation state and consistent iterator/features terminology so active work and navigation remain unambiguous. +description: Dashboard workflows keep backlog candidates separate from active work until selected candidates are consumed by approved plan creation. status: accepted date: 2026-07-15 tags: @@ -33,22 +33,20 @@ files: - test/bundle.test.mjs - test/gather.test.mjs - test/write.test.mjs -timestamp: 2026-07-16T11:27:44.913Z +timestamp: "2026-07-17T13:53:47.909Z" --- ## Context -Iterator’s dashboard and feature workflow had accumulated mixed `lr`/`chunk` terminology and broad action payloads. That made state ownership, navigation, and plan creation harder to reason about. +Iterator’s dashboard and feature workflow use scoped actions and deterministic writer operations to keep active work unambiguous. ## Decision -Use a state-driven dashboard and scoped action protocol. Actions submit only the fields they own, while operation lifecycle state drives contextual controls and Work/Knowledge activity badges. Keep Knowledge as a persistent destination without a Close control; retain Settings close as navigation. - -Standardize the plugin on `iterator` and `features` terminology across commands, documentation, paths, UI, and persisted records. Treat the rename as an explicit breaking migration for existing `CHUNKS.md`-based data rather than silently preserving legacy aliases. +Keep saved backlog candidates separate from active plan features. A candidate selected for planning is consumed from the backlog only when the deterministic plan operation writes an approved plan; draft writes and non-approval review outcomes retain it. The writer reports the consumed candidate IDs and regenerates the backlog index in the same operation. ## Consequences -The browser never hand-writes workflow records, and active plan graphs remain focused on implementation work. Existing persisted plans require the supported migration path. +The Planning backlog shows only candidates that have not entered approved planning work. Browser selection remains intent only; it never directly mutates workflow records. # Retired plan diff --git a/memory/features/consume-accepted-backlog-ideas.md b/memory/features/consume-accepted-backlog-ideas.md index f753a1c..94808d8 100644 --- a/memory/features/consume-accepted-backlog-ideas.md +++ b/memory/features/consume-accepted-backlog-ideas.md @@ -2,17 +2,18 @@ type: Feature title: Consume accepted backlog ideas description: Remove selected backlog candidates atomically when their plan and resulting feature set are accepted. -status: implemented +status: done size: medium depends_on: [] files: ["lib/write.mjs", "lib/gather.mjs", "lib/bundle.mjs", "lib/views/planning.mjs", "extensions/iterator.js", "test/write.test.mjs", "test/gather.test.mjs", "test/ui.test.mjs", "test/client-js-parse.test.mjs", "test/sync.test.mjs", "skills/iterator/lib/write.mjs", "skills/iterator/lib/gather.mjs", "skills/iterator/lib/bundle.mjs", "skills/iterator/lib/views/planning.mjs"] memories: [pitfalls/client-js-template-literal-escaping, architecture/package-and-skill-layout, architecture/workflow-state-ownership, decisions/iterator-dashboard-feature-workflow, decisions/polish-dashboard-and-multi-agent-workflows, decisions/powerline-shows-sandbox-ui-port, decisions/settings-close-returns-to-work, decisions/synced-droppable-skill-libs] -timestamp: "2026-07-17T13:52:37.857Z" +timestamp: "2026-07-17T13:53:47.894Z" tags: [] commits: - sha: b2d4863bbdf801d70d9d6e05b0516d983b11bdf0 kind: implement date: 2026-07-17 +done: 2026-07-17 --- # Implementation notes diff --git a/memory/features/index.md b/memory/features/index.md index c6923c2..8f5d743 100644 --- a/memory/features/index.md +++ b/memory/features/index.md @@ -1,3 +1,3 @@ # Features -* [Consume accepted backlog ideas](consume-accepted-backlog-ideas.md) - ⬜ pending · medium · Remove selected backlog candidates atomically when their plan and resulting feature set are accepted. +* [Consume accepted backlog ideas](consume-accepted-backlog-ideas.md) - ✅ done · medium · Remove selected backlog candidates atomically when their plan and resulting feature set are accepted. diff --git a/memory/log.md b/memory/log.md index e82b8bf..5430fa1 100644 --- a/memory/log.md +++ b/memory/log.md @@ -1,6 +1,8 @@ # iterator update log ## 2026-07-17 +* **Update**: Memorized [Unify Iterator dashboard and feature workflow](/decisions/iterator-dashboard-feature-workflow.md). +* **Review**: Accepted [Consume accepted backlog ideas](/features/consume-accepted-backlog-ideas.md) (committed as feature(consume-accepted-backlog-ideas)). * **Implementation**: Committed feature(consume-accepted-backlog-ideas) on branch iterator/consume-accepted-backlog-ideas; awaiting review. * **Update**: Applied 1 feature adjustment(s). * **Creation**: 1 feature(s) written. From 0e47470027e48704153c161c8053c6b876768246 Mon Sep 17 00:00:00 2001 From: Christoph Maier Date: Fri, 17 Jul 2026 15:57:05 +0200 Subject: [PATCH 4/5] backlog --- lib/write.mjs | 21 ++++++++--- .../consume-accepted-backlog-ideas.md | 24 +++++++++++++ memory/decisions/index.md | 1 + .../consume-accepted-backlog-ideas.md | 0 .../index.md | 0 .../plan.md | 0 .../usage.md | 36 +++++++++++++++++++ memory/index.md | 2 -- memory/log.md | 1 + memory/usage.md | 16 +++------ skills/iterator/lib/write.mjs | 21 ++++++++--- test/write.test.mjs | 36 +++++++++++++++---- 12 files changed, 130 insertions(+), 28 deletions(-) create mode 100644 memory/decisions/consume-accepted-backlog-ideas.md rename memory/features/{ => archive/2026-07-17-consume-accepted-backlog-ideas}/consume-accepted-backlog-ideas.md (100%) rename memory/features/{ => archive/2026-07-17-consume-accepted-backlog-ideas}/index.md (100%) rename memory/{ => features/archive/2026-07-17-consume-accepted-backlog-ideas}/plan.md (100%) create mode 100644 memory/features/archive/2026-07-17-consume-accepted-backlog-ideas/usage.md diff --git a/lib/write.mjs b/lib/write.mjs index e2860c7..e50f18d 100644 --- a/lib/write.mjs +++ b/lib/write.mjs @@ -61,7 +61,14 @@ import { rmSync, writeFileSync, } from "node:fs"; -import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path"; +import { + basename, + dirname, + isAbsolute, + join, + relative, + resolve, +} from "node:path"; import { gatherReview, loadBundle, @@ -1581,7 +1588,10 @@ function acceptCommit(payload, root) { // commit. Unstage everything first: each feature commit then contains // exactly its own staged paths. The working tree is untouched. Slim-only // accepts commit nothing here, so the index is left alone. - if (needsStaging && gitSoft(["rev-parse", "--verify", "HEAD"], b.root) !== "") { + if ( + needsStaging && + gitSoft(["rev-parse", "--verify", "HEAD"], b.root) !== "" + ) { gitSoft(["reset", "-q"], b.root); } @@ -2473,7 +2483,8 @@ function recordPlanReview(payload, root) { const b = loadBundle(root); if (!b.plan) fail("record-plan-review: no memory/plan.md"); const report = String(payload.report || "").trim(); - if (!report) fail("record-plan-review needs a report (the review's findings)"); + if (!report) + fail("record-plan-review needs a report (the review's findings)"); if (payload.by && !["agent", "human"].includes(payload.by)) fail(`invalid by '${payload.by}' (agent|human)`); const d = payload.date || today(); @@ -2543,7 +2554,9 @@ function scrubMainPlanPointer(b, notes) { return; } rmSync(pointer); - notes.push(`removed the stale plan pointer ${pointer} from the main checkout`); + notes.push( + `removed the stale plan pointer ${pointer} from the main checkout`, + ); } /** diff --git a/memory/decisions/consume-accepted-backlog-ideas.md b/memory/decisions/consume-accepted-backlog-ideas.md new file mode 100644 index 0000000..dacaa7e --- /dev/null +++ b/memory/decisions/consume-accepted-backlog-ideas.md @@ -0,0 +1,24 @@ +--- +type: Decision +title: Consume selected backlog ideas on plan approval +description: Selected idea or bug candidates leave the backlog only after deterministic plan approval. +status: accepted +date: 2026-07-17 +tags: [backlog, planning, workflow] +files: ["lib/write.mjs", "lib/views/planning.mjs", "test/write.test.mjs", "skills/iterator/lib/write.mjs", "skills/iterator/lib/views/planning.mjs"] +timestamp: 2026-07-17T13:54:35.778Z +--- + +## Decision + +Treat backlog selection as planning intent, not a workflow mutation. The approved plan writer consumes every selected candidate after writing the plan and regenerates the backlog index in the same deterministic operation. + +## Consequences + +Draft plans and review outcomes that do not run the approved writer preserve all selected candidates. Approved writes report the consumed IDs and leave unselected ideas and bugs available in Planning. The Planning handoff explains this lifecycle boundary, and the writer test covers both draft retention and approved-plan removal. + +# Retired plan + +Condensed from plan "Remove accepted backlog ideas" (1 features, archived under /features/archive/2026-07-17-consume-accepted-backlog-ideas/). + +Token usage: 237410 in / 8441 out / 2277888 cache-read / 0 cache-write over 49 turns (per-step breakdown in the archived usage.md). diff --git a/memory/decisions/index.md b/memory/decisions/index.md index f9abb2b..bface48 100644 --- a/memory/decisions/index.md +++ b/memory/decisions/index.md @@ -2,6 +2,7 @@ Durable product and implementation choices agents should preserve. +* [Consume selected backlog ideas on plan approval](/decisions/consume-accepted-backlog-ideas.md) - Selected idea or bug candidates leave the backlog only after deterministic plan approval. * [Polish dashboard and multi-agent workflows](/decisions/polish-dashboard-and-multi-agent-workflows.md) - Dashboard polish and workflow refinements that clarify project context, constrain settings to usable models, and support deterministic Claude Code feature execution. * [Powerline shows the sandbox-published UI port](/decisions/powerline-shows-sandbox-ui-port.md) - The footer trails a ui:PORT segment resolved from ITERATOR_DISPLAY_PORT, falling back to ~/.pisbx-env because sbx run never sources it into pi's environment. * [Return to Work when Settings closes](/decisions/settings-close-returns-to-work.md) - Idle Settings close events restore the refreshed Work hub without changing the settings persistence path. diff --git a/memory/features/consume-accepted-backlog-ideas.md b/memory/features/archive/2026-07-17-consume-accepted-backlog-ideas/consume-accepted-backlog-ideas.md similarity index 100% rename from memory/features/consume-accepted-backlog-ideas.md rename to memory/features/archive/2026-07-17-consume-accepted-backlog-ideas/consume-accepted-backlog-ideas.md diff --git a/memory/features/index.md b/memory/features/archive/2026-07-17-consume-accepted-backlog-ideas/index.md similarity index 100% rename from memory/features/index.md rename to memory/features/archive/2026-07-17-consume-accepted-backlog-ideas/index.md diff --git a/memory/plan.md b/memory/features/archive/2026-07-17-consume-accepted-backlog-ideas/plan.md similarity index 100% rename from memory/plan.md rename to memory/features/archive/2026-07-17-consume-accepted-backlog-ideas/plan.md diff --git a/memory/features/archive/2026-07-17-consume-accepted-backlog-ideas/usage.md b/memory/features/archive/2026-07-17-consume-accepted-backlog-ideas/usage.md new file mode 100644 index 0000000..432f1a8 --- /dev/null +++ b/memory/features/archive/2026-07-17-consume-accepted-backlog-ideas/usage.md @@ -0,0 +1,36 @@ +--- +type: Usage +title: Token usage +description: Per-step model/token ledger for the active plan — written only by the usage op. +totals: "{\"steps\":{\"hub\":{\"openai-codex/gpt-5.6-terra\":{\"input\":4812,\"output\":587,\"cacheRead\":46080,\"cacheWrite\":0,\"turns\":4}},\"plan\":{\"openai-codex/gpt-5.6-terra\":{\"input\":49508,\"output\":2430,\"cacheRead\":403456,\"cacheWrite\":0,\"turns\":17}},\"implement\":{\"openai-codex/gpt-5.6-terra\":{\"input\":183090,\"output\":5424,\"cacheRead\":1828352,\"cacheWrite\":0,\"turns\":28}}},\"features\":{\"retire-plan\":{\"input\":4812,\"output\":587,\"cacheRead\":46080,\"cacheWrite\":0,\"turns\":4},\"consume-accepted-backlog-ideas\":{\"input\":183090,\"output\":5424,\"cacheRead\":1828352,\"cacheWrite\":0,\"turns\":28}}}" +timestamp: 2026-07-17T13:53:55.128Z +--- + +# Usage + +## hub + +| model | input | output | cache read | cache write | turns | +| --- | ---: | ---: | ---: | ---: | ---: | +| openai-codex/gpt-5.6-terra | 4812 | 587 | 46080 | 0 | 4 | + +## plan + +| model | input | output | cache read | cache write | turns | +| --- | ---: | ---: | ---: | ---: | ---: | +| openai-codex/gpt-5.6-terra | 49508 | 2430 | 403456 | 0 | 17 | + +## implement + +| model | input | output | cache read | cache write | turns | +| --- | ---: | ---: | ---: | ---: | ---: | +| openai-codex/gpt-5.6-terra | 183090 | 5424 | 1828352 | 0 | 28 | + +## Per feature + +| feature | input | output | cache read | cache write | turns | +| --- | ---: | ---: | ---: | ---: | ---: | +| retire-plan | 4812 | 587 | 46080 | 0 | 4 | +| consume-accepted-backlog-ideas | 183090 | 5424 | 1828352 | 0 | 28 | + +Total: 237410 in / 8441 out / 2277888 cache-read / 0 cache-write over 49 turns. diff --git a/memory/index.md b/memory/index.md index 6188d3c..1f5f167 100644 --- a/memory/index.md +++ b/memory/index.md @@ -10,8 +10,6 @@ last_memorized_commit: a5d59c50453e568ec486a3c9c017c2506898700e * [Design](design.md) - A compact dark developer control plane with clear workflow state. * [Backlog](backlog/index.md) - Saved ideas and bugs outside active plan features. * [Settings](settings.md) - Project settings (auto mode, models, git flow). -* [Features](features/) - One document per implementation feature. -* [Plan](plan.md) - Keep the backlog limited to ideas that have not entered accepted planning work. * [Architecture](/architecture/) - How the system is structured. * [Decisions](/decisions/) - Durable product and implementation choices agents should preserve. * [Patterns & Conventions](/patterns/) - How code and workflows are written in this repo. diff --git a/memory/log.md b/memory/log.md index 5430fa1..11046f9 100644 --- a/memory/log.md +++ b/memory/log.md @@ -1,6 +1,7 @@ # iterator update log ## 2026-07-17 +* **Retirement**: Plan "Remove accepted backlog ideas" condensed into [Consume selected backlog ideas on plan approval](/decisions/consume-accepted-backlog-ideas.md). * **Update**: Memorized [Unify Iterator dashboard and feature workflow](/decisions/iterator-dashboard-feature-workflow.md). * **Review**: Accepted [Consume accepted backlog ideas](/features/consume-accepted-backlog-ideas.md) (committed as feature(consume-accepted-backlog-ideas)). * **Implementation**: Committed feature(consume-accepted-backlog-ideas) on branch iterator/consume-accepted-backlog-ideas; awaiting review. diff --git a/memory/usage.md b/memory/usage.md index 46bf28b..f105601 100644 --- a/memory/usage.md +++ b/memory/usage.md @@ -2,8 +2,8 @@ type: Usage title: Token usage description: Per-step model/token ledger for the active plan — written only by the usage op. -totals: "{\"steps\":{\"hub\":{\"openai-codex/gpt-5.6-terra\":{\"input\":4812,\"output\":587,\"cacheRead\":46080,\"cacheWrite\":0,\"turns\":4}},\"plan\":{\"openai-codex/gpt-5.6-terra\":{\"input\":49508,\"output\":2430,\"cacheRead\":403456,\"cacheWrite\":0,\"turns\":17}}},\"features\":{\"retire-plan\":{\"input\":4812,\"output\":587,\"cacheRead\":46080,\"cacheWrite\":0,\"turns\":4}}}" -timestamp: 2026-07-17T13:49:44.849Z +totals: "{\"steps\":{\"hub\":{\"openai-codex/gpt-5.6-terra\":{\"input\":175681,\"output\":515,\"cacheRead\":308736,\"cacheWrite\":0,\"turns\":5}}},\"features\":{\"retire-plan\":{\"input\":175681,\"output\":515,\"cacheRead\":308736,\"cacheWrite\":0,\"turns\":5}}}" +timestamp: 2026-07-17T13:54:38.269Z --- # Usage @@ -12,18 +12,12 @@ timestamp: 2026-07-17T13:49:44.849Z | model | input | output | cache read | cache write | turns | | --- | ---: | ---: | ---: | ---: | ---: | -| openai-codex/gpt-5.6-terra | 4812 | 587 | 46080 | 0 | 4 | - -## plan - -| model | input | output | cache read | cache write | turns | -| --- | ---: | ---: | ---: | ---: | ---: | -| openai-codex/gpt-5.6-terra | 49508 | 2430 | 403456 | 0 | 17 | +| openai-codex/gpt-5.6-terra | 175681 | 515 | 308736 | 0 | 5 | ## Per feature | feature | input | output | cache read | cache write | turns | | --- | ---: | ---: | ---: | ---: | ---: | -| retire-plan | 4812 | 587 | 46080 | 0 | 4 | +| retire-plan | 175681 | 515 | 308736 | 0 | 5 | -Total: 54320 in / 3017 out / 449536 cache-read / 0 cache-write over 21 turns. +Total: 175681 in / 515 out / 308736 cache-read / 0 cache-write over 5 turns. diff --git a/skills/iterator/lib/write.mjs b/skills/iterator/lib/write.mjs index e2860c7..e50f18d 100644 --- a/skills/iterator/lib/write.mjs +++ b/skills/iterator/lib/write.mjs @@ -61,7 +61,14 @@ import { rmSync, writeFileSync, } from "node:fs"; -import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path"; +import { + basename, + dirname, + isAbsolute, + join, + relative, + resolve, +} from "node:path"; import { gatherReview, loadBundle, @@ -1581,7 +1588,10 @@ function acceptCommit(payload, root) { // commit. Unstage everything first: each feature commit then contains // exactly its own staged paths. The working tree is untouched. Slim-only // accepts commit nothing here, so the index is left alone. - if (needsStaging && gitSoft(["rev-parse", "--verify", "HEAD"], b.root) !== "") { + if ( + needsStaging && + gitSoft(["rev-parse", "--verify", "HEAD"], b.root) !== "" + ) { gitSoft(["reset", "-q"], b.root); } @@ -2473,7 +2483,8 @@ function recordPlanReview(payload, root) { const b = loadBundle(root); if (!b.plan) fail("record-plan-review: no memory/plan.md"); const report = String(payload.report || "").trim(); - if (!report) fail("record-plan-review needs a report (the review's findings)"); + if (!report) + fail("record-plan-review needs a report (the review's findings)"); if (payload.by && !["agent", "human"].includes(payload.by)) fail(`invalid by '${payload.by}' (agent|human)`); const d = payload.date || today(); @@ -2543,7 +2554,9 @@ function scrubMainPlanPointer(b, notes) { return; } rmSync(pointer); - notes.push(`removed the stale plan pointer ${pointer} from the main checkout`); + notes.push( + `removed the stale plan pointer ${pointer} from the main checkout`, + ); } /** diff --git a/test/write.test.mjs b/test/write.test.mjs index abb3f66..e94187e 100644 --- a/test/write.test.mjs +++ b/test/write.test.mjs @@ -1922,8 +1922,14 @@ test("commit-feature commits only the feature's files, flips implemented, record ); assert.match(res.branch, /^iterator\/feature-a$/, "moved off main"); assert.deepEqual(res.staged, ["src/a.ts"]); - assert.ok(res.leftovers.includes("notes.txt"), "churn reported as leftover"); - assert.ok(res.leftovers.includes("src/b.ts"), "the other feature's file stays uncommitted"); + assert.ok( + res.leftovers.includes("notes.txt"), + "churn reported as leftover", + ); + assert.ok( + res.leftovers.includes("src/b.ts"), + "the other feature's file stays uncommitted", + ); const body = git(root, "log", "--format=%B", "-1", res.sha); assert.match(body, /feature\(feature-a\): implement a/); assert.match(body, /Feature: feature-a/); @@ -2057,7 +2063,11 @@ test("accept-commit slim path: already-committed features flip done with no new res.accepted.map((a) => a.feature), ["feature-a"], ); - assert.deepEqual(res.committed, [], "no new feature commit on the slim path"); + assert.deepEqual( + res.committed, + [], + "no new feature commit on the slim path", + ); const after = git(root, "rev-list", "--count", "HEAD"); assert.equal( Number(after) - Number(before), @@ -2079,7 +2089,10 @@ test("accept-commit slim path: already-committed features flip done with no new ); assert.ok(res.leftovers.includes("notes.txt"), "churn stays uncommitted"); // Untouched: feature-b keeps the full staging path on a later accept. - const res2 = applyOp({ op: "accept-commit", features: ["feature-b"] }, root); + const res2 = applyOp( + { op: "accept-commit", features: ["feature-b"] }, + root, + ); assert.equal(res2.committed.length, 1); assert.deepEqual(res2.accepted, []); } finally { @@ -2344,7 +2357,13 @@ test("accept-commit honors explicit skip and lands pre-staged baselines as chore .filter(Boolean), ["src/baseline.txt"], ); - git(root, "merge-base", "--is-ancestor", res.bootstrapCommit, res.committed[0].sha); + git( + root, + "merge-base", + "--is-ancestor", + res.bootstrapCommit, + res.committed[0].sha, + ); const feat = git( root, "show", @@ -2389,7 +2408,8 @@ test("accept-commit accepts implemented features and gates deps via review_requi // review_required on (default): the dependent cannot land first. assert.throws( - () => applyOp({ op: "accept-commit", features: ["auth-middleware"] }, root), + () => + applyOp({ op: "accept-commit", features: ["auth-middleware"] }, root), /waiting on: config-module/, ); // The implemented feature itself lands fine → done. @@ -2904,7 +2924,9 @@ test("loadBundle re-roots every gather/write into the plan's worktree", async () // Writes follow: a feature written "from main" lands in the worktree. applyOp(FEATURES_OP, root); assert.ok( - existsSync(join(planRes.worktree, "memory", "features", "config-module.md")), + existsSync( + join(planRes.worktree, "memory", "features", "config-module.md"), + ), "feature file written to the worktree bundle", ); assert.ok( From 67af0713164053fc3770af05d8a7015cf2ccf03f Mon Sep 17 00:00:00 2001 From: Christoph Maier Date: Fri, 17 Jul 2026 16:05:01 +0200 Subject: [PATCH 5/5] better feeatures --- lib/gather.mjs | 27 +++++++++++++++++++++++---- lib/write.mjs | 22 ++++++++++++++++++++-- memory/backlog/index.md | 10 +++------- memory/index.md | 1 + memory/log.md | 5 +++++ skills/iterator-feature/SKILL.md | 13 ++++++++++--- skills/iterator-implement/SKILL.md | 9 +++++---- skills/iterator/lib/gather.mjs | 27 +++++++++++++++++++++++---- skills/iterator/lib/write.mjs | 22 ++++++++++++++++++++-- test/gather.test.mjs | 3 +++ test/write.test.mjs | 25 +++++++++++++++++++++++++ 11 files changed, 138 insertions(+), 26 deletions(-) diff --git a/lib/gather.mjs b/lib/gather.mjs index e016504..c79e8e3 100644 --- a/lib/gather.mjs +++ b/lib/gather.mjs @@ -536,6 +536,25 @@ const implementContract = (c, concepts = []) => ({ relevantMemories: unionMemories(c, concepts), }); +// Per-concept ceiling for inlined bodies — a runaway concept file must not +// blow up the implement contract; the `path` stays readable for the rest. +const MAX_MEMORY_BODY = 6000; + +/** Concept body with the frontmatter stripped — the metadata (tags, anchors, + * timestamps) is machine bookkeeping the implementer doesn't need in context. */ +function memoryBody(path) { + let raw; + try { + raw = readFileSync(path, "utf8"); + } catch { + return ""; + } + const text = body(raw).trim(); + return text.length > MAX_MEMORY_BODY + ? `${text.slice(0, MAX_MEMORY_BODY)}\n… (truncated — read the file at \`path\` for the rest)` + : text; +} + function unionMemories(c, concepts) { const dynamic = relevantMemories(concepts, listy(c.fm.files)); const byId = new Map(); @@ -551,7 +570,9 @@ function unionMemories(c, concepts) { } } for (const m of dynamic) if (!byId.has(m.id)) byId.set(m.id, m); - return [...byId.values()]; + // Inline the stripped body so the implementer reads knowledge straight from + // the contract instead of round-tripping Read calls over raw files. + return [...byId.values()].map((m) => ({ ...m, body: memoryBody(m.path) })); } export function gatherImplement(startDir) { @@ -992,9 +1013,7 @@ export function gatherKnowledge(startDir) { // The concept body (frontmatter stripped) — the Knowledge browser's // read-in-place drawer renders it, so checking a decision never // requires leaving the tab. - body: raw.startsWith("---\n") - ? raw.slice(raw.indexOf("\n---", 4) + 4).trim() - : raw.trim(), + body: body(raw).trim(), }); } diff --git a/lib/write.mjs b/lib/write.mjs index e50f18d..6777f6c 100644 --- a/lib/write.mjs +++ b/lib/write.mjs @@ -649,6 +649,10 @@ function featureDoc(c, titles, existingReview) { ); } +// Soft ceiling on a feature's declared `files` before the writer warns: a +// vertical slice rarely changes more than this many files, tests included. +const MAX_FEATURE_FILES = 8; + function writeFeatures(payload, root) { const b = loadBundle(root); if (!b.plan) fail("no memory/plan.md — run the plan op first"); @@ -786,13 +790,25 @@ function writeFeatures(payload, root) { .filter((w) => w.globs.length) : []; + // An over-broad `files` list (warn, never fail): it inflates review + // ownership and saturates the memories anchor-match — usually the slice is + // too big, or the list is padded with reference-only/generated files. + const broadFiles = incoming + .filter((c) => !doneProtected.includes(c.name)) + .map((c) => ({ feature: c.name, count: listy(c.files).length })) + .filter((w) => w.count > MAX_FEATURE_FILES); + + const warnings = { + ...(unmatchedGlobs.length ? { unmatchedGlobs } : {}), + ...(broadFiles.length ? { broadFiles } : {}), + }; return { op: "features", written, skipped: doneProtected, deleted: deletes, ...(normalized.length ? { normalized } : {}), - ...(unmatchedGlobs.length ? { warnings: { unmatchedGlobs } } : {}), + ...(Object.keys(warnings).length ? { warnings } : {}), memoryDir: b.memDir, }; } @@ -3043,7 +3059,9 @@ const SCHEMAS = { "status?": "draft|pending (done is owned by accept-commit)", "size?": "small|medium|large", "dependsOn?": ["slug"], - files: ["path or glob — result.warnings.unmatchedGlobs flags typos"], + files: [ + "path or glob the feature will change (warnings.unmatchedGlobs flags typos; warnings.broadFiles flags >8 entries)", + ], "implementationNotes?": "string", "snippets?": [{ "lang?": "string", code: "string" }], "blastRadius?": "string", diff --git a/memory/backlog/index.md b/memory/backlog/index.md index 8d446a1..f42193e 100644 --- a/memory/backlog/index.md +++ b/memory/backlog/index.md @@ -2,14 +2,10 @@ type: Backlog title: Iterator backlog description: Saved ideas and bugs kept separate from active plan features. -items: "[{\"id\":\"improve-overall-styling\",\"title\":\"improve overall styling\",\"details\":\"In the center on top where the tabs are add iterator as title like iterator./planning in the head line. In the headlline replace iterator with the current work dir folder name\",\"kind\":\"idea\",\"selected\":false,\"created\":\"2026-07-16T14:46:33.605Z\",\"updated\":\"2026-07-17T13:46:36.058Z\"},{\"id\":\"show-in-settings-for-models-only-the-scoped-models\",\"title\":\"Show in settings for models only the scoped models\",\"details\":\"In the settings the selection of a model should only show the scoped models and not all models.\",\"kind\":\"idea\",\"selected\":false,\"created\":\"2026-07-16T14:47:34.877Z\",\"updated\":\"2026-07-17T13:46:36.796Z\"},{\"id\":\"improve-the-interaction-with-claude-code\",\"title\":\"Improve the interaction with claude code\",\"details\":\"THe server is only really usable by pi so claude relies on the skills. We need to improve the skills so that claude loads them correctly and follows the overall plan. So that it can use the ouput of plan or featuers for usage. Claude always implements then all like auto mode. And at the end the worktree is commited so that the user can see the changes.This is different from the pi workflow where the ui is the center piece.\",\"kind\":\"idea\",\"selected\":false,\"created\":\"2026-07-16T14:50:12.210Z\",\"updated\":\"2026-07-17T13:46:37.399Z\"},{\"id\":\"idea-backlog-layout\",\"title\":\"Idea backlog layout\",\"details\":\"The idea backlog needs better spacing and layout. also make the idead baclog list and retired plan list with finite size and vertical scrolling so its not a gigantic page.\",\"kind\":\"bug\",\"selected\":false,\"created\":\"2026-07-16T14:51:23.470Z\",\"updated\":\"2026-07-17T13:46:38.156Z\"},{\"id\":\"remove-elements-from-idea-backlog-when-the-plan-and-feature-is-created\",\"title\":\"remove elements from idea backlog when the plan and feature is created.\",\"details\":\"elements part of an accepted plan and feature should be removed so no old one stay.\",\"kind\":\"idea\",\"selected\":true,\"created\":\"2026-07-17T13:46:33.926Z\",\"updated\":\"2026-07-17T13:46:38.906Z\"}]" -timestamp: 2026-07-17T13:46:38.906Z +items: "[]" +timestamp: 2026-07-17T13:58:53.939Z --- # Backlog -* [idea] improve overall styling -* [idea] Show in settings for models only the scoped models -* [idea] Improve the interaction with claude code -* [bug] Idea backlog layout -* [idea] remove elements from idea backlog when the plan and feature is created. — selected +(empty) diff --git a/memory/index.md b/memory/index.md index 1f5f167..1801d32 100644 --- a/memory/index.md +++ b/memory/index.md @@ -10,6 +10,7 @@ last_memorized_commit: a5d59c50453e568ec486a3c9c017c2506898700e * [Design](design.md) - A compact dark developer control plane with clear workflow state. * [Backlog](backlog/index.md) - Saved ideas and bugs outside active plan features. * [Settings](settings.md) - Project settings (auto mode, models, git flow). +* [Features](features/) - One document per implementation feature. * [Architecture](/architecture/) - How the system is structured. * [Decisions](/decisions/) - Durable product and implementation choices agents should preserve. * [Patterns & Conventions](/patterns/) - How code and workflows are written in this repo. diff --git a/memory/log.md b/memory/log.md index 11046f9..19d6569 100644 --- a/memory/log.md +++ b/memory/log.md @@ -1,6 +1,11 @@ # iterator update log ## 2026-07-17 +* **Backlog**: delete remove-elements-from-idea-backlog-when-the-plan-and-feature-is-created. +* **Backlog**: delete idea-backlog-layout. +* **Backlog**: delete improve-the-interaction-with-claude-code. +* **Backlog**: delete show-in-settings-for-models-only-the-scoped-models. +* **Backlog**: delete improve-overall-styling. * **Retirement**: Plan "Remove accepted backlog ideas" condensed into [Consume selected backlog ideas on plan approval](/decisions/consume-accepted-backlog-ideas.md). * **Update**: Memorized [Unify Iterator dashboard and feature workflow](/decisions/iterator-dashboard-feature-workflow.md). * **Review**: Accepted [Consume accepted backlog ideas](/features/consume-accepted-backlog-ideas.md) (committed as feature(consume-accepted-backlog-ideas)). diff --git a/skills/iterator-feature/SKILL.md b/skills/iterator-feature/SKILL.md index 640a4ce..0badf0a 100644 --- a/skills/iterator-feature/SKILL.md +++ b/skills/iterator-feature/SKILL.md @@ -78,7 +78,12 @@ Split the whole plan (using `ARCHITECTURE.md` for context): enough that an implementer can build from notes + snippets + `ARCHITECTURE.md`. - Assign the `files` each feature owns (paths or simple globs), including its - test files. + test files — but **only files the implementation will actually create or + change**. Never list generated or synced copies (a build/sync step owns + them) and never files that are merely read for context. `files` drives + review ownership and the memories anchor-match, so padding it degrades + both; a feature needing more than ~8 files usually means the slice is too + broad or the list is padded. - **Cut along the recorded architecture.** When `architecture` is non-empty, prefer feature boundaries that follow those subsystem seams (a feature inside one concept's territory beats one straddling two), and seed each feature's @@ -114,8 +119,10 @@ FEATURES_WRITE The writer validates before writing (acyclic graph, existing `depends_on` targets, valid size, done features untouched) and on failure writes **nothing** -— fix the breakdown and re-pipe. Watch `warnings.unmatchedGlobs` in its -result: a feature glob matching no files usually means a typo'd path. +— fix the breakdown and re-pipe. Watch the result's warnings: +`warnings.unmatchedGlobs` (a glob matching no files usually means a typo'd +path) and `warnings.broadFiles` (a feature declaring more than 8 files — +tighten the list to what the feature changes, or split the feature). Then open the UI **from disk** — no hand-authored feature payload, ever: diff --git a/skills/iterator-implement/SKILL.md b/skills/iterator-implement/SKILL.md index 2ce0d1d..7405499 100644 --- a/skills/iterator-implement/SKILL.md +++ b/skills/iterator-implement/SKILL.md @@ -70,10 +70,11 @@ Prefer keeping edits inside the feature's `files`; when the work genuinely requires touching other paths, that is fine — the review will show them as this feature's incidental changes. -**Memory first:** before coding, read the files in the contract's -`relevantMemories` (the feature's stored `memories:` reading list unioned with -a fresh anchor match; each entry carries the absolute `path` of one knowledge -concept) — and ONLY those; never crawl all of `memory/`. Treat `pitfalls/*` +**Memory first:** before coding, read the contract's `relevantMemories` (the +feature's stored `memories:` reading list unioned with a fresh anchor match). +Each entry inlines the concept's `body` with the frontmatter already stripped +— read the bodies straight from the contract; open the file at `path` only +when a body is marked truncated. Never crawl all of `memory/`. Treat `pitfalls/*` entries as constraints (a known sharp edge in exactly the files you are about to change), `architecture/*` and `patterns/*` as how the surrounding code expects to be extended. An empty list means no anchored knowledge — proceed diff --git a/skills/iterator/lib/gather.mjs b/skills/iterator/lib/gather.mjs index e016504..c79e8e3 100644 --- a/skills/iterator/lib/gather.mjs +++ b/skills/iterator/lib/gather.mjs @@ -536,6 +536,25 @@ const implementContract = (c, concepts = []) => ({ relevantMemories: unionMemories(c, concepts), }); +// Per-concept ceiling for inlined bodies — a runaway concept file must not +// blow up the implement contract; the `path` stays readable for the rest. +const MAX_MEMORY_BODY = 6000; + +/** Concept body with the frontmatter stripped — the metadata (tags, anchors, + * timestamps) is machine bookkeeping the implementer doesn't need in context. */ +function memoryBody(path) { + let raw; + try { + raw = readFileSync(path, "utf8"); + } catch { + return ""; + } + const text = body(raw).trim(); + return text.length > MAX_MEMORY_BODY + ? `${text.slice(0, MAX_MEMORY_BODY)}\n… (truncated — read the file at \`path\` for the rest)` + : text; +} + function unionMemories(c, concepts) { const dynamic = relevantMemories(concepts, listy(c.fm.files)); const byId = new Map(); @@ -551,7 +570,9 @@ function unionMemories(c, concepts) { } } for (const m of dynamic) if (!byId.has(m.id)) byId.set(m.id, m); - return [...byId.values()]; + // Inline the stripped body so the implementer reads knowledge straight from + // the contract instead of round-tripping Read calls over raw files. + return [...byId.values()].map((m) => ({ ...m, body: memoryBody(m.path) })); } export function gatherImplement(startDir) { @@ -992,9 +1013,7 @@ export function gatherKnowledge(startDir) { // The concept body (frontmatter stripped) — the Knowledge browser's // read-in-place drawer renders it, so checking a decision never // requires leaving the tab. - body: raw.startsWith("---\n") - ? raw.slice(raw.indexOf("\n---", 4) + 4).trim() - : raw.trim(), + body: body(raw).trim(), }); } diff --git a/skills/iterator/lib/write.mjs b/skills/iterator/lib/write.mjs index e50f18d..6777f6c 100644 --- a/skills/iterator/lib/write.mjs +++ b/skills/iterator/lib/write.mjs @@ -649,6 +649,10 @@ function featureDoc(c, titles, existingReview) { ); } +// Soft ceiling on a feature's declared `files` before the writer warns: a +// vertical slice rarely changes more than this many files, tests included. +const MAX_FEATURE_FILES = 8; + function writeFeatures(payload, root) { const b = loadBundle(root); if (!b.plan) fail("no memory/plan.md — run the plan op first"); @@ -786,13 +790,25 @@ function writeFeatures(payload, root) { .filter((w) => w.globs.length) : []; + // An over-broad `files` list (warn, never fail): it inflates review + // ownership and saturates the memories anchor-match — usually the slice is + // too big, or the list is padded with reference-only/generated files. + const broadFiles = incoming + .filter((c) => !doneProtected.includes(c.name)) + .map((c) => ({ feature: c.name, count: listy(c.files).length })) + .filter((w) => w.count > MAX_FEATURE_FILES); + + const warnings = { + ...(unmatchedGlobs.length ? { unmatchedGlobs } : {}), + ...(broadFiles.length ? { broadFiles } : {}), + }; return { op: "features", written, skipped: doneProtected, deleted: deletes, ...(normalized.length ? { normalized } : {}), - ...(unmatchedGlobs.length ? { warnings: { unmatchedGlobs } } : {}), + ...(Object.keys(warnings).length ? { warnings } : {}), memoryDir: b.memDir, }; } @@ -3043,7 +3059,9 @@ const SCHEMAS = { "status?": "draft|pending (done is owned by accept-commit)", "size?": "small|medium|large", "dependsOn?": ["slug"], - files: ["path or glob — result.warnings.unmatchedGlobs flags typos"], + files: [ + "path or glob the feature will change (warnings.unmatchedGlobs flags typos; warnings.broadFiles flags >8 entries)", + ], "implementationNotes?": "string", "snippets?": [{ "lang?": "string", code: "string" }], "blastRadius?": "string", diff --git a/test/gather.test.mjs b/test/gather.test.mjs index 2adda60..a750b88 100644 --- a/test/gather.test.mjs +++ b/test/gather.test.mjs @@ -807,6 +807,9 @@ test("implement contracts carry relevantMemories anchored to each feature, pitfa mem.path.endsWith(join("memory", "architecture", "auth-shape.md")), "path is readable directly", ); + // The concept body is inlined with the frontmatter stripped — the + // implementer reads knowledge from the contract, not from raw files. + assert.equal(mem.body, "body"); // A pitfall anchored to the same files sorts before architecture. writeFileSync( diff --git a/test/write.test.mjs b/test/write.test.mjs index e94187e..c8ebe3a 100644 --- a/test/write.test.mjs +++ b/test/write.test.mjs @@ -2151,6 +2151,31 @@ test("features op warns about globs that match nothing in the repo", () => { } }); +test("features op warns when a feature declares an over-broad files list", () => { + const root = makeWaveRepo(); + try { + const res = applyOp( + { + op: "features", + features: [ + { + name: "broad-feature", + description: "d", + files: Array.from({ length: 9 }, (_, i) => `src/f${i}.ts`), + }, + ], + }, + root, + ); + assert.deepEqual(res.warnings.broadFiles, [ + { feature: "broad-feature", count: 9 }, + ]); + assert.equal(res.validation.ok, true); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test("plan op warns on todo-shaped dependencies and uninitialized knowledge", () => { const root = makeRepo(); try {