Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
30c6a6b
feature(preserve-review-across-planning): preserve active reviews dur…
Christoph Jul 17, 2026
47646de
chore(iterator): record feature commit
Christoph Jul 17, 2026
dd02899
feature(preserve-review-across-planning): make review navigation canc…
Christoph Jul 17, 2026
4d4477e
chore(iterator): record feature commit
Christoph Jul 17, 2026
43d0fcb
chore(iterator): record feature commits and memory updates
Christoph Jul 17, 2026
e0b0453
feature(show-active-work-in-work): move active feature context to Work
Christoph Jul 17, 2026
8f90ea0
chore(iterator): record feature commit
Christoph Jul 17, 2026
e0fba14
chore(iterator): record feature commits and memory updates
Christoph Jul 17, 2026
e7d5824
feature(streamline-review-interface): keep review labels readable and…
Christoph Jul 17, 2026
9b0d606
chore(iterator): record feature commit
Christoph Jul 17, 2026
216f360
chore(iterator): record feature commits and memory updates
Christoph Jul 17, 2026
c5b75e3
implemented bug fixes
Christoph Jul 17, 2026
ff50833
updated memroy
Christoph Jul 17, 2026
f4ecbdc
feature(reset-plan-runtime-state): reset runtime state for approved p…
Christoph Jul 17, 2026
362043a
chore(iterator): record feature commit
Christoph Jul 17, 2026
e61895d
chore(iterator): record feature commits and memory updates
Christoph Jul 17, 2026
4b007e5
feature(apply-role-models-manual-turns): apply configured role models…
Christoph Jul 17, 2026
3adee48
chore(iterator): record feature commit
Christoph Jul 17, 2026
c7157f5
feature(apply-role-models-manual-turns): apply planner role before bu…
Christoph Jul 17, 2026
817cd24
chore(iterator): record feature commit
Christoph Jul 17, 2026
e5c95c4
chore(iterator): record feature commits and memory updates
Christoph Jul 17, 2026
ca73b65
features
Christoph Jul 17, 2026
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
25 changes: 22 additions & 3 deletions extensions/iterator.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
nextFeatureWaveAction,
pauseFeatureWave,
projectRoot,
roleFromInput,
roleModelSpec,
runJson,
scriptPath,
Expand Down Expand Up @@ -511,6 +512,8 @@ export default function iteratorExtension(pi) {
let autoSteps = 0;
let featureWave = null; // fixed ready-feature snapshot; review stays manual
let preAutoModel = null; // the user's model before the first role switch
let pendingRole = null; // exact role command captured from the current input
let manualRoleActive = false; // this turn switched a manual role model

const notifyUi = (msg, level = "info") => {
if (lastCtx?.hasUI) lastCtx.ui.notify(`iterator: ${msg}`, level);
Expand Down Expand Up @@ -564,7 +567,7 @@ export default function iteratorExtension(pi) {
}
};

/** Restore the user's pre-auto model on done/escalate/pause. */
/** Restore the user's model after an automatic or manual role turn. */
const restoreModel = async () => {
if (!preAutoModel) return;
const m = preAutoModel;
Expand Down Expand Up @@ -679,7 +682,12 @@ export default function iteratorExtension(pi) {
if (!action) return;

if (action.done) {
await writeState({ phase: "done", active_feature: null });
await writeState({
mode: "manual",
paused: false,
phase: "done",
active_feature: null,
});
autoSteps = 0;
await restoreModel();
notifyUi(
Expand Down Expand Up @@ -1449,8 +1457,14 @@ export default function iteratorExtension(pi) {
pi.on("before_agent_start", async (_event, ctx) => {
rememberCtx(ctx);
try {
const { hub, implement, settings, state } = await gatherSession(ctx.cwd);
if (pendingRole && state?.mode !== "auto" && !featureWave) {
await applyRole(pendingRole, settings);
manualRoleActive = true;
}
// Model selection also applies to /iterator-plan before a bundle exists;
// only the ambient bundle context depends on durable plan state.
if (!bundleExists(ctx.cwd)) return undefined;
const { hub, implement } = await gatherSession(ctx.cwd);
let matched = [];
if (recentFiles.size) {
const knowledge = await gatherPayload(ctx.cwd, "knowledge");
Expand Down Expand Up @@ -1484,6 +1498,7 @@ export default function iteratorExtension(pi) {
let usageBuffer = [];

pi.on("input", async (event) => {
pendingRole = roleFromInput(event.text);
const a = attributionFromInput(event.text);
if (a) attribution = a;
});
Expand Down Expand Up @@ -1567,6 +1582,10 @@ export default function iteratorExtension(pi) {
rememberCtx(ctx);
invalidateSession(); // the turn may have changed files/commits
await flushUsage(ctx.cwd);
if (manualRoleActive) {
manualRoleActive = false;
await restoreModel();
}
await refreshHub(ctx.cwd);
await refreshStatus(ctx);
// Keep abortPending set until this stale agent_end reaches its final
Expand Down
17 changes: 17 additions & 0 deletions lib/pi-tools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,23 @@ export function roleModelSpec(settings, role) {
// ---------------------------------------------------------------------------
// Token-usage attribution (pi turn_end capture → usage op rows)

const ROLE_MAP = {
"iterator-plan": "planner",
"iterator-test": "tester",
"iterator-implement": "implementer",
"iterator-next": "implementer",
"iterator-review": "reviewer",
"iterator-review-plan": "plan_reviewer",
};

/** The role model for an exact Iterator command, or null for other input. */
export function roleFromInput(text) {
const match = String(text || "")
.trim()
.match(/^\/(?:skill:)?([a-z-]+)(?=\s|$)/);
return match ? (ROLE_MAP[match[1]] ?? null) : null;
}

const ATTRIBUTION_MAP = {
"iterator-plan": "plan",
"iterator-feature": "feature",
Expand Down
10 changes: 9 additions & 1 deletion lib/session-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,12 @@ document.getElementById('ov-pause').addEventListener('click', () => control(paus
document.getElementById('ov-abort').addEventListener('click', () => control('abort'));
es.onopen = () => { conn.style.display = 'none'; };
es.onerror = () => { conn.style.display = 'block'; };
// Embedded views never own pagehide cancellation: switching tabs replaces the
// iframe. The persistent shell owns cancellation and beacons only when the
// whole dashboard unloads; reload grace on the server preserves normal reloads.
window.addEventListener('pagehide', () => {
try { navigator.sendBeacon('/cancel', '{}'); } catch (e) {}
});
setTab(tab, 0);
</script>
</body>
Expand Down Expand Up @@ -497,11 +503,13 @@ export function createSessionServer({ onUnsolicited, onControl, log } = {}) {
res.writeHead(204);
res.end();
if (r && r !== RUN_ID) return; // an outgoing iframe's pagehide beacon
if (!pending || cancelTimer) return;
if (!pending) return;
if (url.searchParams.get("now") === "1") {
clearCancelGrace();
settle({ type: "cancel" });
return;
}
if (cancelTimer) return;
// Held: a reload (GET / or /view) within the grace window keeps the
// round alive; a really-closed tab lets the timer fire.
cancelTimer = setTimeout(() => {
Expand Down
5 changes: 4 additions & 1 deletion lib/ui.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,10 @@ const SHARED_JS = `
// server ignore /cancel-/submit from tabs that belong to an earlier round.
function __q(path){ return path + (path.indexOf('?')>=0 ? '&' : '?') + 'r=' + __RUN; }
let __submitted = false;
function sendCancel(){ if(__submitted) return; __submitted = true;
function sendCancel(){
// In Pi's persistent session, the parent shell owns unload cancellation.
// An embedded view unloads on every tab switch and must never end the round.
if(__submitted || window.parent !== window) return; __submitted = true;
try{ navigator.sendBeacon(__q('/cancel'),'{}'); }catch(e){} }
window.addEventListener('pagehide', sendCancel);
async function cancelFlow(){ __submitted = true;
Expand Down
33 changes: 24 additions & 9 deletions lib/views/hub.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/**
* iterator: Work dashboard UI on the shared shell (../ui.mjs,
* ../server.mjs). The execution surface of the flow: the active plan's
* progress, the escalation banner, and the feature cards with their
* Test / Implement / Review actions. Plan management — backlog, plan
* creation/revision/retirement, the dependency graph, feature cancellation —
* lives on the planning surface (./planning.mjs); both render from the same
* gather payload so they can never disagree about state.
* progress, dependency graph, and feature cards with their Test / Implement /
* Review actions. Planning owns backlog and plan-lifecycle controls; active
* plan context and feature management live here. Both surfaces render from
* the same gather payload so they can never disagree about state.
*
* input: { step:"hub", branch,
* plan: { title, status } | null, // null = no bundle yet
Expand All @@ -21,12 +20,13 @@
* retired: [ { name, title, created } ] // archived plans, newest first
* output: one JSON line to stdout —
* { type:"action", action:"planning"|"test"|"implement"|"review"|"review-all"|"implement-wave"|"auto-implement"
* |"escalation-restart"|"escalation-guide",
* |"cancel-feature"|"escalation-restart"|"escalation-guide",
* feature:"<slug>"|null,
* prompt:"<guidance>"|null } // escalation-guide only
* plus the shared { type:"cancel" } / { type:"timeout" }.
*/
import { renderPage } from "../ui.mjs";
import { GRAPH_CSS, GRAPH_JS } from "./graph.mjs";
import { WIDGETS_CSS, WIDGETS_JS } from "./widgets.mjs";

const CSS = `
Expand Down Expand Up @@ -147,6 +147,14 @@ function render(){
}
w.appendChild(bar);

// Active dependency structure belongs beside execution status. Readiness and
// dependencies are supplied by gather; this view only renders them.
const gt = document.createElement('div'); gt.className='sec-title'; gt.textContent='Dependency graph';
const cw = document.createElement('div'); cw.id='cyclewarn';
const g = document.createElement('div'); g.className='graph'; g.id='graph';
w.appendChild(gt); w.appendChild(cw); w.appendChild(g);
renderGraphInto(g, cw, CH, 'fix depends-on in /iterator-feature before implementing.');

// cards
const ct = document.createElement('div'); ct.className='sec-title'; ct.textContent='Features';
w.appendChild(ct);
Expand Down Expand Up @@ -214,7 +222,14 @@ function makeCard(c){
else { rev.disabled = true; rev.title = 'Implement first — review unlocks once the feature is implemented'; }
rev.addEventListener('click', () => action('review', c.name, 'Starting /iterator-review'));

btns.appendChild(impl); btns.appendChild(test); btns.appendChild(rev);
const cancel = document.createElement('button');
cancel.className = 'act danger';
cancel.textContent = 'Cancel feature';
cancel.title = 'Remove this feature from the plan (its file is archived; dependents are unwired)';
confirmButton(cancel, 'Really cancel \\u2014 click again', () =>
action('cancel-feature', c.name, 'Cancelling feature'));

btns.appendChild(impl); btns.appendChild(test); btns.appendChild(rev); btns.appendChild(cancel);
card.appendChild(btns);
return card;
}
Expand All @@ -227,9 +242,9 @@ export function render(data) {
branch: data.branch,
title: data.plan && data.plan.title,
data,
css: WIDGETS_CSS + CSS,
css: WIDGETS_CSS + GRAPH_CSS + CSS,
body: BODY,
clientJs: WIDGETS_JS + JS,
clientJs: WIDGETS_JS + GRAPH_JS + JS,
primary: false, // the per-card action buttons are the primaries here
cancel: false, // idle dashboard — there is no round to cancel
});
Expand Down
65 changes: 6 additions & 59 deletions lib/views/planning.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/**
* iterator: planning UI on the shared shell (../ui.mjs, ../server.mjs).
* The plan-management surface: where ideas and bugs are collected (backlog),
* plans are created, revised, featured, reviewed, retired, or cancelled, and
* the feature set is inspected (read-only cards + dependency graph). The
* execution side — Test / Implement / Review — lives on the Work surface
* (./hub.mjs).
* plans are created, revised, featured, reviewed, retired, or cancelled.
* Active plan features, dependency structure, and execution controls live on
* the Work surface (./hub.mjs).
*
* Rendered from the SAME gather payload as the hub (gather --step planning
* just stamps step:"planning"), so the two surfaces can never disagree about
Expand All @@ -13,13 +12,12 @@
*
* output: one JSON line to stdout —
* { type:"action", action:"plan"|"feature"|"review-plan"|"retire"|
* "cancel-plan"|"cancel-feature"|"iterator-init"|"view-archive",
* "cancel-plan"|"iterator-init"|"view-archive",
* feature, prompt }
* { type:"backlog", action:"create"|"edit"|"delete"|"select", ... }
* plus the shared { type:"cancel" } / { type:"timeout" }.
*/
import { renderPage } from "../ui.mjs";
import { GRAPH_CSS, GRAPH_JS } from "./graph.mjs";
import { WIDGETS_CSS, WIDGETS_JS } from "./widgets.mjs";

const PLANNING_CSS = `
Expand Down Expand Up @@ -243,28 +241,6 @@ function render(){
bar.appendChild(cancelPlan);
w.appendChild(bar);
renderBacklog(w);

// graph
const gt = document.createElement('div'); gt.className='sec-title'; gt.textContent='Dependency graph';
const cw = document.createElement('div'); cw.id='cyclewarn';
const g = document.createElement('div'); g.className='graph'; g.id='graph';
w.appendChild(gt); w.appendChild(cw); w.appendChild(g);
renderGraphInto(g, cw, CH, 'fix depends-on in /iterator-feature before implementing.');

// Read-only feature cards: state at a glance + Cancel; the action buttons
// (Test / Implement / Review) live on the Work tab.
const ct = document.createElement('div'); ct.className='sec-title'; ct.textContent='Features';
w.appendChild(ct);
if(!CH.length){
const e = document.createElement('div'); e.className='hero';
e.innerHTML = '<h2>No features yet</h2><p>The plan has not been broken into features.</p>';
const b = document.createElement('button');
b.className='act primary-act'; b.textContent='Feature the plan';
b.addEventListener('click', () => action('feature', null, 'Starting /iterator-feature'));
e.appendChild(b); w.appendChild(e);
return;
}
CH.forEach(c => w.appendChild(makeCard(c)));
renderRetired(w);
}

Expand Down Expand Up @@ -340,35 +316,6 @@ function renderRetired(w){
w.appendChild(section);
}

function makeCard(c){
const card = document.createElement('div');
card.className = 'card' + (c.status==='done'?' done':'');
const draft = c.status==='draft';
const implemented = c.status==='implemented';
const icon = '<i class="st '+(c.status==='done'?'done':draft?'draft':implemented?'implemented':'pending')+'"></i>';
card.innerHTML =
'<div class="ch"><span class="cn">'+icon+esc(c.title||c.name)+'</span>'+
'<span class="chip cmut">'+esc(c.name)+'</span>'+
(draft?'<span class="chip cy">draft</span>':'')+
(implemented?'<span class="chip cy">implemented \\u2014 awaiting review</span>':'')+
sizeChip(c)+testBadge(c)+
(c.conflicts?'<span class="chip cr" title="This feature contradicts a project decision — check its Decision conflicts section">\\u26a0 '+c.conflicts+' decision conflict'+(c.conflicts!==1?'s':'')+'</span>':'')+'</div>'+
'<div class="cdesc">'+esc(c.description||'')+'</div>'+
((c.dependsOn&&c.dependsOn.length)?'<div class="deps">depends on '+c.dependsOn.map(d=>'<code>'+esc(d)+'</code>').join(' ')+
((c.ready===false)?' <span class="chip cy">waiting on '+esc((c.waitingOn||[]).join(', '))+'</span>':'')+'</div>':'');

const btns = document.createElement('div');
btns.className = 'btns';
const cancel = document.createElement('button');
cancel.className = 'act danger';
cancel.textContent = 'Cancel feature';
cancel.title = 'Remove this feature from the plan (its file is archived; dependents are unwired)';
confirmButton(cancel, 'Really cancel \\u2014 click again', () =>
action('cancel-feature', c.name, 'Cancelling feature'));
btns.appendChild(cancel);
card.appendChild(btns);
return card;
}
`;

export function render(data) {
Expand All @@ -378,9 +325,9 @@ export function render(data) {
branch: data.branch,
title: data.plan && data.plan.title,
data,
css: WIDGETS_CSS + PLANNING_CSS + GRAPH_CSS,
css: WIDGETS_CSS + PLANNING_CSS,
body: BODY,
clientJs: WIDGETS_JS + GRAPH_JS + JS,
clientJs: WIDGETS_JS + JS,
primary: false, // the inline buttons are the primaries here
cancel: false, // idle dashboard — there is no round to cancel
});
Expand Down
Loading
Loading