@ferris1225/pi-subagents 0.24.0 → 0.26.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ferris1225/pi-subagents",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "Focused sub-agent delegation for pi: explore / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/background.ts CHANGED
@@ -1,106 +1,112 @@
1
- /**
2
- * Bounded background task scheduler.
3
- *
4
- * Tasks get their own AbortSignal rather than inheriting the foreground agent
5
- * turn's signal. The owning extension cancels all work only on session teardown.
6
- *
7
- * Task exceptions are never swallowed: the per-task onError callback receives
8
- * them (unless the task was cancelled) so callers can surface the failure to
9
- * the user and the main agent instead of it vanishing into the queue.
10
- */
11
-
12
- export type BackgroundTask = (signal: AbortSignal) => Promise<void>;
13
-
14
- interface PendingTask {
15
- task: BackgroundTask;
16
- controller: AbortController;
17
- onCancelled?: () => void;
18
- /** Invoked when the task throws and was not cancelled (cancellation is not a
19
- * failure e.g. session shutdown races must never be reported as errors). */
20
- onError?: (error: unknown) => void;
21
- }
22
-
23
- export class BackgroundTaskQueue {
24
- private concurrency: number;
25
- private readonly pending: PendingTask[] = [];
26
- private readonly active = new Set<AbortController>();
27
- private stopped = false;
28
-
29
- constructor(concurrency: number) {
30
- this.concurrency = Math.max(1, concurrency);
31
- }
32
-
33
- /**
34
- * Update the concurrency limit (e.g. after a config change). Raising it
35
- * immediately starts more queued work; lowering it takes effect as running
36
- * tasks finish — already-running tasks are never interrupted.
37
- */
38
- setConcurrency(concurrency: number): void {
39
- this.concurrency = Math.max(1, concurrency);
40
- this.drain();
41
- }
42
-
43
- enqueue(task: BackgroundTask, onCancelled?: () => void, onError?: (error: unknown) => void): AbortController {
44
- const controller = new AbortController();
45
- if (this.stopped) {
46
- controller.abort();
47
- this.runCancelled(onCancelled);
48
- return controller;
49
- }
50
-
51
- this.pending.push({ task, controller, onCancelled, onError });
52
- this.drain();
53
- return controller;
54
- }
55
-
56
- /** Stop queued work and request cancellation for running work. */
57
- cancelAll(): void {
58
- if (this.stopped) return;
59
- this.stopped = true;
60
-
61
- for (const entry of this.pending.splice(0)) {
62
- entry.controller.abort();
63
- this.runCancelled(entry.onCancelled);
64
- }
65
- for (const controller of this.active) controller.abort();
66
- }
67
-
68
- /** Cancellation callbacks are user-supplied: a throw must never break the queue
69
- * (mirrors the try/catch around onError in drain). */
70
- private runCancelled(callback: (() => void) | undefined): void {
71
- if (!callback) return;
72
- try {
73
- callback();
74
- } catch {
75
- /* cancellation callbacks must never break the queue */
76
- }
77
- }
78
-
79
- private drain(): void {
80
- while (!this.stopped && this.active.size < this.concurrency) {
81
- const entry = this.pending.shift();
82
- if (!entry) return;
83
- if (entry.controller.signal.aborted) {
84
- this.runCancelled(entry.onCancelled);
85
- continue;
86
- }
87
-
88
- this.active.add(entry.controller);
89
- void entry.task(entry.controller.signal)
90
- .catch((error: unknown) => {
91
- // Cancellation is not a failure: aborted work (e.g. session
92
- // shutdown) must never be reported as an exception.
93
- if (entry.controller.signal.aborted) return;
94
- try {
95
- entry.onError?.(error);
96
- } catch {
97
- /* error reporting must never break the queue */
98
- }
99
- })
100
- .finally(() => {
101
- this.active.delete(entry.controller);
102
- this.drain();
103
- });
104
- }
105
- }
106
- }
1
+ /**
2
+ * Bounded background task scheduler.
3
+ *
4
+ * Tasks get their own AbortSignal rather than inheriting the foreground agent
5
+ * turn's signal. The owning extension cancels all work only on session teardown.
6
+ *
7
+ * Task exceptions are never swallowed: the per-task onError callback receives
8
+ * them (unless the task was cancelled) so callers can surface the failure to
9
+ * the user and the main agent instead of it vanishing into the queue.
10
+ */
11
+
12
+ export type BackgroundTask = (signal: AbortSignal) => Promise<void>;
13
+
14
+ interface PendingTask {
15
+ task: BackgroundTask;
16
+ controller: AbortController;
17
+ /** Called when a queued task is aborted before its body ever runs (drain skips
18
+ * an already-aborted entry; cancelAll aborts every pending entry), so the
19
+ * task body never produces a result. Callers that resolve waiters on a run id
20
+ * must register a synthetic result here (or via a stop path) otherwise a
21
+ * waiter resolves via a "removed before its result was recorded" note. Never
22
+ * called for a task whose body already started; that path owns its result. */
23
+ onCancelled?: () => void;
24
+ /** Invoked when the task throws and was not cancelled (cancellation is not a
25
+ * failure e.g. session shutdown races must never be reported as errors). */
26
+ onError?: (error: unknown) => void;
27
+ }
28
+
29
+ export class BackgroundTaskQueue {
30
+ private concurrency: number;
31
+ private readonly pending: PendingTask[] = [];
32
+ private readonly active = new Set<AbortController>();
33
+ private stopped = false;
34
+
35
+ constructor(concurrency: number) {
36
+ this.concurrency = Math.max(1, concurrency);
37
+ }
38
+
39
+ /**
40
+ * Update the concurrency limit (e.g. after a config change). Raising it
41
+ * immediately starts more queued work; lowering it takes effect as running
42
+ * tasks finish — already-running tasks are never interrupted.
43
+ */
44
+ setConcurrency(concurrency: number): void {
45
+ this.concurrency = Math.max(1, concurrency);
46
+ this.drain();
47
+ }
48
+
49
+ enqueue(task: BackgroundTask, onCancelled?: () => void, onError?: (error: unknown) => void): AbortController {
50
+ const controller = new AbortController();
51
+ if (this.stopped) {
52
+ controller.abort();
53
+ this.runCancelled(onCancelled);
54
+ return controller;
55
+ }
56
+
57
+ this.pending.push({ task, controller, onCancelled, onError });
58
+ this.drain();
59
+ return controller;
60
+ }
61
+
62
+ /** Stop queued work and request cancellation for running work. */
63
+ cancelAll(): void {
64
+ if (this.stopped) return;
65
+ this.stopped = true;
66
+
67
+ for (const entry of this.pending.splice(0)) {
68
+ entry.controller.abort();
69
+ this.runCancelled(entry.onCancelled);
70
+ }
71
+ for (const controller of this.active) controller.abort();
72
+ }
73
+
74
+ /** Cancellation callbacks are user-supplied: a throw must never break the queue
75
+ * (mirrors the try/catch around onError in drain). */
76
+ private runCancelled(callback: (() => void) | undefined): void {
77
+ if (!callback) return;
78
+ try {
79
+ callback();
80
+ } catch {
81
+ /* cancellation callbacks must never break the queue */
82
+ }
83
+ }
84
+
85
+ private drain(): void {
86
+ while (!this.stopped && this.active.size < this.concurrency) {
87
+ const entry = this.pending.shift();
88
+ if (!entry) return;
89
+ if (entry.controller.signal.aborted) {
90
+ this.runCancelled(entry.onCancelled);
91
+ continue;
92
+ }
93
+
94
+ this.active.add(entry.controller);
95
+ void entry.task(entry.controller.signal)
96
+ .catch((error: unknown) => {
97
+ // Cancellation is not a failure: aborted work (e.g. session
98
+ // shutdown) must never be reported as an exception.
99
+ if (entry.controller.signal.aborted) return;
100
+ try {
101
+ entry.onError?.(error);
102
+ } catch {
103
+ /* error reporting must never break the queue */
104
+ }
105
+ })
106
+ .finally(() => {
107
+ this.active.delete(entry.controller);
108
+ this.drain();
109
+ });
110
+ }
111
+ }
112
+ }
package/src/fixloop.ts CHANGED
@@ -1,76 +1,84 @@
1
- /**
2
- * Auto-fix loop: when a reviewer returns REVIEW_FAIL, the extension dispatches a
3
- * worker (briefed with the review's concrete findings) and then a reviewer
4
- * re-review, repeating up to maxFixRounds times before waking the main agent with
5
- * the full chain. The reviewer stays read-only and in its own context; the loop
6
- * is orchestrated by the extension layer, not by the reviewer itself, so the
7
- * independence guarantee (no self-confirmation bias) is preserved.
8
- *
9
- * The main agent is never woken mid-loop: the reviewer's FAIL result is intercepted
10
- * before delivery, the chain runs in the background, and only the final group
11
- * (initial review → worker fixes → re-reviews) is delivered at the end.
12
- */
13
-
14
- import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
15
- import type { SubagentsConfig } from "./config.ts";
16
-
17
- /**
18
- * Whether a completed result should trigger the auto-fix loop instead of being
19
- * delivered to the main agent. Only a REVIEW_FAIL verdict from a healthy
20
- * reviewer run counts; failed processes and passing reviews are delivered
21
- * normally. Loop-internal re-review results never reach this path (they are
22
- * awaited inside the loop, not delivered through the completion flow).
23
- */
24
- export function shouldTriggerFixLoop(result: SingleResult, config: SubagentsConfig): boolean {
25
- if (config.maxFixRounds <= 0) return false;
26
- if (result.agent !== "reviewer") return false;
27
- if (isFailedResult(result)) return false;
28
- return reviewVerdict(getResultOutput(result)) === "fail";
29
- }
30
-
31
- /**
32
- * Build the worker task brief for one fix round from a reviewer's findings.
33
- * The worker gets the full review text so it can address concrete file:line
34
- * issues, with instructions to fix only blockers and self-verify.
35
- */
36
- export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, maxRounds: number): string {
37
- const review = getResultOutput(reviewerResult);
38
- const remaining = maxRounds - round;
39
- return [
40
- `Auto-fix round ${round} of ${maxRounds} (triggered by a failed review).`,
41
- ``,
42
- `A reviewer ran in an isolated context and returned REQUEST_CHANGES. Its full report:`,
43
- `---`,
44
- review,
45
- `---`,
46
- ``,
47
- `Fix the concrete blockers the reviewer flagged. Do NOT refactor unrelated code.`,
48
- `Address every "Critical" item; address "Warnings" only if they are genuine.`,
49
- `After editing, run the project's format/build/tests when they exist and report`,
50
- `exactly what you changed (paths + short rationale) so a reviewer can verify.`,
51
- remaining > 0
52
- ? `A reviewer will re-review your changes automatically after you finish.`
53
- : `This is the last auto-fix round; the main agent will be woken with the full chain.`,
54
- ].join("\n");
55
- }
56
-
57
- /**
58
- * The re-review brief handed to the reviewer after a worker fix round. Includes
59
- * the prior review so the reviewer can verify the fixes without re-discovering
60
- * the original issues.
61
- */
62
- export function buildReReviewBrief(reviewerResult: SingleResult, round: number): string {
63
- const review = getResultOutput(reviewerResult);
64
- return [
65
- `Re-review after auto-fix round ${round}.`,
66
- ``,
67
- `The previous review (REQUEST_CHANGES) found these issues:`,
68
- `---`,
69
- review,
70
- `---`,
71
- ``,
72
- `Verify the worker's fixes address each blocker. Run \`git diff\` to see what changed.`,
73
- `Classify honestly: APPROVE if blockers are resolved, REQUEST_CHANGES if not.`,
74
- `End with your machine-readable verdict line as usual (VERDICT: REVIEW_PASS / REVIEW_FAIL).`,
75
- ].join("\n");
76
- }
1
+ /**
2
+ * Auto-fix loop: when a reviewer returns REVIEW_FAIL, the extension dispatches a
3
+ * worker (briefed with the review's concrete findings) and then a reviewer
4
+ * re-review, repeating up to maxFixRounds times before waking the main agent with
5
+ * the full chain. The reviewer stays read-only and in its own context; the loop
6
+ * is orchestrated by the extension layer, not by the reviewer itself, so the
7
+ * independence guarantee (no self-confirmation bias) is preserved.
8
+ *
9
+ * The main agent is never woken mid-loop: the reviewer's FAIL result is intercepted
10
+ * before delivery, the chain runs in the background, and only the final group
11
+ * (initial review → worker fixes → re-reviews) is delivered at the end.
12
+ */
13
+
14
+ import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
15
+ import type { SubagentsConfig } from "./config.ts";
16
+
17
+ /**
18
+ * Whether a completed result should trigger the auto-fix loop instead of being
19
+ * delivered to the main agent. Only a REVIEW_FAIL verdict from a healthy
20
+ * reviewer run counts; failed processes and passing reviews are delivered
21
+ * normally. Loop-internal re-review results never reach this path (they are
22
+ * awaited inside the loop, not delivered through the completion flow).
23
+ */
24
+ export function shouldTriggerFixLoop(result: SingleResult, config: SubagentsConfig): boolean {
25
+ if (config.maxFixRounds <= 0) return false;
26
+ if (result.agent !== "reviewer") return false;
27
+ if (isFailedResult(result)) return false;
28
+ // A dispatch crash (spawn infra, delivery API, ...) is never a real review
29
+ // verdict: its output is an error message plus whatever partial text the
30
+ // child happened to emit, which could end in a stray `VERDICT: REVIEW_FAIL`.
31
+ // Guard explicitly in addition to isFailedResult so the intent is clear and
32
+ // a future change to isFailedResult can never let a crashed reviewer start a
33
+ // phantom auto-fix chain (and re-add a run controller id that was deleted in
34
+ // the catch path).
35
+ if (result.dispatchFailed) return false;
36
+ return reviewVerdict(getResultOutput(result)) === "fail";
37
+ }
38
+
39
+ /**
40
+ * Build the worker task brief for one fix round from a reviewer's findings.
41
+ * The worker gets the full review text so it can address concrete file:line
42
+ * issues, with instructions to fix only blockers and self-verify.
43
+ */
44
+ export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, maxRounds: number): string {
45
+ const review = getResultOutput(reviewerResult);
46
+ const remaining = maxRounds - round;
47
+ return [
48
+ `Auto-fix round ${round} of ${maxRounds} (triggered by a failed review).`,
49
+ ``,
50
+ `A reviewer ran in an isolated context and returned REQUEST_CHANGES. Its full report:`,
51
+ `---`,
52
+ review,
53
+ `---`,
54
+ ``,
55
+ `Fix the concrete blockers the reviewer flagged. Do NOT refactor unrelated code.`,
56
+ `Address every "Critical" item; address "Warnings" only if they are genuine.`,
57
+ `After editing, run the project's format/build/tests when they exist and report`,
58
+ `exactly what you changed (paths + short rationale) so a reviewer can verify.`,
59
+ remaining > 0
60
+ ? `A reviewer will re-review your changes automatically after you finish.`
61
+ : `This is the last auto-fix round; the main agent will be woken with the full chain.`,
62
+ ].join("\n");
63
+ }
64
+
65
+ /**
66
+ * The re-review brief handed to the reviewer after a worker fix round. Includes
67
+ * the prior review so the reviewer can verify the fixes without re-discovering
68
+ * the original issues.
69
+ */
70
+ export function buildReReviewBrief(reviewerResult: SingleResult, round: number): string {
71
+ const review = getResultOutput(reviewerResult);
72
+ return [
73
+ `Re-review after auto-fix round ${round}.`,
74
+ ``,
75
+ `The previous review (REQUEST_CHANGES) found these issues:`,
76
+ `---`,
77
+ review,
78
+ `---`,
79
+ ``,
80
+ `Verify the worker's fixes address each blocker. Run \`git diff\` to see what changed.`,
81
+ `Classify honestly: APPROVE if blockers are resolved, REQUEST_CHANGES if not.`,
82
+ `End with your machine-readable verdict line as usual (VERDICT: REVIEW_PASS / REVIEW_FAIL).`,
83
+ ].join("\n");
84
+ }