pi-subagents 0.80.0 → 0.81.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/bundle/agents.js +18 -0
  3. package/bundle/background-work.js +16 -0
  4. package/bundle/capability-ceiling.js +22 -0
  5. package/bundle/child-tool-plan.js +11 -0
  6. package/bundle/chunk-26C3WLYQ.js +8388 -0
  7. package/bundle/chunk-2XYWN7QJ.js +128 -0
  8. package/bundle/chunk-2ZSNH26H.js +4164 -0
  9. package/bundle/chunk-3DC5KBVP.js +235 -0
  10. package/bundle/chunk-3IF2PGW7.js +380 -0
  11. package/bundle/chunk-4QFZZV3O.js +57 -0
  12. package/bundle/chunk-563BHAZ5.js +1365 -0
  13. package/bundle/chunk-5LFLVSTM.js +260 -0
  14. package/bundle/chunk-62OCY2S4.js +14 -0
  15. package/bundle/chunk-74RT5YQE.js +326 -0
  16. package/bundle/chunk-7F6RKT6W.js +132 -0
  17. package/bundle/chunk-7J4ZXMY7.js +18 -0
  18. package/bundle/chunk-ARRKBB2B.js +179 -0
  19. package/bundle/chunk-BJN5LUQY.js +458 -0
  20. package/bundle/chunk-BPS2AGBZ.js +5421 -0
  21. package/bundle/chunk-CEKS3TFO.js +1337 -0
  22. package/bundle/chunk-CTPAE36Y.js +94 -0
  23. package/bundle/chunk-D4EHT5SJ.js +14 -0
  24. package/bundle/chunk-EYNJ6VL6.js +526 -0
  25. package/bundle/chunk-G5DKFLLI.js +1185 -0
  26. package/bundle/chunk-KSWQVGDW.js +1027 -0
  27. package/bundle/chunk-L5ZHE4L4.js +946 -0
  28. package/bundle/chunk-LSRUTHT7.js +57 -0
  29. package/bundle/chunk-N2BN7BF5.js +159 -0
  30. package/bundle/chunk-NF7IEKBH.js +490 -0
  31. package/bundle/chunk-NIXWDZZV.js +9 -0
  32. package/bundle/chunk-NNLASTQE.js +4082 -0
  33. package/bundle/chunk-TUWLXTAM.js +935 -0
  34. package/bundle/chunk-UKG4V45O.js +228 -0
  35. package/bundle/chunk-V2JWDB5U.js +753 -0
  36. package/bundle/chunk-V3BIUPLU.js +9163 -0
  37. package/bundle/chunk-V6MR5TOQ.js +270 -0
  38. package/bundle/chunk-VJHOELQP.js +8245 -0
  39. package/bundle/chunk-WSBNENFS.js +732 -0
  40. package/bundle/chunk-XMFLCKMQ.js +16664 -0
  41. package/bundle/chunk-YFCK7H2S.js +129 -0
  42. package/bundle/chunk-YYREO77V.js +137 -0
  43. package/bundle/chunk-Z7KVHQHL.js +173 -0
  44. package/bundle/chunk-ZBC44JJ3.js +8606 -0
  45. package/bundle/chunk-ZP3HODFH.js +175 -0
  46. package/bundle/control-channel.js +11 -0
  47. package/bundle/delegation.js +14 -0
  48. package/bundle/external-job-provider.js +20 -0
  49. package/bundle/external-runs.js +21 -0
  50. package/bundle/index.js +30 -0
  51. package/bundle/inspectors.js +8 -0
  52. package/bundle/intercom-bridge.js +11 -0
  53. package/bundle/preflight.js +425 -0
  54. package/bundle/project-panes.js +28 -0
  55. package/bundle/required-child-extensions.js +6 -0
  56. package/bundle/shared-types.js +8 -0
  57. package/bundle/workflow-resources.js +7 -0
  58. package/docs/extension-api.md +27 -0
  59. package/docs/observability.md +6 -2
  60. package/package.json +18 -18
  61. package/src/agents/frontmatter.d.ts +2 -0
  62. package/src/agents/frontmatter.js +9 -2
  63. package/src/api/delegation.d.ts +20 -0
  64. package/src/extension/index.js +27 -7
  65. package/src/runs/background/run-child-session.js +18 -0
  66. package/src/runs/background/stale-run-reconciler.js +2 -0
  67. package/src/runs/foreground/execution.js +39 -2
  68. package/src/runs/shared/structured-output.d.ts +1 -0
  69. package/src/runs/shared/structured-output.js +1 -0
  70. package/src/shared/accessible-dir.js +4 -1
  71. package/src/shared/provider-diagnostics.d.ts +9 -0
  72. package/src/shared/provider-diagnostics.js +75 -0
  73. package/src/shared/types.d.ts +3 -0
  74. package/src/slash/delegation-adapters.d.ts +3 -1
  75. package/src/slash/delegation-adapters.js +7 -0
  76. package/src/slash/prompt-template-bridge.js +3 -1
  77. package/src/slash/subagents-admin.d.ts +1 -0
  78. package/src/slash/subagents-admin.js +1 -1
  79. package/src/tui/fleet-layout.d.ts +20 -0
  80. package/src/tui/fleet-layout.js +17 -0
  81. package/src/tui/fleet-viewport.d.ts +12 -0
  82. package/src/tui/fleet-viewport.js +35 -0
  83. package/src/tui/fleet.d.ts +5 -6
  84. package/src/tui/fleet.js +69 -51
  85. package/src/workflows/structured-workflow-scripts.d.ts +1 -0
  86. package/src/workflows/structured-workflow-scripts.js +5 -4
  87. package/src/workflows/workflow-resources.js +2 -2
@@ -0,0 +1,94 @@
1
+ // src/shared/required-child-extensions.ts
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
5
+ var MAX_REQUIRED_EXTENSIONS = 32;
6
+ var MAX_PATH_BYTES = 4096;
7
+ var EMPTY_SNAPSHOT = Object.freeze([]);
8
+ function snapshotRequiredChildExtensions(value, label = "Required child extensions", canonicalizeFiles = false) {
9
+ if (!Array.isArray(value) || value.length > MAX_REQUIRED_EXTENSIONS) throw new Error(`${label} must be an array of at most ${MAX_REQUIRED_EXTENSIONS} entries.`);
10
+ const ids = /* @__PURE__ */ new Set();
11
+ const paths = /* @__PURE__ */ new Set();
12
+ return Object.freeze(value.map((entry, index) => {
13
+ if (!entry || typeof entry !== "object" || Array.isArray(entry) || Object.keys(entry).some((key) => key !== "id" && key !== "path" && (canonicalizeFiles || key !== "requireForAllRunners"))) throw new Error(`${label} entry ${index} requires only id and path.`);
14
+ const id = "id" in entry ? entry.id : void 0;
15
+ const rawPath = "path" in entry ? entry.path : void 0;
16
+ const requireForAllRunners = "requireForAllRunners" in entry ? entry.requireForAllRunners : void 0;
17
+ if (typeof id !== "string" || !ID_PATTERN.test(id)) throw new Error(`${label} entry ${index} requires a safe id of at most 128 characters.`);
18
+ if (ids.has(id)) throw new Error(`${label} id '${id}' is duplicated.`);
19
+ if (requireForAllRunners !== void 0 && requireForAllRunners !== true) throw new Error(`${label} '${id}' requireForAllRunners must be true when present.`);
20
+ if (typeof rawPath !== "string" || !rawPath.trim() || rawPath.includes("\0") || Buffer.byteLength(rawPath, "utf8") > MAX_PATH_BYTES || !canonicalizeFiles && !path.isAbsolute(rawPath)) throw new Error(`${label} '${id}' requires ${canonicalizeFiles ? "a" : "an absolute"} non-empty path of at most ${MAX_PATH_BYTES} bytes without NUL.`);
21
+ let extensionPath = rawPath;
22
+ if (canonicalizeFiles) {
23
+ try {
24
+ extensionPath = fs.realpathSync(path.resolve(rawPath));
25
+ if (!fs.statSync(extensionPath).isFile()) throw new Error("not a file");
26
+ } catch (error) {
27
+ throw new Error(`${label} '${id}' is not an importable file: ${error instanceof Error ? error.message : String(error)}`);
28
+ }
29
+ }
30
+ if (paths.has(extensionPath)) throw new Error(`${label} path '${extensionPath}' is duplicated.`);
31
+ ids.add(id);
32
+ paths.add(extensionPath);
33
+ return Object.freeze(requireForAllRunners ? { id, path: extensionPath, requireForAllRunners } : { id, path: extensionPath });
34
+ }));
35
+ }
36
+ function registry() {
37
+ const key = /* @__PURE__ */ Symbol.for("pi-subagents.required-child-extensions.v1");
38
+ const root = globalThis;
39
+ const existing = root[key];
40
+ if (existing === void 0) {
41
+ const created = { version: 1, bySession: /* @__PURE__ */ new Map() };
42
+ root[key] = created;
43
+ return created;
44
+ }
45
+ if (!existing || typeof existing !== "object" || !("version" in existing) || existing.version !== 1 || !("bySession" in existing) || !(existing.bySession instanceof Map)) throw new Error("Malformed or unsupported required child extension registry.");
46
+ return { version: 1, bySession: existing.bySession };
47
+ }
48
+ function registerRequiredChildExtensions(input) {
49
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key) => key !== "sessionId" && key !== "extensions" && key !== "requireForAllRunners")) throw new Error("Required child extension registration accepts only sessionId, extensions, and requireForAllRunners.");
50
+ if (typeof input.sessionId !== "string" || !input.sessionId || input.sessionId.trim() !== input.sessionId || input.sessionId.length > 256 || input.sessionId.includes("\0")) throw new Error("Required child extension registration requires a non-empty trimmed sessionId of at most 256 characters without NUL.");
51
+ if (input.requireForAllRunners !== void 0 && typeof input.requireForAllRunners !== "boolean") throw new Error("Required child extension registration requireForAllRunners must be a boolean.");
52
+ const canonical = snapshotRequiredChildExtensions(input.extensions, "Required child extensions", true);
53
+ const frozen = input.requireForAllRunners ? Object.freeze(canonical.map((entry) => Object.freeze({ ...entry, requireForAllRunners: true }))) : canonical;
54
+ const store = registry();
55
+ if (store.bySession.has(input.sessionId)) throw new Error(`Required child extensions are already registered for session '${input.sessionId}'; dispose them first.`);
56
+ store.bySession.set(input.sessionId, frozen);
57
+ return { dispose() {
58
+ if (store.bySession.get(input.sessionId) === frozen) store.bySession.delete(input.sessionId);
59
+ } };
60
+ }
61
+ function resolveRequiredChildExtensions(sessionId) {
62
+ if (!sessionId) return EMPTY_SNAPSHOT;
63
+ return registry().bySession.get(sessionId) ?? EMPTY_SNAPSHOT;
64
+ }
65
+ function assertRequiredChildExtensionsAdmitted(snapshots, launch) {
66
+ const ids = [...new Set(snapshots.flatMap((snapshot) => snapshot ?? []).filter((entry) => entry.requireForAllRunners).map((entry) => entry.id))];
67
+ if (ids.length === 0) return;
68
+ const route = launch.machine ? `machine '${launch.machine}'` : launch.runnerType !== void 0 && launch.runnerType !== "pi" ? `runner.type='${launch.runnerType}'` : void 0;
69
+ if (route) throw new Error(`Agent '${launch.agent}' cannot run on ${route}: the host requires child extensions (${ids.join(", ")}) for every runner, and only local native Pi children load them.`);
70
+ }
71
+ function hasMandatoryRequiredChildExtensions(snapshot) {
72
+ return snapshot?.some((entry) => entry.requireForAllRunners) === true;
73
+ }
74
+ var RETAINED_FILE = "required-child-extensions.json";
75
+ function writeRetainedRequiredChildExtensions(asyncDir, snapshot) {
76
+ if (!hasMandatoryRequiredChildExtensions(snapshot)) return;
77
+ fs.writeFileSync(path.join(asyncDir, RETAINED_FILE), `${JSON.stringify(snapshot)}
78
+ `, { mode: 384 });
79
+ }
80
+ function readRetainedRequiredChildExtensions(asyncDir) {
81
+ const filePath = path.join(asyncDir, RETAINED_FILE);
82
+ if (!fs.existsSync(filePath)) return void 0;
83
+ return snapshotRequiredChildExtensions(JSON.parse(fs.readFileSync(filePath, "utf-8")), "Retained required child extensions");
84
+ }
85
+
86
+ export {
87
+ snapshotRequiredChildExtensions,
88
+ registerRequiredChildExtensions,
89
+ resolveRequiredChildExtensions,
90
+ assertRequiredChildExtensionsAdmitted,
91
+ hasMandatoryRequiredChildExtensions,
92
+ writeRetainedRequiredChildExtensions,
93
+ readRetainedRequiredChildExtensions
94
+ };
@@ -0,0 +1,14 @@
1
+ // src/api/delegation.ts
2
+ var SUBAGENT_DELEGATION_REQUEST_EVENT = "prompt-template:subagent:request";
3
+ var SUBAGENT_DELEGATION_STARTED_EVENT = "prompt-template:subagent:started";
4
+ var SUBAGENT_DELEGATION_UPDATE_EVENT = "prompt-template:subagent:update";
5
+ var SUBAGENT_DELEGATION_RESPONSE_EVENT = "prompt-template:subagent:response";
6
+ var SUBAGENT_DELEGATION_CANCEL_EVENT = "prompt-template:subagent:cancel";
7
+
8
+ export {
9
+ SUBAGENT_DELEGATION_REQUEST_EVENT,
10
+ SUBAGENT_DELEGATION_STARTED_EVENT,
11
+ SUBAGENT_DELEGATION_UPDATE_EVENT,
12
+ SUBAGENT_DELEGATION_RESPONSE_EVENT,
13
+ SUBAGENT_DELEGATION_CANCEL_EVENT
14
+ };
@@ -0,0 +1,526 @@
1
+ import {
2
+ getProjectSubagentsDir
3
+ } from "./chunk-3DC5KBVP.js";
4
+ import {
5
+ discoverAgents,
6
+ formatUnknownAgentError,
7
+ resolveAgentName,
8
+ unknownAgentDiagnosticContext
9
+ } from "./chunk-ZBC44JJ3.js";
10
+
11
+ // src/agents/agent-refinements.ts
12
+ import { createHash, randomUUID } from "node:crypto";
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+ var REFINEMENT_FORMAT_VERSION = 1;
16
+ var REFINEMENT_DIR = "refinements";
17
+ var CURRENT_FENCE = "pi-subagents-refinement-current";
18
+ var SNAPSHOTS_FENCE = "pi-subagents-refinement-snapshots-json";
19
+ var MAX_EVIDENCE_ITEMS = 8;
20
+ var MAX_AGE_DAYS = 14;
21
+ var MAX_ITEM_BYTES = 2048;
22
+ var MAX_PACKET_BYTES = 16384;
23
+ var PROPOSAL_AGENT = "reviewer";
24
+ function result(text2, isError = false) {
25
+ return {
26
+ content: [{ type: "text", text: text2 }],
27
+ ...isError ? { isError: true } : {},
28
+ details: { mode: "management", results: [] }
29
+ };
30
+ }
31
+ function record(value) {
32
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
33
+ }
34
+ function text(value) {
35
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
36
+ }
37
+ function textArray(value) {
38
+ if (!Array.isArray(value)) return [];
39
+ return value.map(text).filter((entry) => entry !== null);
40
+ }
41
+ function isoTime(value) {
42
+ const numberValue = typeof value === "number" && Number.isFinite(value) ? value : void 0;
43
+ if (numberValue !== void 0) return new Date(numberValue).toISOString();
44
+ const stringValue = text(value);
45
+ if (!stringValue) return void 0;
46
+ const parsed = Date.parse(stringValue);
47
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : void 0;
48
+ }
49
+ function hashPrompt(systemPrompt) {
50
+ return createHash("sha256").update(systemPrompt).digest("hex");
51
+ }
52
+ function safeAgentFileName(agentName) {
53
+ const trimmed = agentName.trim();
54
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(trimmed) || trimmed.includes("..")) {
55
+ throw new Error(`Agent name '${agentName}' cannot be used as a refinement file name.`);
56
+ }
57
+ return `${trimmed}.md`;
58
+ }
59
+ function getAgentRefinementPath(cwd, agentName) {
60
+ const refinementRoot = path.join(getProjectSubagentsDir(cwd), REFINEMENT_DIR);
61
+ const resolvedRoot = path.resolve(refinementRoot);
62
+ const resolvedPath = path.resolve(refinementRoot, safeAgentFileName(agentName));
63
+ if (resolvedPath !== path.join(resolvedRoot, path.basename(resolvedPath))) {
64
+ throw new Error(`Agent name '${agentName}' cannot be used as a refinement file name.`);
65
+ }
66
+ return resolvedPath;
67
+ }
68
+ function readExisting(cwd, agentName) {
69
+ const filePath = getAgentRefinementPath(cwd, agentName);
70
+ if (!fs.existsSync(filePath)) return { path: filePath };
71
+ return { path: filePath, parsed: parseRefinementFile(fs.readFileSync(filePath, "utf-8"), filePath) };
72
+ }
73
+ function extractFence(markdown, fence) {
74
+ const escaped = fence.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75
+ const match = markdown.match(new RegExp("\\n```" + escaped + "\\n([\\s\\S]*?)\\n```", "m"));
76
+ return match?.[1] ?? null;
77
+ }
78
+ function parseRefinementFile(markdown, label = "refinement file") {
79
+ const metadataMatch = markdown.match(/^<!-- pi-subagents-refinement:v1\n([\s\S]*?)\n-->\n/);
80
+ if (!metadataMatch?.[1]) throw new Error(`${label} is missing refinement metadata.`);
81
+ const metadataValue = JSON.parse(metadataMatch[1]);
82
+ const metadataRecord = record(metadataValue);
83
+ if (!metadataRecord) throw new Error(`${label} metadata must be an object.`);
84
+ const agent = text(metadataRecord.agent);
85
+ const revision = metadataRecord.revision;
86
+ const updatedAt = text(metadataRecord.updatedAt);
87
+ const base = record(metadataRecord.base);
88
+ const evidence = record(metadataRecord.evidence);
89
+ if (!agent || typeof revision !== "number" || !Number.isInteger(revision) || !updatedAt || !base || !evidence) {
90
+ throw new Error(`${label} metadata is invalid.`);
91
+ }
92
+ const source = text(base.source);
93
+ const filePath = text(base.filePath);
94
+ const systemPromptSha256 = text(base.systemPromptSha256);
95
+ if (source !== "builtin" && source !== "package" && source !== "user" && source !== "project") throw new Error(`${label} base source is invalid.`);
96
+ if (!filePath || !systemPromptSha256) throw new Error(`${label} base metadata is invalid.`);
97
+ const current = extractFence(markdown, CURRENT_FENCE);
98
+ if (current === null) throw new Error(`${label} is missing current refinement block.`);
99
+ const snapshotsRaw = extractFence(markdown, SNAPSHOTS_FENCE) ?? "[]";
100
+ const snapshotsValue = JSON.parse(snapshotsRaw);
101
+ if (!Array.isArray(snapshotsValue)) throw new Error(`${label} snapshots must be an array.`);
102
+ const snapshots = snapshotsValue.map((entry, index) => {
103
+ const item = record(entry);
104
+ if (!item) throw new Error(`${label} snapshot ${index} must be an object.`);
105
+ const action = text(item.action);
106
+ const before = typeof item.before === "string" ? item.before : null;
107
+ const after = typeof item.after === "string" ? item.after : null;
108
+ const at = text(item.at);
109
+ if (typeof item.revision !== "number" || !Number.isInteger(item.revision) || !at || action !== "refine" && action !== "rollback" || before === null || after === null) {
110
+ throw new Error(`${label} snapshot ${index} is invalid.`);
111
+ }
112
+ return {
113
+ revision: item.revision,
114
+ at,
115
+ action,
116
+ before,
117
+ after,
118
+ evidenceIds: textArray(item.evidenceIds),
119
+ ...text(item.proposalAgent) ? { proposalAgent: text(item.proposalAgent) } : {}
120
+ };
121
+ });
122
+ return {
123
+ metadata: {
124
+ agent,
125
+ revision,
126
+ updatedAt,
127
+ base: { source, filePath, systemPromptSha256 },
128
+ evidence: {
129
+ maxItems: typeof evidence.maxItems === "number" ? evidence.maxItems : MAX_EVIDENCE_ITEMS,
130
+ maxAgeDays: typeof evidence.maxAgeDays === "number" ? evidence.maxAgeDays : MAX_AGE_DAYS,
131
+ itemBytes: typeof evidence.itemBytes === "number" ? evidence.itemBytes : MAX_ITEM_BYTES,
132
+ totalBytes: typeof evidence.totalBytes === "number" ? evidence.totalBytes : MAX_PACKET_BYTES
133
+ }
134
+ },
135
+ current,
136
+ snapshots
137
+ };
138
+ }
139
+ function serializeRefinementFile(parsed) {
140
+ const metadata = JSON.stringify(parsed.metadata, null, 2);
141
+ const snapshots = JSON.stringify(parsed.snapshots, null, 2);
142
+ return [
143
+ `<!-- pi-subagents-refinement:v${REFINEMENT_FORMAT_VERSION}`,
144
+ metadata,
145
+ "-->",
146
+ "",
147
+ `# Current refinement for \`${parsed.metadata.agent}\``,
148
+ "",
149
+ `\`\`\`${CURRENT_FENCE}`,
150
+ parsed.current,
151
+ "```",
152
+ "",
153
+ "# Snapshots",
154
+ "",
155
+ `\`\`\`${SNAPSHOTS_FENCE}`,
156
+ snapshots,
157
+ "```",
158
+ ""
159
+ ].join("\n");
160
+ }
161
+ function writeRefinementFile(filePath, parsed) {
162
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
163
+ const tmp = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
164
+ fs.writeFileSync(tmp, serializeRefinementFile(parsed), "utf-8");
165
+ fs.renameSync(tmp, filePath);
166
+ }
167
+ function baseMetadata(agent) {
168
+ return {
169
+ source: agent.source,
170
+ filePath: agent.filePath,
171
+ systemPromptSha256: hashPrompt(agent.systemPrompt)
172
+ };
173
+ }
174
+ function metadataFor(agent, revision, now) {
175
+ return {
176
+ agent: agent.name,
177
+ revision,
178
+ updatedAt: now,
179
+ base: baseMetadata(agent),
180
+ evidence: {
181
+ maxItems: MAX_EVIDENCE_ITEMS,
182
+ maxAgeDays: MAX_AGE_DAYS,
183
+ itemBytes: MAX_ITEM_BYTES,
184
+ totalBytes: MAX_PACKET_BYTES
185
+ }
186
+ };
187
+ }
188
+ function withinAge(at, now = Date.now()) {
189
+ if (!at) return true;
190
+ const parsed = Date.parse(at);
191
+ return Number.isFinite(parsed) && now - parsed <= MAX_AGE_DAYS * 24 * 60 * 60 * 1e3;
192
+ }
193
+ function tailBytes(value, maxBytes) {
194
+ const buffer = Buffer.from(value, "utf-8");
195
+ if (buffer.byteLength <= maxBytes) return value;
196
+ return buffer.subarray(buffer.byteLength - maxBytes).toString("utf-8");
197
+ }
198
+ function pushCapped(items, item) {
199
+ if (items.length >= MAX_EVIDENCE_ITEMS) return;
200
+ items.push({ ...item, outputTail: item.outputTail ? tailBytes(item.outputTail, MAX_ITEM_BYTES) : void 0 });
201
+ }
202
+ function acceptanceFields(value) {
203
+ const ledger = record(value);
204
+ if (!ledger) return {};
205
+ const childReport = record(ledger.childReport);
206
+ const reviewResult = record(ledger.reviewResult);
207
+ const reviewFindings = [
208
+ ...textArray(childReport?.reviewFindings),
209
+ ...Array.isArray(reviewResult?.findings) ? reviewResult.findings.map((finding) => text(record(finding)?.issue)).filter((entry) => entry !== null) : []
210
+ ];
211
+ const residualRisks = textArray(childReport?.residualRisks);
212
+ return {
213
+ ...text(ledger.status) ? { acceptanceStatus: text(ledger.status) } : {},
214
+ ...reviewFindings.length > 0 ? { reviewFindings } : {},
215
+ ...residualRisks.length > 0 ? { residualRisks } : {}
216
+ };
217
+ }
218
+ function controlSignals(value) {
219
+ if (!Array.isArray(value)) return [];
220
+ return value.map((entry) => {
221
+ const item = record(entry);
222
+ if (!item) return null;
223
+ const message = text(item.message);
224
+ const reason = text(item.reason);
225
+ return message ? reason ? `${reason}: ${message}` : message : null;
226
+ }).filter((entry) => entry !== null);
227
+ }
228
+ function evidencePacket(items) {
229
+ const packet = [];
230
+ let bytes = 2;
231
+ for (const item of items) {
232
+ const encoded = Buffer.byteLength(JSON.stringify(item), "utf-8") + 1;
233
+ if (bytes + encoded > MAX_PACKET_BYTES) break;
234
+ packet.push(item);
235
+ bytes += encoded;
236
+ }
237
+ return packet;
238
+ }
239
+ function collectBoundedRefinementEvidence(cwd, agentName, state) {
240
+ const items = [];
241
+ const jobs = [...state.asyncJobs.values()].filter((job) => !job.cwd || path.resolve(job.cwd) === path.resolve(cwd)).filter((job) => job.agents?.includes(agentName) || job.steps?.some((step) => step.agent === agentName)).sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
242
+ for (const job of jobs) {
243
+ const at = isoTime(job.updatedAt ?? job.startedAt);
244
+ if (!withinAge(at)) continue;
245
+ const matchingSteps = job.steps?.filter((step) => step.agent === agentName) ?? [];
246
+ if (matchingSteps.length === 0) {
247
+ pushCapped(items, {
248
+ id: `live:${job.asyncId}`,
249
+ source: "live-state",
250
+ runId: job.asyncId,
251
+ agent: agentName,
252
+ ...at ? { at } : {},
253
+ status: job.status,
254
+ refs: [job.asyncDir]
255
+ });
256
+ continue;
257
+ }
258
+ for (const [index, step] of matchingSteps.entries()) {
259
+ const stepRecord = step;
260
+ pushCapped(items, {
261
+ id: `live:${job.asyncId}:${index}`,
262
+ source: "live-state",
263
+ runId: job.asyncId,
264
+ agent: agentName,
265
+ ...at ? { at } : {},
266
+ status: step.status,
267
+ ...text(stepRecord.model) ? { model: text(stepRecord.model) } : {},
268
+ ...text(stepRecord.thinking) ? { thinking: text(stepRecord.thinking) } : {},
269
+ ...acceptanceFields(stepRecord.acceptance),
270
+ ...text(step.error) ? { errors: [text(step.error)] } : {},
271
+ ...text(stepRecord.recentOutput) ? { outputTail: text(stepRecord.recentOutput) } : {},
272
+ refs: [job.asyncDir]
273
+ });
274
+ }
275
+ }
276
+ const artifactsDir = path.join(getProjectSubagentsDir(cwd), "artifacts");
277
+ if (fs.existsSync(artifactsDir)) {
278
+ const files = fs.readdirSync(artifactsDir).filter((file) => file.endsWith("_meta.json")).map((file) => path.join(artifactsDir, file)).sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
279
+ for (const file of files) {
280
+ if (items.length >= MAX_EVIDENCE_ITEMS) break;
281
+ let metadata = null;
282
+ try {
283
+ metadata = record(JSON.parse(fs.readFileSync(file, "utf-8")));
284
+ } catch {
285
+ metadata = null;
286
+ }
287
+ if (!metadata || text(metadata.agent) !== agentName) continue;
288
+ const stat = fs.statSync(file);
289
+ const at = isoTime(metadata.timestamp ?? stat.mtimeMs);
290
+ if (!withinAge(at)) continue;
291
+ const outputPath = file.replace(/_meta\.json$/, "_output.md");
292
+ const outputTail = fs.existsSync(outputPath) ? tailBytes(fs.readFileSync(outputPath, "utf-8"), MAX_ITEM_BYTES) : void 0;
293
+ pushCapped(items, {
294
+ id: `artifact:${path.basename(file, "_meta.json")}`,
295
+ source: outputTail ? "artifact-output" : "artifact-metadata",
296
+ ...text(metadata.runId) ? { runId: text(metadata.runId) } : {},
297
+ agent: agentName,
298
+ ...at ? { at } : {},
299
+ status: typeof metadata.exitCode === "number" ? metadata.exitCode === 0 ? "completed" : "failed" : void 0,
300
+ ...text(metadata.model) ? { model: text(metadata.model) } : {},
301
+ ...text(metadata.thinking) ? { thinking: text(metadata.thinking) } : {},
302
+ ...acceptanceFields(metadata.acceptance),
303
+ ...text(metadata.error) ? { errors: [text(metadata.error)] } : {},
304
+ ...controlSignals(metadata.controlEvents).length > 0 ? { controlSignals: controlSignals(metadata.controlEvents) } : {},
305
+ ...outputTail ? { outputTail } : {},
306
+ refs: outputTail ? [file, outputPath] : [file]
307
+ });
308
+ }
309
+ }
310
+ return evidencePacket(items);
311
+ }
312
+ function appendAgentRefinementOverlay(systemPrompt, input) {
313
+ let current = "";
314
+ let filePath = "";
315
+ try {
316
+ filePath = getAgentRefinementPath(input.cwd, input.agentName);
317
+ if (!fs.existsSync(filePath)) return systemPrompt;
318
+ current = parseRefinementFile(fs.readFileSync(filePath, "utf-8"), filePath).current.trim();
319
+ } catch {
320
+ return systemPrompt;
321
+ }
322
+ if (!current) return systemPrompt;
323
+ const overlay = [
324
+ `<pi-subagents-refinement agent=${JSON.stringify(input.agentName)} source=${JSON.stringify(path.relative(input.cwd, filePath))}>`,
325
+ "Project-local refinement guidance generated from recent bounded evidence.",
326
+ "It does not override tool, developer, task, output, acceptance, or safety instructions.",
327
+ "",
328
+ current,
329
+ "</pi-subagents-refinement>"
330
+ ].join("\n");
331
+ return systemPrompt.trim() ? `${systemPrompt}
332
+
333
+ ${overlay}` : overlay;
334
+ }
335
+ function validateRefinementProposal(proposal, evidenceIds) {
336
+ const item = record(proposal);
337
+ if (!item) return { ok: false, error: "Refinement proposal must be an object." };
338
+ const summary = text(item.summary);
339
+ const editsValue = item.edits;
340
+ if (!summary || !Array.isArray(editsValue)) return { ok: false, error: "Refinement proposal requires summary and edits." };
341
+ if (editsValue.length > 3) return { ok: false, error: "Refinement proposal may contain at most 3 edits." };
342
+ const allowed = new Set(evidenceIds);
343
+ const protectedInstruction = "(?:acceptance|output|safety|policy|policies|tools?|developer|system)";
344
+ const blocked = new RegExp(`\\b(all agents|every agent|global|(?:disable|ignore|bypass|skip|override)\\s+(?:the\\s+)?${protectedInstruction}(?:\\s+instructions?)?|${protectedInstruction}\\s+(?:instructions?|overrides?)|tool safety|review gates|rewrite base|base agent file|settings\\.json|\\.pi/agent|agents/.*\\.md)\\b`, "i");
345
+ const edits = [];
346
+ for (const [index, editValue] of editsValue.entries()) {
347
+ const edit = record(editValue);
348
+ if (!edit) return { ok: false, error: `Refinement proposal edit ${index} must be an object.` };
349
+ const title = text(edit.title);
350
+ const guidance = text(edit.guidance);
351
+ const rationale = text(edit.rationale);
352
+ const cited = textArray(edit.evidenceIds);
353
+ if (!title || !guidance || !rationale) return { ok: false, error: `Refinement proposal edit ${index} is missing title, guidance, or rationale.` };
354
+ if (cited.length === 0 || cited.some((id) => !allowed.has(id))) return { ok: false, error: `Refinement proposal edit ${index} must cite known evidence ids.` };
355
+ if (guidance.includes("```") || guidance.includes("</pi-subagents-refinement>") || blocked.test(guidance)) return { ok: false, error: `Refinement proposal edit ${index} contains disallowed guidance.` };
356
+ edits.push({ title, guidance, rationale, evidenceIds: cited });
357
+ }
358
+ return { ok: true, proposal: { summary, edits, rejectedIdeas: textArray(item.rejectedIdeas), residualRisks: textArray(item.residualRisks) } };
359
+ }
360
+ function proposalSchema() {
361
+ return {
362
+ type: "object",
363
+ additionalProperties: false,
364
+ required: ["summary", "edits", "residualRisks"],
365
+ properties: {
366
+ summary: { type: "string" },
367
+ edits: {
368
+ type: "array",
369
+ maxItems: 3,
370
+ items: {
371
+ type: "object",
372
+ additionalProperties: false,
373
+ required: ["title", "guidance", "evidenceIds", "rationale"],
374
+ properties: {
375
+ title: { type: "string" },
376
+ guidance: { type: "string" },
377
+ evidenceIds: { type: "array", items: { type: "string" }, minItems: 1 },
378
+ rationale: { type: "string" }
379
+ }
380
+ }
381
+ },
382
+ rejectedIdeas: { type: "array", items: { type: "string" } },
383
+ residualRisks: { type: "array", items: { type: "string" } }
384
+ }
385
+ };
386
+ }
387
+ function proposalFromChild(child) {
388
+ for (const entry of child.details?.results ?? []) {
389
+ if (entry.structuredOutput !== void 0) return entry.structuredOutput;
390
+ }
391
+ const output = child.details?.results?.map((entry) => entry.finalOutput ?? "").find((entry) => entry.trim().length > 0) ?? child.content?.map((entry) => entry.text ?? "").find((entry) => entry.trim().length > 0) ?? "";
392
+ const jsonMatch = output.match(/```(?:json)?\n([\s\S]*?)\n```/) ?? output.match(/({[\s\S]*})/);
393
+ if (!jsonMatch?.[1]) return null;
394
+ try {
395
+ return JSON.parse(jsonMatch[1]);
396
+ } catch {
397
+ return null;
398
+ }
399
+ }
400
+ function proposalTask(agent, current, evidence) {
401
+ return [
402
+ `You are a fresh read-only proposal child for project-local refinement of one subagent: ${agent.name}.`,
403
+ "Do not read files. Do not use write, edit, or shell tools. Use only the evidence packet below.",
404
+ "Propose at most 2-3 minimal overlay guidance edits.",
405
+ "Each edit must cite one or more evidence ids from the packet.",
406
+ "Do not propose base agent file, settings, global, automatic, tool, safety, output, acceptance, developer, or system instruction changes.",
407
+ "Return only structured output that matches the schema.",
408
+ "",
409
+ "Agent metadata:",
410
+ JSON.stringify({ name: agent.name, source: agent.source, filePath: agent.filePath, basePromptSha256: hashPrompt(agent.systemPrompt) }, null, 2),
411
+ "",
412
+ "Current overlay:",
413
+ current.trim() || "(none)",
414
+ "",
415
+ "Evidence packet:",
416
+ JSON.stringify(evidence, null, 2)
417
+ ].join("\n");
418
+ }
419
+ function guidanceFromProposal(proposal) {
420
+ return proposal.edits.map((edit) => `- ${edit.guidance.trim()}`).join("\n");
421
+ }
422
+ function resolveOneAgent(cwd, agentName) {
423
+ const discovered = discoverAgents(cwd, "both");
424
+ const resolved = resolveAgentName(agentName, discovered.agents);
425
+ if (resolved.error) return { ok: false, error: resolved.error };
426
+ if (!resolved.agent) return { ok: false, error: formatUnknownAgentError(agentName, unknownAgentDiagnosticContext(discovered)) };
427
+ return { ok: true, agent: resolved.agent };
428
+ }
429
+ async function handleRefinementAction(action, params, ctx) {
430
+ const requestedAgent = params.agent?.trim();
431
+ if (!requestedAgent) return result(`${action} requires agent. Use /subagents-refine <agent> or subagent({ action: "${action}", agent: "<agent>" }).`, true);
432
+ let resolved;
433
+ try {
434
+ resolved = resolveOneAgent(ctx.cwd, requestedAgent);
435
+ } catch (error) {
436
+ return result(error instanceof Error ? error.message : String(error), true);
437
+ }
438
+ if (!resolved.ok) return result(resolved.error, true);
439
+ const agent = resolved.agent;
440
+ let existing;
441
+ try {
442
+ existing = readExisting(ctx.cwd, agent.name);
443
+ } catch (error) {
444
+ return result(error instanceof Error ? error.message : String(error), true);
445
+ }
446
+ if (action === "refine.show") {
447
+ if (!existing.parsed) return result(`No refinement overlay exists for '${agent.name}'.`);
448
+ const currentDigest = hashPrompt(agent.systemPrompt);
449
+ const drift = existing.parsed.metadata.base.systemPromptSha256 === currentDigest ? "no" : "yes";
450
+ const history = existing.parsed.snapshots.slice(-5).map((snapshot2) => `- r${snapshot2.revision} ${snapshot2.action} at ${snapshot2.at} (${snapshot2.evidenceIds.length} evidence id${snapshot2.evidenceIds.length === 1 ? "" : "s"})`).join("\n") || "- none";
451
+ return result([
452
+ `Refinement overlay for '${agent.name}'`,
453
+ `Path: ${existing.path}`,
454
+ `Revision: ${existing.parsed.metadata.revision}`,
455
+ `Updated: ${existing.parsed.metadata.updatedAt}`,
456
+ `Base: ${existing.parsed.metadata.base.source} ${existing.parsed.metadata.base.filePath}`,
457
+ `Base prompt changed since overlay: ${drift}`,
458
+ "",
459
+ "Current guidance:",
460
+ existing.parsed.current.trim() || "(empty)",
461
+ "",
462
+ "Recent history:",
463
+ history
464
+ ].join("\n"));
465
+ }
466
+ if (action === "refine.rollback") {
467
+ if (!existing.parsed) return result(`No refinement overlay exists for '${agent.name}'.`, true);
468
+ const latest = existing.parsed.snapshots.at(-1);
469
+ if (!latest) return result(`No refinement snapshot exists for '${agent.name}'.`, true);
470
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
471
+ const nextRevision2 = existing.parsed.metadata.revision + 1;
472
+ const next2 = {
473
+ metadata: metadataFor(agent, nextRevision2, now2),
474
+ current: latest.before,
475
+ snapshots: [...existing.parsed.snapshots, { revision: nextRevision2, at: now2, action: "rollback", before: existing.parsed.current, after: latest.before, evidenceIds: latest.evidenceIds }]
476
+ };
477
+ try {
478
+ writeRefinementFile(existing.path, next2);
479
+ } catch (error) {
480
+ return result(error instanceof Error ? error.message : String(error), true);
481
+ }
482
+ return result(`Rolled back refinement overlay for '${agent.name}' to revision ${nextRevision2}.
483
+ Path: ${existing.path}`);
484
+ }
485
+ const evidence = collectBoundedRefinementEvidence(ctx.cwd, agent.name, ctx.state);
486
+ if (evidence.length === 0) return result(`No bounded recent evidence was found for '${agent.name}'. No proposal child was launched and no overlay was written.`);
487
+ const current = existing.parsed?.current ?? "";
488
+ const child = await ctx.launchProposalChild(proposalTask(agent, current, evidence), proposalSchema(), ctx.signal);
489
+ if (child.isError) return result(`Refinement proposal child failed. No overlay was written.`, true);
490
+ const validation = validateRefinementProposal(proposalFromChild(child), evidence.map((item) => item.id));
491
+ if (!validation.ok) return result(`${validation.error} No overlay was written.`, true);
492
+ if (validation.proposal.edits.length === 0) return result(`The proposal child returned no edits for '${agent.name}'. No overlay was written.`);
493
+ const now = (/* @__PURE__ */ new Date()).toISOString();
494
+ const nextRevision = (existing.parsed?.metadata.revision ?? 0) + 1;
495
+ const after = guidanceFromProposal(validation.proposal);
496
+ const snapshot = {
497
+ revision: nextRevision,
498
+ at: now,
499
+ action: "refine",
500
+ before: current,
501
+ after,
502
+ evidenceIds: [...new Set(validation.proposal.edits.flatMap((edit) => edit.evidenceIds))],
503
+ proposalAgent: PROPOSAL_AGENT
504
+ };
505
+ const next = {
506
+ metadata: metadataFor(agent, nextRevision, now),
507
+ current: after,
508
+ snapshots: [...existing.parsed?.snapshots ?? [], snapshot]
509
+ };
510
+ try {
511
+ writeRefinementFile(existing.path, next);
512
+ } catch (error) {
513
+ return result(error instanceof Error ? error.message : String(error), true);
514
+ }
515
+ return result([
516
+ `Wrote refinement overlay for '${agent.name}' at revision ${nextRevision}.`,
517
+ `Path: ${existing.path}`,
518
+ `Evidence items: ${evidence.length}`,
519
+ `Edits: ${validation.proposal.edits.length}`
520
+ ].join("\n"));
521
+ }
522
+
523
+ export {
524
+ appendAgentRefinementOverlay,
525
+ handleRefinementAction
526
+ };