pi-orchestrator-config 4.1.5 → 4.2.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 +36 -8
- package/agents/code-reviewer-docs.md +11 -1
- package/agents/code-reviewer-guidelines.md +11 -1
- package/agents/code-reviewer-quality.md +11 -1
- package/agents/code-reviewer-security.md +11 -1
- package/agents/code-reviewer-spec.md +11 -1
- package/agents/git-expert.md +38 -3
- package/agents/github-expert.md +8 -0
- package/extensions/orchestrator/async-agents.ts +63 -26
- package/extensions/orchestrator/async-capability.ts +8 -8
- package/extensions/orchestrator/cron.ts +1 -1
- package/extensions/orchestrator/dreaming.ts +1 -1
- package/extensions/orchestrator/enforcement.ts +4 -5
- package/extensions/orchestrator/index.ts +55 -0
- package/extensions/orchestrator/parse-model-override.ts +29 -0
- package/extensions/orchestrator/pi-config-review-state.ts +13 -0
- package/extensions/orchestrator/project-settings.ts +118 -111
- package/extensions/orchestrator/resolve-agent-model.ts +24 -13
- package/extensions/orchestrator/review-ui.ts +56 -14
- package/extensions/orchestrator/rule-placeholders.ts +28 -2
- package/extensions/orchestrator/subagent-tool.ts +53 -10
- package/extensions/pidiff/pidiff.ts +36 -4
- package/package.json +4 -3
- package/rules/10-agent-routing.md +11 -0
- package/rules/20-code-review-loop.md +14 -9
- package/settings-keys.json +102 -0
package/README.md
CHANGED
|
@@ -12,8 +12,9 @@ Single extension that provides:
|
|
|
12
12
|
|
|
13
13
|
| Feature | Description |
|
|
14
14
|
|---------|-------------|
|
|
15
|
-
| **Subagent tool** | Delegate tasks to specialist agents (single, parallel, chain, async modes) |
|
|
16
|
-
|
|
|
15
|
+
| **Subagent tool** | Delegate tasks to specialist agents (single, parallel, chain, async modes); optional `model` param for model override |
|
|
16
|
+
| **`list_models`** | List available models and providers for subagent model override |
|
|
17
|
+
| **Async background agents** | Spawn agents in background with `async: true` — results surface automatically when complete. Fullscreen overlay dashboard with live output, keyboard nav, and kill support (`/async-status`, `/async-kill`). On **acpx** parents, optional async is coerced to sync; dream/cron need `internal_operations_provider` + `internal_operations_model`. `cli-*` providers support async natively (see `dev-docs/cli-provider.md`) |
|
|
17
18
|
| **CLI / ACPX providers** | Optional `cli_agents` / `acpx_agents` register `cli-*` / `acpx-*` via native `createProvider` (pi ≥ 0.81): `/login cli-<agent>` or `/login acpx-<agent>`, model refresh, filter when unavailable |
|
|
18
19
|
| **`/btw` command** | Quick side questions without polluting conversation history — ephemeral overlay |
|
|
19
20
|
| **`/async-status` command** | Show status of background agents — select one for live output streaming |
|
|
@@ -212,6 +213,19 @@ Use python-expert to fix the type errors in src/models.py
|
|
|
212
213
|
Run scout and planner in a chain to analyze the auth module
|
|
213
214
|
```
|
|
214
215
|
|
|
216
|
+
**Run agent with specific model:**
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# Discover available models
|
|
220
|
+
list_models(provider="litellm")
|
|
221
|
+
|
|
222
|
+
# Run agent with explicit model
|
|
223
|
+
subagent(agent="worker", task="...", model="litellm/claude-opus-4-6-1m")
|
|
224
|
+
|
|
225
|
+
# Parallel tasks with per-task models
|
|
226
|
+
subagent(tasks=[{agent: "worker", task: "...", model: "litellm/claude-opus-4-6-1m", cwd: "..."}])
|
|
227
|
+
```
|
|
228
|
+
|
|
215
229
|
## Code Review Loop
|
|
216
230
|
|
|
217
231
|
After any code change, the orchestrator runs 6 agents **in parallel** (5 reviewers + test-automator):
|
|
@@ -231,9 +245,9 @@ always completes fix/explain (5a) before the cap check; the cap only blocks re-d
|
|
|
231
245
|
At cap, report **Not fixed** (explained why not → outstanding) vs **Fixed**
|
|
232
246
|
(verification blocked by the cap — cannot re-dispatch to confirm clean). Invalid values (including non-digit
|
|
233
247
|
forms like `"10.0"` / `"1e1"`) fall through to the next resolution layer / default `3`. Disable the review loop via
|
|
234
|
-
`review_loop_enforcement: false`, not via max_cycles.
|
|
235
|
-
|
|
236
|
-
`
|
|
248
|
+
`review_loop_enforcement: false`, not via max_cycles. Commit blocking is code-enforced via `isCommitAllowed`:
|
|
249
|
+
allows commit when `isReviewClean` (`status: clean` + `tests_passed: true`, or `status: none`), or when max cycles are exhausted
|
|
250
|
+
(`status` is `has_findings` or `clean`, no pending reviewers, `cycle >= review_loop_max_cycles` — no `tests_passed` requirement).
|
|
237
251
|
|
|
238
252
|
Staged mode (`--autorabbit`/`--autoqodo` in `/review-handler`) shares one total `review_loop_max_cycles` budget
|
|
239
253
|
across both its Spec Compliance and Code Quality stages — not a separate cap per stage.
|
|
@@ -260,8 +274,8 @@ Create `.pi/pi-config-settings.json` in your project to override global defaults
|
|
|
260
274
|
"image_model": "gemini-3-pro-image",
|
|
261
275
|
"acpx_agents": ["cursor"],
|
|
262
276
|
"cli_agents": ["claude", "cursor"],
|
|
263
|
-
"
|
|
264
|
-
"
|
|
277
|
+
"internal_operations_provider": "anthropic",
|
|
278
|
+
"internal_operations_model": "claude-sonnet-4-20250514",
|
|
265
279
|
"agent_provider": "cli-cursor",
|
|
266
280
|
"agent_model": "cursor:cursor-grok-4.5-high-fast",
|
|
267
281
|
"agent_overrides": {
|
|
@@ -630,8 +644,10 @@ PI_PIDIFF_ENABLE=false pi
|
|
|
630
644
|
| `git` | Version control |
|
|
631
645
|
| `gh` | GitHub CLI (PRs, issues) |
|
|
632
646
|
| `glab` | GitLab CLI (MRs, issues, pipelines) — install [gitlab-cli-skills](https://github.com/vince-winkintel/gitlab-cli-skills) for full agent support |
|
|
647
|
+
| `gcc` | C compiler — needed for building Python C extensions (e.g. ovirt-engine-sdk-python) |
|
|
633
648
|
| `uv` / `uvx` | Python execution (enforced by orchestrator) |
|
|
634
649
|
| `go` | Go development and code review |
|
|
650
|
+
| `libxml2-dev` | libxml2 headers — needed for building Python C extensions (e.g. ovirt-engine-sdk-python) |
|
|
635
651
|
| `mcpl` | MCP server access (search, Jenkins, etc.) |
|
|
636
652
|
| `myk-pi-tools` | PR review, release, and other CLI utilities |
|
|
637
653
|
| `prek` | Pre-commit hook runner |
|
|
@@ -663,7 +679,8 @@ PI_PIDIFF_ENABLE=false pi
|
|
|
663
679
|
| `myk-pi-tools reviews` | Review handling commands |
|
|
664
680
|
| `myk-pi-tools memory` | Project memory commands — persistent per-repo learning |
|
|
665
681
|
| `myk-pi-tools ai-cli` | AI CLI commands (cursor, claude, gemini) |
|
|
666
|
-
| `myk-pi-tools coderabbit` | CodeRabbit commands |
|
|
682
|
+
| `myk-pi-tools coderabbit` | CodeRabbit commands (check, trigger, retry) |
|
|
683
|
+
| `myk-pi-tools settings get [key ...]` | Resolve settings (project → global → env → default). No args = all keys |
|
|
667
684
|
|
|
668
685
|
### What's protected
|
|
669
686
|
|
|
@@ -802,6 +819,17 @@ A standalone HTTP sidecar wrapping the Pi SDK, living under `packages/pi-sidecar
|
|
|
802
819
|
CLI commands: `npx pi-sidecar` (start server), `npx pi-sidecar-start` (background start/stop).
|
|
803
820
|
|
|
804
821
|
See [`packages/pi-sidecar/README.md`](packages/pi-sidecar/README.md) for full documentation.
|
|
822
|
+
See [`packages/pi-sidecar/CONSUMER-GUIDE.md`](packages/pi-sidecar/CONSUMER-GUIDE.md) for integration best practices.
|
|
823
|
+
|
|
824
|
+
### pi-vertex-claude
|
|
825
|
+
|
|
826
|
+
Google Vertex AI Claude provider for Pi, living under `packages/pi-vertex-claude/`.
|
|
827
|
+
|
|
828
|
+
| Package | Language | Registry |
|
|
829
|
+
|---------|----------|----------|
|
|
830
|
+
| `@myk-org/pi-vertex-claude` | TypeScript | npm |
|
|
831
|
+
|
|
832
|
+
See [`packages/pi-vertex-claude/README.md`](packages/pi-vertex-claude/README.md) for full documentation.
|
|
805
833
|
|
|
806
834
|
## License
|
|
807
835
|
|
|
@@ -11,7 +11,7 @@ You are a code review specialist focused on **documentation quality, completenes
|
|
|
11
11
|
- Execute first, explain after
|
|
12
12
|
- Do NOT modify files — only review and report findings
|
|
13
13
|
- If a task falls outside your domain, report it and hand off
|
|
14
|
-
- Get the diff with `git diff origin/$PI_REVIEW_BASE_BRANCH
|
|
14
|
+
- Get the diff with `git diff origin/$PI_REVIEW_BASE_BRANCH` (includes uncommitted changes)
|
|
15
15
|
|
|
16
16
|
## Project Guidelines (MANDATORY — read before reviewing)
|
|
17
17
|
|
|
@@ -143,3 +143,13 @@ If no issues: `{"findings": []}`
|
|
|
143
143
|
Severity values: `CRITICAL`, `WARNING`, `SUGGESTION`
|
|
144
144
|
|
|
145
145
|
After writing your response, validate it is parseable JSON.
|
|
146
|
+
|
|
147
|
+
## Prior Review Cycle
|
|
148
|
+
|
|
149
|
+
If your prompt includes a `<prior-review-cycle>` block with previous findings and responses:
|
|
150
|
+
|
|
151
|
+
- **Fixed findings** → verify the fix is correct. Do NOT re-raise if the fix addresses the issue.
|
|
152
|
+
- **Explained findings** → accept valid technical explanations. Only re-raise if the explanation is factually wrong (state why in the finding description field).
|
|
153
|
+
- **Focus on NEW issues** not covered in prior cycles.
|
|
154
|
+
|
|
155
|
+
Do NOT repeat findings that were adequately addressed. All output MUST remain JSON-only — no prose outside the JSON object.
|
|
@@ -11,7 +11,7 @@ You are a code review specialist focused on **project guidelines and style adher
|
|
|
11
11
|
- Execute first, explain after
|
|
12
12
|
- Do NOT modify files — only review and report findings
|
|
13
13
|
- If a task falls outside your domain, report it and hand off
|
|
14
|
-
- Get the diff with `git diff origin/$PI_REVIEW_BASE_BRANCH
|
|
14
|
+
- Get the diff with `git diff origin/$PI_REVIEW_BASE_BRANCH` (includes uncommitted changes)
|
|
15
15
|
|
|
16
16
|
## Review Focus
|
|
17
17
|
|
|
@@ -95,3 +95,13 @@ If no issues: `{"findings": []}`
|
|
|
95
95
|
Severity values: `CRITICAL`, `WARNING`, `SUGGESTION`
|
|
96
96
|
|
|
97
97
|
After writing your response, validate it is parseable JSON.
|
|
98
|
+
|
|
99
|
+
## Prior Review Cycle
|
|
100
|
+
|
|
101
|
+
If your prompt includes a `<prior-review-cycle>` block with previous findings and responses:
|
|
102
|
+
|
|
103
|
+
- **Fixed findings** → verify the fix is correct. Do NOT re-raise if the fix addresses the issue.
|
|
104
|
+
- **Explained findings** → accept valid technical explanations. Only re-raise if the explanation is factually wrong (state why in the finding description field).
|
|
105
|
+
- **Focus on NEW issues** not covered in prior cycles.
|
|
106
|
+
|
|
107
|
+
Do NOT repeat findings that were adequately addressed. All output MUST remain JSON-only — no prose outside the JSON object.
|
|
@@ -11,7 +11,7 @@ You are a code review specialist focused on **general code quality and maintaina
|
|
|
11
11
|
- Execute first, explain after
|
|
12
12
|
- Do NOT modify files — only review and report findings
|
|
13
13
|
- If a task falls outside your domain, report it and hand off
|
|
14
|
-
- Get the diff with `git diff origin/$PI_REVIEW_BASE_BRANCH
|
|
14
|
+
- Get the diff with `git diff origin/$PI_REVIEW_BASE_BRANCH` (includes uncommitted changes)
|
|
15
15
|
|
|
16
16
|
## Project Guidelines (MANDATORY — read before reviewing)
|
|
17
17
|
|
|
@@ -140,3 +140,13 @@ If no issues: `{"findings": []}`
|
|
|
140
140
|
Severity values: `CRITICAL`, `WARNING`, `SUGGESTION`
|
|
141
141
|
|
|
142
142
|
After writing your response, validate it is parseable JSON.
|
|
143
|
+
|
|
144
|
+
## Prior Review Cycle
|
|
145
|
+
|
|
146
|
+
If your prompt includes a `<prior-review-cycle>` block with previous findings and responses:
|
|
147
|
+
|
|
148
|
+
- **Fixed findings** → verify the fix is correct. Do NOT re-raise if the fix addresses the issue.
|
|
149
|
+
- **Explained findings** → accept valid technical explanations. Only re-raise if the explanation is factually wrong (state why in the finding description field).
|
|
150
|
+
- **Focus on NEW issues** not covered in prior cycles.
|
|
151
|
+
|
|
152
|
+
Do NOT repeat findings that were adequately addressed. All output MUST remain JSON-only — no prose outside the JSON object.
|
|
@@ -11,7 +11,7 @@ You are a code review specialist focused on **bugs, logic errors, and security v
|
|
|
11
11
|
- Execute first, explain after
|
|
12
12
|
- Do NOT modify files — only review and report findings
|
|
13
13
|
- If a task falls outside your domain, report it and hand off
|
|
14
|
-
- Get the diff with `git diff origin/$PI_REVIEW_BASE_BRANCH
|
|
14
|
+
- Get the diff with `git diff origin/$PI_REVIEW_BASE_BRANCH` (includes uncommitted changes)
|
|
15
15
|
|
|
16
16
|
## Review Focus
|
|
17
17
|
|
|
@@ -105,3 +105,13 @@ If no issues: `{"findings": []}`
|
|
|
105
105
|
Severity values: `CRITICAL`, `WARNING`, `SUGGESTION`
|
|
106
106
|
|
|
107
107
|
After writing your response, validate it is parseable JSON.
|
|
108
|
+
|
|
109
|
+
## Prior Review Cycle
|
|
110
|
+
|
|
111
|
+
If your prompt includes a `<prior-review-cycle>` block with previous findings and responses:
|
|
112
|
+
|
|
113
|
+
- **Fixed findings** → verify the fix is correct. Do NOT re-raise if the fix addresses the issue.
|
|
114
|
+
- **Explained findings** → accept valid technical explanations. Only re-raise if the explanation is factually wrong (state why in the finding description field).
|
|
115
|
+
- **Focus on NEW issues** not covered in prior cycles.
|
|
116
|
+
|
|
117
|
+
Do NOT repeat findings that were adequately addressed. All output MUST remain JSON-only — no prose outside the JSON object.
|
|
@@ -11,7 +11,7 @@ You are a code review specialist focused on **spec compliance — alignment betw
|
|
|
11
11
|
- Execute first, explain after
|
|
12
12
|
- Do NOT modify files — only review and report findings
|
|
13
13
|
- If a task falls outside your domain, report it and hand off
|
|
14
|
-
- Get the diff with `git diff origin/$PI_REVIEW_BASE_BRANCH
|
|
14
|
+
- Get the diff with `git diff origin/$PI_REVIEW_BASE_BRANCH` (includes uncommitted changes)
|
|
15
15
|
- You MUST run `gh pr view` and `gh issue view` commands every time — even if you think you already have the data from a prior turn. Prior turn data is STALE.
|
|
16
16
|
- If `$PI_HAS_PR` is `false`, this is a pre-push review — no PR exists yet. Follow the reduced flow in Step 1.
|
|
17
17
|
|
|
@@ -162,3 +162,13 @@ If no issues: `{"findings": []}`
|
|
|
162
162
|
Severity values: `SUGGESTION`
|
|
163
163
|
|
|
164
164
|
After writing your response, validate it is parseable JSON.
|
|
165
|
+
|
|
166
|
+
## Prior Review Cycle
|
|
167
|
+
|
|
168
|
+
If your prompt includes a `<prior-review-cycle>` block with previous findings and responses:
|
|
169
|
+
|
|
170
|
+
- **Fixed findings** → verify the fix is correct. Do NOT re-raise if the fix addresses the issue.
|
|
171
|
+
- **Explained findings** → accept valid technical explanations. Only re-raise if the explanation is factually wrong (state why in the finding description field).
|
|
172
|
+
- **Focus on NEW issues** not covered in prior cycles.
|
|
173
|
+
|
|
174
|
+
Do NOT repeat findings that were adequately addressed. All output MUST remain JSON-only — no prose outside the JSON object.
|
package/agents/git-expert.md
CHANGED
|
@@ -13,9 +13,44 @@ You are a Git Expert responsible for all local git operations and version contro
|
|
|
13
13
|
- Do NOT ask for confirmation — execute directly
|
|
14
14
|
- If a task falls outside your domain, report it and hand off
|
|
15
15
|
|
|
16
|
+
## Review Loop Enforcement
|
|
17
|
+
|
|
18
|
+
{{SETTINGS:review_loop_enforcement,review_loop_max_cycles}}
|
|
19
|
+
|
|
20
|
+
Before ANY `git commit`, if `review_loop_enforcement` is `true`:
|
|
21
|
+
|
|
22
|
+
1. Read `.pi/data/pi-config-review-state.json` (use the `read` tool, NOT `cat`/`grep`) and check the state.
|
|
23
|
+
|
|
24
|
+
2. **BLOCK the commit unless one of the allow cases in step 4 holds.**
|
|
25
|
+
Common block reasons: `status` is `"needs_review"` / `"in_progress"`,
|
|
26
|
+
`status` is `"clean"` with `tests_passed: false` (does **not** block the max-cycle path),
|
|
27
|
+
`reviewers_pending` is not empty,
|
|
28
|
+
or `has_findings` but `cycle` still below `review_loop_max_cycles`.
|
|
29
|
+
|
|
30
|
+
3. When blocking, report: "⛔ Review loop incomplete (status: X, tests_passed: Y). Run the review loop before committing." and STOP — do NOT proceed with the commit.
|
|
31
|
+
|
|
32
|
+
4. **Only commit when:**
|
|
33
|
+
- Status is `"clean"` with `tests_passed: true`, OR
|
|
34
|
+
- Status is `"none"` (no review tracking), OR
|
|
35
|
+
- Max cycles exhausted: status is `"has_findings"`, `"clean"`, or `"needs_review"`,
|
|
36
|
+
`reviewers_pending` is empty, and `cycle` >= `review_loop_max_cycles`. No `tests_passed` requirement.
|
|
37
|
+
|
|
38
|
+
## Project Settings
|
|
39
|
+
|
|
40
|
+
{{SETTINGS:dco,commit_trailer,use_worktrees,allow_push_to_protected_branches}}
|
|
41
|
+
|
|
42
|
+
- If `dco` is `true`: add `--signoff` to `git commit`. Skip if already present.
|
|
43
|
+
- If `commit_trailer` is a string (e.g. `"Assisted-by"`): append trailer to the commit message.
|
|
44
|
+
Format: `TRAILER: PI (MODEL) <noreply@pi.dev>` using `$PI_MODEL` for model. Skip if already present.
|
|
45
|
+
If the value contains commas, use the first name.
|
|
46
|
+
- If `use_worktrees` is `true`: BLOCK `git checkout` (except `git checkout -- <file>`) and `git switch`.
|
|
47
|
+
Use `git worktree add .worktrees/NAME -b BRANCH DEFAULT_BRANCH` instead
|
|
48
|
+
(detect default branch via `git symbolic-ref refs/remotes/origin/HEAD`, fallback to `main`).
|
|
49
|
+
- If `allow_push_to_protected_branches` is `true`: commits and pushes to main/master are allowed.
|
|
50
|
+
|
|
16
51
|
## Protection Rules
|
|
17
52
|
|
|
18
|
-
- NEVER commit or push to main/master branch
|
|
53
|
+
- NEVER commit or push to main/master branch (unless `allow_push_to_protected_branches` is `true` — see Project Settings)
|
|
19
54
|
- NEVER commit to already-merged branches
|
|
20
55
|
- NEVER use `--no-verify` flag
|
|
21
56
|
- Branch prefixes: `feature/`, `fix/`, `hotfix/`, `refactor/`
|
|
@@ -38,7 +73,7 @@ Format rules:
|
|
|
38
73
|
- First line: Clear, concise title (50 chars or less)
|
|
39
74
|
- Blank line separator
|
|
40
75
|
- Body: Detailed explanation if needed
|
|
41
|
-
- NO
|
|
76
|
+
- NO ad-hoc AI signatures. When `commit_trailer` is configured, append that trailer only (see Project Settings)
|
|
42
77
|
|
|
43
78
|
## Standard Workflows
|
|
44
79
|
|
|
@@ -51,7 +86,7 @@ Format rules:
|
|
|
51
86
|
|
|
52
87
|
**Create branch and push:**
|
|
53
88
|
|
|
54
|
-
1. `git checkout -b branch-name`
|
|
89
|
+
1. `git checkout -b branch-name` (if `use_worktrees` is enabled: `git worktree add .worktrees/NAME -b BRANCH DEFAULT_BRANCH`)
|
|
55
90
|
2. Verify changes committed
|
|
56
91
|
3. `git push -u origin branch-name`
|
|
57
92
|
|
package/agents/github-expert.md
CHANGED
|
@@ -27,6 +27,14 @@ This agent does NOT run tests. When tests are required (e.g., before creating a
|
|
|
27
27
|
1. Ask orchestrator: "Have all tests passed?"
|
|
28
28
|
2. If NO/UNKNOWN: "Please delegate to test-runner, then call me again"
|
|
29
29
|
|
|
30
|
+
## Project Settings
|
|
31
|
+
|
|
32
|
+
{{SETTINGS:comment_signature}}
|
|
33
|
+
|
|
34
|
+
- If `comment_signature` is `true`: append a signature to every PR/issue comment body.
|
|
35
|
+
Format: `\n\n---\n*Assisted-by: PI (<model>)*` using `$PI_MODEL` for model.
|
|
36
|
+
If `$PI_COMMENT_SIGNATURE` is set, use: `\n\n---\n*$PI_COMMENT_SIGNATURE*`.
|
|
37
|
+
|
|
30
38
|
## Core Operations
|
|
31
39
|
|
|
32
40
|
### Pull Requests
|
|
@@ -7,6 +7,7 @@ import * as fs from "node:fs";
|
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
10
11
|
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
13
|
|
|
@@ -16,12 +17,23 @@ import { discoverAgents } from "./agents.js";
|
|
|
16
17
|
import { resolveAgentModelProvider } from "./resolve-agent-model.js";
|
|
17
18
|
import { getPiInvocation, getProjectTmpDir, parseProcStartTime, djb2Hash } from "./utils.js";
|
|
18
19
|
import { addReviewerPending, recordReviewerResult, countFindings, readReviewState, markTestsPassed, markTestsFailed } from "./pi-config-review-state.js";
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
getMainBranch,
|
|
22
|
+
} from "./git-helpers.js";
|
|
20
23
|
import { waitForResultFiles } from "./async-wait.js";
|
|
21
24
|
import { openAsyncStatusOverlay } from "./async-status-ui.js";
|
|
22
25
|
export { autoCompleteTask, autoMarkInProgress } from "./task-lifecycle.js";
|
|
23
26
|
import { autoCompleteTask, autoMarkInProgress } from "./task-lifecycle.js";
|
|
24
27
|
import { setSlot } from "./status-bar.js";
|
|
28
|
+
import { getSetting } from "./project-settings.js";
|
|
29
|
+
import { substituteSettingsPlaceholders } from "./rule-placeholders.js";
|
|
30
|
+
|
|
31
|
+
const SETTINGS_KEYS: Record<string, unknown> = JSON.parse(
|
|
32
|
+
fs.readFileSync(
|
|
33
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "settings-keys.json"),
|
|
34
|
+
"utf-8",
|
|
35
|
+
),
|
|
36
|
+
);
|
|
25
37
|
|
|
26
38
|
// ── Constants ────────────────────────────────────────────────────────────
|
|
27
39
|
|
|
@@ -101,7 +113,7 @@ export function registerAsyncAgents(
|
|
|
101
113
|
pi: ExtensionAPI,
|
|
102
114
|
terminalNotify: (title: string, body: string) => void,
|
|
103
115
|
): {
|
|
104
|
-
spawnAsyncAgent: (agentName: string, task: string, cwd: string, agents: AgentConfig[], options?: { fireAndForget?: boolean; name?: string; parentModelId?: string; parentProvider?: string; groupId?: string; taskId?: string; onComplete?: () => void; persistSession?: boolean }) => { id: string; error?: string; model?: string };
|
|
116
|
+
spawnAsyncAgent: (agentName: string, task: string, cwd: string, agents: AgentConfig[], options?: { fireAndForget?: boolean; name?: string; parentModelId?: string; parentProvider?: string; groupId?: string; taskId?: string; onComplete?: () => void; persistSession?: boolean; explicit?: { model?: string; provider?: string } }) => { id: string; error?: string; model?: string };
|
|
105
117
|
killAsyncAgent: (target: string) => { killed: string[]; errors: string[] };
|
|
106
118
|
getAsyncJobs: () => Array<{ id: string; agent: string; name?: string; task: string; status: string; startedAt: number }>;
|
|
107
119
|
} {
|
|
@@ -193,7 +205,7 @@ export function registerAsyncAgents(
|
|
|
193
205
|
job.updatedAt = Date.now();
|
|
194
206
|
if (job.agent.startsWith("code-reviewer-")) {
|
|
195
207
|
const findings = countFindings(typeof data.output === "string" ? data.output : "");
|
|
196
|
-
try { recordReviewerResult(jobCwd(job), job.agent, findings < 0 ? 1 : findings); } catch {}
|
|
208
|
+
try { recordReviewerResult(jobCwd(job), job.agent, findings < 0 ? 1 : findings); } catch (e: any) { asyncLog(`recordReviewerResult failed for ${job.agent}: ${e?.message}`); }
|
|
197
209
|
}
|
|
198
210
|
try { fs.unlinkSync(resultFilePath); } catch {}
|
|
199
211
|
} catch {
|
|
@@ -202,7 +214,7 @@ export function registerAsyncAgents(
|
|
|
202
214
|
job.durationMs = Date.now() - job.startedAt;
|
|
203
215
|
job.updatedAt = Date.now();
|
|
204
216
|
if (job.agent.startsWith("code-reviewer-")) {
|
|
205
|
-
try { recordReviewerResult(jobCwd(job), job.agent, 0); } catch {}
|
|
217
|
+
try { recordReviewerResult(jobCwd(job), job.agent, 0); } catch (e: any) { asyncLog(`recordReviewerResult failed for ${job.agent}: ${e?.message}`); }
|
|
206
218
|
}
|
|
207
219
|
}
|
|
208
220
|
} else {
|
|
@@ -212,7 +224,7 @@ export function registerAsyncAgents(
|
|
|
212
224
|
job.updatedAt = Date.now();
|
|
213
225
|
// Record killed reviewer as having 0 findings — prevents permanent commit block
|
|
214
226
|
if (job.agent.startsWith("code-reviewer-")) {
|
|
215
|
-
try { recordReviewerResult(jobCwd(job), job.agent, 0); } catch {
|
|
227
|
+
try { recordReviewerResult(jobCwd(job), job.agent, 0); } catch (e: any) { asyncLog(`recordReviewerResult failed for ${job.agent}: ${e?.message}`); }
|
|
216
228
|
}
|
|
217
229
|
}
|
|
218
230
|
// Check if this completes a group — deliver remaining siblings' results
|
|
@@ -260,7 +272,6 @@ export function registerAsyncAgents(
|
|
|
260
272
|
// Ingest any unprocessed result files — zombie/kill paths may trigger delivery
|
|
261
273
|
// before processResultFile() has read all group members' outputs.
|
|
262
274
|
// Wait up to 2s total (not per-job) for all missing result files.
|
|
263
|
-
const lateIngestedIds = new Set<string>();
|
|
264
275
|
// Wait for non-failed, non-fireAndForget jobs that haven't been ingested yet
|
|
265
276
|
const waitJobs = groupJobs.filter(j => j.output === undefined && !j.fireAndForget && j.status !== "failed");
|
|
266
277
|
if (waitJobs.length > 0) {
|
|
@@ -277,21 +288,33 @@ export function registerAsyncAgents(
|
|
|
277
288
|
j.exitCode = data.exitCode;
|
|
278
289
|
j.durationMs = data.durationMs;
|
|
279
290
|
if (data.success !== undefined) j.status = data.success ? "complete" : "failed";
|
|
280
|
-
lateIngestedIds.add(j.id);
|
|
281
291
|
asyncLog(`deliverGroupResults: late-ingested result for ${j.id}`);
|
|
282
292
|
} catch (e: any) { asyncLog(`deliverGroupResults: late-ingest failed for ${j.id}: ${e?.message}`); }
|
|
283
293
|
}
|
|
284
294
|
|
|
285
|
-
// Track reviewer completions for
|
|
286
|
-
//
|
|
295
|
+
// Track reviewer completions for ALL code-reviewer jobs in the group.
|
|
296
|
+
// processResultFile may have already called recordReviewerResult for some,
|
|
297
|
+
// but recordReviewerResult is idempotent (skips if reviewer already reported).
|
|
287
298
|
for (const j of groupJobs) {
|
|
288
|
-
if (!j.agent.startsWith("code-reviewer-")
|
|
299
|
+
if (!j.agent.startsWith("code-reviewer-")) continue;
|
|
289
300
|
try {
|
|
290
301
|
const output = typeof j.output === "string" ? j.output : "";
|
|
291
302
|
const findings = countFindings(output);
|
|
292
303
|
// -1 means invalid JSON output — treat conservatively as having findings
|
|
293
304
|
recordReviewerResult(jobCwd(j), j.agent, findings < 0 ? 1 : findings);
|
|
294
|
-
} catch {
|
|
305
|
+
} catch (e: any) { asyncLog(`recordReviewerResult failed for ${j.agent}: ${e?.message}`); }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Track test agent completions for grouped test-automator/test-runner
|
|
309
|
+
for (const j of groupJobs) {
|
|
310
|
+
if (j.agent !== "test-automator" && j.agent !== "test-runner") continue;
|
|
311
|
+
try {
|
|
312
|
+
if (j.status === "complete") {
|
|
313
|
+
markTestsPassed(jobCwd(j));
|
|
314
|
+
} else if (j.status === "failed") {
|
|
315
|
+
markTestsFailed(jobCwd(j));
|
|
316
|
+
}
|
|
317
|
+
} catch (e: any) { asyncLog(`markTests failed for ${j.agent}: ${e?.message}`); }
|
|
295
318
|
}
|
|
296
319
|
|
|
297
320
|
// Skip delivery if ALL jobs in group are fire-and-forget
|
|
@@ -364,8 +387,16 @@ export function registerAsyncAgents(
|
|
|
364
387
|
const data = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
365
388
|
const job = asyncState.jobs.get(data.id);
|
|
366
389
|
asyncLog(`processResultFile: ${path.basename(resultPath)} job=${!!job} delivered=${job?.delivered} hasCtx=${!!asyncState.lastCtx} fireAndForget=${job?.fireAndForget} groupId=${job?.groupId}`);
|
|
367
|
-
if (!job)
|
|
368
|
-
|
|
390
|
+
if (!job) {
|
|
391
|
+
// Orphan result file — job not in current session. Clean up to prevent re-delivery on reload.
|
|
392
|
+
try { fs.unlinkSync(resultPath); } catch (e: any) { asyncLog(`orphan cleanup failed: ${resultPath}: ${e?.message}`); }
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (job.delivered) {
|
|
396
|
+
// Already delivered — clean up stale file
|
|
397
|
+
try { fs.unlinkSync(resultPath); } catch (e: any) { asyncLog(`stale cleanup failed: ${resultPath}: ${e?.message}`); }
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
369
400
|
// Skip if already ingested (grouped jobs stay on disk until group delivers)
|
|
370
401
|
if (job.output !== undefined) return;
|
|
371
402
|
|
|
@@ -387,7 +418,7 @@ export function registerAsyncAgents(
|
|
|
387
418
|
const findings = countFindings(output);
|
|
388
419
|
// -1 means invalid JSON output — treat conservatively as having findings
|
|
389
420
|
recordReviewerResult(jobCwd(job), job.agent, findings < 0 ? 1 : findings);
|
|
390
|
-
} catch {
|
|
421
|
+
} catch (e: any) { asyncLog(`recordReviewerResult failed for ${job.agent}: ${e?.message}`); }
|
|
391
422
|
// Clear reviewer session if context usage > 80% to prevent overflow on next cycle
|
|
392
423
|
// Use model context window from the agent's effective model.
|
|
393
424
|
// If agent uses a custom model, we pass its context window via the config.
|
|
@@ -513,7 +544,7 @@ export function registerAsyncAgents(
|
|
|
513
544
|
task: string,
|
|
514
545
|
cwd: string,
|
|
515
546
|
agents: AgentConfig[],
|
|
516
|
-
options?: { fireAndForget?: boolean; name?: string; parentModelId?: string; parentProvider?: string; groupId?: string; taskId?: string; onComplete?: () => void; persistSession?: boolean },
|
|
547
|
+
options?: { fireAndForget?: boolean; name?: string; parentModelId?: string; parentProvider?: string; groupId?: string; taskId?: string; onComplete?: () => void; persistSession?: boolean; explicit?: { model?: string; provider?: string } },
|
|
517
548
|
): { id: string; error?: string; model?: string } {
|
|
518
549
|
const agent = agents.find(a => a.name === agentName);
|
|
519
550
|
if (!agent) return { id: "", error: `Unknown agent: "${agentName}"` };
|
|
@@ -536,6 +567,7 @@ export function registerAsyncAgents(
|
|
|
536
567
|
if (!parentStartTime) {
|
|
537
568
|
console.debug("[async-agents] WARNING: could not read /proc/self/stat starttime after 3 attempts");
|
|
538
569
|
}
|
|
570
|
+
const { model: effectiveModel, provider: effectiveProvider } = resolveAgentModelProvider(agentName, agent, options?.parentModelId, options?.parentProvider, cwd, options?.explicit);
|
|
539
571
|
fs.writeFileSync(path.join(workerDir, "session.json"), JSON.stringify({
|
|
540
572
|
resultsDir: ASYNC_RESULTS_DIR,
|
|
541
573
|
fireAndForget: options?.fireAndForget || false,
|
|
@@ -572,14 +604,18 @@ export function registerAsyncAgents(
|
|
|
572
604
|
: agentName + ':' + task.slice(0, 100);
|
|
573
605
|
const deterministicSessionId = `async-${agentName}-${djb2Hash(sessionIdSource).toString(36)}`;
|
|
574
606
|
piArgs.push("--session-id", deterministicSessionId);
|
|
575
|
-
const { model: effectiveModel, provider: effectiveProvider } = resolveAgentModelProvider(agentName, agent, options?.parentModelId, options?.parentProvider, cwd);
|
|
576
607
|
if (effectiveModel) piArgs.push("--model", effectiveModel);
|
|
577
608
|
if (effectiveProvider) piArgs.push("--provider", effectiveProvider);
|
|
578
609
|
if (agent.tools?.length) piArgs.push("--tools", agent.tools.join(","));
|
|
579
610
|
|
|
580
611
|
if (agent.systemPrompt?.trim()) {
|
|
581
612
|
const promptPath = path.join(workerDir, "system-prompt.md");
|
|
582
|
-
|
|
613
|
+
const resolvedPrompt = substituteSettingsPlaceholders(
|
|
614
|
+
agent.systemPrompt,
|
|
615
|
+
(key) => getSetting(cwd, key as any),
|
|
616
|
+
Object.keys(SETTINGS_KEYS),
|
|
617
|
+
);
|
|
618
|
+
fs.writeFileSync(promptPath, resolvedPrompt, { mode: 0o600 });
|
|
583
619
|
piArgs.push("--append-system-prompt", promptPath);
|
|
584
620
|
}
|
|
585
621
|
|
|
@@ -664,14 +700,8 @@ export function registerAsyncAgents(
|
|
|
664
700
|
}
|
|
665
701
|
}
|
|
666
702
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
stdio: "ignore",
|
|
670
|
-
windowsHide: true,
|
|
671
|
-
env: spawnEnv,
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
// Track the job
|
|
703
|
+
// Register job BEFORE spawn — child can finish and write result file before
|
|
704
|
+
// jobs.set runs; watcher/poller would then treat it as orphan and delete it.
|
|
675
705
|
const job: AsyncJob = {
|
|
676
706
|
id,
|
|
677
707
|
agent: agentName,
|
|
@@ -692,6 +722,13 @@ export function registerAsyncAgents(
|
|
|
692
722
|
};
|
|
693
723
|
asyncState.jobs.set(id, job);
|
|
694
724
|
|
|
725
|
+
const proc = spawn(process.execPath, spawnArgs, {
|
|
726
|
+
cwd,
|
|
727
|
+
stdio: "ignore",
|
|
728
|
+
windowsHide: true,
|
|
729
|
+
env: spawnEnv,
|
|
730
|
+
});
|
|
731
|
+
|
|
695
732
|
// addReviewerPending already called above (before piArgs construction)
|
|
696
733
|
|
|
697
734
|
updateAsyncWidget();
|
|
@@ -891,7 +928,7 @@ export function registerAsyncAgents(
|
|
|
891
928
|
job.updatedAt = Date.now();
|
|
892
929
|
// Record killed reviewer as having 0 findings — prevents permanent commit block
|
|
893
930
|
if (job.agent.startsWith("code-reviewer-")) {
|
|
894
|
-
try { recordReviewerResult(jobCwd(job), job.agent, 0); } catch {
|
|
931
|
+
try { recordReviewerResult(jobCwd(job), job.agent, 0); } catch (e: any) { asyncLog(`recordReviewerResult failed for ${job.agent}: ${e?.message}`); }
|
|
895
932
|
}
|
|
896
933
|
// Check if this completes a group — deliver remaining siblings' results
|
|
897
934
|
if (job.groupId) {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { asStringArray, getSetting } from "./project-settings.js";
|
|
15
15
|
|
|
16
|
-
export interface
|
|
16
|
+
export interface InternalOpsProvider {
|
|
17
17
|
provider: string;
|
|
18
18
|
model: string;
|
|
19
19
|
}
|
|
@@ -61,9 +61,9 @@ export function supportsAsyncLlm(
|
|
|
61
61
|
* Sidecar model for must-async LLM work (dream, fireAndForget) when parent is acpx.
|
|
62
62
|
* Both provider and model must be set. Returns null if incomplete or sidecar is acpx-*.
|
|
63
63
|
*/
|
|
64
|
-
export function
|
|
65
|
-
const provider = getSetting(cwd, "
|
|
66
|
-
const model = getSetting(cwd, "
|
|
64
|
+
export function getInternalOpsProvider(cwd: string): InternalOpsProvider | null {
|
|
65
|
+
const provider = getSetting(cwd, "internal_operations_provider");
|
|
66
|
+
const model = getSetting(cwd, "internal_operations_model");
|
|
67
67
|
if (
|
|
68
68
|
typeof provider === "string" &&
|
|
69
69
|
provider.trim() &&
|
|
@@ -80,7 +80,7 @@ export function getAsyncLlmSidecar(cwd: string): AsyncLlmSidecar | null {
|
|
|
80
80
|
export type AsyncDispatchDecision =
|
|
81
81
|
| { action: "keep-async" }
|
|
82
82
|
| { action: "coerce-sync"; note: string }
|
|
83
|
-
| { action: "sidecar-async"; sidecar:
|
|
83
|
+
| { action: "sidecar-async"; sidecar: InternalOpsProvider; note: string }
|
|
84
84
|
| { action: "skip"; note: string };
|
|
85
85
|
|
|
86
86
|
/**
|
|
@@ -106,20 +106,20 @@ export function decideAsyncLlmDispatch(opts: {
|
|
|
106
106
|
if (asyncOk) {
|
|
107
107
|
return { action: "keep-async" };
|
|
108
108
|
}
|
|
109
|
-
const sidecar =
|
|
109
|
+
const sidecar = getInternalOpsProvider(opts.cwd);
|
|
110
110
|
if (opts.mustAsync) {
|
|
111
111
|
if (!sidecar) {
|
|
112
112
|
return {
|
|
113
113
|
action: "skip",
|
|
114
114
|
note:
|
|
115
115
|
"Skipped must-async LLM work: parent is acpx and " +
|
|
116
|
-
"
|
|
116
|
+
"internal_operations_provider/internal_operations_model are not set (or sidecar is acpx-*).",
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
119
|
return {
|
|
120
120
|
action: "sidecar-async",
|
|
121
121
|
sidecar,
|
|
122
|
-
note: `Using
|
|
122
|
+
note: `Using internal ops provider ${sidecar.provider}/${sidecar.model} (parent is acpx).`,
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
125
|
return {
|
|
@@ -544,7 +544,7 @@ export function registerCron(
|
|
|
544
544
|
console.debug(`[cron] ${dispatch.note} (task #${task.id})`);
|
|
545
545
|
try {
|
|
546
546
|
state.lastCtx?.ui?.notify?.(
|
|
547
|
-
`Cron #${task.id} skipped: set
|
|
547
|
+
`Cron #${task.id} skipped: set internal_operations_provider/internal_operations_model for acpx`,
|
|
548
548
|
"warning",
|
|
549
549
|
);
|
|
550
550
|
} catch { /* stale UI */ }
|
|
@@ -87,7 +87,7 @@ export function registerDreaming(
|
|
|
87
87
|
dreamingLog("warn", dreamDispatch.note || "dream skipped (no async LLM path)");
|
|
88
88
|
try {
|
|
89
89
|
lastCtx?.ui?.notify?.(
|
|
90
|
-
"Dream skipped: set
|
|
90
|
+
"Dream skipped: set internal_operations_provider and internal_operations_model for acpx sessions",
|
|
91
91
|
"warning",
|
|
92
92
|
);
|
|
93
93
|
} catch { /* stale UI */ }
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
runGit,
|
|
28
28
|
} from "./git-helpers.js";
|
|
29
29
|
import { spawnSync } from "node:child_process";
|
|
30
|
-
import { markNeedsReview,
|
|
30
|
+
import { markNeedsReview, isCommitAllowed, readReviewState, statePath, markTestsPassed, markTestsFailed } from "./pi-config-review-state.js";
|
|
31
31
|
import {
|
|
32
32
|
checkPythonPipBlock,
|
|
33
33
|
checkRemoteExecBlock,
|
|
@@ -82,8 +82,7 @@ async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd
|
|
|
82
82
|
const trailerSetting = getSetting(gitCwd, "commit_trailer");
|
|
83
83
|
if (typeof trailerSetting !== "string") return undefined;
|
|
84
84
|
|
|
85
|
-
const modelId = (ctx as any).model?.id;
|
|
86
|
-
if (!modelId) return undefined;
|
|
85
|
+
const modelId = (process.env.PI_MODEL || (ctx as any).model?.id || "unknown").replace(/[^a-zA-Z0-9._:@/ -]/g, "").trim() || "unknown";
|
|
87
86
|
|
|
88
87
|
const piIdentity = `PI (${modelId}) <noreply@pi.dev>`;
|
|
89
88
|
let trailerName: string;
|
|
@@ -362,7 +361,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
|
|
|
362
361
|
cachedTrailerSelection = null;
|
|
363
362
|
// Set comment signature env var for CLI tools to read
|
|
364
363
|
if (getSetting(ctx.cwd, "comment_signature")) {
|
|
365
|
-
const modelId = (ctx as any).model?.id || "unknown";
|
|
364
|
+
const modelId = (process.env.PI_MODEL || (ctx as any).model?.id || "unknown").replace(/[^a-zA-Z0-9._:@/ -]/g, "").trim() || "unknown";
|
|
366
365
|
process.env.PI_COMMENT_SIGNATURE = `Assisted-by: PI (${modelId})`;
|
|
367
366
|
} else {
|
|
368
367
|
delete process.env.PI_COMMENT_SIGNATURE;
|
|
@@ -518,7 +517,7 @@ export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): vo
|
|
|
518
517
|
// Use resolveEffectiveCwd to detect the worktree from cd commands (e.g., cd .worktrees/issue-622 && git commit)
|
|
519
518
|
if (process.env.PI_AGENT_NAME === "git-expert" && hasGitSub(command, "commit")) {
|
|
520
519
|
const commitCwd = resolveEffectiveCwd(command, ctx.cwd);
|
|
521
|
-
if (getSetting(ctx.cwd, "review_loop_enforcement") && !
|
|
520
|
+
if (getSetting(ctx.cwd, "review_loop_enforcement") && !isCommitAllowed(commitCwd)) {
|
|
522
521
|
const state = readReviewState(commitCwd);
|
|
523
522
|
const testInfo = state.status === "clean" && !state.tests_passed ? " Tests have not passed yet — run tests before committing." : "";
|
|
524
523
|
const reviewAdvice = state.status !== "clean" ? " Fix findings and re-run all reviewers until 0 comments." : "";
|