Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions lib/gather.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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) {
Expand Down Expand Up @@ -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(),
});
}

Expand Down
2 changes: 1 addition & 1 deletion lib/views/planning.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
67 changes: 59 additions & 8 deletions lib/write.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -450,11 +457,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.
Expand Down Expand Up @@ -536,7 +557,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
Expand Down Expand Up @@ -622,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");
Expand Down Expand Up @@ -759,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,
};
}
Expand Down Expand Up @@ -1561,7 +1604,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);
}

Expand Down Expand Up @@ -2453,7 +2499,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();
Expand Down Expand Up @@ -2523,7 +2570,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`,
);
}

/**
Expand Down Expand Up @@ -3010,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",
Expand Down
9 changes: 3 additions & 6 deletions memory/backlog/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +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\":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: "[]"
timestamp: 2026-07-17T13:58:53.939Z
---

# 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
(empty)
24 changes: 24 additions & 0 deletions memory/decisions/consume-accepted-backlog-ideas.md
Original file line number Diff line number Diff line change
@@ -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).
3 changes: 2 additions & 1 deletion memory/decisions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

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.
* [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.
12 changes: 5 additions & 7 deletions memory/decisions/iterator-dashboard-feature-workflow.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
type: Feature
title: Consume accepted backlog ideas
description: Remove selected backlog candidates atomically when their plan and resulting feature set are accepted.
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:53:47.894Z"
tags: []
commits:
- sha: b2d4863bbdf801d70d9d6e05b0516d983b11bdf0
kind: implement
date: 2026-07-17
done: 2026-07-17
---

# 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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Features

* [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.
Loading
Loading