@cr1ms0n/pi-subagent 0.9.0 → 0.11.0

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/docs/RELEASING.md CHANGED
@@ -1,32 +1,151 @@
1
- # Releasing @cr1ms0n/pi-subagent
2
-
3
- This is an independent community fork of `@parke.dev/pi-subagent` by Luke Parke. Preserve the MIT license and copyright, and do not publish this fork under the upstream scope. The upstream monorepo's tag automation does not publish this package.
4
-
5
- ## Prepare
6
-
7
- 1. Update `package.json` and `CHANGELOG.md`; use an unpublished version.
8
- 2. Run strict type checking against the installed Pi version and offline fake-runner/extension tests. Do not run real provider calls without permission.
9
- 3. Run `npm pack --dry-run --json` and review the file list. Do not include authentication, user settings, machine-specific paths, local backups, session transcripts or generated test bundles.
10
- 4. Run `npm pack --ignore-scripts`, install the resulting tarball into an isolated prefix with `--ignore-scripts --legacy-peer-deps`, then repeat the offline checks against the packed source.
11
-
12
- ## Publish
13
-
14
- Use an existing npm login or a securely supplied environment-based credential. Never put tokens in command text, source files, or a committed `.npmrc`. Enable TLS verification.
15
-
16
- ```bash
17
- npm whoami --strict-ssl=true --registry=https://registry.npmjs.org/
18
- npm publish ./cr1ms0n-pi-subagent-<version>.tgz --access public --ignore-scripts --strict-ssl=true --registry=https://registry.npmjs.org/
19
- npm view @cr1ms0n/pi-subagent@<version> name version dist.integrity --strict-ssl=true --registry=https://registry.npmjs.org/
20
- ```
21
-
22
- A successful public release makes every file in the tarball public. Verify the package metadata and registry integrity before replacing an existing installation.
23
-
24
- ## Install and replace
25
-
26
- ```bash
27
- pi install npm:@cr1ms0n/pi-subagent@<version>
28
- ```
29
-
30
- Do not enable this package and `@parke.dev/pi-subagent` at the same time: both register the same tool names. Back up the old package selection, activate only the new package after verification, and keep the old source for rollback. Reload or restart Pi after changing packages. Configure `~/.pi/subagent.json` `jevRouting` (candidate models plus the credential environment variable) before spawning tasks.
31
-
32
- This fork uses the same config and persisted state paths as upstream. Switching packages is not a data migration. Upgrading must not overwrite user model choices or credentials.
1
+ # Release and repository maintenance
2
+
3
+ This is an independent community fork of Luke Parke's `@parke.dev/pi-subagent`. Preserve the original [MIT license](../LICENSE), copyright and upstream attribution. Do not publish under the upstream scope. This standalone repository does not inherit the upstream monorepo's tag automation.
4
+
5
+ ---
6
+
7
+ ### Choose the scope first
8
+
9
+ A README or repository-presentation update does not require a new npm version, tag, GitHub Release, package installation or binary upload. An edit to a Release body should change only that body. A new package release is a separate operation with artifact verification and explicit publication approval.
10
+
11
+ Read [package.json](../package.json), [CHANGELOG.md](../CHANGELOG.md), the working tree and index before making changes. Record the original local HEAD, remote main and any remote metadata you intend to update. Preserve unrelated modifications and existing commit history by default. A separately approved single-root conversion must follow the history-replacement safeguards below; do not infer that permission from a documentation update or another project’s release rules.
12
+
13
+ ---
14
+
15
+ ### Public source-tree boundary
16
+
17
+ Commit production source, the distributed [subagent skill](../skills/subagent/SKILL.md), public documentation and package metadata. Keep local maintainer tooling and private artifacts out of the public tree: `.trellis/`, `.agents/`, `.codex/`, project-local `.pi/` configuration, `AGENTS.md`, `LOCAL-PATCH.md`, Trellis-named support files, credentials, sessions, logs, dependencies, generated bundles and tarballs.
18
+
19
+ The product directory [skills/](../skills/) is not the local `.agents/skills/` directory. Never remove the product skill merely because local workflow skills are excluded. No Actions workflow or contributor guide is required for this manual release process.
20
+
21
+ Do not stage or discard local `.gitignore` edits. Where exclusion rules are maintained in `.git/info/exclude`, preserve its existing content and keep that file local. Ignore rules do not untrack files already in Git. When a reviewed cleanup needs to stop tracking local files, use `git rm --cached -- <explicit paths>` after backing them up and verifying their contents; never delete the working copies to clean the public tree. An ordinary commit does not erase those files from older history.
22
+
23
+ ---
24
+
25
+ ### Documentation-only source updates
26
+
27
+ Keep [README.md](../README.md) and [README.zh-CN.md](../README.zh-CN.md) synchronized in the same change. Preserve the language switch, package identity, original copyright and license links. Each README ends with one Linux.do acknowledgement; do not expand it into a community/contact section. Do not add a documentation-index table, contribution guide or stale release-promotion block to the landing page.
28
+
29
+ Run the relevant [development checks](DEVELOPMENT.md), including relative-link and translation review, whitespace checks and the package dry run. A fresh checkout has no bundled test runner or typecheck script; do not claim upstream/private harness commands are available.
30
+
31
+ Stage only explicitly reviewed paths. For example, when both README files are the complete change:
32
+
33
+ ```bash
34
+ git add -- README.md README.zh-CN.md
35
+ ```
36
+
37
+ Inspect the staged names:
38
+
39
+ ```bash
40
+ git diff --cached --name-status
41
+ ```
42
+
43
+ Inspect the actual staged patch:
44
+
45
+ ```bash
46
+ git diff --cached
47
+ ```
48
+
49
+ Create an ordinary commit after reviewing and approving its scope. Do not use broad `git add -A`, `--amend -a`, history squashing or a force push for documentation maintenance.
50
+
51
+ Before an authorized push, re-read remote main and compare it with the value saved at the start. Stop on an unexplained change rather than accepting a new baseline. The local branch can be named differently from remote main; target the reviewed commit deliberately:
52
+
53
+ ```bash
54
+ git push origin HEAD:refs/heads/main
55
+ ```
56
+
57
+ Use a normal fast-forward push for ordinary maintenance. Afterward, confirm local HEAD and remote main match, read back affected files, and verify the intended current tree no longer exposes local-only paths. Update GitHub About fields only when that specific edit was approved, and re-read them afterward. Source push and About edits can succeed separately; report their actual status separately.
58
+
59
+ ---
60
+
61
+ ### Explicitly approved history replacement
62
+
63
+ Only replace the main history when the repository owner has explicitly requested that scope. Preserve the original graph in a local-only backup ref and an external Git bundle, verify the bundle in a separate repository, and retain working-file and index backups. Construct a parentless candidate from the reviewed public tree; verify its tree and that its reachable history contains exactly one commit before changing the local branch. Local maintenance files must remain on disk.
64
+
65
+ Before replacing remote main, obtain approval for the exact candidate and original remote SHA. Recheck remote state and use an explicit `--force-with-lease=refs/heads/main:<original-sha>` with only `<candidate-sha>:refs/heads/main`. Stop if the lease fails; never refresh the expected SHA to bypass it. Do not push backup refs, use an unqualified force or alter tags and other branches.
66
+
67
+ Read back the remote commit, parent list and tree after the update. A single-root main history does not erase old objects from GitHub caches, other clones or local backups. It also does not authorize npm publication, installation or About changes.
68
+
69
+ ---
70
+
71
+ ### Prepare an npm release
72
+
73
+ Only perform this section for an approved version release:
74
+
75
+ 1. Choose an unpublished version and update package metadata and changelog together. Keep README install examples consistent with the intended release.
76
+ 2. Run the checks available in the actual environment, as described in [development](DEVELOPMENT.md). A syntax transform is not a semantic typecheck. Record missing tooling and any separately configured typecheck/fixture results. Do not run real provider calls without permission.
77
+ 3. Inspect the package dry-run list for unexpected files.
78
+ 4. Pack the source, record the tarball's integrity, and test the packed source in an isolated installation where suitable offline verification is available. Do not imply a missing harness was run.
79
+ 5. Review the final name, version, tarball, contents and verification results before requesting publication approval.
80
+
81
+ Dry-run contents:
82
+
83
+ ```bash
84
+ npm pack --dry-run --ignore-scripts --json
85
+ ```
86
+
87
+ Create the release artifact only when preparing a release:
88
+
89
+ ```bash
90
+ npm pack --ignore-scripts
91
+ ```
92
+
93
+ Install the reviewed artifact into a separate temporary prefix using `--ignore-scripts --legacy-peer-deps` if installation checks are part of the approved release scope. Never replace the working Pi installation merely to inspect a package.
94
+
95
+ ---
96
+
97
+ ### Publish with verified TLS
98
+
99
+ Use an existing npm login or a securely supplied environment-based credential. Never put tokens in command text, source files, a committed `.npmrc` or diagnostics.
100
+
101
+ If the environment contains `NODE_TLS_REJECT_UNAUTHORIZED=0`, remove that override before any credential-bearing registry operation. `--strict-ssl=true` does not undo a Node-level TLS override.
102
+
103
+ In Bash:
104
+
105
+ ```bash
106
+ unset NODE_TLS_REJECT_UNAUTHORIZED
107
+ ```
108
+
109
+ In PowerShell:
110
+
111
+ ```powershell
112
+ Remove-Item Env:NODE_TLS_REJECT_UNAUTHORIZED -ErrorAction SilentlyContinue
113
+ ```
114
+
115
+ Check the authenticated account without displaying credentials:
116
+
117
+ ```bash
118
+ npm whoami --strict-ssl=true --registry=https://registry.npmjs.org/
119
+ ```
120
+
121
+ After explicit approval, publish the reviewed tarball. Replace `<version>` with the approved version:
122
+
123
+ ```bash
124
+ npm publish "./cr1ms0n-pi-subagent-<version>.tgz" --access public --ignore-scripts --strict-ssl=true --registry=https://registry.npmjs.org/
125
+ ```
126
+
127
+ Read the exact version's registry metadata:
128
+
129
+ ```bash
130
+ npm view "@cr1ms0n/pi-subagent@<version>" name version dist.integrity --strict-ssl=true --registry=https://registry.npmjs.org/
131
+ ```
132
+
133
+ Registry propagation can lag. A successful publish is not proof that the exact version is already readable. Verify name/version/integrity against the reviewed local artifact before replacing an installation; do not repeat a publication blindly after an uncertain response. All files in a published tarball become public.
134
+
135
+ GitHub tags, Releases and attachments are separate from npm publication. Do not create or modify them implicitly. If explicitly requested, inspect the existing remote objects, use a tool that actually supports the operation, update only the approved fields/assets, then read back and verify the result.
136
+
137
+ ---
138
+
139
+ ### Install and replace
140
+
141
+ Installation is a separate local change. After artifact verification and approval, replace `<version>` with the verified release:
142
+
143
+ ```bash
144
+ pi install "npm:@cr1ms0n/pi-subagent@<version>"
145
+ ```
146
+
147
+ Back up the old package selection/source and keep it for rollback. Do not enable this fork and `@parke.dev/pi-subagent` simultaneously: both register the same tools. Verify the physical installed package version and files rather than relying only on a command's exit status or the settings entry. Reload or restart Pi after changing packages.
148
+
149
+ This fork uses the same configuration and persisted-state paths as upstream. Switching packages is not a data migration. Do not overwrite model choices or credentials during upgrades. Configure `jevRouting` in `~/.pi/subagent.json` before starting new tasks, as described in the [reference](REFERENCE.md#jev-routing).
150
+
151
+ Report preparation, publication, remote synchronization and local installation as distinct states, including checks that could not be performed.
package/docs/ROADMAP.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Roadmap
2
2
 
3
+ > **Historical upstream roadmap.** Retained for rationale and deferred ideas, not as this fork's current release schedule. Phases 2-4 are recorded as shipped in [the historical execution plan](PLAN.md); their future-tense sketches below do not mean these features are missing. Jev routing supersedes the old model-selection assumptions. Use [the current reference](REFERENCE.md), [architecture contract](ARCHITECTURE.md) and [development checks](DEVELOPMENT.md) for current behavior and available tooling. Historical test claims are not verification results for this checkout.
4
+
3
5
  > Execution details for the remaining phases (work breakdown, acceptance
4
6
  > criteria, test plans, release gates) live in [PLAN.md](./PLAN.md). This
5
7
  > document holds the rationale, design sketches, and deferral decisions.
package/docs/SECURITY.md CHANGED
@@ -6,11 +6,11 @@ and can use tools according to their capability profile.
6
6
 
7
7
  ## What subagents can do
8
8
 
9
- | Profile | Default tools | Writes? |
10
- |---------|---------------|---------|
11
- | `explore` | `read`, `grep`, `find`, `ls` (+ safe extras and Pi context tools) | No project-file writes |
12
- | `review` | same as explore | No project-file writes |
13
- | `general` | Jev-chosen subset of the full available locally permitted catalog (+ Pi context tools) | Yes if the selected tools include `bash`/`edit`/`write` |
9
+ | Profile | Finalized tools | Writes? |
10
+ |---------|-----------------|---------|
11
+ | `explore` | Jev-chosen subset of locally permitted read-only candidates, plus available Pi context tools | No project-file writes |
12
+ | `review` | Same as explore | No project-file writes |
13
+ | `general` | Jev-chosen subset of the full available locally permitted catalog, plus available Pi context tools | Yes if write-capable tools are selected |
14
14
 
15
15
  Parallel mode defaults to `explore` to avoid concurrent shared writes.
16
16
 
@@ -50,20 +50,20 @@ Parallel mode defaults to `explore` to avoid concurrent shared writes.
50
50
  (RPC mode). Extension UI dialogs raised inside a child are auto-cancelled so
51
51
  they can never hang a run — which also means a child can never obtain
52
52
  interactive consent. Prefer restricted tools for async/background runs.
53
- 11. **Steering messages** (`action: "steer"` and the overlay `s` key) inject text
54
- into a running child's conversation with user-level authority. Anything that
55
- can call the subagent tool can steer any live run in the same session.
56
- 7. **Process cleanup.** On POSIX, children run in their own process group so tree
53
+ 7. **Steering messages** (`action: "steer"` and the overlay `s` key) inject text
54
+ into a running child's conversation with user-level authority. Anything that
55
+ can call the subagent tool can steer any live run in the same session.
56
+ 8. **Process cleanup.** On POSIX, children run in their own process group so tree
57
57
  kills work for ordinary descendants. Parent (re)start reaps orphans recorded
58
58
  under `~/.pi/subagent-locks/runs/` so resume cannot race a still-alive writer.
59
59
  Grandchildren that call `setsid()` can still escape a simple process-group kill.
60
- 8. **Resume exclusivity.** Direct resume takes a durable per-session file lock;
60
+ 9. **Resume exclusivity.** Direct resume takes a durable per-session file lock;
61
61
  concurrent parents cannot append to the same child session.
62
- 9. **Profiles are tool-selection policy, not a sandbox.** Children inherit
62
+ 10. **Profiles are tool-selection policy, not a sandbox.** Children inherit
63
63
  `$HOME`, SSH/cloud credentials, network access, and the parent filesystem.
64
64
  Git worktrees only isolate the checkout. For untrusted tasks, use an outer
65
65
  container/cgroup/network policy.
66
- 10. **`max_cost` is accounting, not a hard provider gate.** Usage arrives after a
66
+ 11. **`max_cost` is accounting, not a hard provider gate.** Usage arrives after a
67
67
  turn; orphans may spend money the ledger never sees. It caps provider-reported
68
68
  execution cost only: TypeSafe reports routing tokens, not currency, so selector
69
69
  cost is unreported and outside `max_cost`. Combine with provider account
@@ -81,12 +81,11 @@ and synthesis select from the new task instruction rather than the assembled
81
81
  transcript. Task text and model descriptions are user content and can themselves
82
82
  contain secrets; there is no guaranteed redaction.
83
83
 
84
- The TypeSafe credential is read from the environment variable named by
85
- `jevRouting.apiKeyEnv` (default `TYPESAFE_API_KEY`) and sent only as an
86
- `Authorization` header to the fixed official HTTPS endpoint, with redirects
87
- disabled. It is never stored in the config file, prompts, argv, logs or results.
88
- Rotate any credential that has been pasted into a transcript or shared in
89
- conversation.
84
+ Version `0.10.0` reads the TypeSafe credential from `jevRouting.apiKey` in the private user-level `~/.pi/subagent.json`. This is plaintext storage: restrict file access and protect editor backups and synchronized copies. Same-user processes, including children with filesystem access, may read it. Profiles and worktrees do not protect this file from those processes.
85
+
86
+ The transport sends the key only as an `Authorization` header to the fixed official HTTPS endpoint, with redirects disabled. It does not automatically copy the key into prompts, selector JSON bodies, argv, child manifests, logs, receipts or results. Never serialize or log the complete routing configuration. Rotate any credential pasted into a transcript or shared in conversation.
87
+
88
+ Published npm `0.9.0` uses the older environment-based mechanism. In `0.10.0`, `apiKeyEnv` is rejected with manual migration guidance and no environment fallback. Unrelated `PI_SUBAGENT_*` runtime settings remain supported.
90
89
 
91
90
  New extension-managed dispatch is Pi-only. A `backend: "codex"` or
92
91
  `backend: "claude"` new task is rejected before any selector or provider work,
package/docs/UX.md CHANGED
@@ -41,14 +41,9 @@ The standalone pi-subagent provides rich TUI support for monitoring, inspecting,
41
41
  per-task stats, and a one-line tail (live activity or first output line).
42
42
  - Expanded (Ctrl+O / `app.tools.expand`): full task output capped with a dim
43
43
  `… +N lines` trailer pointing at the artifact/child session.
44
- - Expanded detail adds one bounded route line for Jev-routed runs: selected
45
- execution model, selected tools (plus locally added control-plane tools),
46
- selector version, confidence, outcome and selection latency. Legacy runs
47
- simply have no route line.
44
+ - Expanded detail adds a bounded route summary for Jev-routed runs: original selection, ranked probabilities, shared selected tools (plus locally added control-plane tools), selector version, answer-level confidence, outcome and selection latency. The actual execution model stays separate from the original choice. Large lists show a preview and total count; old runs without new fields remain readable. Compact results and completion notifications show at most the last five attempt models and label a shortened chain with its total attempt count.
48
45
  - Durations freeze at `endedAt`; running durations tick at render time.
49
- - Reliability annotations render inline: `[attempt 2]` during a same-model
50
- retry, `[stalled 2m]` while the stall watchdog is flagging silence, and
51
- `◐ wrapped up` on budget-stopped runs that concluded gracefully.
46
+ - Reliability annotations render inline: `[attempt 2]` during retry/failover, the actual attempt model and bounded attempt chain, `[stalled 2m]` while the stall watchdog is flagging silence, and `◐ wrapped up` on budget-stopped runs that concluded gracefully. Availability failures can switch models only before tools begin; a stalled indicator is not a promise of another attempt.
52
47
 
53
48
  ### Footer status
54
49
  Terse and actionable only: `⚙ 2 running · 1 ready · /subagents`. Cleared when
@@ -80,6 +75,7 @@ text with run ids and a `wait { id }` pointer.
80
75
  failures bypass batching and flush immediately, carrying held successes.
81
76
  - A `wait` that already delivered the run suppresses the redundant
82
77
  notification (delivered-state is re-checked at flush time).
78
+ - Model/attempt annotations use the actual execution history, not the immutable original Jev choice. Fallback creates no extra completion notification or delivery path.
83
79
 
84
80
  ### `/subagents` overlay
85
81
  - Header: title + running/ready counters + full usage ledger + rule.
@@ -129,11 +125,11 @@ usage and a bounded `Optional synthesis blocked: …` diagnostic instead of
129
125
  discarding or re-routing them.
130
126
 
131
127
  ### Plan results (tool output, not TUI)
132
- `action:"plan"` returns the resolved model/tools and the selector usage it
133
- incurred, and states that a later dispatch selects again. It starts no child and
134
- creates no run entry, so plan never adds an overlay row or ambient widget. A
135
- plan whose optional synthesis selection fails still returns the valid worker plan
136
- and labels only that synthetic stage blocked with its diagnostic.
128
+ `action:"plan"` returns the initial model, a bounded probability-ranked candidate preview, common tools, effective total attempt limit and selector usage. Probabilities are selector preferences, not uptime estimates, and `confidence` belongs to the original answer. A plan reports no actual attempts; a later invocation selects again. It starts no child and creates no run entry, so plan never adds an overlay row or ambient widget. A plan whose optional synthesis selection fails still returns the valid worker plan and labels only that stage blocked with its diagnostic.
129
+
130
+ ### Failed-attempt output
131
+
132
+ Earlier attempts retain bounded, attributed output previews and child-session pointers. If all attempts fail, the final state, failure reason, model and session stay authoritative; earlier text does not become that model's answer or a successful structured result. A human-readable earlier preview names its source attempt. Successful final JSON is never concatenated with failed JSON. Full output remains in existing child sessions, which are retained while referenced on the active branch. Compact views remain within their existing output limits.
137
133
 
138
134
  ## States
139
135
  - **Queued/Running**: spinner + live stats + activity tail from live text.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cr1ms0n/pi-subagent",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Community fork of Luke Parke's pi-subagent with Jev model/tool routing and verified Pi child capabilities",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,6 +10,13 @@
10
10
  "cr1ms0n (fork maintainer)"
11
11
  ],
12
12
  "homepage": "https://www.npmjs.com/package/@cr1ms0n/pi-subagent",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/awoaCrim/pi-smart-subagents.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/awoaCrim/pi-smart-subagents/issues"
19
+ },
13
20
  "keywords": [
14
21
  "pi-package",
15
22
  "pi",
@@ -31,6 +38,7 @@
31
38
  "skills",
32
39
  "docs",
33
40
  "README.md",
41
+ "README.zh-CN.md",
34
42
  "CHANGELOG.md",
35
43
  "LICENSE"
36
44
  ],
@@ -109,7 +109,7 @@ Pi path are **refused**, not silently degraded:
109
109
  - Prefer `max_turns`, `max_cost`, and/or `timeout_ms` on long or write-capable runs.
110
110
  `timeout_ms` is absolute: local preflight, Jev selection, setup, queue and
111
111
  runtime all count against it.
112
- - `output_schema` asks the child for a fenced `json:result` block (one repair round).
112
+ - `output_schema` asks the child for a fenced `json:result` block. An otherwise successful invalid answer gets one repair round; a failed provider attempt neither repairs nor publishes structured output.
113
113
  - `context: "fork"` continues from a fork of the parent session.
114
114
  - Do not poll `status` in a tight loop. Use `wait` / `subagent_wait`, or let the
115
115
  completion notification arrive for `async: true` runs.
@@ -121,22 +121,27 @@ Pi path are **refused**, not silently degraded:
121
121
 
122
122
  Omit `model` and `fallback_models` on every new call: both are legacy fields,
123
123
  and an explicit value is rejected rather than bypassing selection. Jev chooses
124
- one execution model from the user's dedicated candidate list plus an individual
125
- include/exclude decision per eligible tool. The local policy then re-validates
124
+ an initial execution model and probabilities for the user's eligible candidates, plus one task-based include/exclude decision per eligible tool shared by all attempts. The local policy then re-validates
126
125
  the answer: unknown or unsafe tools cannot launch, explore/review stay read-only,
127
126
  and management actions need no routing config or credential.
128
127
 
129
- There are no emergency or fallback models, and low confidence is accepted rather
130
- than treated as a threshold. A Jev timeout or API failure stops the affected new
131
- dispatch with an actionable error; existing runs stay queryable and cancellable.
132
- Transient child failures retry the already selected model and tool set within the
133
- original deadline, up to `max_retries`; a quality failure never reselects.
128
+ A Jev timeout or invalid decision still stops new dispatch; there is no emergency model. A valid route retains every candidate probability and tries higher values first. Tied maxima keep the returned choice first; other ties follow configured order. Low confidence and zero probability are accepted, not thresholds. Per-model probability is a selector preference, not uptime or a separate confidence score.
129
+
130
+ Recognized settled model-unavailable, temporary rate-limit/service and transport errors can advance to the next candidate only before any tool execution begins in the current invocation. Once a tool starts, or protocol evidence is uncertain, do not restart the child on another or the same model. Auth/configuration, quota/billing, context, invalid requests, task/schema quality, cancellation and exhausted budgets never trigger model switching. Historical resume/fork messages are not new tool execution.
131
+
132
+ `max_retries` limits all extension-level extra attempts: 0 means one initial attempt; 2 means at most three attempts. The built-in default remains 1. Availability failure advances directly to the next candidate; candidate exhaustion never wraps. Conclusively pre-work infrastructure failures may retry the same model within that budget. Every attempt shares tools, absolute deadline and cumulative reported cost/turn budgets, with fresh exact-model/tool startup verification. Switching makes no extra Jev call. Pi's internal provider retries are separate, unchanged and may delay fallback.
133
+
134
+ Plan/status distinguish original choice, ranked alternatives and actual attempts. Earlier failed output is retained as attributed previews/session pointers, not mixed into a later structured answer. All-failed tasks keep their final failure. Existing runs remain manageable without selector configuration or a credential.
134
135
 
135
136
  An optional candidate `thinking` value is an opaque Pi thinking-level string;
136
137
  common values include `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and
137
138
  `max`, but model-specific values are passed through unchanged. It is a default:
138
139
  explicit task, agent, and profile `taskDefaults.thinking` values override it.
139
- The extension re-reads `jevRouting` on each dispatch and injects the current
140
- routing guidance into the parent prompt. If `jevRouting` is missing or invalid,
141
- or the credential environment variable is unset, management remains available but
142
- new spawns, `/btw`, plan, resume, fork and synthesis are rejected.
140
+ The extension re-reads `jevRouting` on each dispatch and injects non-secret
141
+ routing guidance into the parent prompt. The user stores the TypeSafe credential
142
+ in `jevRouting.apiKey` in the private `~/.pi/subagent.json`; do not read, display
143
+ or copy the key into task text, prompts or output. Legacy `apiKeyEnv` is rejected
144
+ with migration guidance; there is no environment fallback. If the config or key
145
+ is missing or invalid, management remains available but new spawns, `/btw`, plan,
146
+ resume, fork and synthesis are rejected. This config-file credential contract ships in
147
+ npm 0.10.0; published npm 0.9.0 uses the old environment mechanism.
package/src/backend.ts CHANGED
@@ -24,7 +24,7 @@
24
24
  */
25
25
 
26
26
  import type { ProtocolUpdate } from "./protocol.js";
27
- import type { TaskResult, TaskSpec } from "./types.js";
27
+ import type { TaskResult, TaskSpec, ToolActivity } from "./types.js";
28
28
 
29
29
  /** Normalized event-stream parser contract, implemented per backend. */
30
30
  export interface BackendParser {
@@ -40,6 +40,21 @@ export interface BackendParser {
40
40
  getLiveText(): string;
41
41
  /** Completed messages so far. */
42
42
  getMessages(): import("@earendil-works/pi-ai").Message[];
43
+ /**
44
+ * Optional sticky current-invocation tool-activity observation used by the
45
+ * ranked failover gate. Parsers that do not implement it provide no
46
+ * conclusive evidence (callers must treat activity as unknown on the ranked
47
+ * path); the legacy unranked path is unaffected.
48
+ */
49
+ getToolActivity?(): ToolActivity | undefined;
50
+ /**
51
+ * Optional stop reason of the latest completed assistant message, used to
52
+ * suppress the ranked structured-output repair prompt and final publication
53
+ * after a settled provider error/abort. Absent means "not observable".
54
+ */
55
+ getAssistantStopReason?(): string | undefined;
56
+ /** Latest completed assistant text; empty must not fall back to earlier turns. */
57
+ getAssistantText?(): string | undefined;
43
58
  }
44
59
 
45
60
  export interface BackendInvocation {
package/src/config.ts CHANGED
@@ -61,7 +61,7 @@ export interface SubagentConfig {
61
61
  lockRetentionDays: number;
62
62
  /** Legacy per-profile defaults. Model/fallback fields are retained for config compatibility but ignored by Jev routing. */
63
63
  taskDefaults?: TaskDefaultsByProfile;
64
- /** Mandatory, user-owned Jev candidate configuration. Never contains a credential. */
64
+ /** Private Jev configuration including apiKey. Never log or serialize the whole config. */
65
65
  jevRouting?: JevRoutingConfig;
66
66
  /** Safe migration or parse failure; existing-run management remains available. */
67
67
  jevRoutingError?: string;
package/src/extension.ts CHANGED
@@ -8,10 +8,13 @@ import { Value } from "typebox/value";
8
8
  import { defaultConfig, loadConfig, readConfigFile, type SubagentConfig } from "./config.js";
9
9
  import {
10
10
  formatDuration,
11
+ formatRankedPreview,
11
12
  formatStatusPreview,
12
13
  formatTokens,
13
14
  isActiveState,
14
15
  oneLine,
16
+ projectRoutingForDisplay,
17
+ projectAttemptsForDisplay,
15
18
  renderCallLine,
16
19
  renderRunLines,
17
20
  SPINNERS,
@@ -25,7 +28,7 @@ import { runTasks } from "./orchestrator.js";
25
28
  import { OutputManager } from "./output.js";
26
29
  import { parseDepth, parseSpawnPolicy, SPAWNS_ENV_VAR, validateSubagentRequest, type PreparedTask, type ParentContext, type PreparationOptions, type ResolvedTask } from "./policy.js";
27
30
  import type { ChildRunner } from "./runner.js";
28
- import { ProcessLockManager } from "./process-lock.js";
31
+ import { ProcessLockManager, runRecordSessionIds } from "./process-lock.js";
29
32
  import { SessionScopedRunRegistry, snapshotFromLiveRun } from "./registry.js";
30
33
  import {
31
34
  ProviderSubagentParamsSchema,
@@ -49,6 +52,7 @@ import { eligibleModelCandidates, formatJevRoutingPrompt, toToolCandidates } fro
49
52
  import { JevRouter } from "./jev-router.js";
50
53
  import { routePreparedTasks, type RoutingCatalog } from "./dispatch-routing.js";
51
54
  import { runLocalPreflights } from "./dispatch-preflight.js";
55
+ import { rankedMaxAttempts } from "./model-failover.js";
52
56
  import type { RoutingReceipt } from "./routing-types.js";
53
57
  import { buildRoutingEvent, foldRoutingReceipts, MAX_ROUTING_DELIVERY_IDS, ROUTING_ENTRY_TYPE, type PersistedRoutingEvent } from "./persistence.js";
54
58
 
@@ -318,7 +322,7 @@ function compactDetails(
318
322
  errorMessage: result.errorMessage?.slice(0, 1_000),
319
323
  usage: result.usage ?? emptyUsage(),
320
324
  model: result.model,
321
- routing: result.routing,
325
+ routing: projectRoutingForDisplay(result.routing),
322
326
  thinking: result.thinking,
323
327
  profile: result.profile,
324
328
  canWrite: result.canWrite,
@@ -332,7 +336,8 @@ function compactDetails(
332
336
  wrappedUp: result.wrappedUp,
333
337
  stalledSince: result.stalledSince,
334
338
  attempts: result.attempts,
335
- attemptedModels: result.attemptedModels,
339
+ ...projectAttemptsForDisplay(result, Math.min(4_096, perResultText)),
340
+ toolActivity: result.toolActivity,
336
341
  structuredOutput: result.structuredOutput,
337
342
  structuredError: result.structuredError,
338
343
  })),
@@ -430,7 +435,7 @@ async function runPlanPreflights(
430
435
  });
431
436
  }
432
437
 
433
- function formatPlanEntry(task: ResolvedTask, index: number) {
438
+ function formatPlanEntry(task: ResolvedTask, index: number, maxRetriesDefault: number) {
434
439
  const agentNote = task.resolutionNotes.find((note) => note.startsWith("agent="));
435
440
  const agent = agentNote?.slice("agent=".length);
436
441
  return {
@@ -438,12 +443,16 @@ function formatPlanEntry(task: ResolvedTask, index: number) {
438
443
  label: task.label,
439
444
  agent,
440
445
  model: task.model,
441
- fallbackModels: task.fallbackModels,
446
+ // The ranked route replaces legacy fallback semantics for this path: show
447
+ // the bounded ranked preview and the effective extension attempt budget.
448
+ rankedPreview: formatRankedPreview(task.routing?.rankedModels),
449
+ rankedTotal: task.routing?.rankedModels?.length,
450
+ maxAttempts: rankedMaxAttempts(task.maxRetries ?? maxRetriesDefault),
442
451
  thinking: task.thinking,
443
452
  profile: task.profile,
444
453
  access: task.canWrite ? "RW" : "RO" as const,
445
454
  tools: task.effectiveTools,
446
- routing: task.routing,
455
+ routing: projectRoutingForDisplay(task.routing),
447
456
  budgets: {
448
457
  timeoutMs: task.timeoutMs,
449
458
  maxTurns: task.maxTurns,
@@ -466,8 +475,10 @@ function formatPlanText(mode: "single" | "parallel", plan: ReturnType<typeof for
466
475
  ].filter(Boolean).join(" ");
467
476
  return [
468
477
  `${entry.index + 1}. ${entry.label}${entry.agent ? ` [agent:${entry.agent}]` : ""} (${entry.profile}/${entry.access})`,
469
- ` model=${entry.model ?? "(none)"} fallback_models=[${(entry.fallbackModels ?? []).join(", ")}] thinking=${entry.thinking ?? "(default)"} isolation=${entry.isolation}`,
470
- ` tools=[${entry.tools.join(",")}]`,
478
+ ` model=${entry.model ?? "(none)"} thinking=${entry.thinking ?? "(default)"} isolation=${entry.isolation}`,
479
+ ` ranked_models=[${entry.rankedPreview ?? entry.model ?? "(none)"}]${entry.rankedTotal && entry.rankedTotal > 5 ? ` (total ${entry.rankedTotal})` : ""}`,
480
+ ` attempt_budget=${entry.maxAttempts} (max_retries limits EXTRA extension-level attempts; pre-tool availability failure advances the ranking, never wraps)`,
481
+ ` shared_tools=[${entry.tools.join(",")}]`,
471
482
  ` ${budgets}`,
472
483
  ` notes: ${entry.resolutionNotes.join(", ")}`,
473
484
  ].join("\n");
@@ -502,7 +513,7 @@ function guidelines(catalog?: Map<string, AgentDefinition>): string[] {
502
513
  "Profiles: explore/review are strictly read-only (safe for fanout); general offers the full available locally permitted catalog to Jev and may write. Explicit tools are a ceiling; agent tool defaults do not narrow candidates. Single tasks default to general, parallel tasks to explore.",
503
514
  "Parallel writers need isolation:'worktree' (each gets an isolated checkout; changed work lands on a branch). After a worktree run finishes, use action:'diff' to inspect, then 'apply' to bring changes into the main checkout or 'discard' to drop them.",
504
515
  "Set budgets: at max_turns/max_cost the child is steered to wrap up and given grace turns for a final answer (grace_turns tunes this); results end as 'partial' with wrappedUp:true when the child concluded. timeout_ms includes Jev selection, setup, queue and retries; max_cost excludes unreported TypeSafe currency; timeout results report the phase.",
505
- "Transient child failures may retry on the already selected model/tools within the original deadline; no selector retries or fallback models. Task-quality failures never retry.",
516
+ "Transient child failures may retry within the same invocation: ranked Jev routes advance to the next probability-ranked candidate only for a recognized model-availability failure that settles before any tool execution, sharing one task-based tool set and the total max_retries attempt budget (0 = first attempt only; never wraps back). A tool that started, uncertain evidence, or auth/quota/context/schema failures stop without switching. No selector retries or emergency models are used. Task-quality failures never retry.",
506
517
  "context:'fork' starts a single child from a branched copy of this conversation — use it when the task depends on discussion context instead of re-explaining. Single-task only.",
507
518
  "Use async:true only when you have independent work meanwhile; then use action:'wait' with the run id (interruptible, does not cancel). action:'steer' injects mid-run guidance into a running child instead of cancel + retry.",
508
519
  "For parallel research, add synthesis:'<instruction>' to have one read-only child fold all outputs into a single brief, delivered first.",
@@ -614,7 +625,8 @@ function buildCompletionDetails(runtime: SessionRuntime, runIds: string[]): Comp
614
625
  tokens: (result.usage?.input ?? 0) + (result.usage?.output ?? 0),
615
626
  cost: result.usage?.cost ?? 0,
616
627
  model: result.model,
617
- attemptedModels: result.attemptedModels,
628
+ attempts: result.attempts,
629
+ attemptedModels: projectAttemptsForDisplay({ attemptedModels: result.attemptedModels }).attemptedModels,
618
630
  pointers: taskPointers,
619
631
  };
620
632
  });
@@ -632,6 +644,7 @@ function buildCompletionDetails(runtime: SessionRuntime, runIds: string[]): Comp
632
644
  // Preserve the old top-level fields only for single-task consumers. The
633
645
  // complete per-task model/attempt data lives in tasks[].
634
646
  model: tasks.length === 1 ? first?.model : undefined,
647
+ attempts: tasks.length === 1 ? first?.attempts : undefined,
635
648
  attemptedModels: tasks.length === 1 ? first?.attemptedModels : undefined,
636
649
  pointers,
637
650
  tasks,
@@ -665,7 +678,7 @@ function scheduleMaintenance(runtime: SessionRuntime): void {
665
678
  const keep = new Set(runtime.registry.planSessionRetention().keep);
666
679
  const busy = new Set<string>();
667
680
  for (const record of runtime.locks.listRunRecords()) {
668
- if (record.state === "running" && record.childSessionId) busy.add(record.childSessionId);
681
+ if (record.state === "running") for (const id of runRecordSessionIds(record)) busy.add(id);
669
682
  }
670
683
  for (const run of runtime.registry.getLiveRuns(runtime.key)) {
671
684
  for (const id of run.childSessionIds) busy.add(id);
@@ -912,6 +925,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
912
925
  state: run.state,
913
926
  preview: run.preview,
914
927
  model: run.model,
928
+ attempts: run.attempts,
915
929
  attemptedModels: run.attemptedModels,
916
930
  pointers: run.pointers,
917
931
  turns: run.turns,
@@ -920,7 +934,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
920
934
  }];
921
935
  return tasks.map((task) => {
922
936
  const attemptText = task.attemptedModels && task.attemptedModels.length > 1
923
- ? `; attempts: ${task.attemptedModels.join(" → ")}`
937
+ ? `; attempts${task.attempts ? ` (${task.attempts} total)` : ""}: ${task.attemptedModels.join(" → ")}`
924
938
  : "";
925
939
  const label = tasks.length > 1 ? `${run.label}/${task.label}` : task.label;
926
940
  return `- [${run.id.slice(0, 8)}] ${label}: ${task.state}${task.model ? ` on ${task.model}` : ""}${attemptText}${task.preview ? ` — ${task.preview}` : ""}${task.pointers.length ? ` (${task.pointers.join(", ")})` : ""}`;
@@ -1025,6 +1039,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
1025
1039
  tokens: run.tokens,
1026
1040
  cost: run.cost,
1027
1041
  model: run.model,
1042
+ attempts: run.attempts,
1028
1043
  attemptedModels: run.attemptedModels,
1029
1044
  pointers: run.pointers,
1030
1045
  }];
@@ -1041,7 +1056,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
1041
1056
  lines.push(truncateToWidth(`${glyph} ${theme.fg("dim", task.model ?? "model unknown")} · ${theme.bold(theme.fg("toolTitle", label))} ${theme.fg("dim", `[${run.id.slice(0, 8)}] ${stats}`)}`, width));
1042
1057
  if (task.preview) lines.push(truncateToWidth(` ${theme.fg("dim", "⎿")} ${theme.fg("toolOutput", task.preview)}`, width));
1043
1058
  if (task.attemptedModels && task.attemptedModels.length > 1) {
1044
- lines.push(truncateToWidth(` ${theme.fg("warning", `models: ${task.attemptedModels.join(" → ")}`)}`, width));
1059
+ lines.push(truncateToWidth(` ${theme.fg("warning", `models: ${task.attemptedModels.join(" → ")}${task.attempts && task.attempts > task.attemptedModels.length ? ` (last ${task.attemptedModels.length} of ${task.attempts})` : ""}`)}`, width));
1045
1060
  }
1046
1061
  if ((expanded || tasks.length === 1) && task.pointers.length) {
1047
1062
  lines.push(truncateToWidth(theme.fg("dim", ` ${task.pointers.join(" · ")}`), width));
@@ -1339,7 +1354,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
1339
1354
  const planned = await routePreparedTasks(synthetic, catalog, router, {
1340
1355
  purpose: "plan", signal: routingScope.controller.signal, assertOwner: routingScope.assertOwner,
1341
1356
  });
1342
- synthesis = { state: "resolved", plan: formatPlanEntry(planned[0]!, 0) };
1357
+ synthesis = { state: "resolved", plan: formatPlanEntry(planned[0]!, 0, runtime.config.maxRetries) };
1343
1358
  } catch (error) {
1344
1359
  routingScope.assertOwner();
1345
1360
  synthesis = { state: "blocked", error: error instanceof Error ? error.message : "Optional synthesis routing failed." };
@@ -1348,7 +1363,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
1348
1363
  routingScope.assertOwner();
1349
1364
  await requireRoutingPersistence(runtime);
1350
1365
  routingScope.assertOwner();
1351
- const plan = resolved.map((task, index) => formatPlanEntry(task, index));
1366
+ const plan = resolved.map((task, index) => formatPlanEntry(task, index, runtime.config.maxRetries));
1352
1367
  const mode = validated.mode as "single" | "parallel";
1353
1368
  const receipts = [...routingScope.receipts.values()];
1354
1369
  const selectorUsage = await claimRoutingUsage(runtime, { ids: new Set(receipts.map((receipt) => receipt.requestId)) });
@@ -1612,6 +1627,9 @@ export default function registerSubagent(pi: ExtensionAPI): void {
1612
1627
  wrappedUp: task.wrappedUp,
1613
1628
  stalledSince: task.stalledSince,
1614
1629
  attempts: task.attempts,
1630
+ attemptedModels: task.attemptedModels,
1631
+ toolActivity: task.toolActivity,
1632
+ modelAttempts: task.modelAttempts,
1615
1633
  structuredOutput: task.structuredOutput,
1616
1634
  structuredError: task.structuredError,
1617
1635
  })),