pi-orchestrator-config 4.24.1 → 4.25.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/CHANGELOG.md +36 -0
- package/extensions/orchestrator/index.ts +2 -0
- package/extensions/orchestrator/subagent-tool.ts +85 -46
- package/extensions/orchestrator/tmp-cleanup.ts +428 -0
- package/extensions/orchestrator/utils.ts +23 -3
- package/extensions/pidiff/pidiff.ts +60 -35
- package/extensions/pidiff/spawn-lock.ts +34 -0
- package/extensions/pitasks/index.ts +31 -16
- package/extensions/pitasks/reminders.ts +70 -0
- package/extensions/reload-flag/index.ts +99 -29
- package/extensions/shared/daemon-manager.ts +44 -23
- package/extensions/shared/system-note.ts +44 -0
- package/extensions/shared/task-store-bridge.ts +43 -0
- package/package.json +1 -1
- package/prompts/review-handler.md +12 -0
- package/rules/00-orchestrator-core.md +2 -1
- package/rules/40-critical-rules.md +1 -1
- package/rules/60-task-tracking.md +27 -6
- package/scripts/pidiff-server.ts +26 -7
- package/settings-keys.json +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.25.0] - 2026-11-06
|
|
4
|
+
|
|
5
|
+
- `.pi/tmp` is cleaned at session start: above `project_tmp_max_size` (default `2G`), entries older than
|
|
6
|
+
`project_tmp_retention_days` (default 7 days) are deleted oldest-first until the tree fits, and `0`
|
|
7
|
+
disables it entirely. The sweep lock lives in `.pi/run` rather than in the directory being swept, a run
|
|
8
|
+
that cannot take the lock deletes nothing, an entry replaced mid-scan is skipped, and oneshot sessions
|
|
9
|
+
skip the whole thing. A bad size value deletes nothing rather than falling back to a deleting default.
|
|
10
|
+
- Daemon control files moved to `.pi/run` — `pidiff.pid`, `pidiff.port`, and the `reload.flag` /
|
|
11
|
+
`reload.done` / `reload.pending` handshake. Readers fall back to `.pi/tmp` and `reload.done` is mirrored
|
|
12
|
+
back, so a daemon started before the upgrade is still found instead of a second one spawning on its
|
|
13
|
+
port. **The migration release needs exactly one manual `/reload`**: a session whose watcher was loaded
|
|
14
|
+
before the move cannot observe the new flag path. The old temp-dir reload files stay protected until no
|
|
15
|
+
pre-upgrade daemon remains.
|
|
16
|
+
- The pending-task reminder keys on how long a task has actually been pending rather than on the first
|
|
17
|
+
60-second tick, so it no longer fires about a minute after creation. It measures the oldest active task,
|
|
18
|
+
treats an undated task as old so a fresh sibling cannot mask it, and covers `agentBusy` in tests.
|
|
19
|
+
- `reviews fetch` approves on a clean findings catalog at the PR head instead of requiring a `completed`
|
|
20
|
+
Qodo session, so a legitimately skipped auto-run can approve. A run that is still in progress never does.
|
|
21
|
+
`reason` reports `no_findings`, `open_findings`, `not_at_head`, or `all_resolved` instead of always
|
|
22
|
+
claiming `all_resolved`, and the review-loop exit names which reviewer actually approved — a CodeRabbit
|
|
23
|
+
approval is no longer reported as Qodo's.
|
|
24
|
+
|
|
25
|
+
## [4.24.2] - 2026-11-04
|
|
26
|
+
|
|
27
|
+
- Async work is tracked even when an agent starts it: the task trigger now covers work set in
|
|
28
|
+
motion by an async result, and `taskId: "-1"` is restricted to a single read-only check in
|
|
29
|
+
every place that taught the old habit.
|
|
30
|
+
- Task-id validation actually runs. `pitasks` exports the store as a live binding that the
|
|
31
|
+
subagent tool used to snapshot before it was assigned, so an unknown id was accepted; the
|
|
32
|
+
store is now published on a symbol bridge and read at access time. Verified live: unknown
|
|
33
|
+
ids rejected, a real id and the read-only sentinel accepted.
|
|
34
|
+
- Machine-generated notices declare their provenance: follow-ups are prefixed
|
|
35
|
+
"[system message — not written by the user]", so a guard rail cannot be read as user input.
|
|
36
|
+
- Board updates are success-gated — a failed agent keeps its task open — and reconciling the
|
|
37
|
+
board before new dispatch is mandatory. Status reports derive from `TaskList`.
|
|
38
|
+
|
|
3
39
|
## [4.24.1] - 2026-11-03
|
|
4
40
|
|
|
5
41
|
- Dreaming no longer stores re-derivable changelog: `[done]` takes non-derivable outcomes only (conclusions, measurements, root causes) and reorganizing prunes PR/version/ticket
|
|
@@ -22,6 +22,7 @@ import { registerEnforcement } from "./enforcement.js";
|
|
|
22
22
|
import { registerProjectSettings } from "./project-settings.js";
|
|
23
23
|
import { registerRules } from "./rules.js";
|
|
24
24
|
import { registerSessionValidation } from "./session-validation.js";
|
|
25
|
+
import { registerTmpCleanup } from "./tmp-cleanup.js";
|
|
25
26
|
import { registerStatusLine } from "./status-line.js";
|
|
26
27
|
import { registerSubagentTool } from "./subagent-tool.js";
|
|
27
28
|
import { registerGithubAutocomplete } from "./github-autocomplete.js";
|
|
@@ -144,6 +145,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
144
145
|
registerPreferenceExtractor(pi);
|
|
145
146
|
registerMemoryTools(pi);
|
|
146
147
|
registerReviewLearning(pi);
|
|
148
|
+
registerTmpCleanup(pi);
|
|
147
149
|
registerSessionSearch(pi);
|
|
148
150
|
registerSettingsTui(pi);
|
|
149
151
|
registerMarkdownTransformer(pi);
|
|
@@ -16,6 +16,8 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
|
16
16
|
import type { Message } from "@earendil-works/pi-ai";
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import { createLogger } from "../shared/logger.js";
|
|
19
|
+
import { sendSystemNote } from "../shared/system-note.js";
|
|
20
|
+
import { publishedTaskStore } from "../shared/task-store-bridge.js";
|
|
19
21
|
import { createBridgedUi } from "../shared/ui-bridge.js";
|
|
20
22
|
import {
|
|
21
23
|
getMarkdownTheme,
|
|
@@ -30,12 +32,44 @@ import {
|
|
|
30
32
|
import { Type } from "typebox";
|
|
31
33
|
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
|
|
32
34
|
|
|
33
|
-
// Task store access via
|
|
34
|
-
let
|
|
35
|
+
// Task store access via the pitasks module.
|
|
36
|
+
// `taskStore` is `export let` — a live binding the pitasks extension assigns when it
|
|
37
|
+
// initializes. Copying the value at import time captured `undefined` and disabled
|
|
38
|
+
// task-id validation for the whole session (issue #1016), so the binding is read fresh.
|
|
39
|
+
let _pitasksModule: any = null;
|
|
40
|
+
let _pitasksLoad: Promise<any> | null = null;
|
|
41
|
+
async function pitasksModule(): Promise<any> {
|
|
42
|
+
if (_pitasksModule) return _pitasksModule;
|
|
43
|
+
if (!_pitasksLoad) {
|
|
44
|
+
_pitasksLoad = (async () => {
|
|
45
|
+
try {
|
|
46
|
+
_pitasksModule = await import("../pitasks/index.js");
|
|
47
|
+
return _pitasksModule;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
log.warn(`[subagent] pitasks module unavailable: ${String((err as Error)?.message ?? err).slice(0, 120)}`);
|
|
50
|
+
_pitasksLoad = null; // retry on the next call
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
}
|
|
55
|
+
return _pitasksLoad;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The live task store, or null while pitasks has not initialized it yet.
|
|
59
|
+
* Read from the global symbol registry: this tool's module instance cannot see the
|
|
60
|
+
* pitasks extension's `export let taskStore` binding (it snapshots `undefined`), which
|
|
61
|
+
* is why task-id validation silently failed open (issue #1016). */
|
|
35
62
|
async function getTaskStore(): Promise<any> {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
63
|
+
const published = publishedTaskStore();
|
|
64
|
+
if (published) return published;
|
|
65
|
+
// Fall back to the module binding for in-process loads that share this instance.
|
|
66
|
+
const mod = await pitasksModule();
|
|
67
|
+
const store = mod?.taskStore ?? null;
|
|
68
|
+
if (!store) {
|
|
69
|
+
// Validation is about to be skipped: that is a degraded state, not routine flow.
|
|
70
|
+
log.warn("task_store_unavailable", { note: "pitasks store not initialized — taskId validation skipped" });
|
|
71
|
+
}
|
|
72
|
+
return store;
|
|
39
73
|
}
|
|
40
74
|
import {
|
|
41
75
|
decideAsyncLlmDispatch,
|
|
@@ -153,7 +187,7 @@ const SubagentParams = Type.Object({
|
|
|
153
187
|
Type.String({ description: "Display name for async agents in status line and notifications (e.g., 'Dream', 'Code Review'). Defaults to agent name." }),
|
|
154
188
|
),
|
|
155
189
|
taskId: Type.Optional(
|
|
156
|
-
Type.String({ description: "Task ID to auto-complete when this async agent finishes. Required for async agents — pass \"-1\"
|
|
190
|
+
Type.String({ description: "Task ID to auto-complete when this async agent finishes. Required for async agents — pass \"-1\" only for a single read-only check; an agent that edits, pushes, or drives a PR needs a real task id." }),
|
|
157
191
|
),
|
|
158
192
|
asyncKill: Type.Optional(
|
|
159
193
|
Type.String({ description: "Kill async agent(s) by name, id prefix, or 'all'. Returns which agents were killed." }),
|
|
@@ -577,22 +611,27 @@ export async function runSingleAgent(
|
|
|
577
611
|
}
|
|
578
612
|
}
|
|
579
613
|
|
|
580
|
-
/**
|
|
581
|
-
function
|
|
614
|
+
/** The message for a task id the store does not know; null when it is valid or exempt. */
|
|
615
|
+
export function taskIdError(taskId: string, lookup: (id: string) => boolean): string | null {
|
|
616
|
+
if (taskId === "-1") return null;
|
|
617
|
+
return lookup(taskId)
|
|
618
|
+
? null
|
|
619
|
+
: `Task #${taskId} not found. Verify the task ID exists (use TaskList to check), or pass taskId: "-1" for a read-only check (editing or PR work needs a real task id).`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Validate that a taskId references an existing task. Awaits the store load, so the
|
|
623
|
+
* first async call of a session validates too instead of failing open (issue #1016). */
|
|
624
|
+
async function validateTaskId(taskId: string, _cwd: string, _sessionId?: string): Promise<string | null> {
|
|
582
625
|
if (taskId === "-1") return null;
|
|
626
|
+
const store = await getTaskStore();
|
|
627
|
+
if (!store) return null; // pitasks genuinely unavailable — warned in getTaskStore
|
|
583
628
|
try {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
// Store not loaded yet (async init) — fail open, skip validation
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
if (store.get(taskId)) return null;
|
|
594
|
-
} catch { return null; }
|
|
595
|
-
return `Task #${taskId} not found. Verify the task ID exists (use TaskList to check), or pass taskId: "-1" if not linked to a task.`;
|
|
629
|
+
return taskIdError(taskId, (id) => Boolean(store.get(id)));
|
|
630
|
+
} catch (err) {
|
|
631
|
+
// Same fail-open as before, but a broken store must not look like a valid id.
|
|
632
|
+
log.warn("task_id_validation_failed", { reason: String((err as Error)?.message ?? err).slice(0, 120) });
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
596
635
|
}
|
|
597
636
|
|
|
598
637
|
// ── Registration ─────────────────────────────────────────────────────────
|
|
@@ -629,7 +668,7 @@ export function registerSubagentTool(
|
|
|
629
668
|
};
|
|
630
669
|
}
|
|
631
670
|
|
|
632
|
-
function executeAsync(
|
|
671
|
+
async function executeAsync(
|
|
633
672
|
params: any,
|
|
634
673
|
agents: AgentConfig[],
|
|
635
674
|
mkd: (mode: "single" | "parallel" | "chain") => (results: SingleResult[]) => SubagentDetails,
|
|
@@ -659,13 +698,13 @@ export function registerSubagentTool(
|
|
|
659
698
|
}
|
|
660
699
|
const noTaskId = params.tasks.filter((t: any) => (t as any).taskId == null);
|
|
661
700
|
if (noTaskId.length > 0) {
|
|
662
|
-
const errorMsg = `Missing required parameter: taskId. Every async agent MUST have a taskId.\n\nMissing taskId for: ${noTaskId.map((t: any) => t.agent).join(", ")}\n\nIf an agent is working on a task: pass the task ID (e.g., taskId: "5")\nIf an agent is
|
|
701
|
+
const errorMsg = `Missing required parameter: taskId. Every async agent MUST have a taskId.\n\nMissing taskId for: ${noTaskId.map((t: any) => t.agent).join(", ")}\n\nIf an agent is working on a task: pass the task ID (e.g., taskId: "5")\nIf an agent is only doing a single read-only check: pass taskId: "-1" (editing or PR work needs a real task id)`;
|
|
663
702
|
// Force a follow-up turn so the AI MUST deal with this error
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
703
|
+
await sendSystemNote(
|
|
704
|
+
pi,
|
|
705
|
+
`🚨 CRITICAL: Async subagent call was REJECTED — missing taskId for: ${noTaskId.map((t: any) => t.agent).join(", ")}. NO agents were spawned. You MUST retry with taskId for each agent. Use TaskList to find task IDs; "-1" is only for a single read-only check.`,
|
|
706
|
+
{ customType: "subagent-taskid-error", triggerTurn: true, deliverAs: "followUp" },
|
|
707
|
+
);
|
|
669
708
|
return {
|
|
670
709
|
content: [{ type: "text" as const, text: errorMsg }],
|
|
671
710
|
details: mkd("single")([]),
|
|
@@ -674,13 +713,13 @@ export function registerSubagentTool(
|
|
|
674
713
|
}
|
|
675
714
|
for (const t of params.tasks) {
|
|
676
715
|
const tid = (t as any).taskId as string;
|
|
677
|
-
const taskErr = validateTaskId(tid, ctx.cwd, ctx.sessionManager?.getSessionId?.());
|
|
716
|
+
const taskErr = await validateTaskId(tid, ctx.cwd, ctx.sessionManager?.getSessionId?.());
|
|
678
717
|
if (taskErr) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
718
|
+
await sendSystemNote(
|
|
719
|
+
pi,
|
|
720
|
+
`🚨 CRITICAL: ${taskErr} (agent: ${t.agent}). NO agents were spawned. Fix the taskId and retry.`,
|
|
721
|
+
{ customType: "subagent-taskid-error", triggerTurn: true, deliverAs: "followUp" },
|
|
722
|
+
);
|
|
684
723
|
return {
|
|
685
724
|
content: [{ type: "text" as const, text: `${taskErr} (agent: ${t.agent})` }],
|
|
686
725
|
details: mkd("single")([]),
|
|
@@ -747,13 +786,13 @@ export function registerSubagentTool(
|
|
|
747
786
|
};
|
|
748
787
|
}
|
|
749
788
|
if (params.taskId == null) {
|
|
750
|
-
const errorMsg = `Missing required parameter: taskId. Every async agent MUST have a taskId.\n\nIf this agent is working on a task: pass the task ID (e.g., taskId: "5")\nIf this agent is
|
|
789
|
+
const errorMsg = `Missing required parameter: taskId. Every async agent MUST have a taskId.\n\nIf this agent is working on a task: pass the task ID (e.g., taskId: "5")\nIf this agent is only doing a single read-only check: pass taskId: "-1" (editing or PR work needs a real task id)\n\nExample: subagent(agent="${params.agent}", task="...", async=true, name="${params.name}", taskId="-1")`;
|
|
751
790
|
// Force a follow-up turn so the AI MUST deal with this error
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
791
|
+
await sendSystemNote(
|
|
792
|
+
pi,
|
|
793
|
+
`🚨 CRITICAL: Async subagent call for "${params.agent}" was REJECTED — missing taskId. The agent was NOT spawned. You MUST retry the subagent call with taskId. Use TaskList to find the task ID; "-1" is only for a single read-only check. Do NOT proceed without fixing this.`,
|
|
794
|
+
{ customType: "subagent-taskid-error", triggerTurn: true, deliverAs: "followUp" },
|
|
795
|
+
);
|
|
757
796
|
return {
|
|
758
797
|
content: [{ type: "text" as const, text: errorMsg }],
|
|
759
798
|
details: mkd("single")([]),
|
|
@@ -761,13 +800,13 @@ export function registerSubagentTool(
|
|
|
761
800
|
};
|
|
762
801
|
}
|
|
763
802
|
{
|
|
764
|
-
const taskErr = validateTaskId(params.taskId, ctx.cwd, ctx.sessionManager?.getSessionId?.());
|
|
803
|
+
const taskErr = await validateTaskId(params.taskId, ctx.cwd, ctx.sessionManager?.getSessionId?.());
|
|
765
804
|
if (taskErr) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
805
|
+
await sendSystemNote(
|
|
806
|
+
pi,
|
|
807
|
+
`🚨 CRITICAL: ${taskErr} The agent was NOT spawned. Fix the taskId and retry.`,
|
|
808
|
+
{ customType: "subagent-taskid-error", triggerTurn: true, deliverAs: "followUp" },
|
|
809
|
+
);
|
|
771
810
|
return {
|
|
772
811
|
content: [{ type: "text" as const, text: taskErr }],
|
|
773
812
|
details: mkd("single")([]),
|
|
@@ -1689,7 +1728,7 @@ export function registerSubagentTool(
|
|
|
1689
1728
|
|
|
1690
1729
|
// Async mode — spawn in background and return immediately
|
|
1691
1730
|
if (params.async === true) {
|
|
1692
|
-
const result = executeAsync(params, agents, mkd, parentModelId, parentProvider, ctx, explicit);
|
|
1731
|
+
const result = await executeAsync(params, agents, mkd, parentModelId, parentProvider, ctx, explicit);
|
|
1693
1732
|
if (capabilityNote && result.content?.[0]?.type === "text") {
|
|
1694
1733
|
result.content[0].text = `${capabilityNote}\n\n${result.content[0].text}`;
|
|
1695
1734
|
}
|