@qodeca/xezar 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -113,7 +113,7 @@ and an orchestrator keeps a whole queue of them moving.
113
113
  Take it over interactively in one click (`claude --resume <id>`), or continue it
114
114
  in-process from the cockpit.
115
115
  - 🔀 **Locked into one agent vendor.** Most tools wed you to a single CLI. xezar
116
- drives **Claude Code, Codex and OpenCode (experimental)** through one runner seam — set a
116
+ drives **Claude Code, Codex, OpenCode and pi (the last two experimental)** through one runner seam — set a
117
117
  default, pick a backend per task, or mix them inside one workflow (implement
118
118
  with one agent, review with another) — and through **OpenCode** you can point
119
119
  a run at **open-source or local models**, not just the big vendors. See
@@ -147,8 +147,9 @@ and an orchestrator keeps a whole queue of them moving.
147
147
 
148
148
  **Prerequisites:** Node 20+, at least one logged-in agent CLI — the
149
149
  [`claude` CLI](https://github.com/anthropics/claude-code) (Pro/Max subscription),
150
- the [`codex` CLI](https://github.com/openai/codex), or
151
- [OpenCode](https://opencode.ai) — and, optionally, `git` and the `gh` CLI.
150
+ the [`codex` CLI](https://github.com/openai/codex),
151
+ [OpenCode](https://opencode.ai), or [pi](https://github.com/badlogic/pi-mono)
152
+ and, optionally, `git` and the `gh` CLI.
152
153
 
153
154
  Install it once, then run it in any repo:
154
155
 
@@ -192,8 +193,8 @@ To upgrade later: `npm install -g @qodeca/xezar@latest`.
192
193
 
193
194
  You describe a task. xezar runs it as a **workflow** — an ordered list of agent
194
195
  steps and shell checks — shelling out to your locally installed agent CLI
195
- (Claude Code by default; Codex and OpenCode are drop-in alternatives, per task
196
- or per step). In a git repo each task gets its own worktree by default — switch
196
+ (Claude Code by default; Codex, OpenCode and pi are drop-in alternatives, per
197
+ task or per step). In a git repo each task gets its own worktree by default — switch
197
198
  the composer's **Worktree** toggle off to run in the repo working tree (under
198
199
  xezar's repository-root lease), and a non-git directory runs in place. A git
199
200
  task that asks for a worktree it cannot create stops as `failed`; it never falls
@@ -259,7 +260,7 @@ Three words, no jargon — **task**, **skill**, **chain**:
259
260
  AI draft a chain for your task that you review, trim and start. The built-in
260
261
  `quick-task` (one agent step) works with zero setup.
261
262
 
262
- Five moves that make the cockpit worth the browser tab:
263
+ Six moves that make the cockpit worth the browser tab:
263
264
 
264
265
  - 🗃️ **Queue + orchestration.** Start as many tasks as you like: xezar runs up to
265
266
  `maxParallel` at once across every project (default **2**; a non-git directory
@@ -316,7 +317,7 @@ Nine views, one browser window, all live over Server-Sent Events (seven by defau
316
317
  | **Automations** | **Opt-in** (`XEZ_AUTOMATIONS=1`; hidden by default). Scheduled GitHub watches: each automation polls on its own interval and launches a task when its bounded filter matches. Test a filter before enabling it, and read the per-check log. |
317
318
  | **Skills** | Local skills plus the team skills repo, with a rendered body + prompt preview. Refresh pulls the latest from the remote. |
318
319
  | **Workflows** | Build a chain by drag-ordering skills, save it as portable YAML, import/export, or delete. Built-ins always come back. |
319
- | **Settings** | Appearance (dark/light theme, accent, density), agent backends, notifications, and the skills catalog. |
320
+ | **Settings** | Appearance (dark/light theme, accent, density), agent backends and their own config files, agent accounts, notifications, machine resources, projects, and skill-update checks. |
320
321
 
321
322
  The cockpit is a React app served pre-built from the package — running `xezar`
322
323
  needs no dev server on your machine — with a dark/light
@@ -373,10 +374,11 @@ These read and write `~/.xezar/config.json` directly, so they work with the
373
374
  server stopped, and `XEZ_HOME` selects which workspace they operate on.
374
375
 
375
376
  Settings split along the same line: **General** (the project's folder, its
376
- registry facts, its parallel-task ceiling, and Remove), **Agents**,
377
- **Worktrees**, **Bookmarklets**, **Prompt templates** and **MCP** describe one
378
- repo and live under `/p/<projectId>/settings`; **Appearance**,
379
- **Notifications**, **Resources**, **Projects** and **Keyboard** are yours or the
377
+ registry facts, its parallel-task ceiling, and Remove), **Agents**, **Agent
378
+ config** (each agent's own settings, MCP and memory files), **Worktrees**,
379
+ **Bookmarklets** and **Prompt templates** describe one repo and live under
380
+ `/p/<projectId>/settings`; **Appearance**, **Notifications**, **Resources**,
381
+ **Skills** (update checks), **Agent accounts** and **Projects** are yours or the
380
382
  machine's and live at `/settings/global`.
381
383
 
382
384
  ### Grouping connected repositories: tags and the All tasks page
@@ -623,16 +625,16 @@ On startup xezar probes which CLIs are installed and the cockpit only offers
623
625
  the backends it found — install any one of the four and you're operational.
624
626
 
625
627
  **Models come from your own machine.** The model picker does not ship a list of
626
- vendor releases that goes stale between xezar versions. For Claude, Codex and
627
- OpenCode xezar asks the CLI on your host what *it* currently offers (Claude
628
- Code's `list_models` control request; the Codex app-server's `model/list`;
629
- `opencode models`) and shows exactly that, in that order — so a model your
630
- account gained yesterday is selectable today with no xezar release, and one your
631
- provider retired stops being offered. Discovery is read-only, costs no tokens,
632
- and is cached briefly in memory. If the CLI is missing, logged out, too old or
633
- slow, the picker quietly falls back to that runner's built-in entries (`auto`
634
- plus Claude's tier aliases) and says so in a status row; `pi`, which has no
635
- host-local catalog yet, always shows its built-in entries. `auto` — send no
628
+ vendor releases that goes stale between xezar versions. For each backend xezar
629
+ reads what *your host* currently offers Claude Code's `list_models` control
630
+ request, the Codex app-server's `model/list`, `opencode models`, and for pi its
631
+ own `~/.pi/agent/models.json` and `settings.json` — and shows exactly that, in
632
+ that order, so a model your account gained yesterday is selectable today with no
633
+ xezar release, and one your provider retired stops being offered. Discovery is
634
+ read-only, costs no tokens, and is cached briefly in memory. If a CLI is
635
+ missing, logged out, too old or slow or pi has no providers configured yet —
636
+ the picker quietly falls back to that runner's built-in entries (`auto` plus
637
+ Claude's tier aliases) and says so in a status row. `auto` — send no
636
638
  model at all and let the CLI decide — is always available, and a model you
637
639
  pinned by hand stays selectable even when it is no longer advertised.
638
640
 
@@ -688,8 +690,8 @@ happens per task and per step, not inside a variant group.
688
690
 
689
691
  The seam is deliberately small: a backend is one class implementing the
690
692
  `AgentRunner` interface (`packages/xezar/src/core/agent-runner.ts`) that turns a prompt into
691
- a stream of normalized events. Other CLIs pi, aider, whatever ships next
692
- can slot in the same way.
693
+ a stream of normalized events. `pi` was added exactly that way; other CLIs
694
+ aider, whatever ships next — can slot into the same seam.
693
695
 
694
696
  ---
695
697
 
@@ -823,8 +825,9 @@ cd xezar
823
825
  npm install
824
826
  ```
825
827
 
826
- **3. Build** — compiles the api-client and the server (`tsc → packages/xezar/dist/`) and the cockpit
827
- (`vite build → packages/xezar/web/dist/`), then runs the pack gate:
828
+ **3. Build** — compiles the server (`tsc → packages/xezar/dist/`), folds the
829
+ contract into `dist/contract/` so the published tarball resolves it, and builds
830
+ the cockpit (`vite build → packages/xezar/web/dist/`), then runs the pack gate:
828
831
 
829
832
  ```bash
830
833
  npm run build
@@ -34,7 +34,7 @@ export interface RematerializeStore {
34
34
  * run regains its isolated tree and becomes eligible for retention again;
35
35
  * without it the run would keep a directory on disk while staying invisible to
36
36
  * the enforcer forever (a leak). Returns true when it re-materialized.
37
- * Best-effort: never throws (the caller falls back to the repo root).
37
+ * Best-effort: never throws; the continuation caller refuses lost isolation before spawning.
38
38
  */
39
39
  export declare function rematerializeReclaimedWorktree(repoRoot: string, store: RematerializeStore, runId: string): Promise<boolean>;
40
40
  /**
@@ -45,7 +45,7 @@ export function selectReclaimableWorktrees(runs, keep) {
45
45
  * run regains its isolated tree and becomes eligible for retention again;
46
46
  * without it the run would keep a directory on disk while staying invisible to
47
47
  * the enforcer forever (a leak). Returns true when it re-materialized.
48
- * Best-effort: never throws (the caller falls back to the repo root).
48
+ * Best-effort: never throws; the continuation caller refuses lost isolation before spawning.
49
49
  */
50
50
  export async function rematerializeReclaimedWorktree(repoRoot, store, runId) {
51
51
  const run = store.getRun(runId);
@@ -1981,6 +1981,12 @@ export class RunManager {
1981
1981
  // human got there first — and then the counter starts over, because the cap only exists to
1982
1982
  // bound UNATTENDED resumes.
1983
1983
  this.clearAutoResume(runId);
1984
+ // Accepted Continue is explicit new work: make it discoverable in Active again.
1985
+ // Refusals above leave the user's archive decision untouched.
1986
+ if (run.archived) {
1987
+ this.store.setArchived(runId, false);
1988
+ this.store.appendEvent(runId, { type: 'note', message: 'unarchived for continuation' });
1989
+ }
1984
1990
  const continuations = run.steps.filter((s) => s.id.startsWith('continue-')).length;
1985
1991
  const stepId = `continue-${continuations + 1}`;
1986
1992
  this.store.addStep(runId, { id: stepId, name: 'Continue', kind: 'agent' });
@@ -2023,15 +2029,49 @@ export class RunManager {
2023
2029
  * reconstructed at dequeue. Keep them separate from fresh `images` so
2024
2030
  * opening a recovered continuation does not persist duplicate files. */
2025
2031
  persistedImages = [], persistedAttachments = []) {
2032
+ // Continue has already been accepted. Preserve its input before asynchronous
2033
+ // recovery or isolation refusal can stop startup; persistence is not runner receipt.
2034
+ // Attachments pasted into the follow-up composer, on the same terms as a live-session
2035
+ // message (#357): persisted to the run's own attachment store so the thread renders the
2036
+ // bubble's images rather than a bare count, and handed to the agent as absolute paths
2037
+ // appended to the prompt (so it can operate on them — and because codex/opencode drop image
2038
+ // blocks before they reach the model). An image ALSO rides along as a base64 block so the
2039
+ // model can view it; a file (#950) has nothing to view and travels as its path alone.
2040
+ const freshAttachments = this.persistPastedAttachments(runId, images);
2041
+ const openingImages = [...contentBlocksOf(images), ...persistedImages];
2042
+ const attachments = [...freshAttachments, ...persistedAttachments];
2043
+ this.store.appendEvent(runId, {
2044
+ type: 'user-message',
2045
+ stepId,
2046
+ text: prompt,
2047
+ imageCount: openingImages.filter((b) => b.type === 'image').length,
2048
+ ...(attachments.length ? { images: attachments.map((saved) => saved.url) } : {}),
2049
+ });
2026
2050
  // Continuation runs in the task's worktree when it still exists (spec
2027
2051
  // 006) — the resumed session sees exactly what the original run left.
2028
2052
  // Retention (#483) may have reclaimed this run's worktree directory while
2029
2053
  // keeping its branch and worktreePath. Re-materialize it on resume and clear
2030
2054
  // the stamp so the session regains its isolated tree and the run is eligible
2031
2055
  // for retention again — otherwise it keeps a dir on disk while staying
2032
- // invisible to the enforcer forever. Best-effort; falls back to repoRoot.
2056
+ // invisible to the enforcer forever. A failed restoration must not widen isolation.
2033
2057
  await rematerializeReclaimedWorktree(this.repoRoot, this.store, runId);
2034
2058
  const record = this.store.getRun(runId);
2059
+ // A recorded path/branch proves prior isolation; an absent policy alone does not
2060
+ // (legacy and non-git runs legitimately ran in place). Never resume that isolated
2061
+ // session against primary while that isolation identity is still recorded.
2062
+ // Legacy cleanup that erased BOTH path and branch cannot be distinguished
2063
+ // from an old in-place run; it is not a qualified isolation recovery route.
2064
+ if (record?.worktree !== false && (record?.worktreePath || record?.branch) &&
2065
+ (!record.worktreePath || !existsSync(record.worktreePath))) {
2066
+ const error = 'continuation isolation unavailable — restore the task worktree before retrying';
2067
+ const finishedAt = new Date().toISOString();
2068
+ this.store.updateStep(runId, stepId, { status: 'failed', error, finishedAt });
2069
+ this.store.updateRun(runId, { status: 'failed', error, finishedAt, currentStepId: undefined });
2070
+ this.store.appendEvent(runId, { type: 'lifecycle', message: `run failed — ${error}` });
2071
+ this.starting.delete(runId);
2072
+ this.dropActive(runId);
2073
+ return;
2074
+ }
2035
2075
  // The env is a live ceiling: a run created while the inbox was on must not keep writing
2036
2076
  // follow-ups after it is switched off.
2037
2077
  const generateFollowups = this.semaphore.followupsEnabled() && record?.generateFollowups !== false;
@@ -2100,22 +2140,6 @@ export class RunManager {
2100
2140
  backend,
2101
2141
  });
2102
2142
  this.store.appendEvent(runId, { type: 'step-start', stepId, name: 'Continue', kind: 'agent', iteration: 1 });
2103
- // Attachments pasted into the follow-up composer, on the same terms as a live-session
2104
- // message (#357): persisted to the run's own attachment store so the thread renders the
2105
- // bubble's images rather than a bare count, and handed to the agent as absolute paths
2106
- // appended to the prompt (so it can operate on them — and because codex/opencode drop image
2107
- // blocks before they reach the model). An image ALSO rides along as a base64 block so the
2108
- // model can view it; a file (#950) has nothing to view and travels as its path alone.
2109
- const freshAttachments = this.persistPastedAttachments(runId, images);
2110
- const openingImages = [...contentBlocksOf(images), ...persistedImages];
2111
- const attachments = [...freshAttachments, ...persistedAttachments];
2112
- this.store.appendEvent(runId, {
2113
- type: 'user-message',
2114
- stepId,
2115
- text: prompt,
2116
- imageCount: openingImages.filter((b) => b.type === 'image').length,
2117
- ...(attachments.length ? { images: attachments.map((saved) => saved.url) } : {}),
2118
- });
2119
2143
  let stepCost = 0;
2120
2144
  let turnText = '';
2121
2145
  let sessionError;
@@ -2179,13 +2203,14 @@ export class RunManager {
2179
2203
  // Autonomous (#autonomous): never hand the ball back to the user. Nudge the agent to
2180
2204
  // keep going (bounded by MAX_AUTO_CONTINUES) instead of parking at `waiting`. Shared
2181
2205
  // with `execute`'s turn-end handler — one sender, both sites (#141).
2182
- autoContinued = this.autoContinueTurn(runId, state);
2206
+ autoContinued = !monitoring && this.autoContinueTurn(runId, state);
2183
2207
  if (!autoContinued) {
2184
2208
  // `XEZ:ASK` → park `waiting` (attention) AND surface the structured
2185
2209
  // question as an ask card (#473). `XEZ:MONITORING` → non-attention
2186
2210
  // `running`/`activity:'monitoring'` (#490). Both share the waiting
2187
2211
  // lifecycle (free the slot, keep the idle timer); the autonomous
2188
- // nudge above still wins over either.
2212
+ // nudge may answer ASK in autonomous mode, but explicit monitoring
2213
+ // parks with its bounded wake timer instead of spinning immediately.
2189
2214
  if (ask)
2190
2215
  emitAskRequested(sink, ask);
2191
2216
  if (monitoring) {
@@ -2861,7 +2886,7 @@ export class RunManager {
2861
2886
  // `runContinuation`'s handler (#141) — the field `execute` has always set is finally
2862
2887
  // read here. `interactive` still gates it: a mid-workflow step ends its own session and
2863
2888
  // the next step follows, so there is no ball to hand back and nothing to nudge.
2864
- const autoContinued = waiting ? this.autoContinueTurn(runId, state) : false;
2889
+ const autoContinued = waiting && !monitoring ? this.autoContinueTurn(runId, state) : false;
2865
2890
  if (waiting && !autoContinued) {
2866
2891
  // Turn over, session open. Either the ball is in the user's court
2867
2892
  // (`waiting`) — optionally with a structured `XEZ:ASK` question the