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
6 changes: 0 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ on:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm test
51 changes: 39 additions & 12 deletions extensions/iterator.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { relative, resolve } from "node:path";
import { Type } from "typebox";

import { matchConcepts, OKF_AREA_NAMES } from "../lib/bundle.mjs";
import { dispatchAfterRefresh } from "../lib/dashboard-dispatch.mjs";
import { hydrateMemoryCards } from "../lib/gather.mjs";
import {
checkBashCommit,
Expand All @@ -64,12 +65,14 @@ import {
implementationCommand,
implementationHandoffState,
mergePayload,
modelSwitchSucceeded,
nextAutoAction,
nextFeatureWaveAction,
pauseFeatureWave,
projectRoot,
roleFromInput,
roleModelSpec,
resolveRoleModel,
runJson,
shouldApplyRole,
scriptPath,
Expand Down Expand Up @@ -664,14 +667,23 @@ export default function iteratorExtension(pi) {
let switchedModel = false;
try {
if (spec.model) {
const slash = spec.model.indexOf("/");
const provider = spec.model.slice(0, slash);
const id = spec.model.slice(slash + 1);
const m = lastCtx?.modelRegistry?.find?.(provider, id);
if (m) {
const target = resolveRoleModel(
spec.model,
lastCtx?.model,
preAutoModel,
lastCtx?.modelRegistry,
);
if (!target) {
notifyUi(
`unknown model ${spec.model} for ${role} — staying on the active model`,
"warning",
);
} else if (target.switchRequired) {
const previousModel = preAutoModel || lastCtx?.model || null;
const ok = await pi.setModel(m);
if (ok) {
const result = await pi.setModel(target.model);
// Modern Pi resolves void on success and throws on missing auth;
// retain explicit false support for older extension runtimes.
if (modelSwitchSucceeded(result)) {
if (!preAutoModel) preAutoModel = previousModel;
switchedModel = true;
} else {
Expand All @@ -680,12 +692,9 @@ export default function iteratorExtension(pi) {
"warning",
);
}
} else {
notifyUi(
`unknown model ${spec.model} for ${role} — staying on the active model`,
"warning",
);
}
// Otherwise the configured identity is already active. Keeping that
// exact object preserves Pi's host proxy routing and credentials.
}
if (spec.thinking) pi.setThinkingLevel(spec.thinking);
} catch (e) {
Expand Down Expand Up @@ -1158,6 +1167,24 @@ export default function iteratorExtension(pi) {
}
const cmd = actionToCommand(result);
if (!cmd) return;
if (result.action === "plan") {
// Planning creates active work: deliberately land on Work before
// showing its owned overlay and starting the planner. A broken
// refresh is advisory; it must never swallow the requested action.
void dispatchAfterRefresh(
() => refreshHub(ctxCwd(), { activateWork: true }),
() => {
session.showWorking(`Dispatched ${cmd} — Agent is working…`);
dispatch(cmd);
},
(error) =>
notifyUi(
`could not activate Work before planning: ${error.message}`,
"warning",
),
);
return;
}
session.showWorking(`Dispatched ${cmd} — Agent is working…`);
dispatch(cmd);
},
Expand Down
16 changes: 16 additions & 0 deletions lib/dashboard-dispatch.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export async function dispatchAfterRefresh(
refresh,
dispatch,
onRefreshError = () => {},
) {
try {
await refresh();
} catch (error) {
try {
onRefreshError(error);
} catch {
// Reporting a dashboard refresh failure must not block the action.
}
}
return dispatch();
}
50 changes: 48 additions & 2 deletions lib/pi-tools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -485,10 +485,56 @@ export function nextAutoAction(sessionPayload, settings, state) {
};
}

/** Parse the persisted provider/model identity without losing slashes in ids. */
export function parseModelSpec(spec) {
const value = String(spec || "");
const slash = value.indexOf("/");
if (slash <= 0 || slash === value.length - 1) return null;
return { provider: value.slice(0, slash), id: value.slice(slash + 1) };
}

/** Whether a runtime model carries the persisted provider/model identity. */
export function modelMatchesSpec(model, spec) {
const identity = parseModelSpec(spec);
return Boolean(
identity &&
model?.provider === identity.provider &&
model?.id === identity.id,
);
}

/**
* Resolve a configured role model without discarding host-owned runtime model
* metadata (proxy base URLs, managed credentials, headers). The active model
* wins, then the model saved for restoration, and only then the registry.
*/
export function resolveRoleModel(
spec,
activeModel,
restoreModel,
modelRegistry,
) {
const identity = parseModelSpec(spec);
if (!identity) return null;
if (modelMatchesSpec(activeModel, spec)) {
return { model: activeModel, switchRequired: false };
}
if (modelMatchesSpec(restoreModel, spec)) {
return { model: restoreModel, switchRequired: true };
}
const model = modelRegistry?.find?.(identity.provider, identity.id) || null;
return model ? { model, switchRequired: true } : null;
}

/** Modern Pi resolves void on success; older runtimes may return a boolean. */
export function modelSwitchSucceeded(result) {
return result !== false;
}

/**
* The model/thinking overrides for a role turn: null fields mean "leave the
* session as-is" ('active'). The extension resolves the model string against
* ctx.modelRegistry and applies pi.setModel/pi.setThinkingLevel.
* session as-is" ('active'). The extension resolves the model string while
* preserving matching runtime model objects, then applies model/thinking.
*/
export function roleModelSpec(settings, role) {
const model = settings?.[`${role}_model`];
Expand Down
6 changes: 3 additions & 3 deletions memory/backlog/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
type: Backlog
title: Iterator backlog
description: Saved ideas and bugs kept separate from active plan features.
items: "[]"
timestamp: 2026-07-20T17:48:20.856Z
items: "[{\"id\":\"after-initial-install-no-update-possible\",\"title\":\"after initial install no update possible\",\"details\":\"it returns that it cannot find the latest version after new start in a repo each time.\",\"kind\":\"bug\",\"selected\":false,\"created\":\"2026-07-21T14:03:15.367Z\",\"updated\":\"2026-07-21T14:03:15.368Z\"}]"
timestamp: 2026-07-21T14:03:15.368Z
---

# Backlog

(empty)
* [bug] after initial install no update possible
26 changes: 26 additions & 0 deletions memory/decisions/auto-plan-review-terminal-reset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
type: Decision
title: Auto plan review resets runtime state
description: The recorded terminal auto plan review is the durable boundary that returns auto mode and the Work surface to a completed manual state.
status: accepted
date: 2026-07-21
tags: [auto-mode, plan-review, runtime-state, dashboard]
files: ["lib/write.mjs", "lib/pi-tools.mjs", "extensions/iterator.js", "test/write.test.mjs", "test/pi-tools.test.mjs", "test/extension-model-lifecycle.test.mjs"]
timestamp: 2026-07-21T09:00:21.635Z
---

## Decision

Treat a successful agent-authored `record-plan-review` as the durable terminal boundary for an automatic run. It clears auto runtime ownership before the next driver tick; manual plan reviews retain their existing state.

The extension then converges idempotently: it restores any role model once, clears only its own Work overlay, and refreshes from server-derived state. A completed plan remains available for explicit retirement rather than being retired automatically.

## Rationale

Persisting the terminal reset at the review-recording boundary prevents a finishing agent turn or refresh race from leaving the dashboard stuck on “Auto: plan-review”. Keeping the extension as a convergence layer preserves `lib/status.mjs` as the lifecycle authority and avoids browser-local state inference.

# Retired plan

Condensed from plan "Finish auto mode after plan review" (1 features, archived under /features/archive/2026-07-20-auto-plan-review-terminal-reset/).

Token usage: 2040732 in / 21484 out / 15654400 cache-read / 0 cache-write over 75 turns (per-step breakdown in the archived usage.md).
2 changes: 2 additions & 0 deletions memory/decisions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
Durable product and implementation choices agents should preserve.

* [Apply role models to manual turns and reset stale runtime state](/decisions/manual-role-models-and-runtime-reset.md) - Manual Iterator role commands temporarily select configured models, while approved plans and terminal auto runs reset runtime state deterministically.
* [Auto plan review resets runtime state](/decisions/auto-plan-review-terminal-reset.md) - The recorded terminal auto plan review is the durable boundary that returns auto mode and the Work surface to a completed manual state.
* [Backlog planning and parallel feature waves](/decisions/backlog-planning-and-feature-waves.md) - Keep low-risk backlog editing available during active work while implementing a fixed ready-feature wave and reviewing its commit-backed results together.
* [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.
* [Focus feature execution and dashboard ownership](/decisions/focus-feature-execution-and-dashboard-ownership.md) - Implementation now starts in fresh feature sessions, Work owns active plans, Settings preserves context as a shell modal, and red tests are explicit handoffs.
* [Keep dependency-free CI minimal](/decisions/minimal-dependency-free-ci.md) - For this dependency-free Node.js package, CI runs the repository test suite once without an unnecessary Node matrix or setup step.
* [Memory relevance, usage costs, and dashboard recovery](/decisions/memory-relevance-usage-and-dashboard-recovery.md) - Iterator now bounds implementation context, keeps knowledge/retirement changes reviewed, prices usage only from project-owned rates, and maintains an authoritative active-work dashboard state.
* [Parallel feature waves and consolidated review](/decisions/parallel-feature-waves-and-consolidated-review.md) - The dashboard supports fixed dependency-ready implementation waves and commit-backed multi-feature review without weakening explicit acceptance.
* [Persist budget prices across plans](/decisions/persistent-budget-prices.md) - Project-owned Budget rates now persist across plans while active and archived cost reports remain reproducible.
Expand Down
24 changes: 24 additions & 0 deletions memory/decisions/minimal-dependency-free-ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
type: Decision
title: Keep dependency-free CI minimal
description: For this dependency-free Node.js package, CI runs the repository test suite once without an unnecessary Node matrix or setup step.
status: accepted
date: 2026-07-21
tags: [ci, github-actions, nodejs, testing]
files: [".github/workflows/ci.yml"]
timestamp: 2026-07-21T09:35:38.104Z
---

## Decision

Keep the GitHub Actions workflow as a single `ubuntu-latest` job that checks out the repository and runs `npm test` for pushes to `main` and pull requests. Do not add a Node version matrix, `actions/setup-node`, or dependency-install step while the test suite has no installed dependencies and uses only the runner-provided Node.js runtime.

## Rationale

The previous Node 18/20/22 matrix repeated the same dependency-free test suite three times, and explicit Node setup added no value. A minimal workflow provides the required regression backstop with less CI time and fewer failure points. Revisit this decision if the project gains runtime-specific compatibility requirements or install-time test dependencies.

# Retired plan

Condensed from plan "Simplify the failing CI pipeline" (1 features, archived under /features/archive/2026-07-21-minimal-dependency-free-ci/).

Token usage: 281758 in / 5588 out / 1730560 cache-read / 0 cache-write over 52 turns (per-step breakdown in the archived usage.md).
48 changes: 48 additions & 0 deletions memory/features/activate-work-on-plan-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
type: Feature
title: Activate Work when planning starts
description: Starting a plan from Planning immediately opens the Work progress surface while preserving the later plan review and approval landings.
status: done
size: small
depends_on: []
files: ["extensions/iterator.js", "test/extension-work-activation.test.mjs"]
memories: [architecture/package-and-skill-layout, decisions/auto-plan-review-terminal-reset, decisions/backlog-planning-and-feature-waves, decisions/code-exact-red-test-review-and-agent-wording, decisions/focus-feature-execution-and-dashboard-ownership, decisions/iterator-dashboard-feature-workflow, decisions/manual-role-models-and-runtime-reset, decisions/memory-relevance-usage-and-dashboard-recovery]
timestamp: "2026-07-21T14:06:04.307Z"
tags: []
commits:
- sha: eadb89bf9ca7fc3e753bb78d11cc2f4fb114971b
kind: implement
date: 2026-07-21
- sha: 37a06f5a6c19c7114ce0ae72a10f5c9b401040cf
kind: implement
date: 2026-07-21
reviewed: 2026-07-21
done: 2026-07-21
---

# Implementation notes

Route the unsolicited `plan` dashboard action through an intentional `refreshHub(..., { activateWork: true })` before showing the Work-owned overlay and dispatching the planner command. Keep ordinary refreshes tab-stable, keep the plan review on Planning, and retain approved-plan Work activation. Add a focused extension routing regression; no styling changes are needed.

# Snippets

```js
const cmd = actionToCommand(result);
if (!cmd) return;
session.showWorking(`Dispatched ${cmd} — Agent is working…`);
dispatch(cmd);
```

```js
await refreshHub(cwd, { activateWork: true });
```

# Blast radius

Planning-to-Work navigation and visibility of the agent working overlay when plan creation starts.

# Review

## 2026-07-21
* **Approved** _(agent review: openai-codex/gpt-5.6-sol)_ — Approved after rework: plan dispatch waits for intentional Work activation, reports refresh failures without swallowing the action, and behavioral tests prove dispatch occurs exactly once even when refresh or reporting fails.
* **Needs changes** _(agent review: openai-codex/gpt-5.6-sol)_ — The new plan path chains dispatch only inside `refreshHub(...).then(...)`. If dashboard gathering/rendering rejects, the promise is unhandled and the requested plan never starts, whereas the prior generic path always dispatched. Wrap the activation in an async helper or catch/finally so refresh failure is reported but `showWorking`/`dispatch(cmd)` still occur exactly once; add a behavioral regression for the rejection path rather than only source-pattern assertions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Features

* [Finalize auto mode after plan review](finalize-auto-plan-review.md) - ✅ done · medium · Agent plan-review completion resets durable auto state and refreshes Work so the final Auto step cannot remain stuck.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
type: Plan
title: Finish auto mode after plan review
description: Return the dashboard to completed manual state after the last automatic plan review records its report.
status: approved
branch: iterator/safe-role-model-handoff
created: 2026-07-20
timestamp: "2026-07-20T18:20:18.871Z"
plan_reviewed: 2026-07-20
---

# Goal

Make the final automatic plan-review transition reliably finish the auto run: after the final feature and plan review complete, clear stale auto state, release the Work UI from “Auto: plan-review”, and refresh it to the completed/retirable plan state.

# Architecture

- Preserve `lib/status.mjs` as the source of derived plan lifecycle state; the fix only converges runtime state held in `memory/state.md` and the extension’s owned Work overlay.
- Extend the deterministic terminal path around `record-plan-review` / auto dispatch so an agent-authored final review records `plan_reviewed` and resets auto mode to manual, unpaused, terminal state with no active feature, strikes, or escalation. The transition must be idempotent for retries and must not reset a human-initiated plan review.
- Harden `extensions/iterator.js`’s auto completion driver to recognize the recorded terminal review even if the originating agent turn is ending or session refresh races it, restore any role model once, clear its owned overlay, and refresh the Work hub from gathered state.
- Cover the full transition in writer, auto-state-machine, extension lifecycle, and UI/session tests; develop in root `lib/` and extension code, then run `npm run sync` for shipped copies.
- Follow `architecture/workflow-state-ownership`, `decisions/manual-role-models-and-runtime-reset`, and `decisions/focus-feature-execution-and-dashboard-ownership`: state is server-derived and terminal auto runs reset deterministically without browser-local lifecycle inference.

# Dependencies

(none)

# Key decisions

- An agent’s successful `record-plan-review` is the durable terminal boundary for an auto run; it clears stale runtime ownership before the next driver tick, while manual reviews retain their existing state.
- The extension’s later `kickAuto` completion remains an idempotent convergence and UI-refresh path, not a second lifecycle authority.
- A completed plan remains retirable rather than being auto-retired; this change only removes the stale working state.
- No new dependencies or workflow statuses are introduced.

# Features

* [Finalize auto mode after plan review](/features/finalize-auto-plan-review.md) - Agent plan-review completion resets durable auto state and refreshes Work so the final Auto step cannot remain stuck.

# Plan review

## 2026-07-20 _(agent review: openai-codex/gpt-5.6-sol)_

## Finding

- **Goal coverage / terminal-boundary decision:** `a0f4243` makes `recordPlanReview` reset runtime ownership only when `b.state.mode === "auto" && !b.state.paused` (`lib/write.mjs`). If the user pauses auto mode while the plan-review agent is already running, the successful agent review still records `plan_reviewed` but returns `autoCompleted: false`; the extension therefore skips model restoration, overlay clearing, and the completed Work refresh, leaving the runtime in paused auto/reviewing state. This contradicts the plan’s unconditional agent-review terminal boundary and its reliability goal. The terminal transition should distinguish a stale/unrelated agent review without excluding an in-flight final review merely because pause was toggled, with regression coverage for that race.

## Otherwise verified

The normal unpaused path durably resets state, delayed driver ticks are inert, manual reviews retain runtime ownership, both completion paths honor `auto_retire_prompt`, synced writer copies match, and the full suite passes (392 passed, 4 skipped). The only scope drift is a formatting-only `writeUsage` line in `a0f4243`, with no behavior change.
Loading
Loading