@wrongstack/core 0.285.0 → 0.286.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/autophase/phase-orchestrator.d.ts.map +1 -1
  2. package/dist/boot.d.ts.map +1 -1
  3. package/dist/coordination/agent-monitor.d.ts +29 -0
  4. package/dist/coordination/agent-monitor.d.ts.map +1 -1
  5. package/dist/coordination/autonomous-brain.d.ts +0 -2
  6. package/dist/coordination/autonomous-brain.d.ts.map +1 -1
  7. package/dist/coordination/autonomous-coordinator.d.ts +4 -0
  8. package/dist/coordination/autonomous-coordinator.d.ts.map +1 -1
  9. package/dist/coordination/brain-ledger.d.ts +142 -0
  10. package/dist/coordination/brain-ledger.d.ts.map +1 -0
  11. package/dist/coordination/brain-monitor.d.ts +31 -2
  12. package/dist/coordination/brain-monitor.d.ts.map +1 -1
  13. package/dist/coordination/brain.d.ts +43 -0
  14. package/dist/coordination/brain.d.ts.map +1 -1
  15. package/dist/coordination/delegate-tool.d.ts.map +1 -1
  16. package/dist/coordination/director-construction.d.ts.map +1 -1
  17. package/dist/coordination/director.d.ts +1 -1
  18. package/dist/coordination/director.d.ts.map +1 -1
  19. package/dist/coordination/fleet.d.ts +4 -4
  20. package/dist/coordination/fleet.d.ts.map +1 -1
  21. package/dist/coordination/global-mailbox.d.ts +63 -2
  22. package/dist/coordination/global-mailbox.d.ts.map +1 -1
  23. package/dist/coordination/index.d.ts +2 -1
  24. package/dist/coordination/index.d.ts.map +1 -1
  25. package/dist/coordination/index.js +1566 -462
  26. package/dist/coordination/index.js.map +4 -4
  27. package/dist/coordination/knowledge-graph.d.ts +13 -1
  28. package/dist/coordination/knowledge-graph.d.ts.map +1 -1
  29. package/dist/coordination/mailbox-actions.d.ts +12 -13
  30. package/dist/coordination/mailbox-actions.d.ts.map +1 -1
  31. package/dist/coordination/mailbox-constants.d.ts +64 -0
  32. package/dist/coordination/mailbox-constants.d.ts.map +1 -0
  33. package/dist/coordination/mailbox-events.d.ts +32 -0
  34. package/dist/coordination/mailbox-events.d.ts.map +1 -0
  35. package/dist/coordination/mailbox-message-codec.d.ts +8 -0
  36. package/dist/coordination/mailbox-message-codec.d.ts.map +1 -0
  37. package/dist/coordination/mailbox-types.d.ts +88 -4
  38. package/dist/coordination/mailbox-types.d.ts.map +1 -1
  39. package/dist/coordination/mailbox.d.ts +21 -2
  40. package/dist/coordination/mailbox.d.ts.map +1 -1
  41. package/dist/coordination/package-author-tracker.d.ts.map +1 -1
  42. package/dist/coordination/subagent-result-tool.d.ts.map +1 -1
  43. package/dist/defaults/index.js +846 -523
  44. package/dist/defaults/index.js.map +4 -4
  45. package/dist/execution/autonomous-runner.d.ts +7 -1
  46. package/dist/execution/autonomous-runner.d.ts.map +1 -1
  47. package/dist/execution/autonomy-brain.d.ts +72 -11
  48. package/dist/execution/autonomy-brain.d.ts.map +1 -1
  49. package/dist/execution/brain-chain.d.ts +53 -0
  50. package/dist/execution/brain-chain.d.ts.map +1 -0
  51. package/dist/execution/brain-evaluation.d.ts +66 -0
  52. package/dist/execution/brain-evaluation.d.ts.map +1 -0
  53. package/dist/execution/brain-runtime.d.ts +171 -0
  54. package/dist/execution/brain-runtime.d.ts.map +1 -0
  55. package/dist/execution/council-brain.d.ts +79 -0
  56. package/dist/execution/council-brain.d.ts.map +1 -0
  57. package/dist/execution/eternal-autonomy.d.ts +12 -0
  58. package/dist/execution/eternal-autonomy.d.ts.map +1 -1
  59. package/dist/execution/index.d.ts +5 -1
  60. package/dist/execution/index.d.ts.map +1 -1
  61. package/dist/execution/index.js +1350 -377
  62. package/dist/execution/index.js.map +4 -4
  63. package/dist/execution/parallel-eternal-engine.d.ts +12 -0
  64. package/dist/execution/parallel-eternal-engine.d.ts.map +1 -1
  65. package/dist/execution/tool-executor.d.ts.map +1 -1
  66. package/dist/hq/auth-store.d.ts +21 -3
  67. package/dist/hq/auth-store.d.ts.map +1 -1
  68. package/dist/hq/factory.d.ts +3 -0
  69. package/dist/hq/factory.d.ts.map +1 -1
  70. package/dist/hq/index.js +628 -72
  71. package/dist/hq/index.js.map +4 -4
  72. package/dist/hq/protocol.d.ts +54 -2
  73. package/dist/hq/protocol.d.ts.map +1 -1
  74. package/dist/hq/publisher.d.ts +4 -0
  75. package/dist/hq/publisher.d.ts.map +1 -1
  76. package/dist/index.d.ts +12 -6
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +3482 -1495
  79. package/dist/index.js.map +4 -4
  80. package/dist/kernel/events.d.ts +22 -4
  81. package/dist/kernel/events.d.ts.map +1 -1
  82. package/dist/kernel/index.js.map +2 -2
  83. package/dist/mailbox-attach.d.ts.map +1 -1
  84. package/dist/observability/index.js +16 -15
  85. package/dist/observability/index.js.map +2 -2
  86. package/dist/security/index.d.ts +1 -0
  87. package/dist/security/index.d.ts.map +1 -1
  88. package/dist/security/index.js +253 -21
  89. package/dist/security/index.js.map +4 -4
  90. package/dist/security/permission-policy-schema.d.ts +77 -0
  91. package/dist/security/permission-policy-schema.d.ts.map +1 -0
  92. package/dist/security/permission-policy.d.ts +5 -0
  93. package/dist/security/permission-policy.d.ts.map +1 -1
  94. package/dist/security/secret-vault.d.ts +9 -0
  95. package/dist/security/secret-vault.d.ts.map +1 -1
  96. package/dist/session-registry.d.ts.map +1 -1
  97. package/dist/storage/config-loader.d.ts +10 -0
  98. package/dist/storage/config-loader.d.ts.map +1 -1
  99. package/dist/storage/goal-store.d.ts +1 -1
  100. package/dist/storage/goal-store.d.ts.map +1 -1
  101. package/dist/storage/index.js +147 -162
  102. package/dist/storage/index.js.map +2 -2
  103. package/dist/storage/plan-store.d.ts +1 -1
  104. package/dist/storage/plan-store.d.ts.map +1 -1
  105. package/dist/storage/queue-store.d.ts +4 -0
  106. package/dist/storage/queue-store.d.ts.map +1 -1
  107. package/dist/storage/session-store.d.ts +9 -0
  108. package/dist/storage/session-store.d.ts.map +1 -1
  109. package/dist/storage/task-store.d.ts +1 -1
  110. package/dist/storage/task-store.d.ts.map +1 -1
  111. package/dist/storage/todos-checkpoint.d.ts +2 -2
  112. package/dist/storage/todos-checkpoint.d.ts.map +1 -1
  113. package/dist/types/config.d.ts +209 -1
  114. package/dist/types/config.d.ts.map +1 -1
  115. package/dist/types/index.js +11 -0
  116. package/dist/types/index.js.map +2 -2
  117. package/dist/types/multi-agent.d.ts +8 -0
  118. package/dist/types/multi-agent.d.ts.map +1 -1
  119. package/dist/types/observability.d.ts +5 -0
  120. package/dist/types/observability.d.ts.map +1 -1
  121. package/dist/types/tool.d.ts +7 -0
  122. package/dist/types/tool.d.ts.map +1 -1
  123. package/dist/utils/glob-expand.d.ts.map +1 -1
  124. package/dist/utils/incoming-images.d.ts +39 -0
  125. package/dist/utils/incoming-images.d.ts.map +1 -0
  126. package/dist/utils/index.d.ts +1 -0
  127. package/dist/utils/index.d.ts.map +1 -1
  128. package/dist/utils/index.js +81 -3
  129. package/dist/utils/index.js.map +3 -3
  130. package/dist/worktree/worktree-manager.d.ts +13 -4
  131. package/dist/worktree/worktree-manager.d.ts.map +1 -1
  132. package/instructions/agents/browser.md +25 -16
  133. package/instructions/coordination/subagent-baseline.md +16 -2
  134. package/instructions/leader-after-task.md +3 -1
  135. package/instructions/sections/tool/common-patterns.md +7 -1
  136. package/instructions/system.md +47 -0
  137. package/package.json +2 -2
  138. package/skills/docker-deploy/SKILL.md +1 -1
  139. package/skills/wrongstack-mailbox/SKILL.md +51 -12
  140. package/dist/plugins/git-plugin.d.ts +0 -15
  141. package/dist/plugins/git-plugin.d.ts.map +0 -1
  142. package/dist/plugins/observability-plugin.d.ts +0 -19
  143. package/dist/plugins/observability-plugin.d.ts.map +0 -1
  144. package/dist/plugins/plan-plugin.d.ts +0 -16
  145. package/dist/plugins/plan-plugin.d.ts.map +0 -1
@@ -528,9 +528,17 @@ function globToRegex(pat) {
528
528
  return new RegExp(re + "$");
529
529
  }
530
530
  function baseDir(pat) {
531
- let i = pat.length - 1;
532
- while (i >= 0 && !GLOB_CHARS.has(expectDefined(pat[i])) && pat[i] !== SEP && pat[i] !== "/") i--;
533
- const cut = i >= 0 ? pat.lastIndexOf(SEP, i) : pat.lastIndexOf("/", i);
531
+ let firstGlob = pat.length;
532
+ for (let i = 0; i < pat.length; i++) {
533
+ if (GLOB_CHARS.has(expectDefined(pat[i]))) {
534
+ firstGlob = i;
535
+ break;
536
+ }
537
+ }
538
+ const cut = Math.max(
539
+ pat.lastIndexOf(SEP, firstGlob - 1),
540
+ pat.lastIndexOf("/", firstGlob - 1)
541
+ );
534
542
  return cut < 0 ? "." : pat.slice(0, cut);
535
543
  }
536
544
  async function expandGlob(pattern) {
@@ -3187,9 +3195,11 @@ function normalizeSubagentStructuredReport(value) {
3187
3195
  const filesExamined = stringArray(value["files_examined"]);
3188
3196
  const nextSteps = stringArray(value["suggested_next_steps"]);
3189
3197
  const confidence = value["confidence"];
3198
+ const completion = value["completion"];
3199
+ const remainingWork = optionalString(value["remaining_work"]);
3190
3200
  if (!summary || summary.length > MAX_SUMMARY_CHARS || !findings || findings.length > MAX_FINDINGS || !filesExamined || filesExamined.length > MAX_FILES || !nextSteps || nextSteps.length > MAX_NEXT_STEPS || [...findings, ...filesExamined, ...nextSteps].some(
3191
3201
  (item) => item.trim().length > MAX_ITEM_CHARS
3192
- ) || typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
3202
+ ) || typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1 || completion !== void 0 && completion !== "complete" && completion !== "partial" || completion === "partial" && !remainingWork || remainingWork !== void 0 && remainingWork.length > MAX_SUMMARY_CHARS) {
3193
3203
  return void 0;
3194
3204
  }
3195
3205
  const report = {
@@ -3197,7 +3207,9 @@ function normalizeSubagentStructuredReport(value) {
3197
3207
  findings: findings.map((item) => item.trim()).filter(Boolean),
3198
3208
  files_examined: filesExamined.map((item) => item.trim()).filter(Boolean),
3199
3209
  confidence,
3200
- suggested_next_steps: nextSteps.map((item) => item.trim()).filter(Boolean)
3210
+ suggested_next_steps: nextSteps.map((item) => item.trim()).filter(Boolean),
3211
+ ...completion === "complete" || completion === "partial" ? { completion } : {},
3212
+ ...remainingWork ? { remaining_work: remainingWork } : {}
3201
3213
  };
3202
3214
  return JSON.stringify(report).length <= MAX_SUBAGENT_STRUCTURED_REPORT_CHARS ? report : void 0;
3203
3215
  }
@@ -3210,6 +3222,8 @@ function formatSubagentStructuredReport(report) {
3210
3222
  const next = report.suggested_next_steps.length ? report.suggested_next_steps.map((item) => `- ${item}`).join("\n") : "- (none)";
3211
3223
  return [
3212
3224
  report.summary,
3225
+ report.completion ? `Completion: ${report.completion}` : void 0,
3226
+ report.remaining_work ? `Remaining work: ${report.remaining_work}` : void 0,
3213
3227
  `
3214
3228
  Findings:
3215
3229
  ${findings}`,
@@ -3219,7 +3233,7 @@ Files examined: ${files}`,
3219
3233
  `
3220
3234
  Suggested next steps:
3221
3235
  ${next}`
3222
- ].join("\n");
3236
+ ].filter((line) => typeof line === "string").join("\n");
3223
3237
  }
3224
3238
  function isRecord3(value) {
3225
3239
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -3227,6 +3241,9 @@ function isRecord3(value) {
3227
3241
  function nonEmptyString(value) {
3228
3242
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
3229
3243
  }
3244
+ function optionalString(value) {
3245
+ return value === void 0 ? void 0 : nonEmptyString(value);
3246
+ }
3230
3247
  function stringArray(value) {
3231
3248
  return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
3232
3249
  }
@@ -5119,58 +5136,27 @@ import * as path5 from "node:path";
5119
5136
  import { randomUUID as randomUUID2 } from "node:crypto";
5120
5137
 
5121
5138
  // src/coordination/fleet.ts
5122
- var AUDIT_LOG_AGENT = {
5123
- id: "audit-log",
5124
- name: "Audit Log",
5125
- role: "audit-log",
5126
- prompt: agentPrompt("audit-log")
5127
- // No hardcoded budgets — the orchestrator (delegate tool or
5128
- // spawn_subagent) decides per-task how much room a subagent gets.
5129
- // A monorepo audit needs hours; a single-file lint check needs
5130
- // seconds. Pinning a number here forces the orchestrator to fight
5131
- // the role's default instead of just asking for what it needs.
5132
- };
5133
- var BUG_HUNTER_AGENT = {
5134
- id: "bug-hunter",
5135
- name: "Bug Hunter",
5136
- role: "bug-hunter",
5137
- prompt: agentPrompt("bug-hunter")
5138
- // Budgets are set by the orchestrator per task — see fleet.ts header.
5139
- };
5140
- var REFACTOR_PLANNER_AGENT = {
5141
- id: "refactor-planner",
5142
- name: "Refactor Planner",
5143
- role: "refactor-planner",
5144
- prompt: agentPrompt("refactor-planner")
5145
- // Budgets are set by the orchestrator per task — see fleet.ts header.
5146
- };
5147
- var SECURITY_SCANNER_AGENT = {
5148
- id: "security-scanner",
5149
- name: "Security Scanner",
5150
- role: "security-scanner",
5151
- prompt: agentPrompt("security-scanner")
5152
- // Budgets are set by the orchestrator per task — see fleet.ts header.
5153
- };
5154
- var SHADOW_AGENT = {
5155
- id: "shadow-agent",
5156
- name: "Shadow",
5157
- role: "shadow-agent",
5158
- prompt: agentPrompt("shadow-agent")
5159
- // Budgets are set by the orchestrator per task — see fleet.ts header.
5160
- };
5161
- var CRITIC_AGENT = {
5162
- id: "critic",
5163
- name: "Critic",
5164
- role: "critic",
5165
- prompt: agentPrompt("critic")
5166
- // Budgets are set by the orchestrator per task — see fleet.ts header.
5167
- };
5139
+ function defineAgent(id, name, promptRole = id, provider) {
5140
+ return {
5141
+ id,
5142
+ name,
5143
+ role: id,
5144
+ prompt: agentPrompt(promptRole),
5145
+ ...provider === void 0 ? {} : { provider }
5146
+ };
5147
+ }
5148
+ var AUDIT_LOG_AGENT = defineAgent("audit-log", "Audit Log");
5149
+ var BUG_HUNTER_AGENT = defineAgent("bug-hunter", "Bug Hunter");
5150
+ var REFACTOR_PLANNER_AGENT = defineAgent("refactor-planner", "Refactor Planner");
5151
+ var SECURITY_SCANNER_AGENT = defineAgent("security-scanner", "Security Scanner");
5152
+ var SHADOW_AGENT = defineAgent("shadow-agent", "Shadow");
5153
+ var CRITIC_AGENT = defineAgent("critic", "Critic");
5168
5154
  var FLEET_ROSTER = {
5169
5155
  "audit-log": AUDIT_LOG_AGENT,
5170
5156
  "bug-hunter": BUG_HUNTER_AGENT,
5171
5157
  "refactor-planner": REFACTOR_PLANNER_AGENT,
5172
5158
  "security-scanner": SECURITY_SCANNER_AGENT,
5173
- "critic": CRITIC_AGENT,
5159
+ critic: CRITIC_AGENT,
5174
5160
  "shadow-agent": SHADOW_AGENT,
5175
5161
  ...Object.fromEntries(
5176
5162
  ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
@@ -5182,7 +5168,7 @@ var FLEET_ROSTER_BUDGETS = {
5182
5168
  "bug-hunter": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
5183
5169
  "refactor-planner": { timeoutMs: 7.5 * 60 * 60 * 1e3, maxIterations: 6e3, maxToolCalls: 18e3 },
5184
5170
  "security-scanner": { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 },
5185
- "critic": { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 12e3 },
5171
+ critic: { timeoutMs: 5 * 60 * 60 * 1e3, maxIterations: 4e3, maxToolCalls: 12e3 },
5186
5172
  "shadow-agent": {
5187
5173
  idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
5188
5174
  maxIterations: 2e3,
@@ -5219,41 +5205,11 @@ function applyRosterBudget(cfg) {
5219
5205
  };
5220
5206
  }
5221
5207
  var ALL_FLEET_AGENTS = Object.values(FLEET_ROSTER);
5222
- var CLINE_AGENT = {
5223
- id: "cline",
5224
- name: "Cline",
5225
- role: "cline",
5226
- prompt: agentPrompt("acp-cline"),
5227
- provider: "acp"
5228
- };
5229
- var GEMINI_CLI_AGENT = {
5230
- id: "gemini-cli",
5231
- name: "Gemini CLI",
5232
- role: "gemini-cli",
5233
- prompt: agentPrompt("acp-gemini-cli"),
5234
- provider: "acp"
5235
- };
5236
- var COPILOT_AGENT = {
5237
- id: "copilot",
5238
- name: "GitHub Copilot",
5239
- role: "copilot",
5240
- prompt: agentPrompt("acp-copilot"),
5241
- provider: "acp"
5242
- };
5243
- var OPENHANDS_AGENT = {
5244
- id: "openhands",
5245
- name: "OpenHands",
5246
- role: "openhands",
5247
- prompt: agentPrompt("acp-openhands"),
5248
- provider: "acp"
5249
- };
5250
- var GOOSE_AGENT = {
5251
- id: "goose",
5252
- name: "Goose",
5253
- role: "goose",
5254
- prompt: agentPrompt("acp-goose"),
5255
- provider: "acp"
5256
- };
5208
+ var CLINE_AGENT = defineAgent("cline", "Cline", "acp-cline", "acp");
5209
+ var GEMINI_CLI_AGENT = defineAgent("gemini-cli", "Gemini CLI", "acp-gemini-cli", "acp");
5210
+ var COPILOT_AGENT = defineAgent("copilot", "GitHub Copilot", "acp-copilot", "acp");
5211
+ var OPENHANDS_AGENT = defineAgent("openhands", "OpenHands", "acp-openhands", "acp");
5212
+ var GOOSE_AGENT = defineAgent("goose", "Goose", "acp-goose", "acp");
5257
5213
  var ACP_AGENTS = [
5258
5214
  CLINE_AGENT,
5259
5215
  GEMINI_CLI_AGENT,
@@ -5261,11 +5217,14 @@ var ACP_AGENTS = [
5261
5217
  OPENHANDS_AGENT,
5262
5218
  GOOSE_AGENT
5263
5219
  ];
5264
- FLEET_ROSTER_BUDGETS["cline"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 };
5265
- FLEET_ROSTER_BUDGETS["gemini-cli"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 };
5266
- FLEET_ROSTER_BUDGETS["copilot"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 };
5267
- FLEET_ROSTER_BUDGETS["openhands"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 };
5268
- FLEET_ROSTER_BUDGETS["goose"] = { timeoutMs: 10 * 60 * 60 * 1e3, maxIterations: 8e3, maxToolCalls: 2e4 };
5220
+ var ACP_AGENT_BUDGET = {
5221
+ timeoutMs: 10 * 60 * 60 * 1e3,
5222
+ maxIterations: 8e3,
5223
+ maxToolCalls: 2e4
5224
+ };
5225
+ for (const agent of ACP_AGENTS) {
5226
+ FLEET_ROSTER_BUDGETS[agent.role] = { ...ACP_AGENT_BUDGET };
5227
+ }
5269
5228
  var FLEET_ROSTER_WITHACP = {
5270
5229
  ...FLEET_ROSTER,
5271
5230
  ...Object.fromEntries(ACP_AGENTS.map((a) => [a.role, a]))
@@ -5273,7 +5232,7 @@ var FLEET_ROSTER_WITHACP = {
5273
5232
 
5274
5233
  // src/coordination/delegate-tool.ts
5275
5234
  function createDelegateTool(opts) {
5276
- const defaultTimeoutMs = opts.defaultTimeoutMs ?? 30 * 60 * 1e3;
5235
+ const defaultTimeoutMs = opts.defaultTimeoutMs ?? 4 * 60 * 60 * 1e3;
5277
5236
  const rosterIds = opts.roster ? Object.keys(opts.roster) : [];
5278
5237
  const inputSchema = {
5279
5238
  type: "object",
@@ -5332,16 +5291,23 @@ function createDelegateTool(opts) {
5332
5291
  type: "number",
5333
5292
  minimum: 0,
5334
5293
  description: "Maximum estimated USD cost the subagent may incur. Unset = use the role/coordinator default."
5294
+ },
5295
+ maxHandoffs: {
5296
+ type: "number",
5297
+ minimum: 0,
5298
+ maximum: 8,
5299
+ description: "Maximum fresh-worker continuations after partial completion or budget exhaustion. Default 1; set 0 to disable. Each successor receives the prior structured/partial report and is told to continue only the remaining work."
5335
5300
  }
5336
5301
  },
5337
5302
  required: ["task"]
5338
5303
  };
5339
5304
  return {
5340
5305
  name: "delegate",
5341
- description: "Hand a discrete piece of work to a dedicated subagent and wait for its result. The subagent has its own context, its own LLM call, and its own budget \u2014 use this when a task is self-contained, would otherwise blow up your context, or benefits from a specialized role (bug-hunter, security-scanner, refactor-planner, audit-log). For free-form coding tasks (not tied to a pre-defined role), pass `name` + `task` \u2014 the subagent runs as a general-purpose coding agent with a large default budget. YOU decide how big the budget is: pass `timeoutMs`, `maxIterations`, and `maxToolCalls` sized to the actual work. There is no hidden cap forcing a 3-minute / 80-iteration limit \u2014 if a monorepo audit needs 2 hours and 500 tool calls, ask for that. Call multiple delegates in parallel through the provider's parallel-tool-call surface to fan work out across roles.",
5342
- usageHint: "Set `task` to a complete instruction. Either pick `role` from the roster (audit-log, bug-hunter, refactor-planner, security-scanner) or pass `name` to run a free-form coding agent. For non-trivial work, also pass `timeoutMs`, `maxIterations`, and `maxToolCalls`. Returns the subagent's `TaskResult` \u2014 including the textual `result`, iteration count, tool count, and duration. Requires Director mode to be active in normal CLI sessions.",
5306
+ description: "Hand a discrete piece of work to a dedicated subagent and wait for its result. The subagent has its own context, its own LLM call, and a generous, auto-extending budget. If it reports explicit partial completion or still exhausts a recoverable budget, delegate can continue the remaining work with a fresh subagent (bounded by `maxHandoffs`, default 1). Workers may ask the leader to spawn a parallel helper through the mailbox, but cannot recursively spawn agents themselves. Use specialized roster roles when possible; otherwise pass `name` + `task`. Call multiple delegates in parallel through the provider's parallel-tool-call surface for independent work.",
5307
+ usageHint: "Set `task` to a complete instruction. Either pick `role` from the roster or pass `name` for a free-form coding agent. Defaults are intentionally generous; override `timeoutMs`, `maxIterations`, `maxToolCalls`, or `maxHandoffs` only when the task warrants it. Returns the final TaskResult plus a `handoffs` history when fresh workers continued partial work. Requires Director mode in normal CLI sessions.",
5343
5308
  permission: "auto",
5344
5309
  mutating: false,
5310
+ managesOwnTimeout: true,
5345
5311
  capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
5346
5312
  inputSchema,
5347
5313
  async execute(input, _ctx, execOpts) {
@@ -5381,7 +5347,7 @@ function createDelegateTool(opts) {
5381
5347
  error: `Unknown role "${i.role}". Available: ${rosterIds.join(", ") || "(no roster configured)"}.`
5382
5348
  };
5383
5349
  }
5384
- cfg = instantiateRosterConfig(i.role, base);
5350
+ cfg = instantiateRosterConfig(i.role, base, i.timeoutMs, defaultTimeoutMs);
5385
5351
  if (i.systemPromptOverride) cfg.systemPromptOverride = i.systemPromptOverride;
5386
5352
  if (i.provider) cfg.provider = i.provider;
5387
5353
  if (i.model) cfg.model = i.model;
@@ -5420,172 +5386,175 @@ function createDelegateTool(opts) {
5420
5386
  cfg.timeoutMs = Math.max(3e4, timeoutMs - SUBAGENT_TIMEOUT_BUFFER_MS);
5421
5387
  }
5422
5388
  opts.events?.emit("delegate.started", { sessionId, target, task: i.task });
5423
- const subagentId = await director.spawn(cfg);
5424
- const taskId = await director.assign({
5425
- id: `${randomUUID2()}`,
5426
- description: i.task,
5427
- subagentId
5428
- });
5429
5389
  const dir = director;
5430
- const result = await new Promise((resolve12) => {
5431
- let settled = false;
5432
- let timer;
5433
- let offAbort = () => {
5434
- };
5435
- const finish = (value) => {
5436
- if (settled) return;
5437
- settled = true;
5438
- if (timer) clearTimeout(timer);
5439
- offTool();
5440
- offIter();
5441
- offProgress();
5442
- offAbort();
5443
- resolve12(value);
5444
- };
5445
- const arm = () => {
5446
- if (timer) clearTimeout(timer);
5447
- timer = setTimeout(() => finish({ __timeout: true }), timeoutMs);
5448
- };
5449
- const bump = (e) => {
5450
- if (e.subagentId === subagentId) arm();
5451
- };
5452
- const offTool = dir.fleet.filter("tool.executed", bump);
5453
- const offIter = dir.fleet.filter("iteration.started", bump);
5454
- const offProgress = dir.fleet.filter("tool.progress", bump);
5455
- if (abortSignal) {
5456
- const onAbortSignal = () => finish({ __aborted: true });
5457
- abortSignal.addEventListener("abort", onAbortSignal, { once: true });
5458
- offAbort = () => abortSignal.removeEventListener("abort", onAbortSignal);
5459
- if (abortSignal.aborted) onAbortSignal();
5460
- }
5461
- arm();
5462
- dir.awaitTasks([taskId]).then((r) => finish(r[0] ?? { __emptyResult: true })).catch(() => finish({ __timeout: true }));
5463
- });
5464
- if ("__aborted" in result) {
5465
- try {
5466
- await dir.terminate(subagentId);
5467
- } catch {
5468
- }
5469
- const partial2 = await readSubagentPartial(opts, subagentId);
5470
- opts.events?.emit("delegate.completed", {
5471
- sessionId,
5472
- target,
5473
- task: i.task,
5474
- ok: false,
5475
- status: "aborted",
5476
- summary: `[${target}] aborted \u2014 the run was interrupted`,
5477
- durationMs: 0,
5478
- iterations: partial2?.events ?? 0,
5479
- toolCalls: partial2?.toolUsesObserved ?? 0,
5390
+ const maxHandoffs = Math.min(8, Math.max(0, Math.floor(i.maxHandoffs ?? 1)));
5391
+ const handoffs = [];
5392
+ let delegatedTask = i.task;
5393
+ let handoffCount = 0;
5394
+ for (; ; ) {
5395
+ const attemptConfig = handoffCount === 0 ? cfg : freshHandoffConfig(cfg, i.role, handoffCount);
5396
+ const subagentId = await dir.spawn(attemptConfig);
5397
+ const taskId = await dir.assign({
5398
+ id: randomUUID2(),
5399
+ description: delegatedTask,
5480
5400
  subagentId
5481
5401
  });
5482
- return {
5483
- ok: false,
5484
- stopReason: "aborted",
5485
- error: "Delegated task aborted \u2014 the run was interrupted.",
5402
+ const result = await awaitDelegateAttempt(
5403
+ dir,
5486
5404
  subagentId,
5487
5405
  taskId,
5488
- partial: partial2
5489
- };
5490
- }
5491
- if ("__timeout" in result) {
5492
- const partial2 = await readSubagentPartial(opts, subagentId);
5493
- opts.events?.emit("delegate.completed", {
5494
- sessionId,
5495
- target,
5496
- task: i.task,
5497
- ok: false,
5498
- status: "host_timeout",
5499
- summary: `[${target}] timed out \u2014 no result within ${Math.round(timeoutMs / 1e3)}s`,
5500
- durationMs: timeoutMs,
5501
- iterations: partial2?.events ?? 0,
5502
- toolCalls: partial2?.toolUsesObserved ?? 0,
5503
- subagentId
5504
- });
5505
- return {
5506
- ok: false,
5507
- stopReason: "host_timeout",
5508
- error: `Subagent did not finish within ${timeoutMs}ms.`,
5509
- hint: "Reduce scope of the next delegate, raise timeoutMs, or use spawn_subagent + await_tasks for long-running work.",
5510
- subagentId,
5511
- taskId,
5512
- partial: partial2
5513
- };
5514
- }
5515
- if ("__emptyResult" in result) {
5516
- const partial2 = await readSubagentPartial(opts, subagentId);
5406
+ timeoutMs,
5407
+ abortSignal
5408
+ );
5409
+ if ("__aborted" in result) {
5410
+ try {
5411
+ await dir.terminate(subagentId);
5412
+ } catch {
5413
+ }
5414
+ const partial2 = await readSubagentPartial(opts, subagentId);
5415
+ opts.events?.emit("delegate.completed", {
5416
+ sessionId,
5417
+ target,
5418
+ task: i.task,
5419
+ ok: false,
5420
+ status: "aborted",
5421
+ summary: `[${target}] aborted \u2014 the run was interrupted`,
5422
+ durationMs: 0,
5423
+ iterations: partial2?.events ?? 0,
5424
+ toolCalls: partial2?.toolUsesObserved ?? 0,
5425
+ subagentId
5426
+ });
5427
+ return {
5428
+ ok: false,
5429
+ stopReason: "aborted",
5430
+ error: "Delegated task aborted \u2014 the run was interrupted.",
5431
+ subagentId,
5432
+ taskId,
5433
+ partial: partial2,
5434
+ ...handoffs.length > 0 ? { handoffs } : {}
5435
+ };
5436
+ }
5437
+ if ("__timeout" in result) {
5438
+ try {
5439
+ await dir.terminate(subagentId);
5440
+ } catch {
5441
+ }
5442
+ const partial2 = await readSubagentPartial(opts, subagentId);
5443
+ opts.events?.emit("delegate.completed", {
5444
+ sessionId,
5445
+ target,
5446
+ task: i.task,
5447
+ ok: false,
5448
+ status: "host_timeout",
5449
+ summary: `[${target}] timed out \u2014 no progress within ${Math.round(timeoutMs / 1e3)}s`,
5450
+ durationMs: timeoutMs,
5451
+ iterations: partial2?.events ?? 0,
5452
+ toolCalls: partial2?.toolUsesObserved ?? 0,
5453
+ subagentId
5454
+ });
5455
+ return {
5456
+ ok: false,
5457
+ stopReason: "host_timeout",
5458
+ error: `Subagent timed out: it did not finish or report progress within ${timeoutMs}ms.`,
5459
+ hint: "Raise timeoutMs for unusually long single operations, or split the remaining work.",
5460
+ subagentId,
5461
+ taskId,
5462
+ partial: partial2,
5463
+ ...handoffs.length > 0 ? { handoffs } : {}
5464
+ };
5465
+ }
5466
+ if ("__emptyResult" in result) {
5467
+ const partial2 = await readSubagentPartial(opts, subagentId);
5468
+ opts.events?.emit("delegate.completed", {
5469
+ sessionId,
5470
+ target,
5471
+ task: i.task,
5472
+ ok: false,
5473
+ status: "empty_result",
5474
+ summary: `[${target}] completed without a task result`,
5475
+ durationMs: 0,
5476
+ iterations: partial2?.events ?? 0,
5477
+ toolCalls: partial2?.toolUsesObserved ?? 0,
5478
+ subagentId
5479
+ });
5480
+ return {
5481
+ ok: false,
5482
+ stopReason: "error",
5483
+ error: "Director returned no task result for the delegated task.",
5484
+ hint: "Check fleet state, then retry or reassign the task.",
5485
+ subagentId,
5486
+ taskId,
5487
+ partial: partial2,
5488
+ ...handoffs.length > 0 ? { handoffs } : {}
5489
+ };
5490
+ }
5491
+ const partial = result.status === "success" ? void 0 : result.partial ? {
5492
+ lastAssistantText: result.partial.text,
5493
+ toolUsesObserved: result.toolCalls,
5494
+ events: result.iterations
5495
+ } : await readSubagentPartial(opts, subagentId);
5496
+ const continuation = continuationFor(result, partial, attemptConfig);
5497
+ if (continuation && handoffCount < maxHandoffs) {
5498
+ handoffs.push({
5499
+ fromSubagentId: result.subagentId,
5500
+ fromTaskId: result.taskId,
5501
+ status: result.status,
5502
+ errorKind: result.error?.kind,
5503
+ summary: continuation.summary,
5504
+ remainingWork: continuation.remainingWork
5505
+ });
5506
+ handoffCount += 1;
5507
+ delegatedTask = buildHandoffTask(i.task, continuation, handoffCount, maxHandoffs);
5508
+ continue;
5509
+ }
5510
+ const incomplete = result.report?.completion === "partial";
5511
+ const baseStopReason = incomplete ? "handoff_limit" : result.status === "success" ? "end_turn" : result.status === "timeout" ? "subagent_timeout" : result.status === "stopped" ? "aborted" : "budget_exhausted";
5512
+ const errorKind = result.error?.kind;
5513
+ const retryable = result.error?.retryable;
5514
+ const backoffMs = result.error?.backoffMs;
5515
+ const summary = incomplete ? `[${target}] partial checkpoint \u2014 handoff limit reached after ${handoffCount} continuation(s)` : buildDelegateSummary(i.role, result);
5516
+ let costUsd;
5517
+ try {
5518
+ costUsd = dir.snapshot().perSubagent[result.subagentId]?.cost;
5519
+ } catch {
5520
+ costUsd = void 0;
5521
+ }
5517
5522
  opts.events?.emit("delegate.completed", {
5518
5523
  sessionId,
5519
5524
  target,
5520
5525
  task: i.task,
5521
- ok: false,
5522
- status: "empty_result",
5523
- summary: `[${target}] completed without a task result`,
5524
- durationMs: 0,
5525
- iterations: partial2?.events ?? 0,
5526
- toolCalls: partial2?.toolUsesObserved ?? 0,
5527
- subagentId
5526
+ ok: result.status === "success" && !incomplete,
5527
+ status: incomplete ? "partial" : result.status,
5528
+ summary,
5529
+ durationMs: result.durationMs,
5530
+ iterations: result.iterations,
5531
+ toolCalls: result.toolCalls,
5532
+ costUsd,
5533
+ subagentId: result.subagentId
5528
5534
  });
5529
5535
  return {
5530
- ok: false,
5531
- stopReason: "error",
5532
- error: "Director returned no task result for the delegated task.",
5533
- hint: "Check fleet state with /fleet status, then retry or reassign the task.",
5534
- subagentId,
5535
- taskId,
5536
- partial: partial2
5536
+ ok: result.status === "success" && !incomplete,
5537
+ status: incomplete ? "partial" : result.status,
5538
+ stopReason: baseStopReason,
5539
+ errorKind,
5540
+ retryable,
5541
+ backoffMs,
5542
+ subagentId: result.subagentId,
5543
+ taskId: result.taskId,
5544
+ result: result.result,
5545
+ report: result.report,
5546
+ error: result.error,
5547
+ iterations: result.iterations,
5548
+ toolCalls: result.toolCalls,
5549
+ durationMs: result.durationMs,
5550
+ ...partial ? { partial } : {},
5551
+ ...handoffs.length > 0 ? { handoffs } : {},
5552
+ ...incomplete ? {
5553
+ hint: "A clean partial checkpoint remains. Reinvoke delegate with a larger maxHandoffs or assign report.remaining_work explicitly."
5554
+ } : hintForKind(errorKind, retryable, backoffMs, partial) ? { hint: hintForKind(errorKind, retryable, backoffMs, partial) } : {},
5555
+ summary
5537
5556
  };
5538
5557
  }
5539
- const baseStopReason = result.status === "success" ? "end_turn" : result.status === "timeout" ? "subagent_timeout" : result.status === "stopped" ? "aborted" : "budget_exhausted";
5540
- const partial = result.status === "success" ? void 0 : result.partial ? {
5541
- lastAssistantText: result.partial.text,
5542
- toolUsesObserved: result.toolCalls,
5543
- events: result.iterations
5544
- } : await readSubagentPartial(opts, subagentId);
5545
- const errorKind = result.error?.kind;
5546
- const retryable = result.error?.retryable;
5547
- const backoffMs = result.error?.backoffMs;
5548
- const summary = buildDelegateSummary(i.role, result);
5549
- let costUsd;
5550
- try {
5551
- costUsd = dir.snapshot().perSubagent[result.subagentId]?.cost;
5552
- } catch {
5553
- costUsd = void 0;
5554
- }
5555
- opts.events?.emit("delegate.completed", {
5556
- sessionId,
5557
- target,
5558
- task: i.task,
5559
- ok: result.status === "success",
5560
- status: result.status,
5561
- summary,
5562
- durationMs: result.durationMs,
5563
- iterations: result.iterations,
5564
- toolCalls: result.toolCalls,
5565
- costUsd,
5566
- subagentId: result.subagentId
5567
- });
5568
- return {
5569
- ok: result.status === "success",
5570
- status: result.status,
5571
- stopReason: baseStopReason,
5572
- errorKind,
5573
- retryable,
5574
- backoffMs,
5575
- subagentId: result.subagentId,
5576
- taskId: result.taskId,
5577
- result: result.result,
5578
- report: result.report,
5579
- error: result.error,
5580
- iterations: result.iterations,
5581
- toolCalls: result.toolCalls,
5582
- durationMs: result.durationMs,
5583
- ...partial ? { partial } : {},
5584
- ...hintForKind(errorKind, retryable, backoffMs, partial) ? { hint: hintForKind(errorKind, retryable, backoffMs, partial) } : {},
5585
- // Summary is included so callers (TUI, CLI renderer) can surface
5586
- // it as a chat history line — the LLM also sees it for continuity.
5587
- summary
5588
- };
5589
5558
  } catch (err) {
5590
5559
  const message = toErrorMessage(err);
5591
5560
  opts.events?.emit("delegate.completed", {
@@ -5608,10 +5577,103 @@ function createDelegateTool(opts) {
5608
5577
  }
5609
5578
  };
5610
5579
  }
5611
- function instantiateRosterConfig(role, base) {
5580
+ async function awaitDelegateAttempt(director, subagentId, taskId, timeoutMs, abortSignal) {
5581
+ return new Promise((resolve12) => {
5582
+ let settled = false;
5583
+ let timer;
5584
+ let offAbort = () => {
5585
+ };
5586
+ const finish = (value) => {
5587
+ if (settled) return;
5588
+ settled = true;
5589
+ if (timer) clearTimeout(timer);
5590
+ offTool();
5591
+ offIter();
5592
+ offProgress();
5593
+ offAbort();
5594
+ resolve12(value);
5595
+ };
5596
+ const arm = () => {
5597
+ if (timer) clearTimeout(timer);
5598
+ timer = setTimeout(() => finish({ __timeout: true }), timeoutMs);
5599
+ };
5600
+ const bump = (event) => {
5601
+ if (event.subagentId === subagentId) arm();
5602
+ };
5603
+ const offTool = director.fleet.filter("tool.executed", bump);
5604
+ const offIter = director.fleet.filter("iteration.started", bump);
5605
+ const offProgress = director.fleet.filter("tool.progress", bump);
5606
+ if (abortSignal) {
5607
+ const onAbort = () => finish({ __aborted: true });
5608
+ abortSignal.addEventListener("abort", onAbort, { once: true });
5609
+ offAbort = () => abortSignal.removeEventListener("abort", onAbort);
5610
+ if (abortSignal.aborted) onAbort();
5611
+ }
5612
+ arm();
5613
+ director.awaitTasks([taskId]).then((results) => finish(results[0] ?? { __emptyResult: true })).catch(() => finish({ __timeout: true }));
5614
+ });
5615
+ }
5616
+ function freshHandoffConfig(cfg, role, handoffCount) {
5617
+ return {
5618
+ ...cfg,
5619
+ id: role ? `${role}-${randomUUID2().slice(0, 8)}` : `${cfg.name.toLowerCase().replace(/[^a-z0-9]+/g, "-") || "subagent"}-handoff-${handoffCount}-${randomUUID2().slice(0, 6)}`
5620
+ };
5621
+ }
5622
+ function continuationFor(result, partial, config) {
5623
+ if (result.report?.completion === "partial" && result.report.remaining_work) {
5624
+ return {
5625
+ summary: result.report.summary,
5626
+ remainingWork: result.report.remaining_work,
5627
+ partialText: typeof result.result === "string" ? result.result : partial?.lastAssistantText
5628
+ };
5629
+ }
5630
+ const budgetKinds = /* @__PURE__ */ new Set([
5631
+ "budget_iterations",
5632
+ "budget_tool_calls",
5633
+ "budget_tokens",
5634
+ "budget_cost",
5635
+ "budget_timeout"
5636
+ ]);
5637
+ const partialText = result.partial?.text ?? partial?.lastAssistantText;
5638
+ const mayHaveIsolatedWrites = config.worktree !== false && config.worktree !== "off" && (!config.tools || config.tools.some(
5639
+ (tool) => ["write", "edit", "replace", "patch", "bash", "exec", "install", "format"].includes(tool)
5640
+ ));
5641
+ if (!mayHaveIsolatedWrites && result.status !== "success" && result.error?.kind && budgetKinds.has(result.error.kind) && partialText) {
5642
+ return {
5643
+ summary: `Prior worker stopped at ${result.error.kind} after ${result.iterations} iterations and ${result.toolCalls} tool calls.`,
5644
+ remainingWork: "Inspect the existing workspace and finish only the work that remains from the original task.",
5645
+ partialText
5646
+ };
5647
+ }
5648
+ return void 0;
5649
+ }
5650
+ function buildHandoffTask(originalTask, continuation, handoffCount, maxHandoffs) {
5651
+ const partial = continuation.partialText?.trim().slice(-6e3);
5652
+ return [
5653
+ `Continue an oversized delegated task as fresh worker ${handoffCount} of ${maxHandoffs}.`,
5654
+ "Do not blindly repeat completed actions. Inspect the current workspace, git diff, tests, and any files named below before changing anything.",
5655
+ 'If the remaining work is still too large, stop at a clean checkpoint and call submit_result with completion="partial" plus concrete remaining_work.',
5656
+ "If parallel help would materially improve the outcome and mail_send is available, send the leader an `ask` that names the exact helper task; do not spawn agents yourself.",
5657
+ `Original task:
5658
+ ${originalTask}`,
5659
+ `Prior checkpoint summary:
5660
+ ${continuation.summary}`,
5661
+ `Remaining work:
5662
+ ${continuation.remainingWork}`,
5663
+ partial ? `Prior worker's last useful output:
5664
+ ${partial}` : ""
5665
+ ].filter(Boolean).join("\n\n");
5666
+ }
5667
+ function instantiateRosterConfig(role, base, requestedTimeoutMs, defaultTimeoutMs) {
5612
5668
  const withBudget = applyRosterBudget({ ...base, role });
5669
+ const rosterTimeoutMs = FLEET_ROSTER_BUDGETS[role]?.timeoutMs;
5613
5670
  return {
5614
5671
  ...withBudget,
5672
+ // Without an explicit host wait, apply the role's tuned multi-hour
5673
+ // wall-clock budget. With an explicit wait, leave this unset so the buffer
5674
+ // logic above derives a slightly shorter child budget without clamping the
5675
+ // role defaults used by ordinary calls.
5676
+ timeoutMs: requestedTimeoutMs === void 0 ? rosterTimeoutMs ?? defaultTimeoutMs : void 0,
5615
5677
  // Give each spawn a fresh id so parallel or repeated delegates
5616
5678
  // can use the same role safely.
5617
5679
  id: `${role}-${randomUUID2().slice(0, 8)}`
@@ -6253,9 +6315,9 @@ var CollabSession = class extends EventEmitter {
6253
6315
  this._timeoutTimer = void 0;
6254
6316
  }
6255
6317
  this.cleanup();
6256
- const error = err instanceof Error ? err : new Error(String(err));
6257
- this.emit("session.error", error);
6258
- throw error;
6318
+ const error2 = err instanceof Error ? err : new Error(String(err));
6319
+ this.emit("session.error", error2);
6320
+ throw error2;
6259
6321
  }
6260
6322
  for (const result of results?.flat() ?? []) {
6261
6323
  await this.parseAndEmit(result);
@@ -7392,13 +7454,13 @@ ${a.reports.map((r) => `- ${r.role}: ${r.verdict}${r.error ? ` (${r.error})` : "
7392
7454
  }
7393
7455
  function summarizeTaskResult(result) {
7394
7456
  const text = typeof result.result === "string" ? result.result : result.result !== void 0 ? JSON.stringify(result.result, null, 2) : "";
7395
- const error = result.error ? `${result.error.kind}: ${result.error.message}` : void 0;
7457
+ const error2 = result.error ? `${result.error.kind}: ${result.error.message}` : void 0;
7396
7458
  return {
7397
7459
  taskId: result.taskId,
7398
7460
  subagentId: result.subagentId,
7399
7461
  status: result.status,
7400
- summary: excerpt(text || error || "(no output)", 4e3),
7401
- error
7462
+ summary: excerpt(text || error2 || "(no output)", 4e3),
7463
+ error: error2
7402
7464
  };
7403
7465
  }
7404
7466
  function assessRoleResult(role, result) {
@@ -10146,7 +10208,7 @@ var Director = class _Director {
10146
10208
  this.subagentBaseline = opts.subagentBaseline ?? DEFAULT_SUBAGENT_BASELINE;
10147
10209
  this.taskResultNotifier = opts.taskResultNotifier;
10148
10210
  this.subagentIdleTimeoutMs = typeof opts.subagentIdleTimeoutMs === "number" && Number.isFinite(opts.subagentIdleTimeoutMs) && opts.subagentIdleTimeoutMs >= 0 ? opts.subagentIdleTimeoutMs : void 0;
10149
- this.retireSubagentOnTaskComplete = opts.retireSubagentOnTaskComplete ?? false;
10211
+ this.retireSubagentOnTaskComplete = opts.retireSubagentOnTaskComplete ?? true;
10150
10212
  this.sharedScratchpadPath = opts.sharedScratchpadPath ?? null;
10151
10213
  this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
10152
10214
  this.maxSpawnDepth = opts.fleetManager?.maxSpawnDepth ?? resolveMaxSpawnDepth(opts.maxSpawnDepth);
@@ -10157,7 +10219,7 @@ var Director = class _Director {
10157
10219
  this.dispatchClassifier = opts.dispatchClassifier;
10158
10220
  this.maxFleetCostUsd = opts.directorBudget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
10159
10221
  this.maxFleetTokens = opts.directorBudget?.maxTokens ?? Number.POSITIVE_INFINITY;
10160
- this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 5;
10222
+ this.maxBudgetExtensions = opts.maxBudgetExtensions ?? 12;
10161
10223
  this.maxLeaderContextLoad = opts.maxLeaderContextLoad ?? 0.85;
10162
10224
  this.maxContext = opts.maxContext ?? 128e3;
10163
10225
  this.modelMatrix = opts.modelMatrix;
@@ -10406,7 +10468,7 @@ var Director = class _Director {
10406
10468
  payload.deny();
10407
10469
  return;
10408
10470
  }
10409
- if (decision.optionId === "stop" || /\bstop\b/i.test(decision.text)) {
10471
+ if (decision.optionId === "stop") {
10410
10472
  payload.deny();
10411
10473
  return;
10412
10474
  }
@@ -10934,6 +10996,7 @@ var Director = class _Director {
10934
10996
  await this.stateCheckpoint.flush().catch((err) => this.logShutdownError("state_checkpoint_flush", err));
10935
10997
  await this.stateCheckpoint.releaseLock().catch((err) => this.logShutdownError("state_checkpoint_lock_release", err));
10936
10998
  }
10999
+ this.largeAnswerStore.clear();
10937
11000
  }
10938
11001
  /**
10939
11002
  * Funnel for shutdown-phase errors. We can't throw — `shutdown()` is
@@ -11135,9 +11198,18 @@ var Director = class _Director {
11135
11198
  }
11136
11199
  async terminate(subagentId) {
11137
11200
  await this.coordinator.stop(subagentId);
11201
+ void this.remove(subagentId).catch(
11202
+ (err) => this.logShutdownError("terminate_remove", err)
11203
+ );
11138
11204
  }
11139
11205
  async terminateAll() {
11206
+ const ids = this.status().subagents.map((s) => s.id);
11140
11207
  await this.coordinator.stopAll();
11208
+ for (const id of ids) {
11209
+ void this.remove(id).catch(
11210
+ (err) => this.logShutdownError("terminate_all_remove", err)
11211
+ );
11212
+ }
11141
11213
  }
11142
11214
  async remove(subagentId) {
11143
11215
  this.clearSubagentIdleRetirement(subagentId);
@@ -11177,7 +11249,7 @@ var Director = class _Director {
11177
11249
  const timer = setTimeout(() => {
11178
11250
  this.subagentIdleTimers.delete(subagentId);
11179
11251
  const entry = this.coordinator.getStatus().subagents.find((a) => a.id === subagentId);
11180
- if (!entry || entry.status !== "idle") return;
11252
+ if (entry?.status !== "idle") return;
11181
11253
  if (this.coordinator.listPendingTasks().some((task) => task.subagentId === subagentId)) {
11182
11254
  this.armSubagentIdleRetirement(subagentId, this.subagentIdleTimeoutMs);
11183
11255
  return;
@@ -12769,6 +12841,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
12769
12841
  projectRoot;
12770
12842
  checkpointCas;
12771
12843
  isSessionInUse;
12844
+ logger;
12772
12845
  /**
12773
12846
  * In-memory cache for load() results, keyed by session ID. The cache is
12774
12847
  * invalidated when the file's mtimeMs or size changes (indicating the
@@ -12794,6 +12867,18 @@ var DefaultSessionStore = class _DefaultSessionStore {
12794
12867
  this.events = opts.events;
12795
12868
  this.secretScrubber = opts.secretScrubber;
12796
12869
  this.isSessionInUse = opts.isSessionInUse;
12870
+ this.logger = opts.logger;
12871
+ }
12872
+ /**
12873
+ * Emit a structured warning. Uses the configured Logger when available;
12874
+ * falls back to console.warn(JSON) so warnings are never silently dropped.
12875
+ */
12876
+ logWarn(msg, ctx) {
12877
+ if (this.logger) {
12878
+ this.logger.warn(msg, ctx);
12879
+ } else {
12880
+ console.warn(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
12881
+ }
12797
12882
  }
12798
12883
  /**
12799
12884
  * Clear the load() cache. Useful for testing or when the caller knows
@@ -12807,7 +12892,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
12807
12892
  }
12808
12893
  }
12809
12894
  // ── Storage event helpers ───────────────────────────────────────────────────
12810
- emitRead(sessionId, filePath, operation, outcome, durationMs, error) {
12895
+ emitRead(sessionId, filePath, operation, outcome, durationMs, error2) {
12811
12896
  this.events?.emit("storage.read", {
12812
12897
  sessionId,
12813
12898
  store: "session",
@@ -12815,10 +12900,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
12815
12900
  operation,
12816
12901
  outcome,
12817
12902
  durationMs,
12818
- ...error !== void 0 ? { error } : {}
12903
+ ...error2 !== void 0 ? { error: error2 } : {}
12819
12904
  });
12820
12905
  }
12821
- emitWrite(sessionId, filePath, operation, outcome, durationMs, eventCount, error) {
12906
+ emitWrite(sessionId, filePath, operation, outcome, durationMs, eventCount, error2) {
12822
12907
  this.events?.emit("storage.write", {
12823
12908
  sessionId,
12824
12909
  store: "session",
@@ -12827,16 +12912,16 @@ var DefaultSessionStore = class _DefaultSessionStore {
12827
12912
  outcome,
12828
12913
  durationMs,
12829
12914
  ...eventCount !== void 0 ? { eventCount } : {},
12830
- ...error !== void 0 ? { error } : {}
12915
+ ...error2 !== void 0 ? { error: error2 } : {}
12831
12916
  });
12832
12917
  }
12833
- emitError(sessionId, filePath, operation, error, recoverable) {
12918
+ emitError(sessionId, filePath, operation, error2, recoverable) {
12834
12919
  this.events?.emit("storage.error", {
12835
12920
  sessionId,
12836
12921
  store: "session",
12837
12922
  filePath,
12838
12923
  operation,
12839
- error,
12924
+ error: error2,
12840
12925
  recoverable
12841
12926
  });
12842
12927
  }
@@ -12895,12 +12980,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
12895
12980
  this.emitWrite(id, file, "create", "success", Date.now() - t0);
12896
12981
  return writer;
12897
12982
  } catch (err) {
12898
- await handle.close().catch((e) => console.warn(JSON.stringify({
12899
- level: "warn",
12900
- event: "session_store.handle_close_failed",
12901
- message: e instanceof Error ? e.message : String(e),
12902
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
12903
- })));
12983
+ await handle.close().catch((e) => this.logWarn(
12984
+ "Session handle close failed",
12985
+ { event: "session_store.handle_close_failed", message: e instanceof Error ? e.message : String(e) }
12986
+ ));
12904
12987
  this.emitError(id, file, "create", toErrorMessage(err), true);
12905
12988
  throw err;
12906
12989
  }
@@ -13031,12 +13114,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
13031
13114
  this.emitWrite(id, file, "resume", "success", Date.now() - t0);
13032
13115
  return { writer, data: resumedData };
13033
13116
  } catch (err) {
13034
- await handle.close().catch((e) => console.warn(JSON.stringify({
13035
- level: "warn",
13036
- event: "session_store.handle_close_failed",
13037
- message: e instanceof Error ? e.message : String(e),
13038
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
13039
- })));
13117
+ await handle.close().catch((e) => this.logWarn(
13118
+ "Session handle close failed",
13119
+ { event: "session_store.handle_close_failed", message: e instanceof Error ? e.message : String(e) }
13120
+ ));
13040
13121
  this.emitError(id, file, "resume", toErrorMessage(err), true);
13041
13122
  throw err;
13042
13123
  }
@@ -13657,13 +13738,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
13657
13738
  await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
13658
13739
  const msg = toErrorMessage(err);
13659
13740
  this.emitError(id, manifest, "summary_fallback", msg, true);
13660
- console.warn(JSON.stringify({
13661
- level: "warn",
13662
- event: "session_store.manifest_write_failed",
13663
- sessionId: id,
13664
- message: msg,
13665
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
13666
- }));
13741
+ this.logWarn(
13742
+ "Session manifest write failed",
13743
+ { event: "session_store.manifest_write_failed", sessionId: id, message: msg }
13744
+ );
13667
13745
  });
13668
13746
  outcome = "failure";
13669
13747
  errorMsg = "summary fallback \xE2\u20AC\u201D manifest rebuilt";
@@ -14280,12 +14358,12 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
14280
14358
  const stillHard = afterLoad >= pressure.hardThreshold;
14281
14359
  const fatal = stillHard && (this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard");
14282
14360
  if (stillHard) {
14283
- const error = new Error(
14361
+ const error2 = new Error(
14284
14362
  `Auto-compaction left context above the hard threshold after ${pressure.level} compaction`
14285
14363
  );
14286
14364
  this.events?.emit("compaction.failed", {
14287
14365
  sessionId: ctx.session.id,
14288
- err: error,
14366
+ err: error2,
14289
14367
  aggressive,
14290
14368
  level: pressure.level,
14291
14369
  tokens: afterTokens,
@@ -14309,11 +14387,11 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
14309
14387
  }
14310
14388
  }
14311
14389
  } catch (err) {
14312
- const error = err instanceof Error ? err : new Error(String(err));
14390
+ const error2 = err instanceof Error ? err : new Error(String(err));
14313
14391
  const fatal = this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard";
14314
14392
  this.events?.emit("compaction.failed", {
14315
14393
  sessionId: ctx.session.id,
14316
- err: error,
14394
+ err: error2,
14317
14395
  aggressive,
14318
14396
  level: pressure.level,
14319
14397
  tokens: pressure.tokens,
@@ -14374,19 +14452,27 @@ function adaptThresholdsForSignals(thresholds, signals) {
14374
14452
 
14375
14453
  // src/execution/autonomous-runner.ts
14376
14454
  var DoneConditionChecker = class {
14377
- constructor(condition) {
14455
+ constructor(condition, opts) {
14378
14456
  this.condition = condition;
14457
+ this.logger = opts?.logger;
14379
14458
  if (condition.type === "output_match" && condition.pattern) {
14380
14459
  const result = compileUserRegex(condition.pattern, "");
14381
14460
  this.compiledRegex = result.ok ? result.regex : null;
14382
14461
  if (!result.ok) {
14383
- console.warn(JSON.stringify({
14384
- level: "warn",
14385
- event: "autonomous.done_condition_invalid_regex",
14386
- pattern: condition.pattern,
14387
- reason: result.reason,
14388
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
14389
- }));
14462
+ if (this.logger) {
14463
+ this.logger.warn(
14464
+ "Done condition has invalid regex \u2014 will never match",
14465
+ { event: "autonomous.done_condition_invalid_regex", pattern: condition.pattern, reason: result.reason }
14466
+ );
14467
+ } else {
14468
+ console.warn(JSON.stringify({
14469
+ level: "warn",
14470
+ event: "autonomous.done_condition_invalid_regex",
14471
+ pattern: condition.pattern,
14472
+ reason: result.reason,
14473
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
14474
+ }));
14475
+ }
14390
14476
  }
14391
14477
  } else {
14392
14478
  this.compiledRegex = null;
@@ -14394,6 +14480,7 @@ var DoneConditionChecker = class {
14394
14480
  }
14395
14481
  condition;
14396
14482
  compiledRegex;
14483
+ logger;
14397
14484
  check(state) {
14398
14485
  switch (this.condition.type) {
14399
14486
  case "iterations":
@@ -14444,7 +14531,7 @@ var DoneConditionChecker = class {
14444
14531
  var AutonomousRunner = class {
14445
14532
  constructor(opts) {
14446
14533
  this.opts = opts;
14447
- this.doneChecker = new DoneConditionChecker(opts.doneCondition);
14534
+ this.doneChecker = new DoneConditionChecker(opts.doneCondition, { logger: opts.logger });
14448
14535
  }
14449
14536
  opts;
14450
14537
  iterations = 0;
@@ -14555,7 +14642,7 @@ var MAX_JOURNAL_ENTRIES = 500;
14555
14642
  function goalFilePath(projectRoot) {
14556
14643
  return resolveWstackPaths({ projectRoot }).projectGoal;
14557
14644
  }
14558
- async function loadGoal(filePath, events) {
14645
+ async function loadGoal(filePath, events, warn) {
14559
14646
  const t0 = Date.now();
14560
14647
  let raw;
14561
14648
  try {
@@ -14586,13 +14673,9 @@ async function loadGoal(filePath, events) {
14586
14673
  try {
14587
14674
  const parsed = JSON.parse(raw);
14588
14675
  if (parsed?.version !== 1 || typeof parsed.goal !== "string" || !Array.isArray(parsed.journal)) {
14589
- console.warn(JSON.stringify({
14590
- level: "warn",
14591
- event: "goal_store.invalid_schema",
14592
- path: filePath,
14593
- message: "invalid schema \u2014 consider deleting and re-creating",
14594
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
14595
- }));
14676
+ (warn ?? ((msg) => console.warn(JSON.stringify({ level: "warn", event: "goal_store.invalid_schema", path: filePath, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }))))(
14677
+ "invalid schema \u2014 consider deleting and re-creating"
14678
+ );
14596
14679
  events?.emit("storage.read", {
14597
14680
  sessionId: "~boot~",
14598
14681
  store: "goal",
@@ -14614,13 +14697,9 @@ async function loadGoal(filePath, events) {
14614
14697
  });
14615
14698
  return parsed;
14616
14699
  } catch {
14617
- console.warn(JSON.stringify({
14618
- level: "warn",
14619
- event: "goal_store.parse_failed",
14620
- path: filePath,
14621
- message: "JSON parse failed \u2014 consider deleting and re-creating",
14622
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
14623
- }));
14700
+ (warn ?? ((msg) => console.warn(JSON.stringify({ level: "warn", event: "goal_store.parse_failed", path: filePath, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }))))(
14701
+ "JSON parse failed \u2014 consider deleting and re-creating"
14702
+ );
14624
14703
  events?.emit("storage.read", {
14625
14704
  sessionId: "~boot~",
14626
14705
  store: "goal",
@@ -14993,8 +15072,8 @@ function summarizeToolResultElision(block, tokens) {
14993
15072
  if (block.name) parts.push(`tool=${block.name}`);
14994
15073
  const files = extractPathHints(block.content).slice(0, 5);
14995
15074
  if (files.length > 0) parts.push(`files=${files.join(", ")}`);
14996
- const error = firstErrorLine(block.content);
14997
- if (error) parts.push(`error=${error}`);
15075
+ const error2 = firstErrorLine(block.content);
15076
+ if (error2) parts.push(`error=${error2}`);
14998
15077
  return `[${parts.join("; ")}]`;
14999
15078
  }
15000
15079
  function extractPathHints(content) {
@@ -16530,22 +16609,40 @@ var EternalAutonomyEngine = class {
16530
16609
  currentCtrl = null;
16531
16610
  iterationsSinceCompact = 0;
16532
16611
  goalPath;
16612
+ /**
16613
+ * Emit a structured error. Uses the configured Logger when available;
16614
+ * falls back to console.error(JSON) so events are never silently dropped.
16615
+ */
16616
+ logError(msg, ctx) {
16617
+ if (this.opts.logger) {
16618
+ this.opts.logger.error(msg, ctx);
16619
+ } else {
16620
+ console.error(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
16621
+ }
16622
+ }
16533
16623
  /** Current engine state — readable for UIs. */
16534
16624
  get currentState() {
16535
16625
  return this.state;
16536
16626
  }
16627
+ /**
16628
+ * Load the goal file with structured logging for parse/schema warnings.
16629
+ */
16630
+ loadGoal() {
16631
+ return loadGoal(
16632
+ this.goalPath,
16633
+ this.opts.events,
16634
+ this.opts.logger ? (msg) => this.opts.logger.warn(msg) : void 0
16635
+ );
16636
+ }
16537
16637
  /** Synchronously request stop. Resolves once the running iteration aborts. */
16538
16638
  stop() {
16539
16639
  this.stopRequested = true;
16540
16640
  this.currentCtrl?.abort();
16541
16641
  void this.persistEngineState("stopped").catch((err) => {
16542
- console.error(JSON.stringify({
16543
- level: "error",
16544
- event: "engine.persist_state_failed",
16545
- message: toErrorMessage(err),
16546
- context: { expectedState: "stopped" },
16547
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
16548
- }));
16642
+ this.logError(
16643
+ "Engine persist state failed",
16644
+ { event: "engine.persist_state_failed", message: toErrorMessage(err), context: { expectedState: "stopped" } }
16645
+ );
16549
16646
  });
16550
16647
  this.state = "stopped";
16551
16648
  }
@@ -16601,7 +16698,7 @@ var EternalAutonomyEngine = class {
16601
16698
  const emit = (stage) => {
16602
16699
  this.opts.onStage?.(stage);
16603
16700
  };
16604
- const goal = await loadGoal(this.goalPath, this.opts.events);
16701
+ const goal = await this.loadGoal();
16605
16702
  if (!goal) {
16606
16703
  emit({ phase: "stopped" });
16607
16704
  this.stopRequested = true;
@@ -16705,16 +16802,13 @@ var EternalAutonomyEngine = class {
16705
16802
  emit({ phase: "reflect", status, note });
16706
16803
  let iterationIndex = 0;
16707
16804
  try {
16708
- const reloaded = await loadGoal(this.goalPath, this.opts.events);
16805
+ const reloaded = await this.loadGoal();
16709
16806
  iterationIndex = reloaded?.iterations ?? 0;
16710
16807
  } catch (err) {
16711
- console.error(JSON.stringify({
16712
- level: "warn",
16713
- event: "autonomy.goal_reload_failed",
16714
- message: toErrorMessage(err),
16715
- context: { goalPath: this.goalPath },
16716
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
16717
- }));
16808
+ this.logError(
16809
+ "Goal reload failed",
16810
+ { event: "autonomy.goal_reload_failed", message: toErrorMessage(err), context: { goalPath: this.goalPath } }
16811
+ );
16718
16812
  }
16719
16813
  this.opts.onIteration?.({
16720
16814
  at: (this.opts.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
@@ -16950,13 +17044,10 @@ ${lastFew}` : "No prior iterations yet.",
16950
17044
  clearTimeout(timer);
16951
17045
  }
16952
17046
  } catch (err) {
16953
- console.error(JSON.stringify({
16954
- level: "warn",
16955
- event: "autonomy.brainstorm_failed",
16956
- message: toErrorMessage(err),
16957
- context: { goal: goal.goal.slice(0, 100) },
16958
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
16959
- }));
17047
+ this.logError(
17048
+ "Brainstorm failed",
17049
+ { event: "autonomy.brainstorm_failed", message: toErrorMessage(err), context: { goal: goal.goal.slice(0, 100) } }
17050
+ );
16960
17051
  return null;
16961
17052
  }
16962
17053
  }
@@ -17039,7 +17130,7 @@ ${recentJournal}` : "No prior iterations.",
17039
17130
  }
17040
17131
  }
17041
17132
  async appendIterationEntry(entry) {
17042
- const current = await loadGoal(this.goalPath, this.opts.events);
17133
+ const current = await this.loadGoal();
17043
17134
  if (!current) {
17044
17135
  return;
17045
17136
  }
@@ -17053,7 +17144,7 @@ ${recentJournal}` : "No prior iterations.",
17053
17144
  * the counter to rotate past stuck todos once they cross `todoMaxAttempts`.
17054
17145
  */
17055
17146
  async bumpTodoAttempt(todoId) {
17056
- const current = await loadGoal(this.goalPath, this.opts.events);
17147
+ const current = await this.loadGoal();
17057
17148
  if (!current) return;
17058
17149
  const attempts = { ...current.todoAttempts ?? {} };
17059
17150
  attempts[todoId] = (attempts[todoId] ?? 0) + 1;
@@ -17067,7 +17158,7 @@ ${recentJournal}` : "No prior iterations.",
17067
17158
  * goal is already `completed`.
17068
17159
  */
17069
17160
  async markGoalCompleted(action, note) {
17070
- const current = await loadGoal(this.goalPath, this.opts.events);
17161
+ const current = await this.loadGoal();
17071
17162
  if (!current) return;
17072
17163
  if (current.goalState === "completed") return;
17073
17164
  const withFlag = { ...current, goalState: "completed" };
@@ -17078,6 +17169,11 @@ ${recentJournal}` : "No prior iterations.",
17078
17169
  note: note.slice(0, 240)
17079
17170
  });
17080
17171
  await saveGoal(this.goalPath, withEntry, this.opts.events);
17172
+ try {
17173
+ const { unlink: unlink12 } = await import("node:fs/promises");
17174
+ await unlink12(this.goalPath);
17175
+ } catch {
17176
+ }
17081
17177
  this.opts.onEternalStop?.();
17082
17178
  }
17083
17179
  /**
@@ -17086,7 +17182,7 @@ ${recentJournal}` : "No prior iterations.",
17086
17182
  * `onEternalStop` so the REPL returns to normal mode.
17087
17183
  */
17088
17184
  async clearGoalManually(note) {
17089
- const current = await loadGoal(this.goalPath, this.opts.events);
17185
+ const current = await this.loadGoal();
17090
17186
  if (current) {
17091
17187
  const abandoned = { ...current, goalState: "abandoned" };
17092
17188
  await saveGoal(this.goalPath, abandoned, this.opts.events);
@@ -17134,7 +17230,23 @@ Recent work:
17134
17230
  ${recentJournal}` : ""
17135
17231
  ].join("\n"),
17136
17232
  risk: "high",
17137
- fallback: "continue"
17233
+ fallback: "continue",
17234
+ // Structured verdict — completion is control-plane input, decided by
17235
+ // exact option id rather than sniffing prose for "done"/"complete".
17236
+ options: [
17237
+ {
17238
+ id: "goal_complete",
17239
+ label: "The goal is complete \u2014 stop the eternal run",
17240
+ consequence: "The engine stops and the goal file is archived.",
17241
+ risk: "high"
17242
+ },
17243
+ {
17244
+ id: "keep_working",
17245
+ label: "Not complete yet \u2014 keep working",
17246
+ consequence: "The engine continues iterating on the goal.",
17247
+ risk: "low"
17248
+ }
17249
+ ]
17138
17250
  });
17139
17251
  const summary = formatDecisionSummary(decision, {
17140
17252
  id: `goal-done-${goal.iterations}`,
@@ -17154,8 +17266,7 @@ ${recentJournal}` : ""
17154
17266
  return false;
17155
17267
  }
17156
17268
  if (decision.type === "answer") {
17157
- const text = decision.text.toLowerCase();
17158
- return text.includes("complete") || text.includes("done") || text.includes("yes");
17269
+ return decision.optionId === "goal_complete";
17159
17270
  }
17160
17271
  return true;
17161
17272
  } catch {
@@ -17166,13 +17277,13 @@ ${recentJournal}` : ""
17166
17277
  * Persist a progress update from the agent's [PROGRESS: N%] output.
17167
17278
  */
17168
17279
  async updateProgress(progress, note) {
17169
- const current = await loadGoal(this.goalPath, this.opts.events);
17280
+ const current = await this.loadGoal();
17170
17281
  if (!current) return;
17171
17282
  const updated = recordProgress(current, progress, note);
17172
17283
  await saveGoal(this.goalPath, updated, this.opts.events);
17173
17284
  }
17174
17285
  async persistEngineState(state) {
17175
- const current = await loadGoal(this.goalPath, this.opts.events);
17286
+ const current = await this.loadGoal();
17176
17287
  if (!current) return;
17177
17288
  if (current.engineState === state) return;
17178
17289
  await saveGoal(this.goalPath, { ...current, engineState: state }, this.opts.events);
@@ -17361,6 +17472,27 @@ var ParallelEternalEngine = class {
17361
17472
  agentFactory;
17362
17473
  dispatchEnabled;
17363
17474
  dispatchClassifier;
17475
+ /**
17476
+ * Emit a structured error. Uses the configured Logger when available;
17477
+ * falls back to console.error(JSON) so events are never silently dropped.
17478
+ */
17479
+ logError(msg, ctx) {
17480
+ if (this.opts.logger) {
17481
+ this.opts.logger.error(msg, ctx);
17482
+ } else {
17483
+ console.error(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
17484
+ }
17485
+ }
17486
+ /**
17487
+ * Load the goal file with structured logging for parse/schema warnings.
17488
+ */
17489
+ loadGoal() {
17490
+ return loadGoal(
17491
+ this.goalPath,
17492
+ this.opts.events,
17493
+ this.opts.logger ? (msg) => this.opts.logger.warn(msg) : void 0
17494
+ );
17495
+ }
17364
17496
  get currentState() {
17365
17497
  return this.state;
17366
17498
  }
@@ -17373,13 +17505,10 @@ var ParallelEternalEngine = class {
17373
17505
  stop() {
17374
17506
  this.stopRequested = true;
17375
17507
  void this.persistState("stopped").catch((err) => {
17376
- console.error(JSON.stringify({
17377
- level: "error",
17378
- event: "engine.persist_state_failed",
17379
- message: toErrorMessage(err),
17380
- context: { expectedState: "stopped" },
17381
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
17382
- }));
17508
+ this.logError(
17509
+ "Engine persist state failed",
17510
+ { event: "engine.persist_state_failed", message: toErrorMessage(err), context: { expectedState: "stopped" } }
17511
+ );
17383
17512
  });
17384
17513
  this.state = "stopped";
17385
17514
  }
@@ -17433,7 +17562,7 @@ var ParallelEternalEngine = class {
17433
17562
  this.opts.onStage?.(stage);
17434
17563
  };
17435
17564
  this.iterations++;
17436
- const goal = await loadGoal(this.goalPath, this.opts.events);
17565
+ const goal = await this.loadGoal();
17437
17566
  if (!goal) {
17438
17567
  this.stopRequested = true;
17439
17568
  emit({ phase: "stopped" });
@@ -17577,13 +17706,10 @@ ${personaLine}Task: ${task}
17577
17706
  taskIds.push(taskId);
17578
17707
  await coordinator.assign(spec);
17579
17708
  } catch (err) {
17580
- console.error(JSON.stringify({
17581
- level: "warn",
17582
- event: "parallel_engine.spawn_failed",
17583
- message: toErrorMessage(err),
17584
- context: { slot: i, task, subagentId },
17585
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
17586
- }));
17709
+ this.logError(
17710
+ "Parallel spawn failed",
17711
+ { event: "parallel_engine.spawn_failed", message: toErrorMessage(err), context: { slot: i, task, subagentId } }
17712
+ );
17587
17713
  }
17588
17714
  })()
17589
17715
  );
@@ -17609,13 +17735,10 @@ ${personaLine}Task: ${task}
17609
17735
  clearTimeout(timer);
17610
17736
  }
17611
17737
  } catch (err) {
17612
- console.error(JSON.stringify({
17613
- level: "warn",
17614
- event: "parallel_engine.brainstorm_results_failed",
17615
- message: toErrorMessage(err),
17616
- context: { slotCount, taskIds },
17617
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
17618
- }));
17738
+ this.logError(
17739
+ "Brainstorm results failed",
17740
+ { event: "parallel_engine.brainstorm_results_failed", message: toErrorMessage(err), context: { slotCount, taskIds } }
17741
+ );
17619
17742
  results = coordinator.results().slice(-taskIds.length);
17620
17743
  }
17621
17744
  await Promise.allSettled(subagentIds.map((id) => coordinator.remove(id)));
@@ -17650,13 +17773,10 @@ ${personaLine}Task: ${task}
17650
17773
  }
17651
17774
  }
17652
17775
  } catch (err) {
17653
- console.error(JSON.stringify({
17654
- level: "warn",
17655
- event: "parallel_engine.git_status_failed",
17656
- message: toErrorMessage(err),
17657
- context: { projectRoot: this.opts.projectRoot },
17658
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
17659
- }));
17776
+ this.logError(
17777
+ "Git status failed",
17778
+ { event: "parallel_engine.git_status_failed", message: toErrorMessage(err), context: { projectRoot: this.opts.projectRoot } }
17779
+ );
17660
17780
  }
17661
17781
  }
17662
17782
  if (tasks.length < this.slots) {
@@ -17737,7 +17857,7 @@ ${lastFew}` : "No prior iterations.",
17737
17857
  // Helpers
17738
17858
  // -------------------------------------------------------------------------
17739
17859
  async appendIterationEntry(entry) {
17740
- const current = await loadGoal(this.goalPath, this.opts.events);
17860
+ const current = await this.loadGoal();
17741
17861
  if (!current) return;
17742
17862
  const updated = appendJournal(current, entry);
17743
17863
  await saveGoal(this.goalPath, updated, this.opts.events);
@@ -17752,7 +17872,7 @@ ${lastFew}` : "No prior iterations.",
17752
17872
  await this.appendIterationEntry({ source: "manual", task, status: "failure", note });
17753
17873
  }
17754
17874
  async persistState(state) {
17755
- const current = await loadGoal(this.goalPath, this.opts.events);
17875
+ const current = await this.loadGoal();
17756
17876
  if (!current) return;
17757
17877
  if (current.engineState === state) return;
17758
17878
  await saveGoal(this.goalPath, { ...current, engineState: state }, this.opts.events);
@@ -19991,12 +20111,12 @@ ${post.additionalContext}`;
19991
20111
  if (parentSignal.reason instanceof Error) throw parentSignal.reason;
19992
20112
  throw new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
19993
20113
  }
19994
- const timeoutMs = clampTimeoutMs(
19995
- tool.timeoutMs ?? this.iterationTimeoutMs,
19996
- this.maxToolTimeoutMs
19997
- );
19998
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
19999
- const combined = AbortSignal.any([parentSignal, timeoutSignal]);
20114
+ const combined = tool.managesOwnTimeout ? parentSignal : AbortSignal.any([
20115
+ parentSignal,
20116
+ AbortSignal.timeout(
20117
+ clampTimeoutMs(tool.timeoutMs ?? this.iterationTimeoutMs, this.maxToolTimeoutMs)
20118
+ )
20119
+ ]);
20000
20120
  let output;
20001
20121
  const toolPromise = typeof tool.executeStream === "function" ? this.runStreamedTool(tool, input, ctx, combined, toolUseId) : (async () => tool.execute(input, ctx, { signal: combined }))();
20002
20122
  toolPromise.catch(() => {
@@ -21960,34 +22080,35 @@ function wireMetricsToEvents(events, sink) {
21960
22080
  retryable: String(e.retryable)
21961
22081
  })
21962
22082
  ),
21963
- events.on("tool.started", (e) => sink.counter("tool.starts.total", 1, { tool: e.name })),
22083
+ events.on(
22084
+ "tool.started",
22085
+ (e) => sink.counter("tool.starts.total", 1, { tool: metricToolName(e.name) })
22086
+ ),
21964
22087
  events.on("tool.executed", (e) => {
21965
- sink.counter("tool.executions.total", 1, { tool: e.name, ok: String(e.ok) });
21966
- sink.histogram("tool.duration_ms", e.durationMs, { tool: e.name });
22088
+ const tool = metricToolName(e.name);
22089
+ sink.counter("tool.executions.total", 1, { tool, ok: String(e.ok) });
22090
+ sink.histogram("tool.duration_ms", e.durationMs, { tool });
21967
22091
  }),
21968
22092
  events.on("token.threshold", (e) => sink.gauge("agent.tokens.used", e.used)),
21969
22093
  events.on("compaction.fired", (e) => {
21970
22094
  sink.counter("compaction.fired.total");
21971
22095
  sink.histogram("compaction.reduction_tokens", e.report.before - e.report.after);
21972
22096
  }),
21973
- events.on(
21974
- "mcp.server.connected",
21975
- (e) => sink.counter("mcp.connects.total", 1, { server: e.name })
21976
- ),
21977
- events.on(
21978
- "mcp.server.reconnected",
21979
- (e) => sink.counter("mcp.reconnects.total", 1, { server: e.name })
21980
- ),
21981
- events.on(
21982
- "mcp.server.disconnected",
21983
- (e) => sink.counter("mcp.disconnects.total", 1, { server: e.name })
21984
- ),
22097
+ // MCP server names are user-controlled and may contain tenant/project
22098
+ // identifiers. Keep them out of metric labels to bound cardinality and
22099
+ // prevent operational metadata leaks.
22100
+ events.on("mcp.server.connected", () => sink.counter("mcp.connects.total")),
22101
+ events.on("mcp.server.reconnected", () => sink.counter("mcp.reconnects.total")),
22102
+ events.on("mcp.server.disconnected", () => sink.counter("mcp.disconnects.total")),
21985
22103
  events.on("error", (e) => sink.counter("agent.errors.total", 1, { phase: e.phase }))
21986
22104
  );
21987
22105
  return () => {
21988
22106
  for (const u of unsubs) u();
21989
22107
  };
21990
22108
  }
22109
+ function metricToolName(name) {
22110
+ return name.startsWith("mcp__") ? "mcp_proxy" : name;
22111
+ }
21991
22112
 
21992
22113
  // src/observability/prometheus.ts
21993
22114
  var NUMBER_FORMAT_INFINITY = "NaN";
@@ -22398,6 +22519,172 @@ function startOtlpTraceExporter(opts) {
22398
22519
  // src/security/permission-policy.ts
22399
22520
  import * as fs10 from "node:fs/promises";
22400
22521
 
22522
+ // src/security/permission-policy-schema.ts
22523
+ var TRUST_POLICY_LIMITS = Object.freeze({
22524
+ maxTools: 1e3,
22525
+ maxPatternsPerRule: 1e3,
22526
+ maxToolNameChars: 256,
22527
+ maxPatternChars: 4096
22528
+ });
22529
+ var TRUST_POLICY_JSON_SCHEMA = Object.freeze({
22530
+ $schema: "https://json-schema.org/draft/2020-12/schema",
22531
+ $id: "https://wrongstack.dev/schemas/trust-policy-v1.json",
22532
+ title: "WrongStack Trust Policy",
22533
+ type: "object",
22534
+ maxProperties: TRUST_POLICY_LIMITS.maxTools,
22535
+ propertyNames: {
22536
+ minLength: 1,
22537
+ maxLength: TRUST_POLICY_LIMITS.maxToolNameChars,
22538
+ not: { enum: ["__proto__", "prototype", "constructor"] }
22539
+ },
22540
+ additionalProperties: {
22541
+ type: "object",
22542
+ additionalProperties: false,
22543
+ properties: {
22544
+ allow: {
22545
+ type: "array",
22546
+ maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
22547
+ items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
22548
+ },
22549
+ deny: {
22550
+ type: "array",
22551
+ maxItems: TRUST_POLICY_LIMITS.maxPatternsPerRule,
22552
+ items: { type: "string", minLength: 1, maxLength: TRUST_POLICY_LIMITS.maxPatternChars }
22553
+ },
22554
+ auto: { type: "boolean" },
22555
+ trustWorkdir: { type: "boolean" },
22556
+ denyPrivate: { type: "boolean" }
22557
+ }
22558
+ }
22559
+ });
22560
+ var RULE_FIELDS = /* @__PURE__ */ new Set(["allow", "deny", "auto", "trustWorkdir", "denyPrivate"]);
22561
+ var UNSAFE_PROPERTY_NAMES = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
22562
+ function isRecord5(value) {
22563
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22564
+ }
22565
+ function error(diagnostics, code, path29, message) {
22566
+ diagnostics.push({ severity: "error", code, path: path29, message });
22567
+ }
22568
+ function validatePatterns(value, path29, diagnostics) {
22569
+ if (!Array.isArray(value)) {
22570
+ error(diagnostics, "invalid_pattern_list", path29, "must be an array of strings");
22571
+ return void 0;
22572
+ }
22573
+ if (value.length > TRUST_POLICY_LIMITS.maxPatternsPerRule) {
22574
+ error(
22575
+ diagnostics,
22576
+ "too_many_patterns",
22577
+ path29,
22578
+ `must contain at most ${TRUST_POLICY_LIMITS.maxPatternsPerRule} patterns`
22579
+ );
22580
+ return void 0;
22581
+ }
22582
+ const patterns = [];
22583
+ const seen = /* @__PURE__ */ new Set();
22584
+ for (const [index, pattern] of value.entries()) {
22585
+ const itemPath = `${path29}[${index}]`;
22586
+ if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > TRUST_POLICY_LIMITS.maxPatternChars) {
22587
+ error(
22588
+ diagnostics,
22589
+ "invalid_pattern",
22590
+ itemPath,
22591
+ `must be a non-empty string up to ${TRUST_POLICY_LIMITS.maxPatternChars} characters`
22592
+ );
22593
+ continue;
22594
+ }
22595
+ if (seen.has(pattern)) {
22596
+ diagnostics.push({
22597
+ severity: "warning",
22598
+ code: "duplicate_pattern",
22599
+ path: itemPath,
22600
+ message: "duplicates an earlier pattern and was normalized away"
22601
+ });
22602
+ continue;
22603
+ }
22604
+ seen.add(pattern);
22605
+ patterns.push(pattern);
22606
+ }
22607
+ return patterns;
22608
+ }
22609
+ function validateTrustPolicy(value) {
22610
+ const diagnostics = [];
22611
+ if (!isRecord5(value)) {
22612
+ return {
22613
+ ok: false,
22614
+ diagnostics: [
22615
+ {
22616
+ severity: "error",
22617
+ code: "invalid_root",
22618
+ path: "$",
22619
+ message: "trust policy must be an object"
22620
+ }
22621
+ ]
22622
+ };
22623
+ }
22624
+ const entries = Object.entries(value);
22625
+ if (entries.length > TRUST_POLICY_LIMITS.maxTools) {
22626
+ error(
22627
+ diagnostics,
22628
+ "too_many_tools",
22629
+ "$",
22630
+ `must contain at most ${TRUST_POLICY_LIMITS.maxTools} tool rules`
22631
+ );
22632
+ }
22633
+ const policy = {};
22634
+ for (const [toolName, rawRule] of entries.slice(0, TRUST_POLICY_LIMITS.maxTools)) {
22635
+ const toolPath = `$[${JSON.stringify(toolName)}]`;
22636
+ if (toolName.length === 0 || toolName.length > TRUST_POLICY_LIMITS.maxToolNameChars) {
22637
+ error(
22638
+ diagnostics,
22639
+ "invalid_tool_name",
22640
+ toolPath,
22641
+ `tool name must be 1-${TRUST_POLICY_LIMITS.maxToolNameChars} characters`
22642
+ );
22643
+ continue;
22644
+ }
22645
+ if (UNSAFE_PROPERTY_NAMES.has(toolName)) {
22646
+ error(
22647
+ diagnostics,
22648
+ "unsafe_tool_name",
22649
+ toolPath,
22650
+ "reserved object property names are not valid tool rules"
22651
+ );
22652
+ continue;
22653
+ }
22654
+ if (!isRecord5(rawRule)) {
22655
+ error(diagnostics, "invalid_rule", toolPath, "tool rule must be an object");
22656
+ continue;
22657
+ }
22658
+ for (const field of Object.keys(rawRule)) {
22659
+ if (!RULE_FIELDS.has(field)) {
22660
+ error(
22661
+ diagnostics,
22662
+ "unknown_field",
22663
+ `${toolPath}.${field}`,
22664
+ `unknown policy field "${field}"`
22665
+ );
22666
+ }
22667
+ }
22668
+ const rule = {};
22669
+ for (const field of ["allow", "deny"]) {
22670
+ if (rawRule[field] === void 0) continue;
22671
+ const patterns = validatePatterns(rawRule[field], `${toolPath}.${field}`, diagnostics);
22672
+ if (patterns) rule[field] = patterns;
22673
+ }
22674
+ for (const field of ["auto", "trustWorkdir", "denyPrivate"]) {
22675
+ const setting = rawRule[field];
22676
+ if (setting === void 0) continue;
22677
+ if (typeof setting !== "boolean") {
22678
+ error(diagnostics, "invalid_boolean", `${toolPath}.${field}`, "must be a boolean");
22679
+ } else {
22680
+ rule[field] = setting;
22681
+ }
22682
+ }
22683
+ policy[toolName] = rule;
22684
+ }
22685
+ return diagnostics.some((diagnostic) => diagnostic.severity === "error") ? { ok: false, diagnostics } : { ok: true, policy, diagnostics };
22686
+ }
22687
+
22401
22688
  // src/utils/lru-cache.ts
22402
22689
  var LruCache = class {
22403
22690
  store = /* @__PURE__ */ new Map();
@@ -22566,6 +22853,8 @@ var DefaultPermissionPolicy = class {
22566
22853
  * the LRU evicts the least-recently-used entries first.
22567
22854
  */
22568
22855
  _evalCache = new LruCache(500);
22856
+ policyDiagnostics = [];
22857
+ policyInvalid = false;
22569
22858
  constructor(opts) {
22570
22859
  this.trustFile = opts.trustFile;
22571
22860
  this.yolo = opts.yolo ?? false;
@@ -22609,13 +22898,50 @@ var DefaultPermissionPolicy = class {
22609
22898
  getConfirmDestructive() {
22610
22899
  return this.confirmDestructive;
22611
22900
  }
22901
+ /** Read-only diagnostics for policy inspector/editor surfaces. */
22902
+ getPolicyDiagnostics() {
22903
+ return this.policyDiagnostics.map((diagnostic) => ({ ...diagnostic }));
22904
+ }
22612
22905
  async reload() {
22906
+ this.policyDiagnostics = [];
22907
+ this.policyInvalid = false;
22613
22908
  try {
22614
22909
  const raw = await fs10.readFile(this.trustFile, "utf8");
22615
22910
  const parsed = safeParse(raw);
22616
- if (parsed.ok && parsed.value) this.policy = parsed.value;
22617
- } catch {
22911
+ if (!parsed.ok) {
22912
+ this.policy = {};
22913
+ this.policyInvalid = true;
22914
+ this.policyDiagnostics = [
22915
+ {
22916
+ severity: "error",
22917
+ code: "invalid_json",
22918
+ path: "$",
22919
+ message: parsed.error ?? "trust policy is not valid JSON"
22920
+ }
22921
+ ];
22922
+ } else {
22923
+ const validation = validateTrustPolicy(parsed.value);
22924
+ this.policyDiagnostics = validation.diagnostics;
22925
+ if (validation.ok) {
22926
+ this.policy = validation.policy;
22927
+ } else {
22928
+ this.policy = {};
22929
+ this.policyInvalid = true;
22930
+ }
22931
+ }
22932
+ } catch (err) {
22618
22933
  this.policy = {};
22934
+ if (err.code !== "ENOENT") {
22935
+ this.policyInvalid = true;
22936
+ this.policyDiagnostics = [
22937
+ {
22938
+ severity: "error",
22939
+ code: "read_error",
22940
+ path: "$",
22941
+ message: err instanceof Error ? err.message : String(err)
22942
+ }
22943
+ ];
22944
+ }
22619
22945
  }
22620
22946
  this.wildcardEntries = [];
22621
22947
  for (const [key, val] of Object.entries(this.policy)) {
@@ -22628,6 +22954,13 @@ var DefaultPermissionPolicy = class {
22628
22954
  }
22629
22955
  async evaluate(tool, input, ctx) {
22630
22956
  if (!this.loaded) await this.reload();
22957
+ if (this.policyInvalid) {
22958
+ return {
22959
+ permission: "deny",
22960
+ source: "deny",
22961
+ reason: "trust policy is invalid; refusing tool execution until it is repaired"
22962
+ };
22963
+ }
22631
22964
  const namespaceEntry = this.findNamespaceEntry(tool.name);
22632
22965
  const entry = this.policy[tool.name] ?? namespaceEntry;
22633
22966
  const subject = subjectForToolInput(tool.name, input, tool.subjectKey);
@@ -22757,6 +23090,9 @@ var DefaultPermissionPolicy = class {
22757
23090
  }
22758
23091
  async trust(rule) {
22759
23092
  if (!this.loaded) await this.reload();
23093
+ if (this.policyInvalid) {
23094
+ throw new Error("Cannot update trust rules while trust.json is invalid; repair it first.");
23095
+ }
22760
23096
  const entry = this.policy[rule.tool] ?? {};
22761
23097
  entry.allow = Array.from(/* @__PURE__ */ new Set([...entry.allow ?? [], rule.pattern]));
22762
23098
  this.policy[rule.tool] = entry;
@@ -22775,6 +23111,9 @@ var DefaultPermissionPolicy = class {
22775
23111
  /** Persist a deny rule — this tool+pattern pair is permanently blocked. */
22776
23112
  async deny(rule) {
22777
23113
  if (!this.loaded) await this.reload();
23114
+ if (this.policyInvalid) {
23115
+ throw new Error("Cannot update deny rules while trust.json is invalid; repair it first.");
23116
+ }
22778
23117
  const entry = this.policy[rule.tool] ?? {};
22779
23118
  entry.deny = Array.from(/* @__PURE__ */ new Set([...entry.deny ?? [], rule.pattern]));
22780
23119
  this.policy[rule.tool] = entry;
@@ -23178,31 +23517,40 @@ function unwrapDataKey(buf, keyFile) {
23178
23517
  });
23179
23518
  }
23180
23519
  }
23181
- function checkKeyFilePermissions(keyFile) {
23520
+ function checkKeyFilePermissions(keyFile, opts) {
23182
23521
  if (process.platform === "win32") return;
23522
+ const warn = opts?.warn ?? ((msg) => console.warn(msg));
23183
23523
  try {
23184
23524
  const stat13 = fs11.statSync(keyFile);
23185
23525
  const actualMode = stat13.mode & 511;
23186
23526
  if (actualMode !== KEY_FILE_MODE) {
23187
- console.warn(JSON.stringify({
23188
- level: "warn",
23189
- event: "vault.key_file_wrong_permissions",
23190
- message: `Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Run: chmod ${KEY_FILE_MODE.toString(8)} ${keyFile}`,
23191
- keyFile,
23192
- expectedMode: KEY_FILE_MODE,
23193
- actualMode,
23194
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
23195
- }));
23527
+ warn(
23528
+ `Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Run: chmod ${KEY_FILE_MODE.toString(8)} ${keyFile}`
23529
+ );
23196
23530
  }
23197
23531
  } catch {
23198
23532
  }
23199
23533
  }
23200
23534
  var DefaultSecretVault = class {
23201
23535
  keyFile;
23536
+ logger;
23202
23537
  key;
23203
23538
  _keyVersion = 1;
23204
23539
  constructor(opts) {
23205
23540
  this.keyFile = opts.keyFile;
23541
+ this.logger = opts.logger;
23542
+ }
23543
+ /**
23544
+ * Emit a structured warning. Uses the configured Logger when available;
23545
+ * falls back to console.warn(JSON) so warnings are never silently dropped
23546
+ * during early boot.
23547
+ */
23548
+ logWarn(msg, ctx) {
23549
+ if (this.logger) {
23550
+ this.logger.warn(msg, ctx);
23551
+ } else {
23552
+ console.warn(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
23553
+ }
23206
23554
  }
23207
23555
  /** Current key version. Starts at 1; incremented by rotateKey(). */
23208
23556
  get keyVersion() {
@@ -23281,7 +23629,7 @@ var DefaultSecretVault = class {
23281
23629
  newKey.copy(keyFileBuf, KEY_FILE_MAGIC.length + 1);
23282
23630
  fs11.writeFileSync(this.keyFile, keyFileBuf, { mode: 384 });
23283
23631
  }
23284
- checkKeyFilePermissions(this.keyFile);
23632
+ checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
23285
23633
  this.key = newKey;
23286
23634
  this._keyVersion = newVersion;
23287
23635
  return { oldVersion, newVersion };
@@ -23300,7 +23648,7 @@ var DefaultSecretVault = class {
23300
23648
  fs11.writeFileSync(this.keyFile, wrapDataKey(this.key, this._keyVersion, passphrase), {
23301
23649
  mode: 384
23302
23650
  });
23303
- checkKeyFilePermissions(this.keyFile);
23651
+ checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
23304
23652
  } catch {
23305
23653
  }
23306
23654
  }
@@ -23312,13 +23660,13 @@ var DefaultSecretVault = class {
23312
23660
  const { key: key2, version } = unwrapDataKey(buf, this.keyFile);
23313
23661
  this.key = key2;
23314
23662
  this._keyVersion = version;
23315
- checkKeyFilePermissions(this.keyFile);
23663
+ checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
23316
23664
  return this.key;
23317
23665
  }
23318
23666
  if (buf.length === KEY_BYTES) {
23319
23667
  this.key = buf;
23320
23668
  this._keyVersion = 1;
23321
- checkKeyFilePermissions(this.keyFile);
23669
+ checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
23322
23670
  this.migrateToWrappedIfPassphrase();
23323
23671
  return this.key;
23324
23672
  }
@@ -23342,7 +23690,7 @@ var DefaultSecretVault = class {
23342
23690
  }
23343
23691
  this.key = Buffer.from(key2);
23344
23692
  this._keyVersion = version;
23345
- checkKeyFilePermissions(this.keyFile);
23693
+ checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
23346
23694
  this.migrateToWrappedIfPassphrase();
23347
23695
  return this.key;
23348
23696
  }
@@ -23367,13 +23715,13 @@ var DefaultSecretVault = class {
23367
23715
  const { key: winnerKey, version } = unwrapDataKey(buf, this.keyFile);
23368
23716
  this.key = winnerKey;
23369
23717
  this._keyVersion = version;
23370
- checkKeyFilePermissions(this.keyFile);
23718
+ checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
23371
23719
  return this.key;
23372
23720
  }
23373
23721
  if (buf.length === KEY_BYTES) {
23374
23722
  this.key = buf;
23375
23723
  this._keyVersion = 1;
23376
- checkKeyFilePermissions(this.keyFile);
23724
+ checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
23377
23725
  return this.key;
23378
23726
  }
23379
23727
  if (buf.length === VERSIONED_KEY_FILE_SIZE) {
@@ -23389,7 +23737,7 @@ var DefaultSecretVault = class {
23389
23737
  const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
23390
23738
  this.key = Buffer.from(winnerKey);
23391
23739
  this._keyVersion = version;
23392
- checkKeyFilePermissions(this.keyFile);
23740
+ checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
23393
23741
  return this.key;
23394
23742
  }
23395
23743
  throw new ConfigError({
@@ -23944,6 +24292,10 @@ var KNOWN_DENIED_IN_PROJECT = [
23944
24292
  key: "fleet",
23945
24293
  reason: "Fleet supervision knobs: a repo-committed config could enable autonomous subagent spawning/steering/termination and mailbox traffic on the victim machine."
23946
24294
  },
24295
+ {
24296
+ key: "brain",
24297
+ reason: "Brain decision-layer knobs: a repo-committed config could raise the autonomy risk ceiling, switch the Brain to headless (removing the human tier), or reroute Brain decisions to an attacker-chosen provider/model."
24298
+ },
23947
24299
  {
23948
24300
  key: "git",
23949
24301
  reason: "Carries git.identity (GIT_AUTHOR_*/GIT_COMMITTER_* injection): a repo-committed config could spoof the author identity written into the victim's commit history (impersonation)."
@@ -23992,6 +24344,7 @@ var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
23992
24344
  "extensions",
23993
24345
  "acp",
23994
24346
  "fleet",
24347
+ "brain",
23995
24348
  "git"
23996
24349
  ]);
23997
24350
  function assertInProjectAllowListComplete() {
@@ -24143,6 +24496,7 @@ var DefaultConfigLoader = class {
24143
24496
  extraSources;
24144
24497
  events;
24145
24498
  traceId;
24499
+ logger;
24146
24500
  jsonCache = /* @__PURE__ */ new Map();
24147
24501
  constructor(opts) {
24148
24502
  this.paths = opts.paths;
@@ -24151,6 +24505,19 @@ var DefaultConfigLoader = class {
24151
24505
  this.extraSources = opts.sources ?? [];
24152
24506
  this.events = opts.events;
24153
24507
  this.traceId = opts.traceId;
24508
+ this.logger = opts.logger;
24509
+ }
24510
+ /**
24511
+ * Emit a structured warning. Uses the configured Logger when available;
24512
+ * falls back to console.warn(JSON) so warnings are never silently dropped
24513
+ * during early boot (before a Logger is constructed).
24514
+ */
24515
+ logWarn(msg, ctx) {
24516
+ if (this.logger) {
24517
+ this.logger.warn(msg, ctx);
24518
+ } else {
24519
+ console.warn(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
24520
+ }
24154
24521
  }
24155
24522
  async load(opts = {}) {
24156
24523
  let cfg = { ...BEHAVIOR_DEFAULTS };
@@ -24163,7 +24530,14 @@ var DefaultConfigLoader = class {
24163
24530
  ]);
24164
24531
  cfg = deepMerge2(cfg, global);
24165
24532
  cfg = deepMerge2(cfg, local);
24166
- cfg = deepMerge2(cfg, stripUnsafeInProjectFields(inProject, this.paths.inProjectConfig));
24533
+ cfg = deepMerge2(
24534
+ cfg,
24535
+ stripUnsafeInProjectFields(
24536
+ inProject,
24537
+ this.paths.inProjectConfig,
24538
+ this.logger ? (msg) => this.logger.warn(msg) : void 0
24539
+ )
24540
+ );
24167
24541
  for (const [key, fn] of Object.entries(ENV_MAP)) {
24168
24542
  const v = process.env[key];
24169
24543
  if (v) fn(cfg, v);
@@ -24180,14 +24554,9 @@ var DefaultConfigLoader = class {
24180
24554
  cfg = deepMerge2(cfg, patch);
24181
24555
  }
24182
24556
  } catch (err) {
24183
- console.warn(
24184
- JSON.stringify({
24185
- level: "warn",
24186
- event: "config.source_load_failed",
24187
- source: src.name,
24188
- message: toErrorMessage(err),
24189
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
24190
- })
24557
+ this.logWarn(
24558
+ "Config source load failed",
24559
+ { event: "config.source_load_failed", source: src.name, message: toErrorMessage(err) }
24191
24560
  );
24192
24561
  }
24193
24562
  }
@@ -24247,14 +24616,9 @@ var DefaultConfigLoader = class {
24247
24616
  durationMs: Date.now() - t0,
24248
24617
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
24249
24618
  });
24250
- console.warn(
24251
- JSON.stringify({
24252
- level: "warn",
24253
- event: "config.defaults_read_failed",
24254
- path: fp,
24255
- message: toErrorMessage(err),
24256
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
24257
- })
24619
+ this.logWarn(
24620
+ "Config defaults read failed",
24621
+ { event: "config.defaults_read_failed", path: fp, message: toErrorMessage(err) }
24258
24622
  );
24259
24623
  return;
24260
24624
  }
@@ -24288,14 +24652,9 @@ var DefaultConfigLoader = class {
24288
24652
  durationMs: Date.now() - t0,
24289
24653
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
24290
24654
  });
24291
- console.warn(
24292
- JSON.stringify({
24293
- level: "warn",
24294
- event: "config.defaults_write_failed",
24295
- path: fp,
24296
- message: toErrorMessage(err),
24297
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
24298
- })
24655
+ this.logWarn(
24656
+ "Config defaults write failed",
24657
+ { event: "config.defaults_write_failed", path: fp, message: toErrorMessage(err) }
24299
24658
  );
24300
24659
  }
24301
24660
  }
@@ -24398,13 +24757,9 @@ var DefaultConfigLoader = class {
24398
24757
  error: storageErrorString(err),
24399
24758
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
24400
24759
  });
24401
- console.warn(
24402
- JSON.stringify({
24403
- level: "warn",
24404
- event: "config.sync_load_failed",
24405
- message: toErrorMessage(err),
24406
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
24407
- })
24760
+ this.logWarn(
24761
+ "Config sync load failed",
24762
+ { event: "config.sync_load_failed", message: toErrorMessage(err) }
24408
24763
  );
24409
24764
  return null;
24410
24765
  }
@@ -24434,14 +24789,9 @@ var DefaultConfigLoader = class {
24434
24789
  error: storageErrorString(err),
24435
24790
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
24436
24791
  });
24437
- console.warn(
24438
- JSON.stringify({
24439
- level: "warn",
24440
- event: "config.read_failed",
24441
- path: file,
24442
- message: toErrorMessage(err),
24443
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
24444
- })
24792
+ this.logWarn(
24793
+ "Config read failed (stat)",
24794
+ { event: "config.read_failed", path: file, message: toErrorMessage(err) }
24445
24795
  );
24446
24796
  return {};
24447
24797
  }
@@ -24460,14 +24810,9 @@ var DefaultConfigLoader = class {
24460
24810
  error: storageErrorString(err),
24461
24811
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
24462
24812
  });
24463
- console.warn(
24464
- JSON.stringify({
24465
- level: "warn",
24466
- event: "config.read_failed",
24467
- path: file,
24468
- message: toErrorMessage(err),
24469
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
24470
- })
24813
+ this.logWarn(
24814
+ "Config read failed (read)",
24815
+ { event: "config.read_failed", path: file, message: toErrorMessage(err) }
24471
24816
  );
24472
24817
  }
24473
24818
  this.jsonCache.set(file, { mtimeMs: null, value: {} });
@@ -24485,14 +24830,9 @@ var DefaultConfigLoader = class {
24485
24830
  error: "parse error or empty file",
24486
24831
  ...this.traceId !== void 0 ? { traceId: this.traceId } : {}
24487
24832
  });
24488
- console.warn(
24489
- JSON.stringify({
24490
- level: "warn",
24491
- event: "config.parse_failed",
24492
- path: file,
24493
- message: "invalid JSON \u2014 falling back to defaults for this layer",
24494
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
24495
- })
24833
+ this.logWarn(
24834
+ "Config parse failed \u2014 falling back to defaults",
24835
+ { event: "config.parse_failed", path: file }
24496
24836
  );
24497
24837
  return {};
24498
24838
  }
@@ -24539,8 +24879,9 @@ var DefaultConfigLoader = class {
24539
24879
  }
24540
24880
  if (c.mode !== void 0 && !isContextWindowModeId(c.mode)) {
24541
24881
  const known = listContextWindowModes().map((m) => m.id).join(", ");
24542
- console.warn(
24543
- `[config] Ignoring unknown context.mode "${c.mode}" (expected one of: ${known}); falling back to "${DEFAULT_CONTEXT_WINDOW_MODE_ID}".`
24882
+ this.logWarn(
24883
+ `Ignoring unknown context.mode "${c.mode}" \u2014 falling back to "${DEFAULT_CONTEXT_WINDOW_MODE_ID}"`,
24884
+ { event: "config.unknown_context_mode", mode: c.mode, known }
24544
24885
  );
24545
24886
  c.mode = DEFAULT_CONTEXT_WINDOW_MODE_ID;
24546
24887
  }
@@ -25670,7 +26011,7 @@ async function loadPlan(filePath, events) {
25670
26011
  return null;
25671
26012
  }
25672
26013
  }
25673
- async function savePlan(filePath, plan, events) {
26014
+ async function savePlan(filePath, plan, events, warn) {
25674
26015
  const t0 = Date.now();
25675
26016
  try {
25676
26017
  await atomicWrite(filePath, JSON.stringify(plan, null, 2), { mode: 384 });
@@ -25692,10 +26033,14 @@ async function savePlan(filePath, plan, events) {
25692
26033
  error: toErrorMessage(err),
25693
26034
  recoverable: false
25694
26035
  });
25695
- console.warn(
25696
- "[plan-store] save failed:",
25697
- toErrorMessage(err)
25698
- );
26036
+ (warn ?? ((m) => console.warn(
26037
+ JSON.stringify({
26038
+ level: "warn",
26039
+ event: "plan_store.save_failed",
26040
+ message: m,
26041
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
26042
+ })
26043
+ )))(toErrorMessage(err));
25699
26044
  return false;
25700
26045
  }
25701
26046
  }
@@ -25736,9 +26081,7 @@ function setPlanItemStatus(plan, idOrIndex, status) {
25736
26081
  const idx = matchIndex(plan, idOrIndex);
25737
26082
  if (idx === -1) return plan;
25738
26083
  const now = (/* @__PURE__ */ new Date()).toISOString();
25739
- const items = plan.items.map(
25740
- (it, i) => i === idx ? { ...it, status, updatedAt: now } : it
25741
- );
26084
+ const items = plan.items.map((it, i) => i === idx ? { ...it, status, updatedAt: now } : it);
25742
26085
  return { ...plan, items, updatedAt: now };
25743
26086
  }
25744
26087
  function clearPlan(plan) {
@@ -25920,10 +26263,19 @@ var QueueStore = class {
25920
26263
  // reject assigning an optional constructor parameter to these fields.
25921
26264
  events;
25922
26265
  traceId;
26266
+ logger;
25923
26267
  constructor(opts) {
25924
26268
  this.file = path27.join(opts.dir, "queue.json");
25925
26269
  this.events = opts.events;
25926
26270
  this.traceId = opts.traceId;
26271
+ this.logger = opts.logger;
26272
+ }
26273
+ logWarn(msg, ctx) {
26274
+ if (this.logger) {
26275
+ this.logger.warn(msg, ctx);
26276
+ } else {
26277
+ console.warn(JSON.stringify({ ...ctx, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }));
26278
+ }
25927
26279
  }
25928
26280
  async write(items) {
25929
26281
  const t0 = Date.now();
@@ -25953,13 +26305,7 @@ var QueueStore = class {
25953
26305
  recoverable: false,
25954
26306
  ...this.traceId !== void 0 && { traceId: this.traceId }
25955
26307
  });
25956
- console.warn(JSON.stringify({
25957
- level: "warn",
25958
- event: "queue_store.write_failed",
25959
- path: this.file,
25960
- message: toErrorMessage(err),
25961
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
25962
- }));
26308
+ this.logWarn("Queue store write failed", { event: "queue_store.write_failed", path: this.file, message: toErrorMessage(err) });
25963
26309
  }
25964
26310
  }
25965
26311
  async read() {
@@ -25991,13 +26337,7 @@ var QueueStore = class {
25991
26337
  recoverable: true,
25992
26338
  ...this.traceId !== void 0 && { traceId: this.traceId }
25993
26339
  });
25994
- console.warn(JSON.stringify({
25995
- level: "warn",
25996
- event: "queue_store.read_failed",
25997
- path: this.file,
25998
- message: toErrorMessage(err),
25999
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
26000
- }));
26340
+ this.logWarn("Queue store read failed", { event: "queue_store.read_failed", path: this.file, message: toErrorMessage(err) });
26001
26341
  return [];
26002
26342
  }
26003
26343
  let parsed;
@@ -26070,13 +26410,7 @@ var QueueStore = class {
26070
26410
  recoverable: true,
26071
26411
  ...this.traceId !== void 0 && { traceId: this.traceId }
26072
26412
  });
26073
- console.warn(JSON.stringify({
26074
- level: "warn",
26075
- event: "queue_store.clear_failed",
26076
- path: this.file,
26077
- message: err.message,
26078
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
26079
- }));
26413
+ this.logWarn("Queue store clear failed", { event: "queue_store.clear_failed", path: this.file, message: err.message });
26080
26414
  }
26081
26415
  }
26082
26416
  };
@@ -26888,7 +27222,7 @@ async function loadTodosCheckpoint(filePath, events, traceId) {
26888
27222
  return null;
26889
27223
  }
26890
27224
  }
26891
- async function saveTodosCheckpoint(filePath, sessionId, todos, events, traceId) {
27225
+ async function saveTodosCheckpoint(filePath, sessionId, todos, events, traceId, warn) {
26892
27226
  const t0 = Date.now();
26893
27227
  const payload = {
26894
27228
  version: 1,
@@ -26917,28 +27251,17 @@ async function saveTodosCheckpoint(filePath, sessionId, todos, events, traceId)
26917
27251
  error: toErrorMessage(err),
26918
27252
  recoverable: false
26919
27253
  });
26920
- console.warn(JSON.stringify({
26921
- level: "warn",
26922
- event: "todos_checkpoint.save_failed",
26923
- message: toErrorMessage(err),
26924
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
26925
- }));
27254
+ (warn ?? ((m) => console.warn(JSON.stringify({ level: "warn", event: "todos_checkpoint.save_failed", message: m, timestamp: (/* @__PURE__ */ new Date()).toISOString() }))))(toErrorMessage(err));
26926
27255
  }
26927
27256
  }
26928
- function attachTodosCheckpoint(state, filePath, sessionId, events, traceId) {
27257
+ function attachTodosCheckpoint(state, filePath, sessionId, events, traceId, warn) {
26929
27258
  let timer = null;
26930
27259
  let pending = null;
26931
27260
  let writeChain = Promise.resolve();
26932
27261
  const enqueueWrite = (todos) => {
26933
27262
  writeChain = writeChain.then(() => saveTodosCheckpoint(filePath, sessionId, todos, events, traceId)).catch((err) => {
26934
27263
  const msg = toErrorMessage(err);
26935
- console.error(JSON.stringify({
26936
- level: "error",
26937
- event: "todos_checkpoint.write_chain_failed",
26938
- sessionId,
26939
- message: msg,
26940
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
26941
- }));
27264
+ (warn ?? ((m) => console.error(JSON.stringify({ level: "error", event: "todos_checkpoint.write_chain_failed", sessionId, message: m, timestamp: (/* @__PURE__ */ new Date()).toISOString() }))))(msg);
26942
27265
  });
26943
27266
  return writeChain;
26944
27267
  };