pi-advisor-flow 0.5.0 → 0.5.2

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +37 -71
  3. package/extensions/index.ts +7 -7
  4. package/package.json +19 -6
  5. package/src/commands/activation-preparation.ts +117 -0
  6. package/src/commands/activation.ts +135 -0
  7. package/src/commands/lifecycle.ts +83 -0
  8. package/src/commands/manual-command.ts +107 -0
  9. package/src/commands/manual-consultation.ts +144 -0
  10. package/src/commands/manual-progress.ts +93 -0
  11. package/src/commands/model-commands.ts +66 -0
  12. package/src/commands/model-options.ts +181 -0
  13. package/src/commands/model-picker.ts +110 -0
  14. package/src/commands/registration.ts +27 -0
  15. package/src/commands/renderers.ts +76 -0
  16. package/src/commands/runtime.ts +114 -0
  17. package/src/commands/settings-commands.ts +80 -0
  18. package/src/commands/settings-persistence.ts +81 -0
  19. package/src/commands/types.ts +78 -0
  20. package/src/commands.ts +9 -946
  21. package/src/config/args.ts +37 -0
  22. package/src/config/defaults.ts +190 -0
  23. package/src/config/state.ts +199 -0
  24. package/src/config/storage.ts +224 -0
  25. package/src/config/types.ts +63 -0
  26. package/src/config/validation.ts +234 -0
  27. package/src/config.ts +104 -791
  28. package/src/conversation.ts +4 -2
  29. package/src/herdr.ts +1 -1
  30. package/src/model-stream.ts +25 -2
  31. package/src/scout-context.ts +2 -2
  32. package/src/scout.ts +1 -1
  33. package/src/tools/consultation.ts +337 -0
  34. package/src/tools/gate-policy.ts +132 -0
  35. package/src/tools/gate-protocol.ts +127 -0
  36. package/src/tools/loop-gate.ts +210 -0
  37. package/src/tools/prompts.ts +159 -0
  38. package/src/tools/register-ask-advisor.ts +225 -0
  39. package/src/tools/register-lifecycle.ts +89 -0
  40. package/src/tools/register-outcome.ts +75 -0
  41. package/src/tools/register-renderers.ts +88 -0
  42. package/src/tools/registration.ts +35 -0
  43. package/src/tools/render-advisor-result.ts +168 -0
  44. package/src/tools/render-common.ts +120 -0
  45. package/src/tools/scout-status.ts +206 -0
  46. package/src/tools/session.ts +3 -0
  47. package/src/tools/types.ts +119 -0
  48. package/src/tools.ts +42 -1811
  49. package/src/ui/manual-dialog-render.ts +150 -0
  50. package/src/ui/manual-dialog.ts +305 -0
  51. package/src/ui/model-selector.ts +166 -0
  52. package/src/ui/settings-formatting.ts +133 -0
  53. package/src/ui/settings-items.ts +323 -0
  54. package/src/ui/settings-list-adapter.ts +103 -0
  55. package/src/ui/settings-mutations.ts +80 -0
  56. package/src/ui/settings-selector.ts +161 -0
  57. package/src/ui/text-setting-submenu.ts +66 -0
  58. package/src/ui/types.ts +111 -0
  59. package/src/ui.ts +9 -1418
package/CHANGELOG.md CHANGED
@@ -4,6 +4,60 @@ All notable changes to this project are documented here.
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 0.5.2 - 2026-09-05
8
+
9
+ ### Changed
10
+
11
+ - Reorganized configuration, UI, Advisor tool, and command code into smaller modules behind the existing public facades without changing the extension's public API.
12
+
13
+ ### Fixed
14
+
15
+ - Kept failed or aborted Advisor streams from treating partial responses as successful advice or automatic gate permission.
16
+ - Made malformed automatic-gate Markdown fences fail closed when their character or delimiter length does not match.
17
+ - Applied optional secret redaction to targeted Advisor questions before sending provider requests while retaining the raw question for local displays.
18
+ - Kept the TUI tab keybinding out of Socket's URL-string heuristic without changing its runtime behavior.
19
+ - Made benchmark extension attestations follow the root `package.json` version instead of a duplicated constant.
20
+
21
+ ## 0.5.1
22
+
23
+ ### Changed
24
+
25
+ - Updated CI and local validation to Bun 1.4.1 and kept the Pi compatibility packages at 0.84.4; Pi 0.85.0 currently adds an eager `/server` import without declaring the `@earendil-works/pi-server` dependency required by its coding-agent entry point.
26
+ - Kept benchmark tests out of the normal test run; use `bun run test:bench` to run them explicitly.
27
+ - Added Socket security dependency overrides and a `socket` script for `bunx socket optimize`.
28
+
29
+ ### Added
30
+
31
+ - Added a repository-only failsafe benchmark with an offline replay tier, a
32
+ 24-item decision-point corpus, a fail-closed Pi/Harbor ReactBench adapter,
33
+ pinned live-tier controls, hard budget limits, and task-level
34
+ uplift/dominance reporting. See [Benchmarking](docs/benchmark.md).
35
+ - Added bounded local Harbor runtime controls for Apple Container experiments,
36
+ Docker build-cache cleanup, and a recorded one-hour agent timeout for complex
37
+ ReactBench tasks.
38
+ - Added a disposable, credential-free GitHub build compatibility layer for
39
+ pinned ReactBench Dockerfiles whose Git 2.39 transport is rejected by the
40
+ GitHub endpoint, with bounded pre-agent Harbor infrastructure retries and
41
+ per-attempt artifacts for transient build and transport failures.
42
+ - Derived the host Harbor command timeout from the bounded agent timeout and
43
+ made timeout cleanup terminate the full Harbor process group, including the
44
+ broker and descendants.
45
+ - Ensured configured Advisor reasoning effort reaches the provider-facing
46
+ request field used by current Pi AI adapters.
47
+
48
+ ### Fixed
49
+
50
+ - Made `/advisor` open the available-model picker on first use or when either saved model is missing or unavailable; activation never silently selects a model outside the user's available catalog.
51
+ - Added a concise `/advisor` explanation of the Advisor's second-opinion role.
52
+ - Preserved Markdown formatting in visible Scout and nested Advisor thinking previews while retaining their speech-bubble cue. During streaming, incomplete Markdown delimiters display transiently until they close, which is expected behavior when thinking arrives incrementally.
53
+ - Adopted an explicit `/model` selection made before `/advisor` as the Executor on the next successful activation, without persisting ordinary model changes while the flow is off.
54
+
55
+ ## [5.1.0] - 2026-09-04 [YANKED]
56
+
57
+ ### Release status
58
+
59
+ - Published by mistake instead of `0.5.1`; withdrawn from npm and removed from Git, with `latest` restored to `0.5.0`.
60
+
7
61
  ## 0.5.0
8
62
 
9
63
  ### Changed
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # pi-advisor
1
+ # [pi-advisor](https://github.com/philipbrembeck/pi-advisor)
2
2
 
3
3
  <div align="center">
4
4
 
@@ -8,114 +8,80 @@ A configurable second-opinion workflow for <a href="https://github.com/earendil-
8
8
 
9
9
  </div>
10
10
 
11
+ ![d18m Downloads](https://img.shields.io/npm/d18m/pi-advisor-flow?style=flat) ![NPM Version](https://img.shields.io/npm/v/pi-advisor-flow?style=flat) ![Pi Advisor Flow badge](https://img.shields.io/badge/advisor%20flow-fff?logo=pi&logoColor=000)
11
12
 
12
- ![d18m Downloads](https://img.shields.io/npm/d18m/pi-advisor-flow?style=flat) ![NPM Version](https://img.shields.io/npm/v/pi-advisor-flow?style=flat)
13
13
 
14
14
  `pi-advisor-flow` keeps one model focused on execution and makes a second, smarter model available for consequential decisions, stalled work, and final reviews. The Executor still owns the work. The Advisor challenges assumptions, exposes risks, and suggests verification steps without taking over or running tools.
15
15
 
16
- The idea is simple: keep implementation on a fast model and borrow frontier reasoning only when decisions matter. [Read why this workflow is useful](https://philipbrembeck.com/writings/2026/07/only-as-much-intelligence-as-you-need).
16
+ Keep implementation on a fast model and borrow frontier reasoning only when decisions matter. [Read why this workflow is useful](https://philipbrembeck.com/writings/2026/07/only-as-much-intelligence-as-you-need).
17
17
 
18
- ## Features
18
+ ## How it works
19
+
20
+ 1. The Executor works on your task as usual.
21
+ 2. It calls `ask_advisor`, or an enabled gate starts a review.
22
+ 3. pi-advisor reconstructs the relevant conversation and allowed repository context.
23
+ 4. The Advisor returns an opinion. The Executor decides what to adopt, changes the code, and validates it.
19
24
 
20
- - **On-demand second opinions** through the `ask_advisor` tool or `/advisor-manual`.
21
- - **Configurable review gates** before plans, after repeated failures, and before declaring completion.
22
- - **Automatic loop detection** for repeated tool calls, with explicit proceed, revise, or blocked decisions.
23
- - **Separate model and reasoning controls** for the Executor and Advisor.
24
- - **Advisor usage accounting** with per-response token/cost details and optional cumulative direct usage in the Pi footer and session summary. Per-response usage and cost details are shown by default and can be hidden independently from the footer in `/advisor-settings`.
25
- - **Privacy controls** for conversation history, repository context, explicit tracked/untracked file handoff, tool results, secret redaction, and outcome logging.
26
- - **Optional persistent activation, Simple mode, session summaries, and Herdr integration.**
27
- - **Compact searchable `/advisor-settings` controls** that match Pi's settings list and save changes immediately.
28
- - **EXPERIMENTAL Advisor Scout** that uses the configured Executor model to curate conversation evidence before every Advisor call.
25
+ Regular consultations do not block execution. Automatic loop gates are different: they can stop a repeated tool action or session based on your configured failure policy.
29
26
 
30
27
  ## Install
31
28
 
32
- Requires Pi 0.84.1 or later and is compatible with Herdr 0.8.0.
29
+ Requires Pi 0.84.1 or later.
33
30
 
34
31
  ```bash
35
- # npm
36
32
  pi install npm:pi-advisor-flow
33
+ ```
37
34
 
38
- # GitHub
39
- pi install git:github.com/philipbrembeck/pi-advisor.git
35
+ You can also install from GitHub:
40
36
 
41
- # local checkout
42
- pi install /path/to/pi-advisor
37
+ ```bash
38
+ pi install git:github.com/philipbrembeck/pi-advisor.git
43
39
  ```
44
40
 
45
- Restart or reload Pi after installation.
41
+ Reload Pi after installing.
46
42
 
47
43
  ## Quick start
48
44
 
49
- 1. Run `/advisor` to enable the flow and register `ask_advisor`.
50
- 2. Run `/advisor-models` to choose the Executor and Advisor models. Current model and thinking-level selections appear first and ticked, so pressing Enter keeps them.
51
- 3. Run `/advisor-settings` to configure review gates, context, privacy, and limits. Type to fuzzy-search settings; changes save immediately.
52
-
53
- ![Advisor Settings](https://raw.githubusercontent.com/philipbrembeck/pi-advisor/refs/heads/main/assets/settings.png)
54
-
55
- Unknown fields in `advisor.json` are preserved for forward compatibility and reported as non-blocking warnings. Invalid recognized values remain errors, and Advisor commands show the configuration problem without crashing their handlers.
56
-
57
- You can also enable the flow and select both models at once:
58
-
59
45
  ```text
60
- /advisor executor=openai-codex/gpt-5.6-luna advisor=openai-codex/gpt-5.6-sol
46
+ /advisor # Enable the Advisor Flow
47
+ /advisor-models # Choose the Executor and Advisor models
48
+ /advisor-settings # Configure behavior, modes, etc.
61
49
  ```
62
50
 
63
- ## How it works
64
-
65
- 1. The Executor investigates the task and forms its own candidate direction.
66
- 2. For a consequential decision, stalled attempt, or final review, it calls `ask_advisor` with the reconstructed conversation and allowed repository context.
67
- 3. When Experimental Advisor Scout is enabled, the configured Executor model selects relevant conversation groups and writes a short, explicitly untrusted synthesis.
68
- 4. The Advisor receives selected verbatim evidence, required current-request context, and the unchanged deterministic repository, preference, draft, and attachment regions.
69
- 5. The Executor decides what to adopt, performs the work, and validates the result.
70
-
71
- A normal consultation never blocks execution. The optional automatic loop gate is different: it evaluates repeated tool calls and applies the configured failure policy when the Advisor says to revise, reports a block, is unavailable, or returns an invalid decision.
51
+ On first use, or whenever a saved model is unavailable, `/advisor` opens the same available-model picker as `/advisor-models`; it never silently chooses an unconfigured model. After activation, `/advisor` explains that the Advisor reviews the Executor's context without changing files or running tools.
72
52
 
73
- Advisor responses show provider-reported input, output, cache, and cost details when available. Successful `ask_advisor` tool results also carry normalized usage into Pi's built-in `Tools/summaries` and `/cost` totals. Manual consultations and automatic gates remain in the separate session-local direct Advisor accounting because they are custom messages, so they are not double-counted in Pi's Executor totals. Missing or partial provider usage is shown as unavailable rather than fabricated as zero usage. `/advisor-settings` independently controls per-response usage details and the cumulative Advisor footer without disabling this accounting; the footer is off by default.
53
+ From the Executor, `ask_advisor({})` requests a general review. A targeted `question` or concise `draft` can focus the review on a particular decision.
74
54
 
75
- Successful calls return an opaque `adviceId`. If global outcome logging is enabled, the Executor can call `record_advisor_outcome` once to record whether the advice was adopted and whether final validation passed.
76
-
77
- ### Experimental Advisor Scout
78
-
79
- Experimental Advisor Scout is off by default. Enable `Experimental Advisor Scout` in the advanced `/advisor-settings` screen or set `"advisorScoutEnabled": true` in the global `advisor.json`.
80
-
81
- Scout runs before `ask_advisor`, `/advisor-manual`, and automatic Advisor gates. It uses the configured Executor model and Executor reasoning effort in a separate model call. This adds cost and latency, but can reduce cost in the Advisor call. The compact result shows the model, selection counts, elapsed time, and usage/cost details; `Ctrl+O` shows bounded selected labels and the synthesis. Usage and cost details can be hidden in `/advisor-settings`.
82
-
83
- Scout receives a bounded manifest of conversation and tool-history groups after the normal tool disclosure, result-cap, and redaction policies are applied. The Scout manifest has its own fixed transport limit, while the reconstructed conversation remains bounded by the Advisor's remaining context budget after repository context; manifest metadata no longer consumes that Advisor conversation budget. A zero remaining budget produces no history groups. For a pending `ask_advisor` call, Scout receives only the allowlisted question and Git-context preference, never the draft or explicit attachment paths. Scout does not receive the deterministic Git context, draft, project preferences, or explicit tracked and untracked attachments. Those regions are appended later through their existing consent and cap rules.
84
-
85
- This experiment adapts the context-boundary idea from Zhang et al., ["FastContext: Training Efficient Repository Explorer for Coding Agents"](https://arxiv.org/html/2606.14066v1). It is not a reproduction of FastContext. pi-advisor Scout curates conversation history only.
55
+ In the Settings, enable the Simple Mode for a quick start.
56
+ ![Pi Advisor Settings Panel](https://raw.githubusercontent.com/philipbrembeck/pi-advisor/refs/heads/main/assets/settings.png)
86
57
 
87
58
  ## Commands
88
59
 
89
- | Command | Purpose |
90
- | --- | --- |
91
- | `/advisor` | Enable the flow and optionally override the Executor, Advisor, or context limit. |
92
- | `/advisor-manual [focus]` | Open a TUI form for a parallel consultation, or consult immediately in non-TUI modes; shows progress in the footer. |
93
- | `/advisor-models` | Choose both models and their reasoning effort; current models are preselected. |
94
- | `/advisor-settings` | Configure behavior, context, gates, privacy, and output limits. |
95
- | `/advisor-off` | Disable the flow and turn off persistent activation. |
60
+ | Command | What it does |
61
+ | ------------------------- | ------------------------------------------------- |
62
+ | `/advisor` | Enable the flow; choose available models when needed. |
63
+ | `/advisor-manual [focus]` | Ask for an immediate second opinion. |
64
+ | `/advisor-models` | Choose the Executor and Advisor models. |
65
+ | `/advisor-settings` | Configure behavior, context, privacy, and limits. |
66
+ | `/advisor-off` | Disable the flow and persistent activation. |
96
67
 
97
- In the interactive TUI, `/advisor-manual [focus]` opens a centered overlay. The optional focus text is prefilled and editable; Enter submits, Shift+Enter inserts a newline, Tab/Shift+Tab moves focus, and Escape cancels. The form lets you choose the permitted Git context level, with choices above the configured ceiling hidden. `None` withholds Git data only; it does not remove configured conversation history. After submission, the call, Scout, and Advisor streaming progress appear immediately in the transcript. Canceling has no consultation side effects. RPC, print, and JSON invocations retain their immediate argument-driven behavior.
98
-
99
- The Executor calls `ask_advisor({})` for a general review. It can pass a targeted `question` or a concise `draft` describing proposed work, validation, and remaining risks. If the Advisor explicitly says it cannot review a specifically named file, the Executor may make a sequential follow-up call with `includeTrackedFiles` when global consent is enabled and the file is relevant. Draft claims give the Advisor review context; they are not verification evidence.
68
+ ### Experimental Advisor Scout
100
69
 
101
- ## What gets sent to the Advisor
70
+ Advisor Scout is off by default. When enabled, the Executor model first selects relevant conversation history before the Advisor sees it. This adds a model call, latency, and cost. See the [configuration guide](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/configuration.md) for details.
102
71
 
103
- Advisor context can include user messages, tool calls, tool results, and repository information. Secret redaction is off by default, and tools without an explicit disclosure policy default to full context. Review the privacy settings before using the extension with sensitive work.
72
+ ## Privacy
104
73
 
105
- When Experimental Advisor Scout is enabled, the Executor model provider also receives bounded Advisor-eligible conversation history. Scout does not receive the deterministic repository, draft, preference, or explicit-file regions described below.
74
+ Advisor requests can include user messages, tool calls, tool results, targeted questions, and repository information. `/advisor-settings` controls context, tool disclosure, redaction, and explicit file handoff. Secret redaction is off by default; when enabled, credential-shaped values in targeted questions are redacted before the provider request. Tools without an explicit policy use full context. Settings are global, so a project cannot silently change them.
106
75
 
107
- Repository context is configurable from no access through changed-file summaries to a capped patch. When context is disabled or its budget is zero, the Advisor is told it was withheld rather than shown an apparently clean tree. Explicit tracked and untracked file contents require separate global opt-ins; attachments are capped, redacted when configured, and sent as untrusted data.
76
+ Read [Privacy and data handling](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/privacy.md) before using pi-advisor with sensitive work.
108
77
 
109
78
  ## Documentation
110
79
 
111
80
  - [Configuration and automatic loop gates](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/configuration.md)
112
81
  - [Privacy and data handling](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/privacy.md)
113
82
  - [Development](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/development.md)
114
- - [Documentation index](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/README.md)
115
-
116
- ## Links
117
-
118
- - [MIT License](LICENSE)
83
+ - [Benchmarking](docs/benchmark.md)
119
84
  - [Changelog](CHANGELOG.md)
85
+ - [MIT License](LICENSE)
120
86
  - [npm package](https://www.npmjs.com/package/pi-advisor-flow)
121
87
  - [Why use an Advisor flow?](https://philipbrembeck.com/writings/2026/07/only-as-much-intelligence-as-you-need)
@@ -1,16 +1,16 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { registerCommands } from "../src/commands.js";
2
+ import { registerCommands } from "../src/commands/registration.js";
3
3
  import { setHerdrBlockedEmitter } from "../src/herdr.js";
4
4
  import { AdvisorSessionState } from "../src/session-state.js";
5
5
  import {
6
6
  consultAdvisor as consultAdvisorImplementation,
7
- parseAutomaticDecision as parseAutomaticDecisionImplementation,
8
- registerAdvisorTool,
9
7
  runAdvisorGate as runAdvisorGateImplementation,
10
- ScoutStatusManager,
11
- } from "../src/tools.js";
8
+ } from "../src/tools/consultation.js";
9
+ import { parseAutomaticDecision as parseAutomaticDecisionImplementation } from "../src/tools/gate-protocol.js";
10
+ import { registerAdvisorTool } from "../src/tools/registration.js";
11
+ import { ScoutStatusManager } from "../src/tools/scout-status.js";
12
12
 
13
- export type { AdvisorConfig, GateFailureMode } from "../src/config.js";
13
+ export type { AdvisorConfig, GateFailureMode } from "../src/config/types.js";
14
14
  export type {
15
15
  AdvisorConsultationResult,
16
16
  AdvisorGateFailure,
@@ -19,7 +19,7 @@ export type {
19
19
  ConsultationTrigger,
20
20
  GateDecision,
21
21
  GateTrigger,
22
- } from "../src/tools.js";
22
+ } from "../src/tools/types.js";
23
23
  export const consultAdvisor = (
24
24
  ...args: Parameters<typeof consultAdvisorImplementation>
25
25
  ) => consultAdvisorImplementation(...args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-advisor-flow",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -28,33 +28,46 @@
28
28
  "README.md"
29
29
  ],
30
30
  "scripts": {
31
+ "bench:decisions": "bun bench/src/cli.ts decisions",
32
+ "bench:evaluate": "bun bench/src/cli.ts evaluate",
33
+ "bench:replay": "bun bench/src/cli.ts replay",
34
+ "bench:screen": "bun bench/src/cli.ts screen",
35
+ "check:boundaries": "node scripts/check-module-boundaries.mjs",
31
36
  "format": "bunx ultracite fix --linter-enabled=false",
32
37
  "lint": "bunx ultracite check",
33
38
  "lint:fix": "bunx ultracite fix",
34
39
  "package:check": "node scripts/check-package.mjs",
35
40
  "prepare": "husky",
41
+ "socket": "bunx socket optimize",
36
42
  "test": "bun test",
43
+ "test:bench": "bun --config=./bunfig.bench.toml test ./bench/test/",
37
44
  "typecheck": "tsc --noEmit"
38
45
  },
39
46
  "lint-staged": {
40
47
  "*.{json,jsonc,ts}": "bun run lint:fix --"
41
48
  },
49
+ "resolutions": {
50
+ "is-unicode-supported": "npm:@socketregistry/is-unicode-supported@^1",
51
+ "safe-buffer": "npm:@socketregistry/safe-buffer@^1"
52
+ },
42
53
  "overrides": {
43
54
  "brace-expansion": "5.0.9",
55
+ "is-unicode-supported": "npm:@socketregistry/is-unicode-supported@^1",
56
+ "safe-buffer": "npm:@socketregistry/safe-buffer@^1",
44
57
  "undici": "8.9.0"
45
58
  },
46
59
  "devDependencies": {
47
- "@biomejs/biome": "2.5.11",
60
+ "@biomejs/biome": "2.5.12",
48
61
  "@earendil-works/pi-ai": "^0.84.4",
49
62
  "@earendil-works/pi-coding-agent": "^0.84.4",
50
63
  "@earendil-works/pi-tui": "^0.84.4",
51
- "@types/node": "^26.4.0",
52
- "bun-types": "1.3.14",
64
+ "@types/node": "^26.4.1",
65
+ "bun-types": "1.4.1",
53
66
  "husky": "^9.1.7",
54
67
  "lint-staged": "^17.4.1",
55
- "typebox": "^1.3.21",
68
+ "typebox": "^1.3.25",
56
69
  "typescript": "^7.0.2",
57
- "ultracite": "7.10.7"
70
+ "ultracite": "7.10.8"
58
71
  },
59
72
  "peerDependencies": {
60
73
  "@earendil-works/pi-ai": "^0.84.1",
@@ -0,0 +1,117 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ advisorEffortRef,
4
+ advisorRef,
5
+ executorEffortRef,
6
+ executorRef,
7
+ getPersistedModelRefs,
8
+ setAdvisorEffortRef,
9
+ setAdvisorRef,
10
+ setExecutorEffortRef,
11
+ setExecutorRef,
12
+ } from "../config/state.js";
13
+ import { loadConfig } from "../config/storage.js";
14
+ import {
15
+ getAvailableModelRefs,
16
+ getExplicitModelError,
17
+ planActivationModels,
18
+ } from "./model-options.js";
19
+ import { selectAdvisorModels } from "./model-picker.js";
20
+ import { notify } from "./runtime.js";
21
+ import type { CommandRuntime } from "./types.js";
22
+
23
+ export interface PreparedActivationModels {
24
+ pendingExecutor?: string;
25
+ pickedModels: boolean;
26
+ }
27
+
28
+ export const loadCommandConfig = (ctx: ExtensionContext) => {
29
+ try {
30
+ loadConfig(ctx);
31
+ return true;
32
+ } catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ notify(
35
+ ctx,
36
+ `Advisor command could not load configuration: ${message} Fix advisor.json and retry.`,
37
+ "error"
38
+ );
39
+ return false;
40
+ }
41
+ };
42
+
43
+ export const prepareActivationModels = async (
44
+ runtime: CommandRuntime,
45
+ ctx: ExtensionContext,
46
+ announce: boolean,
47
+ executorOverride: boolean,
48
+ advisorOverride: boolean
49
+ ): Promise<PreparedActivationModels | undefined> => {
50
+ const persisted = getPersistedModelRefs();
51
+ const availableRefs = getAvailableModelRefs(ctx);
52
+ const availableRefSet = availableRefs ? new Set(availableRefs) : undefined;
53
+ const explicitError =
54
+ getExplicitModelError(
55
+ ctx,
56
+ executorRef,
57
+ "Executor",
58
+ executorOverride,
59
+ availableRefSet
60
+ ) ??
61
+ getExplicitModelError(
62
+ ctx,
63
+ advisorRef,
64
+ "Advisor",
65
+ advisorOverride,
66
+ availableRefSet
67
+ );
68
+ if (explicitError) {
69
+ notify(ctx, explicitError, "error");
70
+ return;
71
+ }
72
+
73
+ const plan = planActivationModels(
74
+ ctx,
75
+ executorRef,
76
+ advisorRef,
77
+ runtime.pendingExecutorModelRef,
78
+ persisted,
79
+ executorOverride,
80
+ advisorOverride,
81
+ availableRefSet
82
+ );
83
+ setExecutorRef(plan.pendingExecutor ?? executorRef);
84
+ if (!(plan.selectExecutor || plan.selectAdvisor)) {
85
+ return { pendingExecutor: plan.pendingExecutor, pickedModels: false };
86
+ }
87
+
88
+ // Always-on startup cannot open an interactive picker, so it leaves the
89
+ // flow disabled until the user selects both models with `/advisor`.
90
+ if (!announce) {
91
+ notify(
92
+ ctx,
93
+ "Advisor models are not configured or available. Run /advisor to choose them.",
94
+ "error"
95
+ );
96
+ return;
97
+ }
98
+ const selection = await selectAdvisorModels(ctx, {
99
+ advisor: advisorOverride || persisted.advisor ? advisorRef : "",
100
+ advisorEffort: advisorEffortRef,
101
+ executor:
102
+ executorOverride || plan.pendingExecutor || persisted.executor
103
+ ? executorRef
104
+ : "",
105
+ executorEffort: executorEffortRef,
106
+ selectAdvisor: plan.selectAdvisor,
107
+ selectExecutor: plan.selectExecutor,
108
+ });
109
+ if (!selection) {
110
+ return;
111
+ }
112
+ setAdvisorRef(selection.advisor);
113
+ setAdvisorEffortRef(selection.advisorEffort);
114
+ setExecutorRef(selection.executor);
115
+ setExecutorEffortRef(selection.executorEffort);
116
+ return { pendingExecutor: plan.pendingExecutor, pickedModels: true };
117
+ };
@@ -0,0 +1,135 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { parseArgs } from "../config/args.js";
3
+ import {
4
+ advisorEffortRef,
5
+ advisorRef,
6
+ contextMaxCharsRef,
7
+ executorEffortRef,
8
+ executorRef,
9
+ setAdvisorEffortRef,
10
+ setAdvisorRef,
11
+ setContextMaxCharsRef,
12
+ setExecutorEffortRef,
13
+ setExecutorRef,
14
+ } from "../config/state.js";
15
+ import { saveConfig } from "../config/storage.js";
16
+ import {
17
+ loadCommandConfig,
18
+ prepareActivationModels,
19
+ } from "./activation-preparation.js";
20
+ import {
21
+ ADVISOR_ACTIVATION_EXPLANATION,
22
+ findConfiguredModel,
23
+ hasAdvisorOverride,
24
+ hasExecutorOverride,
25
+ } from "./model-options.js";
26
+ import { notify } from "./runtime.js";
27
+ import type { CommandRuntime, ThinkingLevel } from "./types.js";
28
+
29
+ const resolveActivationModels = async (
30
+ runtime: CommandRuntime,
31
+ ctx: ExtensionContext
32
+ ) => {
33
+ const executor = findConfiguredModel(ctx, executorRef);
34
+ if (!executor) {
35
+ return {
36
+ error: executorRef
37
+ ? `Executor model not found: ${executorRef}`
38
+ : "Executor model not configured",
39
+ };
40
+ }
41
+ const advisor = findConfiguredModel(ctx, advisorRef);
42
+ if (!advisor) {
43
+ return {
44
+ error: advisorRef
45
+ ? `Advisor model not found: ${advisorRef}`
46
+ : "Advisor model not configured",
47
+ };
48
+ }
49
+ const advisorAuth = await ctx.modelRegistry.getApiKeyAndHeaders(advisor);
50
+ if (!(advisorAuth.ok && advisorAuth.apiKey)) {
51
+ return { error: `No API key for Advisor ${advisorRef}` };
52
+ }
53
+ if (!(await runtime.setExecutorModel(executor))) {
54
+ return { error: `No API key for Executor ${executorRef}` };
55
+ }
56
+ return {};
57
+ };
58
+
59
+ export const activateAdvisor = async (
60
+ runtime: CommandRuntime,
61
+ args: string,
62
+ ctx: ExtensionContext,
63
+ announce = true
64
+ ) => {
65
+ if (!loadCommandConfig(ctx)) {
66
+ return;
67
+ }
68
+ const previous = {
69
+ advisor: advisorRef,
70
+ advisorEffort: advisorEffortRef,
71
+ contextMaxChars: contextMaxCharsRef,
72
+ executor: executorRef,
73
+ executorEffort: executorEffortRef,
74
+ };
75
+ const restoreRefs = () => {
76
+ setAdvisorRef(previous.advisor);
77
+ setAdvisorEffortRef(previous.advisorEffort);
78
+ setContextMaxCharsRef(previous.contextMaxChars);
79
+ setExecutorRef(previous.executor);
80
+ setExecutorEffortRef(previous.executorEffort);
81
+ };
82
+ const executorOverride = hasExecutorOverride(args);
83
+ const advisorOverride = hasAdvisorOverride(args);
84
+ const argumentError = parseArgs(args);
85
+ if (argumentError) {
86
+ restoreRefs();
87
+ notify(ctx, argumentError, "error");
88
+ return;
89
+ }
90
+
91
+ const prepared = await prepareActivationModels(
92
+ runtime,
93
+ ctx,
94
+ announce,
95
+ executorOverride,
96
+ advisorOverride
97
+ );
98
+ if (!prepared) {
99
+ restoreRefs();
100
+ return;
101
+ }
102
+ const { error } = await resolveActivationModels(runtime, ctx);
103
+ if (error) {
104
+ restoreRefs();
105
+ notify(ctx, error, "error");
106
+ return;
107
+ }
108
+ // parseArgs, model picking, and an inactive `/model` selection only mutate
109
+ // in-memory refs. Persist them once both models are known and authenticated,
110
+ // so an unusable model reference is never written to the configuration.
111
+ if (args.trim() || prepared.pickedModels || prepared.pendingExecutor) {
112
+ saveConfig(ctx, { persistAdvisor: true, persistExecutor: true });
113
+ }
114
+ // A successful activation has committed the effective Executor. Do not let
115
+ // an older inactive selection override an explicit activation argument on a
116
+ // later attempt.
117
+ runtime.pendingExecutorModelRef = undefined;
118
+ if (executorEffortRef) {
119
+ runtime.pi.setThinkingLevel(executorEffortRef as ThinkingLevel);
120
+ }
121
+ if (!runtime.flowEnabled()) {
122
+ runtime.pi.setActiveTools([
123
+ ...runtime.pi.getActiveTools(),
124
+ "ask_advisor",
125
+ "record_advisor_outcome",
126
+ ]);
127
+ }
128
+ if (announce) {
129
+ notify(
130
+ ctx,
131
+ `${ADVISOR_ACTIVATION_EXPLANATION}\n\nAdvisor flow ready — Executor: ${executorRef} (thinking: ${executorEffortRef || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`,
132
+ "info"
133
+ );
134
+ }
135
+ };
@@ -0,0 +1,83 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ alwaysOnRef,
4
+ executorRef,
5
+ getPersistedModelRefs,
6
+ setExecutorRef,
7
+ } from "../config/state.js";
8
+ import { loadConfig, saveConfig } from "../config/storage.js";
9
+ import { herdrAdvisorActivity } from "../herdr.js";
10
+ import { notify } from "./runtime.js";
11
+ import type { CommandRuntime } from "./types.js";
12
+
13
+ export type ActivateAdvisor = (
14
+ args: string,
15
+ ctx: ExtensionContext,
16
+ announce?: boolean
17
+ ) => Promise<void>;
18
+
19
+ export const registerCommandLifecycle = (
20
+ runtime: CommandRuntime,
21
+ activateAdvisor: ActivateAdvisor
22
+ ) => {
23
+ runtime.pi.on("session_start", async (_event, ctx) => {
24
+ runtime.pendingExecutorModelRef = undefined;
25
+ // A malformed advisor.json or a provider auth failure must not reject a
26
+ // lifecycle handler and break session startup.
27
+ try {
28
+ loadConfig(ctx);
29
+ if (alwaysOnRef) {
30
+ await activateAdvisor("", ctx, false);
31
+ }
32
+ } catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ notify(ctx, `Advisor activation failed: ${message}`, "error");
35
+ }
36
+ });
37
+
38
+ runtime.pi.on("model_select", (event, ctx) => {
39
+ // "restore" replays a stored session model and "cycle" changes the active
40
+ // model without an explicit `/model` choice. Neither should redefine the
41
+ // configured Executor.
42
+ if (event.source !== "set" || runtime.suppressModelSelectionSync) {
43
+ return;
44
+ }
45
+ const selected = `${event.model.provider}/${event.model.id}`;
46
+ if (!runtime.flowEnabled()) {
47
+ // Defer persistence until `/advisor` succeeds. This keeps ordinary model
48
+ // selection global defaults untouched when the flow is not enabled.
49
+ runtime.pendingExecutorModelRef = selected;
50
+ return;
51
+ }
52
+ runtime.pendingExecutorModelRef = undefined;
53
+ if (selected === executorRef) {
54
+ return;
55
+ }
56
+ const persisted = getPersistedModelRefs();
57
+ setExecutorRef(selected);
58
+ saveConfig(ctx, {
59
+ persistAdvisor: Boolean(persisted.advisor),
60
+ persistExecutor: true,
61
+ });
62
+ });
63
+
64
+ runtime.pi.on("session_shutdown", (_event, ctx) => {
65
+ if (ctx.hasUI) {
66
+ ctx.ui.setStatus("advisor-usage", undefined);
67
+ }
68
+ for (const [controller, token] of runtime.manualConsultations) {
69
+ controller.abort();
70
+ const timer = runtime.manualProgressTimers.get(controller);
71
+ if (timer) {
72
+ clearInterval(timer);
73
+ runtime.manualProgressTimers.delete(controller);
74
+ }
75
+ runtime.scoutStatus.release(ctx, token);
76
+ }
77
+ runtime.scoutStatus.clear(ctx);
78
+ runtime.manualConsultations.clear();
79
+ runtime.manualProgressTimers.clear();
80
+ runtime.manualProgress.clear();
81
+ herdrAdvisorActivity.clear();
82
+ });
83
+ };