@selesai/code 0.5.29 → 0.6.2

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 (94) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +1 -1
  3. package/dist/config.d.ts +16 -3
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +106 -5
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/system-prompt.d.ts.map +1 -1
  8. package/dist/core/system-prompt.js +18 -0
  9. package/dist/core/system-prompt.js.map +1 -1
  10. package/dist/core/system-prompt.test.d.ts +2 -0
  11. package/dist/core/system-prompt.test.d.ts.map +1 -0
  12. package/dist/core/system-prompt.test.js +89 -0
  13. package/dist/core/system-prompt.test.js.map +1 -0
  14. package/dist/defaults/models.json +13 -45
  15. package/dist/defaults/settings.json +5 -7
  16. package/dist/extensions/copy-turn.test.ts +131 -0
  17. package/dist/extensions/copy-turn.ts +6 -1
  18. package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  19. package/dist/extensions/package.json +0 -1
  20. package/dist/extensions/pi-subagents/CHANGELOG.md +3 -0
  21. package/dist/extensions/pi-subagents/README.md +27 -32
  22. package/dist/extensions/pi-subagents/agents/architect.md +4 -4
  23. package/dist/extensions/pi-subagents/agents/builder.md +5 -4
  24. package/dist/extensions/pi-subagents/agents/commentator.md +3 -2
  25. package/dist/extensions/pi-subagents/agents/explorer.md +3 -2
  26. package/dist/extensions/pi-subagents/agents/recapper.md +3 -2
  27. package/dist/extensions/pi-subagents/agents/researcher.md +4 -3
  28. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -0
  29. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +10 -9
  30. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +12 -11
  31. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +10 -11
  32. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +56 -9
  33. package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +125 -0
  34. package/dist/extensions/pi-subagents/src/api/preflight.ts +1 -1
  35. package/dist/extensions/pi-subagents/src/extension/index.ts +5 -1
  36. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  37. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +24 -7
  38. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +23 -5
  39. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +27 -1
  40. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +64 -6
  41. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +16 -1
  42. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +72 -18
  43. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +19 -5
  44. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +127 -31
  45. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +4 -6
  46. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +63 -9
  47. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -0
  48. package/dist/extensions/pi-subagents/src/shared/types.ts +41 -2
  49. package/dist/extensions/pi-subagents/src/shared/utils.ts +29 -1
  50. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +5 -1
  51. package/dist/extensions/pi-subagents/src/tui/render.ts +32 -6
  52. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +111 -6
  53. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +74 -43
  54. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +36 -21
  55. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +5 -3
  56. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -8
  57. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +14 -7
  58. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +227 -0
  59. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +81 -5
  60. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +49 -10
  61. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +18 -2
  62. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
  63. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -6
  64. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +161 -1
  65. package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +63 -0
  66. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +34 -0
  67. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +24 -0
  68. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +6 -1
  69. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +29 -0
  70. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +2 -0
  71. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +12 -0
  72. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +91 -1
  73. package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +213 -0
  74. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +23 -1
  75. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +60 -9
  76. package/dist/extensions/pi-web-agent/package.json +1 -1
  77. package/dist/skills/pi-subagents/SKILL.md +43 -0
  78. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +257 -0
  79. package/dist/skills/pi-subagents/references/execution-controls.md +431 -0
  80. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +144 -0
  81. package/dist/skills/pi-subagents/references/prompting-and-roles.md +281 -0
  82. package/dist/skills/ponytail/SKILL.md +1 -3
  83. package/docs/plans/subagent-delegation/phase-0-correctness.md +265 -0
  84. package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +486 -0
  85. package/docs/plans/subagent-delegation/phase-2-context-controls.md +282 -0
  86. package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +362 -0
  87. package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +381 -0
  88. package/package.json +2 -2
  89. package/dist/extensions/caveman/caveman-instructions.cjs +0 -11
  90. package/dist/extensions/caveman/index.js +0 -118
  91. package/dist/extensions/caveman/package.json +0 -8
  92. package/dist/extensions/caveman/test/extension.test.js +0 -203
  93. package/dist/extensions/caveman/test/helpers.test.js +0 -58
  94. package/dist/skills/caveman/SKILL.md +0 -50
@@ -25,7 +25,6 @@ import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/
25
25
  import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts";
26
26
  import { buildModelCandidates, normalizeParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, type ParentModel } from "../shared/model-fallback.ts";
27
27
  import type { ModelScopeConfig } from "../shared/model-scope.ts";
28
- import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
29
28
  import { recordRun } from "../shared/run-history.ts";
30
29
  import {
31
30
  buildChainInstructions,
@@ -57,7 +56,7 @@ import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
57
56
  import { usageBudgetExceededMessage, usageBudgetState, validateUsageBudgetConfig } from "../shared/usage-budget.ts";
58
57
  import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
59
58
  import { isAgentContractV1 } from "../shared/agent-contract.ts";
60
- import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
59
+ import { finalizeSingleOutput, formatBoundedPersistenceFallback, formatSavedOutputReference, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
61
60
  import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-output.ts";
62
61
  import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
63
62
  import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
@@ -521,7 +520,12 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
521
520
  updatedAt,
522
521
  ...(input.result.exitCode !== undefined ? { exitCode: input.result.exitCode } : {}),
523
522
  ...(input.result.error ? { error: input.result.error } : {}),
524
- ...(input.result.finalOutput ? { finalOutput: input.result.finalOutput } : {}),
523
+ // Reference-first remembered state: when an authoritative saved output path
524
+ // exists, do not retain the full final output in memory; status/transcript
525
+ // views recover it from `savedOutputPath` via `rememberedForegroundChildOutput`
526
+ // (runs/background/run-status.ts) and the TUI via `resultOutputForUi`
527
+ // (tui/render.ts).
528
+ ...(input.result.finalOutput && !input.result.savedOutputPath ? { finalOutput: input.result.finalOutput } : {}),
525
529
  outputState: input.result.outputState,
526
530
  outputMode: input.result.outputMode,
527
531
  savedOutputPath: input.result.savedOutputPath,
@@ -541,10 +545,11 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
541
545
  ...(input.result.capabilityAudit ? { capabilityAudit: input.result.capabilityAudit } : {}),
542
546
  };
543
547
  trimRememberedForegroundRuns(state);
544
- const output = getSingleResultOutput(input.result).trim();
548
+ const reference = input.result.outputReference?.message;
549
+ const output = reference ?? getSingleResultOutput(input.result).trim();
545
550
  const success = terminalStatus === "completed";
546
551
  const summary = !success && input.result.error
547
- ? `${input.result.error}${output ? `\n\nOutput:\n${output}` : ""}`
552
+ ? `${input.result.error}${output ? `\n\n${output}` : ""}`
548
553
  : output || input.result.error || "Detached child exited without final output.";
549
554
  // A detached callback may outlive its extension runtime. Stale sessions are
550
555
  // intentionally dropped rather than routed through a replacement runtime.
@@ -1288,7 +1293,7 @@ async function resumeAsyncRun(input: {
1288
1293
  };
1289
1294
  }
1290
1295
  const runId = randomUUID().slice(0, 8);
1291
- const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
1296
+ const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts === true, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
1292
1297
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1293
1298
  const contextPolicy = resolveExplicitContextPolicy(input.params);
1294
1299
  const workflowTask = (input.params.task ?? followUp) || undefined;
@@ -1354,7 +1359,7 @@ async function resumeAsyncRun(input: {
1354
1359
 
1355
1360
  const runId = randomUUID().slice(0, 8);
1356
1361
  const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(agentConfig, recoveryDescriptor) : agentConfig;
1357
- const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
1362
+ const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts === true, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir };
1358
1363
  const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
1359
1364
  const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
1360
1365
  const parentModel = input.parentModel;
@@ -1435,14 +1440,82 @@ async function resumeAsyncRun(input: {
1435
1440
  }
1436
1441
 
1437
1442
  function resultSummaryForIntercom(result: SingleResult): string {
1438
- const output = getSingleResultOutput(result);
1439
- if (result.exitCode !== 0 && result.error) {
1440
- return output ? `${result.error}\n\nOutput:\n${output}` : result.error;
1443
+ const reference = result.outputReference?.message;
1444
+ if (reference) {
1445
+ return result.exitCode !== 0 && result.error ? `${result.error}\n\n${reference}` : reference;
1441
1446
  }
1442
- return output || result.error || "(no output)";
1447
+ if (result.savedOutputPath) {
1448
+ try {
1449
+ const ref = formatSavedOutputReference(result.savedOutputPath, fs.readFileSync(result.savedOutputPath, "utf-8"));
1450
+ return result.exitCode !== 0 && result.error ? `${result.error}\n\n${ref.message}` : ref.message;
1451
+ } catch (error) {
1452
+ const fallback = formatBoundedPersistenceFallback({
1453
+ error: `Failed to read saved output: ${error instanceof Error ? error.message : String(error)}`,
1454
+ outputPath: result.savedOutputPath,
1455
+ fullOutput: getSingleResultOutput(result),
1456
+ exitCode: result.exitCode,
1457
+ processError: result.error,
1458
+ });
1459
+ return result.exitCode !== 0 && result.error ? `${result.error}\n\n${fallback}` : fallback;
1460
+ }
1461
+ }
1462
+ if (result.outputSaveError) {
1463
+ return formatBoundedPersistenceFallback({
1464
+ error: result.outputSaveError,
1465
+ outputPath: result.savedOutputPath,
1466
+ fullOutput: getSingleResultOutput(result),
1467
+ exitCode: result.exitCode,
1468
+ processError: result.error,
1469
+ });
1470
+ }
1471
+ // Legacy / `output: false` child without a durable saved path: bounded fallback
1472
+ // only, never the raw full output.
1473
+ return formatBoundedPersistenceFallback({
1474
+ error: "No saved output path is available for this child.",
1475
+ fullOutput: getSingleResultOutput(result),
1476
+ exitCode: result.exitCode,
1477
+ processError: result.error,
1478
+ });
1479
+ }
1480
+
1481
+ /**
1482
+ * Compact terminal per-child line for bridge-off foreground parallel content:
1483
+ * status plus either the saved-output reference (default/file-only), the legacy
1484
+ * inline full output when explicitly requested, or the bounded fallback.
1485
+ */
1486
+ function formatParallelChildResult(result: SingleResult, index: number): string {
1487
+ const status = resolveSubagentResultStatus({
1488
+ exitCode: result.exitCode,
1489
+ interrupted: result.interrupted,
1490
+ detached: result.detached,
1491
+ processSignal: result.processSignal,
1492
+ timedOut: result.timedOut,
1493
+ stopped: result.stopped,
1494
+ turnBudgetExceeded: result.turnBudgetExceeded,
1495
+ });
1496
+ const header = `=== Task ${index + 1}: ${result.agent} [${status}] ===`;
1497
+ if (result.outputMode === "inline") {
1498
+ const output = getSingleResultOutput(result).trim();
1499
+ return `${header}\n${output || "(no output)"}`;
1500
+ }
1501
+ const reference = result.outputReference?.message;
1502
+ if (reference) {
1503
+ const body = result.exitCode !== 0 && result.error ? `${result.error}\n${reference}` : reference;
1504
+ return `${header}\n${body}`;
1505
+ }
1506
+ return `${header}\n${formatBoundedPersistenceFallback({
1507
+ error: result.outputSaveError ?? "No saved output path is available for this child.",
1508
+ outputPath: result.savedOutputPath,
1509
+ fullOutput: getSingleResultOutput(result),
1510
+ exitCode: result.exitCode,
1511
+ processError: result.error,
1512
+ })}`;
1443
1513
  }
1444
1514
 
1445
1515
  function formatFailedSingleRunOutput(result: SingleResult, displayOutput: string): string {
1516
+ // Reference-first and persistence-failure fallbacks already carry error/status
1517
+ // and must not be re-wrapped as raw "Output:" text.
1518
+ if (result.savedOutputPath || result.outputSaveError) return displayOutput;
1446
1519
  const error = result.error || "Failed";
1447
1520
  const output = displayOutput.trim();
1448
1521
  const lines = [error];
@@ -2287,7 +2360,6 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2287
2360
  }
2288
2361
  const rawOutput = params.output !== undefined ? params.output : a.output;
2289
2362
  const effectiveOutput = normalizeSingleOutputOverride(rawOutput, a.output);
2290
- const effectiveOutputMode = params.outputMode ?? "inline";
2291
2363
  const normalizedSkills = normalizeSkillInput(params.skill);
2292
2364
  const skills = normalizedSkills === false ? [] : normalizedSkills;
2293
2365
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
@@ -2309,7 +2381,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
2309
2381
  context: contextPolicy.contextForAgent(params.agent!),
2310
2382
  skills,
2311
2383
  output: effectiveOutput,
2312
- outputMode: effectiveOutputMode,
2384
+ outputMode: params.outputMode,
2313
2385
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
2314
2386
  modelOverride,
2315
2387
  thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
@@ -2610,6 +2682,19 @@ function resolveSingleRunOutputBaseDir(deps: ExecutorDeps, artifactsDir: string,
2610
2682
  : path.join(artifactsDir, "outputs", runId);
2611
2683
  }
2612
2684
 
2685
+ /**
2686
+ * Generated per-run durable output path for foreground single runs when the
2687
+ * caller omits `output` (and does not set `output: false`):
2688
+ * `<singleRunOutputBaseDir>/<runId>/result.md`. The base is the configured
2689
+ * `singleRunOutputBaseDir` when present, otherwise `{artifactsDir}/outputs`.
2690
+ */
2691
+ function resolveGeneratedSingleOutputPath(deps: ExecutorDeps, artifactsDir: string, runId: string): string {
2692
+ const base = deps.config.singleRunOutputBaseDir
2693
+ ? path.resolve(deps.expandTilde(deps.config.singleRunOutputBaseDir))
2694
+ : path.join(artifactsDir, "outputs");
2695
+ return path.join(base, runId, "result.md");
2696
+ }
2697
+
2613
2698
  function buildChainWorktreeTaskCwdError(chain: ChainStep[], sharedCwd: string): string | undefined {
2614
2699
  for (let stepIndex = 0; stepIndex < chain.length; stepIndex++) {
2615
2700
  const step = chain[stepIndex]!;
@@ -3068,8 +3153,19 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3068
3153
 
3069
3154
  const behaviors = tasks.map((task, index) => {
3070
3155
  let behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfigs[index]!, behaviorOverrides[index]!), taskTexts[index]);
3071
- if (behaviorOverrides[index]?.output === undefined && typeof behavior.output === "string" && !path.isAbsolute(behavior.output)) {
3072
- behavior = { ...behavior, output: path.join("parallel-0", `${index}-${task.agent}`, behavior.output) };
3156
+ if (behaviorOverrides[index]?.output === undefined) {
3157
+ if (typeof behavior.output === "string" && !path.isAbsolute(behavior.output)) {
3158
+ behavior = { ...behavior, output: path.join("parallel-0", `${index}-${task.agent}`, behavior.output) };
3159
+ } else if (behavior.output === false && behaviorOverrides[index]?.output !== false && behaviorOverrides[index]?.output !== "false") {
3160
+ // Omitted output: generate a collision-safe per-task durable path in the
3161
+ // existing per-task namespace (parallel-0/<index>-<agent>/result.md).
3162
+ behavior = { ...behavior, output: path.join("parallel-0", `${index}-${task.agent}`, "result.md") };
3163
+ }
3164
+ }
3165
+ // Omitted outputMode resolves to file-only whenever an output path is active;
3166
+ // explicit `outputMode: "inline"` is preserved exactly.
3167
+ if (behaviorOverrides[index]?.outputMode === undefined && typeof behavior.output === "string") {
3168
+ behavior = { ...behavior, outputMode: "file-only" };
3073
3169
  }
3074
3170
  return behavior;
3075
3171
  });
@@ -3235,21 +3331,12 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
3235
3331
  const worktreeSuffix = handoff?.suffix ?? "";
3236
3332
  const ok = results.filter((result) => result.exitCode === 0).length;
3237
3333
  const downgradeNote = backgroundRequestedWhileClarifying ? " (background requested, but clarify kept this run foreground)" : "";
3238
- const aggregatedOutput = aggregateParallelOutputs(
3239
- results.map((result) => ({
3240
- agent: result.agent,
3241
- output: result.truncation?.text || getSingleResultOutput(result),
3242
- exitCode: result.exitCode,
3243
- error: result.error,
3244
- timedOut: result.timedOut,
3245
- })),
3246
- (i, agent) => `=== Task ${i + 1}: ${agent} ===`,
3247
- );
3334
+ const perChildResults = results.map((result, i) => formatParallelChildResult(result, i)).join("\n\n");
3248
3335
 
3249
3336
  const summary = `${ok}/${results.length} succeeded${downgradeNote}`;
3250
3337
  const fullContent = worktreeSuffix
3251
- ? `${summary}\n\n${aggregatedOutput}\n\n${worktreeSuffix}`
3252
- : `${summary}\n\n${aggregatedOutput}`;
3338
+ ? `${summary}\n\n${perChildResults}\n\n${worktreeSuffix}`
3339
+ : `${summary}\n\n${perChildResults}`;
3253
3340
 
3254
3341
  return {
3255
3342
  content: [{ type: "text", text: fullContent }],
@@ -3309,7 +3396,6 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3309
3396
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
3310
3397
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
3311
3398
  let effectiveOutput = normalizeSingleOutputOverride(rawOutput, agentConfig.output);
3312
- const effectiveOutputMode = params.outputMode ?? "inline";
3313
3399
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
3314
3400
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, agentConfig.maxSubagentDepth);
3315
3401
 
@@ -3381,7 +3467,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3381
3467
  context: contextPolicy.contextForAgent(params.agent!),
3382
3468
  skills: skillOverride === false ? [] : skillOverride,
3383
3469
  output: effectiveOutput,
3384
- outputMode: effectiveOutputMode,
3470
+ outputMode: params.outputMode,
3385
3471
  outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id),
3386
3472
  modelOverride,
3387
3473
  thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride),
@@ -3410,7 +3496,16 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3410
3496
  task = wrapForkTask(task);
3411
3497
  }
3412
3498
  const cleanTask = task;
3413
- const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, effectiveCwd, resolveSingleRunOutputBaseDir(deps, artifactsDir, runId));
3499
+ // Omitted output resolves to a generated per-run durable path
3500
+ // (`<singleRunOutputBaseDir>/<runId>/result.md`); omitted outputMode resolves to
3501
+ // file-only whenever an output path is active. Explicit paths, `output: false`,
3502
+ // and explicit `outputMode: "inline"` are preserved exactly.
3503
+ const outputPath = typeof effectiveOutput === "string"
3504
+ ? resolveSingleOutputPath(effectiveOutput, ctx.cwd, effectiveCwd, resolveSingleRunOutputBaseDir(deps, artifactsDir, runId))
3505
+ : effectiveOutput === false
3506
+ ? undefined
3507
+ : resolveGeneratedSingleOutputPath(deps, artifactsDir, runId);
3508
+ const effectiveOutputMode = params.outputMode ?? (outputPath ? "file-only" : "inline");
3414
3509
  const validationError = validateFileOnlyOutputMode(effectiveOutputMode, outputPath, `Single run (${params.agent})`);
3415
3510
  if (validationError) {
3416
3511
  return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } };
@@ -3549,6 +3644,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
3549
3644
  savedPath: r.savedOutputPath,
3550
3645
  outputReference: r.outputReference,
3551
3646
  saveError: r.outputSaveError,
3647
+ error: r.error,
3552
3648
  });
3553
3649
  if (foregroundControl) {
3554
3650
  updateForegroundNestedProjection(foregroundControl);
@@ -4225,7 +4321,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
4225
4321
 
4226
4322
  const artifactConfig: ArtifactConfig = {
4227
4323
  ...DEFAULT_ARTIFACT_CONFIG,
4228
- enabled: effectiveParams.artifacts !== false,
4324
+ enabled: effectiveParams.artifacts === true,
4229
4325
  dir: deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir,
4230
4326
  };
4231
4327
  const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir);
@@ -22,7 +22,7 @@ import type {
22
22
  SubagentRunMode,
23
23
  } from "../../shared/types.ts";
24
24
  import { isAgentContractV1 } from "./agent-contract.ts";
25
- import { classifyTaskMutationIntent, taskMayMutate } from "./task-intent.ts";
25
+ import { classifyTaskMutationIntent, resolveAgentRoutingRole, taskMayMutate } from "./task-intent.ts";
26
26
 
27
27
  const LEVEL_RANK: Record<Exclude<AcceptanceLevel, "auto">, number> = {
28
28
  none: 0,
@@ -82,7 +82,6 @@ function inferLevel(input: {
82
82
  dynamic?: boolean;
83
83
  dynamicGroup?: boolean;
84
84
  }): { level: Exclude<AcceptanceLevel, "auto">; reasons: string[]; criteria: string[]; evidence: AcceptanceEvidenceKind[]; review?: { agent?: string; required?: boolean } } {
85
- const agent = input.agentName.toLowerCase();
86
85
  const task = input.task?.toLowerCase() ?? "";
87
86
  const reasons: string[] = [];
88
87
  // Declared roles replace name heuristics, so use the full writer grammar to detect explicit mutation independently of the actual agent name.
@@ -94,11 +93,10 @@ function inferLevel(input: {
94
93
  && !/\b(?:do not|don't|must not)\s+patch\b/.test(task)
95
94
  && /\bpatch\s+(?:(?:\.{0,2}[\\/])?(?:[\w.-]+[\\/])+[\w.-]+|[\w.-]+\.[a-z0-9]+\b|(?:the\s+)?parser\b)/.test(task);
96
95
  const taskMayWrite = readOnlyTask ? false : taskMayMutate(input.task ?? "") || intent.kind === "implementation" || rolePatchTask;
97
- const readOnlyAgent = input.acceptanceRole === "read-only"
98
- || (input.acceptanceRole === undefined && /\b(?:architect|commentator|explorer|recapper|researcher|analyst)\b/.test(agent));
96
+ const routingRole = resolveAgentRoutingRole(input.agentName, input.acceptanceRole);
97
+ const readOnlyAgent = routingRole === "read-only";
99
98
  const writeTask = taskMayWrite
100
- || (input.acceptanceRole === "writer" && !readOnlyTask)
101
- || (input.acceptanceRole === undefined && /\bbuilder\b/.test(agent) && !readOnlyTask);
99
+ || (routingRole === "writer" && !readOnlyTask);
102
100
  const inferredReadOnly = readOnlyTask || (input.acceptanceRole === "read-only" && !taskMayWrite);
103
101
  const roleResolvesReadOnly = input.acceptanceRole !== undefined && inferredReadOnly;
104
102
  const keywordRiskReadOnly = input.acceptanceRole === undefined ? intent.kind === "read-only" : inferredReadOnly;
@@ -1,9 +1,19 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { Message } from "@earendil-works/pi-ai";
4
- import type { OutputMode, SavedOutputReference } from "../../shared/types.ts";
4
+ import type { MaxOutputConfig, OutputMode, SavedOutputReference } from "../../shared/types.ts";
5
+ import { truncateOutput } from "../../shared/types.ts";
5
6
  import { hasMutationToolCapability } from "./completion-guard.ts";
6
7
 
8
+ /**
9
+ * Fixed internal context-fallback cap used when persisted output is unavailable
10
+ * (persistence/read failure, `output: false`, or legacy results without a saved
11
+ * path). Deliberately not exposed as a public parameter; reuse `truncateOutput()`
12
+ * so the excerpt is bounded by both lines and bytes and never falls back to
13
+ * unbounded output.
14
+ */
15
+ export const CONTEXT_FALLBACK_LIMIT: Required<MaxOutputConfig> = { bytes: 4096, lines: 80 };
16
+
7
17
  export interface SingleOutputSnapshot {
8
18
  exists: boolean;
9
19
  mtimeMs?: number;
@@ -125,6 +135,34 @@ function formatByteSize(bytes: number): string {
125
135
  return `${value.toFixed(1)} ${units[unitIndex]}`;
126
136
  }
127
137
 
138
+ /**
139
+ * Bounded fallback for deliveries where the durable full output cannot be
140
+ * referenced: persistence/write errors, unreadable saved paths, or no saved
141
+ * path at all (legacy/`output: false`). Contains process status, the intended
142
+ * output path, the concrete error, and a bounded 80-line/4-KiB excerpt of the
143
+ * available output. Never claims a full artifact exists.
144
+ */
145
+ export function formatBoundedPersistenceFallback(input: {
146
+ error: string;
147
+ outputPath?: string;
148
+ fullOutput: string;
149
+ exitCode: number;
150
+ processError?: string;
151
+ }): string {
152
+ const status = input.processError?.trim()
153
+ ? `Process status: ${input.processError}`
154
+ : `Process status: ${input.exitCode === 0 ? "completed" : `failed (exit ${input.exitCode})`}`;
155
+ const bounded = truncateOutput(input.fullOutput, CONTEXT_FALLBACK_LIMIT);
156
+ const lines = [
157
+ `[Full output unavailable] ${input.error}`,
158
+ status,
159
+ ...(input.outputPath ? [`Intended output path: ${input.outputPath}`] : []),
160
+ "Full output is unavailable; showing a bounded excerpt (first 80 lines / 4 KiB).",
161
+ bounded.text,
162
+ ];
163
+ return lines.join("\n");
164
+ }
165
+
128
166
  export function formatSavedOutputReference(savedPath: string, fullOutput: string): SavedOutputReference {
129
167
  const absolutePath = path.resolve(savedPath);
130
168
  const bytes = Buffer.byteLength(fullOutput, "utf-8");
@@ -217,19 +255,35 @@ export function finalizeSingleOutput(params: {
217
255
  savedPath?: string;
218
256
  outputReference?: SavedOutputReference;
219
257
  saveError?: string;
258
+ error?: string;
220
259
  }): { displayOutput: string; savedPath?: string; outputReference?: SavedOutputReference; saveError?: string } {
221
- let displayOutput = params.truncatedOutput || params.fullOutput;
222
- if (params.exitCode === 0 && params.savedPath) {
260
+ if (params.savedPath) {
223
261
  const outputReference = params.outputReference ?? formatSavedOutputReference(params.savedPath, params.fullOutput);
224
- if (params.outputMode === "file-only") {
262
+ if (params.exitCode === 0 && params.outputMode === "file-only") {
225
263
  return { displayOutput: outputReference.message, savedPath: params.savedPath, outputReference };
226
264
  }
227
- displayOutput += `\n\n${outputReference.message}`;
265
+ if (params.exitCode !== 0) {
266
+ // Failed runs with a successfully persisted result surface the error/status
267
+ // plus the saved-output reference, never raw child output.
268
+ const status = params.error?.trim() || `Subagent failed with exit code ${params.exitCode}`;
269
+ return { displayOutput: `${status}\n\n${outputReference.message}`, savedPath: params.savedPath, outputReference };
270
+ }
271
+ const displayOutput = `${params.truncatedOutput || params.fullOutput}\n\n${outputReference.message}`;
228
272
  return { displayOutput, savedPath: params.savedPath, outputReference };
229
273
  }
230
- if (params.exitCode === 0 && params.saveError && params.outputPath) {
231
- displayOutput += `\n\nOutput file error: ${params.outputPath}\n${params.saveError}`;
232
- return { displayOutput, saveError: params.saveError };
274
+ if (params.saveError && params.outputPath) {
275
+ // Persistence/read failure: only the bounded fallback is delivered. It must
276
+ // not claim a full artifact exists.
277
+ return {
278
+ displayOutput: formatBoundedPersistenceFallback({
279
+ error: params.saveError,
280
+ outputPath: params.outputPath,
281
+ fullOutput: params.fullOutput,
282
+ exitCode: params.exitCode,
283
+ processError: params.error,
284
+ }),
285
+ saveError: params.saveError,
286
+ };
233
287
  }
234
- return { displayOutput };
288
+ return { displayOutput: params.truncatedOutput || params.fullOutput };
235
289
  }
@@ -1,3 +1,5 @@
1
+ import type { AcceptanceRole } from "../../shared/types.ts";
2
+
1
3
  /**
2
4
  * Shared task mutation-intent classifier.
3
5
  *
@@ -139,6 +141,25 @@ function isReviewerStyleAgent(agent: string): boolean {
139
141
  return /\b(?:architect|commentator|explorer|recapper|researcher)\b/i.test(agent);
140
142
  }
141
143
 
144
+ /**
145
+ * Resolve an agent's acceptance routing role: a declared `acceptanceRole`
146
+ * always wins; otherwise the established name heuristics apply (builder-named
147
+ * agents are writers; architect/commentator/explorer/recapper/researcher/
148
+ * analyst-named agents are read-only). This is the single shared source of
149
+ * truth for acceptance inference and task-aware advisory routing so the two
150
+ * cannot drift. Alias values are never role signals.
151
+ */
152
+ export function resolveAgentRoutingRole(
153
+ agentName: string,
154
+ acceptanceRole?: AcceptanceRole,
155
+ ): "writer" | "read-only" | undefined {
156
+ if (acceptanceRole !== undefined) return acceptanceRole;
157
+ const agent = agentName.toLowerCase();
158
+ if (/\bbuilder\b/.test(agent)) return "writer";
159
+ if (/\b(?:architect|commentator|explorer|recapper|researcher|analyst)\b/.test(agent)) return "read-only";
160
+ return undefined;
161
+ }
162
+
142
163
  function hasImplementationIntent(agent: string, taskText: string): boolean {
143
164
  if (isReviewerStyleAgent(agent)) return REVIEWER_REQUIRED_EDIT_PATTERNS.some((pattern) => pattern.test(taskText));
144
165
  if (agent === "builder") return WORKER_IMPLEMENTATION_PATTERNS.some((pattern) => pattern.test(taskText));
@@ -5,7 +5,7 @@
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
7
  import type { Message } from "@earendil-works/pi-ai";
8
- import type { AgentConfig } from "../agents/agents.ts";
8
+ import type { AgentConfig, AgentSource } from "../agents/agents.ts";
9
9
  import type { FSWatcher } from "node:fs";
10
10
  import type { ExtensionContext } from "@selesai/code";
11
11
  import type { ModelScopeConfig } from "../runs/shared/model-scope.ts";
@@ -912,12 +912,51 @@ export interface SpawnBudgetSnapshot {
912
912
  grantHistory: SpawnBudgetGrant[];
913
913
  }
914
914
 
915
+ // ============================================================================
916
+ // Runtime catalog (action:list machine metadata)
917
+ // ============================================================================
918
+
919
+ /** Machine-readable catalog entry for one visible runtime agent. */
920
+ export interface CatalogAgentMetadata {
921
+ name: string;
922
+ source: AgentSource;
923
+ description: string;
924
+ /** false for capability-ceiling-restricted agents (still visible, not launchable). */
925
+ executable: boolean;
926
+ /** Present only when the agent is capability-ceiling-restricted. */
927
+ restrictionSources?: string[];
928
+ aliases?: string[];
929
+ /** Normalized to explicit "fresh" when the agent has no defaultContext. */
930
+ defaultContext: "fresh" | "fork";
931
+ acceptanceRole?: AcceptanceRole;
932
+ /** Effective declared tools: normal tools plus mcp:-prefixed direct MCP tools. */
933
+ tools?: string[];
934
+ }
935
+
936
+ /** Machine-readable catalog entry for one visible chain. */
937
+ export interface CatalogChainMetadata {
938
+ name: string;
939
+ source: AgentSource;
940
+ description: string;
941
+ }
942
+
943
+ /** Versioned action:list catalog mirroring the human list output. */
944
+ export interface CatalogMetadataV1 {
945
+ version: 1;
946
+ agents: CatalogAgentMetadata[];
947
+ chains: CatalogChainMetadata[];
948
+ /** Present only when a capability ceiling restricted visible agents. */
949
+ capabilityCeilingSources?: string[];
950
+ }
951
+
915
952
  export interface Details {
916
953
  mode: SubagentRunMode | "management";
917
954
  runId?: string;
918
955
  /** Run-level context summary. "mixed" when children resolved to different modes. */
919
956
  context?: "fresh" | "fork" | "mixed";
920
957
  results: SingleResult[];
958
+ /** Runtime-resolved human+machine catalog for { action: "list" } results. */
959
+ catalog?: CatalogMetadataV1;
921
960
  controlEvents?: ControlEvent[];
922
961
  steering?: SteerActionResult;
923
962
  asyncId?: string;
@@ -1645,7 +1684,7 @@ export const DEFAULT_MAX_OUTPUT: Required<MaxOutputConfig> = {
1645
1684
  };
1646
1685
 
1647
1686
  export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
1648
- enabled: true,
1687
+ enabled: false,
1649
1688
  dir: "project",
1650
1689
  includeInput: true,
1651
1690
  includeOutput: true,
@@ -7,7 +7,7 @@ import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import type { Message } from "@earendil-works/pi-ai";
9
9
  import { formatToolCall } from "./formatters.ts";
10
- import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, NestedRunSummary, SingleResult, ToolCallSummary, Usage } from "./types.ts";
10
+ import type { AgentProgress, AsyncStatus, ChainOutputMap, Details, DisplayItem, ErrorInfo, NestedRunSummary, SingleResult, ToolCallSummary, Usage } from "./types.ts";
11
11
 
12
12
  // ============================================================================
13
13
  // File System Utilities
@@ -414,12 +414,40 @@ export function compactForegroundResult(result: SingleResult): SingleResult {
414
414
  messages: undefined,
415
415
  progress: undefined,
416
416
  toolCalls: toolCalls.length ? toolCalls : undefined,
417
+ // Reference-first terminal details: once an authoritative saved output path
418
+ // exists, drop the raw final output and truncation marker from the model-
419
+ // visible projection; consumers recover the file from `savedOutputPath`.
420
+ // Explicit `outputMode: "inline"` is the sole legacy full-text opt-out and
421
+ // keeps its final output in the terminal projection (e.g. delegation v1
422
+ // `response.output` stays populated).
423
+ finalOutput: result.savedOutputPath && result.outputMode !== "inline" ? undefined : result.finalOutput,
424
+ truncation: result.savedOutputPath && result.outputMode !== "inline" ? undefined : result.truncation,
417
425
  };
418
426
  }
419
427
 
428
+ /**
429
+ * Strip chain `details.outputs` text/structured payloads from the terminal
430
+ * projection while retaining the output names and step metadata. Chain output
431
+ * bindings themselves remain reference-first in the completion content and
432
+ * `{outputs.name}` interpolation (see outputEntryFromResult).
433
+ */
434
+ function compactChainOutputs(outputs: ChainOutputMap | undefined): ChainOutputMap | undefined {
435
+ if (!outputs) return undefined;
436
+ const compact: ChainOutputMap = {};
437
+ for (const [name, entry] of Object.entries(outputs)) {
438
+ compact[name] = {
439
+ agent: entry.agent,
440
+ stepIndex: entry.stepIndex,
441
+ text: "",
442
+ };
443
+ }
444
+ return compact;
445
+ }
446
+
420
447
  export function compactForegroundDetails(details: Details): Details {
421
448
  return {
422
449
  ...details,
450
+ outputs: compactChainOutputs(details.outputs),
423
451
  results: details.results.map(compactForegroundResult),
424
452
  progress: details.progress
425
453
  ? details.progress.map(compactCompletedProgress)
@@ -375,7 +375,11 @@ export function toSubagentDelegationExecutionParams(request: SubagentDelegationR
375
375
  toolBudget: request.toolBudget,
376
376
  skill: request.skill,
377
377
  output: request.output,
378
- outputMode: request.outputMode,
378
+ // v1 has no default for outputMode: preserve the legacy full-text contract by
379
+ // resolving an omitted outputMode to explicit inline (response.output stays
380
+ // populated). The model-facing tool's mode-dependent reference-first default
381
+ // (omitted outputMode -> file-only) is intentionally NOT applied here.
382
+ outputMode: request.outputMode ?? "inline",
379
383
  outputSchema: request.outputSchema,
380
384
  agentContract: request.agentContract,
381
385
  acceptance: request.acceptance,