@selesai/code 0.13.32 → 0.13.34
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 +28 -0
- package/dist/extensions/capability-gateway/catalog.ts +4 -1
- package/dist/extensions/capability-gateway/index.test.ts +93 -4
- package/dist/extensions/capability-gateway/index.ts +294 -59
- package/dist/extensions/capability-gateway/integration.test.ts +71 -0
- package/dist/extensions/capability-gateway/routing.test.ts +154 -1
- package/dist/extensions/capability-gateway/routing.ts +227 -45
- package/dist/extensions/jev/decisions.test.ts +37 -0
- package/dist/extensions/jev/decisions.ts +148 -43
- package/dist/extensions/jev-ask-tool.test.ts +436 -0
- package/dist/extensions/jev-ask-tool.ts +587 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-hermes-memory/README.md +11 -36
- package/dist/extensions/pi-hermes-memory/src/config.ts +36 -8
- package/dist/extensions/pi-hermes-memory/src/constants.ts +5 -5
- package/dist/extensions/pi-hermes-memory/src/handlers/auto-consolidate.ts +60 -46
- package/dist/extensions/pi-hermes-memory/tests/config.test.ts +26 -2
- package/dist/extensions/pi-hermes-memory/tests/handlers/auto-consolidate.test.ts +7 -1
- package/dist/extensions/pi-intercom/index.ts +5 -1
- package/dist/extensions/pi-subagents/agents/worker.md +3 -2
- package/dist/extensions/pi-subagents/docs/agents.md +2 -0
- package/dist/extensions/pi-subagents/docs/extension-api.md +3 -1
- package/dist/extensions/pi-subagents/docs/observability.md +2 -0
- package/dist/extensions/pi-subagents/docs/tool-reference.md +2 -2
- package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +10 -1
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +3 -21
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/background/run-child-session.ts +1 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +5 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +10 -3
- package/dist/extensions/pi-subagents/src/runs/background/workflow-terminal-proof.ts +67 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +6 -2
- package/dist/extensions/pi-subagents/src/runs/shared/child-tool-plan.ts +67 -5
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +6 -0
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/shared/git-environment.ts +29 -0
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +69 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +20 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +4 -238
- package/dist/extensions/pi-subagents/src/slash/subagent-cost.ts +280 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-3.test.ts +67 -0
- package/dist/extensions/pi-subagents/test/integration/in-process-child.test.ts +29 -1
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +6 -3
- package/dist/extensions/pi-subagents/test/integration/single-execution.part-2.test.ts +25 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +5 -5
- package/dist/extensions/pi-subagents/test/unit/async-spawn-preload.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/child-tool-plan-permission-system.test.ts +98 -0
- package/dist/extensions/pi-subagents/test/unit/child-tool-plan.test.ts +11 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +25 -0
- package/dist/extensions/pi-subagents/test/unit/git-environment.test.ts +38 -0
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +3 -1
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +105 -1
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/structured-output-rejection.test.ts +67 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-terminal-proof.test.ts +98 -0
- package/dist/extensions/rtk.test.ts +21 -13
- package/dist/extensions/tps.test.ts +32 -1
- package/dist/extensions/tps.ts +3 -1
- package/dist/skills/pi-subagents/references/constraints-and-recipes.md +8 -8
- package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
- package/dist/skills/pi-subagents/references/prompting-and-roles.md +1 -1
- package/docs/settings.md +46 -7
- package/package.json +3 -3
|
@@ -75,13 +75,42 @@ export const DEFAULT_CONFIG_PATH = path.join(
|
|
|
75
75
|
AGENT_ROOT,
|
|
76
76
|
"hermes-memory-config.json",
|
|
77
77
|
);
|
|
78
|
+
export const DEFAULT_SETTINGS_PATH = path.join(AGENT_ROOT, "settings.json");
|
|
79
|
+
export const HERMES_MEMORY_SETTINGS_KEY = "hermesMemory";
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
function readConfigObject(configPath: string | undefined): Record<string, unknown> | undefined {
|
|
82
|
+
if (!configPath) return undefined;
|
|
80
83
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
85
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
86
|
+
? parsed as Record<string, unknown>
|
|
87
|
+
: undefined;
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
94
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Read extension settings from settings.json, falling back to the legacy file. */
|
|
98
|
+
export function loadConfig(
|
|
99
|
+
configPath = DEFAULT_CONFIG_PATH,
|
|
100
|
+
settingsPath?: string,
|
|
101
|
+
): MemoryConfig {
|
|
102
|
+
try {
|
|
103
|
+
const legacyConfig = readConfigObject(configPath);
|
|
104
|
+
const resolvedSettingsPath = settingsPath
|
|
105
|
+
?? (configPath === DEFAULT_CONFIG_PATH ? DEFAULT_SETTINGS_PATH : undefined);
|
|
106
|
+
const settings = readConfigObject(resolvedSettingsPath);
|
|
107
|
+
const extensionSettings = settings?.[HERMES_MEMORY_SETTINGS_KEY];
|
|
108
|
+
if (legacyConfig || isRecord(extensionSettings)) {
|
|
109
|
+
const parsed: Record<string, unknown> = {
|
|
110
|
+
...(legacyConfig ?? {}),
|
|
111
|
+
...(isRecord(extensionSettings) ? extensionSettings : {}),
|
|
112
|
+
};
|
|
113
|
+
// New settings.json values take precedence over the legacy file.
|
|
85
114
|
const config: MemoryConfig = { ...DEFAULT_CONFIG };
|
|
86
115
|
const isNonNegativeNumber = (value: unknown): value is number => (
|
|
87
116
|
typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
@@ -156,8 +185,7 @@ export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
|
|
|
156
185
|
if (normalizedProjectsMemoryDir) config.projectsMemoryDir = normalizedProjectsMemoryDir;
|
|
157
186
|
}
|
|
158
187
|
if (
|
|
159
|
-
|
|
160
|
-
parsed.sessionSearch !== null &&
|
|
188
|
+
isRecord(parsed.sessionSearch) &&
|
|
161
189
|
isSessionSearchVariant(parsed.sessionSearch.variant)
|
|
162
190
|
) {
|
|
163
191
|
config.sessionSearch = { variant: parsed.sessionSearch.variant };
|
|
@@ -204,7 +232,7 @@ export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
|
|
|
204
232
|
return config;
|
|
205
233
|
}
|
|
206
234
|
} catch {
|
|
207
|
-
// Fall back to defaults on parse
|
|
235
|
+
// Fall back to defaults on unexpected parse or access issues.
|
|
208
236
|
}
|
|
209
237
|
return { ...DEFAULT_CONFIG };
|
|
210
238
|
}
|
|
@@ -29,12 +29,12 @@ export const DEFAULT_NUDGE_TOOL_CALLS = 15;
|
|
|
29
29
|
export const DEFAULT_REVIEW_RECENT_MESSAGES = 0;
|
|
30
30
|
export const DEFAULT_FLUSH_RECENT_MESSAGES = 0;
|
|
31
31
|
/**
|
|
32
|
-
* A consolidation run pays child-process boot plus a full LLM turn
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* A consolidation run pays child-process boot plus a full LLM turn. Five
|
|
33
|
+
* minutes leaves room for slower models and larger memory stores. Configured
|
|
34
|
+
* values are honored verbatim, including lower ones; `loadConfig` warns when a
|
|
35
|
+
* value below this is set.
|
|
36
36
|
*/
|
|
37
|
-
export const DEFAULT_CONSOLIDATION_TIMEOUT_MS =
|
|
37
|
+
export const DEFAULT_CONSOLIDATION_TIMEOUT_MS = 300000;
|
|
38
38
|
/** Wall-clock grace after overflow before an automatic consolidation may run. */
|
|
39
39
|
export const DEFAULT_OVERFLOW_GRACE_MS = 180000;
|
|
40
40
|
export const DEFAULT_FAILURE_INJECTION_MAX_AGE_DAYS = 7;
|
|
@@ -337,64 +337,78 @@ export function registerConsolidateCommand(
|
|
|
337
337
|
});
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
} catch {
|
|
346
|
-
// Best-effort only. If the command context is already stale, continue
|
|
347
|
-
// with the consolidation work rather than failing before it starts.
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
for (const item of targets) {
|
|
351
|
-
const entries = entriesForTarget(item.store, item.target);
|
|
352
|
-
|
|
353
|
-
if (entries.length === 0) {
|
|
354
|
-
results.push(`${item.label}: (empty, nothing to consolidate)`);
|
|
355
|
-
continue;
|
|
340
|
+
const setStatus = (text?: string) => {
|
|
341
|
+
try {
|
|
342
|
+
ctx.ui.setStatus("pi-hermes-memory:consolidation", text);
|
|
343
|
+
} catch {
|
|
344
|
+
// Best-effort progress feedback only.
|
|
356
345
|
}
|
|
346
|
+
};
|
|
357
347
|
|
|
348
|
+
setStatus("Preparing memory consolidation…");
|
|
349
|
+
try {
|
|
358
350
|
try {
|
|
359
351
|
ctx.ui.notify(
|
|
360
|
-
|
|
352
|
+
`🔄 Starting memory consolidation for ${targets.length} target${targets.length === 1 ? "" : "s"}...`,
|
|
361
353
|
"info",
|
|
362
354
|
);
|
|
363
355
|
} catch {
|
|
364
|
-
// Best-effort
|
|
356
|
+
// Best-effort only. If the command context is already stale, continue
|
|
357
|
+
// with the consolidation work rather than failing before it starts.
|
|
365
358
|
}
|
|
366
359
|
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
360
|
+
for (const [index, item] of targets.entries()) {
|
|
361
|
+
const entries = entriesForTarget(item.store, item.target);
|
|
362
|
+
|
|
363
|
+
if (entries.length === 0) {
|
|
364
|
+
results.push(`${item.label}: (empty, nothing to consolidate)`);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
setStatus(`Consolidating ${item.label} (${index + 1}/${targets.length})…`);
|
|
369
|
+
try {
|
|
370
|
+
ctx.ui.notify(
|
|
371
|
+
`⏳ Consolidating ${item.label}...`,
|
|
372
|
+
"info",
|
|
373
|
+
);
|
|
374
|
+
} catch {
|
|
375
|
+
// Best-effort progress feedback only.
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const result = await triggerConsolidation(
|
|
379
|
+
pi,
|
|
380
|
+
item.store,
|
|
381
|
+
item.target,
|
|
382
|
+
ctx.signal,
|
|
383
|
+
timeoutMs,
|
|
384
|
+
item.toolTarget,
|
|
385
|
+
llmConfig,
|
|
386
|
+
ctx,
|
|
387
|
+
dbManager,
|
|
388
|
+
activeProjectName,
|
|
389
|
+
deps,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
if (result.consolidated) {
|
|
393
|
+
await item.store.loadFromDisk();
|
|
394
|
+
results.push(`${item.label}: ✅ consolidated`);
|
|
395
|
+
} else {
|
|
396
|
+
results.push(`${item.label}: ❌ ${result.error}`);
|
|
397
|
+
}
|
|
386
398
|
}
|
|
387
|
-
}
|
|
388
399
|
|
|
389
|
-
|
|
400
|
+
const summary = `\n 🔄 Memory Consolidation\n ${"─".repeat(30)}\n${results.map((r) => ` ${r}`).join("\n")}`;
|
|
390
401
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
402
|
+
try {
|
|
403
|
+
ctx.ui.notify(summary, "info");
|
|
404
|
+
} catch {
|
|
405
|
+
// Child consolidation can indirectly trigger a runtime reload/session
|
|
406
|
+
// replacement. If that happens, the original command ctx is stale by
|
|
407
|
+
// the time we reach the final summary, so the command should exit
|
|
408
|
+
// quietly instead of surfacing a stale-ctx error.
|
|
409
|
+
}
|
|
410
|
+
} finally {
|
|
411
|
+
setStatus(undefined);
|
|
398
412
|
}
|
|
399
413
|
},
|
|
400
414
|
});
|
|
@@ -7,9 +7,11 @@ import { loadConfig } from "../src/config.js";
|
|
|
7
7
|
import { AGENT_ROOT } from "../src/paths.js";
|
|
8
8
|
|
|
9
9
|
const TEST_CONFIG_PATH = path.join(os.tmpdir(), `hermes-memory-config-test-${process.pid}.json`);
|
|
10
|
+
const TEST_SETTINGS_PATH = path.join(os.tmpdir(), `hermes-settings-test-${process.pid}.json`);
|
|
10
11
|
|
|
11
12
|
afterEach(() => {
|
|
12
13
|
fs.rmSync(TEST_CONFIG_PATH, { force: true });
|
|
14
|
+
fs.rmSync(TEST_SETTINGS_PATH, { force: true });
|
|
13
15
|
});
|
|
14
16
|
|
|
15
17
|
describe("loadConfig", () => {
|
|
@@ -30,7 +32,7 @@ describe("loadConfig", () => {
|
|
|
30
32
|
assert.strictEqual(config.flushRecentMessages, 0);
|
|
31
33
|
assert.strictEqual(config.memoryOverflowStrategy, "auto-consolidate");
|
|
32
34
|
assert.strictEqual(config.autoConsolidate, true);
|
|
33
|
-
assert.strictEqual(config.consolidationTimeoutMs,
|
|
35
|
+
assert.strictEqual(config.consolidationTimeoutMs, 300000);
|
|
34
36
|
assert.strictEqual(config.overflowGraceMs, 180000);
|
|
35
37
|
assert.strictEqual(config.autoConsolidationWarnOnFailure, true);
|
|
36
38
|
assert.strictEqual(config.failureInjectionEnabled, true);
|
|
@@ -65,7 +67,7 @@ describe("loadConfig", () => {
|
|
|
65
67
|
"a lower configured value must be honored, not clamped",
|
|
66
68
|
);
|
|
67
69
|
assert.strictEqual(warnings.length, 1, "a sub-default value should warn once");
|
|
68
|
-
assert.match(warnings[0], /60000ms.*below the
|
|
70
|
+
assert.match(warnings[0], /60000ms.*below the 300000ms default/);
|
|
69
71
|
} finally {
|
|
70
72
|
console.warn = originalWarn;
|
|
71
73
|
}
|
|
@@ -112,6 +114,28 @@ describe("loadConfig", () => {
|
|
|
112
114
|
assert.strictEqual(config.reviewEnabled, true);
|
|
113
115
|
});
|
|
114
116
|
|
|
117
|
+
it("loads hermesMemory from settings.json with precedence over the legacy config", () => {
|
|
118
|
+
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
119
|
+
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({
|
|
120
|
+
llmThinkingOverride: "high",
|
|
121
|
+
consolidationTimeoutMs: 240000,
|
|
122
|
+
}));
|
|
123
|
+
fs.writeFileSync(TEST_SETTINGS_PATH, JSON.stringify({
|
|
124
|
+
theme: "dark",
|
|
125
|
+
hermesMemory: {
|
|
126
|
+
llmThinkingOverride: "off",
|
|
127
|
+
llmModelOverride: " tokenin/deepseek-v4.1-flash ",
|
|
128
|
+
consolidationTimeoutMs: 300000,
|
|
129
|
+
},
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
const config = loadConfig(TEST_CONFIG_PATH, TEST_SETTINGS_PATH);
|
|
133
|
+
assert.strictEqual(config.llmThinkingOverride, "off");
|
|
134
|
+
assert.strictEqual(config.llmModelOverride, "tokenin/deepseek-v4.1-flash");
|
|
135
|
+
assert.strictEqual(config.consolidationTimeoutMs, 300000);
|
|
136
|
+
assert.strictEqual(config.memoryMode, "policy-only");
|
|
137
|
+
});
|
|
138
|
+
|
|
115
139
|
it("only accepts boolean quickCheckOnOpen overrides", () => {
|
|
116
140
|
fs.mkdirSync(path.dirname(TEST_CONFIG_PATH), { recursive: true });
|
|
117
141
|
fs.writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ quickCheckOnOpen: "false" }));
|
|
@@ -631,6 +631,7 @@ describe("registerConsolidateCommand", () => {
|
|
|
631
631
|
it("includes project memory when a project store is available", async () => {
|
|
632
632
|
let handler: any;
|
|
633
633
|
const notifications: string[] = [];
|
|
634
|
+
const statuses: Array<[string, string | undefined]> = [];
|
|
634
635
|
let projectReloaded = false;
|
|
635
636
|
|
|
636
637
|
const pi = {
|
|
@@ -655,7 +656,10 @@ describe("registerConsolidateCommand", () => {
|
|
|
655
656
|
registerConsolidateCommand(pi, mockStore, 60000, projectStore, "demo-project");
|
|
656
657
|
await handler({}, {
|
|
657
658
|
signal: undefined,
|
|
658
|
-
ui: {
|
|
659
|
+
ui: {
|
|
660
|
+
notify: (message: string) => { notifications.push(message); },
|
|
661
|
+
setStatus: (key: string, text: string | undefined) => { statuses.push([key, text]); },
|
|
662
|
+
},
|
|
659
663
|
});
|
|
660
664
|
|
|
661
665
|
assert.strictEqual(execCalls.length, 4, "should consolidate memory, user, failure, and project stores");
|
|
@@ -670,6 +674,8 @@ describe("registerConsolidateCommand", () => {
|
|
|
670
674
|
assert.ok(projectReloaded, "project store should reload after consolidation");
|
|
671
675
|
assert.ok(notifications.some((message) => message.includes("Starting memory consolidation")), "should show an initial progress notification");
|
|
672
676
|
assert.ok(notifications.some((message) => message.includes("⏳ Consolidating memory")), "should show per-target progress");
|
|
677
|
+
assert.ok(statuses.some(([key, text]) => key === "pi-hermes-memory:consolidation" && text?.includes("Consolidating memory")), "should publish busy progress");
|
|
678
|
+
assert.deepStrictEqual(statuses.at(-1), ["pi-hermes-memory:consolidation", undefined], "should clear busy progress when done");
|
|
673
679
|
const finalNotification = notifications[notifications.length - 1] ?? "";
|
|
674
680
|
assert.ok(finalNotification.includes("failure: ✅ consolidated"), "final notification should include failure result");
|
|
675
681
|
assert.ok(finalNotification.includes("project:demo-project: ✅ consolidated"), "final notification should include project result");
|
|
@@ -1186,7 +1186,11 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1186
1186
|
const deliveredEntry = { ...entry, message: injectedMessage, replyCommand };
|
|
1187
1187
|
replyTracker.queueTurnContext({ from: entry.from, message: injectedMessage, receivedAt: Date.now() });
|
|
1188
1188
|
const senderDisplay = entry.from.name || entry.from.id.slice(0, 8);
|
|
1189
|
-
|
|
1189
|
+
// The tool may be an inactive optional capability: say how to activate it, not just its call.
|
|
1190
|
+
const activateHint = pi.getActiveTools().includes("intercom")
|
|
1191
|
+
? ""
|
|
1192
|
+
: ' (not active yet: call capability_discover with name "intercom" first)';
|
|
1193
|
+
const replyInstruction = replyCommand ? `\n\nTo reply, use the intercom tool${activateHint}: ${replyCommand}` : "";
|
|
1190
1194
|
const deliveryMetadata = formatInboundDeliveryMetadata(injectedMessage);
|
|
1191
1195
|
pi.sendMessage(
|
|
1192
1196
|
{
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
name: worker
|
|
3
3
|
description: Implementation agent for normal tasks and approved oracle handoffs
|
|
4
4
|
aliases: developer, coder, implementer, develop
|
|
5
|
+
acceptanceRole: writer
|
|
5
6
|
thinking: high
|
|
6
7
|
systemPromptMode: replace
|
|
7
8
|
inheritProjectContext: true
|
|
8
9
|
inheritSkills: false
|
|
9
10
|
tools: read, grep, find, ls, bash, edit, write, contact_supervisor
|
|
10
|
-
defaultContext:
|
|
11
|
+
defaultContext: fresh
|
|
11
12
|
output: implementation.md
|
|
12
13
|
defaultReads: context.md, research.md, plan.md, implementation.md, review.md
|
|
13
14
|
defaultProgress: true
|
|
@@ -17,7 +18,7 @@ You are `worker`: the implementation subagent.
|
|
|
17
18
|
|
|
18
19
|
You are the single writer thread. Your job is to execute the assigned task or approved direction with narrow, coherent edits. The main agent and user remain the decision authority.
|
|
19
20
|
|
|
20
|
-
Use the provided tools directly. First read the
|
|
21
|
+
Use the provided tools directly. First read the provided context, supplied files, plan, task paths, and named seams. Then implement carefully and minimally. Use broad search only to verify or expand from that starting point.
|
|
21
22
|
|
|
22
23
|
The builtin worker uses a strict tool allowlist. It does not inherit ambient extension tools from the parent session. To use an extension tool, configure a custom agent with the tool name explicitly listed in `tools` and load its provider through `extensions` or `subagentOnlyExtensions`.
|
|
23
24
|
|
|
@@ -198,6 +198,8 @@ fallbackModels:
|
|
|
198
198
|
|
|
199
199
|
Field notes:
|
|
200
200
|
|
|
201
|
+
Native children expose the capability gateway only when extension policy permits it. The gateway catalogs and activates only tools visible in that child's effective registry; it does not install or provide a missing tool provider.
|
|
202
|
+
|
|
201
203
|
| Field | Notes |
|
|
202
204
|
|-------|-------|
|
|
203
205
|
| `package` | Optional package identifier. A file with `name: scout` and `package: code-analysis` registers as `code-analysis.scout`; serialization keeps `name` and `package` separate. |
|
|
@@ -114,7 +114,7 @@ pi.events.emit("subagents:rpc:v1:request", {
|
|
|
114
114
|
});
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
The RPC methods are `ping`, `status`, `manage`, `spawn`, `steer`, `interrupt`, `stop`, and `
|
|
117
|
+
The RPC methods are `ping`, `status`, `manage`, `spawn`, `steer`, `interrupt`, `stop`, `resume`, and `cost`. `status`, `manage`, `steer`, `interrupt`, and `resume` reuse normal package-owned actions.
|
|
118
118
|
|
|
119
119
|
Method notes:
|
|
120
120
|
|
|
@@ -124,6 +124,7 @@ Method notes:
|
|
|
124
124
|
- `resume` requires a run target and non-empty `message`. It delegates to the existing revival path, which validates current-session ownership, persisted session/recovery metadata, stopped/live state, capability ceilings, and the exclusive session lease before returning the new async run details. Callers may request a `file-only` output path for the revived result without overriding its model, tools, or budgets. `ping.capabilities.resume` advertises this seam.
|
|
125
125
|
- `stop` targets current-session top-level async runs through the stop control channel and records a `stopped` lifecycle instead of reporting a timeout.
|
|
126
126
|
- `status` keeps targeted and rich requests on the executor-backed path. A request with no `id`, `runId`, `dir`, `index`, `view`, or `lines` may use the restored in-memory projections and a short summary; when the live state is missing, stale, session-mismatched, or not restored, it falls back to normal executor status. Status `view`, `lines`, and `index` are forwarded for targeted transcript/fleet requests. Successful replies retain `text`, `details`, `fleet`, and `asyncSnapshot`; the short summary intentionally omits canonical filesystem details, wait subscriptions, and budget annotations.
|
|
127
|
+
- `cost` returns the same parent-plus-child accounting `/subagent-cost` renders, as `{ version: 1, parent, children, childTotal, total, unresolvedAsyncChildren }`. Usage objects contain `input`, `output`, `cacheRead`, `cacheWrite`, `cost`, and `turns`; child rows add `label` plus `agent`, `runId`, or `sessionFile` when known. It is read-only and walks the current session branch and existing artifacts, so request it at a turn boundary rather than on a timer. A non-zero `unresolvedAsyncChildren` means `childTotal` is a lower bound. `ping.capabilities.cost` advertises `{ version: 1 }`.
|
|
127
128
|
|
|
128
129
|
Capability advertisements on `ping`:
|
|
129
130
|
|
|
@@ -136,6 +137,7 @@ Capability advertisements on `ping`:
|
|
|
136
137
|
- `resume` — the revival seam described above.
|
|
137
138
|
- `statusProjection: { version: 1, untargeted: "in-memory-when-ready", targeted: "executor" }` — untargeted status may use restored bounded projections; targeted or rich status remains executor-backed.
|
|
138
139
|
- `fleetStatus: { version: 1 }` — successful `status` replies additionally include `data.fleet`.
|
|
140
|
+
- `cost: { version: 1 }` — the `cost` method is available with the report shape described above.
|
|
139
141
|
|
|
140
142
|
Structured delegation progress updates carry `runId` as soon as foreground execution allocates it, so a caller can retain the package-owned revival target even if its own tool turn is interrupted before the terminal response. Foreground `details.results[]` rows also include a numeric `index` that is unique within the run and stable across partial progress snapshots and the final result; use `(runId, index)` instead of row position to correlate single, counted parallel, and chain children.
|
|
141
143
|
|
|
@@ -27,6 +27,8 @@ subagent({ action: "status", id: "..." }) // one run
|
|
|
27
27
|
|
|
28
28
|
Or ask naturally: "Show me the current async runs."
|
|
29
29
|
|
|
30
|
+
Use `/subagent-cost` for combined parent-plus-child usage. Other extensions can request the same versioned data through the in-process RPC `cost` method instead of scraping slash-command text; it is read-only and should be called at a turn boundary, not polled. `unresolvedAsyncChildren` counts children whose usage metadata could not be read, so a non-zero count means the child total is a lower bound. See [extension-api.md](extension-api.md#in-process-event-bus-rpc).
|
|
31
|
+
|
|
30
32
|
The under-editor async widget gives a short view while work runs. Its expand key follows your Pi keybinding:
|
|
31
33
|
|
|
32
34
|
```text
|
|
@@ -90,7 +90,7 @@ The complete plain-JSON inventory is validated before the first launch (maximum
|
|
|
90
90
|
| `action` | string | - | Offline workflow `validate`, agent management (including `guide`, `children.list`, and `refine`/`refine.show`/`refine.rollback`), lane evidence (`lane.status`, `lane.recordMerge`, `lane.recordSupersession`), mission (`mission.create/list/show/update/resolve-decision/attach-run/close`), Herdr inspector (`inspector.open/status/close`), Herdr project pane (`project.open/status/close`), status/control, plan-only `worktree.cleanup`, schedule, watchdog, or doctor action. |
|
|
91
91
|
| `topic` | `overview \| workflows \| agents \| missions \| observability \| tool-reference \| configuration \| models \| watchdog \| extension-api` | `overview` | Packaged guide topic for `action: "guide"`. |
|
|
92
92
|
| `config` | object/string | - | Agent config for management create/update. |
|
|
93
|
-
| `context` | `fresh \| fork` | global or per-agent default, else `fresh` | Explicit `fresh` or `fork` overrides every workflow child. When omitted, [`defaultSubagentContext`](configuration.md#defaultsubagentcontext) wins over each agent's `defaultContext`; `"fork"` creates a real branched session when the parent session file and current leaf exist, otherwise it falls back to `fresh`. Packaged `worker
|
|
93
|
+
| `context` | `fresh \| fork` | global or per-agent default, else `fresh` | Explicit `fresh` or `fork` overrides every workflow child. When omitted, [`defaultSubagentContext`](configuration.md#defaultsubagentcontext) wins over each agent's `defaultContext`; `"fork"` creates a real branched session when the parent session file and current leaf exist, otherwise it falls back to `fresh`. Packaged `worker` defaults to `fresh`; `oracle` and `advisor` default to `fork`. |
|
|
94
94
|
| `missionId` | string | - | Attach a workflow to an existing project mission instead of creating its default enclosing mission. |
|
|
95
95
|
| `mission` | object/false | auto-create | Override the default enclosing mission with `{ title \| summary, objective?, goal?, budget?, labels? }`. Set exactly one non-empty `title` or `summary`; `objective` and `labels` are optional. `goal` may only be `true`, requires `budget.tokens`, and enables continuation notices. Pass `false` for an intentionally ephemeral workflow with no mission for it or its children and no `state` global. Explicit mission persistence failures are strict. |
|
|
96
96
|
| `handoffPath` | string | - | Aggregate handoff manifest for `action: "worktree.discard"` or lane evidence actions, or optional explicit metadata for `action: "worktree.cleanup"`. |
|
|
@@ -134,7 +134,7 @@ Explicit `context: "fork"` fails fast when the parent session is not persisted,
|
|
|
134
134
|
|
|
135
135
|
When the inherited transcript contains signed Anthropic `thinking` / `redacted_thinking` blocks, `pi-subagents` strips those provider-private blocks from the forked child session. It forces thinking `off` only when the child's effective primary or fallback model resolves through the model registry to the Anthropic provider or `anthropic-messages` API; unresolved models are treated conservatively. The result reports every affected child, including on failed runs. Use `context: "fresh"` when an Anthropic child needs thinking. Explicit `context: "fork"` never silently downgrades to `fresh`.
|
|
136
136
|
|
|
137
|
-
In workflow runs that omit `context`, each `runs.run` child follows the global `defaultSubagentContext` when set, then its own `defaultContext`. Without the global setting, a fresh-default
|
|
137
|
+
In workflow runs that omit `context`, each `runs.run` child follows the global `defaultSubagentContext` when set, then its own `defaultContext`. Without the global setting, a fresh-default worker can run fresh beside a fork-default oracle. If the parent session file or current leaf is not available yet, implicit fork-default children run fresh. Pass explicit `context: "fork"` or `context: "fresh"` when you intentionally want one context for every child.
|
|
138
138
|
|
|
139
139
|
### Workflow steering
|
|
140
140
|
|
|
@@ -10,7 +10,7 @@ Use orchestration as parent-agent guidance, not as a runtime workflow mode. For
|
|
|
10
10
|
clarify → scout → worker → fresh reviewers → worker
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Packaged `worker
|
|
13
|
+
Packaged `worker` defaults to fresh context; `oracle` and `advisor` default to forked context when a launch omits `context`. An implicit fork preference falls back to `fresh` when the parent has no persisted session file or current leaf. Pass `context: "fork"` when you intentionally want a worker to reuse the parent thread, or when fork must remain strict.
|
|
14
14
|
|
|
15
15
|
Child-safety boundaries are enforced at runtime:
|
|
16
16
|
|
|
@@ -23,6 +23,7 @@ import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text
|
|
|
23
23
|
import { readStatus } from "../shared/utils.ts";
|
|
24
24
|
import { SubagentParams } from "./schemas.ts";
|
|
25
25
|
import { normalizePublicSubagentExecution } from "./public-execution.ts";
|
|
26
|
+
import { collectSubagentCost, SUBAGENT_COST_REPORT_VERSION } from "../slash/subagent-cost.ts";
|
|
26
27
|
import { ASYNC_STATUS_SNAPSHOT_KIND, ASYNC_STATUS_SNAPSHOT_VERSION, buildAsyncStatusSnapshotForState } from "../runs/background/async-status-snapshot.ts";
|
|
27
28
|
import { isStoppableAsyncStatusStep, resolveAsyncStatusChild, stopStoppableAsyncStatusChildren, type ResolvedAsyncStatusChild } from "../runs/shared/child-identity.ts";
|
|
28
29
|
|
|
@@ -31,7 +32,7 @@ export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
|
|
|
31
32
|
export const SUBAGENT_RPC_READY_EVENT = "subagents:rpc:v1:ready";
|
|
32
33
|
export const SUBAGENT_RPC_REPLY_EVENT_PREFIX = "subagents:rpc:v1:reply:";
|
|
33
34
|
|
|
34
|
-
export const SUBAGENT_RPC_METHODS = ["ping", "status", "manage", "spawn", "steer", "interrupt", "stop", "resume"] as const;
|
|
35
|
+
export const SUBAGENT_RPC_METHODS = ["ping", "status", "manage", "spawn", "steer", "interrupt", "stop", "resume", "cost"] as const;
|
|
35
36
|
export type SubagentRpcMethod = typeof SUBAGENT_RPC_METHODS[number];
|
|
36
37
|
|
|
37
38
|
export interface SubagentRpcRequestEnvelope {
|
|
@@ -456,6 +457,7 @@ function pingData(ctx: ExtensionContext | null) {
|
|
|
456
457
|
launchResolvedExtensions: { version: 1, source: "launch-resolved" },
|
|
457
458
|
runtimeAcknowledgedExtensions: { version: 1, source: "child-runtime", event: "subagent:acknowledge-extension" },
|
|
458
459
|
processTerminalProof: { version: 1, lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION },
|
|
460
|
+
cost: { version: SUBAGENT_COST_REPORT_VERSION },
|
|
459
461
|
},
|
|
460
462
|
events: {
|
|
461
463
|
ready: SUBAGENT_RPC_READY_EVENT,
|
|
@@ -761,6 +763,13 @@ async function handleRequest(
|
|
|
761
763
|
if (request.method === "resume") {
|
|
762
764
|
return executeChecked(options, ctx, request.requestId, request.method, resumeParams(request.params));
|
|
763
765
|
}
|
|
766
|
+
if (request.method === "cost") {
|
|
767
|
+
// The same parent-plus-child accounting `/subagent-cost` renders, as data.
|
|
768
|
+
// Read-only: it walks the current session branch and existing artifacts,
|
|
769
|
+
// so callers should request it on their own turn boundaries, not on a timer.
|
|
770
|
+
if (request.params !== undefined && !isRecord(request.params)) throw new SubagentRpcError("invalid_params", "RPC cost params must be an object when provided.");
|
|
771
|
+
return collectSubagentCost(ctx, options.state ?? { baseCwd: ctx.cwd });
|
|
772
|
+
}
|
|
764
773
|
throw new SubagentRpcError("unsupported_method", `Unsupported subagent RPC method: ${String(request.method)}`);
|
|
765
774
|
}
|
|
766
775
|
|
|
@@ -6,6 +6,7 @@ import { TEMP_ROOT_DIR, type ActiveAsyncCapacitySnapshot, type AsyncStatus } fro
|
|
|
6
6
|
import { readStatus } from "../../shared/utils.ts";
|
|
7
7
|
import { checkPidLiveness, type PidLiveness } from "./stale-run-reconciler.ts";
|
|
8
8
|
import { readProcessTerminal } from "./process-terminal.ts";
|
|
9
|
+
import { isTerminalAsyncState as terminalState, readWorkflowChildProcessEvidence } from "./workflow-terminal-proof.ts";
|
|
9
10
|
|
|
10
11
|
export const ACTIVE_ASYNC_CAPACITY_DIR = path.join(TEMP_ROOT_DIR, "session-active-async-capacity");
|
|
11
12
|
export const DEFAULT_ABANDONED_SLOT_RELEASE_AFTER_MS = 20 * 60 * 1000;
|
|
@@ -209,10 +210,6 @@ function appendAbandonedReleaseEvent(asyncDir: string, owner: ActiveAsyncCapacit
|
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
function terminalState(state: AsyncStatus["state"]): boolean {
|
|
213
|
-
return state !== "queued" && state !== "running" && state !== "paused";
|
|
214
|
-
}
|
|
215
|
-
|
|
216
213
|
function runnerReleaseVerdict(owner: ActiveAsyncCapacityOwner, status: AsyncStatus | null, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
|
|
217
214
|
if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
|
|
218
215
|
if (!owner.runnerProcessInstanceId) return { state: "retained", reason: "runner process identity has not been recorded" };
|
|
@@ -269,23 +266,8 @@ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwner, status: AsyncSt
|
|
|
269
266
|
if (status.mode !== "workflow") return { state: "retained", reason: `status mode is ${status.mode}, not workflow` };
|
|
270
267
|
if (!terminalState(status.state)) return { state: "retained", reason: `workflow is still ${status.state}` };
|
|
271
268
|
if (liveWorkflowRunIds.has(owner.runId)) return { state: "retained", reason: "workflow controller is still live" };
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (typeof step.async !== "boolean") return { state: "retained", reason: `workflow child ${label} is missing async classification` };
|
|
275
|
-
if (!step.async) continue;
|
|
276
|
-
if (!step.runId) return { state: "retained", reason: `async workflow child ${label} is missing run id` };
|
|
277
|
-
const childDir = path.join(path.dirname(owner.asyncDir), step.runId);
|
|
278
|
-
if (!fs.existsSync(childDir)) return { state: "retained", reason: `async workflow child ${label} directory is missing` };
|
|
279
|
-
const childStatus = readStatus(childDir);
|
|
280
|
-
if (!childStatus) return { state: "retained", reason: `async workflow child ${label} status is missing or unreadable` };
|
|
281
|
-
if (!terminalState(childStatus.state)) return { state: "retained", reason: `async workflow child ${label} is still ${childStatus.state}` };
|
|
282
|
-
if (!childStatus.processTerminal?.runnerProcessInstanceId) return { state: "retained", reason: `async workflow child ${label} has no runner process identity` };
|
|
283
|
-
const proof = readProcessTerminal(childDir, {
|
|
284
|
-
runId: step.runId,
|
|
285
|
-
runnerProcessInstanceId: childStatus.processTerminal.runnerProcessInstanceId,
|
|
286
|
-
});
|
|
287
|
-
if (proof?.state !== "observed" || proof.runId !== step.runId) return { state: "retained", reason: `async workflow child ${label} process-terminal proof is ${proof?.state ?? "missing"}` };
|
|
288
|
-
}
|
|
269
|
+
const evidence = readWorkflowChildProcessEvidence(owner.asyncDir, status.steps);
|
|
270
|
+
if (evidence.state !== "observed") return { state: "retained", reason: evidence.reason };
|
|
289
271
|
return { state: "releasable", reason: "workflow is terminal, controller is gone, and async children have observed proof" };
|
|
290
272
|
}
|
|
291
273
|
|
|
@@ -83,6 +83,7 @@ import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeili
|
|
|
83
83
|
import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
|
|
84
84
|
import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
|
|
85
85
|
import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBindings } from "../shared/extension-bindings.ts";
|
|
86
|
+
import { omitGitRoutingEnv } from "../shared/git-environment.ts";
|
|
86
87
|
import { assertWorkflowLaneKey, normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
|
|
87
88
|
|
|
88
89
|
const require = createRequire(import.meta.url);
|
|
@@ -595,7 +596,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
|
|
|
595
596
|
...backgroundProcessOptions(),
|
|
596
597
|
stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
|
|
597
598
|
env: {
|
|
598
|
-
...omitExtensionBindingsEnv(process.env),
|
|
599
|
+
...omitGitRoutingEnv(omitExtensionBindingsEnv(process.env)),
|
|
599
600
|
[SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot,
|
|
600
601
|
[JITI_ALIAS_ENV]: JSON.stringify(hostPeerAliases.aliases),
|
|
601
602
|
},
|
|
@@ -124,6 +124,7 @@ export interface RunChildSessionResult {
|
|
|
124
124
|
observedMutationAttempt?: boolean;
|
|
125
125
|
structuredOutputToolInvoked?: boolean;
|
|
126
126
|
structuredOutputMessageStartIndex?: number;
|
|
127
|
+
structuredOutputFailed?: boolean;
|
|
127
128
|
watchdog?: ChildWatchdogStateSnapshot;
|
|
128
129
|
sessionFile?: string;
|
|
129
130
|
currentTool?: string;
|
|
@@ -16,6 +16,7 @@ import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts
|
|
|
16
16
|
import { normalizeExternalCliRunnerStatus } from "../shared/external-cli-contract.ts";
|
|
17
17
|
import { resolveSubagentResultStatus } from "../../intercom/result-intercom.ts";
|
|
18
18
|
import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
|
|
19
|
+
import { readWorkflowTerminalProof } from "./workflow-terminal-proof.ts";
|
|
19
20
|
import { formatWaitSubscriptions } from "./wait-subscriptions.ts";
|
|
20
21
|
import { resolveAsyncRunLocation } from "./async-resume.ts";
|
|
21
22
|
import { resolveSubagentRunId } from "./run-id-resolver.ts";
|
|
@@ -704,7 +705,10 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
704
705
|
|
|
705
706
|
const workflowChildren = parseWorkflowChildSummary(status.workflowChildren);
|
|
706
707
|
if (workflowChildren && workflowChildren.workflowRunId !== status.runId) throw new Error("workflowChildren.workflowRunId does not match async status runId.");
|
|
707
|
-
|
|
708
|
+
const workflowTerminalProof = workflowChildren
|
|
709
|
+
? readWorkflowTerminalProof(asyncDir, status.steps, workflowChildren, validHostStepNodes(status.workflowGraph).length, status.endedAt ?? status.lastUpdate ?? status.startedAt)
|
|
710
|
+
: undefined;
|
|
711
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "single", results: [], ...(status.workflowReceiptPath ? { workflowReceiptPath: status.workflowReceiptPath } : {}), ...(status.preflight ? { preflight: status.preflight } : {}), ...(status.workflow?.preflightWarnings?.length ? { preflightWarnings: status.workflow.preflightWarnings } : {}), ...(workflowChildren ? { workflowChildren } : {}), ...(workflowTerminalProof ? { workflowTerminalProof } : {}), ...(runFanoutBudget ? { runFanoutBudget } : {}), ...(processTerminal ? { lifecycleStatus: { processTerminal } } : {}) } };
|
|
708
712
|
}
|
|
709
713
|
}
|
|
710
714
|
|