@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/extensions/capability-gateway/catalog.ts +4 -1
  3. package/dist/extensions/capability-gateway/index.test.ts +93 -4
  4. package/dist/extensions/capability-gateway/index.ts +294 -59
  5. package/dist/extensions/capability-gateway/integration.test.ts +71 -0
  6. package/dist/extensions/capability-gateway/routing.test.ts +154 -1
  7. package/dist/extensions/capability-gateway/routing.ts +227 -45
  8. package/dist/extensions/jev/decisions.test.ts +37 -0
  9. package/dist/extensions/jev/decisions.ts +148 -43
  10. package/dist/extensions/jev-ask-tool.test.ts +436 -0
  11. package/dist/extensions/jev-ask-tool.ts +587 -0
  12. package/dist/extensions/package.json +1 -0
  13. package/dist/extensions/pi-hermes-memory/README.md +11 -36
  14. package/dist/extensions/pi-hermes-memory/src/config.ts +36 -8
  15. package/dist/extensions/pi-hermes-memory/src/constants.ts +5 -5
  16. package/dist/extensions/pi-hermes-memory/src/handlers/auto-consolidate.ts +60 -46
  17. package/dist/extensions/pi-hermes-memory/tests/config.test.ts +26 -2
  18. package/dist/extensions/pi-hermes-memory/tests/handlers/auto-consolidate.test.ts +7 -1
  19. package/dist/extensions/pi-intercom/index.ts +5 -1
  20. package/dist/extensions/pi-subagents/agents/worker.md +3 -2
  21. package/dist/extensions/pi-subagents/docs/agents.md +2 -0
  22. package/dist/extensions/pi-subagents/docs/extension-api.md +3 -1
  23. package/dist/extensions/pi-subagents/docs/observability.md +2 -0
  24. package/dist/extensions/pi-subagents/docs/tool-reference.md +2 -2
  25. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  26. package/dist/extensions/pi-subagents/src/extension/rpc.ts +10 -1
  27. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +3 -21
  28. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +2 -1
  29. package/dist/extensions/pi-subagents/src/runs/background/run-child-session.ts +1 -0
  30. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +5 -1
  31. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +10 -3
  32. package/dist/extensions/pi-subagents/src/runs/background/workflow-terminal-proof.ts +67 -0
  33. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +6 -2
  34. package/dist/extensions/pi-subagents/src/runs/shared/child-tool-plan.ts +67 -5
  35. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +6 -0
  36. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +2 -1
  37. package/dist/extensions/pi-subagents/src/runs/shared/git-environment.ts +29 -0
  38. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +69 -0
  39. package/dist/extensions/pi-subagents/src/shared/types.ts +20 -0
  40. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +4 -238
  41. package/dist/extensions/pi-subagents/src/slash/subagent-cost.ts +280 -0
  42. package/dist/extensions/pi-subagents/test/integration/async-execution.part-3.test.ts +67 -0
  43. package/dist/extensions/pi-subagents/test/integration/in-process-child.test.ts +29 -1
  44. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +6 -3
  45. package/dist/extensions/pi-subagents/test/integration/single-execution.part-2.test.ts +25 -0
  46. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +5 -5
  47. package/dist/extensions/pi-subagents/test/unit/async-spawn-preload.test.ts +21 -0
  48. package/dist/extensions/pi-subagents/test/unit/child-tool-plan-permission-system.test.ts +98 -0
  49. package/dist/extensions/pi-subagents/test/unit/child-tool-plan.test.ts +11 -0
  50. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +12 -0
  51. package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +25 -0
  52. package/dist/extensions/pi-subagents/test/unit/git-environment.test.ts +38 -0
  53. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +3 -1
  54. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +105 -1
  55. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +36 -0
  56. package/dist/extensions/pi-subagents/test/unit/structured-output-rejection.test.ts +67 -0
  57. package/dist/extensions/pi-subagents/test/unit/workflow-terminal-proof.test.ts +98 -0
  58. package/dist/extensions/rtk.test.ts +21 -13
  59. package/dist/extensions/tps.test.ts +32 -1
  60. package/dist/extensions/tps.ts +3 -1
  61. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +8 -8
  62. package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
  63. package/dist/skills/pi-subagents/references/prompting-and-roles.md +1 -1
  64. package/docs/settings.md +46 -7
  65. package/package.json +3 -3
@@ -105,7 +105,7 @@ import { SUBAGENT_CHILD_ENV } from "../shared/child-runtime-config.ts";
105
105
  import { deriveChildSessionName } from "../../shared/child-session-name.ts";
106
106
  import { alignForkedSessionCwd } from "../../shared/fork-session-cwd.ts";
107
107
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
108
- import { clearStructuredOutputCaptures, createStructuredOutputFileCapture, createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
108
+ import { clearStructuredOutputCaptures, createStructuredOutputFileCapture, createStructuredOutputRuntime, formatStructuredOutputRejectionError, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
109
109
  import { formatMidToolExitError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
110
110
  import { formatChildToolDiagnostic } from "../shared/tool-availability.ts";
111
111
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
@@ -285,6 +285,7 @@ interface StepResult {
285
285
  review?: import("../../shared/types.ts").ReviewProjection;
286
286
  effects?: import("../../shared/types.ts").EffectsProjection;
287
287
  structuredOutput?: unknown;
288
+ structuredOutputFailed?: boolean;
288
289
  structuredOutputPath?: string;
289
290
  structuredOutputSchemaPath?: string;
290
291
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
@@ -1221,7 +1222,8 @@ export async function runSingleStepInner(
1221
1222
  schemaPath: effectiveStructuredOutput.schemaPath,
1222
1223
  outputPath: effectiveStructuredOutput.outputPath,
1223
1224
  });
1224
- if (structured.error) structuredError = structured.error;
1225
+ if (structured.error === MISSING_STRUCTURED_OUTPUT_CALL_ERROR) structuredError = formatStructuredOutputRejectionError(run.messages);
1226
+ else if (structured.error) structuredError = structured.error;
1225
1227
  else {
1226
1228
  structuredOutput = structured.value;
1227
1229
  const acceptanceReport = readStructuredOutputAcceptanceReport(effectiveStructuredOutput);
@@ -1343,7 +1345,7 @@ export async function runSingleStepInner(
1343
1345
  afterCompactionSettlement: run.afterCompactionSettlement === true,
1344
1346
  });
1345
1347
  const fileMutationEffect = completionEvidence.fileMutation ?? (missingRequiredOutputAfterMutation ? { status: "observed" as const, expected: completionEvidence.mutationExpected, attempted: true, evidence: mutationEvidence } : undefined);
1346
- finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, runtimeAcknowledgedExtensions, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect || settlementDiagnostic ? { effects: { ...(fileMutationEffect ? { fileMutation: fileMutationEffect } : {}), ...(settlementDiagnostic ? { settlementDiagnostic } : {}) } } : {}) } as RunChildSessionResult;
1348
+ finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, structuredOutputFailed: structuredError ? true : undefined, runtimeAcknowledgedExtensions, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect || settlementDiagnostic ? { effects: { ...(fileMutationEffect ? { fileMutation: fileMutationEffect } : {}), ...(settlementDiagnostic ? { settlementDiagnostic } : {}) } } : {}) } as RunChildSessionResult;
1347
1349
  const abortRecovery = !attempt.success ? planAbortRecovery({
1348
1350
  messages: run.messages,
1349
1351
  error,
@@ -1603,6 +1605,7 @@ export async function runSingleStepInner(
1603
1605
  completionGuardTriggered: completionGuardTriggeredFinal,
1604
1606
  ...((finalResult as (RunChildSessionResult & { effects?: import("../../shared/types.ts").EffectsProjection }) | undefined)?.effects ? { effects: (finalResult as RunChildSessionResult & { effects?: import("../../shared/types.ts").EffectsProjection }).effects } : {}),
1605
1607
  structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : (finalResult as (RunChildSessionResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
1608
+ structuredOutputFailed: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : finalResult?.structuredOutputFailed,
1606
1609
  structuredOutputPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.outputPath,
1607
1610
  structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance ? undefined : effectiveStructuredOutput?.schemaPath,
1608
1611
  acceptance: effectiveAcceptance,
@@ -3788,6 +3791,7 @@ export async function runSubagent(
3788
3791
  review: pr.review,
3789
3792
  timeoutRecovery: pr.timeoutRecovery,
3790
3793
  structuredOutput: pr.structuredOutput,
3794
+ structuredOutputFailed: pr.structuredOutputFailed,
3791
3795
  structuredOutputPath: pr.structuredOutputPath,
3792
3796
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
3793
3797
  acceptance: pr.acceptance,
@@ -4245,6 +4249,7 @@ export async function runSubagent(
4245
4249
  review: pr.review,
4246
4250
  timeoutRecovery: pr.timeoutRecovery,
4247
4251
  structuredOutput: pr.structuredOutput,
4252
+ structuredOutputFailed: pr.structuredOutputFailed,
4248
4253
  structuredOutputPath: pr.structuredOutputPath,
4249
4254
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
4250
4255
  acceptance: pr.acceptance,
@@ -4539,6 +4544,7 @@ export async function runSubagent(
4539
4544
  review: singleResult.review,
4540
4545
  timeoutRecovery: singleResult.timeoutRecovery,
4541
4546
  structuredOutput: singleResult.structuredOutput,
4547
+ structuredOutputFailed: singleResult.structuredOutputFailed,
4542
4548
  structuredOutputPath: singleResult.structuredOutputPath,
4543
4549
  structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
4544
4550
  acceptance: singleResult.acceptance,
@@ -4923,6 +4929,7 @@ export async function runSubagent(
4923
4929
  review: r.review,
4924
4930
  effects: r.effects,
4925
4931
  structuredOutput: r.structuredOutput,
4932
+ structuredOutputFailed: r.structuredOutputFailed,
4926
4933
  structuredOutputPath: r.structuredOutputPath,
4927
4934
  structuredOutputSchemaPath: r.structuredOutputSchemaPath,
4928
4935
  acceptance: r.acceptance,
@@ -0,0 +1,67 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { AsyncStatus, ProcessTerminal, WorkflowChildSummary, WorkflowTerminalProof } from "../../shared/types.ts";
4
+ import { readStatus } from "../../shared/utils.ts";
5
+ import { readProcessTerminal } from "./process-terminal.ts";
6
+
7
+ const TERMINAL_WORKFLOW_STATES = new Set<WorkflowChildSummary["workflowState"]>(["completed", "failed", "stopped"]);
8
+
9
+ export function isTerminalAsyncState(state: AsyncStatus["state"]): boolean {
10
+ return state !== "queued" && state !== "running" && state !== "paused";
11
+ }
12
+
13
+ export type WorkflowChildProcessEvidence =
14
+ | { state: "observed"; children: ProcessTerminal[] }
15
+ | { state: "pending" | "unknown"; reason: string };
16
+
17
+ /**
18
+ * Process evidence for a workflow's async children. Status proofs and capacity
19
+ * release both use this so they cannot disagree about the same workflow.
20
+ * Synchronous children run inside the workflow host and have no process of their own.
21
+ */
22
+ export function readWorkflowChildProcessEvidence(workflowAsyncDir: string, steps: AsyncStatus["steps"]): WorkflowChildProcessEvidence {
23
+ const children: ProcessTerminal[] = [];
24
+ for (const step of steps ?? []) {
25
+ const label = step.workflowKey ?? step.agent;
26
+ if (typeof step.async !== "boolean") return { state: "unknown", reason: `workflow child ${label} is missing async classification` };
27
+ if (!step.async) continue;
28
+ if (!step.runId || path.basename(step.runId) !== step.runId) return { state: "unknown", reason: `async workflow child ${label} is missing run id` };
29
+ const childDir = path.join(path.dirname(workflowAsyncDir), step.runId);
30
+ if (!fs.existsSync(childDir)) return { state: "unknown", reason: `async workflow child ${label} directory is missing` };
31
+ const childStatus = readStatus(childDir);
32
+ if (!childStatus) return { state: "unknown", reason: `async workflow child ${label} status is missing or unreadable` };
33
+ if (!isTerminalAsyncState(childStatus.state)) return { state: "pending", reason: `async workflow child ${label} is still ${childStatus.state}` };
34
+ const recorded = childStatus.processTerminal;
35
+ if (!recorded?.runnerProcessInstanceId) return { state: "unknown", reason: `async workflow child ${label} has no runner process identity` };
36
+ // A runner startup failure records `not-started` in status and never writes a process-terminal sidecar.
37
+ if (recorded.state === "not-started" && recorded.runId === step.runId && typeof childStatus.error === "string" && childStatus.error) {
38
+ children.push(recorded);
39
+ continue;
40
+ }
41
+ const proof = readProcessTerminal(childDir, { runId: step.runId, runnerProcessInstanceId: recorded.runnerProcessInstanceId });
42
+ if (proof?.state !== "observed" || proof.runId !== step.runId) {
43
+ return { state: !proof || proof.state === "pending" ? "pending" : "unknown", reason: `async workflow child ${label} process-terminal proof is ${proof?.state ?? "missing"}` };
44
+ }
45
+ children.push(proof);
46
+ }
47
+ return { state: "observed", children };
48
+ }
49
+
50
+ function unresolved(runId: string, state: "pending" | "unknown", dispatchClosed: boolean, reason: string): WorkflowTerminalProof {
51
+ return { version: 1, kind: "workflow", runId, state, dispatchClosed, reason };
52
+ }
53
+
54
+ /** A persistent workflow host is terminal only after dispatch closes and every async child has process evidence. */
55
+ export function readWorkflowTerminalProof(asyncDir: string, steps: AsyncStatus["steps"], summary: WorkflowChildSummary, hostCommandCount: number, closedAt: number): WorkflowTerminalProof {
56
+ const runId = summary.workflowRunId;
57
+ if (!summary.inventoryComplete || !TERMINAL_WORKFLOW_STATES.has(summary.workflowState)) {
58
+ return unresolved(runId, "pending", false, "Workflow dispatch is still open.");
59
+ }
60
+ if (hostCommandCount > 0) {
61
+ return unresolved(runId, "unknown", true, "Workflow host commands have no process-terminal proof.");
62
+ }
63
+ const evidence = readWorkflowChildProcessEvidence(asyncDir, steps);
64
+ if (evidence.state !== "observed") return unresolved(runId, evidence.state, true, evidence.reason);
65
+ const observedAt = Math.max(closedAt, ...evidence.children.map((child) => child.state === "observed" ? child.observedAt : 0));
66
+ return { version: 1, kind: "workflow", runId, state: "observed", dispatchClosed: true, observedAt, children: evidence.children };
67
+ }
@@ -71,7 +71,7 @@ import { deriveChildSessionName } from "../../shared/child-session-name.ts";
71
71
  import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
72
72
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
73
73
  import { assertThinkingWithinCeiling, intersectThinkingCeilings } from "../../shared/thinking-ceiling.ts";
74
- import { MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, MISSING_STRUCTURED_OUTPUT_CALL_ERROR } from "../shared/structured-output.ts";
74
+ import { formatStructuredOutputRejectionError, MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR, MISSING_STRUCTURED_OUTPUT_CALL_ERROR } from "../shared/structured-output.ts";
75
75
  import { formatMidToolExitError, isOrdinaryToolForMidToolExit } from "../shared/process-signal.ts";
76
76
  import { formatChildToolDiagnostic } from "../shared/tool-availability.ts";
77
77
  import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
@@ -1431,10 +1431,14 @@ async function runSingleAttempt(
1431
1431
  result.structuredOutputSchemaPath = options.structuredOutput.schemaPath;
1432
1432
  result.structuredOutputPath = options.structuredOutput.outputPath;
1433
1433
  const structured = capture.structuredOutput();
1434
- if (!structuredOutputToolInvoked || !structured.called) {
1434
+ if (!structuredOutputToolInvoked) {
1435
1435
  result.exitCode = 1;
1436
1436
  result.error = MISSING_STRUCTURED_OUTPUT_CALL_ERROR;
1437
1437
  result.structuredOutputFailed = true;
1438
+ } else if (!structured.called) {
1439
+ result.exitCode = 1;
1440
+ result.error = formatStructuredOutputRejectionError(result.messages ?? []);
1441
+ result.structuredOutputFailed = true;
1438
1442
  } else {
1439
1443
  result.structuredOutput = structured.value;
1440
1444
  const acceptanceMode = options.structuredOutput.acceptanceReportPath
@@ -30,6 +30,17 @@ import {
30
30
  } from "./capability-ceiling.ts";
31
31
 
32
32
  const MAX_LAUNCH_RESOLVED_EXTENSION_IDS = 32;
33
+ export const CAPABILITY_GATEWAY_EXTENSION_PATH = path.join(
34
+ path.dirname(fileURLToPath(import.meta.url)),
35
+ "..",
36
+ "..",
37
+ "..",
38
+ "..",
39
+ "capability-gateway",
40
+ "index.ts",
41
+ );
42
+ const CAPABILITY_GATEWAY_TOOL_NAMES = ["capability_catalog", "capability_discover", "capability_skill_show"] as const;
43
+ const CAPABILITY_GATEWAY_ENABLED_ENV = "SELESAI_CAPABILITY_GATEWAY";
33
44
  const PROMPT_RUNTIME_EXTENSION_PATH = path.join(
34
45
  path.dirname(fileURLToPath(import.meta.url)),
35
46
  "subagent-prompt-runtime.ts",
@@ -172,6 +183,8 @@ export interface PiLaunchToolPlan {
172
183
  extensionArgs: string[];
173
184
  disableAmbientExtensions: boolean;
174
185
  capabilityAudit?: SubagentCapabilityAudit;
186
+ /** Whether the capability gateway is loaded for this child's permitted extension/tool policy. */
187
+ capabilityGatewayEnabled: boolean;
175
188
  /** Non-fatal launch warnings; they do not change behavior. */
176
189
  warnings: string[];
177
190
  }
@@ -195,6 +208,14 @@ function hasPermissionRules(rules: PermissionRules | undefined): boolean {
195
208
  return rules !== undefined && Object.keys(rules).length > 0;
196
209
  }
197
210
 
211
+ function isCapabilityGatewayExtensionPath(value: string, cwd?: string): boolean {
212
+ const resolved = path.normalize(path.resolve(cwd ?? process.cwd(), value));
213
+ return (
214
+ resolved === path.normalize(CAPABILITY_GATEWAY_EXTENSION_PATH) ||
215
+ resolved === path.normalize(path.dirname(CAPABILITY_GATEWAY_EXTENSION_PATH))
216
+ );
217
+ }
218
+
198
219
  /**
199
220
  * Children are pi sessions inside the parent or the runner process; a spawned
200
221
  * `pi` received extra MCP server definitions as a CLI argument, but a session
@@ -330,6 +351,9 @@ export function resolvePiLaunchToolPlan(
330
351
  !excludedToolSet.has("subagent") &&
331
352
  (!allowedToolSet || allowedToolSet.has("subagent"))
332
353
  );
354
+ if (effectiveDeclaredBuiltinTools.includes("subagent_supervisor") && !fanoutAuthorized) {
355
+ throw new Error("Tool 'subagent_supervisor' requires fanout authorization: include 'subagent' in the effective tools allowlist or enable allowNestedSubagents.");
356
+ }
333
357
  const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions
334
358
  ? []
335
359
  : (input.tools ?? []).filter(
@@ -363,13 +387,41 @@ export function resolvePiLaunchToolPlan(
363
387
  (input.mcpDirectTools?.length ?? 0) > 0 ||
364
388
  allowedToolSet !== undefined;
365
389
  const internalTools = (input.structuredOutput ? ["structured_output"] : []).filter((tool) => !excludedToolSet.has(tool));
366
- const effectiveToolAllowlist = [
390
+ const baseEffectiveToolAllowlist = [
367
391
  ...new Set([
368
392
  ...effectiveDeclaredBuiltinTools,
369
393
  ...effectiveMcpTools,
370
394
  ...internalTools,
371
395
  ]),
372
396
  ];
397
+ const explicitlyNoTools =
398
+ explicitToolAllowlist &&
399
+ effectiveDeclaredBuiltinTools.length === 0 &&
400
+ effectiveMcpTools.length === 0;
401
+ const gatewayExplicitlyConfigured = [
402
+ ...toolExtensionPaths,
403
+ ...(input.extensions ?? []),
404
+ ...(input.subagentOnlyExtensions ?? []),
405
+ ].some((extensionPath) => isCapabilityGatewayExtensionPath(extensionPath, input.cwd));
406
+ const gatewayExtensionPolicyAllows =
407
+ capabilityCeiling?.denyExtensions !== true &&
408
+ fs.existsSync(CAPABILITY_GATEWAY_EXTENSION_PATH) &&
409
+ (input.extensions === undefined || gatewayExplicitlyConfigured);
410
+ const permittedGatewayTools = CAPABILITY_GATEWAY_TOOL_NAMES.filter(
411
+ (name) => (!allowedToolSet || allowedToolSet.has(name)) && !excludedToolSet.has(name),
412
+ );
413
+ const capabilityGatewayEnabled =
414
+ gatewayExtensionPolicyAllows &&
415
+ !explicitlyNoTools &&
416
+ permittedGatewayTools.length > 0 &&
417
+ process.env[CAPABILITY_GATEWAY_ENABLED_ENV] !== "0";
418
+ const gatewayTools = capabilityGatewayEnabled ? permittedGatewayTools : [];
419
+ const effectiveToolAllowlist = [
420
+ ...new Set([
421
+ ...baseEffectiveToolAllowlist,
422
+ ...(explicitToolAllowlist ? gatewayTools : []),
423
+ ]),
424
+ ];
373
425
  // Supervisor-coordination names stay in the --tools allowlist but are never
374
426
  // strict requirements: children register contact_supervisor at runtime through
375
427
  // the native supervisor channel (or pi-intercom). The pre-0.50 bridge always
@@ -383,6 +435,7 @@ export function resolvePiLaunchToolPlan(
383
435
  ...(input.tools !== undefined ? effectiveDeclaredBuiltinTools : []),
384
436
  ...(input.mcpDirectTools?.length ? effectiveMcpTools : []),
385
437
  ...internalTools,
438
+ ...gatewayTools,
386
439
  ].filter((tool) => tool !== "contact_supervisor" && (!legacySupervisorPairing || tool !== "intercom"))),
387
440
  ]
388
441
  : [];
@@ -395,6 +448,9 @@ export function resolvePiLaunchToolPlan(
395
448
  const fastModeExtensions = resolveFastModeExtension({ fast: input.fast, model: input.model, modelCandidates: input.modelCandidates, agentName: input.agentName });
396
449
  const runtimeExtensions = [
397
450
  PROMPT_RUNTIME_EXTENSION_PATH,
451
+ ...(capabilityGatewayEnabled && !gatewayExplicitlyConfigured
452
+ ? [CAPABILITY_GATEWAY_EXTENSION_PATH]
453
+ : []),
398
454
  ...fastModeExtensions,
399
455
  ...(fanoutAuthorized ? [FANOUT_CHILD_EXTENSION_PATH] : []),
400
456
  ...(permSystemExt ? [permSystemExt] : []),
@@ -412,20 +468,25 @@ export function resolvePiLaunchToolPlan(
412
468
  + "List the extensions this child actually needs instead of an empty array.",
413
469
  );
414
470
  }
471
+ const gatewayFilteredExtensions = (extensions: string[]) =>
472
+ extensions.filter(
473
+ (extensionPath) =>
474
+ !isCapabilityGatewayExtensionPath(extensionPath, input.cwd) || capabilityGatewayEnabled,
475
+ );
415
476
  const configuredExtensions = capabilityCeiling?.denyExtensions
416
477
  ? []
417
- : [
478
+ : gatewayFilteredExtensions([
418
479
  ...toolExtensionPaths,
419
480
  ...(input.extensions ?? []),
420
481
  ...(input.subagentOnlyExtensions ?? []),
421
- ];
482
+ ]);
422
483
  const extensionArgs = disableAmbientExtensions
423
484
  ? [...new Set([...runtimeExtensions, ...configuredExtensions])]
424
485
  : [
425
486
  ...new Set([
426
487
  ...runtimeExtensions,
427
- ...toolExtensionPaths,
428
- ...(input.subagentOnlyExtensions ?? []),
488
+ ...gatewayFilteredExtensions(toolExtensionPaths),
489
+ ...gatewayFilteredExtensions(input.subagentOnlyExtensions ?? []),
429
490
  ]),
430
491
  ];
431
492
  const requestedToolNames =
@@ -494,6 +555,7 @@ export function resolvePiLaunchToolPlan(
494
555
  configuredExtensions,
495
556
  extensionArgs,
496
557
  disableAmbientExtensions,
558
+ capabilityGatewayEnabled,
497
559
  warnings,
498
560
  ...(capabilityAudit ? { capabilityAudit } : {}),
499
561
  };
@@ -17,6 +17,12 @@ const READ_ONLY_BUILTIN_TOOLS = new Set([
17
17
  "intercom",
18
18
  "contact_supervisor",
19
19
  "structured_output",
20
+ // Gateway controls only search, activate, or load capabilities; activation stays
21
+ // filtered by the child's own tool policy, so they are not mutation-capable.
22
+ // Kept literal: child-tool-plan transitively imports this module (via single-output).
23
+ "capability_catalog",
24
+ "capability_discover",
25
+ "capability_skill_show",
20
26
  ]);
21
27
 
22
28
  // Cursor native edit/write often land as thinking traces (inactive_trace /
@@ -6,6 +6,7 @@ import { finished } from "node:stream/promises";
6
6
  import type { ExternalProcessStatus } from "../../shared/types.ts";
7
7
  import { createOwnedProcessTreeController, type OwnedProcessTreeController } from "../background/owned-process-tree.ts";
8
8
  import { omitExtensionBindingsEnv } from "./extension-bindings.ts";
9
+ import { omitGitRoutingEnv } from "./git-environment.ts";
9
10
  import {
10
11
  invalidateExternalCliPreflight,
11
12
  preflightExternalCli,
@@ -86,7 +87,7 @@ function narrowLimit(value: number | undefined, ceiling: number, label: string):
86
87
  }
87
88
 
88
89
  function externalEnvironment(allowlist: readonly string[] | undefined, values: Readonly<Record<string, string>> | undefined): NodeJS.ProcessEnv {
89
- if (!allowlist) return omitExtensionBindingsEnv(process.env);
90
+ if (!allowlist) return omitGitRoutingEnv(omitExtensionBindingsEnv(process.env));
90
91
  const allowed = new Set(allowlist);
91
92
  const env: NodeJS.ProcessEnv = {};
92
93
  for (const key of allowed) {
@@ -0,0 +1,29 @@
1
+ // ponytail: this static list can lag Git additions; refresh from `git rev-parse --local-env-vars` and indexed config keys when Git changes.
2
+ const GIT_ROUTING_VARIABLES = new Set([
3
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
4
+ "GIT_COMMON_DIR",
5
+ "GIT_CONFIG",
6
+ "GIT_CONFIG_COUNT",
7
+ "GIT_CONFIG_PARAMETERS",
8
+ "GIT_DIR",
9
+ "GIT_GRAFT_FILE",
10
+ "GIT_IMPLICIT_WORK_TREE",
11
+ "GIT_INDEX_FILE",
12
+ "GIT_NAMESPACE",
13
+ "GIT_NO_REPLACE_OBJECTS",
14
+ "GIT_OBJECT_DIRECTORY",
15
+ "GIT_PREFIX",
16
+ "GIT_REPLACE_REF_BASE",
17
+ "GIT_SHALLOW_FILE",
18
+ "GIT_WORK_TREE",
19
+ ]);
20
+
21
+ function isGitRoutingVariable(name: string): boolean {
22
+ const upper = name.toUpperCase();
23
+ return GIT_ROUTING_VARIABLES.has(upper) || /^GIT_CONFIG_(?:KEY|VALUE)_\d+$/.test(upper);
24
+ }
25
+
26
+ /** Remove inherited values that can route Git commands away from the child cwd. */
27
+ export function omitGitRoutingEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
28
+ return Object.fromEntries(Object.entries(env).filter(([name]) => !isGitRoutingVariable(name)));
29
+ }
@@ -3,12 +3,81 @@ import { createRequire } from "node:module";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
+ import type { Message } from "@earendil-works/pi-ai";
6
7
  import { SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts";
7
8
  import type { JsonSchemaObject } from "../../shared/types.ts";
8
9
  import type { ResolvedAcceptanceReportMode } from "./acceptance.ts";
9
10
 
10
11
  export const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output.";
11
12
  export const MISSING_STRUCTURED_ACCEPTANCE_REPORT_ERROR = "Missing acceptanceReport in structured_output call; acceptance.report is \"on\".";
13
+ export const STRUCTURED_OUTPUT_REJECTION_ERROR = "structured_output was invoked but no valid output was captured.";
14
+ export const INVALID_STRUCTURED_OUTPUT_SCHEMA_ERROR = "Structured output invocation was rejected: invalid outputSchema.";
15
+ export const STRUCTURED_OUTPUT_VALIDATOR_UNAVAILABLE_ERROR = "Structured output invocation was rejected: validator unavailable.";
16
+ export const MAX_STRUCTURED_OUTPUT_REJECTION_ERROR_BYTES = 4096;
17
+
18
+ function utf8Prefix(value: string, maxBytes: number): string {
19
+ if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
20
+ const suffix = "...";
21
+ const contentLimit = maxBytes - Buffer.byteLength(suffix, "utf8");
22
+ let bytes = 0;
23
+ let result = "";
24
+ for (const character of value) {
25
+ const characterBytes = Buffer.byteLength(character, "utf8");
26
+ if (bytes + characterBytes > contentLimit) break;
27
+ result += character;
28
+ bytes += characterBytes;
29
+ }
30
+ return result + suffix;
31
+ }
32
+
33
+ function messageText(content: unknown): string {
34
+ if (typeof content === "string") return content;
35
+ if (!Array.isArray(content)) return "";
36
+ return content
37
+ .filter((part): part is { type: "text"; text: string } => Boolean(part) && typeof part === "object" && (part as { type?: unknown }).type === "text" && typeof (part as { text?: unknown }).text === "string")
38
+ .map((part) => part.text)
39
+ .join("\n");
40
+ }
41
+
42
+ function sanitizeStructuredOutputRejection(text: string): string {
43
+ const withoutControls = text.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
44
+ if (/invalid outputSchema(?:\s*:|$)/i.test(withoutControls)) return INVALID_STRUCTURED_OUTPUT_SCHEMA_ERROR;
45
+ if (/failed to validate structured output:|cannot load typebox\/compile/i.test(withoutControls)) return STRUCTURED_OUTPUT_VALIDATOR_UNAVAILABLE_ERROR;
46
+ const validationMarker = "Structured output validation failed:";
47
+ const markerIndex = withoutControls.indexOf(validationMarker);
48
+ const diagnostic = markerIndex >= 0 ? withoutControls.slice(markerIndex) : withoutControls;
49
+ const lines = diagnostic.split(/\r?\n/).filter((line) => {
50
+ const trimmed = line.trim();
51
+ return trimmed.length > 0
52
+ && !/^at\s/.test(trimmed)
53
+ && !/^(?:arguments?|parameters?|submitted value|input|outputSchema|schema|stack)\s*:/i.test(trimmed);
54
+ });
55
+ const sanitized = lines.join("\n")
56
+ .replace(/\b(received|actual|got)(?:\s+value)?\s*[:=]\s*(?:\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*'|\{[^\n]*\}|\[[^\n]*\]|\S+)/gi, "$1: [redacted]")
57
+ .trim();
58
+ return sanitized || STRUCTURED_OUTPUT_REJECTION_ERROR;
59
+ }
60
+
61
+ /** Returns bounded evidence from the latest failed structured_output result in a terminal transcript. */
62
+ export function formatStructuredOutputRejectionError(messages: readonly Message[]): string {
63
+ const toolCallIds = new Set<string>();
64
+ for (const message of messages) {
65
+ if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
66
+ for (const part of message.content) {
67
+ const call = part as { type?: unknown; id?: unknown; name?: unknown };
68
+ if (call.type === "toolCall" && call.name === "structured_output" && typeof call.id === "string") toolCallIds.add(call.id);
69
+ }
70
+ }
71
+ for (let index = messages.length - 1; index >= 0; index--) {
72
+ const message = messages[index] as Message & { toolCallId?: unknown; toolName?: unknown; isError?: unknown };
73
+ if (message.role !== "toolResult" || message.isError !== true) continue;
74
+ const matchingName = message.toolName === "structured_output";
75
+ const matchingId = typeof message.toolCallId === "string" && toolCallIds.has(message.toolCallId);
76
+ if (!matchingName && !matchingId) continue;
77
+ return utf8Prefix(sanitizeStructuredOutputRejection(messageText(message.content)), MAX_STRUCTURED_OUTPUT_REJECTION_ERROR_BYTES);
78
+ }
79
+ return STRUCTURED_OUTPUT_REJECTION_ERROR;
80
+ }
12
81
 
13
82
  export interface StructuredOutputRuntime {
14
83
  schema: JsonSchemaObject;
@@ -704,6 +704,25 @@ export type ProcessTerminal =
704
704
  diagnostic?: string;
705
705
  });
706
706
 
707
+ export type WorkflowTerminalProof =
708
+ | {
709
+ version: 1;
710
+ kind: "workflow";
711
+ runId: string;
712
+ state: "observed";
713
+ dispatchClosed: true;
714
+ observedAt: number;
715
+ children: ProcessTerminal[];
716
+ }
717
+ | {
718
+ version: 1;
719
+ kind: "workflow";
720
+ runId: string;
721
+ state: "pending" | "unknown";
722
+ dispatchClosed: boolean;
723
+ reason: string;
724
+ };
725
+
707
726
  /** Identifies the durable schedule that launched a run, so its completion is attributable. */
708
727
  export interface ScheduleOrigin {
709
728
  id: string;
@@ -1398,6 +1417,7 @@ export interface Details {
1398
1417
  context?: "fresh" | "fork" | "mixed";
1399
1418
  results: SingleResult[];
1400
1419
  workflowChildren?: WorkflowChildSummary;
1420
+ workflowTerminalProof?: WorkflowTerminalProof;
1401
1421
  /**
1402
1422
  * Terminal completion payloads for runs this bg_wait call observed
1403
1423
  * finishing. Async completions travel as result files that are consumed and