pi-orchestrator-config 4.3.11 → 4.3.12

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.
@@ -13,8 +13,7 @@ import { fileURLToPath } from "node:url";
13
13
  const require = createRequire(import.meta.url);
14
14
 
15
15
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
- import type { AgentConfig } from "./agents.js";
17
- import { discoverAgents } from "./agents.js";
16
+ import type { AgentConfig, AgentDiscoveryResult } from "./agents.js";
18
17
  import { resolveAgentModelProvider } from "./resolve-agent-model.js";
19
18
  import { getPiInvocation, getProjectTmpDir, parseProcStartTime, djb2Hash } from "./utils.js";
20
19
  import { addReviewerPending, recordReviewerResult, countFindings, readReviewState, markTestsPassed, markTestsFailed } from "./pi-config-review-state.js";
@@ -22,7 +21,8 @@ import {
22
21
  getMainBranch,
23
22
  } from "./git-helpers.js";
24
23
  import { waitForResultFiles } from "./async-wait.js";
25
- import { openAsyncStatusOverlay } from "./async-status-ui.js";
24
+ import { formatAsyncResultOutput, reviewerOutputArchivePath } from "./async-result-format.js";
25
+ import { cleanupReviewerOutputArchives } from "./reviewer-output-archive.js";
26
26
  const log = createLogger("async_agents");
27
27
 
28
28
  export { autoCompleteTask, autoMarkInProgress } from "./task-lifecycle.js";
@@ -117,6 +117,10 @@ export function formatDuration(ms: number): string {
117
117
  export function registerAsyncAgents(
118
118
  pi: ExtensionAPI,
119
119
  terminalNotify: (title: string, body: string) => void,
120
+ runtime: {
121
+ spawnProcess?: typeof spawn;
122
+ discoverAgents?: (cwd: string, scope: "both") => Pick<AgentDiscoveryResult, "agents">;
123
+ } = {},
120
124
  ): {
121
125
  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 };
122
126
  killAsyncAgent: (target: string) => { killed: string[]; errors: string[] };
@@ -125,6 +129,28 @@ export function registerAsyncAgents(
125
129
  let PROJECT_TMP_DIR = path.join(process.cwd(), ".pi", "tmp"); // Computed only; created on session_start
126
130
  let ASYNC_RESULTS_DIR = ""; // Set on session_start to project-scoped dir
127
131
 
132
+ function resultOutputPath(job: Pick<AsyncJob, "agent" | "id" | "workerDir">): string {
133
+ const workerOutputPath = path.join(job.workerDir, "output.log");
134
+ const outputPath = job.agent.startsWith("code-reviewer-")
135
+ ? reviewerOutputArchivePath(PROJECT_TMP_DIR, job.id)
136
+ : workerOutputPath;
137
+ log.debug("async_result_output_path", { agent: job.agent, archived: outputPath !== workerOutputPath });
138
+ return outputPath;
139
+ }
140
+
141
+ function preserveReviewerOutput(job: Pick<AsyncJob, "agent" | "id">, output: string): void {
142
+ if (!job.agent.startsWith("code-reviewer-")) return;
143
+ const outputPath = reviewerOutputArchivePath(PROJECT_TMP_DIR, job.id);
144
+ try {
145
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true, mode: 0o700 });
146
+ fs.writeFileSync(outputPath, output, { mode: 0o600 });
147
+ fs.chmodSync(outputPath, 0o600);
148
+ cleanupReviewerOutputArchives(path.dirname(outputPath));
149
+ log.info("preserved_reviewer_output", { agent: job.agent, bytes: Buffer.byteLength(output, "utf8") });
150
+ } catch (e: any) {
151
+ log.error(`preserve reviewer output failed for ${job.id}: ${e?.message}`);
152
+ }
153
+ }
128
154
 
129
155
  const asyncState: AsyncState = {
130
156
  jobs: new Map(),
@@ -392,9 +418,14 @@ export function registerAsyncAgents(
392
418
  log.error(`reconcile: auto-complete failed for task #${safeTaskId}: ${e?.message}`);
393
419
  }
394
420
  }
395
- // Enforce same output-length budget as processResultFile
396
- const maxOutput = 3000 - autoCompleteError.length;
397
- const output = (job.output || "").slice(0, Math.max(maxOutput, 500));
421
+ // Enforce same output-length budget as processResultFile.
422
+ const maxOutput = Math.max(3000 - autoCompleteError.length, 500);
423
+ const output = formatAsyncResultOutput(
424
+ job.agent,
425
+ job.output || "",
426
+ resultOutputPath(job),
427
+ maxOutput,
428
+ );
398
429
  // Emit lifecycle events
399
430
  if (job.status === "complete") {
400
431
  pi.events.emit("subagents:completed", { id: job.id, result: job.output || "" });
@@ -481,6 +512,7 @@ export function registerAsyncAgents(
481
512
  try {
482
513
  const data = JSON.parse(fs.readFileSync(rp, "utf-8"));
483
514
  j.output = data.output;
515
+ preserveReviewerOutput(j, typeof data.output === "string" ? data.output : JSON.stringify(data.output ?? ""));
484
516
  j.exitCode = data.exitCode;
485
517
  j.durationMs = data.durationMs;
486
518
  if (data.success !== undefined) j.status = data.success ? "complete" : "failed";
@@ -549,7 +581,11 @@ export function registerAsyncAgents(
549
581
  if (j.fireAndForget) { j.delivered = true; continue; }
550
582
  const resultStatus = j.status === "complete" ? "✅ completed" : "❌ failed";
551
583
  const displayName = j.name || j.agent;
552
- const output = (j.output || "").slice(0, 3000);
584
+ const output = formatAsyncResultOutput(
585
+ j.agent,
586
+ j.output || "",
587
+ resultOutputPath(j),
588
+ );
553
589
  let autoCompleteError = "";
554
590
  // Auto-complete linked task directly in the store file (no AI involvement)
555
591
  if (j.taskId && j.taskId !== "-1" && j.status === "complete" && j.cwd) {
@@ -639,6 +675,7 @@ export function registerAsyncAgents(
639
675
  job.exitCode = data.exitCode;
640
676
  job.durationMs = data.durationMs;
641
677
  job.updatedAt = Date.now();
678
+ preserveReviewerOutput(job, typeof data.output === "string" ? data.output : JSON.stringify(data.output ?? ""));
642
679
 
643
680
  // Notify terminal per-agent (lightweight, non-conversational)
644
681
  const displayName = job.name || data.agent;
@@ -703,7 +740,11 @@ export function registerAsyncAgents(
703
740
  try {
704
741
  const statusPath = path.join(job.workerDir, "status.json");
705
742
  const existing = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
706
- existing.output = (data.output || "").slice(0, 3000);
743
+ existing.output = formatAsyncResultOutput(
744
+ job.agent,
745
+ data.output || "",
746
+ resultOutputPath(job),
747
+ );
707
748
  existing.state = job.status;
708
749
  existing.exitCode = job.exitCode;
709
750
  existing.durationMs = job.durationMs;
@@ -748,8 +789,13 @@ export function registerAsyncAgents(
748
789
  log.error(`auto-complete failed for task #${job.taskId}: ${e?.message}`);
749
790
  }
750
791
  }
751
- const maxOutput = 3000 - autoCompleteError.length;
752
- const output = (data.output || "").slice(0, Math.max(maxOutput, 500));
792
+ const maxOutput = Math.max(3000 - autoCompleteError.length, 500);
793
+ const output = formatAsyncResultOutput(
794
+ job.agent,
795
+ data.output || "",
796
+ resultOutputPath(job),
797
+ maxOutput,
798
+ );
753
799
  const pContent = `## Async Agent Result: ${displayName} ${resultStatus}\n\nTask: ${data.task}\nDuration: ${formatDuration(data.durationMs)}\n\n${output}${autoCompleteError}`;
754
800
  if (wasAlreadyDelivered(job.id)) {
755
801
  // Already delivered in previous lifecycle — skip send AND onComplete
@@ -815,7 +861,7 @@ export function registerAsyncAgents(
815
861
  setTimeout(() => processResultFile(resultPath), 100);
816
862
  });
817
863
  if (asyncState.watcher.unref) asyncState.watcher.unref();
818
- } catch (e: any) { console.debug("[async-agents] watcher setup failed:", e?.message || e); }
864
+ } catch (e: any) { log.error(`watcher setup failed: ${e?.message || e}`); }
819
865
  }
820
866
 
821
867
  function spawnAsyncAgent(
@@ -844,7 +890,7 @@ export function registerAsyncAgents(
844
890
  } catch { /* retry */ }
845
891
  }
846
892
  if (!parentStartTime) {
847
- console.debug("[async-agents] WARNING: could not read /proc/self/stat starttime after 3 attempts");
893
+ log.warn("async-spawn: could not read /proc/self/stat starttime after 3 attempts");
848
894
  }
849
895
  const { model: effectiveModel, provider: effectiveProvider } = resolveAgentModelProvider(agentName, agent, options?.parentModelId, options?.parentProvider, cwd, options?.explicit);
850
896
  fs.writeFileSync(path.join(workerDir, "session.json"), JSON.stringify({
@@ -912,6 +958,9 @@ export function registerAsyncAgents(
912
958
  model: effectiveModel,
913
959
  contextWindow: asyncState.lastCtx?.model?.contextWindow || 0,
914
960
  resultPath,
961
+ reviewerOutputPath: agentName.startsWith("code-reviewer-")
962
+ ? reviewerOutputArchivePath(PROJECT_TMP_DIR, id)
963
+ : undefined,
915
964
  workerDir,
916
965
  sessionId: `${process.pid}:${process.cwd()}`,
917
966
  piCommand: inv.command,
@@ -982,7 +1031,7 @@ export function registerAsyncAgents(
982
1031
 
983
1032
  // Fail fast with a self-explanatory error instead of spawning a child that
984
1033
  // cannot load TypeScript (bare node dies on ".js"→".ts" specifiers).
985
- if (!jitiCliPath) {
1034
+ if (!jitiCliPath && !runtime.spawnProcess) {
986
1035
  log.error(`async-spawn: ${id} — no jiti-cli.mjs found. Probes:\n ${(jitiProbes.slice(-14)).join("\n ")}`);
987
1036
  // Clean up the artifacts we already created — nothing will consume them.
988
1037
  try { fs.rmSync(configPath, { force: true }); } catch {}
@@ -1056,7 +1105,7 @@ export function registerAsyncAgents(
1056
1105
  stderrLog.on("error", () => {}); // never let a log write failure crash the session
1057
1106
  log.debug(`async-spawn: ${job.id} stderr → ${stderrLogPath}`);
1058
1107
 
1059
- const proc = spawn(process.execPath, spawnArgs, {
1108
+ const proc = (runtime.spawnProcess ?? spawn)(process.execPath, spawnArgs, {
1060
1109
  cwd,
1061
1110
  stdio: ["ignore", "ignore", "pipe"],
1062
1111
  windowsHide: true,
@@ -1153,6 +1202,8 @@ export function registerAsyncAgents(
1153
1202
  // Export as env var so prompts/CLI commands can reference it
1154
1203
  process.env.PROJECT_TMP_DIR = PROJECT_TMP_DIR;
1155
1204
 
1205
+ cleanupReviewerOutputArchives(path.join(PROJECT_TMP_DIR, "reviewer-results"));
1206
+
1156
1207
  // Scan worker directories for jobs with delivered=true in status.json
1157
1208
  // This is more reliable than content hashing — uses job IDs directly
1158
1209
  try {
@@ -1205,7 +1256,7 @@ export function registerAsyncAgents(
1205
1256
  try { fs.rmSync(jobDir, { recursive: true, force: true }); } catch {}
1206
1257
  } catch {} // skip unreadable dirs
1207
1258
  }
1208
- } catch (e: any) { console.debug("[async-agents] zombie cleanup failed:", e?.message?.slice(0, 100)); }
1259
+ } catch (e: any) { log.error(`zombie cleanup failed: ${e?.message?.slice(0, 100)}`); }
1209
1260
 
1210
1261
  // Clean up stale result directories from dead sessions
1211
1262
  try {
@@ -1296,13 +1347,13 @@ export function registerAsyncAgents(
1296
1347
  log.info(`restored job: ${id} state=${job.status}`);
1297
1348
  }
1298
1349
  }
1299
- } catch (e: any) { console.debug("[async-agents] job restore failed:", e?.message || e); }
1350
+ } catch (e: any) { log.error(`job restore failed: ${e?.message || e}`); }
1300
1351
 
1301
1352
  // Start poller if we have jobs or unprocessed result files
1302
1353
  let hasResultFiles = false;
1303
1354
  try {
1304
1355
  hasResultFiles = fs.existsSync(ASYNC_RESULTS_DIR) && fs.readdirSync(ASYNC_RESULTS_DIR).some(f => f.endsWith(".json"));
1305
- } catch (e: any) { console.debug("[async-agents] result files check failed:", e?.message || e); }
1356
+ } catch (e: any) { log.error(`result files check failed: ${e?.message || e}`); }
1306
1357
  if (asyncState.jobs.size > 0 || hasResultFiles) {
1307
1358
  ensureAsyncPoller();
1308
1359
  }
@@ -1321,6 +1372,8 @@ export function registerAsyncAgents(
1321
1372
  // /async-status — fullscreen overlay list → live output detail
1322
1373
  async function handleAsyncStatus(ctx: any): Promise<void> {
1323
1374
  if (!ctx.hasUI) return;
1375
+ log.debug("opening_async_status_overlay", { jobs: asyncState.jobs.size });
1376
+ const { openAsyncStatusOverlay } = await import("./async-status-ui.js");
1324
1377
  await openAsyncStatusOverlay(ctx, {
1325
1378
  listJobs: () => Array.from(asyncState.jobs.values()),
1326
1379
  killJob: (id) => {
@@ -1413,7 +1466,11 @@ export function registerAsyncAgents(
1413
1466
  const displayName = job.name || job.agent;
1414
1467
  const duration = job.durationMs || (Date.now() - job.startedAt);
1415
1468
  const rawOutput = typeof job.output === "string" ? job.output : "Killed by user";
1416
- const output = rawOutput.slice(0, 3000);
1469
+ const output = formatAsyncResultOutput(
1470
+ job.agent,
1471
+ rawOutput,
1472
+ resultOutputPath(job),
1473
+ );
1417
1474
  const killContent = `## Async Agent Result: ${displayName} ❌ failed\n\nTask: ${job.task}\nDuration: ${formatDuration(duration)}\n\n${output}`;
1418
1475
  if (wasAlreadyDelivered(job.id)) {
1419
1476
  job.delivered = true;
@@ -1524,13 +1581,15 @@ export function registerAsyncAgents(
1524
1581
 
1525
1582
  // Spawn — create an async agent from pitasks' TaskExecute
1526
1583
  handleRpc<{ requestId: string; type: string; prompt: string; options?: any }>(
1527
- "subagents:rpc:spawn", ({ type, prompt, options }) => {
1584
+ "subagents:rpc:spawn", async ({ type, prompt, options }) => {
1528
1585
  const ctx = asyncState.lastCtx;
1529
1586
  if (!ctx) throw new Error("No active session");
1530
1587
  if (!type || typeof type !== "string") throw new Error("Missing or invalid 'type' parameter");
1531
1588
  if (!prompt || typeof prompt !== "string") throw new Error("Missing or invalid 'prompt' parameter");
1532
1589
 
1533
1590
  const cwd = options?.cwd || ctx.cwd;
1591
+ log.debug("rpc_spawn_requested", { type, customCwd: Boolean(options?.cwd) });
1592
+ const discoverAgents = runtime.discoverAgents ?? (await import("./agents.js")).discoverAgents;
1534
1593
  const discovery = discoverAgents(cwd, "both");
1535
1594
  const agents = discovery.agents;
1536
1595
 
@@ -0,0 +1,58 @@
1
+ /** Format async-agent output for bounded delivery and status persistence. */
2
+
3
+ import { createHash } from "node:crypto";
4
+ import * as path from "node:path";
5
+ import { createLogger } from "../shared/logger.js";
6
+
7
+ const log = createLogger("async_agents");
8
+ const MAX_OUTPUT_CHARS = 3000;
9
+
10
+ /** Return the completed-result archive path, outside cleanup-prone worker directories. */
11
+ export function reviewerOutputArchivePath(projectTmpDir: string, jobId: string): string {
12
+ const archiveDir = path.resolve(projectTmpDir, "reviewer-results");
13
+ const filename = `${createHash("sha256").update(jobId).digest("hex")}.json`;
14
+ const archivePath = path.resolve(archiveDir, filename);
15
+ if (path.dirname(archivePath) !== archiveDir) {
16
+ throw new Error("Reviewer archive destination escapes its archive root");
17
+ }
18
+ log.debug("reviewer_output_archive_path", { jobId, archivePath });
19
+ return archivePath;
20
+ }
21
+
22
+ /**
23
+ * Preserve valid reviewer JSON unless it exceeds the delivery budget.
24
+ * Oversized reviewer output is available only in its worker output.log.
25
+ */
26
+ export function formatAsyncResultOutput(
27
+ agent: string,
28
+ output: string,
29
+ outputPath: string,
30
+ maxChars = MAX_OUTPUT_CHARS,
31
+ ): string {
32
+ log.debug("format_async_result_output", { agent, outputChars: output.length, maxChars });
33
+ if (agent.startsWith("code-reviewer-") && output.length > maxChars) {
34
+ try {
35
+ const parsed = JSON.parse(output);
36
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.findings)) {
37
+ const metadata = {
38
+ truncated: true,
39
+ format: "json",
40
+ outputBytes: Buffer.byteLength(output, "utf8"),
41
+ outputPath,
42
+ };
43
+ const serialized = JSON.stringify(metadata);
44
+ if (serialized.length <= maxChars) return serialized;
45
+ log.warn("reviewer_metadata_path_exceeds_budget", { agent, maxChars });
46
+ return JSON.stringify({
47
+ truncated: true,
48
+ format: "json",
49
+ outputBytes: metadata.outputBytes,
50
+ outputPathOmitted: true,
51
+ });
52
+ }
53
+ } catch {
54
+ // Invalid reviewer output retains ordinary bounded-text behavior.
55
+ }
56
+ }
57
+ return output.slice(0, maxChars);
58
+ }
@@ -9,6 +9,10 @@ import * as fs from "node:fs";
9
9
  import * as path from "node:path";
10
10
  import { createRequire } from "node:module";
11
11
  import { getSetting } from "./project-settings.js";
12
+ import { createLogger } from "../shared/logger.js";
13
+ import { archiveReviewerOutput } from "./reviewer-output-archive.js";
14
+
15
+ const log = createLogger("async_agents");
12
16
 
13
17
  interface RunConfig {
14
18
  id: string;
@@ -20,6 +24,7 @@ interface RunConfig {
20
24
  tools?: string[];
21
25
  systemPrompt?: string;
22
26
  resultPath: string;
27
+ reviewerOutputPath?: string;
23
28
  workerDir: string;
24
29
  sessionId?: string;
25
30
  piCommand: string;
@@ -72,7 +77,7 @@ async function run(config: RunConfig): Promise<void> {
72
77
  writeJson(statusPath, status);
73
78
  let lastUsage: any = null;
74
79
 
75
- const outputStream = fs.createWriteStream(outputPath, { flags: "w" });
80
+ const outputStream = fs.createWriteStream(outputPath, { flags: "w", mode: 0o600 });
76
81
 
77
82
  // Connect to pidash server to stream events
78
83
  const pidashPort = getSetting(config.cwd, "pidash_port");
@@ -157,7 +162,7 @@ async function run(config: RunConfig): Promise<void> {
157
162
  if (pidashWs?.readyState === 1) {
158
163
  pidashWs.send(JSON.stringify({ type: "async_event", id: config.id, event: ev }));
159
164
  }
160
- } catch (e: any) { console.debug("[async-runner] event processing failed:", e?.message || e); }
165
+ } catch (e: any) { log.warn(`event processing skipped: ${e?.message || e}`); }
161
166
  }
162
167
 
163
168
  // Update status periodically (every ~10 lines)
@@ -193,7 +198,7 @@ async function run(config: RunConfig): Promise<void> {
193
198
  if (p.type === "text") finalOutput = p.text;
194
199
  }
195
200
  }
196
- } catch (e: any) { console.debug("[async-runner] final line parse failed:", e?.message || e); }
201
+ } catch (e: any) { log.error(`final line parse failed: ${e?.message || e}`); }
197
202
  }
198
203
 
199
204
  // Write final status
@@ -283,7 +288,18 @@ async function run(config: RunConfig): Promise<void> {
283
288
  }
284
289
 
285
290
  // End the output stream after all retries are done
286
- outputStream.end();
291
+ await new Promise<void>((resolve, reject) => {
292
+ outputStream.once("error", reject);
293
+ outputStream.end(resolve);
294
+ });
295
+ if (config.reviewerOutputPath && config.agent.startsWith("code-reviewer-")) {
296
+ try {
297
+ archiveReviewerOutput(outputPath, config.reviewerOutputPath);
298
+ log.info("archived_reviewer_output", { agent: config.agent, outputPath: config.reviewerOutputPath });
299
+ } catch (err) {
300
+ log.error("archive_reviewer_output_failed", err);
301
+ }
302
+ }
287
303
 
288
304
  // Write result for the watcher to pick up
289
305
  writeJson(config.resultPath, {
@@ -0,0 +1,69 @@
1
+ /** Durable reviewer-output archival and bounded retention. */
2
+
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { createLogger } from "../shared/logger.js";
6
+
7
+ const log = createLogger("async_agents");
8
+
9
+ export const REVIEWER_ARCHIVE_RETENTION = {
10
+ maxAgeMs: 7 * 24 * 60 * 60 * 1000,
11
+ maxFiles: 100,
12
+ maxBytes: 50 * 1024 * 1024,
13
+ };
14
+
15
+ type Retention = typeof REVIEWER_ARCHIVE_RETENTION;
16
+
17
+ /** Copy reviewer output into a durable owner-only archive. */
18
+ export function archiveReviewerOutput(outputPath: string, archivePath: string): void {
19
+ fs.chmodSync(outputPath, 0o600);
20
+ fs.mkdirSync(path.dirname(archivePath), { recursive: true, mode: 0o700 });
21
+ fs.copyFileSync(outputPath, archivePath);
22
+ fs.chmodSync(archivePath, 0o600);
23
+ cleanupReviewerOutputArchives(path.dirname(archivePath));
24
+ log.info("archived_reviewer_output", { outputPath: archivePath });
25
+ }
26
+
27
+ /** Remove expired and oldest regular-file archives while preserving bounded storage. */
28
+ export function cleanupReviewerOutputArchives(
29
+ archiveDir: string,
30
+ retention: Retention = REVIEWER_ARCHIVE_RETENTION,
31
+ ): void {
32
+ let archives: Array<{ path: string; mtimeMs: number; size: number }>;
33
+ try {
34
+ archives = fs.readdirSync(archiveDir).flatMap((entry) => {
35
+ const archivePath = path.join(archiveDir, entry);
36
+ try {
37
+ const stat = fs.lstatSync(archivePath);
38
+ return stat.isFile() ? [{ path: archivePath, mtimeMs: stat.mtimeMs, size: stat.size }] : [];
39
+ } catch {
40
+ return [];
41
+ }
42
+ });
43
+ } catch (error: any) {
44
+ if (error?.code !== "ENOENT") log.warn("reviewer_archive_cleanup_scan_failed", { archiveDir, error: error?.message });
45
+ return;
46
+ }
47
+
48
+ const now = Date.now();
49
+ const retained = archives
50
+ .filter((archive) => {
51
+ if (now - archive.mtimeMs <= retention.maxAgeMs) return true;
52
+ try { fs.unlinkSync(archive.path); } catch (error: any) { log.warn("reviewer_archive_expiry_delete_failed", { archivePath: archive.path, error: error?.message }); return true; }
53
+ return false;
54
+ })
55
+ .sort((left, right) => right.mtimeMs - left.mtimeMs);
56
+
57
+ let totalBytes = retained.reduce((total, archive) => total + archive.size, 0);
58
+ for (let index = retained.length - 1; index >= 0; index--) {
59
+ const archive = retained[index];
60
+ if (index < retention.maxFiles && totalBytes <= retention.maxBytes) continue;
61
+ try {
62
+ fs.unlinkSync(archive.path);
63
+ totalBytes -= archive.size;
64
+ } catch (error: any) {
65
+ log.warn("reviewer_archive_retention_delete_failed", { archivePath: archive.path, error: error?.message });
66
+ }
67
+ }
68
+ log.debug("reviewer_archive_cleanup_complete", { archiveDir, retainedFiles: retained.length, totalBytes });
69
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pidiff-ui",
3
3
  "private": true,
4
- "version": "4.3.11",
4
+ "version": "4.3.12",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
@@ -61,7 +61,7 @@ All tasks are created with status \`pending\`.
61
61
  - Include enough detail in the description for another agent to understand and complete the task
62
62
  - After creating tasks, use TaskUpdate to set up dependencies (blocks/blockedBy) if needed
63
63
  - Check TaskList first to avoid creating duplicate tasks
64
- - Include \`agentType\` (e.g., "general-purpose", "Explore") to mark tasks for subagent execution via TaskExecute`,
64
+ - Use \`agentType\` only as optional descriptive metadata; it does not dispatch or execute an agent`,
65
65
  promptGuidelines: [
66
66
  "When working on complex multi-step tasks, use TaskCreate to track progress and TaskUpdate to update status.",
67
67
  "Mark tasks as in_progress before starting work and completed when done.",
@@ -71,7 +71,7 @@ All tasks are created with status \`pending\`.
71
71
  subject: Type.String({ description: "A brief title for the task" }),
72
72
  description: Type.String({ description: "A detailed description of what needs to be done" }),
73
73
  activeForm: Type.Optional(Type.String({ description: "Present continuous form shown in spinner when in_progress (e.g., 'Running tests')" })),
74
- agentType: Type.Optional(Type.String({ description: "Agent type for subagent execution (e.g., 'general-purpose', 'Explore'). Tasks with agentType can be started via TaskExecute." })),
74
+ agentType: Type.Optional(Type.String({ description: "Optional descriptive metadata for the task. It does not dispatch or execute an agent." })),
75
75
  metadata: Type.Optional(Type.Record(Type.String(), Type.Any(), { description: "Arbitrary metadata to attach to the task" })),
76
76
  }),
77
77
  execute(_toolCallId: string, params: any) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-orchestrator-config",
3
- "version": "4.3.11",
3
+ "version": "4.3.12",
4
4
  "description": "Orchestrator pattern for pi: specialist subagents, enforcement, code review loop, workflow commands",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -7,17 +7,17 @@ REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
7
7
 
8
8
  build_ui() {
9
9
  local ui_dir="$1"
10
- echo "Building ${ui_dir}..."
10
+ echo "Building ${ui_dir}..." >&2
11
11
  cd "${REPO_DIR}/${ui_dir}"
12
12
  if [ ! -d node_modules ]; then
13
- npm install --production=false
13
+ npm install --production=false >&2
14
14
  fi
15
- npm run build
15
+ npm run build >&2
16
16
  }
17
17
 
18
18
  build_ui extensions/pidash/pidash-ui
19
19
  build_ui extensions/pidiff/pidiff-ui
20
20
 
21
- echo "UI builds complete."
22
- echo " ${REPO_DIR}/extensions/pidash/pidash-ui/dist/index.html"
23
- echo " ${REPO_DIR}/extensions/pidiff/pidiff-ui/dist/index.html"
21
+ echo "UI builds complete." >&2
22
+ echo " ${REPO_DIR}/extensions/pidash/pidash-ui/dist/index.html" >&2
23
+ echo " ${REPO_DIR}/extensions/pidiff/pidiff-ui/dist/index.html" >&2