demian-cli 1.1.2 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
3
3
 
4
4
  // src/cli.ts
5
- import path35 from "node:path";
5
+ import path36 from "node:path";
6
6
 
7
7
  // src/agents/types.ts
8
8
  function normalizeAgent(input2) {
@@ -17,6 +17,9 @@ function normalizeAgent(input2) {
17
17
  defaultDecision: definition.defaultDecision ?? definition.authority?.defaultDecision,
18
18
  mode: definition.mode,
19
19
  hidden: definition.hidden,
20
+ defaultProviders: definition.defaultProviders,
21
+ permissionIntent: definition.permissionIntent,
22
+ runtimeOverlays: definition.runtimeOverlays,
20
23
  provider: definition.provider,
21
24
  delegation: definition.delegation,
22
25
  catalog: definition.catalog,
@@ -42,7 +45,7 @@ function isCoworkableAgent(agent) {
42
45
  if (agent.hidden) return false;
43
46
  const mode = agent.mode ?? "primary";
44
47
  if (mode !== "subagent" && mode !== "all") return false;
45
- return agent.delegation?.coworkable ?? agent.delegation?.callable ?? false;
48
+ return agent.delegation?.coworkable === true;
46
49
  }
47
50
 
48
51
  // src/agents/build.ts
@@ -50,10 +53,12 @@ var buildAgent = {
50
53
  name: "build",
51
54
  description: "General coding agent for scoped implementation work.",
52
55
  mode: "all",
53
- tools: ["read_file", "write_file", "edit_file", "bash", "grep", "glob", "web_search"],
56
+ tools: ["read_file", "write_file", "edit_file", "bash", "grep", "glob", "web_search", "update_plan", "report_progress"],
54
57
  systemPrompt: [
55
58
  "You are demian build, a local coding agent.",
56
59
  "Use tools to inspect and modify the workspace. Keep changes scoped to the user's request.",
60
+ "Before tool-heavy implementation work, use update_plan and report_progress so the user can see what you are about to verify or change and why it matters.",
61
+ "During longer work, report meaningful milestones in conversational sentences. Explain the purpose, what the evidence or result means, and what you will do next instead of listing raw tool names or command output.",
57
62
  "Match the user's language for all user-facing text, including progress-style narration and final results. If the user writes in Korean, write user-facing text in Korean while keeping code identifiers, paths, commands, and tool names unchanged.",
58
63
  "Use web_search when current external information is needed.",
59
64
  "Tool names are fixed by the runtime. In multi-agent mode, delegate_agent may also be available for bounded specialist work.",
@@ -64,6 +69,8 @@ var buildAgent = {
64
69
  { tool: "read_file", decision: "allow" },
65
70
  { tool: "grep", decision: "allow" },
66
71
  { tool: "glob", decision: "allow" },
72
+ { tool: "update_plan", decision: "allow" },
73
+ { tool: "report_progress", decision: "allow" },
67
74
  { tool: "web_search", decision: "ask" },
68
75
  { tool: "write_file", decision: "ask" },
69
76
  { tool: "edit_file", decision: "ask" },
@@ -109,6 +116,7 @@ var claudeCodeAgent = {
109
116
  { tool: "claudecode.Read", decision: "allow" },
110
117
  { tool: "claudecode.Grep", decision: "allow" },
111
118
  { tool: "claudecode.Glob", decision: "allow" },
119
+ { tool: "claudecode.LS", decision: "allow" },
112
120
  { tool: "claudecode.Edit", decision: "ask" },
113
121
  { tool: "claudecode.Write", decision: "ask" },
114
122
  { tool: "claudecode.MultiEdit", decision: "ask" },
@@ -137,144 +145,186 @@ var claudeCodeAgent = {
137
145
  ]
138
146
  }
139
147
  };
140
- var claudeCodeExplorerAgent = {
141
- name: "claudecode-explorer",
142
- description: "Claude Code external read-only explorer for cowork repository inspection.",
143
- mode: "subagent",
144
- provider: { profile: "claudecode", permissionProfile: "explore" },
145
- tools: [],
146
- systemPrompt: [
147
- "You are demian claudecode-explorer, a Claude Code external runtime working as a read-only cowork sub agent for Demian.",
148
- "Demian is the main coordinator. Inspect the repository only within the delegated task and return concise findings.",
149
- "Never edit files, write files, run shell commands, or request write permissions. If the task requires mutation, return a blocker for Demian."
150
- ].join("\n"),
151
- permissions: [
152
- { tool: "claudecode.Read", decision: "allow" },
153
- { tool: "claudecode.Grep", decision: "allow" },
154
- { tool: "claudecode.Glob", decision: "allow" },
155
- { tool: "claudecode.Edit", decision: "deny", reason: "read-only explorer" },
156
- { tool: "claudecode.Write", decision: "deny", reason: "read-only explorer" },
157
- { tool: "claudecode.MultiEdit", decision: "deny", reason: "read-only explorer" },
158
- { tool: "claudecode.Bash", decision: "deny", reason: "read-only explorer" }
148
+
149
+ // src/agents/cowork.ts
150
+ var readPermissions = [
151
+ { tool: "read_file", decision: "allow" },
152
+ { tool: "grep", decision: "allow" },
153
+ { tool: "glob", decision: "allow" },
154
+ { tool: "update_plan", decision: "allow" },
155
+ { tool: "report_progress", decision: "allow" }
156
+ ];
157
+ var readProgressTools = ["read_file", "grep", "glob", "update_plan", "report_progress"];
158
+ var writePermissions = [
159
+ ...readPermissions,
160
+ { tool: "edit_file", decision: "ask" },
161
+ { tool: "write_file", decision: "ask" },
162
+ { tool: "bash", decision: "ask" },
163
+ { tool: "*", match: { pathGlob: "**/.env" }, decision: "deny", reason: "secrets" },
164
+ { tool: "*", match: { pathGlob: "**/.env.*" }, decision: "deny", reason: "secrets" },
165
+ { tool: "*", match: { pathGlob: "node_modules/**" }, decision: "deny", reason: "vendored" }
166
+ ];
167
+ var plannerAgent = coworkAgent({
168
+ name: "planner",
169
+ description: "Provider-agnostic planner for decomposing ambiguous or multi-step engineering work.",
170
+ defaultProviders: ["codex", "anthropic", "openai"],
171
+ permissionIntent: "manage",
172
+ category: "planner",
173
+ cost: "standard",
174
+ tools: readProgressTools,
175
+ permissions: readPermissions,
176
+ defaultDecision: "deny",
177
+ prompt: [
178
+ "You are demian planner, a planning cowork sub agent for Demian.",
179
+ "Inspect the delegated scope read-only and return an implementation plan, sequencing, risks, and verification strategy.",
180
+ "Use update_plan and report_progress to keep Demian's visible progress current when the investigation takes multiple tool calls. Report the planning question you are narrowing, what the evidence implies, and what you will inspect next. Avoid raw tool names, command output, and file previews unless a specific identifier is essential.",
181
+ "Do not edit files. Do not ask the user directly; return blockers or questions for the parent Demian coordinator."
159
182
  ],
183
+ useWhen: ["A parallel planning perspective may clarify scope, sequencing, risks, or verification."],
184
+ avoidWhen: ["The work is already clear and a bounded writer can proceed."]
185
+ });
186
+ var architectAgent = coworkAgent({
187
+ name: "architect",
188
+ description: "Provider-agnostic architect for structure, design, and long-term maintainability decisions.",
189
+ defaultProviders: ["codex", "anthropic"],
190
+ permissionIntent: "manage",
191
+ category: "planner",
192
+ cost: "standard",
193
+ tools: readProgressTools,
194
+ permissions: readPermissions,
160
195
  defaultDecision: "deny",
161
- delegation: {
162
- callable: true,
163
- coworkable: true,
164
- canDelegate: false,
165
- canCowork: false,
166
- invocationDecision: "allow"
167
- },
168
- catalog: {
169
- category: "researcher",
170
- cost: "standard",
171
- useWhen: ["A cowork group needs Claude Code's repository navigation for read-only exploration."],
172
- avoidWhen: ["The task requires editing, shell commands, or direct user conversation."]
173
- }
174
- };
175
- var claudeCodeBuilderAgent = {
176
- name: "claudecode-builder",
177
- description: "Claude Code-backed builder for bounded cowork implementation tasks.",
178
- mode: "subagent",
179
- provider: { profile: "claudecode", permissionProfile: "build" },
180
- tools: [],
181
- systemPrompt: [
182
- "You are demian claudecode-builder, a Claude Code external runtime working as a writer cowork sub agent for Demian.",
183
- "Demian is the main coordinator. Implement only the delegated task and only inside the provided writeScope.",
184
- "If you need broader scope, credentials, user approval, or product decisions, stop and return a concise blocker for Demian.",
185
- "Return changed files, verification performed, patch summary, and remaining blockers. Do not present yourself as the primary assistant."
186
- ].join("\n"),
187
- permissions: [
188
- { tool: "claudecode.Read", decision: "allow" },
189
- { tool: "claudecode.Grep", decision: "allow" },
190
- { tool: "claudecode.Glob", decision: "allow" },
191
- { tool: "claudecode.Edit", decision: "ask" },
192
- { tool: "claudecode.Write", decision: "ask" },
193
- { tool: "claudecode.MultiEdit", decision: "ask" },
194
- { tool: "claudecode.Bash", decision: "ask" },
195
- { tool: "claudecode.*", match: { pathGlob: "**/.env" }, decision: "deny", reason: "secrets" },
196
- { tool: "claudecode.*", match: { pathGlob: "**/.env.*" }, decision: "deny", reason: "secrets" },
197
- { tool: "claudecode.*", match: { pathGlob: "node_modules/**" }, decision: "deny", reason: "vendored" }
196
+ prompt: [
197
+ "You are demian architect, a design cowork sub agent for Demian.",
198
+ "Inspect the repository and proposed work read-only. Focus on architecture, interfaces, coupling, migration risk, and maintainability.",
199
+ "Use update_plan and report_progress to summarize what design question you are answering, what the evidence means for the architecture, and what you will inspect next. Avoid raw tool names, command output, and file previews unless a specific identifier is essential.",
200
+ "Return concrete design recommendations and risks for the parent Demian coordinator."
201
+ ],
202
+ useWhen: ["A change needs architectural judgment, interface design, or cross-module risk assessment."],
203
+ avoidWhen: ["The task is a small mechanical edit."]
204
+ });
205
+ var supervisorAgent = coworkAgent({
206
+ name: "supervisor",
207
+ description: "Provider-agnostic supervisory reviewer for overall risk, integration quality, and final review.",
208
+ defaultProviders: ["codex", "anthropic"],
209
+ permissionIntent: "review",
210
+ category: "reviewer",
211
+ cost: "standard",
212
+ tools: readProgressTools,
213
+ permissions: readPermissions,
214
+ defaultDecision: "deny",
215
+ prompt: [
216
+ "You are demian supervisor, a skeptical supervisory cowork sub agent for Demian.",
217
+ "Review plans, diffs, integration risks, missing tests, and final readiness. Do not edit files.",
218
+ "Use update_plan and report_progress to expose review progress when the review requires several reads. Include the review focus, the risk you are checking, and the next evidence you will seek. Avoid raw tool names, command output, and file previews unless a specific identifier is essential.",
219
+ "Return concise findings with severity, file references when useful, and a clear recommendation."
220
+ ],
221
+ useWhen: ["A second model should review a design, implementation plan, diff, or test strategy before Demian answers."],
222
+ avoidWhen: ["The task needs direct workspace edits from this member."]
223
+ });
224
+ var researcherAgent = coworkAgent({
225
+ name: "researcher",
226
+ description: "Provider-agnostic researcher for repo inspection, external research, and evidence gathering.",
227
+ defaultProviders: ["codex", "claudecode", "anthropic"],
228
+ permissionIntent: "read-only",
229
+ category: "researcher",
230
+ cost: "standard",
231
+ tools: [...readProgressTools, "web_search"],
232
+ permissions: [...readPermissions, { tool: "web_search", decision: "ask" }],
233
+ defaultDecision: "deny",
234
+ prompt: [
235
+ "You are demian researcher, a read-only cowork sub agent for Demian.",
236
+ "Gather evidence from the repository and, when needed, current external sources. Do not edit files.",
237
+ "Use update_plan and report_progress to report the evidence gathered at meaningful milestones. Include what the evidence suggests and where you will look next, without dumping raw tool names, command output, or file previews unless a specific identifier is essential.",
238
+ "Return findings with enough context for the parent Demian coordinator to act."
198
239
  ],
240
+ useWhen: ["The task needs repository exploration, usage tracing, documentation lookup, or evidence gathering."],
241
+ avoidWhen: ["The task is already scoped for implementation."]
242
+ });
243
+ var builderAgent = coworkAgent({
244
+ name: "builder",
245
+ description: "Provider-agnostic builder for bounded implementation and module-level development.",
246
+ defaultProviders: ["claudecode", "anthropic", "openai"],
247
+ permissionIntent: "write",
248
+ category: "builder",
249
+ cost: "expensive",
250
+ tools: [...readProgressTools, "edit_file", "write_file", "bash"],
251
+ permissions: writePermissions,
199
252
  defaultDecision: "ask",
200
- delegation: {
201
- callable: true,
202
- coworkable: true,
203
- canDelegate: false,
204
- canCowork: false,
205
- invocationDecision: "ask"
206
- },
207
- catalog: {
208
- category: "builder",
209
- cost: "expensive",
210
- useWhen: ["Claude Code's local coding-agent runtime should implement a bounded writeScope while Demian coordinates review."],
211
- avoidWhen: ["The task is read-only, exploratory, or can be handled by a normal Demian tool call."]
212
- }
213
- };
214
-
215
- // src/agents/cowork.ts
216
- var codexReviewerAgent = {
217
- name: "codex-reviewer",
218
- description: "Codex-backed reviewer for architecture, risk, and patch review.",
219
- mode: "subagent",
220
- provider: { profile: "codex" },
221
- tools: ["read_file", "grep", "glob"],
222
- systemPrompt: [
223
- "You are demian codex-reviewer, a skeptical reviewer working as a cowork sub agent for Demian.",
224
- "Inspect the delegated context, plans, diffs, risks, and missing tests. Do not edit files.",
225
- "Return concise findings with severity, file references when useful, and a clear recommendation for the parent Demian coordinator."
226
- ].join("\n"),
227
- permissions: [
228
- { tool: "read_file", decision: "allow" },
229
- { tool: "grep", decision: "allow" },
230
- { tool: "glob", decision: "allow" }
253
+ prompt: [
254
+ "You are demian builder, a bounded implementation cowork sub agent for Demian.",
255
+ "Implement only the delegated task and only inside the provided writeScope.",
256
+ "Use update_plan and report_progress before writes and after meaningful implementation or verification milestones. Say what you are changing, why it is the next safe step, and how you will verify it. Avoid raw tool names, command output, and file previews unless a specific identifier is essential.",
257
+ "If you need broader scope, credentials, user approval, or product decisions, stop and return a concise blocker for Demian.",
258
+ "Return changed files, verification performed, patch summary, and remaining blockers."
231
259
  ],
260
+ useWhen: ["A bounded writeScope should be implemented while Demian coordinates review."],
261
+ avoidWhen: ["The task is read-only, exploratory, or can be handled by a normal Demian tool call."]
262
+ });
263
+ var reviewerAgent = coworkAgent({
264
+ name: "reviewer",
265
+ description: "Provider-agnostic detailed reviewer for diff-level code review.",
266
+ defaultProviders: ["claudecode", "anthropic", "codex"],
267
+ permissionIntent: "review",
268
+ category: "reviewer",
269
+ cost: "standard",
270
+ tools: readProgressTools,
271
+ permissions: readPermissions,
232
272
  defaultDecision: "deny",
233
- delegation: {
234
- callable: true,
235
- coworkable: true,
236
- canDelegate: false,
237
- canCowork: false,
238
- invocationDecision: "allow"
239
- },
240
- catalog: {
241
- category: "reviewer",
242
- cost: "standard",
243
- useWhen: ["A second model should review a design, implementation plan, diff, or test strategy before Demian answers."],
244
- avoidWhen: ["The task needs direct workspace edits from this member."]
245
- }
246
- };
247
- var codexPlannerAgent = {
248
- name: "codex-planner",
249
- description: "Codex-backed planner for decomposing ambiguous or multi-step engineering work.",
250
- mode: "subagent",
251
- provider: { profile: "codex" },
252
- tools: ["read_file", "grep", "glob"],
253
- systemPrompt: [
254
- "You are demian codex-planner, a planning cowork sub agent for Demian.",
255
- "Inspect the delegated scope read-only and return an implementation plan, sequencing, risks, and verification strategy.",
256
- "Do not edit files. Do not ask the user directly; return blockers or questions for the parent Demian coordinator."
257
- ].join("\n"),
258
- permissions: [
259
- { tool: "read_file", decision: "allow" },
260
- { tool: "grep", decision: "allow" },
261
- { tool: "glob", decision: "allow" }
273
+ prompt: [
274
+ "You are demian reviewer, a detailed code-review cowork sub agent for Demian.",
275
+ "Inspect the delegated context read-only. Focus on correctness, edge cases, tests, consistency, and maintainability.",
276
+ "Use update_plan and report_progress to keep longer review passes observable. Mention the finding area, current confidence, and the next evidence you will inspect. Avoid raw tool names, command output, and file previews unless a specific identifier is essential.",
277
+ "Return findings first, ordered by severity, with file references when useful."
262
278
  ],
279
+ useWhen: ["A diff or focused implementation needs detailed code review."],
280
+ avoidWhen: ["The task needs broad architecture or direct editing."]
281
+ });
282
+ var specialistAgent = coworkAgent({
283
+ name: "specialist",
284
+ description: "Provider-agnostic specialist for focused domain, module, or technology expertise.",
285
+ defaultProviders: ["claudecode", "anthropic"],
286
+ permissionIntent: "read-only",
287
+ category: "researcher",
288
+ cost: "standard",
289
+ tools: readProgressTools,
290
+ permissions: readPermissions,
263
291
  defaultDecision: "deny",
264
- delegation: {
265
- callable: true,
266
- coworkable: true,
267
- canDelegate: false,
268
- canCowork: false,
269
- invocationDecision: "allow"
270
- },
271
- catalog: {
272
- category: "planner",
273
- cost: "standard",
274
- useWhen: ["A parallel planning perspective may clarify scope, sequencing, risks, or verification."],
275
- avoidWhen: ["The work is already clear and a bounded writer can proceed."]
276
- }
277
- };
292
+ prompt: [
293
+ "You are demian specialist, a focused domain cowork sub agent for Demian.",
294
+ "Inspect the requested module, technology, or domain read-only unless explicitly assigned write mode with a narrow writeScope.",
295
+ "Use update_plan and report_progress for multi-step investigation so Demian can show the user what is happening. Explain the domain angle you are checking, what the evidence means, and what you will narrow down next. Avoid raw tool names, command output, and file previews unless a specific identifier is essential.",
296
+ "Return precise, specialist-level findings and recommendations."
297
+ ],
298
+ useWhen: ["A specific module, library, language, or subsystem needs focused expert analysis."],
299
+ avoidWhen: ["The task is broad coordination or final synthesis."]
300
+ });
301
+ var coworkAgents = [plannerAgent, architectAgent, supervisorAgent, researcherAgent, builderAgent, reviewerAgent, specialistAgent];
302
+ function coworkAgent(options) {
303
+ return {
304
+ name: options.name,
305
+ description: options.description,
306
+ mode: "subagent",
307
+ tools: options.tools,
308
+ permissions: options.permissions,
309
+ defaultDecision: options.defaultDecision,
310
+ defaultProviders: options.defaultProviders,
311
+ permissionIntent: options.permissionIntent,
312
+ systemPrompt: options.prompt.join("\n"),
313
+ delegation: {
314
+ callable: true,
315
+ coworkable: true,
316
+ canDelegate: false,
317
+ canCowork: false,
318
+ invocationDecision: options.permissionIntent === "write" ? "ask" : "allow"
319
+ },
320
+ catalog: {
321
+ category: options.category,
322
+ cost: options.cost,
323
+ useWhen: options.useWhen,
324
+ avoidWhen: options.avoidWhen
325
+ }
326
+ };
327
+ }
278
328
 
279
329
  // src/agents/execute.ts
280
330
  var executeAgent = {
@@ -286,8 +336,8 @@ var executeAgent = {
286
336
  "You are demian execute, a bounded implementation agent.",
287
337
  "You receive one task from a coordinator. Treat that task scope as a hard boundary.",
288
338
  "Read the relevant files before changing them. Do not refactor unrelated code.",
289
- "Before changing files, use update_plan to mark your assigned step in_progress and report_progress to say what you are starting.",
290
- "Use report_progress for meaningful milestones during longer work.",
339
+ "Before changing files, use update_plan to mark your assigned step in_progress and report_progress to say what you are starting, why that step matters, and what you will do next.",
340
+ "Use report_progress for meaningful milestones during longer work. Write conversational updates that explain the purpose and implication of the work, not a raw list of tools, command output, or file previews.",
291
341
  "Keep the visible work plan in sync with update_plan when you start, finish, block, skip, or fail your assigned step.",
292
342
  "Match the user's language for all user-facing text: update_plan titles, summaries, step titles/details, report_progress messages, and final results. If the user writes in Korean, write those user-facing fields in Korean while keeping code identifiers, paths, commands, and tool names unchanged.",
293
343
  "When finished, return changed files, verification results, and any remaining issues.",
@@ -337,9 +387,10 @@ var generalAgent = {
337
387
  "For non-trivial work, first think through the request and inspect enough context to avoid a shallow plan. Then call update_plan before writes, shell commands, or execute delegation.",
338
388
  "Use update_plan.title as the user's main goal, update_plan.summary for the current strategy, and short stable step ids for the sub-goals. Keep exactly one step in_progress while executing.",
339
389
  "Match the user's language for all user-facing text: update_plan titles, summaries, step titles/details, report_progress messages, narration, and final answers. If the user writes in Korean, write those user-facing fields in Korean while keeping code identifiers, paths, commands, and tool names unchanged.",
390
+ "For any tool-using work, keep the user oriented before and during the work. Use report_progress with conversational sentences that say what question you are trying to answer, what the latest evidence means, and what you will do next. Do not list raw tool names, command output, or file previews unless the identifier itself is important to the user's decision. This applies to ordinary single-agent work as well as cowork/delegation.",
340
391
  "Use the read-only plan agent for ambiguous, risky, or cross-file design work. Use the execute agent for bounded implementation tasks from the plan. Small obvious edits can be done directly.",
341
392
  "When the user asks to use Claude Code, keep Demian as the main coordinator and delegate bounded work to the claudecode sub agent when it is available.",
342
- "Keep the plan synchronized as steps move from pending to in_progress to completed, blocked, skipped, or failed. Use report_progress when switching phases or finishing meaningful milestones.",
393
+ "Keep the plan synchronized as steps move from pending to in_progress to completed, blocked, skipped, failed, or cancelled. Use report_progress when starting a phase, switching phases, learning something important, or finishing meaningful milestones.",
343
394
  "Use web_search when current external information is needed.",
344
395
  "Keep changes scoped, preserve user work, and explain the outcome clearly.",
345
396
  "Tool names are fixed by the runtime. In multi-agent mode, delegate_agent may also be available for bounded specialist work.",
@@ -386,7 +437,7 @@ var planAgent = {
386
437
  "Use update_plan to materialize the visible work plan: title is the main goal, summary is the strategy, and steps are concrete sub-goals with short stable ids.",
387
438
  "Match the user's language for all user-facing text: update_plan titles, summaries, step titles/details, report_progress messages, and the final planning response. If the user writes in Korean, write those user-facing fields in Korean while keeping code identifiers, paths, commands, and tool names unchanged.",
388
439
  "A planning-only run can leave all steps pending. If you are actively investigating one step, mark only that step in_progress.",
389
- "Use report_progress for major planning milestones, not for every read.",
440
+ "Use report_progress for major planning milestones, not for every read. Make each update conversational: what question you investigated, what that suggests, and what you will check next. Avoid raw tool names, command output, and file previews unless a specific identifier is essential.",
390
441
  "Suggest bounded tasks that can be delegated to execute."
391
442
  ].join("\n"),
392
443
  permissions: [
@@ -414,7 +465,7 @@ var planAgent = {
414
465
  var AgentRegistry = class {
415
466
  #agents = /* @__PURE__ */ new Map();
416
467
  #builtinNames = /* @__PURE__ */ new Set();
417
- constructor(agents = [generalAgent, buildAgent, planAgent, executeAgent, claudeCodeAgent, claudeCodeExplorerAgent, claudeCodeBuilderAgent, codexReviewerAgent, codexPlannerAgent]) {
468
+ constructor(agents = [generalAgent, buildAgent, planAgent, executeAgent, claudeCodeAgent, ...coworkAgents]) {
418
469
  for (const agent of agents) {
419
470
  const normalized = normalizeAgent(agent);
420
471
  this.#builtinNames.add(normalized.name);
@@ -468,7 +519,14 @@ var AgentRegistry = class {
468
519
  }
469
520
  };
470
521
  function buildSystemPrompt(agent, envInfo) {
471
- return [agent.systemPrompt, envInfo, "When a tool result reports an error, adjust your next step instead of repeating the same call."].filter(Boolean).join("\n\n");
522
+ return [
523
+ agent.systemPrompt,
524
+ envInfo,
525
+ "Keep the user oriented for every non-trivial task, including /goal, cowork, delegated work, and ordinary single-agent work. Before the first tool-heavy action, briefly say what you are about to understand, verify, or change and why that is the right next step. For multi-step work, create or update the visible plan before substantial tool work and keep it synchronized as steps start, finish, skip, block, fail, or get cancelled.",
526
+ "When you use report_progress, write it like a brief teammate update: what you are checking or changing, what you have learned so far, why that matters, and what you will do next. Keep it conversational. Do not dump raw tool names, command output, file previews, or status fields unless a specific identifier is essential for the user to understand the decision.",
527
+ "When a task is long-running, report progress at meaningful phase changes and after important evidence. Prefer the purpose and implication of the work over mechanical execution details; the UI already exposes raw tool activity separately.",
528
+ "When a tool result reports an error, adjust your next step instead of repeating the same call."
529
+ ].filter(Boolean).join("\n\n");
472
530
  }
473
531
  function validateAgentName(name) {
474
532
  if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
@@ -481,7 +539,113 @@ import { readFile as readFile6 } from "node:fs/promises";
481
539
  import crypto4 from "node:crypto";
482
540
  import fs7 from "node:fs";
483
541
  import os7 from "node:os";
484
- import path13 from "node:path";
542
+ import path14 from "node:path";
543
+
544
+ // src/util.ts
545
+ function stableStringify(value) {
546
+ return JSON.stringify(sortValue(value));
547
+ }
548
+ function sortValue(value) {
549
+ if (Array.isArray(value)) return value.map(sortValue);
550
+ if (!value || typeof value !== "object") return value;
551
+ const object2 = value;
552
+ const out = {};
553
+ for (const key of Object.keys(object2).sort()) out[key] = sortValue(object2[key]);
554
+ return out;
555
+ }
556
+ function createSessionId() {
557
+ return `ses_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
558
+ }
559
+ function createRootSessionId() {
560
+ return `root_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
561
+ }
562
+ function createAgentSessionId() {
563
+ return `as_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
564
+ }
565
+ function createInvocationId() {
566
+ return `inv_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
567
+ }
568
+ function preview(value, max = 500) {
569
+ if (value.length <= max) return value;
570
+ return `${value.slice(0, max)}...`;
571
+ }
572
+ function errorMessage(error) {
573
+ if (!(error instanceof Error)) return String(error);
574
+ const primary = error.message || error.name || "Error";
575
+ const cause = errorCauseSummary(error);
576
+ const hint = networkFailureHint(`${primary}${cause ? ` ${cause}` : ""}`);
577
+ const details = [cause, hint].filter(Boolean);
578
+ return details.length ? `${primary} (${details.join("; ")})` : primary;
579
+ }
580
+ function errorCauseSummary(error) {
581
+ const cause = error.cause;
582
+ const detail = causeDetail(cause, /* @__PURE__ */ new Set());
583
+ return detail ? `cause: ${detail}` : void 0;
584
+ }
585
+ function causeDetail(cause, seen) {
586
+ if (cause === void 0 || cause === null) return void 0;
587
+ if (seen.has(cause)) return void 0;
588
+ if (typeof cause === "object" || typeof cause === "function") seen.add(cause);
589
+ if (cause instanceof AggregateError) {
590
+ const details = cause.errors.map((item) => causeDetail(item, seen)).filter((item) => Boolean(item)).slice(0, 3);
591
+ return details.length ? details.join("; ") : cleanMessage(cause.message);
592
+ }
593
+ if (cause instanceof Error) {
594
+ const message = cleanMessage(cause.message);
595
+ const fields = errorFields(cause);
596
+ const nested = causeDetail(cause.cause, seen);
597
+ return [message, fields, nested ? `cause: ${nested}` : void 0].filter(Boolean).join("; ");
598
+ }
599
+ if (typeof cause === "object") {
600
+ const fields = errorFields(cause);
601
+ return fields || cleanMessage(safeStringify(cause));
602
+ }
603
+ return cleanMessage(String(cause));
604
+ }
605
+ function errorFields(value) {
606
+ if (!value || typeof value !== "object") return void 0;
607
+ const object2 = value;
608
+ const parts = [];
609
+ for (const key of ["code", "errno", "syscall"]) {
610
+ const field = object2[key];
611
+ if (typeof field === "string" || typeof field === "number") parts.push(`${key}=${field}`);
612
+ }
613
+ const host = stringField(object2, "hostname") ?? stringField(object2, "host") ?? stringField(object2, "address");
614
+ const port = stringField(object2, "port");
615
+ if (host) parts.push(`target=${host}${port ? `:${port}` : ""}`);
616
+ return unique(parts).join(", ");
617
+ }
618
+ function networkFailureHint(text) {
619
+ const lower = text.toLowerCase();
620
+ if (!/\b(fetch failed|econnreset|econnrefused|etimedout|enotfound|eai_again|und_err_connect_timeout|network|socket|certificate|cert_|unable_to_verify|self_signed)\b/i.test(text)) return void 0;
621
+ if (lower.includes("enotfound") || lower.includes("eai_again")) return "DNS lookup failed; check the provider baseURL, network, VPN, or proxy settings.";
622
+ if (lower.includes("etimedout") || lower.includes("und_err_connect_timeout") || lower.includes("timeout")) return "Connection timed out before the provider returned a response; check network reachability, VPN/proxy, or the provider status.";
623
+ if (lower.includes("econnrefused")) return "Connection was refused; check the provider endpoint host/port and whether the service is running.";
624
+ if (lower.includes("econnreset") || lower.includes("socket")) return "Connection was reset while contacting the provider; retry, or check VPN/proxy/provider stability.";
625
+ if (lower.includes("certificate") || lower.includes("cert_") || lower.includes("unable_to_verify") || lower.includes("self_signed")) return "TLS certificate validation failed; check corporate proxy or custom CA configuration.";
626
+ if (lower.includes("fetch failed")) return "Network request failed before an HTTP response was received; check network, proxy, DNS, and provider endpoint settings.";
627
+ return void 0;
628
+ }
629
+ function cleanMessage(value) {
630
+ const text = value?.replace(/\s+/g, " ").trim();
631
+ return text || void 0;
632
+ }
633
+ function stringField(object2, key) {
634
+ const value = object2[key];
635
+ if (typeof value === "string" && value.trim()) return value.trim();
636
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
637
+ return void 0;
638
+ }
639
+ function unique(values) {
640
+ return [...new Set(values)];
641
+ }
642
+ function safeStringify(value) {
643
+ try {
644
+ return JSON.stringify(value);
645
+ } catch {
646
+ return void 0;
647
+ }
648
+ }
485
649
 
486
650
  // src/providers/retry.ts
487
651
  var RETRY_STATUS = /* @__PURE__ */ new Set([408, 409, 425, 429, 500, 502, 503, 504]);
@@ -532,8 +696,7 @@ function retryDelay(error, attempt) {
532
696
  }
533
697
  function retryReason(error) {
534
698
  if (error instanceof ProviderHttpError) return `HTTP ${error.status}`;
535
- if (error instanceof Error) return error.message;
536
- return String(error);
699
+ return errorMessage(error);
537
700
  }
538
701
  function sleep(ms2, signal) {
539
702
  return new Promise((resolve, reject) => {
@@ -3181,7 +3344,8 @@ function buildClaudeCliArgs(config, req, flags, resumeSessionId) {
3181
3344
  const args = ["-p", "--output-format", config.outputFormat ?? "stream-json", "--input-format", config.inputFormat ?? "text", "--verbose"];
3182
3345
  if (flags.includePartialMessages) args.push("--include-partial-messages");
3183
3346
  if (req.model) args.push("--model", req.model);
3184
- if (config.maxTurns !== void 0 && flags.maxTurns) args.push("--max-turns", String(config.maxTurns));
3347
+ const maxTurns = req.maxTurns === null ? void 0 : req.maxTurns ?? config.maxTurns;
3348
+ if (maxTurns !== void 0 && flags.maxTurns) args.push("--max-turns", String(maxTurns));
3185
3349
  if (config.permissionMode && flags.permissionMode) args.push("--permission-mode", config.permissionMode);
3186
3350
  if (config.maxBudgetUsd !== void 0 && flags.maxBudgetUsd) args.push("--max-budget-usd", String(config.maxBudgetUsd));
3187
3351
  if (resumeSessionId) args.push("--resume", resumeSessionId);
@@ -22701,44 +22865,10 @@ function now() {
22701
22865
  }
22702
22866
 
22703
22867
  // src/permissions/engine.ts
22704
- import path11 from "node:path";
22868
+ import path12 from "node:path";
22705
22869
 
22706
22870
  // src/permissions/grants.ts
22707
22871
  import path9 from "node:path";
22708
-
22709
- // src/util.ts
22710
- function stableStringify(value) {
22711
- return JSON.stringify(sortValue(value));
22712
- }
22713
- function sortValue(value) {
22714
- if (Array.isArray(value)) return value.map(sortValue);
22715
- if (!value || typeof value !== "object") return value;
22716
- const object2 = value;
22717
- const out = {};
22718
- for (const key of Object.keys(object2).sort()) out[key] = sortValue(object2[key]);
22719
- return out;
22720
- }
22721
- function createSessionId() {
22722
- return `ses_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
22723
- }
22724
- function createRootSessionId() {
22725
- return `root_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
22726
- }
22727
- function createAgentSessionId() {
22728
- return `as_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
22729
- }
22730
- function createInvocationId() {
22731
- return `inv_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
22732
- }
22733
- function preview(value, max = 500) {
22734
- if (value.length <= max) return value;
22735
- return `${value.slice(0, max)}...`;
22736
- }
22737
- function errorMessage(error) {
22738
- return error instanceof Error ? error.message : String(error);
22739
- }
22740
-
22741
- // src/permissions/grants.ts
22742
22872
  var SessionGrants = class {
22743
22873
  #keys = /* @__PURE__ */ new Set();
22744
22874
  has(key) {
@@ -22753,6 +22883,9 @@ var SessionGrants = class {
22753
22883
  };
22754
22884
  function grantKeyFor(tool, input2, cwd) {
22755
22885
  const object2 = input2 && typeof input2 === "object" && !Array.isArray(input2) ? input2 : {};
22886
+ if (CLAUDE_CODE_WORKSPACE_READ_GRANTS.has(tool)) {
22887
+ return `${tool}:workspace-read`;
22888
+ }
22756
22889
  if (tool === "bash") {
22757
22890
  const command = typeof object2.command === "string" ? object2.command : "";
22758
22891
  return `bash:${firstCommandTokens(command, 2).join(" ")}`;
@@ -22764,6 +22897,7 @@ function grantKeyFor(tool, input2, cwd) {
22764
22897
  }
22765
22898
  return `${tool}:${stableStringify(input2)}`;
22766
22899
  }
22900
+ var CLAUDE_CODE_WORKSPACE_READ_GRANTS = /* @__PURE__ */ new Set(["claudecode.Read", "claudecode.Grep", "claudecode.Glob", "claudecode.LS"]);
22767
22901
  function grantKeyForAgentInvocation(agentName, providerProfile) {
22768
22902
  return `agent:${agentName}:${providerProfile}`;
22769
22903
  }
@@ -22786,31 +22920,151 @@ function firstCommandTokens(command, count) {
22786
22920
  return tokens;
22787
22921
  }
22788
22922
 
22789
- // src/workspace/paths.ts
22923
+ // src/permissions/shell-safety.ts
22790
22924
  import path10 from "node:path";
22925
+ function isClearlyReadOnlyShellCommand(command) {
22926
+ const tokens = splitSimpleShellWords(command);
22927
+ if (!tokens?.length) return false;
22928
+ if (tokens.some(isUnsafePathToken)) return false;
22929
+ const executable = path10.basename(tokens[0] ?? "");
22930
+ if (SIMPLE_READ_ONLY_COMMANDS.has(executable)) return true;
22931
+ if (executable === "find") return isReadOnlyFind(tokens);
22932
+ if (executable === "git") return isReadOnlyGit(tokens);
22933
+ return false;
22934
+ }
22935
+ function isPotentiallyReadOnlyShellCommand(command) {
22936
+ const tokens = splitSimpleShellWords(command);
22937
+ if (!tokens?.length) return false;
22938
+ if (tokens.some(isUnsafePathToken)) return false;
22939
+ const executable = path10.basename(tokens[0] ?? "");
22940
+ if (isClearlyReadOnlyShellCommand(command)) return true;
22941
+ if (POTENTIAL_AGENT_JUDGED_COMMANDS.has(executable)) return !hasKnownMutatingToken(executable, tokens);
22942
+ return false;
22943
+ }
22944
+ function splitSimpleShellWords(command) {
22945
+ if (!command.trim() || /[\r\n]/.test(command)) return void 0;
22946
+ const words = [];
22947
+ let current = "";
22948
+ let quote;
22949
+ for (let index = 0; index < command.length; index++) {
22950
+ const char = command[index];
22951
+ if (quote) {
22952
+ if (char === quote) {
22953
+ quote = void 0;
22954
+ continue;
22955
+ }
22956
+ if (quote === '"' && (char === "$" || char === "`")) return void 0;
22957
+ if (char === "\\" && quote === '"' && index + 1 < command.length) {
22958
+ current += command[++index];
22959
+ continue;
22960
+ }
22961
+ current += char;
22962
+ continue;
22963
+ }
22964
+ if (/\s/.test(char)) {
22965
+ if (current) {
22966
+ words.push(current);
22967
+ current = "";
22968
+ }
22969
+ continue;
22970
+ }
22971
+ if (char === "'" || char === '"') {
22972
+ quote = char;
22973
+ continue;
22974
+ }
22975
+ if (SHELL_CONTROL_CHARS.has(char)) return void 0;
22976
+ if (char === "\\" && index + 1 < command.length) {
22977
+ current += command[++index];
22978
+ continue;
22979
+ }
22980
+ current += char;
22981
+ }
22982
+ if (quote) return void 0;
22983
+ if (current) words.push(current);
22984
+ return words;
22985
+ }
22986
+ function isUnsafePathToken(token) {
22987
+ if (token === "." || token === "-") return false;
22988
+ if (token === "~" || token.startsWith("~/") || token.startsWith("/")) return true;
22989
+ if (token === ".." || token.startsWith("../") || token.includes("/../")) return true;
22990
+ const assignmentValue = token.match(/^[A-Za-z_][A-Za-z0-9_-]*=(.+)$/)?.[1];
22991
+ if (assignmentValue) return isUnsafePathToken(assignmentValue);
22992
+ const optionValue = token.match(/^--?[A-Za-z0-9][A-Za-z0-9_-]*=(.+)$/)?.[1];
22993
+ return optionValue ? isUnsafePathToken(optionValue) : false;
22994
+ }
22995
+ function isReadOnlyFind(tokens) {
22996
+ return tokens.slice(1).every((token) => !UNSAFE_FIND_TOKENS.has(token));
22997
+ }
22998
+ function isReadOnlyGit(tokens) {
22999
+ const parsed = gitSubcommand(tokens);
23000
+ if (!parsed) return false;
23001
+ if (!READ_ONLY_GIT_SUBCOMMANDS.has(parsed.command)) return false;
23002
+ if (parsed.command === "branch") return parsed.args.every((arg) => READ_ONLY_GIT_BRANCH_ARGS.has(arg));
23003
+ if (parsed.command === "remote") return parsed.args.length === 0 || parsed.args.every((arg) => arg === "-v" || arg === "--verbose" || arg === "get-url");
23004
+ if (parsed.command === "config") return parsed.args.some((arg) => READ_ONLY_GIT_CONFIG_ARGS.has(arg));
23005
+ return true;
23006
+ }
23007
+ function gitSubcommand(tokens) {
23008
+ let index = 1;
23009
+ while (index < tokens.length) {
23010
+ const token = tokens[index];
23011
+ if (token === "--no-pager") {
23012
+ index++;
23013
+ continue;
23014
+ }
23015
+ if (token === "-C") {
23016
+ index += 2;
23017
+ continue;
23018
+ }
23019
+ if (token?.startsWith("-")) return void 0;
23020
+ return { command: token ?? "", args: tokens.slice(index + 1) };
23021
+ }
23022
+ return void 0;
23023
+ }
23024
+ function hasKnownMutatingToken(executable, tokens) {
23025
+ if (executable === "sed") {
23026
+ return tokens.slice(1).some((token) => token === "-i" || token.startsWith("-i") || /^w(\s|$)/.test(token) || /;\s*w(\s|$)/.test(token));
23027
+ }
23028
+ if (executable === "git") {
23029
+ const parsed = gitSubcommand(tokens);
23030
+ return !parsed || MUTATING_GIT_SUBCOMMANDS.has(parsed.command);
23031
+ }
23032
+ return true;
23033
+ }
23034
+ var SHELL_CONTROL_CHARS = /* @__PURE__ */ new Set([";", "&", "|", ">", "<", "`", "$", "(", ")"]);
23035
+ var SIMPLE_READ_ONLY_COMMANDS = /* @__PURE__ */ new Set(["pwd", "ls", "cat", "head", "tail", "wc", "file", "stat", "du", "tree", "realpath", "basename", "dirname", "rg", "grep", "which", "command", "type", "printf", "echo"]);
23036
+ var POTENTIAL_AGENT_JUDGED_COMMANDS = /* @__PURE__ */ new Set(["sed", "git"]);
23037
+ var UNSAFE_FIND_TOKENS = /* @__PURE__ */ new Set(["-delete", "-exec", "-execdir", "-ok", "-okdir", "-fls", "-fprint", "-fprintf"]);
23038
+ var READ_ONLY_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set(["status", "diff", "log", "show", "ls-files", "grep", "rev-parse", "describe", "blame", "shortlog", "branch", "remote", "config"]);
23039
+ var READ_ONLY_GIT_BRANCH_ARGS = /* @__PURE__ */ new Set(["--show-current", "-a", "--all", "-r", "--remotes", "-v", "-vv", "--verbose", "--color", "--no-color"]);
23040
+ var READ_ONLY_GIT_CONFIG_ARGS = /* @__PURE__ */ new Set(["--get", "--get-all", "--get-regexp", "--list", "-l"]);
23041
+ var MUTATING_GIT_SUBCOMMANDS = /* @__PURE__ */ new Set(["add", "am", "apply", "bisect", "checkout", "cherry-pick", "clean", "clone", "commit", "fetch", "gc", "init", "merge", "mv", "pull", "push", "rebase", "reset", "restore", "revert", "rm", "stash", "switch", "tag", "worktree"]);
23042
+
23043
+ // src/workspace/paths.ts
23044
+ import path11 from "node:path";
22791
23045
  function normalizePathForMatch(value) {
22792
- return value.split(path10.sep).join("/");
23046
+ return value.split(path11.sep).join("/");
22793
23047
  }
22794
23048
  function isInsidePath(root, candidate) {
22795
- const relative = path10.relative(path10.resolve(root), path10.resolve(candidate));
22796
- return relative === "" || !relative.startsWith("..") && !path10.isAbsolute(relative);
23049
+ const relative = path11.relative(path11.resolve(root), path11.resolve(candidate));
23050
+ return relative === "" || !relative.startsWith("..") && !path11.isAbsolute(relative);
22797
23051
  }
22798
23052
  function resolveInsideCwd(cwd, inputPath) {
22799
23053
  if (typeof inputPath !== "string" || inputPath.length === 0) {
22800
23054
  throw new Error("path must be a non-empty string");
22801
23055
  }
22802
- const resolved = path10.resolve(cwd, inputPath);
23056
+ const resolved = path11.resolve(cwd, inputPath);
22803
23057
  if (!isInsidePath(cwd, resolved)) {
22804
23058
  throw new Error(`Path is outside the workspace: ${inputPath}`);
22805
23059
  }
22806
23060
  return resolved;
22807
23061
  }
22808
23062
  function relativeToCwd(cwd, absolutePath) {
22809
- const relative = path10.relative(cwd, absolutePath);
23063
+ const relative = path11.relative(cwd, absolutePath);
22810
23064
  return normalizePathForMatch(relative || ".");
22811
23065
  }
22812
23066
  function isEnvFile(filePath) {
22813
- const base = path10.basename(filePath);
23067
+ const base = path11.basename(filePath);
22814
23068
  if (base === ".env.example") return false;
22815
23069
  return base === ".env" || base.startsWith(".env.");
22816
23070
  }
@@ -22896,7 +23150,7 @@ var PermissionEngine = class {
22896
23150
  }
22897
23151
  if (this.#defaultDecision === "by-category") {
22898
23152
  return {
22899
- decision: defaultDecisionByCategory(ref.toolName),
23153
+ decision: defaultDecisionByCategory(ref.toolName, input2),
22900
23154
  grantKey,
22901
23155
  source: "category-default"
22902
23156
  };
@@ -22917,7 +23171,7 @@ var PermissionEngine = class {
22917
23171
  #hardDeny(tool, input2, grantKey) {
22918
23172
  const paths = inputPaths(tool, input2);
22919
23173
  for (const item of paths) {
22920
- const resolved = path11.resolve(this.#cwd, item);
23174
+ const resolved = path12.resolve(this.#cwd, item);
22921
23175
  if (!isInsidePath(this.#cwd, resolved)) {
22922
23176
  return {
22923
23177
  decision: "deny",
@@ -22971,7 +23225,7 @@ function matchesRule(rule, tool, input2, cwd) {
22971
23225
  if (rule.match.pathGlob) {
22972
23226
  const paths = inputPaths(tool, input2);
22973
23227
  if (paths.length === 0) return false;
22974
- if (!paths.some((item) => matchesPathRule(rule.match?.pathGlob, relativeToCwd(cwd, path11.resolve(cwd, item))))) {
23228
+ if (!paths.some((item) => matchesPathRule(rule.match?.pathGlob, relativeToCwd(cwd, path12.resolve(cwd, item))))) {
22975
23229
  return false;
22976
23230
  }
22977
23231
  }
@@ -23018,7 +23272,7 @@ function rulePriority(rule, ref) {
23018
23272
  }
23019
23273
  function matchesPathRule(pattern, relativePath) {
23020
23274
  if (!pattern) return true;
23021
- if (path11.basename(relativePath) === ".env.example" && pattern.includes(".env.*")) return false;
23275
+ if (path12.basename(relativePath) === ".env.example" && pattern.includes(".env.*")) return false;
23022
23276
  return matchGlob(pattern, relativePath);
23023
23277
  }
23024
23278
  function mostSpecificRule(rules, input2, cwd, ref) {
@@ -23048,10 +23302,12 @@ function commandInput(input2) {
23048
23302
  const object2 = input2 && typeof input2 === "object" && !Array.isArray(input2) ? input2 : {};
23049
23303
  return typeof object2.command === "string" ? object2.command : "";
23050
23304
  }
23051
- function defaultDecisionByCategory(toolName) {
23052
- return READ_ONLY_TOOLS.has(toolName) ? "allow" : "ask";
23305
+ function defaultDecisionByCategory(toolName, input2) {
23306
+ if (toolName.toLowerCase() === "bash") return isClearlyReadOnlyShellCommand(commandInput(input2)) ? "allow" : "ask";
23307
+ return READ_ONLY_TOOLS.has(toolName) || ORCHESTRATION_TOOLS.has(toolName) ? "allow" : "ask";
23053
23308
  }
23054
- var READ_ONLY_TOOLS = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "read_file", "glob", "grep"]);
23309
+ var READ_ONLY_TOOLS = /* @__PURE__ */ new Set(["Read", "Glob", "Grep", "LS", "read_file", "glob", "grep"]);
23310
+ var ORCHESTRATION_TOOLS = /* @__PURE__ */ new Set(["delegate_agent", "cowork", "update_plan", "report_progress", "get_goal"]);
23055
23311
 
23056
23312
  // src/permissions/prompt.ts
23057
23313
  import readline2 from "node:readline/promises";
@@ -23096,13 +23352,13 @@ function permissionLabel(req) {
23096
23352
  if ((req.tool === "write_file" || req.tool === "edit_file" || req.tool === "read_file") && typeof inputObject.path === "string") {
23097
23353
  return `${req.tool}: ${inputObject.path}`;
23098
23354
  }
23099
- const path36 = typeof inputObject.path === "string" ? inputObject.path : typeof inputObject.file_path === "string" ? inputObject.file_path : void 0;
23100
- if (path36) return `${tool}: ${path36}`;
23355
+ const path37 = typeof inputObject.path === "string" ? inputObject.path : typeof inputObject.file_path === "string" ? inputObject.file_path : void 0;
23356
+ if (path37) return `${tool}: ${path37}`;
23101
23357
  return tool;
23102
23358
  }
23103
23359
 
23104
23360
  // src/workspace/write-scope.ts
23105
- import path12 from "node:path";
23361
+ import path13 from "node:path";
23106
23362
  var WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "Write", "Edit", "MultiEdit", "NotebookEdit"]);
23107
23363
  var WORKSPACE_SCOPE = "**";
23108
23364
  function normalizeWriteScope(cwd, scope) {
@@ -23124,7 +23380,7 @@ function enforceToolWriteScope(input2) {
23124
23380
  if (paths.length === 0) return { ok: true, paths: [] };
23125
23381
  const checked = [];
23126
23382
  for (const target of paths) {
23127
- const resolved = path12.resolve(input2.cwd, target);
23383
+ const resolved = path13.resolve(input2.cwd, target);
23128
23384
  const relative = relativeToCwd(input2.cwd, resolved);
23129
23385
  checked.push(relative);
23130
23386
  const allowed = isPathAllowedByWriteScope(input2.cwd, target, input2.writeScope);
@@ -23148,7 +23404,7 @@ function outOfScopeDiffFiles(cwd, summary, writeScope) {
23148
23404
  return out;
23149
23405
  }
23150
23406
  function isPathAllowedByWriteScope(cwd, inputPath, writeScope) {
23151
- const resolved = path12.resolve(cwd, inputPath);
23407
+ const resolved = path13.resolve(cwd, inputPath);
23152
23408
  if (!isInsidePath(cwd, resolved)) {
23153
23409
  return { ok: false, error: "path is outside the workspace", paths: [inputPath] };
23154
23410
  }
@@ -23167,9 +23423,9 @@ function normalizeScopePattern(cwd, raw) {
23167
23423
  if (!value) return { ok: false, error: "writeScope entries must be non-empty." };
23168
23424
  if (value.includes("\0")) return { ok: false, error: "writeScope entries must not contain NUL bytes." };
23169
23425
  const hasGlob2 = /[*?[\]{}]/.test(value);
23170
- if (path12.isAbsolute(value)) {
23426
+ if (path13.isAbsolute(value)) {
23171
23427
  if (hasGlob2) return { ok: false, error: `writeScope absolute globs are not supported: ${raw}` };
23172
- const resolved = path12.resolve(value);
23428
+ const resolved = path13.resolve(value);
23173
23429
  if (!isInsidePath(cwd, resolved)) return { ok: false, error: `writeScope path is outside the workspace: ${raw}` };
23174
23430
  value = relativeToCwd(cwd, resolved);
23175
23431
  }
@@ -23244,7 +23500,7 @@ function createClaudeCodeCanUseTool(options) {
23244
23500
  return { behavior: "allow", updatedInput: input2, toolUseID: ctx.toolUseID };
23245
23501
  }
23246
23502
  if (evaluation.decision === "allow") {
23247
- return { behavior: "allow", updatedInput: input2, toolUseID: ctx.toolUseID };
23503
+ return allowResult(input2, ctx.toolUseID, evaluation.source === "grant" ? "user_permanent" : void 0);
23248
23504
  }
23249
23505
  if (evaluation.decision === "deny") {
23250
23506
  emitPermission(options, {
@@ -23256,15 +23512,7 @@ function createClaudeCodeCanUseTool(options) {
23256
23512
  });
23257
23513
  return { behavior: "deny", message: evaluation.reason ?? `Permission denied for ${qualifiedName}`, toolUseID: ctx.toolUseID };
23258
23514
  }
23259
- emitPermission(options, {
23260
- type: "permission.requested",
23261
- callId: ctx.toolUseID,
23262
- tool: qualifiedName,
23263
- decision: "ask",
23264
- targetName: toolName
23265
- });
23266
- const prompt = options.permissionPrompt ?? ((req) => askPermission(req, { yes: options.yes }));
23267
- const answer = await prompt({
23515
+ const permissionRequest = {
23268
23516
  tool: qualifiedName,
23269
23517
  executor: "claudecode",
23270
23518
  qualifiedName,
@@ -23275,10 +23523,32 @@ function createClaudeCodeCanUseTool(options) {
23275
23523
  proposedDecision: "ask",
23276
23524
  reason: ctx.title ?? ctx.decisionReason ?? evaluation.reason,
23277
23525
  description: ctx.description ?? ctx.displayName
23278
- });
23279
- if (answer.decision === "allow") {
23280
- if (answer.always) grants.add(evaluation.grantKey);
23281
- if (answer.always) {
23526
+ };
23527
+ const automatic = await options.autoPermission?.(permissionRequest);
23528
+ if (automatic?.decision === "allow") return allowResult(input2, ctx.toolUseID);
23529
+ if (automatic?.decision === "deny") {
23530
+ emitPermission(options, {
23531
+ type: "permission.denied",
23532
+ callId: ctx.toolUseID,
23533
+ tool: qualifiedName,
23534
+ reason: automatic.reason,
23535
+ targetName: toolName
23536
+ });
23537
+ return { behavior: "deny", message: automatic.reason ?? `Permission denied for ${qualifiedName}`, toolUseID: ctx.toolUseID };
23538
+ }
23539
+ emitPermission(options, {
23540
+ type: "permission.requested",
23541
+ callId: ctx.toolUseID,
23542
+ tool: qualifiedName,
23543
+ decision: "ask",
23544
+ targetName: toolName
23545
+ });
23546
+ const prompt = options.permissionPrompt ?? ((req) => askPermission(req, { yes: options.yes }));
23547
+ const answer = await prompt(permissionRequest);
23548
+ if (answer.decision === "allow") {
23549
+ if (answer.always) {
23550
+ grants.add(evaluation.grantKey);
23551
+ await options.onAlwaysGrant?.(evaluation.grantKey);
23282
23552
  emitPermission(options, {
23283
23553
  type: "permission.granted",
23284
23554
  callId: ctx.toolUseID,
@@ -23286,7 +23556,10 @@ function createClaudeCodeCanUseTool(options) {
23286
23556
  targetName: toolName
23287
23557
  });
23288
23558
  }
23289
- return { behavior: "allow", updatedInput: input2, toolUseID: ctx.toolUseID };
23559
+ return {
23560
+ ...allowResult(input2, ctx.toolUseID, answer.always ? "user_permanent" : "user_temporary"),
23561
+ updatedPermissions: answer.always ? ctx.suggestions : void 0
23562
+ };
23290
23563
  }
23291
23564
  emitPermission(options, {
23292
23565
  type: "permission.denied",
@@ -23298,6 +23571,14 @@ function createClaudeCodeCanUseTool(options) {
23298
23571
  return { behavior: "deny", message: answer.reason ?? `Permission denied for ${qualifiedName}`, toolUseID: ctx.toolUseID };
23299
23572
  };
23300
23573
  }
23574
+ function allowResult(input2, toolUseID, decisionClassification) {
23575
+ return {
23576
+ behavior: "allow",
23577
+ updatedInput: input2,
23578
+ toolUseID,
23579
+ ...decisionClassification ? { decisionClassification } : {}
23580
+ };
23581
+ }
23301
23582
  function isImplicitPlanExit(options, toolName, source) {
23302
23583
  if (options.permissionMode !== "plan" || toolName !== "ExitPlanMode") return false;
23303
23584
  return source === "default" || source === "category-default";
@@ -23427,7 +23708,7 @@ function buildClaudeCodeSdkOptions(config, req, abortController, resumeSessionId
23427
23708
  permissionMode,
23428
23709
  disallowedTools: uniqueDisallowedTools.length ? uniqueDisallowedTools : void 0,
23429
23710
  tools,
23430
- maxTurns: config.maxTurns,
23711
+ maxTurns: req.maxTurns === null ? void 0 : req.maxTurns ?? config.maxTurns,
23431
23712
  maxBudgetUsd: config.maxBudgetUsd,
23432
23713
  extraArgs: config.useBareMode ? { bare: null } : void 0,
23433
23714
  allowDangerouslySkipPermissions: permissionMode === "bypassPermissions" ? true : void 0,
@@ -23438,7 +23719,10 @@ function buildClaudeCodeSdkOptions(config, req, abortController, resumeSessionId
23438
23719
  permissionMode,
23439
23720
  defaultDecision: config.defaultDecision,
23440
23721
  writeScope: req.writeScope,
23722
+ grants: req.grants,
23723
+ onAlwaysGrant: req.onPermissionGrant,
23441
23724
  permissionPrompt: req.permissionPrompt,
23725
+ autoPermission: req.autoPermission,
23442
23726
  yes: req.yes,
23443
23727
  sessionId: req.sessionId,
23444
23728
  emit: req.emit,
@@ -23487,6 +23771,9 @@ function mapSdkMessage(message) {
23487
23771
  if (message.type === "assistant") {
23488
23772
  return mapAssistantMessage(message);
23489
23773
  }
23774
+ if (message.type === "user") {
23775
+ return mapUserMessage(message);
23776
+ }
23490
23777
  if (message.type === "stream_event") {
23491
23778
  return mapPartialAssistantMessage(message);
23492
23779
  }
@@ -23526,6 +23813,35 @@ function mapAssistantMessage(message) {
23526
23813
  }
23527
23814
  return events;
23528
23815
  }
23816
+ function mapUserMessage(message) {
23817
+ const events = [];
23818
+ const content = Array.isArray(message.message.content) ? message.message.content : [];
23819
+ for (const block of content) {
23820
+ if (!block || typeof block !== "object" || block.type !== "tool_result") continue;
23821
+ const callId = typeof block.tool_use_id === "string" ? block.tool_use_id : `tool_result_${events.length + 1}`;
23822
+ events.push({
23823
+ type: "tool.completed",
23824
+ callId,
23825
+ name: "unknown",
23826
+ ok: block.is_error !== true,
23827
+ preview: toolResultPreview(block.content),
23828
+ raw: message
23829
+ });
23830
+ }
23831
+ return events;
23832
+ }
23833
+ function toolResultPreview(value) {
23834
+ if (typeof value === "string") return value.slice(0, 800);
23835
+ if (Array.isArray(value)) {
23836
+ return value.map((item) => {
23837
+ if (typeof item === "string") return item;
23838
+ if (item && typeof item === "object" && typeof item.text === "string") return String(item.text);
23839
+ return JSON.stringify(item);
23840
+ }).join("\n").slice(0, 800);
23841
+ }
23842
+ if (value === void 0) return "";
23843
+ return JSON.stringify(value).slice(0, 800);
23844
+ }
23529
23845
  function mapPartialAssistantMessage(message) {
23530
23846
  const event = message.event;
23531
23847
  if (event.type !== "content_block_delta") return [];
@@ -23546,10 +23862,62 @@ function claudeCodePreviewEnabled(config = {}, env = process.env) {
23546
23862
  }
23547
23863
 
23548
23864
  // src/config.ts
23865
+ var COWORK_AGENT_NAMES = ["planner", "architect", "supervisor", "researcher", "builder", "reviewer", "specialist"];
23866
+ var COWORK_AGENT_MIGRATIONS = {
23867
+ "codex-planner": { replacement: "planner", note: "planning and coordination work moved to planner" },
23868
+ "codex-reviewer": { replacement: "supervisor", note: "supervisory risk review moved to supervisor" },
23869
+ "claudecode-explorer": { replacement: "researcher", note: "repository exploration moved to researcher; use reviewer for diff-level review" },
23870
+ "claudecode-builder": { replacement: "builder", note: "bounded implementation moved to builder" },
23871
+ claudecode: { replacement: "builder", note: "Claude Code cowork implementation moved to builder" }
23872
+ };
23549
23873
  var BUILTIN_PROVIDER_ORDER = ["openai", "anthropic", "gemini", "groq", "azure", "lmstudio", "ollama-local", "ollama-cloud", "llamacpp", "vllm", "codex", "claudecode"];
23550
23874
  var CLAUDE_CODE_SONNET_MODEL = "claude-sonnet-4-6";
23551
23875
  var CLAUDE_CODE_OPUS_MODEL = "claude-opus-4-7";
23552
23876
  var CLAUDE_CODE_MODELS = [CLAUDE_CODE_SONNET_MODEL, CLAUDE_CODE_OPUS_MODEL];
23877
+ var DEFAULT_COWORK_AGENT_PROVIDERS = {
23878
+ planner: ["codex", "anthropic", "openai"],
23879
+ architect: ["codex", "anthropic"],
23880
+ supervisor: ["codex", "anthropic"],
23881
+ researcher: ["codex", "claudecode", "anthropic"],
23882
+ builder: ["claudecode", "anthropic", "openai"],
23883
+ reviewer: ["claudecode", "anthropic", "codex"],
23884
+ specialist: ["claudecode", "anthropic"]
23885
+ };
23886
+ var DEFAULT_COWORK_PERMISSION_OVERLAY = {
23887
+ providerToolNamespaces: {
23888
+ claudecode: "claudecode"
23889
+ },
23890
+ modeMappings: {
23891
+ "read-only": {
23892
+ demianTools: ["read_file", "grep", "glob", "list_dir"],
23893
+ providerTools: {
23894
+ claudecode: ["Read", "Grep", "Glob", "LS"]
23895
+ },
23896
+ defaultDecision: "allow"
23897
+ },
23898
+ review: {
23899
+ demianTools: ["read_file", "grep", "glob", "git_diff"],
23900
+ providerTools: {
23901
+ claudecode: ["Read", "Grep", "Glob", "LS"]
23902
+ },
23903
+ defaultDecision: "allow"
23904
+ },
23905
+ write: {
23906
+ demianTools: ["read_file", "grep", "glob", "edit_file", "write_file", "bash"],
23907
+ providerTools: {
23908
+ claudecode: ["Read", "Grep", "Glob", "LS", "Edit", "MultiEdit", "Write", "Bash"]
23909
+ },
23910
+ defaultDecision: "ask"
23911
+ },
23912
+ manage: {
23913
+ demianTools: ["read_file", "grep", "glob", "task_control", "git_diff"],
23914
+ providerTools: {
23915
+ claudecode: ["Read", "Grep", "Glob", "LS"]
23916
+ },
23917
+ defaultDecision: "ask"
23918
+ }
23919
+ }
23920
+ };
23553
23921
  var defaultConfig = {
23554
23922
  agentMode: "single-agent",
23555
23923
  defaultAgent: "general",
@@ -23590,7 +23958,31 @@ var defaultConfig = {
23590
23958
  defaultMergeStrategy: "synthesize",
23591
23959
  defaultInvocationDecision: "ask",
23592
23960
  readOnlyParallelism: true,
23593
- writerIsolation: "off"
23961
+ writerIsolation: "off",
23962
+ providers: ["codex", "claudecode"],
23963
+ preflight: {
23964
+ mode: "session-start",
23965
+ ttlMs: 10 * 60 * 1e3
23966
+ },
23967
+ fallback: {
23968
+ nonWrite: {
23969
+ onConnectionError: "next-provider",
23970
+ onTokenLimit: "next-provider",
23971
+ onAuthFailure: "skip-provider",
23972
+ onAllProvidersFailed: "fail-member"
23973
+ },
23974
+ write: {
23975
+ onFailure: "repair-from-worktree",
23976
+ maxRepairAttempts: 1,
23977
+ readChangedFilesBeforeRepair: true
23978
+ }
23979
+ },
23980
+ routing: {
23981
+ preset: "codex-manager-claudecode-worker",
23982
+ agentProviders: structuredClone(DEFAULT_COWORK_AGENT_PROVIDERS),
23983
+ agentModelPools: {}
23984
+ },
23985
+ permissionOverlay: structuredClone(DEFAULT_COWORK_PERMISSION_OVERLAY)
23594
23986
  },
23595
23987
  goals: {
23596
23988
  enabled: true,
@@ -23670,8 +24062,13 @@ var defaultConfig = {
23670
24062
  build: "build",
23671
24063
  execute: "build",
23672
24064
  claudecode: "build",
23673
- "claudecode-builder": "build",
23674
- "claudecode-explorer": "explore"
24065
+ planner: "plan",
24066
+ architect: "plan",
24067
+ supervisor: "readonly",
24068
+ researcher: "readonly",
24069
+ builder: "build",
24070
+ reviewer: "readonly",
24071
+ specialist: "readonly"
23675
24072
  },
23676
24073
  profiles: {
23677
24074
  default: {
@@ -23857,8 +24254,8 @@ var defaultConfig = {
23857
24254
  };
23858
24255
  async function loadConfig(options) {
23859
24256
  const configs = [];
23860
- const userConfigDir = path13.join(os7.homedir(), ".demian");
23861
- const configPaths = [path13.join(userConfigDir, "config.json"), path13.join(userConfigDir, "config.jsond"), options.configPath].filter(Boolean);
24257
+ const userConfigDir = path14.join(os7.homedir(), ".demian");
24258
+ const configPaths = [path14.join(userConfigDir, "config.json"), path14.join(userConfigDir, "config.jsond"), options.configPath].filter(Boolean);
23862
24259
  for (const filePath of configPaths) {
23863
24260
  if (!fs7.existsSync(filePath)) continue;
23864
24261
  const parsed = parseConfigText(await readFile6(filePath, "utf8"), filePath);
@@ -24046,14 +24443,7 @@ function mergeConfig(base, overlay) {
24046
24443
  ...base.delegation,
24047
24444
  ...overlay.delegation
24048
24445
  },
24049
- cowork: {
24050
- ...base.cowork,
24051
- ...overlay.cowork,
24052
- maxConcurrentPerProvider: {
24053
- ...base.cowork.maxConcurrentPerProvider,
24054
- ...overlay.cowork?.maxConcurrentPerProvider
24055
- }
24056
- },
24446
+ cowork: mergeCoworkConfig(base.cowork, overlay.cowork),
24057
24447
  goals: {
24058
24448
  ...base.goals,
24059
24449
  ...overlay.goals,
@@ -24184,6 +24574,168 @@ function resolveProviderRuntimeConfig(config, selection) {
24184
24574
  modelProfileName: profile.name
24185
24575
  };
24186
24576
  }
24577
+ function mergeCoworkConfig(base, overlay) {
24578
+ if (!overlay) return structuredClone(base);
24579
+ return {
24580
+ ...base,
24581
+ ...overlay,
24582
+ providers: overlay.providers !== void 0 ? canonicalStringArray(overlay.providers, "cowork.providers") : [...base.providers],
24583
+ maxConcurrentPerProvider: {
24584
+ ...base.maxConcurrentPerProvider,
24585
+ ...overlay.maxConcurrentPerProvider
24586
+ },
24587
+ preflight: {
24588
+ ...base.preflight,
24589
+ ...overlay.preflight
24590
+ },
24591
+ fallback: {
24592
+ nonWrite: {
24593
+ ...base.fallback.nonWrite,
24594
+ ...overlay.fallback?.nonWrite
24595
+ },
24596
+ write: {
24597
+ ...base.fallback.write,
24598
+ ...overlay.fallback?.write
24599
+ }
24600
+ },
24601
+ routing: {
24602
+ ...base.routing,
24603
+ ...overlay.routing,
24604
+ agentProviders: mergeCoworkAgentProviders(base.routing.agentProviders, overlay.routing?.agentProviders),
24605
+ agentModelPools: mergeCoworkAgentModelPools(base.routing.agentModelPools, overlay.routing?.agentModelPools)
24606
+ },
24607
+ permissionOverlay: mergeCoworkPermissionOverlay(base.permissionOverlay, overlay.permissionOverlay)
24608
+ };
24609
+ }
24610
+ function mergeCoworkAgentProviders(base, overlay) {
24611
+ const out = { ...cloneAgentProviders(base) };
24612
+ for (const [rawAgent, rawProviders] of Object.entries(overlay ?? {})) {
24613
+ const agent = validateCoworkAgentName(rawAgent, `cowork.routing.agentProviders.${rawAgent}`);
24614
+ out[agent] = canonicalStringArray(rawProviders, `cowork.routing.agentProviders.${agent}`);
24615
+ }
24616
+ return out;
24617
+ }
24618
+ function cloneAgentProviders(input2) {
24619
+ const out = {};
24620
+ for (const [agent, providers] of Object.entries(input2)) {
24621
+ out[agent] = [...providers ?? []];
24622
+ }
24623
+ return out;
24624
+ }
24625
+ function mergeCoworkAgentModelPools(base, overlay) {
24626
+ const out = { ...cloneAgentModelPools(base) };
24627
+ for (const [rawAgent, rawEntries] of Object.entries(overlay ?? {})) {
24628
+ const agent = validateCoworkAgentName(rawAgent, `cowork.routing.agentModelPools.${rawAgent}`);
24629
+ out[agent] = canonicalCoworkModelPool(rawEntries, `cowork.routing.agentModelPools.${agent}`);
24630
+ }
24631
+ return out;
24632
+ }
24633
+ function cloneAgentModelPools(input2) {
24634
+ const out = {};
24635
+ for (const [agent, entries] of Object.entries(input2)) {
24636
+ out[agent] = (entries ?? []).map((entry) => ({ ...entry }));
24637
+ }
24638
+ return out;
24639
+ }
24640
+ function canonicalCoworkModelPool(value, pathName) {
24641
+ if (!Array.isArray(value)) throw new Error(`${pathName} must be an array.`);
24642
+ const out = [];
24643
+ const seen = /* @__PURE__ */ new Set();
24644
+ for (const [index, raw] of value.entries()) {
24645
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${pathName}[${index}] must be an object.`);
24646
+ const entry = raw;
24647
+ if (typeof entry.provider !== "string" || !entry.provider.trim()) throw new Error(`${pathName}[${index}].provider must be a non-empty string.`);
24648
+ const provider = entry.provider.trim();
24649
+ const model = typeof entry.model === "string" && entry.model.trim() ? entry.model.trim() : void 0;
24650
+ const modelProfileName = typeof entry.modelProfileName === "string" && entry.modelProfileName.trim() ? entry.modelProfileName.trim() : void 0;
24651
+ const key = `${provider}\0${modelProfileName ?? ""}\0${model ?? ""}`;
24652
+ if (seen.has(key)) continue;
24653
+ seen.add(key);
24654
+ out.push({
24655
+ provider,
24656
+ ...model ? { model } : {},
24657
+ ...modelProfileName ? { modelProfileName } : {}
24658
+ });
24659
+ }
24660
+ return out;
24661
+ }
24662
+ function mergeCoworkPermissionOverlay(base, overlay) {
24663
+ const providerToolNamespaces = {
24664
+ ...base.providerToolNamespaces,
24665
+ ...stringMap(overlay?.providerToolNamespaces, "cowork.permissionOverlay.providerToolNamespaces")
24666
+ };
24667
+ const modeMappings = {};
24668
+ for (const [mode, mapping] of Object.entries(base.modeMappings)) {
24669
+ if (mapping) modeMappings[mode] = clonePermissionModeOverlay(mapping);
24670
+ }
24671
+ for (const [rawMode, rawMapping] of Object.entries(overlay?.modeMappings ?? {})) {
24672
+ const mode = validateCoworkPermissionIntent(rawMode, `cowork.permissionOverlay.modeMappings.${rawMode}`);
24673
+ const baseMapping = modeMappings[mode];
24674
+ if (!rawMapping || typeof rawMapping !== "object" || Array.isArray(rawMapping)) throw new Error(`cowork.permissionOverlay.modeMappings.${mode} must be an object.`);
24675
+ const mapping = rawMapping;
24676
+ modeMappings[mode] = {
24677
+ demianTools: mapping.demianTools !== void 0 ? canonicalStringArray(mapping.demianTools, `cowork.permissionOverlay.modeMappings.${mode}.demianTools`) : [...baseMapping?.demianTools ?? []],
24678
+ providerTools: {
24679
+ ...baseMapping ? cloneProviderTools(baseMapping.providerTools) : {},
24680
+ ...providerToolsMap(mapping.providerTools, `cowork.permissionOverlay.modeMappings.${mode}.providerTools`)
24681
+ },
24682
+ defaultDecision: validatePermissionDefault(mapping.defaultDecision ?? baseMapping?.defaultDecision ?? "ask", `cowork.permissionOverlay.modeMappings.${mode}.defaultDecision`)
24683
+ };
24684
+ }
24685
+ return { providerToolNamespaces, modeMappings };
24686
+ }
24687
+ function clonePermissionModeOverlay(mapping) {
24688
+ return {
24689
+ demianTools: [...mapping.demianTools],
24690
+ providerTools: cloneProviderTools(mapping.providerTools),
24691
+ defaultDecision: mapping.defaultDecision
24692
+ };
24693
+ }
24694
+ function cloneProviderTools(input2) {
24695
+ return Object.fromEntries(Object.entries(input2).map(([provider, tools]) => [provider, [...tools]]));
24696
+ }
24697
+ function providerToolsMap(value, pathName) {
24698
+ if (value === void 0) return {};
24699
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${pathName} must be an object.`);
24700
+ const out = {};
24701
+ for (const [provider, tools] of Object.entries(value)) {
24702
+ out[provider] = canonicalStringArray(tools, `${pathName}.${provider}`);
24703
+ }
24704
+ return out;
24705
+ }
24706
+ function stringMap(value, pathName) {
24707
+ if (value === void 0) return {};
24708
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${pathName} must be an object.`);
24709
+ const out = {};
24710
+ for (const [key, raw] of Object.entries(value)) {
24711
+ if (typeof raw !== "string" || !raw.trim()) throw new Error(`${pathName}.${key} must be a non-empty string.`);
24712
+ out[key] = raw.trim();
24713
+ }
24714
+ return out;
24715
+ }
24716
+ function canonicalStringArray(value, pathName) {
24717
+ if (!Array.isArray(value)) throw new Error(`${pathName} must be an array of strings.`);
24718
+ const out = [];
24719
+ for (const [index, item] of value.entries()) {
24720
+ if (typeof item !== "string" || !item.trim()) throw new Error(`${pathName}[${index}] must be a non-empty string.`);
24721
+ out.push(item.trim());
24722
+ }
24723
+ return [...new Set(out)];
24724
+ }
24725
+ function validatePermissionDefault(value, pathName) {
24726
+ if (value === "allow" || value === "ask" || value === "deny") return value;
24727
+ throw new Error(`${pathName} must be allow, ask, or deny.`);
24728
+ }
24729
+ function validateCoworkAgentName(value, pathName = "cowork agent") {
24730
+ if (COWORK_AGENT_NAMES.includes(value)) return value;
24731
+ const migration = COWORK_AGENT_MIGRATIONS[value];
24732
+ if (migration) throw new Error(`${pathName} uses removed cowork agent "${value}". Use "${migration.replacement}" instead (${migration.note}).`);
24733
+ throw new Error(`${pathName} must be one of: ${COWORK_AGENT_NAMES.join(", ")}.`);
24734
+ }
24735
+ function validateCoworkPermissionIntent(value, pathName) {
24736
+ if (value === "read-only" || value === "review" || value === "write" || value === "manage") return value;
24737
+ throw new Error(`${pathName} must be read-only, review, write, or manage.`);
24738
+ }
24187
24739
  function mergeModelProfile(providerConfig, profile) {
24188
24740
  const merged = {
24189
24741
  ...providerConfig,
@@ -24445,20 +24997,21 @@ var CLAUDE_CODE_PERMISSION_PROFILE_KEYS = ["permissionMode", "defaultDecision",
24445
24997
  import { spawn as spawn4 } from "node:child_process";
24446
24998
  import { stat as stat4 } from "node:fs/promises";
24447
24999
  import os10 from "node:os";
24448
- import path16 from "node:path";
25000
+ import path17 from "node:path";
24449
25001
  import readline3 from "node:readline/promises";
24450
25002
 
24451
25003
  // src/config-scaffold.ts
24452
25004
  import { chmod as chmod3, mkdir as mkdir6, readFile as readFile7, rename as rename4, stat as stat3, writeFile as writeFile5 } from "node:fs/promises";
24453
25005
  import os8 from "node:os";
24454
- import path14 from "node:path";
25006
+ import path15 from "node:path";
24455
25007
  function defaultUserConfigPath() {
24456
- return path14.join(os8.homedir(), ".demian", "config.json");
25008
+ return path15.join(os8.homedir(), ".demian", "config.json");
24457
25009
  }
24458
25010
  function defaultUserConfig(defaultProvider = detectDefaultProvider()) {
24459
25011
  return {
24460
25012
  version: 2,
24461
25013
  defaultProvider,
25014
+ cowork: defaultCoworkUserConfig(),
24462
25015
  providers: {
24463
25016
  openai: {
24464
25017
  type: "openai-compatible",
@@ -24568,6 +25121,76 @@ function defaultUserConfig(defaultProvider = detectDefaultProvider()) {
24568
25121
  }
24569
25122
  };
24570
25123
  }
25124
+ function defaultCoworkUserConfig() {
25125
+ return {
25126
+ providers: ["codex", "claudecode"],
25127
+ preflight: {
25128
+ mode: "session-start",
25129
+ ttlMs: 6e5
25130
+ },
25131
+ fallback: {
25132
+ nonWrite: {
25133
+ onConnectionError: "next-provider",
25134
+ onTokenLimit: "next-provider",
25135
+ onAuthFailure: "skip-provider",
25136
+ onAllProvidersFailed: "fail-member"
25137
+ },
25138
+ write: {
25139
+ onFailure: "repair-from-worktree",
25140
+ maxRepairAttempts: 1,
25141
+ readChangedFilesBeforeRepair: true
25142
+ }
25143
+ },
25144
+ routing: {
25145
+ preset: "codex-manager-claudecode-worker",
25146
+ agentModelPools: {},
25147
+ agentProviders: {
25148
+ planner: ["codex", "anthropic", "openai"],
25149
+ architect: ["codex", "anthropic"],
25150
+ supervisor: ["codex", "anthropic"],
25151
+ researcher: ["codex", "claudecode", "anthropic"],
25152
+ builder: ["claudecode", "anthropic", "openai"],
25153
+ reviewer: ["claudecode", "anthropic", "codex"],
25154
+ specialist: ["claudecode", "anthropic"]
25155
+ }
25156
+ },
25157
+ permissionOverlay: {
25158
+ providerToolNamespaces: {
25159
+ claudecode: "claudecode"
25160
+ },
25161
+ modeMappings: {
25162
+ "read-only": {
25163
+ demianTools: ["read_file", "grep", "glob", "list_dir"],
25164
+ providerTools: {
25165
+ claudecode: ["Read", "Grep", "Glob", "LS"]
25166
+ },
25167
+ defaultDecision: "allow"
25168
+ },
25169
+ review: {
25170
+ demianTools: ["read_file", "grep", "glob", "git_diff"],
25171
+ providerTools: {
25172
+ claudecode: ["Read", "Grep", "Glob", "LS"]
25173
+ },
25174
+ defaultDecision: "allow"
25175
+ },
25176
+ write: {
25177
+ demianTools: ["read_file", "grep", "glob", "edit_file", "write_file", "bash"],
25178
+ providerTools: {
25179
+ claudecode: ["Read", "Grep", "Glob", "LS", "Edit", "MultiEdit", "Write", "Bash"]
25180
+ },
25181
+ defaultDecision: "ask"
25182
+ },
25183
+ manage: {
25184
+ demianTools: ["read_file", "grep", "glob", "task_control", "git_diff"],
25185
+ providerTools: {
25186
+ claudecode: ["Read", "Grep", "Glob", "LS"]
25187
+ },
25188
+ defaultDecision: "ask"
25189
+ }
25190
+ }
25191
+ }
25192
+ };
25193
+ }
24571
25194
  async function createUserConfig(options = {}) {
24572
25195
  const filePath = expandHome2(options.path ?? defaultUserConfigPath());
24573
25196
  const content = `${JSON.stringify(defaultUserConfig(options.defaultProvider), null, 2)}
@@ -24753,7 +25376,7 @@ function apiKeyAuthFields(options, defaultApiKeyEnv) {
24753
25376
  };
24754
25377
  }
24755
25378
  async function writeJsonAtomic(filePath, content) {
24756
- await mkdir6(path14.dirname(filePath), { recursive: true, mode: 448 });
25379
+ await mkdir6(path15.dirname(filePath), { recursive: true, mode: 448 });
24757
25380
  const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
24758
25381
  await writeFile5(temp, content, { mode: 384 });
24759
25382
  await rename4(temp, filePath);
@@ -24809,7 +25432,7 @@ function expandHome2(value) {
24809
25432
  import crypto5 from "node:crypto";
24810
25433
  import { mkdir as mkdir7, readFile as readFile8, rename as rename5, writeFile as writeFile6 } from "node:fs/promises";
24811
25434
  import os9 from "node:os";
24812
- import path15 from "node:path";
25435
+ import path16 from "node:path";
24813
25436
  var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
24814
25437
  var PING_TIMEOUT_MS = 1500;
24815
25438
  var DEFAULT_CODEX_CLIENT_VERSION = "0.0.0";
@@ -25117,7 +25740,7 @@ async function fetchJson(url, options) {
25117
25740
  return text ? JSON.parse(text) : {};
25118
25741
  }
25119
25742
  function cachePath(cacheKey) {
25120
- return path15.join(os9.homedir(), ".demian", "cache", "models", `${safeCacheName(cacheKey)}.json`);
25743
+ return path16.join(os9.homedir(), ".demian", "cache", "models", `${safeCacheName(cacheKey)}.json`);
25121
25744
  }
25122
25745
  async function readCatalogCache(cacheKey, ttlMs, allow) {
25123
25746
  if (!allow) return void 0;
@@ -25132,7 +25755,7 @@ async function readCatalogCache(cacheKey, ttlMs, allow) {
25132
25755
  }
25133
25756
  async function writeCatalogCache(cacheKey, result) {
25134
25757
  const filePath = cachePath(cacheKey);
25135
- await mkdir7(path15.dirname(filePath), { recursive: true, mode: 448 });
25758
+ await mkdir7(path16.dirname(filePath), { recursive: true, mode: 448 });
25136
25759
  const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
25137
25760
  await writeFile6(temp, JSON.stringify({ savedAt: Date.now(), result }, null, 2), { mode: 384 });
25138
25761
  await rename5(temp, filePath);
@@ -25561,8 +26184,8 @@ async function fileExists2(filePath) {
25561
26184
  }
25562
26185
  async function readJsonSafe(filePath) {
25563
26186
  try {
25564
- const { readFile: readFile18 } = await import("node:fs/promises");
25565
- const text = await readFile18(filePath, "utf8");
26187
+ const { readFile: readFile19 } = await import("node:fs/promises");
26188
+ const text = await readFile19(filePath, "utf8");
25566
26189
  return JSON.parse(text);
25567
26190
  } catch {
25568
26191
  return void 0;
@@ -25663,7 +26286,7 @@ async function walkProviderSetup() {
25663
26286
  process.stdout.write("\nSetup complete. Run `demian` to start a session.\n\n");
25664
26287
  }
25665
26288
  async function checkCodexAuth() {
25666
- const candidates = [path16.join(process.env.HOME ?? os10.homedir(), ".codex", "auth.json"), path16.join(process.env.HOME ?? os10.homedir(), ".codex", "credentials.json")];
26289
+ const candidates = [path17.join(process.env.HOME ?? os10.homedir(), ".codex", "auth.json"), path17.join(process.env.HOME ?? os10.homedir(), ".codex", "credentials.json")];
25667
26290
  for (const candidate of candidates) {
25668
26291
  if (await fileExists2(candidate)) {
25669
26292
  return { ok: true, source: "codex-store", message: candidate };
@@ -25677,7 +26300,7 @@ async function checkCodexAuth() {
25677
26300
  import { existsSync as existsSync2 } from "node:fs";
25678
26301
  import { readFile as readFile9, writeFile as writeFile7 } from "node:fs/promises";
25679
26302
  import os11 from "node:os";
25680
- import path17 from "node:path";
26303
+ import path18 from "node:path";
25681
26304
  function isDoctorCommand(argv) {
25682
26305
  return argv[0] === "doctor";
25683
26306
  }
@@ -25697,7 +26320,7 @@ async function runDoctorCommand(argv, options = {}) {
25697
26320
  stderr.write("Unsupported doctor command. Run `demian doctor policies --upgrade-namespaces [--write]`.\n");
25698
26321
  return 1;
25699
26322
  }
25700
- const cwd = path17.resolve(flags.cwd ?? options.cwd ?? process.cwd());
26323
+ const cwd = path18.resolve(flags.cwd ?? options.cwd ?? process.cwd());
25701
26324
  const filePaths = resolvePolicyConfigPaths(cwd, flags.configPaths);
25702
26325
  if (filePaths.length === 0) {
25703
26326
  stdout.write("No demian config files found to inspect.\n");
@@ -25798,11 +26421,11 @@ function parseDoctorFlags(argv) {
25798
26421
  return flags;
25799
26422
  }
25800
26423
  function resolvePolicyConfigPaths(cwd, explicitPaths) {
25801
- const candidates = explicitPaths.length > 0 ? explicitPaths.map((item) => path17.resolve(cwd, item)) : [
25802
- path17.join(os11.homedir(), ".demian", "config.json"),
25803
- path17.join(os11.homedir(), ".demian", "config.jsond"),
25804
- path17.join(cwd, ".demian", "config.json"),
25805
- path17.join(cwd, ".demian", "config.jsond")
26424
+ const candidates = explicitPaths.length > 0 ? explicitPaths.map((item) => path18.resolve(cwd, item)) : [
26425
+ path18.join(os11.homedir(), ".demian", "config.json"),
26426
+ path18.join(os11.homedir(), ".demian", "config.jsond"),
26427
+ path18.join(cwd, ".demian", "config.json"),
26428
+ path18.join(cwd, ".demian", "config.jsond")
25806
26429
  ];
25807
26430
  return [...new Set(candidates)].filter((item) => existsSync2(item));
25808
26431
  }
@@ -25897,10 +26520,68 @@ Flags:
25897
26520
  `;
25898
26521
  }
25899
26522
 
25900
- // src/transcript.ts
25901
- import { mkdir as mkdir8, appendFile, writeFile as writeFile8 } from "node:fs/promises";
26523
+ // src/permissions/persistent-grants.ts
26524
+ import { mkdir as mkdir8, readFile as readFile10, writeFile as writeFile8 } from "node:fs/promises";
26525
+ import fs8 from "node:fs";
25902
26526
  import os12 from "node:os";
25903
- import path18 from "node:path";
26527
+ import path19 from "node:path";
26528
+ var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
26529
+ var PersistentGrantStore = class {
26530
+ filePath;
26531
+ #config;
26532
+ constructor(cwd, config) {
26533
+ this.#config = config;
26534
+ this.filePath = resolveGrantPath(cwd, config);
26535
+ }
26536
+ async load(workspace) {
26537
+ if (!this.#config.enabled) return [];
26538
+ const file = await this.#read();
26539
+ const now2 = Date.now();
26540
+ const grants = file.grants.filter((item) => !item.expiresAt || item.expiresAt > now2);
26541
+ const changed = grants.length !== file.grants.length;
26542
+ const keys = grants.filter((item) => item.workspace === workspace).map((item) => item.key);
26543
+ if (changed) await this.#write({ version: 1, grants });
26544
+ return [...new Set(keys)].sort();
26545
+ }
26546
+ async add(workspace, key) {
26547
+ if (!this.#config.enabled) return;
26548
+ const file = await this.#read();
26549
+ const now2 = Date.now();
26550
+ const ttlMs = this.#config.ttlMs ?? DEFAULT_TTL_MS;
26551
+ const expiresAt = ttlMs > 0 ? now2 + ttlMs : void 0;
26552
+ const grants = file.grants.filter((item) => !(item.workspace === workspace && item.key === key));
26553
+ grants.push({ workspace, key, createdAt: now2, lastUsedAt: now2, expiresAt });
26554
+ await this.#write({ version: 1, grants });
26555
+ }
26556
+ async #read() {
26557
+ if (!fs8.existsSync(this.filePath)) return { version: 1, grants: [] };
26558
+ const data = JSON.parse(await readFile10(this.filePath, "utf8"));
26559
+ return {
26560
+ version: 1,
26561
+ grants: Array.isArray(data.grants) ? data.grants.filter(isGrantRecord) : []
26562
+ };
26563
+ }
26564
+ async #write(file) {
26565
+ await mkdir8(path19.dirname(this.filePath), { recursive: true });
26566
+ await writeFile8(this.filePath, `${JSON.stringify(file, null, 2)}
26567
+ `, { mode: 384 });
26568
+ }
26569
+ };
26570
+ function resolveGrantPath(cwd, config) {
26571
+ if (config.path) return path19.resolve(cwd, config.path);
26572
+ if ((config.scope ?? "project") === "user") return path19.join(os12.homedir(), ".demian", "grants.json");
26573
+ return path19.join(cwd, ".demian", "grants.json");
26574
+ }
26575
+ function isGrantRecord(value) {
26576
+ if (!value || typeof value !== "object") return false;
26577
+ const item = value;
26578
+ return typeof item.workspace === "string" && typeof item.key === "string" && typeof item.createdAt === "number" && typeof item.lastUsedAt === "number";
26579
+ }
26580
+
26581
+ // src/transcript.ts
26582
+ import { mkdir as mkdir9, appendFile, writeFile as writeFile9 } from "node:fs/promises";
26583
+ import os13 from "node:os";
26584
+ import path20 from "node:path";
25904
26585
  var TranscriptWriter = class {
25905
26586
  filePath;
25906
26587
  #enabled;
@@ -25908,9 +26589,9 @@ var TranscriptWriter = class {
25908
26589
  #queue = Promise.resolve();
25909
26590
  constructor(options) {
25910
26591
  this.#enabled = options.enabled !== false;
25911
- const dir = path18.join(options.storageDir ?? defaultDemianStorageDir(), "transcripts", options.sessionId);
25912
- this.filePath = path18.join(dir, "session.jsonl");
25913
- this.#ready = this.#enabled ? mkdir8(dir, { recursive: true }).then(() => writeFile8(this.filePath, "", { flag: "a" })) : Promise.resolve();
26592
+ const dir = path20.join(options.storageDir ?? defaultDemianStorageDir(), "transcripts", options.sessionId);
26593
+ this.filePath = path20.join(dir, "session.jsonl");
26594
+ this.#ready = this.#enabled ? mkdir9(dir, { recursive: true }).then(() => writeFile9(this.filePath, "", { flag: "a" })) : Promise.resolve();
25914
26595
  }
25915
26596
  write(event) {
25916
26597
  if (!this.#enabled) return Promise.resolve();
@@ -25926,14 +26607,14 @@ var TranscriptWriter = class {
25926
26607
  }
25927
26608
  };
25928
26609
  function defaultDemianStorageDir() {
25929
- return path18.join(os12.homedir(), ".demian");
26610
+ return path20.join(os13.homedir(), ".demian");
25930
26611
  }
25931
26612
 
25932
26613
  // src/external-runtime/snapshot-diff.ts
25933
26614
  import { createHash as createHash2 } from "node:crypto";
25934
26615
  import { createReadStream } from "node:fs";
25935
- import { readdir, readFile as readFile10, stat as stat5 } from "node:fs/promises";
25936
- import path19 from "node:path";
26616
+ import { readdir, readFile as readFile11, stat as stat5 } from "node:fs/promises";
26617
+ import path21 from "node:path";
25937
26618
 
25938
26619
  // src/workspace/diff.ts
25939
26620
  var CONTEXT_LINES = 3;
@@ -26143,7 +26824,7 @@ async function diffWorkspaceSnapshot(before) {
26143
26824
  }
26144
26825
  async function walk(root, relativeDir, entries, options) {
26145
26826
  if (entries.size >= options.maxFiles) return;
26146
- const absoluteDir = path19.join(root, relativeDir);
26827
+ const absoluteDir = path21.join(root, relativeDir);
26147
26828
  let items;
26148
26829
  try {
26149
26830
  items = await readdir(absoluteDir, { withFileTypes: true });
@@ -26154,8 +26835,8 @@ async function walk(root, relativeDir, entries, options) {
26154
26835
  if (entries.size >= options.maxFiles) return;
26155
26836
  if (item.name.startsWith(".") && item.name !== ".env.example" && SKIP_DIRS.has(item.name)) continue;
26156
26837
  if (SKIP_DIRS.has(item.name)) continue;
26157
- const relativePath = normalizeRelativePath(path19.join(relativeDir, item.name));
26158
- const absolutePath = path19.join(root, relativePath);
26838
+ const relativePath = normalizeRelativePath(path21.join(relativeDir, item.name));
26839
+ const absolutePath = path21.join(root, relativePath);
26159
26840
  if (item.isDirectory()) {
26160
26841
  await walk(root, relativePath, entries, options);
26161
26842
  continue;
@@ -26177,7 +26858,7 @@ function snapshotDiffText(entry) {
26177
26858
  `;
26178
26859
  }
26179
26860
  async function readTextContent(filePath) {
26180
- const buffer = await readFile10(filePath);
26861
+ const buffer = await readFile11(filePath);
26181
26862
  if (buffer.includes(0)) return void 0;
26182
26863
  return buffer.toString("utf8");
26183
26864
  }
@@ -26191,7 +26872,7 @@ function sha256File(filePath) {
26191
26872
  });
26192
26873
  }
26193
26874
  function normalizeRelativePath(value) {
26194
- return value.split(path19.sep).join("/");
26875
+ return value.split(path21.sep).join("/");
26195
26876
  }
26196
26877
 
26197
26878
  // src/external-runtime/session-runner.ts
@@ -26201,10 +26882,16 @@ var ExternalAgentSessionRunner = class {
26201
26882
  events;
26202
26883
  #options;
26203
26884
  #transcript;
26885
+ #grants;
26886
+ #persistentStore;
26204
26887
  #activeTools = /* @__PURE__ */ new Map();
26888
+ #toolStats = { requested: 0, completed: 0, failed: 0 };
26889
+ #recentToolEvents = [];
26205
26890
  #cancellationArtifactsEmitted = false;
26206
26891
  constructor(options) {
26207
26892
  this.#options = options;
26893
+ this.#grants = options.grants ?? new SessionGrants();
26894
+ if (options.persistentGrants?.enabled) this.#persistentStore = new PersistentGrantStore(options.cwd, options.persistentGrants);
26208
26895
  this.events = options.eventBus ?? new EventBus();
26209
26896
  this.#transcript = options.transcriptWriter ?? new TranscriptWriter({
26210
26897
  cwd: options.cwd,
@@ -26214,6 +26901,7 @@ var ExternalAgentSessionRunner = class {
26214
26901
  }
26215
26902
  async run() {
26216
26903
  const options = this.#options;
26904
+ for (const key of await this.#persistentStore?.load(options.cwd) ?? []) this.#grants.add(key);
26217
26905
  const messages = [{ role: "system", content: buildSystemPrompt(options.agent) }, ...options.history ?? [], { role: "user", content: options.prompt }];
26218
26906
  let finalAnswer = "";
26219
26907
  let finalTextEmitted = false;
@@ -26257,8 +26945,12 @@ var ExternalAgentSessionRunner = class {
26257
26945
  if (options.signal?.aborted) return this.endCancelled(messages, finalAnswer, externalSessionId, usage, snapshot);
26258
26946
  if (recoveredInvalidResume || !shouldRecoverInvalidResume(failure2.error, runtimePreviousExternalSessionId)) {
26259
26947
  this.emit({ type: "session.failed", sessionId: this.sessionId, error: failure2.error, ts: now(), ...this.eventContext() });
26260
- messages.push({ role: "assistant", content: finalAnswer });
26261
- return { sessionId: this.sessionId, externalSessionId, finalAnswer: finalAnswer || failure2.error, messages, endReason: "error", usage };
26948
+ const answer = this.externalTerminalFinalAnswer("error", finalAnswer || failure2.error);
26949
+ messages.push({ role: "assistant", content: answer });
26950
+ this.emit({ type: "session.ended", sessionId: this.sessionId, reason: "error", ts: now(), ...this.eventContext() });
26951
+ this.emit({ type: "model.text", sessionId: this.sessionId, text: answer, ts: now(), ...this.eventContext() });
26952
+ await this.#transcript.flush();
26953
+ return { sessionId: this.sessionId, externalSessionId, finalAnswer: answer, messages, endReason: "error", usage };
26262
26954
  }
26263
26955
  const policy = options.onInvalidResume ?? "fresh";
26264
26956
  recoveredInvalidResume = true;
@@ -26310,11 +27002,15 @@ var ExternalAgentSessionRunner = class {
26310
27002
  model: options.model,
26311
27003
  agent: options.agent,
26312
27004
  providerName: options.providerName,
27005
+ maxTurns: options.maxTurns,
26313
27006
  signal: options.signal ?? new AbortController().signal,
26314
27007
  historyPolicy: options.historyPolicy ?? "passthrough-resume",
26315
27008
  previousExternalSessionId,
26316
27009
  writeScope: options.writeScope,
26317
27010
  permissionPrompt: options.permissionPrompt,
27011
+ autoPermission: options.autoPermission,
27012
+ grants: this.#grants,
27013
+ onPermissionGrant: (grantKey) => this.#persistentStore?.add(options.cwd, grantKey) ?? Promise.resolve(),
26318
27014
  yes: options.yes,
26319
27015
  emit: (runtimeEvent) => this.emit(runtimeEvent)
26320
27016
  })) {
@@ -26329,6 +27025,8 @@ var ExternalAgentSessionRunner = class {
26329
27025
  this.emit({ type: "model.text", sessionId: this.sessionId, text: event.text, ts: now(), ...this.eventContext() });
26330
27026
  } else if (event.type === "tool.requested") {
26331
27027
  this.#activeTools.set(event.callId, { name: event.name });
27028
+ this.#toolStats.requested += 1;
27029
+ this.recordRecentToolEvent(`\uC694\uCCAD: ${event.name}`);
26332
27030
  this.emit({
26333
27031
  type: "tool.requested",
26334
27032
  sessionId: this.sessionId,
@@ -26342,8 +27040,13 @@ var ExternalAgentSessionRunner = class {
26342
27040
  ...this.eventContext()
26343
27041
  });
26344
27042
  } else if (event.type === "tool.completed") {
27043
+ const activeTool = this.#activeTools.get(event.callId);
27044
+ const name = event.name === "unknown" ? activeTool?.name ?? event.name : event.name;
26345
27045
  this.#activeTools.delete(event.callId);
26346
- this.emit({ type: "tool.completed", sessionId: this.sessionId, callId: event.callId, name: event.name, ok: event.ok, preview: event.preview ?? "", executor: "claudecode", qualifiedName: `claudecode.${event.name}`, agent: options.agent.name, ts: now(), ...this.eventContext() });
27046
+ if (event.ok) this.#toolStats.completed += 1;
27047
+ else this.#toolStats.failed += 1;
27048
+ this.recordRecentToolEvent(`${event.ok ? "\uC644\uB8CC" : "\uC2E4\uD328"}: ${name}${event.preview ? ` - ${event.preview}` : ""}`);
27049
+ this.emit({ type: "tool.completed", sessionId: this.sessionId, callId: event.callId, name, ok: event.ok, preview: event.preview ?? "", executor: "claudecode", qualifiedName: `claudecode.${name}`, agent: options.agent.name, ts: now(), ...this.eventContext() });
26347
27050
  } else if (event.type === "usage") {
26348
27051
  capture.onUsage(event.usage);
26349
27052
  this.emit({ type: "model.usage", sessionId: this.sessionId, usage: event.usage, ts: now(), ...this.eventContext() });
@@ -26362,12 +27065,31 @@ var ExternalAgentSessionRunner = class {
26362
27065
  }
26363
27066
  async endCancelled(messages, finalAnswer, externalSessionId, usage, snapshot) {
26364
27067
  await this.emitCancellationArtifacts(snapshot);
26365
- const answer = finalAnswer || "Cancelled.";
27068
+ const answer = this.externalTerminalFinalAnswer("cancelled", finalAnswer || "Cancelled.");
26366
27069
  messages.push({ role: "assistant", content: answer });
26367
27070
  this.emit({ type: "session.ended", sessionId: this.sessionId, reason: "cancelled", ts: now(), ...this.eventContext() });
27071
+ this.emit({ type: "model.text", sessionId: this.sessionId, text: answer, ts: now(), ...this.eventContext() });
26368
27072
  await this.#transcript.flush();
26369
27073
  return { sessionId: this.sessionId, externalSessionId, finalAnswer: answer, messages, endReason: "cancelled", usage };
26370
27074
  }
27075
+ recordRecentToolEvent(text) {
27076
+ this.#recentToolEvents.push(text.replace(/\s+/g, " ").trim());
27077
+ if (this.#recentToolEvents.length > 5) this.#recentToolEvents.splice(0, this.#recentToolEvents.length - 5);
27078
+ }
27079
+ externalTerminalFinalAnswer(reason, lastMessage) {
27080
+ const lines = ["\uC791\uC5C5\uC774 \uC885\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", "", `\uC885\uB8CC \uC774\uC720: ${reason === "cancelled" ? "\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4." : "\uC678\uBD80 \uB7F0\uD0C0\uC784 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4."}`];
27081
+ if (lastMessage.trim()) lines.push(`\uB9C8\uC9C0\uB9C9 \uB7F0\uD0C0\uC784 \uBA54\uC2DC\uC9C0: ${lastMessage.trim()}`);
27082
+ lines.push("", "\uD604\uC7AC\uAE4C\uC9C0 \uC9C4\uD589\uD55C \uB0B4\uC6A9\uC740 \uC544\uB798\uC640 \uAC19\uC2B5\uB2C8\uB2E4.");
27083
+ const totalTools = this.#toolStats.requested;
27084
+ if (totalTools > 0) {
27085
+ lines.push(`- Claude Code \uB7F0\uD0C0\uC784\uC5D0\uC11C \uB3C4\uAD6C ${totalTools}\uAC1C\uB97C \uC694\uCCAD\uD588\uACE0, \uC644\uB8CC ${this.#toolStats.completed}\uAC1C, \uC2E4\uD328 ${this.#toolStats.failed}\uAC1C\uB85C \uAE30\uB85D\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
27086
+ if (this.#recentToolEvents.length > 0) lines.push(`- \uCD5C\uADFC \uB3C4\uAD6C \uD750\uB984: ${this.#recentToolEvents.join("; ")}`);
27087
+ } else {
27088
+ lines.push("- \uC544\uC9C1 \uC644\uB8CC\uB41C \uB3C4\uAD6C \uC0AC\uC6A9 \uAE30\uB85D\uC740 \uC5C6\uC2B5\uB2C8\uB2E4.");
27089
+ }
27090
+ lines.push("", "\uC774\uC5B4\uC11C \uC9C4\uD589\uD558\uBA74 \uC704 \uC0C1\uD0DC\uB97C \uAE30\uC900\uC73C\uB85C \uB9C8\uC9C0\uB9C9\uC73C\uB85C \uD655\uC778\uD55C \uC9C0\uC810\uBD80\uD130 \uB2E4\uC2DC \uC774\uC5B4\uAC00\uACA0\uC2B5\uB2C8\uB2E4.");
27091
+ return lines.join("\n");
27092
+ }
26371
27093
  async createAbortSnapshot() {
26372
27094
  try {
26373
27095
  return await createWorkspaceSnapshot(this.#options.cwd);
@@ -26626,7 +27348,7 @@ function safeJson(value) {
26626
27348
  }
26627
27349
 
26628
27350
  // src/hooks/dispatcher.ts
26629
- import path21 from "node:path";
27351
+ import path23 from "node:path";
26630
27352
 
26631
27353
  // src/hooks/command.ts
26632
27354
  import { spawn as spawn5 } from "node:child_process";
@@ -26719,7 +27441,7 @@ var blockDangerousBashHook = {
26719
27441
  };
26720
27442
 
26721
27443
  // src/hooks/builtin/protect-env-files.ts
26722
- import path20 from "node:path";
27444
+ import path22 from "node:path";
26723
27445
  var protectEnvFilesHook = {
26724
27446
  name: "protect-env-files",
26725
27447
  event: "PreToolUse",
@@ -26727,7 +27449,7 @@ var protectEnvFilesHook = {
26727
27449
  run(ctx) {
26728
27450
  if (ctx.toolName !== "write_file" && ctx.toolName !== "edit_file") return { decision: "allow" };
26729
27451
  const input2 = ctx.toolInput && typeof ctx.toolInput === "object" ? ctx.toolInput : {};
26730
- const filePath = typeof input2.path === "string" ? path20.resolve(ctx.cwd, input2.path) : "";
27452
+ const filePath = typeof input2.path === "string" ? path22.resolve(ctx.cwd, input2.path) : "";
26731
27453
  if (filePath && isEnvFile(filePath)) {
26732
27454
  return {
26733
27455
  decision: "block",
@@ -26764,7 +27486,7 @@ var maskSecretsHook = {
26764
27486
  };
26765
27487
 
26766
27488
  // src/hooks/builtin/inject-env-info.ts
26767
- import os13 from "node:os";
27489
+ import os14 from "node:os";
26768
27490
  var injectEnvInfoHook = {
26769
27491
  name: "inject-env-info",
26770
27492
  event: "SessionStart",
@@ -26773,7 +27495,7 @@ var injectEnvInfoHook = {
26773
27495
  decision: "allow",
26774
27496
  patch: {
26775
27497
  systemNote: [
26776
- `Environment: ${os13.type()} ${os13.release()} (${os13.platform()}/${os13.arch()})`,
27498
+ `Environment: ${os14.type()} ${os14.release()} (${os14.platform()}/${os14.arch()})`,
26777
27499
  `cwd: ${ctx.cwd}`,
26778
27500
  `provider: ${ctx.provider ?? "unknown"}`,
26779
27501
  `model: ${ctx.model ?? "unknown"}`,
@@ -26842,14 +27564,14 @@ function matchesHook(match, ctx) {
26842
27564
  const input2 = ctx.toolInput && typeof ctx.toolInput === "object" ? ctx.toolInput : {};
26843
27565
  const filePath = typeof input2.path === "string" ? input2.path : typeof input2.workdir === "string" ? input2.workdir : void 0;
26844
27566
  if (!filePath) return false;
26845
- if (!matchGlob(match.pathGlob, relativeToCwd(ctx.cwd, path21.resolve(ctx.cwd, filePath)))) return false;
27567
+ if (!matchGlob(match.pathGlob, relativeToCwd(ctx.cwd, path23.resolve(ctx.cwd, filePath)))) return false;
26846
27568
  }
26847
27569
  return true;
26848
27570
  }
26849
27571
 
26850
27572
  // src/multimodal.ts
26851
- import { readFile as readFile11, stat as stat6 } from "node:fs/promises";
26852
- import path22 from "node:path";
27573
+ import { readFile as readFile12, stat as stat6 } from "node:fs/promises";
27574
+ import path24 from "node:path";
26853
27575
  var DEFAULT_MAX_IMAGE_BYTES = 8 * 1024 * 1024;
26854
27576
  async function buildUserContent(prompt, images = [], options) {
26855
27577
  if (images.length === 0) return prompt;
@@ -26873,11 +27595,11 @@ async function imageToUrl(input2, options) {
26873
27595
  const maxImageBytes = options.maxImageBytes ?? DEFAULT_MAX_IMAGE_BYTES;
26874
27596
  if (info.size > maxImageBytes) throw new Error(`Image is larger than ${maxImageBytes} bytes: ${input2}`);
26875
27597
  const mime = mimeFromPath(filePath);
26876
- const bytes = await readFile11(filePath);
27598
+ const bytes = await readFile12(filePath);
26877
27599
  return `data:${mime};base64,${bytes.toString("base64")}`;
26878
27600
  }
26879
27601
  function mimeFromPath(filePath) {
26880
- switch (path22.extname(filePath).toLowerCase()) {
27602
+ switch (path24.extname(filePath).toLowerCase()) {
26881
27603
  case ".jpg":
26882
27604
  case ".jpeg":
26883
27605
  return "image/jpeg";
@@ -26888,81 +27610,23 @@ function mimeFromPath(filePath) {
26888
27610
  case ".webp":
26889
27611
  return "image/webp";
26890
27612
  default:
26891
- throw new Error(`Unsupported image extension: ${path22.extname(filePath) || "(none)"}`);
27613
+ throw new Error(`Unsupported image extension: ${path24.extname(filePath) || "(none)"}`);
26892
27614
  }
26893
27615
  }
26894
27616
 
26895
- // src/permissions/persistent-grants.ts
26896
- import { mkdir as mkdir9, readFile as readFile12, writeFile as writeFile9 } from "node:fs/promises";
26897
- import fs8 from "node:fs";
26898
- import os14 from "node:os";
26899
- import path23 from "node:path";
26900
- var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
26901
- var PersistentGrantStore = class {
26902
- filePath;
26903
- #config;
26904
- constructor(cwd, config) {
26905
- this.#config = config;
26906
- this.filePath = resolveGrantPath(cwd, config);
26907
- }
26908
- async load(workspace) {
26909
- if (!this.#config.enabled) return [];
26910
- const file = await this.#read();
26911
- const now2 = Date.now();
26912
- const grants = file.grants.filter((item) => !item.expiresAt || item.expiresAt > now2);
26913
- const changed = grants.length !== file.grants.length;
26914
- const keys = grants.filter((item) => item.workspace === workspace).map((item) => item.key);
26915
- if (changed) await this.#write({ version: 1, grants });
26916
- return [...new Set(keys)].sort();
26917
- }
26918
- async add(workspace, key) {
26919
- if (!this.#config.enabled) return;
26920
- const file = await this.#read();
26921
- const now2 = Date.now();
26922
- const ttlMs = this.#config.ttlMs ?? DEFAULT_TTL_MS;
26923
- const expiresAt = ttlMs > 0 ? now2 + ttlMs : void 0;
26924
- const grants = file.grants.filter((item) => !(item.workspace === workspace && item.key === key));
26925
- grants.push({ workspace, key, createdAt: now2, lastUsedAt: now2, expiresAt });
26926
- await this.#write({ version: 1, grants });
27617
+ // src/progress/ledger.ts
27618
+ var ProgressLedger = class {
27619
+ #rootSessionId;
27620
+ #plan;
27621
+ #notes = [];
27622
+ #maxNotes;
27623
+ #planCounter = 0;
27624
+ #noteCounter = 0;
27625
+ constructor(options = {}) {
27626
+ this.#rootSessionId = options.rootSessionId;
27627
+ this.#maxNotes = options.maxNotes ?? 50;
26927
27628
  }
26928
- async #read() {
26929
- if (!fs8.existsSync(this.filePath)) return { version: 1, grants: [] };
26930
- const data = JSON.parse(await readFile12(this.filePath, "utf8"));
26931
- return {
26932
- version: 1,
26933
- grants: Array.isArray(data.grants) ? data.grants.filter(isGrantRecord) : []
26934
- };
26935
- }
26936
- async #write(file) {
26937
- await mkdir9(path23.dirname(this.filePath), { recursive: true });
26938
- await writeFile9(this.filePath, `${JSON.stringify(file, null, 2)}
26939
- `, { mode: 384 });
26940
- }
26941
- };
26942
- function resolveGrantPath(cwd, config) {
26943
- if (config.path) return path23.resolve(cwd, config.path);
26944
- if ((config.scope ?? "project") === "user") return path23.join(os14.homedir(), ".demian", "grants.json");
26945
- return path23.join(cwd, ".demian", "grants.json");
26946
- }
26947
- function isGrantRecord(value) {
26948
- if (!value || typeof value !== "object") return false;
26949
- const item = value;
26950
- return typeof item.workspace === "string" && typeof item.key === "string" && typeof item.createdAt === "number" && typeof item.lastUsedAt === "number";
26951
- }
26952
-
26953
- // src/progress/ledger.ts
26954
- var ProgressLedger = class {
26955
- #rootSessionId;
26956
- #plan;
26957
- #notes = [];
26958
- #maxNotes;
26959
- #planCounter = 0;
26960
- #noteCounter = 0;
26961
- constructor(options = {}) {
26962
- this.#rootSessionId = options.rootSessionId;
26963
- this.#maxNotes = options.maxNotes ?? 50;
26964
- }
26965
- snapshot() {
27629
+ snapshot() {
26966
27630
  return {
26967
27631
  plan: this.state(),
26968
27632
  notes: this.#notes.map((note) => ({ ...note }))
@@ -27080,7 +27744,7 @@ var ProgressLedger = class {
27080
27744
  return this.#plan;
27081
27745
  }
27082
27746
  };
27083
- var statuses = /* @__PURE__ */ new Set(["pending", "in_progress", "completed", "blocked", "skipped", "failed"]);
27747
+ var statuses = /* @__PURE__ */ new Set(["pending", "in_progress", "completed", "blocked", "skipped", "failed", "cancelled"]);
27084
27748
  var FINAL_REVIEW_STEP_ID = "final_review";
27085
27749
  var FINAL_REVIEW_TITLE = "Final review and verification";
27086
27750
  var FINAL_REVIEW_DETAIL = "Review changes, run checks, and confirm the result before finishing.";
@@ -27287,6 +27951,7 @@ function normalizeActiveStep(plan, preferredActive) {
27287
27951
  const inProgress = plan.steps.filter((step) => step.status === "in_progress");
27288
27952
  let active = preferredActive === null ? void 0 : typeof preferredActive === "string" ? preferredActive : void 0;
27289
27953
  if (active && !plan.steps.some((step) => step.id === active)) active = void 0;
27954
+ if (active && isTerminal(plan.steps.find((step) => step.id === active)?.status ?? "pending")) active = void 0;
27290
27955
  if (active) {
27291
27956
  for (const step of plan.steps) {
27292
27957
  if (step.id === active) {
@@ -27349,7 +28014,7 @@ function isStatus(value) {
27349
28014
  return typeof value === "string" && statuses.has(value);
27350
28015
  }
27351
28016
  function isTerminal(status) {
27352
- return status === "completed" || status === "blocked" || status === "skipped" || status === "failed";
28017
+ return status === "completed" || status === "blocked" || status === "skipped" || status === "failed" || status === "cancelled";
27353
28018
  }
27354
28019
  function parseLevel(value) {
27355
28020
  if (value === "info" || value === "success" || value === "warning" || value === "error") return value;
@@ -27376,16 +28041,16 @@ function fail(content, metadata) {
27376
28041
 
27377
28042
  // src/tools/output.ts
27378
28043
  import { mkdir as mkdir10, writeFile as writeFile10 } from "node:fs/promises";
27379
- import path24 from "node:path";
28044
+ import path25 from "node:path";
27380
28045
  var DEFAULT_CAP_BYTES = 32 * 1024;
27381
28046
  var HALF_PREVIEW_BYTES = 16 * 1024;
27382
28047
  async function capToolOutput(result, options) {
27383
28048
  const capBytes = options.capBytes ?? DEFAULT_CAP_BYTES;
27384
28049
  const bytes = Buffer.byteLength(result.content, "utf8");
27385
28050
  if (bytes <= capBytes) return result;
27386
- const dir = path24.join(options.cwd, ".demian", "tmp");
28051
+ const dir = path25.join(options.cwd, ".demian", "tmp");
27387
28052
  await mkdir10(dir, { recursive: true });
27388
- const outputPath = path24.join(dir, `output-${safeCallId(options.callId)}.txt`);
28053
+ const outputPath = path25.join(dir, `output-${safeCallId(options.callId)}.txt`);
27389
28054
  await writeFile10(outputPath, result.content, "utf8");
27390
28055
  return {
27391
28056
  ...result,
@@ -27393,7 +28058,7 @@ async function capToolOutput(result, options) {
27393
28058
  sliceUtf8(result.content, 0, HALF_PREVIEW_BYTES),
27394
28059
  `
27395
28060
 
27396
- [Full output saved to ${path24.relative(options.cwd, outputPath)}; showing head and tail because output exceeded ${capBytes} bytes]
28061
+ [Full output saved to ${path25.relative(options.cwd, outputPath)}; showing head and tail because output exceeded ${capBytes} bytes]
27397
28062
 
27398
28063
  `,
27399
28064
  sliceUtf8(result.content, Math.max(0, bytes - HALF_PREVIEW_BYTES), bytes)
@@ -27418,8 +28083,10 @@ function sliceUtf8(text, startByte, endByte) {
27418
28083
  }
27419
28084
 
27420
28085
  // src/tools/read-file.ts
28086
+ import { createReadStream as createReadStream2 } from "node:fs";
27421
28087
  import { open as open2, stat as stat7 } from "node:fs/promises";
27422
- import path25 from "node:path";
28088
+ import { createInterface } from "node:readline";
28089
+ import path26 from "node:path";
27423
28090
 
27424
28091
  // src/tools/validation.ts
27425
28092
  function assertObject(input2, toolName) {
@@ -27428,7 +28095,7 @@ function assertObject(input2, toolName) {
27428
28095
  }
27429
28096
  return input2;
27430
28097
  }
27431
- function stringField(input2, key, options = {}) {
28098
+ function stringField2(input2, key, options = {}) {
27432
28099
  const value = input2[key];
27433
28100
  if (typeof value !== "string" || !options.allowEmpty && value.length === 0) {
27434
28101
  throw new Error(`${key} must be ${options.allowEmpty ? "a string" : "a non-empty string"}`);
@@ -27468,12 +28135,19 @@ function positiveInteger(value, fallback, key) {
27468
28135
  }
27469
28136
 
27470
28137
  // src/tools/read-file.ts
27471
- var MAX_BYTES = 1024 * 1024;
27472
28138
  var SAMPLE_BYTES = 4096;
27473
28139
  var DEFAULT_LIMIT = 2e3;
28140
+ var MAX_LIMIT = 2e3;
27474
28141
  var readFileTool = {
27475
28142
  name: "read_file",
27476
- description: "Read a text file inside the workspace. Returns line-numbered output.",
28143
+ description: "Read a text file inside the workspace. Streams large files and returns line-numbered output for the requested range.",
28144
+ metadata: {
28145
+ category: "workspace.read",
28146
+ categoryPath: ["workspace", "read"],
28147
+ sideEffect: "none",
28148
+ defaultDecision: "allow",
28149
+ trust: "builtin"
28150
+ },
27477
28151
  inputSchema: {
27478
28152
  type: "object",
27479
28153
  additionalProperties: false,
@@ -27481,32 +28155,30 @@ var readFileTool = {
27481
28155
  properties: {
27482
28156
  path: { type: "string", description: "Path to read, relative to cwd or absolute inside cwd." },
27483
28157
  offset: { type: "number", description: "1-indexed line offset. Defaults to 1." },
27484
- limit: { type: "number", description: "Maximum number of lines. Defaults to 2000." }
28158
+ limit: { type: "number", description: "Maximum number of lines. Defaults to 2000 and is capped at 2000." }
27485
28159
  }
27486
28160
  },
27487
28161
  async execute(input2, ctx) {
27488
28162
  const object2 = assertObject(input2, "read_file");
27489
- const filePath = resolveInsideCwd(ctx.cwd, stringField(object2, "path"));
28163
+ const filePath = resolveInsideCwd(ctx.cwd, stringField2(object2, "path"));
27490
28164
  const offset = positiveInteger(optionalNumberField(object2, "offset"), 1, "offset");
27491
- const limit = positiveInteger(optionalNumberField(object2, "limit"), DEFAULT_LIMIT, "limit");
28165
+ const requestedLimit = positiveInteger(optionalNumberField(object2, "limit"), DEFAULT_LIMIT, "limit");
28166
+ const limit = Math.min(requestedLimit, MAX_LIMIT);
27492
28167
  const info = await stat7(filePath);
27493
28168
  if (info.isDirectory()) throw new Error(`read_file expected a file but got a directory: ${relativeToCwd(ctx.cwd, filePath)}`);
27494
- if (info.size > MAX_BYTES) throw new Error(`File is larger than ${MAX_BYTES} bytes: ${relativeToCwd(ctx.cwd, filePath)}`);
27495
28169
  const sample = await readBytes(filePath, Math.min(SAMPLE_BYTES, info.size));
27496
28170
  if (looksBinary(sample)) throw new Error(`Refusing to read binary file: ${relativeToCwd(ctx.cwd, filePath)}`);
27497
- const text = await readText(filePath);
27498
- const lines = text.split(/\r?\n/);
27499
- const start = offset - 1;
27500
- const sliced = lines.slice(start, start + limit);
27501
- const width = String(Math.min(lines.length, start + sliced.length)).length;
27502
- const content = sliced.map((line, index) => `${String(start + index + 1).padStart(width, " ")} | ${line}`).join("\n");
27503
- const truncated = start + sliced.length < lines.length;
27504
- return ok(content + (truncated ? `
27505
- ... (${lines.length - start - sliced.length} more lines)` : ""), {
27506
- path: path25.relative(ctx.cwd, filePath),
27507
- lines: sliced.length,
27508
- totalLines: lines.length,
27509
- truncated
28171
+ const slice = await readLineSlice(filePath, offset, limit, ctx.signal);
28172
+ return ok(formatLineSlice(slice), {
28173
+ path: path26.relative(ctx.cwd, filePath),
28174
+ offset,
28175
+ limit,
28176
+ requestedLimit,
28177
+ lines: slice.lines.length,
28178
+ totalLines: slice.totalLines,
28179
+ truncated: slice.truncated,
28180
+ nextOffset: slice.nextOffset,
28181
+ fileSizeBytes: info.size
27510
28182
  });
27511
28183
  }
27512
28184
  };
@@ -27520,15 +28192,50 @@ async function readBytes(filePath, length) {
27520
28192
  await handle.close();
27521
28193
  }
27522
28194
  }
27523
- async function readText(filePath) {
27524
- const handle = await open2(filePath, "r");
28195
+ async function readLineSlice(filePath, offset, limit, signal) {
28196
+ if (signal.aborted) throw new Error("read_file was aborted");
28197
+ const stream = createReadStream2(filePath, { encoding: "utf8" });
28198
+ const reader = createInterface({ input: stream, crlfDelay: Infinity });
28199
+ const abort = () => stream.destroy(new Error("read_file was aborted"));
28200
+ signal.addEventListener("abort", abort, { once: true });
28201
+ const lines = [];
28202
+ let lineNumber = 0;
28203
+ let truncated = false;
27525
28204
  try {
27526
- const chunks = [];
27527
- for await (const chunk of handle.createReadStream({ encoding: null })) chunks.push(Buffer.from(chunk));
27528
- return Buffer.concat(chunks).toString("utf8");
28205
+ for await (const line of reader) {
28206
+ lineNumber++;
28207
+ if (lineNumber < offset) continue;
28208
+ if (lines.length < limit) {
28209
+ lines.push(line);
28210
+ continue;
28211
+ }
28212
+ truncated = true;
28213
+ break;
28214
+ }
27529
28215
  } finally {
27530
- await handle.close();
28216
+ signal.removeEventListener("abort", abort);
28217
+ reader.close();
28218
+ stream.destroy();
27531
28219
  }
28220
+ return {
28221
+ lines,
28222
+ offset,
28223
+ totalLines: truncated ? void 0 : lineNumber,
28224
+ truncated,
28225
+ nextOffset: truncated ? offset + lines.length : void 0
28226
+ };
28227
+ }
28228
+ function formatLineSlice(slice) {
28229
+ if (slice.lines.length === 0) {
28230
+ if (slice.totalLines !== void 0) return `... (no lines at offset ${slice.offset}; file has ${slice.totalLines} lines)`;
28231
+ return `... (no lines at offset ${slice.offset})`;
28232
+ }
28233
+ const lastLine = slice.offset + slice.lines.length - 1;
28234
+ const width = String(lastLine).length;
28235
+ const content = slice.lines.map((line, index) => `${String(slice.offset + index).padStart(width, " ")} | ${line}`).join("\n");
28236
+ if (!slice.truncated || slice.nextOffset === void 0) return content;
28237
+ return `${content}
28238
+ ... (more lines available; continue with offset ${slice.nextOffset})`;
27532
28239
  }
27533
28240
  function looksBinary(buffer) {
27534
28241
  if (buffer.length === 0) return false;
@@ -27542,10 +28249,17 @@ function looksBinary(buffer) {
27542
28249
 
27543
28250
  // src/tools/write-file.ts
27544
28251
  import { mkdir as mkdir11, readFile as readFile13, writeFile as writeFile11 } from "node:fs/promises";
27545
- import path26 from "node:path";
28252
+ import path27 from "node:path";
27546
28253
  var writeFileTool = {
27547
28254
  name: "write_file",
27548
28255
  description: "Create or replace a text file inside the workspace.",
28256
+ metadata: {
28257
+ category: "workspace.write",
28258
+ categoryPath: ["workspace", "write"],
28259
+ sideEffect: "workspace",
28260
+ defaultDecision: "ask",
28261
+ trust: "builtin"
28262
+ },
27549
28263
  inputSchema: {
27550
28264
  type: "object",
27551
28265
  additionalProperties: false,
@@ -27557,10 +28271,10 @@ var writeFileTool = {
27557
28271
  },
27558
28272
  async execute(input2, ctx) {
27559
28273
  const object2 = assertObject(input2, "write_file");
27560
- const filePath = resolveInsideCwd(ctx.cwd, stringField(object2, "path"));
27561
- const content = stringField(object2, "content", { allowEmpty: true });
28274
+ const filePath = resolveInsideCwd(ctx.cwd, stringField2(object2, "path"));
28275
+ const content = stringField2(object2, "content", { allowEmpty: true });
27562
28276
  const before = await readOptionalTextFile(filePath);
27563
- await mkdir11(path26.dirname(filePath), { recursive: true });
28277
+ await mkdir11(path27.dirname(filePath), { recursive: true });
27564
28278
  await writeFile11(filePath, content, "utf8");
27565
28279
  const relative = relativeToCwd(ctx.cwd, filePath);
27566
28280
  const diff = createTextDiff(before, content, relative);
@@ -27589,6 +28303,13 @@ import { readFile as readFile14, writeFile as writeFile12 } from "node:fs/promis
27589
28303
  var editFileTool = {
27590
28304
  name: "edit_file",
27591
28305
  description: "Replace an exact string in a text file inside the workspace.",
28306
+ metadata: {
28307
+ category: "workspace.write",
28308
+ categoryPath: ["workspace", "write"],
28309
+ sideEffect: "workspace",
28310
+ defaultDecision: "ask",
28311
+ trust: "builtin"
28312
+ },
27592
28313
  inputSchema: {
27593
28314
  type: "object",
27594
28315
  additionalProperties: false,
@@ -27602,9 +28323,9 @@ var editFileTool = {
27602
28323
  },
27603
28324
  async execute(input2, ctx) {
27604
28325
  const object2 = assertObject(input2, "edit_file");
27605
- const filePath = resolveInsideCwd(ctx.cwd, stringField(object2, "path"));
27606
- const oldString = stringField(object2, "oldString");
27607
- const newString = stringField(object2, "newString", { allowEmpty: true });
28326
+ const filePath = resolveInsideCwd(ctx.cwd, stringField2(object2, "path"));
28327
+ const oldString = stringField2(object2, "oldString");
28328
+ const newString = stringField2(object2, "newString", { allowEmpty: true });
27608
28329
  const replaceAll = optionalBooleanField(object2, "replaceAll") ?? false;
27609
28330
  if (oldString === newString) throw new Error("oldString and newString must be different");
27610
28331
  const before = await readFile14(filePath, "utf8");
@@ -27636,7 +28357,7 @@ function countMatches(text, needle) {
27636
28357
 
27637
28358
  // src/tools/bash.ts
27638
28359
  import { spawn as spawn6 } from "node:child_process";
27639
- import path28 from "node:path";
28360
+ import path29 from "node:path";
27640
28361
 
27641
28362
  // src/sandbox/env-only.ts
27642
28363
  function buildEnvOnlyLaunch(command, config) {
@@ -27696,7 +28417,7 @@ function bwrapPath() {
27696
28417
 
27697
28418
  // src/sandbox/macos.ts
27698
28419
  import { existsSync as existsSync4 } from "node:fs";
27699
- import path27 from "node:path";
28420
+ import path28 from "node:path";
27700
28421
  function canUseMacOSSandbox() {
27701
28422
  return process.platform === "darwin" && existsSync4("/usr/bin/sandbox-exec");
27702
28423
  }
@@ -27722,7 +28443,7 @@ function macosProfile(cwd, config) {
27722
28443
  }
27723
28444
  if (mode === "workspace-write") {
27724
28445
  lines.push("(deny file-write*)");
27725
- lines.push(`(allow file-write* (subpath "${escapeProfilePath(path27.resolve(cwd))}"))`);
28446
+ lines.push(`(allow file-write* (subpath "${escapeProfilePath(path28.resolve(cwd))}"))`);
27726
28447
  lines.push('(allow file-write* (subpath "/tmp"))');
27727
28448
  lines.push('(allow file-write* (subpath "/private/tmp"))');
27728
28449
  lines.push('(allow file-write* (subpath "/private/var/folders"))');
@@ -27752,6 +28473,13 @@ var MAX_TIMEOUT_MS = 6e5;
27752
28473
  var bashTool = {
27753
28474
  name: "bash",
27754
28475
  description: "Run a shell command inside the workspace.",
28476
+ metadata: {
28477
+ category: "process.shell",
28478
+ categoryPath: ["process", "shell"],
28479
+ sideEffect: "process",
28480
+ defaultDecision: "ask",
28481
+ trust: "builtin"
28482
+ },
27755
28483
  inputSchema: {
27756
28484
  type: "object",
27757
28485
  additionalProperties: false,
@@ -27764,7 +28492,7 @@ var bashTool = {
27764
28492
  },
27765
28493
  async execute(input2, ctx) {
27766
28494
  const object2 = assertObject(input2, "bash");
27767
- const command = stringField(object2, "command");
28495
+ const command = stringField2(object2, "command");
27768
28496
  const workdirInput = optionalStringField(object2, "workdir");
27769
28497
  const workdir = workdirInput ? resolveInsideCwd(ctx.cwd, workdirInput) : ctx.cwd;
27770
28498
  const requestedTimeout = optionalNumberField(object2, "timeoutMs");
@@ -27784,7 +28512,7 @@ ${result.stderr}` : ""
27784
28512
  ].filter(Boolean).join("\n");
27785
28513
  return ok(content, {
27786
28514
  command,
27787
- workdir: path28.relative(ctx.cwd, workdir) || ".",
28515
+ workdir: path29.relative(ctx.cwd, workdir) || ".",
27788
28516
  exitCode: result.exitCode,
27789
28517
  signal: result.signal,
27790
28518
  timedOut: result.timedOut,
@@ -27837,11 +28565,18 @@ function runCommand(command, cwd, timeoutMs, signal, sandbox = { mode: "workspac
27837
28565
  // src/tools/grep.ts
27838
28566
  import { spawn as spawn7 } from "node:child_process";
27839
28567
  import { readdir as readdir2, readFile as readFile15, stat as stat8 } from "node:fs/promises";
27840
- import path29 from "node:path";
28568
+ import path30 from "node:path";
27841
28569
  var MAX_MATCHES = 200;
27842
28570
  var grepTool = {
27843
28571
  name: "grep",
27844
28572
  description: "Search text files inside the workspace. Uses rg when available.",
28573
+ metadata: {
28574
+ category: "workspace.search",
28575
+ categoryPath: ["workspace", "search"],
28576
+ sideEffect: "none",
28577
+ defaultDecision: "allow",
28578
+ trust: "builtin"
28579
+ },
27845
28580
  inputSchema: {
27846
28581
  type: "object",
27847
28582
  additionalProperties: false,
@@ -27854,7 +28589,7 @@ var grepTool = {
27854
28589
  },
27855
28590
  async execute(input2, ctx) {
27856
28591
  const object2 = assertObject(input2, "grep");
27857
- const pattern = stringField(object2, "pattern");
28592
+ const pattern = stringField2(object2, "pattern");
27858
28593
  const baseInput = optionalStringField(object2, "path");
27859
28594
  const glob = optionalStringField(object2, "glob");
27860
28595
  const base = baseInput ? resolveInsideCwd(ctx.cwd, baseInput) : ctx.cwd;
@@ -27912,7 +28647,7 @@ function runRg(cwd, base, pattern, glob, signal) {
27912
28647
  }
27913
28648
  function rewriteRgPath(cwd, line) {
27914
28649
  const [file, rest] = splitFirst(line, ":");
27915
- if (!path29.isAbsolute(file)) return line;
28650
+ if (!path30.isAbsolute(file)) return line;
27916
28651
  return `${relativeToCwd(cwd, file)}:${rest}`;
27917
28652
  }
27918
28653
  function splitFirst(value, delimiter) {
@@ -27929,7 +28664,7 @@ async function fallbackSearch(cwd, base, pattern, glob) {
27929
28664
  if (isIgnoredPath(relative)) return;
27930
28665
  const info = await stat8(item);
27931
28666
  if (info.isDirectory()) {
27932
- for (const entry of await readdir2(item)) await walk2(path29.join(item, entry));
28667
+ for (const entry of await readdir2(item)) await walk2(path30.join(item, entry));
27933
28668
  return;
27934
28669
  }
27935
28670
  if (glob && !matchGlob(glob, relative)) return;
@@ -27948,11 +28683,18 @@ async function fallbackSearch(cwd, base, pattern, glob) {
27948
28683
 
27949
28684
  // src/tools/glob.ts
27950
28685
  import { readdir as readdir3, stat as stat9 } from "node:fs/promises";
27951
- import path30 from "node:path";
28686
+ import path31 from "node:path";
27952
28687
  var MAX_PATHS = 1e3;
27953
28688
  var globTool = {
27954
28689
  name: "glob",
27955
28690
  description: "Find paths inside the workspace using a glob pattern.",
28691
+ metadata: {
28692
+ category: "workspace.search",
28693
+ categoryPath: ["workspace", "search"],
28694
+ sideEffect: "none",
28695
+ defaultDecision: "allow",
28696
+ trust: "builtin"
28697
+ },
27956
28698
  inputSchema: {
27957
28699
  type: "object",
27958
28700
  additionalProperties: false,
@@ -27964,7 +28706,7 @@ var globTool = {
27964
28706
  },
27965
28707
  async execute(input2, ctx) {
27966
28708
  const object2 = assertObject(input2, "glob");
27967
- const pattern = stringField(object2, "pattern");
28709
+ const pattern = stringField2(object2, "pattern");
27968
28710
  const base = optionalStringField(object2, "path");
27969
28711
  const root = base ? resolveInsideCwd(ctx.cwd, base) : ctx.cwd;
27970
28712
  const found = await collectPaths(ctx.cwd, root, pattern);
@@ -27980,7 +28722,7 @@ async function collectPaths(cwd, root, pattern) {
27980
28722
  async function walk2(dir) {
27981
28723
  const entries = await readdir3(dir, { withFileTypes: true });
27982
28724
  for (const entry of entries) {
27983
- const absolute = path30.join(dir, entry.name);
28725
+ const absolute = path31.join(dir, entry.name);
27984
28726
  const relative = relativeToCwd(cwd, absolute);
27985
28727
  if (isIgnoredPath(relative)) continue;
27986
28728
  const info = await stat9(absolute);
@@ -27997,6 +28739,8 @@ var updatePlanTool = {
27997
28739
  name: "update_plan",
27998
28740
  description: "Create or update the visible work plan for the current user goal. Write title, summary, step titles, and details in the user's language; for Korean requests, use Korean user-facing text. Keep step ids, code identifiers, paths, commands, and tool names unchanged. Non-empty plans always end with a final review and verification step.",
27999
28741
  metadata: {
28742
+ category: "planning.progress",
28743
+ categoryPath: ["planning", "progress"],
28000
28744
  sideEffect: "none",
28001
28745
  defaultDecision: "allow",
28002
28746
  trust: "builtin"
@@ -28036,7 +28780,7 @@ function stepProperties() {
28036
28780
  return {
28037
28781
  id: { type: "string", maxLength: 64 },
28038
28782
  title: { type: "string" },
28039
- status: { type: "string", enum: ["pending", "in_progress", "completed", "blocked", "skipped", "failed"] },
28783
+ status: { type: "string", enum: ["pending", "in_progress", "completed", "blocked", "skipped", "failed", "cancelled"] },
28040
28784
  detail: { type: "string" },
28041
28785
  agent: { type: "string" }
28042
28786
  };
@@ -28045,8 +28789,10 @@ function stepProperties() {
28045
28789
  // src/tools/report-progress.ts
28046
28790
  var reportProgressTool = {
28047
28791
  name: "report_progress",
28048
- description: "Report a short user-facing progress update for the current work plan. Write the message in the user's language; for Korean requests, use Korean while keeping code identifiers, paths, commands, and tool names unchanged.",
28792
+ description: "Report a conversational user-facing progress update for the current work plan. Use it before starting non-trivial tool-heavy work, at meaningful phase changes, and after important evidence so the user understands what is happening. Write 1-3 natural sentences in the user's language that explain what you are doing, what you learned or why it matters, and what you will do next. Do not list raw tool names, command output, file previews, or status fields; those details are already available in the execution accordion. Mention specific code identifiers, paths, commands, or tool names only when they are essential to the user's understanding. For Korean requests, use Korean \uC874\uB313\uB9D0 while keeping necessary identifiers unchanged. Set showInTimeline=true for major phase changes or any longer tool-using work.",
28049
28793
  metadata: {
28794
+ category: "planning.progress",
28795
+ categoryPath: ["planning", "progress"],
28050
28796
  sideEffect: "none",
28051
28797
  defaultDecision: "allow",
28052
28798
  trust: "builtin"
@@ -28077,6 +28823,13 @@ function createWebSearchTool(config = defaultConfig.webSearch, fetcher = fetch)
28077
28823
  return {
28078
28824
  name: "web_search",
28079
28825
  description: "Search the web using the configured Brave, Tavily, or Exa web search provider.",
28826
+ metadata: {
28827
+ category: "research.web",
28828
+ categoryPath: ["research", "web"],
28829
+ sideEffect: "network",
28830
+ defaultDecision: "ask",
28831
+ trust: "builtin"
28832
+ },
28080
28833
  inputSchema: {
28081
28834
  type: "object",
28082
28835
  additionalProperties: false,
@@ -28096,7 +28849,7 @@ function createWebSearchTool(config = defaultConfig.webSearch, fetcher = fetch)
28096
28849
  },
28097
28850
  async execute(input2, ctx) {
28098
28851
  const object2 = assertObject(input2, "web_search");
28099
- const query = stringField(object2, "query");
28852
+ const query = stringField2(object2, "query");
28100
28853
  const provider = providerField(object2, config.defaultProvider);
28101
28854
  const maxResults = optionalNumberField(object2, "maxResults");
28102
28855
  if (provider === "brave") return runBraveSearch(config.providers.brave, { query, maxResults, fetcher, signal: ctx.signal });
@@ -28307,6 +29060,8 @@ var getGoalTool = {
28307
29060
  return ctx.goals.getGoal();
28308
29061
  },
28309
29062
  metadata: {
29063
+ category: "goal.state",
29064
+ categoryPath: ["goal", "state"],
28310
29065
  sideEffect: "none",
28311
29066
  defaultDecision: "allow",
28312
29067
  trust: "builtin"
@@ -28328,6 +29083,8 @@ var createGoalTool = {
28328
29083
  return ctx.goals.createGoal(input2);
28329
29084
  },
28330
29085
  metadata: {
29086
+ category: "goal.state",
29087
+ categoryPath: ["goal", "state"],
28331
29088
  sideEffect: "workspace",
28332
29089
  defaultDecision: "allow",
28333
29090
  trust: "builtin"
@@ -28349,6 +29106,8 @@ var updateGoalTool = {
28349
29106
  return ctx.goals.updateGoal(input2);
28350
29107
  },
28351
29108
  metadata: {
29109
+ category: "goal.state",
29110
+ categoryPath: ["goal", "state"],
28352
29111
  sideEffect: "workspace",
28353
29112
  defaultDecision: "allow",
28354
29113
  trust: "builtin"
@@ -28443,6 +29202,9 @@ function validateToolName(name) {
28443
29202
  }
28444
29203
 
28445
29204
  // src/session.ts
29205
+ function relaxedToolUseRoundLimit(maxTurns) {
29206
+ return Math.max((maxTurns ?? 25) * 20, 200);
29207
+ }
28446
29208
  var SessionRunner = class {
28447
29209
  sessionId;
28448
29210
  events;
@@ -28500,9 +29262,28 @@ var SessionRunner = class {
28500
29262
  });
28501
29263
  const messages = [{ role: "system", content: buildSystemPrompt(options.agent, envInfo) }, ...options.history ?? [], { role: "user", content: userContent }];
28502
29264
  this.emit({ type: "user.message", sessionId: this.sessionId, text: options.displayPrompt ?? options.prompt, ts: now(), ...this.eventContext() });
29265
+ this.reportSessionKickoff();
28503
29266
  await this.#hooks.dispatch("UserPromptSubmit", this.hookBase());
28504
29267
  const maxTurns = options.maxTurns ?? 25;
28505
- for (let turn = 0; turn < maxTurns; turn++) {
29268
+ const countToolUseTurns = options.countToolUseTurns === true;
29269
+ const maxToolUseRounds = options.maxToolUseRounds ?? relaxedToolUseRoundLimit(maxTurns);
29270
+ let countedTurns = 0;
29271
+ let toolUseRounds = 0;
29272
+ let countedTaskScope = this.#progress.activeStepId() ?? "__session__";
29273
+ const refreshTaskScope = () => {
29274
+ const taskScope = this.#progress.activeStepId() ?? "__session__";
29275
+ if (taskScope === countedTaskScope) return;
29276
+ countedTaskScope = taskScope;
29277
+ countedTurns = 0;
29278
+ };
29279
+ for (; ; ) {
29280
+ if (countToolUseTurns) refreshTaskScope();
29281
+ if (countToolUseTurns && countedTurns >= maxTurns) {
29282
+ return this.end(messages, `Stopped after reaching maxTurns (${maxTurns}).`, "max_turns");
29283
+ }
29284
+ if (!countToolUseTurns && toolUseRounds >= maxToolUseRounds) {
29285
+ return this.end(messages, `Stopped after reaching tool-use safety limit (${maxToolUseRounds}) before producing a final answer.`, "tool_loop_limit");
29286
+ }
28506
29287
  await this.#hooks.dispatch("BeforeModelRequest", this.hookBase());
28507
29288
  const tools = this.#tools.list();
28508
29289
  const compiled = compileMessagesForContext(messages, tools, options.context);
@@ -28580,12 +29361,17 @@ var SessionRunner = class {
28580
29361
  if (toolCalls.length === 0) {
28581
29362
  return this.end(messages, assistant.content ?? "", "end_turn");
28582
29363
  }
29364
+ if (countToolUseTurns) {
29365
+ refreshTaskScope();
29366
+ countedTurns++;
29367
+ } else {
29368
+ toolUseRounds++;
29369
+ }
28583
29370
  for (const call of toolCalls) {
28584
29371
  const toolMessage = await this.runToolCall(call);
28585
29372
  messages.push(toolMessage);
28586
29373
  }
28587
29374
  }
28588
- return this.end(messages, `Stopped after reaching maxTurns (${maxTurns}).`, "max_turns");
28589
29375
  }
28590
29376
  async applyAfterModelHooks(message) {
28591
29377
  const results = await this.#hooks.dispatch("AfterModelResponse", {
@@ -28648,15 +29434,7 @@ var SessionRunner = class {
28648
29434
  return toToolMessage(call, result);
28649
29435
  }
28650
29436
  if (evaluation.decision === "ask") {
28651
- this.emit({ type: "permission.requested", sessionId: this.sessionId, callId: call.id, tool: call.name, decision: "ask", targetType: "tool", targetName: call.name, agent: this.#options.agent.name, ts: now(), ...this.eventContext() });
28652
- this.emitGoalToolEvent(call, "permission");
28653
- await this.#hooks.dispatch("PermissionRequest", {
28654
- ...this.hookBase(),
28655
- callId: call.id,
28656
- toolName: call.name,
28657
- toolInput: input2
28658
- });
28659
- const answer = await (this.#options.permissionPrompt ?? ((req) => askPermission(req, { yes: this.#options.yes })))({
29437
+ const permissionRequest = {
28660
29438
  tool: call.name,
28661
29439
  input: input2,
28662
29440
  actor: this.#options.agent.name,
@@ -28664,16 +29442,33 @@ var SessionRunner = class {
28664
29442
  targetName: call.name,
28665
29443
  proposedDecision: "ask",
28666
29444
  reason: evaluation.reason
28667
- });
28668
- if (answer.decision !== "allow") {
28669
- this.emit({ type: "permission.denied", sessionId: this.sessionId, callId: call.id, tool: call.name, reason: answer.reason, targetType: "tool", targetName: call.name, agent: this.#options.agent.name, ts: now(), ...this.eventContext() });
29445
+ };
29446
+ const automatic = await this.#options.autoPermission?.(permissionRequest);
29447
+ if (automatic?.decision === "deny") {
29448
+ this.emit({ type: "permission.denied", sessionId: this.sessionId, callId: call.id, tool: call.name, reason: automatic.reason, targetType: "tool", targetName: call.name, agent: this.#options.agent.name, ts: now(), ...this.eventContext() });
28670
29449
  this.emitGoalToolEvent(call, "denied");
28671
- return toToolMessage(call, toolFailure(`Permission denied${answer.reason ? `: ${answer.reason}` : ""}`));
28672
- }
28673
- if (answer.always) {
28674
- this.#grants.add(evaluation.grantKey);
28675
- await this.#persistentStore?.add(this.#options.cwd, evaluation.grantKey);
28676
- this.emit({ type: "permission.granted", sessionId: this.sessionId, callId: call.id, key: evaluation.grantKey, targetType: "tool", targetName: call.name, agent: this.#options.agent.name, ts: now(), ...this.eventContext() });
29450
+ return toToolMessage(call, toolFailure(`Permission denied${automatic.reason ? `: ${automatic.reason}` : ""}`));
29451
+ }
29452
+ if (automatic?.decision !== "allow") {
29453
+ this.emit({ type: "permission.requested", sessionId: this.sessionId, callId: call.id, tool: call.name, decision: "ask", targetType: "tool", targetName: call.name, agent: this.#options.agent.name, ts: now(), ...this.eventContext() });
29454
+ this.emitGoalToolEvent(call, "permission");
29455
+ await this.#hooks.dispatch("PermissionRequest", {
29456
+ ...this.hookBase(),
29457
+ callId: call.id,
29458
+ toolName: call.name,
29459
+ toolInput: input2
29460
+ });
29461
+ const answer = await (this.#options.permissionPrompt ?? ((req) => askPermission(req, { yes: this.#options.yes })))(permissionRequest);
29462
+ if (answer.decision !== "allow") {
29463
+ this.emit({ type: "permission.denied", sessionId: this.sessionId, callId: call.id, tool: call.name, reason: answer.reason, targetType: "tool", targetName: call.name, agent: this.#options.agent.name, ts: now(), ...this.eventContext() });
29464
+ this.emitGoalToolEvent(call, "denied");
29465
+ return toToolMessage(call, toolFailure(`Permission denied${answer.reason ? `: ${answer.reason}` : ""}`));
29466
+ }
29467
+ if (answer.always) {
29468
+ this.#grants.add(evaluation.grantKey);
29469
+ await this.#persistentStore?.add(this.#options.cwd, evaluation.grantKey);
29470
+ this.emit({ type: "permission.granted", sessionId: this.sessionId, callId: call.id, key: evaluation.grantKey, targetType: "tool", targetName: call.name, agent: this.#options.agent.name, ts: now(), ...this.eventContext() });
29471
+ }
28677
29472
  }
28678
29473
  }
28679
29474
  this.emit({ type: "tool.started", sessionId: this.sessionId, callId: call.id, name: call.name, agent: this.#options.agent.name, ts: now(), ...this.eventContext() });
@@ -28688,7 +29483,7 @@ var SessionRunner = class {
28688
29483
  cwd: this.#options.cwd,
28689
29484
  signal: this.#controller.signal,
28690
29485
  emit: (event) => this.emit(event),
28691
- ask: (req) => (this.#options.permissionPrompt ?? ((request) => askPermission(request, { yes: this.#options.yes })))(req),
29486
+ ask: async (req) => await this.#options.autoPermission?.(req) ?? (this.#options.permissionPrompt ?? ((request) => askPermission(request, { yes: this.#options.yes })))(req),
28692
29487
  dryRun: this.#options.dryRun,
28693
29488
  sandbox: this.#options.sandbox,
28694
29489
  rootSessionId: this.#options.rootSessionId,
@@ -28728,15 +29523,18 @@ var SessionRunner = class {
28728
29523
  }
28729
29524
  }
28730
29525
  async end(messages, finalAnswer, reason) {
29526
+ const answer = reason === "end_turn" ? finalAnswer : terminalFinalAnswer(finalAnswer, reason, this.#progress.snapshot());
29527
+ const returnedMessages = reason === "end_turn" ? messages : [...messages, { role: "assistant", content: answer }];
28731
29528
  await this.#hooks.dispatch("Stop", this.hookBase());
28732
29529
  await this.#hooks.dispatch("SessionEnd", this.hookBase());
28733
29530
  this.completeGoalWorkGroup();
28734
29531
  this.emit({ type: "session.ended", sessionId: this.sessionId, reason, ts: now(), ...this.eventContext() });
29532
+ if (reason !== "end_turn" && answer.trim()) this.emit({ type: "model.text", sessionId: this.sessionId, text: answer, ts: now(), ...this.eventContext() });
28735
29533
  await this.#transcript.flush();
28736
29534
  return {
28737
29535
  sessionId: this.sessionId,
28738
- finalAnswer,
28739
- messages,
29536
+ finalAnswer: answer,
29537
+ messages: returnedMessages,
28740
29538
  endReason: reason,
28741
29539
  usage: { ...this.#usage }
28742
29540
  };
@@ -28844,6 +29642,24 @@ var SessionRunner = class {
28844
29642
  activeStepId: () => this.#progress.activeStepId()
28845
29643
  };
28846
29644
  }
29645
+ reportSessionKickoff() {
29646
+ if (this.#options.parentInvocationId) return;
29647
+ const message = sessionKickoffMessage(this.#options.displayPrompt ?? this.#options.prompt);
29648
+ if (!message) return;
29649
+ const result = this.#progress.report(
29650
+ { message, level: "info", showInTimeline: true },
29651
+ {
29652
+ sessionId: this.sessionId,
29653
+ agent: this.#options.agent.name,
29654
+ rootSessionId: this.#options.rootSessionId,
29655
+ agentSessionId: this.#options.agentSessionId,
29656
+ invocationId: this.#options.invocationId,
29657
+ parentInvocationId: this.#options.parentInvocationId,
29658
+ agentPath: this.#options.agentPath
29659
+ }
29660
+ );
29661
+ for (const event of result.events) this.emit(event);
29662
+ }
28847
29663
  eventContext() {
28848
29664
  return {
28849
29665
  rootSessionId: this.#options.rootSessionId,
@@ -28927,6 +29743,134 @@ function normalizeDiffContent(value) {
28927
29743
  after: typeof after === "string" ? after : void 0
28928
29744
  };
28929
29745
  }
29746
+ function sessionKickoffMessage(prompt) {
29747
+ const normalized = prompt.replace(/^\/\w+\s*/, "").trim();
29748
+ if (isCasualPrompt(normalized) || isSimpleQuestionPrompt(normalized)) return void 0;
29749
+ const isKorean = /[가-힣]/.test(normalized);
29750
+ if (isKorean) {
29751
+ if (/(수정|고쳐|개선|변경|추가|삭제|구현|반영|업데이트|만들|작성|전환|마이그레이션)/.test(normalized)) {
29752
+ return "\uBA3C\uC800 \uAD00\uB828 \uB3D9\uC791\uC774 \uC5B4\uB514\uC5D0\uC11C \uB9CC\uB4E4\uC5B4\uC9C0\uB294\uC9C0\uC640 \uC601\uD5A5 \uBC94\uC704\uB97C \uD655\uC778\uD55C \uB4A4, \uD544\uC694\uD55C \uBCC0\uACBD\uC744 \uC791\uC740 \uB2E8\uC704\uB85C \uC9C4\uD589\uD558\uACA0\uC2B5\uB2C8\uB2E4. \uD655\uC778\uD55C \uB0B4\uC6A9\uC774 \uB2E4\uC74C \uC218\uC815\uC5D0 \uC5B4\uB5A4 \uC758\uBBF8\uAC00 \uC788\uB294\uC9C0\uB3C4 \uC911\uAC04\uC911\uAC04 \uC815\uB9AC\uD574 \uB4DC\uB9AC\uACA0\uC2B5\uB2C8\uB2E4.";
29753
+ }
29754
+ if (/(테스트|검증|확인|재현|실행|빌드|publish|배포)/i.test(normalized)) {
29755
+ return "\uBA3C\uC800 \uD604\uC7AC \uC0C1\uD0DC\uC640 \uC7AC\uD604 \uC870\uAC74\uC744 \uD655\uC778\uD558\uACE0, \uACB0\uACFC\uAC00 \uC758\uBBF8\uD558\uB294 \uBC14\uB97C \uC815\uB9AC\uD558\uBA74\uC11C \uD544\uC694\uD55C \uD6C4\uC18D \uC870\uCE58\uB97C \uC774\uC5B4\uAC00\uACA0\uC2B5\uB2C8\uB2E4.";
29756
+ }
29757
+ if (/(설명|분석|검토|원인|왜|찾아|알려|파악)/.test(normalized)) {
29758
+ return "\uBA3C\uC800 \uC694\uCCAD\uC758 \uD575\uC2EC \uC9C8\uBB38\uC744 \uC881\uD788\uACE0, \uADFC\uAC70\uAC00 \uB420 \uB9CC\uD55C \uAD6C\uC870\uC640 \uD750\uB984\uC744 \uD655\uC778\uD55C \uB4A4 \uC774\uD574\uD558\uAE30 \uC26C\uC6B4 \uB2F5\uC73C\uB85C \uC815\uB9AC\uD558\uACA0\uC2B5\uB2C8\uB2E4.";
29759
+ }
29760
+ return "\uBA3C\uC800 \uC694\uCCAD\uC758 \uC758\uB3C4\uB97C \uD655\uC778\uD558\uACE0, \uD544\uC694\uD55C \uB9E5\uB77D\uC744 \uC0B4\uD3B4\uBCF8 \uB4A4 \uB2E4\uC74C\uC5D0 \uD560 \uC77C\uC744 \uC815\uB9AC\uD558\uBA74\uC11C \uC774\uC5B4\uAC00\uACA0\uC2B5\uB2C8\uB2E4.";
29761
+ }
29762
+ if (/\b(fix|change|update|add|delete|remove|implement|write|create|migrate|refactor)\b/i.test(normalized)) {
29763
+ return "I\u2019ll first locate the relevant behavior and check the impact area, then make the change in small, verifiable steps while sharing what each finding means.";
29764
+ }
29765
+ if (/\b(test|verify|check|reproduce|run|build|publish|deploy)\b/i.test(normalized)) {
29766
+ return "I\u2019ll first check the current state and reproduction path, then explain what the result means before moving to the next step.";
29767
+ }
29768
+ if (/\b(explain|analyze|review|why|find|investigate|inspect)\b/i.test(normalized)) {
29769
+ return "I\u2019ll narrow the main question first, inspect the evidence that matters, and then summarize the answer in a way that is easy to act on.";
29770
+ }
29771
+ return "I\u2019ll first clarify the shape of the request, then work through the relevant context and share useful updates as I go.";
29772
+ }
29773
+ function isCasualPrompt(prompt) {
29774
+ const compact = prompt.toLowerCase().replace(/[\s.!?。!?~…"'`]+/g, "");
29775
+ if (!compact) return true;
29776
+ const casual = /* @__PURE__ */ new Set([
29777
+ "hi",
29778
+ "hello",
29779
+ "hey",
29780
+ "yo",
29781
+ "ok",
29782
+ "okay",
29783
+ "thanks",
29784
+ "thankyou",
29785
+ "\uC548\uB155",
29786
+ "\uC548\uB155\uD558\uC138\uC694",
29787
+ "\uD558\uC774",
29788
+ "\uD5EC\uB85C",
29789
+ "\uACE0\uB9C8\uC6CC",
29790
+ "\uACE0\uB9D9\uC2B5\uB2C8\uB2E4",
29791
+ "\uAC10\uC0AC\uD569\uB2C8\uB2E4",
29792
+ "\uAC10\uC0AC",
29793
+ "\uC88B\uC544",
29794
+ "\uB124",
29795
+ "\uC751",
29796
+ "\u3147\u314B"
29797
+ ]);
29798
+ return casual.has(compact) || compact.length <= 12 && /^(hi|hello|hey|안녕|안녕하세요|하이)/.test(compact);
29799
+ }
29800
+ function isSimpleQuestionPrompt(prompt) {
29801
+ const trimmed = prompt.trim();
29802
+ if (trimmed.length > 80) return false;
29803
+ const hasTaskVerb = /(수정|고쳐|개선|변경|추가|삭제|구현|반영|업데이트|만들|작성|전환|마이그레이션|테스트|검증|확인|재현|실행|빌드|분석|검토|찾아|파악|설명해|알려줘|fix|change|update|add|delete|remove|implement|write|create|migrate|refactor|test|verify|check|reproduce|run|build|analyze|review|find|inspect|explain)/i.test(
29804
+ trimmed
29805
+ );
29806
+ if (hasTaskVerb) return false;
29807
+ return /[??]$/.test(trimmed) || /(뭐야|무엇|누구|언제|어디|어떻게|왜|맞아)$/.test(trimmed);
29808
+ }
29809
+ function terminalFinalAnswer(finalAnswer, reason, progress) {
29810
+ const lines = ["\uC791\uC5C5\uC774 \uC885\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", "", `\uC885\uB8CC \uC774\uC720: ${terminalReasonText(reason)}`];
29811
+ const runtimeMessage = finalAnswer.trim();
29812
+ if (runtimeMessage && runtimeMessage !== terminalReasonText(reason)) lines.push(`\uB9C8\uC9C0\uB9C9 \uB7F0\uD0C0\uC784 \uBA54\uC2DC\uC9C0: ${runtimeMessage}`);
29813
+ lines.push("", "\uD604\uC7AC\uAE4C\uC9C0 \uC9C4\uD589\uD55C \uB0B4\uC6A9\uC740 \uC544\uB798\uC640 \uAC19\uC2B5\uB2C8\uB2E4.");
29814
+ const plan = progress.plan;
29815
+ if (plan) {
29816
+ lines.push("", ...planSummaryLines(plan));
29817
+ } else {
29818
+ lines.push("", "- \uC544\uC9C1 \uBA85\uC2DC\uC801\uC778 plan\uC740 \uB9CC\uB4E4\uC5B4\uC9C0\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
29819
+ }
29820
+ const notes = progress.notes.slice(-3);
29821
+ if (notes.length > 0) {
29822
+ lines.push("", "\uCD5C\uADFC \uC911\uAC04 \uBCF4\uACE0:");
29823
+ for (const note of notes) lines.push(`- ${note.message}`);
29824
+ }
29825
+ lines.push("", terminalNextStepText(reason));
29826
+ return lines.join("\n");
29827
+ }
29828
+ function planSummaryLines(plan) {
29829
+ const lines = [`Plan: ${plan.title ?? "\uC791\uC5C5 \uACC4\uD68D"}`];
29830
+ if (plan.summary) lines.push(`\uC694\uC57D: ${plan.summary}`);
29831
+ const counts = countSteps(plan.steps);
29832
+ lines.push(
29833
+ `\uC9C4\uD589 \uC0C1\uD0DC: \uC644\uB8CC ${counts.completed + counts.skipped}\uAC1C, \uC9C4\uD589 \uC911 ${counts.in_progress}\uAC1C, \uB300\uAE30 ${counts.pending}\uAC1C, \uC2E4\uD328/\uCC28\uB2E8 ${counts.failed + counts.blocked + counts.cancelled}\uAC1C\uC785\uB2C8\uB2E4.`
29834
+ );
29835
+ const completed = plan.steps.filter((step) => step.status === "completed" || step.status === "skipped").slice(-3);
29836
+ if (completed.length > 0) lines.push(`\uC644\uB8CC\uD55C \uC791\uC5C5: ${completed.map(formatStep).join("; ")}`);
29837
+ const active = plan.steps.filter((step) => step.status === "in_progress");
29838
+ if (active.length > 0) lines.push(`\uC9C4\uD589 \uC911\uC774\uB358 \uC791\uC5C5: ${active.map(formatStep).join("; ")}`);
29839
+ const unresolved = plan.steps.filter((step) => step.status === "pending" || step.status === "failed" || step.status === "blocked" || step.status === "cancelled").slice(0, 4);
29840
+ if (unresolved.length > 0) lines.push(`\uB0A8\uC740 \uC791\uC5C5: ${unresolved.map(formatStep).join("; ")}`);
29841
+ return lines;
29842
+ }
29843
+ function countSteps(steps) {
29844
+ const counts = {
29845
+ pending: 0,
29846
+ in_progress: 0,
29847
+ completed: 0,
29848
+ blocked: 0,
29849
+ skipped: 0,
29850
+ failed: 0,
29851
+ cancelled: 0
29852
+ };
29853
+ for (const step of steps) counts[step.status] += 1;
29854
+ return counts;
29855
+ }
29856
+ function formatStep(step) {
29857
+ const detail = step.detail ? ` (${preview(step.detail.replace(/\s+/g, " "), 120)})` : "";
29858
+ return `${step.title}${detail}`;
29859
+ }
29860
+ function terminalReasonText(reason) {
29861
+ if (reason === "max_turns") return "\uC0C1\uC704 task turn \uD55C\uB3C4\uC5D0 \uB3C4\uB2EC\uD588\uC2B5\uB2C8\uB2E4.";
29862
+ if (reason === "tool_loop_limit") return "\uB3C4\uAD6C \uC0AC\uC6A9 \uB77C\uC6B4\uB4DC\uAC00 \uB0B4\uBD80 \uC548\uC804 \uD55C\uB3C4\uC5D0 \uB3C4\uB2EC\uD588\uC2B5\uB2C8\uB2E4.";
29863
+ if (reason === "content_filter") return "\uC81C\uACF5\uC790\uC758 \uC548\uC804 \uD544\uD130\uB85C \uC751\uB2F5\uC774 \uCC28\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4.";
29864
+ if (reason === "cancelled") return "\uC0AC\uC6A9\uC790 \uC694\uCCAD \uB610\uB294 \uB7F0\uD0C0\uC784 \uC911\uB2E8\uC73C\uB85C \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.";
29865
+ if (reason === "error") return "\uB7F0\uD0C0\uC784 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
29866
+ return `${reason} \uC0C1\uD0DC\uB85C \uC885\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`;
29867
+ }
29868
+ function terminalNextStepText(reason) {
29869
+ if (reason === "tool_loop_limit") return "\uC774\uC5B4\uC11C \uC9C4\uD589\uD558\uB824\uBA74 \uC774\uBBF8 \uD655\uC778\uD55C \uD30C\uC77C\uACFC \uB2E4\uC74C \uD655\uC778 \uBC94\uC704\uB97C \uAE30\uC900\uC73C\uB85C \uB3C4\uAD6C \uC0AC\uC6A9 \uBC94\uC704\uB97C \uC870\uAE08 \uB354 \uC881\uD600 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uACA0\uC2B5\uB2C8\uB2E4.";
29870
+ if (reason === "max_turns") return "\uC774\uC5B4\uC11C \uC9C4\uD589\uD558\uB824\uBA74 \uAC19\uC740 \uC694\uCCAD\uC744 \uB2E4\uC2DC \uBCF4\uB0B4\uAC70\uB098, plan\uC758 \uD604\uC7AC task\uB97C \uB354 \uC791\uAC8C \uB098\uB204\uC5B4 \uB04A\uAE34 \uC9C0\uC810\uBD80\uD130 \uACC4\uC18D \uC9C4\uD589\uD558\uACA0\uC2B5\uB2C8\uB2E4.";
29871
+ if (reason === "content_filter") return "\uC774\uC5B4\uC11C \uC9C4\uD589\uD558\uB824\uBA74 \uC694\uCCAD \uD45C\uD604\uC774\uB098 \uBC94\uC704\uB97C \uC870\uC815\uD55C \uB4A4 \uC548\uC804\uD558\uAC8C \uB2F5\uBCC0 \uAC00\uB2A5\uD55C \uD615\uD0DC\uB85C \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uACA0\uC2B5\uB2C8\uB2E4.";
29872
+ return "\uC774\uC5B4\uC11C \uC9C4\uD589\uD558\uBA74 \uC704 \uC9C4\uD589 \uC0C1\uD0DC\uB97C \uAE30\uC900\uC73C\uB85C \uB0A8\uC740 \uC791\uC5C5\uBD80\uD130 \uB2E4\uC2DC \uD655\uC778\uD558\uACA0\uC2B5\uB2C8\uB2E4.";
29873
+ }
28930
29874
  function toolFailure(content) {
28931
29875
  return { ok: false, content };
28932
29876
  }
@@ -28959,11 +29903,15 @@ async function runExecutionSession(options) {
28959
29903
  providerName: options.providerName,
28960
29904
  model: options.backend.model,
28961
29905
  agent: options.agent,
29906
+ maxTurns: options.countToolUseTurns === true ? options.maxTurns : null,
28962
29907
  yes: options.yes,
28963
29908
  transcript: options.transcript,
28964
29909
  signal: options.signal,
28965
29910
  eventBus: options.eventBus,
28966
29911
  permissionPrompt: options.permissionPrompt,
29912
+ autoPermission: options.autoPermission,
29913
+ persistentGrants: options.persistentGrants,
29914
+ grants: options.grants,
28967
29915
  transcriptWriter: options.transcriptWriter,
28968
29916
  transcriptSessionId: options.transcriptSessionId,
28969
29917
  rootSessionId: options.rootSessionId,
@@ -28983,13 +29931,13 @@ async function runExecutionSession(options) {
28983
29931
 
28984
29932
  // src/goals/lock.ts
28985
29933
  import { mkdir as mkdir13, open as open3, rm as rm2 } from "node:fs/promises";
28986
- import path32 from "node:path";
29934
+ import path33 from "node:path";
28987
29935
 
28988
29936
  // src/goals/storage.ts
28989
29937
  import { createHash as createHash3 } from "node:crypto";
28990
29938
  import { mkdir as mkdir12, readFile as readFile16, rename as rename6, writeFile as writeFile13 } from "node:fs/promises";
28991
29939
  import fs9 from "node:fs";
28992
- import path31 from "node:path";
29940
+ import path32 from "node:path";
28993
29941
  var GoalStore = class {
28994
29942
  cwd;
28995
29943
  dir;
@@ -29000,8 +29948,8 @@ var GoalStore = class {
29000
29948
  this.cwd = cwd;
29001
29949
  this.scope = normalizeGoalStoreScope(options.scope);
29002
29950
  this.dir = goalStoreDir(cwd, this.scope);
29003
- this.activePath = path31.join(this.dir, "active.json");
29004
- this.archiveDir = path31.join(this.dir, "archive");
29951
+ this.activePath = path32.join(this.dir, "active.json");
29952
+ this.archiveDir = path32.join(this.dir, "archive");
29005
29953
  }
29006
29954
  async loadActive() {
29007
29955
  if (!fs9.existsSync(this.activePath)) return void 0;
@@ -29015,7 +29963,7 @@ var GoalStore = class {
29015
29963
  }
29016
29964
  async saveActive(state) {
29017
29965
  await mkdir12(this.dir, { recursive: true });
29018
- const tmp = path31.join(this.dir, `active.${process.pid}.${Date.now()}.tmp`);
29966
+ const tmp = path32.join(this.dir, `active.${process.pid}.${Date.now()}.tmp`);
29019
29967
  await writeFile13(tmp, `${JSON.stringify(state, null, 2)}
29020
29968
  `, "utf8");
29021
29969
  await rename6(tmp, this.activePath);
@@ -29029,21 +29977,21 @@ var GoalStore = class {
29029
29977
  status,
29030
29978
  updatedAt: Date.now()
29031
29979
  };
29032
- await writeFile13(path31.join(this.archiveDir, `${archived.id}.json`), `${JSON.stringify(archived, null, 2)}
29980
+ await writeFile13(path32.join(this.archiveDir, `${archived.id}.json`), `${JSON.stringify(archived, null, 2)}
29033
29981
  `, "utf8");
29034
29982
  await fs9.promises.rm(this.activePath, { force: true });
29035
29983
  return archived;
29036
29984
  }
29037
29985
  async backupCorrupted(text) {
29038
29986
  await mkdir12(this.dir, { recursive: true });
29039
- const backup = path31.join(this.dir, `active.corrupt.${Date.now()}.json`);
29987
+ const backup = path32.join(this.dir, `active.corrupt.${Date.now()}.json`);
29040
29988
  await writeFile13(backup, text, "utf8");
29041
29989
  await fs9.promises.rm(this.activePath, { force: true });
29042
29990
  }
29043
29991
  };
29044
29992
  function goalStoreDir(cwd, scope) {
29045
29993
  const safeScope = normalizeGoalStoreScope(scope);
29046
- return safeScope ? path31.join(cwd, ".demian", "goals", "sessions", safeScope) : path31.join(cwd, ".demian", "goals");
29994
+ return safeScope ? path32.join(cwd, ".demian", "goals", "sessions", safeScope) : path32.join(cwd, ".demian", "goals");
29047
29995
  }
29048
29996
  function normalizeGoalStoreScope(scope) {
29049
29997
  const trimmed = scope?.trim();
@@ -29057,10 +30005,10 @@ function normalizeGoalStoreScope(scope) {
29057
30005
  var GoalLock = class {
29058
30006
  path;
29059
30007
  constructor(cwd, scope) {
29060
- this.path = path32.join(goalStoreDir(cwd, scope), "active.lock");
30008
+ this.path = path33.join(goalStoreDir(cwd, scope), "active.lock");
29061
30009
  }
29062
30010
  async acquire() {
29063
- await mkdir13(path32.dirname(this.path), { recursive: true });
30011
+ await mkdir13(path33.dirname(this.path), { recursive: true });
29064
30012
  const handle = await open3(this.path, "wx").catch((error) => {
29065
30013
  const code = typeof error === "object" && error && "code" in error ? String(error.code) : "";
29066
30014
  if (code === "EEXIST") throw new Error("Another goal is already active in this workspace.");
@@ -29394,6 +30342,12 @@ function evaluateGoalIteration(input2) {
29394
30342
  const state = input2.state;
29395
30343
  const language = goalUserLanguage(state);
29396
30344
  const verification = input2.verification ?? state.verification ?? { status: "not_required" };
30345
+ const reasonContext = {
30346
+ state,
30347
+ finalAnswer: input2.finalAnswer,
30348
+ reason: input2.reason,
30349
+ verification
30350
+ };
29397
30351
  const completion = input2.completedByTool || state.status === "completed" ? "met" : "unmet";
29398
30352
  const verificationGate = verification.required ? verification.status : verification.status === "passed" || verification.status === "failed" || verification.status === "missing" ? verification.status : "not_required";
29399
30353
  const prohibition = input2.blockedReason ? "violated" : "clear";
@@ -29401,28 +30355,28 @@ function evaluateGoalIteration(input2) {
29401
30355
  const progress = input2.madeProgress === false ? "stalled" : input2.reason === "max_turns" ? "stalled" : "advanced";
29402
30356
  const resource = state.iteration >= state.maxIterations ? "iteration_limited" : state.tokenBudget && state.tokensUsed >= state.tokenBudget ? "budget_limited" : "within_limits";
29403
30357
  let decision = "continue";
29404
- let reason = goalReason("continue", language);
30358
+ let reason = goalReason("continue", language, reasonContext);
29405
30359
  if (state.status === "paused") {
29406
30360
  decision = "paused";
29407
- reason = goalReason("paused", language);
30361
+ reason = goalReason("paused", language, reasonContext);
29408
30362
  } else if (prohibition === "violated" || scope === "drifted") {
29409
30363
  decision = "blocked";
29410
- reason = input2.blockedReason ?? goalReason("blocked_guardrail", language);
30364
+ reason = input2.blockedReason ?? goalReason("blocked_guardrail", language, reasonContext);
29411
30365
  } else if (resource === "budget_limited") {
29412
30366
  decision = "budget_limited";
29413
- reason = goalReason("budget_limited", language);
30367
+ reason = goalReason("budget_limited", language, reasonContext);
29414
30368
  } else if (resource === "iteration_limited") {
29415
30369
  decision = "iteration_limited";
29416
- reason = goalReason("iteration_limited", language);
30370
+ reason = goalReason("iteration_limited", language, reasonContext);
29417
30371
  } else if (completion === "met" && (verificationGate === "passed" || verificationGate === "not_required")) {
29418
30372
  decision = "complete";
29419
- reason = goalReason("complete", language);
30373
+ reason = goalReason("complete", language, reasonContext);
29420
30374
  } else if (completion === "met" && (verificationGate === "failed" || verificationGate === "missing")) {
29421
30375
  decision = "blocked";
29422
- reason = goalReason("verification_failed", language);
30376
+ reason = goalReason("verification_failed", language, reasonContext);
29423
30377
  } else if (progress === "stalled" && input2.reason === "max_turns") {
29424
- decision = "iteration_limited";
29425
- reason = goalReason("max_turns", language);
30378
+ decision = "continue";
30379
+ reason = goalReason("max_turns", language, reasonContext);
29426
30380
  }
29427
30381
  return {
29428
30382
  gates: {
@@ -29556,29 +30510,69 @@ function normalizeSessionId(sessionId) {
29556
30510
  const trimmed = sessionId?.trim();
29557
30511
  return trimmed ? trimmed : void 0;
29558
30512
  }
29559
- function goalReason(key, language) {
30513
+ function goalReason(key, language, context = {}) {
30514
+ if (key === "max_turns") return maxTurnsGoalReason(language, context);
30515
+ if (key === "iteration_limited") return iterationLimitedGoalReason(language, context);
30516
+ if (key === "budget_limited") return budgetLimitedGoalReason(language, context);
29560
30517
  if (language === "ko") {
29561
30518
  return {
29562
30519
  continue: "\uBAA9\uD45C\uB294 \uC544\uC9C1 \uD65C\uC131 \uC0C1\uD0DC\uC785\uB2C8\uB2E4. \uB2E4\uC74C \uC548\uC804\uD55C \uC791\uC5C5\uC744 \uACC4\uC18D \uC9C4\uD589\uD569\uB2C8\uB2E4.",
29563
30520
  paused: "\uC0AC\uC6A9\uC790 \uC694\uCCAD\uC73C\uB85C \uBAA9\uD45C\uAC00 \uC77C\uC2DC \uC911\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
29564
30521
  blocked_guardrail: "\uAE08\uC9C0\uC870\uAC74 \uB610\uB294 \uBC94\uC704 \uAD00\uB9AC \uAE30\uC900\uC744 \uC704\uBC18\uD558\uC5EC \uBAA9\uD45C\uAC00 \uCC28\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
29565
- budget_limited: "\uC644\uB8CC\uAC00 \uAC80\uC99D\uB418\uAE30 \uC804\uC5D0 \uBAA9\uD45C\uAC00 \uD1A0\uD070 \uC608\uC0B0\uC5D0 \uB3C4\uB2EC\uD588\uC2B5\uB2C8\uB2E4.",
29566
- iteration_limited: "\uC644\uB8CC\uAC00 \uAC80\uC99D\uB418\uAE30 \uC804\uC5D0 \uBAA9\uD45C\uAC00 \uBC18\uBCF5 \uD55C\uB3C4\uC5D0 \uB3C4\uB2EC\uD588\uC2B5\uB2C8\uB2E4.",
29567
30522
  complete: "\uC644\uB8CC\uAC00 \uBA85\uC2DC\uC801\uC73C\uB85C \uC2E0\uD638\uB418\uC5C8\uACE0 \uC885\uB8CC \uAC80\uC99D \uC694\uAD6C\uC0AC\uD56D\uC774 \uCDA9\uC871\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
29568
- verification_failed: "\uC644\uB8CC\uAC00 \uC2E0\uD638\uB418\uC5C8\uC9C0\uB9CC \uD544\uC694\uD55C \uAC80\uC99D\uC744 \uD1B5\uACFC\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
29569
- max_turns: "\uAC80\uC99D\uB41C \uC644\uB8CC \uC5C6\uC774 \uC138\uC158\uC774 \uCD5C\uB300 \uD134\uC5D0\uC11C \uC911\uC9C0\uB418\uC5C8\uC2B5\uB2C8\uB2E4."
29570
- }[key];
30523
+ verification_failed: "\uC644\uB8CC\uAC00 \uC2E0\uD638\uB418\uC5C8\uC9C0\uB9CC \uD544\uC694\uD55C \uAC80\uC99D\uC744 \uD1B5\uACFC\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4."
30524
+ }[key] ?? "\uBAA9\uD45C \uC0C1\uD0DC\uB97C \uD3C9\uAC00\uD588\uC2B5\uB2C8\uB2E4.";
29571
30525
  }
29572
30526
  return {
29573
30527
  continue: "Goal remains active; continue with the next safe action.",
29574
30528
  paused: "Goal is paused by user request.",
29575
30529
  blocked_guardrail: "Goal is blocked because a prohibition or scope guardrail was violated.",
29576
- budget_limited: "Goal reached its token budget before completion was verified.",
29577
- iteration_limited: "Goal reached its iteration limit before completion was verified.",
29578
30530
  complete: "Completion was explicitly signaled and termination verification requirements are satisfied.",
29579
- verification_failed: "Completion was signaled, but required verification did not pass.",
29580
- max_turns: "The session stopped at max turns without verified completion."
29581
- }[key];
30531
+ verification_failed: "Completion was signaled, but required verification did not pass."
30532
+ }[key] ?? "Goal state was evaluated.";
30533
+ }
30534
+ function maxTurnsGoalReason(language, context) {
30535
+ const maxTurns = parseMaxTurns(context.finalAnswer);
30536
+ const maxTurnsText = maxTurns ? `maxTurns(${maxTurns})` : "maxTurns";
30537
+ const iteration = goalIterationProgress(context.state);
30538
+ if (language === "ko") {
30539
+ const run2 = iteration ? `${iteration}\uD68C\uCC28 \uC2E4\uD589 \uC911` : "\uC774\uBC88 \uC2E4\uD589\uC5D0\uC11C";
30540
+ return `\uC774\uBC88 \uBAA9\uD45C\uB294 \uC544\uC9C1 \uC644\uB8CC\uB85C \uAC80\uC99D\uB418\uC9C0 \uC54A\uC558\uACE0, ${run2} \uD558\uB098\uC758 \uC791\uC5C5 \uC2DC\uB3C4\uAC00 ${maxTurnsText}\uC5D0 \uB3C4\uB2EC\uD574 \uBA48\uCDC4\uC2B5\uB2C8\uB2E4. goal\uC5D0\uC11C\uB294 \uC774 \uAC12\uC744 \uB3C4\uAD6C \uD638\uCD9C \uC218\uAC00 \uC544\uB2C8\uB77C \uAC19\uC740 \uBAA9\uD45C\uB97C \uC774\uC5B4\uC11C \uC7AC\uC2DC\uB3C4\uD55C \uC0C1\uC704 \uC2E4\uD589 \uD69F\uC218\uB85C \uBD05\uB2C8\uB2E4. \uB530\uB77C\uC11C \uBAA9\uD45C \uC790\uCCB4\uB294 \uBC18\uBCF5 \uD55C\uB3C4\uC5D0 \uB3C4\uB2EC\uD558\uAE30 \uC804\uAE4C\uC9C0 \uACC4\uC18D \uD65C\uC131 \uC0C1\uD0DC\uB85C \uB0A8\uAE30\uACE0, \uB3C4\uAD6C \uD638\uCD9C\uCC98\uB7FC \uC815\uC0C1\uC801\uC778 \uD558\uC704 \uC791\uC5C5\uC740 \uBCC4\uB3C4 \uC548\uC804 \uD55C\uB3C4\uB85C \uB2E4\uB8F9\uB2C8\uB2E4. /retry\uB85C \uB04A\uAE34 \uC9C0\uC810\uBD80\uD130 \uC774\uC5B4\uC11C \uC9C4\uD589\uD574 \uC8FC\uC138\uC694.`;
30541
+ }
30542
+ const run = iteration ? `at goal iteration ${iteration}` : "during this run";
30543
+ return `This goal is not verified complete yet, and one task attempt stopped ${run} because it reached ${maxTurnsText}. In goal mode, this value is treated as a high-level retry/attempt limit for the same objective, not as a count of tool calls. The goal stays active until its iteration limit is actually reached, while ordinary tool-use subwork is guarded by a separate safety limit. Retry from the current state to continue.`;
30544
+ }
30545
+ function iterationLimitedGoalReason(language, context) {
30546
+ const iteration = goalIterationProgress(context.state);
30547
+ if (language === "ko") {
30548
+ const limit2 = iteration ? `${iteration} \uBC18\uBCF5 \uD55C\uB3C4` : "\uBC18\uBCF5 \uD55C\uB3C4";
30549
+ return `\uBAA9\uD45C\uAC00 ${limit2}\uC5D0 \uB3C4\uB2EC\uD588\uC9C0\uB9CC \uC644\uB8CC \uAC80\uC99D\uC744 \uD1B5\uACFC\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uCD5C\uADFC \uC2E4\uD589 \uACB0\uACFC\uC640 \uC2E4\uD328 \uB610\uB294 \uBBF8\uC644\uB8CC step\uC744 \uD655\uC778\uD55C \uB4A4, \uBC94\uC704\uB97C \uC904\uC774\uAC70\uB098 \uC0C8 \uBAA9\uD45C\uB85C \uB2E4\uC2DC \uC2DC\uC791\uD574 \uC8FC\uC138\uC694.`;
30550
+ }
30551
+ const limit = iteration ? `its ${iteration} iteration limit` : "its iteration limit";
30552
+ return `The goal reached ${limit} before completion was verified. Review the latest run output and any failed or unresolved steps, then narrow the scope or start a new goal.`;
30553
+ }
30554
+ function budgetLimitedGoalReason(language, context) {
30555
+ const budget = goalBudgetProgress(context.state);
30556
+ if (language === "ko") {
30557
+ const limit2 = budget ? `\uD1A0\uD070 \uC608\uC0B0 ${budget}` : "\uD1A0\uD070 \uC608\uC0B0";
30558
+ return `\uC644\uB8CC\uAC00 \uAC80\uC99D\uB418\uAE30 \uC804\uC5D0 \uBAA9\uD45C\uAC00 ${limit2}\uC5D0 \uB3C4\uB2EC\uD588\uC2B5\uB2C8\uB2E4. \uD604\uC7AC\uAE4C\uC9C0\uC758 \uACB0\uACFC\uB97C \uD655\uC778\uD55C \uB4A4, \uBC94\uC704\uB97C \uC904\uC774\uAC70\uB098 \uC608\uC0B0\uC744 \uB298\uB824 \uB2E4\uC2DC \uC9C4\uD589\uD574 \uC8FC\uC138\uC694.`;
30559
+ }
30560
+ const limit = budget ? `token budget ${budget}` : "token budget";
30561
+ return `The goal reached its ${limit} before completion was verified. Review the current result, then narrow the scope or increase the budget before continuing.`;
30562
+ }
30563
+ function parseMaxTurns(finalAnswer) {
30564
+ const value = finalAnswer?.match(/\bmaxTurns\s*\(\s*(\d+)\s*\)/i)?.[1];
30565
+ if (!value) return void 0;
30566
+ const parsed = Number(value);
30567
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
30568
+ }
30569
+ function goalIterationProgress(state) {
30570
+ if (!state || !Number.isFinite(state.iteration) || !Number.isFinite(state.maxIterations) || state.maxIterations <= 0) return void 0;
30571
+ return `${state.iteration}/${state.maxIterations}`;
30572
+ }
30573
+ function goalBudgetProgress(state) {
30574
+ if (!state || !Number.isFinite(state.tokensUsed) || !Number.isFinite(state.tokenBudget) || !state.tokenBudget || state.tokenBudget <= 0) return void 0;
30575
+ return `${state.tokensUsed}/${state.tokenBudget}`;
29582
30576
  }
29583
30577
  function createGoalId() {
29584
30578
  return `goal_${createSessionId().slice("ses_".length)}`;
@@ -29647,9 +30641,76 @@ function sha256(value) {
29647
30641
  }
29648
30642
 
29649
30643
  // src/root-session.ts
29650
- import { cp, mkdtemp, rm as rm3 } from "node:fs/promises";
30644
+ import { cp, mkdtemp, readFile as readFile17, rm as rm3 } from "node:fs/promises";
29651
30645
  import os15 from "node:os";
29652
- import path33 from "node:path";
30646
+ import path34 from "node:path";
30647
+
30648
+ // src/permissions/auto-agent.ts
30649
+ function createAutoPermissionAgent(options) {
30650
+ return async (request) => {
30651
+ const command = bashCommandFromRequest(request);
30652
+ if (!command || !isPotentiallyReadOnlyShellCommand(command)) return void 0;
30653
+ const result = await classifyWithAgent(command, request, options).catch(() => void 0);
30654
+ if (!result || result.decision !== "allow" || result.confidence < 0.85) return void 0;
30655
+ return { decision: "allow", reason: `auto permission judge: ${preview(result.reason, 160)}` };
30656
+ };
30657
+ }
30658
+ function bashCommandFromRequest(request) {
30659
+ const toolName = (request.qualifiedName ?? request.tool).split(".").at(-1)?.toLowerCase();
30660
+ if (toolName !== "bash") return void 0;
30661
+ const input2 = request.input && typeof request.input === "object" && !Array.isArray(request.input) ? request.input : {};
30662
+ return typeof input2.command === "string" ? input2.command : void 0;
30663
+ }
30664
+ async function classifyWithAgent(command, request, options) {
30665
+ const response = await options.provider.chat({
30666
+ model: options.model,
30667
+ tools: [],
30668
+ maxTokens: 180,
30669
+ temperature: 0,
30670
+ signal: permissionSignal(options.signal, options.timeoutMs ?? 4e3),
30671
+ messages: [
30672
+ {
30673
+ role: "system",
30674
+ content: [
30675
+ "You are Demian's internal permission judge.",
30676
+ "Classify whether a shell command is safe to auto-allow without asking the user.",
30677
+ "Allow only commands that are clearly read-only, inspect local workspace state, and do not write files, mutate git state, install packages, start long-running services, send network requests, change permissions, or execute nested commands.",
30678
+ "If there is any uncertainty, choose ask. Never choose allow just because the command seems common.",
30679
+ 'Return only compact JSON with this shape: {"decision":"allow"|"ask","confidence":0..1,"reason":"short reason"}.'
30680
+ ].join(" ")
30681
+ },
30682
+ {
30683
+ role: "user",
30684
+ content: JSON.stringify({
30685
+ cwd: options.cwd,
30686
+ tool: request.qualifiedName ?? request.tool,
30687
+ actor: request.actor,
30688
+ command
30689
+ })
30690
+ }
30691
+ ]
30692
+ });
30693
+ return parseJudgeResponse(response.message.content);
30694
+ }
30695
+ function permissionSignal(parent, timeoutMs) {
30696
+ const timeout = AbortSignal.timeout(timeoutMs);
30697
+ return parent ? AbortSignal.any([parent, timeout]) : timeout;
30698
+ }
30699
+ function parseJudgeResponse(content) {
30700
+ if (!content) return void 0;
30701
+ const json = content.match(/\{[\s\S]*\}/)?.[0];
30702
+ if (!json) return void 0;
30703
+ try {
30704
+ const parsed = JSON.parse(json);
30705
+ const decision = parsed.decision === "allow" ? "allow" : parsed.decision === "ask" ? "ask" : void 0;
30706
+ const confidence = typeof parsed.confidence === "number" ? parsed.confidence : Number(parsed.confidence);
30707
+ const reason = typeof parsed.reason === "string" ? parsed.reason.trim() : "";
30708
+ if (!decision || !Number.isFinite(confidence) || confidence < 0 || confidence > 1 || !reason) return void 0;
30709
+ return { decision, confidence, reason };
30710
+ } catch {
30711
+ return void 0;
30712
+ }
30713
+ }
29653
30714
 
29654
30715
  // src/permissions/presets.ts
29655
30716
  var PERMISSION_PRESETS = ["deny", "ask", "auto", "full"];
@@ -29665,9 +30726,19 @@ function defaultDecisionForPermissionPreset(preset) {
29665
30726
  if (preset === "full") return "allow";
29666
30727
  return "by-category";
29667
30728
  }
30729
+ function invocationDecisionForPermissionPreset(preset) {
30730
+ if (preset === "deny") return "deny";
30731
+ if (preset === "ask") return "ask";
30732
+ return "allow";
30733
+ }
29668
30734
  function applyPermissionPresetToAgent(agent, preset) {
30735
+ const invocationDecision = invocationDecisionForPermissionPreset(preset);
29669
30736
  return {
29670
30737
  ...agent,
30738
+ delegation: agent.delegation ? {
30739
+ ...agent.delegation,
30740
+ invocationDecision
30741
+ } : agent.delegation,
29671
30742
  defaultDecision: defaultDecisionForPermissionPreset(preset)
29672
30743
  };
29673
30744
  }
@@ -29679,6 +30750,13 @@ function createCoworkTool(options) {
29679
30750
  return {
29680
30751
  name: "cowork",
29681
30752
  description: "Run multiple bounded Demian cowork agents and return their combined result. members[].agent must be an exact cowork agent name, not the current parent agent.",
30753
+ metadata: {
30754
+ category: "orchestration.cowork",
30755
+ categoryPath: ["orchestration", "cowork"],
30756
+ sideEffect: "external",
30757
+ defaultDecision: options.config.defaultInvocationDecision,
30758
+ trust: "builtin"
30759
+ },
29682
30760
  inputSchema: {
29683
30761
  type: "object",
29684
30762
  properties: {
@@ -29693,6 +30771,9 @@ function createCoworkTool(options) {
29693
30771
  properties: {
29694
30772
  id: { type: "string", pattern: "^[a-z0-9_-]{1,32}$" },
29695
30773
  agent: { type: "string", enum: coworkAgentNames, description: agentDescription },
30774
+ provider: { type: "string", description: "Optional provider override. Must be listed in cowork.providers." },
30775
+ model: { type: "string", description: "Optional model display name or raw model id for this member's provider." },
30776
+ modelProfileName: { type: "string", description: "Optional stable model profile name for this member's provider." },
29696
30777
  task: { type: "string" },
29697
30778
  dependsOn: { type: "array", items: { type: "string" } },
29698
30779
  role: { type: "string" },
@@ -29703,7 +30784,7 @@ function createCoworkTool(options) {
29703
30784
  constraints: { type: "array", items: { type: "string" } },
29704
30785
  expectedOutput: { type: "string" },
29705
30786
  maxTurns: { type: "integer", minimum: 1 },
29706
- mode: { type: "string", enum: ["read-only", "write", "review"] },
30787
+ mode: { type: "string", enum: ["read-only", "write", "review", "manage"] },
29707
30788
  writeScope: { type: "array", items: { type: "string" } },
29708
30789
  returnMode: { type: "string", enum: ["brief", "normal"] }
29709
30790
  },
@@ -29769,6 +30850,9 @@ function parseMember(input2, index) {
29769
30850
  agent: object2.agent.trim(),
29770
30851
  task: object2.task.trim()
29771
30852
  };
30853
+ if (typeof object2.provider === "string" && object2.provider.trim()) member.provider = object2.provider.trim();
30854
+ if (typeof object2.model === "string" && object2.model.trim()) member.model = object2.model.trim();
30855
+ if (typeof object2.modelProfileName === "string" && object2.modelProfileName.trim()) member.modelProfileName = object2.modelProfileName.trim();
29772
30856
  if (typeof object2.id === "string" && object2.id.trim()) member.id = object2.id.trim();
29773
30857
  if (typeof object2.role === "string" && object2.role.trim()) member.role = object2.role.trim();
29774
30858
  if (typeof object2.context === "string") member.context = object2.context;
@@ -29776,7 +30860,7 @@ function parseMember(input2, index) {
29776
30860
  if (object2.returnMode === "brief" || object2.returnMode === "normal") member.returnMode = object2.returnMode;
29777
30861
  if (object2.parentDetail === "preview" || object2.parentDetail === "full" || object2.parentDetail === "none") member.parentDetail = object2.parentDetail;
29778
30862
  if (object2.mode !== void 0) {
29779
- if (object2.mode !== "read-only" && object2.mode !== "write" && object2.mode !== "review") return { ok: false, error: `cowork.members[${index}].mode must be read-only, review, or write.` };
30863
+ if (object2.mode !== "read-only" && object2.mode !== "write" && object2.mode !== "review" && object2.mode !== "manage") return { ok: false, error: `cowork.members[${index}].mode must be read-only, review, manage, or write.` };
29780
30864
  member.mode = object2.mode;
29781
30865
  }
29782
30866
  if (typeof object2.maxTurns === "number" && Number.isInteger(object2.maxTurns) && object2.maxTurns > 0) member.maxTurns = object2.maxTurns;
@@ -29807,6 +30891,13 @@ function createDelegateAgentTool(options) {
29807
30891
  return {
29808
30892
  name: "delegate_agent",
29809
30893
  description: "Delegate a bounded task to a configured demian agent and return its result.",
30894
+ metadata: {
30895
+ category: "orchestration.agent",
30896
+ categoryPath: ["orchestration", "agent"],
30897
+ sideEffect: "external",
30898
+ defaultDecision: "ask",
30899
+ trust: "builtin"
30900
+ },
29810
30901
  inputSchema: {
29811
30902
  type: "object",
29812
30903
  properties: {
@@ -29857,6 +30948,172 @@ function stringArray2(value) {
29857
30948
  return value.filter((item) => typeof item === "string");
29858
30949
  }
29859
30950
 
30951
+ // src/cowork/permissions.ts
30952
+ function resolveCoworkPermissionIntent(agent, mode) {
30953
+ if (agent.permissionIntent) return agent.permissionIntent;
30954
+ if (mode === "write") return "write";
30955
+ if (mode === "review") return "review";
30956
+ if (mode === "manage") return "manage";
30957
+ const category = agent.catalog?.category;
30958
+ if (category === "builder") return "write";
30959
+ if (category === "reviewer") return "review";
30960
+ if (category === "planner") return "manage";
30961
+ return "read-only";
30962
+ }
30963
+ function compileCoworkPermissionOverlay(options) {
30964
+ const intent = resolveCoworkPermissionIntent(options.agent, options.mode);
30965
+ const mapping = options.config.permissionOverlay.modeMappings[intent];
30966
+ const providerNamespace = options.config.permissionOverlay.providerToolNamespaces[options.providerProfile];
30967
+ const providerTools = providerNamespace ? [...mapping?.providerTools[options.providerProfile] ?? []] : [];
30968
+ const demianTools = [...mapping?.demianTools ?? []];
30969
+ const defaultDecision = mapping?.defaultDecision ?? options.agent.defaultDecision ?? "ask";
30970
+ const rules = overlayRules({ intent, providerNamespace, providerTools, demianTools });
30971
+ return {
30972
+ intent,
30973
+ providerProfile: options.providerProfile,
30974
+ providerNamespace,
30975
+ demianTools,
30976
+ providerTools,
30977
+ rules,
30978
+ defaultDecision,
30979
+ explanation: [
30980
+ `permission intent ${intent}`,
30981
+ providerNamespace ? `${providerNamespace} tools: ${providerTools.join(", ") || "(none)"}` : "no provider tool namespace",
30982
+ `default decision ${defaultDecision}`
30983
+ ]
30984
+ };
30985
+ }
30986
+ function overlayRules(options) {
30987
+ const rules = [];
30988
+ for (const tool of options.demianTools) {
30989
+ rules.push({ tool, decision: demianToolDecision(options.intent, tool) });
30990
+ }
30991
+ if (options.providerNamespace) {
30992
+ const allowed = new Set(options.providerTools);
30993
+ for (const tool of options.providerTools) {
30994
+ rules.push({ tool: `${options.providerNamespace}.${tool}`, decision: providerToolDecision(options.intent, tool) });
30995
+ }
30996
+ for (const tool of ["Edit", "Write", "MultiEdit", "Bash"]) {
30997
+ if (!allowed.has(tool)) rules.push({ tool: `${options.providerNamespace}.${tool}`, decision: "deny", reason: `${options.intent} cowork permission overlay` });
30998
+ }
30999
+ }
31000
+ return rules;
31001
+ }
31002
+ function demianToolDecision(intent, tool) {
31003
+ if (intent !== "write") return "allow";
31004
+ return tool === "edit_file" || tool === "write_file" || tool === "bash" ? "ask" : "allow";
31005
+ }
31006
+ function providerToolDecision(intent, tool) {
31007
+ if (intent !== "write") return "allow";
31008
+ return tool === "Edit" || tool === "Write" || tool === "MultiEdit" || tool === "Bash" ? "ask" : "allow";
31009
+ }
31010
+
31011
+ // src/cowork/routing.ts
31012
+ function resolveCoworkAssignment(options) {
31013
+ const memberProvider = options.member.provider?.trim();
31014
+ if (memberProvider && !options.config.providers.includes(memberProvider)) {
31015
+ throw new Error(`cowork member provider ${memberProvider} is not in cowork.providers.`);
31016
+ }
31017
+ const agentName = coworkAgentName(options.agent.name);
31018
+ const sourceAndOrder = providerOrder({
31019
+ memberProvider,
31020
+ memberModel: options.member.model,
31021
+ memberModelProfileName: options.member.modelProfileName,
31022
+ agentName,
31023
+ agent: options.agent,
31024
+ config: options.config
31025
+ });
31026
+ const constrainedOrder = sourceAndOrder.source === "legacy-agent-provider" ? sourceAndOrder.order : sourceAndOrder.order.filter((selection) => options.config.providers.includes(selection.provider));
31027
+ const filtered = constrainedOrder.filter((selection) => options.availability?.[selection.provider] !== "unavailable");
31028
+ if (filtered.length === 0) {
31029
+ throw new Error(`No available cowork provider for ${options.agent.name}. Tried: ${sourceAndOrder.order.map(providerSelectionLabel).join(", ") || "(none)"}.`);
31030
+ }
31031
+ const selected = filtered[0];
31032
+ const providerProfile = selected.provider;
31033
+ const mode = options.member.mode ?? "read-only";
31034
+ const permissionOverlay = compileCoworkPermissionOverlay({
31035
+ config: options.config,
31036
+ agent: options.agent,
31037
+ providerProfile,
31038
+ mode
31039
+ });
31040
+ return {
31041
+ agent: options.agent,
31042
+ agentName,
31043
+ providerProfile,
31044
+ model: selected.model,
31045
+ modelProfileName: selected.modelProfileName,
31046
+ remainingProviders: filtered.slice(1).map((selection) => selection.provider),
31047
+ remainingProviderSelections: filtered.slice(1),
31048
+ source: sourceAndOrder.source,
31049
+ permissionOverlay,
31050
+ explanation: [
31051
+ `provider source: ${sourceAndOrder.source}`,
31052
+ `provider/model order: ${filtered.map(providerSelectionLabel).join(" > ")}`,
31053
+ ...permissionOverlay.explanation
31054
+ ]
31055
+ };
31056
+ }
31057
+ function coworkProviderAvailability(config, providers) {
31058
+ const availability = {};
31059
+ for (const provider of config.providers) {
31060
+ const providerConfig = providers[provider];
31061
+ if (!providerConfig || providerConfig.hidden) availability[provider] = "unavailable";
31062
+ else availability[provider] = "configured";
31063
+ }
31064
+ return availability;
31065
+ }
31066
+ function providerOrder(options) {
31067
+ if (options.memberProvider) {
31068
+ return {
31069
+ source: "explicit-provider",
31070
+ order: [
31071
+ {
31072
+ provider: options.memberProvider,
31073
+ ...options.memberModel ? { model: options.memberModel } : {},
31074
+ ...options.memberModelProfileName ? { modelProfileName: options.memberModelProfileName } : {}
31075
+ }
31076
+ ]
31077
+ };
31078
+ }
31079
+ if (options.agent.provider?.profile && !options.agent.defaultProviders?.length) return { source: "legacy-agent-provider", order: [{ provider: options.agent.provider.profile }] };
31080
+ const userModelPool = options.agentName ? options.config.routing.agentModelPools[options.agentName] : void 0;
31081
+ if (userModelPool?.length) return { source: "user-model-pool", order: uniqueProviderSelections(userModelPool) };
31082
+ const userOrder = options.agentName ? options.config.routing.agentProviders[options.agentName] : void 0;
31083
+ if (userOrder?.length) return { source: "user-routing", order: unique2(userOrder).map((provider) => ({ provider })) };
31084
+ if (options.agent.defaultProviders?.length) return { source: "agent-default", order: unique2(options.agent.defaultProviders).map((provider) => ({ provider })) };
31085
+ if (options.agent.provider?.profile) return { source: "legacy-agent-provider", order: [{ provider: options.agent.provider.profile }] };
31086
+ return { source: "cowork-pool", order: unique2(options.config.providers).map((provider) => ({ provider })) };
31087
+ }
31088
+ function coworkAgentName(value) {
31089
+ return COWORK_AGENT_NAMES.includes(value) ? value : void 0;
31090
+ }
31091
+ function unique2(values) {
31092
+ const seen = /* @__PURE__ */ new Set();
31093
+ const out = [];
31094
+ for (const value of values) {
31095
+ if (seen.has(value)) continue;
31096
+ seen.add(value);
31097
+ out.push(value);
31098
+ }
31099
+ return out;
31100
+ }
31101
+ function uniqueProviderSelections(values) {
31102
+ const seen = /* @__PURE__ */ new Set();
31103
+ const out = [];
31104
+ for (const value of values) {
31105
+ const key = `${value.provider}\0${value.modelProfileName ?? ""}\0${value.model ?? ""}`;
31106
+ if (seen.has(key)) continue;
31107
+ seen.add(key);
31108
+ out.push({ ...value });
31109
+ }
31110
+ return out;
31111
+ }
31112
+ function providerSelectionLabel(selection) {
31113
+ const model = selection.modelProfileName ?? selection.model;
31114
+ return model ? `${selection.provider}:${model}` : selection.provider;
31115
+ }
31116
+
29860
31117
  // src/root-session.ts
29861
31118
  var demianOnlyToolNames = /* @__PURE__ */ new Set(["delegate_agent", "cowork", "get_goal", "create_goal", "update_goal"]);
29862
31119
  var RootSession = class {
@@ -29870,6 +31127,7 @@ var RootSession = class {
29870
31127
  #transcript;
29871
31128
  #agentSessions = /* @__PURE__ */ new Map();
29872
31129
  #externalSessions = new ClaudeCodeSessionMap();
31130
+ #coworkProviderAvailability;
29873
31131
  #mainExternalSessionKey;
29874
31132
  #currentPrompt = "";
29875
31133
  #progress;
@@ -29878,6 +31136,7 @@ var RootSession = class {
29878
31136
  constructor(options) {
29879
31137
  this.#options = options;
29880
31138
  this.#permissionPreset = normalizePermissionPreset(options.permissionPreset);
31139
+ this.#coworkProviderAvailability = coworkProviderAvailability(options.config.cowork, options.config.providers);
29881
31140
  this.events = options.eventBus ?? new EventBus();
29882
31141
  this.agents = new AgentRegistry();
29883
31142
  this.#progress = new ProgressLedger({ rootSessionId: this.id });
@@ -29924,6 +31183,8 @@ var RootSession = class {
29924
31183
  agent: mainAgent,
29925
31184
  hooks: this.#options.hooks ?? this.#options.config.hooks,
29926
31185
  maxTurns: this.#options.maxTurns ?? this.#options.config.maxTurns,
31186
+ countToolUseTurns: false,
31187
+ maxToolUseRounds: relaxedToolUseRoundLimit(this.#options.maxTurns ?? this.#options.config.maxTurns),
29927
31188
  yes: this.#options.yes,
29928
31189
  dryRun: this.#options.dryRun,
29929
31190
  streaming: this.#options.streaming ?? this.#options.config.streaming.enabled,
@@ -29936,6 +31197,7 @@ var RootSession = class {
29936
31197
  signal: options.signal,
29937
31198
  eventBus: this.events,
29938
31199
  permissionPrompt: this.permissionPrompt,
31200
+ autoPermission: this.autoPermissionForBackend(backend, this.#options.cwd, options.signal),
29939
31201
  toolRegistry: this.tools,
29940
31202
  grants: this.#grants,
29941
31203
  transcriptWriter: this.#transcript,
@@ -29984,7 +31246,7 @@ var RootSession = class {
29984
31246
  const providerProfile = childAgent.provider?.profile ?? this.#options.providerName;
29985
31247
  const invocationGrantKey = grantKeyForAgentInvocation(childAgent.name, providerProfile);
29986
31248
  if (this.#grants.has(invocationGrantKey)) return { ok: true };
29987
- const decision = childAgent.delegation?.invocationDecision ?? this.#options.config.delegation.defaultInvocationDecision;
31249
+ const decision = invocationDecisionForPermissionPreset(this.#permissionPreset);
29988
31250
  if (decision === "deny") return { ok: false, error: `Agent invocation denied: ${childAgent.name}` };
29989
31251
  if (decision !== "ask") return { ok: true };
29990
31252
  this.events.emit({
@@ -30103,6 +31365,8 @@ var RootSession = class {
30103
31365
  agent: childRuntimeAgent,
30104
31366
  hooks: this.#options.hooks ?? this.#options.config.hooks,
30105
31367
  maxTurns,
31368
+ countToolUseTurns: false,
31369
+ maxToolUseRounds: relaxedToolUseRoundLimit(maxTurns),
30106
31370
  yes: this.#options.yes,
30107
31371
  dryRun: this.#options.dryRun,
30108
31372
  streaming: false,
@@ -30115,6 +31379,7 @@ var RootSession = class {
30115
31379
  signal,
30116
31380
  eventBus: this.events,
30117
31381
  permissionPrompt: this.permissionPrompt,
31382
+ autoPermission: this.autoPermissionForBackend(backend, cwd, signal),
30118
31383
  toolRegistry: this.tools,
30119
31384
  grants: this.#grants,
30120
31385
  transcriptWriter: this.#transcript,
@@ -30137,6 +31402,31 @@ var RootSession = class {
30137
31402
  session.externalSessionKey = externalKey;
30138
31403
  }
30139
31404
  }
31405
+ if (result.endReason !== "end_turn") {
31406
+ const message = childExecutionFailureMessage(result.endReason, result.finalAnswer);
31407
+ if (result.endReason === "cancelled") {
31408
+ this.events.emit({
31409
+ type: "agent.invocation.cancelled",
31410
+ rootSessionId: this.id,
31411
+ agentSessionId: session.id,
31412
+ invocationId,
31413
+ agent: childRuntimeAgent.name,
31414
+ reason: message,
31415
+ ts: now()
31416
+ });
31417
+ return { ok: false, error: `Agent invocation cancelled: ${message}`, invocationId, agentName: childRuntimeAgent.name, providerProfile, model: backend.model, metadata: { endReason: result.endReason } };
31418
+ }
31419
+ this.events.emit({
31420
+ type: "agent.invocation.failed",
31421
+ rootSessionId: this.id,
31422
+ agentSessionId: session.id,
31423
+ invocationId,
31424
+ agent: childRuntimeAgent.name,
31425
+ error: message,
31426
+ ts: now()
31427
+ });
31428
+ return { ok: false, error: `Agent invocation failed: ${message}`, invocationId, agentName: childRuntimeAgent.name, providerProfile, model: backend.model, metadata: { endReason: result.endReason } };
31429
+ }
30140
31430
  this.updateAgentSession(session, input2, result.messages, result.finalAnswer);
30141
31431
  if (beforeMessages !== session.messages.length) {
30142
31432
  this.events.emit({
@@ -30212,7 +31502,7 @@ var RootSession = class {
30212
31502
  }))
30213
31503
  });
30214
31504
  if (!this.#grants.has(grantKey)) {
30215
- const decision = this.#options.config.cowork.defaultInvocationDecision;
31505
+ const decision = invocationDecisionForPermissionPreset(this.#permissionPreset);
30216
31506
  if (decision === "deny") return fail("Cowork invocation denied by configuration.");
30217
31507
  if (decision === "ask") {
30218
31508
  this.events.emit({
@@ -30396,43 +31686,27 @@ var RootSession = class {
30396
31686
  }
30397
31687
  async runCoworkMember(member, group, outcomes, context) {
30398
31688
  const startedAt = now();
30399
- const coworkAgent = this.decorateCoworkMemberAgent(member.agent, member.mode);
30400
- const runtimeAgent = this.decorateChildAgent(coworkAgent);
30401
- const backend = this.resolveInvocationBackend(member.providerProfile, void 0, runtimeAgent);
30402
- this.events.emit({
30403
- type: "cowork.member.started",
30404
- rootSessionId: this.id,
30405
- parentInvocationId: context.parent.parentInvocationId,
30406
- groupId: context.groupId,
30407
- memberId: member.memberId,
30408
- agent: member.agent.name,
30409
- provider: member.providerProfile,
30410
- model: backend.model,
30411
- mode: member.mode,
30412
- ts: startedAt
30413
- });
30414
31689
  const input2 = withParentContext(member.input, group, outcomes);
30415
31690
  const workspace = member.isolated ? await this.createCoworkIsolatedWorkspace(context.groupId, member.memberId) : { cwd: this.#options.cwd };
30416
31691
  const snapshot = member.mode === "write" ? await this.createCoworkWriterSnapshot(workspace.cwd) : void 0;
30417
- const result = await this.executeChildAgentSession(input2, coworkAgent, context.parent, {
30418
- sessionScope: `cowork:${context.groupId}:${member.memberId}`,
30419
- signal: context.signal,
30420
- writeScope: member.writeScope,
30421
- cwd: workspace.cwd
30422
- });
31692
+ const attempt = member.mode === "write" ? await this.runCoworkWriteAttempt(member, input2, snapshot, workspace.cwd, context) : await this.runCoworkNonWriteAttempts(member, input2, workspace.cwd, context);
30423
31693
  const durationMs = now() - startedAt;
30424
31694
  const audit = member.mode === "write" ? await this.auditCoworkWriter(member, snapshot, workspace.cwd, member.isolated) : void 0;
30425
31695
  if ("cleanup" in workspace) await workspace.cleanup().catch(() => void 0);
31696
+ const result = attempt.result;
31697
+ const providerProfile = result.providerProfile;
30426
31698
  if (result.ok) {
30427
31699
  const outOfScope2 = audit?.outOfScopeFiles ?? [];
30428
31700
  const outcome2 = {
30429
31701
  memberId: member.memberId,
30430
31702
  agent: member.agent.name,
30431
- provider: member.providerProfile,
31703
+ provider: providerProfile,
30432
31704
  status: outOfScope2.length ? "failed" : "completed",
30433
- summary: preview(coworkMemberSummary(result.finalAnswer.trim() || "Completed.", audit), 1e3),
31705
+ summary: preview(coworkMemberSummary(result.finalAnswer.trim() || "Completed.", audit, attempt), 1e3),
30434
31706
  artifacts: audit ? [coworkAuditArtifact(audit)] : [],
30435
31707
  reason: outOfScope2.length ? "out_of_scope" : void 0,
31708
+ fallbackReason: attempt.fallbackReason,
31709
+ repairReason: attempt.repairReason,
30436
31710
  error: outOfScope2.length ? `Writer changed files outside writeScope: ${outOfScope2.join(", ")}` : void 0,
30437
31711
  invocationId: result.invocationId,
30438
31712
  tokens: result.usage,
@@ -30450,11 +31724,13 @@ var RootSession = class {
30450
31724
  const outcome = {
30451
31725
  memberId: member.memberId,
30452
31726
  agent: member.agent.name,
30453
- provider: member.providerProfile,
31727
+ provider: providerProfile,
30454
31728
  status: cancelled ? "cancelled" : "failed",
30455
- summary: preview(coworkMemberSummary(result.error, audit), 1e3),
31729
+ summary: preview(coworkMemberSummary(result.error, audit, attempt), 1e3),
30456
31730
  artifacts: audit ? [coworkAuditArtifact(audit)] : [],
30457
31731
  reason: cancelled ? "cancelled" : outOfScope.length ? "out_of_scope" : void 0,
31732
+ fallbackReason: attempt.fallbackReason,
31733
+ repairReason: attempt.repairReason,
30458
31734
  error: outOfScope.length ? `${result.error}
30459
31735
  Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.error,
30460
31736
  invocationId: result.invocationId,
@@ -30467,6 +31743,115 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
30467
31743
  }
30468
31744
  return outcome;
30469
31745
  }
31746
+ async runCoworkNonWriteAttempts(member, input2, cwd, context) {
31747
+ const providers = [{ provider: member.providerProfile, model: member.model, modelProfileName: member.modelProfileName }, ...member.remainingProviderSelections];
31748
+ const errors = [];
31749
+ let fallbackReason;
31750
+ for (const [index, selection] of providers.entries()) {
31751
+ const overlay = index === 0 && member.permissionOverlay ? member.permissionOverlay : this.compileCoworkOverlay(member, selection.provider);
31752
+ const result2 = await this.executeCoworkMemberAttempt(member, selection, overlay, input2, cwd, context, `cowork:${context.groupId}:${member.memberId}:${providerSelectionScope(selection)}`);
31753
+ if (result2.ok) return { result: result2, fallbackReason };
31754
+ errors.push(`${providerSelectionLabel2(selection)}: ${result2.error}`);
31755
+ if (context.signal.aborted) return { result: result2, fallbackReason };
31756
+ const policy = pickCoworkNonWriteFallbackPolicy(result2.error, this.#options.config.cowork.fallback.nonWrite);
31757
+ const nextProvider = providers[index + 1];
31758
+ if ((policy === "next-provider" || policy === "skip-provider") && nextProvider) {
31759
+ if (policy === "skip-provider") this.#coworkProviderAvailability[selection.provider] = "unavailable";
31760
+ fallbackReason = `${providerSelectionLabel2(selection)} failed (${coworkFailureKind(result2.error)}); switched to ${providerSelectionLabel2(nextProvider)}`;
31761
+ continue;
31762
+ }
31763
+ return { result: withCombinedCoworkErrors(result2, errors), fallbackReason };
31764
+ }
31765
+ const result = {
31766
+ ok: false,
31767
+ error: `Agent invocation failed: all cowork providers failed for ${member.agent.name}.
31768
+ ${errors.join("\n")}`,
31769
+ agentName: member.agent.name,
31770
+ providerProfile: providers.at(-1)?.provider ?? member.providerProfile
31771
+ };
31772
+ return { result, fallbackReason };
31773
+ }
31774
+ async runCoworkWriteAttempt(member, input2, snapshot, cwd, context) {
31775
+ const selection = { provider: member.providerProfile, model: member.model, modelProfileName: member.modelProfileName };
31776
+ const result = await this.executeCoworkMemberAttempt(member, selection, member.permissionOverlay, input2, cwd, context, `cowork:${context.groupId}:${member.memberId}`);
31777
+ if (result.ok || context.signal.aborted) return { result };
31778
+ const policy = this.#options.config.cowork.fallback.write;
31779
+ if (policy.onFailure !== "repair-from-worktree" || policy.maxRepairAttempts < 1) return { result };
31780
+ const repairContext = await this.buildCoworkRepairContext(result.error, snapshot, cwd);
31781
+ if (!repairContext.hasChangedFiles) return { result };
31782
+ let current = result;
31783
+ let repairReason = `write attempt failed on ${member.providerProfile}; retried same provider after reading changed files`;
31784
+ for (let attempt = 1; attempt <= policy.maxRepairAttempts; attempt++) {
31785
+ const repairInput = {
31786
+ ...input2,
31787
+ task: buildCoworkRepairTask(input2.task, current.error, attempt),
31788
+ context: [input2.context, repairContext.text].filter(Boolean).join("\n\n") || void 0
31789
+ };
31790
+ current = await this.executeCoworkMemberAttempt(member, selection, member.permissionOverlay, repairInput, cwd, context, `cowork:${context.groupId}:${member.memberId}:repair:${attempt}`);
31791
+ if (current.ok) return { result: current, repairReason };
31792
+ if (context.signal.aborted) return { result: current, repairReason };
31793
+ }
31794
+ return { result: current, repairReason };
31795
+ }
31796
+ async executeCoworkMemberAttempt(member, selection, overlay, input2, cwd, context, sessionScope) {
31797
+ const providerProfile = selection.provider;
31798
+ const decoratedCoworkAgent = this.decorateCoworkMemberAgent(member.agent, member.mode, overlay);
31799
+ const coworkAgent2 = { ...decoratedCoworkAgent, provider: { ...decoratedCoworkAgent.provider, profile: providerProfile } };
31800
+ const runtimeAgent = this.decorateChildAgent(coworkAgent2);
31801
+ let model = "";
31802
+ try {
31803
+ model = this.resolveInvocationBackend(providerProfile, { model: selection.model, modelProfileName: selection.modelProfileName }, runtimeAgent).model;
31804
+ } catch (error) {
31805
+ return { ok: false, error: `Agent invocation failed: ${errorMessage(error)}`, agentName: runtimeAgent.name, providerProfile };
31806
+ }
31807
+ this.events.emit({
31808
+ type: "cowork.member.started",
31809
+ rootSessionId: this.id,
31810
+ parentInvocationId: context.parent.parentInvocationId,
31811
+ groupId: context.groupId,
31812
+ memberId: member.memberId,
31813
+ agent: member.agent.name,
31814
+ provider: providerProfile,
31815
+ model,
31816
+ mode: member.mode,
31817
+ ts: now()
31818
+ });
31819
+ return this.executeChildAgentSession(input2, coworkAgent2, context.parent, {
31820
+ sessionScope,
31821
+ signal: context.signal,
31822
+ writeScope: member.writeScope,
31823
+ cwd
31824
+ });
31825
+ }
31826
+ compileCoworkOverlay(member, providerProfile) {
31827
+ return compileCoworkPermissionOverlay({
31828
+ config: this.#options.config.cowork,
31829
+ agent: member.agent,
31830
+ providerProfile,
31831
+ mode: member.mode
31832
+ });
31833
+ }
31834
+ async buildCoworkRepairContext(error, snapshot, cwd) {
31835
+ const summary = snapshot ? await diffWorkspaceSnapshot(snapshot).catch(() => void 0) : void 0;
31836
+ const files = summary?.files ?? [];
31837
+ const chunks = [
31838
+ "Cowork write repair context:",
31839
+ "- Do not switch providers for this write repair.",
31840
+ "- Re-read the current workspace state below and repair from it.",
31841
+ "",
31842
+ "Failure:",
31843
+ error,
31844
+ "",
31845
+ files.length ? "Changed files since the failed attempt:" : "No changed files were detected after the failed attempt."
31846
+ ];
31847
+ for (const file of files) chunks.push(`- ${file.path} (${file.kind}, +${file.addedLines}/-${file.removedLines})`);
31848
+ if (summary?.diff) chunks.push("", "Diff since original snapshot:", preview(summary.diff, 6e3));
31849
+ if (this.#options.config.cowork.fallback.write.readChangedFilesBeforeRepair) {
31850
+ const fileContents = await readCoworkRepairFiles(cwd, files.map((file) => file.path));
31851
+ if (fileContents.length) chunks.push("", "Current changed file contents:", ...fileContents);
31852
+ }
31853
+ return { text: chunks.join("\n"), hasChangedFiles: files.length > 0 };
31854
+ }
30470
31855
  async createCoworkWriterSnapshot(cwd) {
30471
31856
  try {
30472
31857
  return await createWorkspaceSnapshot(cwd);
@@ -30493,8 +31878,8 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
30493
31878
  }
30494
31879
  }
30495
31880
  async createCoworkIsolatedWorkspace(groupId, memberId) {
30496
- const root = await mkdtemp(path33.join(os15.tmpdir(), `demian-cowork-${groupId}-${memberId}-`));
30497
- const cwd = path33.join(root, "workspace");
31881
+ const root = await mkdtemp(path34.join(os15.tmpdir(), `demian-cowork-${groupId}-${memberId}-`));
31882
+ const cwd = path34.join(root, "workspace");
30498
31883
  await cp(this.#options.cwd, cwd, {
30499
31884
  recursive: true,
30500
31885
  filter: (source) => shouldCopyIntoCoworkWorkspace(this.#options.cwd, source)
@@ -30570,11 +31955,28 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
30570
31955
  if (denied?.includes(target.name)) return { ok: false, error: `Agent ${parent.parentAgent.name} is configured not to cowork with ${target.name}.` };
30571
31956
  const maxDepth = parent.parentAgent.delegation?.maxDepth ?? this.#options.config.delegation.maxDepth;
30572
31957
  if (parent.agentPath.length > maxDepth) return { ok: false, error: `Max delegation depth (${maxDepth}) exceeded.` };
31958
+ let assignment;
31959
+ try {
31960
+ assignment = resolveCoworkAssignment({
31961
+ member,
31962
+ agent: target,
31963
+ config: this.#options.config.cowork,
31964
+ defaultProvider: this.#options.providerName,
31965
+ availability: this.#coworkProviderAvailability
31966
+ });
31967
+ } catch (error) {
31968
+ return { ok: false, error: errorMessage(error) };
31969
+ }
30573
31970
  group.push({
30574
31971
  memberId,
30575
31972
  input: member,
30576
31973
  agent: target,
30577
- providerProfile: target.provider?.profile ?? this.#options.providerName,
31974
+ providerProfile: assignment.providerProfile,
31975
+ model: assignment.model,
31976
+ modelProfileName: assignment.modelProfileName,
31977
+ remainingProviders: assignment.remainingProviders,
31978
+ remainingProviderSelections: assignment.remainingProviderSelections,
31979
+ permissionOverlay: assignment.permissionOverlay,
30578
31980
  mode,
30579
31981
  writeScope,
30580
31982
  isolated: mode === "write" && strategy === "multi-patch" && this.#options.config.cowork.writerIsolation === "snapshot-diff",
@@ -30636,24 +32038,54 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
30636
32038
  systemPrompt: [agent.systemPrompt, this.callableCatalogPrompt(agent)].filter(Boolean).join("\n\n")
30637
32039
  });
30638
32040
  }
30639
- decorateCoworkMemberAgent(agent, mode) {
30640
- if (mode === "write") return this.withPermissionPreset(agent);
32041
+ decorateCoworkMemberAgent(agent, mode, overlay) {
32042
+ const overlayPermissions = overlay?.rules ?? [];
32043
+ const overlayDefaultDecision = overlay?.defaultDecision ?? agent.defaultDecision;
32044
+ const systemPrompt = [agent.systemPrompt, coworkMemberToolBoundaryPrompt(mode)].join("\n\n");
32045
+ if (mode === "write") {
32046
+ return this.withPermissionPreset({
32047
+ ...agent,
32048
+ systemPrompt,
32049
+ permissions: [...agent.permissions, ...overlayPermissions],
32050
+ defaultDecision: overlayDefaultDecision
32051
+ });
32052
+ }
30641
32053
  const readTools = /* @__PURE__ */ new Set(["read_file", "grep", "glob", "web_search", "update_plan", "report_progress"]);
30642
32054
  const denyTools = ["write_file", "edit_file", "bash", "claudecode.Edit", "claudecode.Write", "claudecode.MultiEdit", "claudecode.Bash"];
30643
32055
  return this.withPermissionPreset({
30644
32056
  ...agent,
30645
- provider: agent.provider?.profile === "claudecode" && agent.name === "claudecode-explorer" ? { ...agent.provider, permissionProfile: "explore" } : agent.provider,
32057
+ systemPrompt,
30646
32058
  tools: agent.tools.filter((tool) => readTools.has(tool)),
30647
32059
  permissions: [
30648
32060
  ...agent.permissions.filter((rule) => !denyTools.includes(rule.tool)),
32061
+ ...overlayPermissions.filter((rule) => !denyTools.includes(rule.tool)),
30649
32062
  ...denyTools.map((tool) => ({ tool, decision: "deny", reason: "cowork read-only member" }))
30650
32063
  ],
30651
- defaultDecision: "deny"
32064
+ defaultDecision: overlayDefaultDecision === "allow" ? "allow" : "deny"
30652
32065
  });
30653
32066
  }
30654
32067
  withPermissionPreset(agent) {
30655
32068
  return applyPermissionPresetToAgent(agent, this.#permissionPreset);
30656
32069
  }
32070
+ autoPermissionForBackend(backend, cwd, signal) {
32071
+ if (this.#permissionPreset !== "auto") return void 0;
32072
+ if (backend.kind === "provider") return createAutoPermissionAgent({ provider: backend.provider, model: backend.model, cwd, signal });
32073
+ return this.autoPermissionForExternalRuntime(cwd, signal);
32074
+ }
32075
+ autoPermissionForExternalRuntime(cwd, signal) {
32076
+ if (this.#permissionPreset !== "auto") return void 0;
32077
+ const candidates = /* @__PURE__ */ new Set([this.#options.providerName, this.#options.config.defaultProvider, ...Object.keys(this.#options.config.providers)]);
32078
+ for (const profileName of candidates) {
32079
+ try {
32080
+ const providerConfig = resolveProviderConfig(this.#options.config, profileName);
32081
+ const runtime = resolveProviderRuntimeConfig(this.#options.config, { providerName: profileName });
32082
+ const backend = this.#options.resolveExecutionBackend ? this.#options.resolveExecutionBackend(profileName, providerConfig, runtime.model) : this.#options.resolveProvider ? { kind: "provider", ...this.#options.resolveProvider(profileName, providerConfig, runtime.model) } : resolveExecutionBackend(providerConfig, { model: runtime.model, config: this.#options.config });
32083
+ if (backend.kind === "provider") return createAutoPermissionAgent({ provider: backend.provider, model: backend.model, cwd, signal });
32084
+ } catch {
32085
+ }
32086
+ }
32087
+ return void 0;
32088
+ }
30657
32089
  callableCatalogPrompt(caller) {
30658
32090
  const allowed = caller.delegation?.allowedAgents;
30659
32091
  const denied = new Set(caller.delegation?.deniedAgents ?? []);
@@ -30688,10 +32120,13 @@ Writer changed files outside writeScope: ${outOfScope.join(", ")}` : result.erro
30688
32120
  }
30689
32121
  return lines.join("\n");
30690
32122
  }
30691
- resolveInvocationBackend(profileName, modelOverride, agent) {
30692
- const runtime = resolveProviderRuntimeConfig(this.#options.config, { providerName: profileName, model: modelOverride });
32123
+ resolveInvocationBackend(profileName, modelSelection, agent) {
32124
+ const modelOverride = typeof modelSelection === "string" ? modelSelection : modelSelection?.model;
32125
+ const modelProfileName = typeof modelSelection === "string" ? void 0 : modelSelection?.modelProfileName;
32126
+ const runtime = resolveProviderRuntimeConfig(this.#options.config, { providerName: profileName, model: modelOverride, modelProfileName });
30693
32127
  const providerConfig = runtime.providerConfig;
30694
- if (this.#options.resolveExecutionBackend) return this.#options.resolveExecutionBackend(profileName, providerConfig, modelOverride);
32128
+ const selectedModel = modelOverride ?? (modelProfileName ? runtime.model : void 0);
32129
+ if (this.#options.resolveExecutionBackend) return this.#options.resolveExecutionBackend(profileName, providerConfig, selectedModel);
30695
32130
  if (this.#options.resolveProvider) {
30696
32131
  const resolved = this.#options.resolveProvider(profileName, providerConfig, runtime.model);
30697
32132
  return { kind: "provider", ...resolved };
@@ -30798,17 +32233,17 @@ function findDependencyCycle(group) {
30798
32233
  const byId = new Map(group.map((member) => [member.memberId, member]));
30799
32234
  const visiting = /* @__PURE__ */ new Set();
30800
32235
  const visited = /* @__PURE__ */ new Set();
30801
- const path36 = [];
32236
+ const path37 = [];
30802
32237
  const visit = (id) => {
30803
- if (visiting.has(id)) return [...path36.slice(path36.indexOf(id)), id];
32238
+ if (visiting.has(id)) return [...path37.slice(path37.indexOf(id)), id];
30804
32239
  if (visited.has(id)) return void 0;
30805
32240
  visiting.add(id);
30806
- path36.push(id);
32241
+ path37.push(id);
30807
32242
  for (const dep of byId.get(id)?.dependsOn ?? []) {
30808
32243
  const cycle = visit(dep);
30809
32244
  if (cycle) return cycle;
30810
32245
  }
30811
- path36.pop();
32246
+ path37.pop();
30812
32247
  visiting.delete(id);
30813
32248
  visited.add(id);
30814
32249
  return void 0;
@@ -30849,7 +32284,7 @@ function scopeStaticPrefix(pattern) {
30849
32284
  return prefix.replace(/\/+$/, "").split("/").filter(Boolean).join("/") || ".";
30850
32285
  }
30851
32286
  function shouldCopyIntoCoworkWorkspace(root, source) {
30852
- const relative = path33.relative(root, source).split(path33.sep).join("/");
32287
+ const relative = path34.relative(root, source).split(path34.sep).join("/");
30853
32288
  if (!relative) return true;
30854
32289
  const parts = relative.split("/");
30855
32290
  return !parts.includes(".git") && !parts.includes("node_modules") && !parts.includes(".demian");
@@ -30980,8 +32415,14 @@ function summarizeCoworkResult(goal, outcomes) {
30980
32415
  }
30981
32416
  return lines.join("\n");
30982
32417
  }
30983
- function coworkMemberSummary(text, audit) {
32418
+ function coworkMemberSummary(text, audit, attempt) {
30984
32419
  const lines = [text];
32420
+ if (attempt?.fallbackReason) {
32421
+ lines.push("", `Fallback: ${attempt.fallbackReason}`);
32422
+ }
32423
+ if (attempt?.repairReason) {
32424
+ lines.push("", `Repair: ${attempt.repairReason}`);
32425
+ }
30985
32426
  if (audit?.summary?.files.length) {
30986
32427
  lines.push("", `Workspace changes: ${audit.summary.files.map((file) => file.path).join(", ")}`);
30987
32428
  }
@@ -31002,6 +32443,16 @@ function externalMainRuntimePrompt() {
31002
32443
  "- Do not announce that delegate_agent or cowork is unavailable unless the user explicitly requested one of those tools."
31003
32444
  ].join("\n");
31004
32445
  }
32446
+ function coworkMemberToolBoundaryPrompt(mode) {
32447
+ const capability = mode === "write" ? "read/write tools allowed by the delegated writeScope" : "read-only inspection tools";
32448
+ return [
32449
+ "Cowork member tool boundary:",
32450
+ "- You are already running inside a Demian cowork group; do not try to call cowork or delegate_agent from this member.",
32451
+ `- Use only the ${capability} exposed by your current runtime.`,
32452
+ "- When the provider is Claude Code, Demian maps workspace capabilities through Claude Code native tools such as Read, Grep, Glob, LS, Edit, MultiEdit, Write, and Bash according to the permission overlay.",
32453
+ "- Do not report missing cowork/delegate_agent as a blocker; report a blocker only when the delegated workspace capability itself is missing."
32454
+ ].join("\n");
32455
+ }
31005
32456
  function coworkAuditArtifact(audit) {
31006
32457
  return {
31007
32458
  type: "workspace-diff-audit",
@@ -31030,6 +32481,80 @@ function formatCoworkResult(result) {
31030
32481
  )
31031
32482
  ].join("\n");
31032
32483
  }
32484
+ function pickCoworkNonWriteFallbackPolicy(error, policy) {
32485
+ const kind = coworkFailureKind(error);
32486
+ if (kind === "auth") return policy.onAuthFailure;
32487
+ if (kind === "token-limit") return policy.onTokenLimit;
32488
+ if (kind === "connection") return policy.onConnectionError;
32489
+ return "fail-member";
32490
+ }
32491
+ function coworkFailureKind(error) {
32492
+ const text = error.toLowerCase();
32493
+ if (/\b(401|403)\b|unauthorized|forbidden|auth|api key|apikey|invalid key|invalid_key/.test(text)) return "auth";
32494
+ if (/token|context length|quota|rate limit|too many requests|\b429\b|maximum context|max_tokens|max tokens/.test(text)) return "token-limit";
32495
+ if (/econnreset|econnrefused|etimedout|enotfound|network|fetch failed|connection|socket|timeout|temporarily unavailable|no scripted response/.test(text)) return "connection";
32496
+ return "unknown";
32497
+ }
32498
+ function childExecutionFailureMessage(endReason, finalAnswer) {
32499
+ const answer = finalAnswer?.trim();
32500
+ if (answer) return answer;
32501
+ if (endReason === "max_turns") return "Stopped after reaching maxTurns before producing a verified result.";
32502
+ if (endReason === "tool_loop_limit") return "Stopped after reaching the tool-use safety limit before producing a verified result.";
32503
+ if (endReason === "content_filter") return "Provider safety filter blocked the child agent response.";
32504
+ if (endReason === "cancelled") return "Cancelled.";
32505
+ if (endReason === "error") return "Child agent runtime ended with an error.";
32506
+ return `Child agent ended with ${endReason}.`;
32507
+ }
32508
+ function withCombinedCoworkErrors(result, errors) {
32509
+ if (result.ok || errors.length <= 1) return result;
32510
+ return {
32511
+ ...result,
32512
+ error: `${result.error}
32513
+
32514
+ Cowork provider attempts:
32515
+ ${errors.join("\n")}`
32516
+ };
32517
+ }
32518
+ function providerSelectionLabel2(selection) {
32519
+ const model = selection.modelProfileName ?? selection.model;
32520
+ return model ? `${selection.provider}:${model}` : selection.provider;
32521
+ }
32522
+ function providerSelectionScope(selection) {
32523
+ return providerSelectionLabel2(selection).replace(/[^a-zA-Z0-9_-]/g, "_");
32524
+ }
32525
+ function buildCoworkRepairTask(originalTask, error, attempt) {
32526
+ return [
32527
+ `Repair attempt ${attempt} for a failed cowork write task.`,
32528
+ "",
32529
+ "Original task:",
32530
+ originalTask,
32531
+ "",
32532
+ "Previous failure:",
32533
+ error,
32534
+ "",
32535
+ "Continue from the current workspace state. Do not restart blindly or overwrite unrelated changes. Stay inside the delegated writeScope."
32536
+ ].join("\n");
32537
+ }
32538
+ async function readCoworkRepairFiles(cwd, relativePaths) {
32539
+ const chunks = [];
32540
+ let budget = 12e3;
32541
+ for (const relativePath of relativePaths.slice(0, 8)) {
32542
+ if (budget <= 0) break;
32543
+ const normalized = relativePath.split(path34.sep).join("/");
32544
+ const absolutePath = path34.resolve(cwd, normalized);
32545
+ if (!absolutePath.startsWith(path34.resolve(cwd) + path34.sep) && absolutePath !== path34.resolve(cwd)) continue;
32546
+ try {
32547
+ const content = await readFile17(absolutePath, "utf8");
32548
+ const clipped = preview(content, budget);
32549
+ budget -= clipped.length;
32550
+ chunks.push([`File: ${normalized}`, "```", clipped, "```"].join("\n"));
32551
+ } catch (error) {
32552
+ chunks.push(`File: ${normalized}
32553
+ (unavailable: ${errorMessage(error)})`);
32554
+ }
32555
+ }
32556
+ return chunks;
32557
+ }
31033
32558
  function sumUsage(outcomes) {
31034
32559
  return outcomes.reduce(
31035
32560
  (sum, outcome) => ({
@@ -31067,10 +32592,10 @@ function parseCoworkCommand(input2) {
31067
32592
  const mergeStrategy = consumeMergeStrategy(tokens);
31068
32593
  const task = tokens.join(" ").trim();
31069
32594
  const memberHints = agents.length ? [] : inferInlineCoworkMembers(task, mode);
31070
- const requestedAgents = agents.length ? agents : unique(memberHints.map((member) => member.agent));
32595
+ const requestedAgents = agents.length ? agents : unique3(memberHints.map((member) => member.agent));
31071
32596
  if (!task) return { kind: "message", message: coworkUsage() };
31072
- if (mode && !["read-only", "review", "write"].includes(mode)) {
31073
- return { kind: "message", message: "Usage: /cowork [--agents a,b] [--mode read-only|review|write] [--parallel|--sequential] [--multi-patch] <task>" };
32597
+ if (mode && !["read-only", "review", "write", "manage"].includes(mode)) {
32598
+ return { kind: "message", message: "Usage: /cowork [--agents a,b] [--mode read-only|review|write|manage] [--parallel|--sequential] [--multi-patch] <task>" };
31074
32599
  }
31075
32600
  return {
31076
32601
  kind: "run",
@@ -31080,8 +32605,8 @@ function parseCoworkCommand(input2) {
31080
32605
  }
31081
32606
  function coworkUsage() {
31082
32607
  return [
31083
- "Usage: /cowork [--agents a,b] [--mode read-only|review|write] [--parallel|--sequential] [--multi-patch] <task>",
31084
- "Example: /cowork --agents codex-reviewer,claudecode-explorer \uC774 \uBCC0\uACBD\uC0AC\uD56D\uC744 \uBCD1\uB82C \uAC80\uD1A0\uD574 \uC918"
32608
+ "Usage: /cowork [--agents a,b] [--mode read-only|review|write|manage] [--parallel|--sequential] [--multi-patch] <task>",
32609
+ "Example: /cowork --agents supervisor,reviewer \uC774 \uBCC0\uACBD\uC0AC\uD56D\uC744 \uBCD1\uB82C \uAC80\uD1A0\uD574 \uC918"
31085
32610
  ].join("\n");
31086
32611
  }
31087
32612
  function buildCoworkPrompt(input2) {
@@ -31101,13 +32626,14 @@ function buildCoworkPrompt(input2) {
31101
32626
  ];
31102
32627
  if (input2.agents.length) lines.push(`- Requested cowork agents: ${input2.agents.join(", ")}. Use these exact names in cowork.members[].agent.`);
31103
32628
  if (input2.memberHints?.length) {
31104
- lines.push("- Provider aliases in the user request were normalized to cowork agents.");
32629
+ lines.push("- Provider names in the user request were mapped to cowork agents.");
31105
32630
  lines.push("Suggested cowork member decomposition:");
31106
32631
  for (const member of input2.memberHints) lines.push(` - ${member.agent} (from "${member.alias}"): ${member.task || input2.task}`);
31107
32632
  }
31108
32633
  if (input2.mode === "read-only") lines.push("- Use read-only cowork members; do not request file edits from members.");
31109
32634
  if (input2.mode === "review") lines.push("- Use review/read-only cowork members focused on findings, risks, and missing tests.");
31110
32635
  if (input2.mode === "write") lines.push("- At least one cowork member may write, but every write member must have a narrow writeScope.");
32636
+ if (input2.mode === "manage") lines.push("- Use planning/management cowork members focused on decomposition, coordination, and review gates.");
31111
32637
  if (input2.execution) lines.push(`- Requested execution mode: ${input2.execution}.`);
31112
32638
  if (input2.mergeStrategy === "multi-patch") lines.push("- Use merge.strategy=multi-patch and non-overlapping writeScope values for writer members.");
31113
32639
  return lines.join("\n");
@@ -31134,15 +32660,15 @@ function inferInlineCoworkMembers(task, mode) {
31134
32660
  }
31135
32661
  function coworkAgentForAlias(alias, mode) {
31136
32662
  const value = alias.toLowerCase();
31137
- if (value === "codex") return mode === "review" ? "codex-reviewer" : "codex-planner";
31138
- if (value === "claudecode" || value === "claude-code" || value === "claude_code") return mode === "write" ? "claudecode-builder" : "claudecode-explorer";
32663
+ if (value === "codex") return mode === "review" ? "supervisor" : "planner";
32664
+ if (value === "claudecode" || value === "claude-code" || value === "claude_code") return mode === "write" ? "builder" : "reviewer";
31139
32665
  return "";
31140
32666
  }
31141
32667
  function cleanInlineCoworkMemberTask(value) {
31142
32668
  const text = value.replace(/^[\s,,;;::-]+/, "").replace(/[\s,,;;]+$/, "").replace(/^(?:너는|너는\s+|는|은|에게)\s*/, "").trim();
31143
32669
  return text || void 0;
31144
32670
  }
31145
- function unique(values) {
32671
+ function unique3(values) {
31146
32672
  return [...new Set(values)];
31147
32673
  }
31148
32674
  function consumeExecution(tokens) {
@@ -31475,13 +33001,13 @@ async function askModel(promptUi, selection) {
31475
33001
  }
31476
33002
 
31477
33003
  // src/ui/preferences.ts
31478
- import { mkdir as mkdir14, readFile as readFile17, writeFile as writeFile14 } from "node:fs/promises";
33004
+ import { mkdir as mkdir14, readFile as readFile18, writeFile as writeFile14 } from "node:fs/promises";
31479
33005
  import fs10 from "node:fs";
31480
- import path34 from "node:path";
33006
+ import path35 from "node:path";
31481
33007
  var UiPreferenceStore = class {
31482
33008
  filePath;
31483
33009
  constructor(cwd, filePath) {
31484
- this.filePath = filePath ? path34.resolve(cwd, filePath) : path34.join(cwd, ".demian", "preferences.json");
33010
+ this.filePath = filePath ? path35.resolve(cwd, filePath) : path35.join(cwd, ".demian", "preferences.json");
31485
33011
  }
31486
33012
  async load() {
31487
33013
  if (!fs10.existsSync(this.filePath)) return void 0;
@@ -31499,13 +33025,13 @@ var UiPreferenceStore = class {
31499
33025
  model: selection.model,
31500
33026
  updatedAt: Date.now()
31501
33027
  };
31502
- await mkdir14(path34.dirname(this.filePath), { recursive: true });
33028
+ await mkdir14(path35.dirname(this.filePath), { recursive: true });
31503
33029
  await writeFile14(this.filePath, `${JSON.stringify(file, null, 2)}
31504
33030
  `, { mode: 384 });
31505
33031
  }
31506
33032
  async #read() {
31507
33033
  try {
31508
- return JSON.parse(await readFile17(this.filePath, "utf8"));
33034
+ return JSON.parse(await readFile18(this.filePath, "utf8"));
31509
33035
  } catch {
31510
33036
  return void 0;
31511
33037
  }
@@ -31531,7 +33057,7 @@ async function main(argv = process.argv.slice(2)) {
31531
33057
  return 0;
31532
33058
  }
31533
33059
  if (!flags.noWizard) await runFirstRunWizard().catch(() => void 0);
31534
- const cwd = path35.resolve(flags.cwd ?? process.cwd());
33060
+ const cwd = path36.resolve(flags.cwd ?? process.cwd());
31535
33061
  const config = await loadConfig({ cwd, configPath: flags.configPath });
31536
33062
  const agentName = flags.agent ?? config.defaultAgent;
31537
33063
  const preferenceStore = new UiPreferenceStore(cwd);
@@ -31677,6 +33203,7 @@ async function runPlainTask(options) {
31677
33203
  for (const agentDefinition of Object.values(config.agents ?? {})) agentRegistry.register(agentDefinition, { scope: "user", trusted: true });
31678
33204
  const agent = agentRegistry.get(agentName);
31679
33205
  if (mode === "multi-agent") {
33206
+ const runMaxTurns2 = flags.maxTurns ?? config.maxTurns;
31680
33207
  const root = new RootSession({
31681
33208
  cwd,
31682
33209
  config,
@@ -31684,7 +33211,7 @@ async function runPlainTask(options) {
31684
33211
  providerName,
31685
33212
  model: selection.model,
31686
33213
  hooks: config.hooks,
31687
- maxTurns: flags.maxTurns ?? config.maxTurns,
33214
+ maxTurns: runMaxTurns2,
31688
33215
  yes: flags.yes,
31689
33216
  dryRun: flags.dryRun,
31690
33217
  streaming: flags.streaming ?? config.streaming.enabled,
@@ -31713,6 +33240,7 @@ async function runPlainTask(options) {
31713
33240
  return { history: interactiveHistoryFromRunMessages(result2.messages), lastExternalSessionKey: options.lastExternalSessionKey };
31714
33241
  }
31715
33242
  const runtimeAgent = activeGoal ? withGoalTools(agent) : agent;
33243
+ const runMaxTurns = flags.maxTurns ?? config.maxTurns;
31716
33244
  const runtime = resolveProviderRuntimeConfig(config, selection);
31717
33245
  const backend = resolveExecutionBackend(runtime.providerConfig, {
31718
33246
  model: runtime.model,
@@ -31747,7 +33275,9 @@ async function runPlainTask(options) {
31747
33275
  providerName,
31748
33276
  agent: runtimeAgent,
31749
33277
  hooks: config.hooks,
31750
- maxTurns: flags.maxTurns ?? config.maxTurns,
33278
+ maxTurns: runMaxTurns,
33279
+ countToolUseTurns: false,
33280
+ maxToolUseRounds: relaxedToolUseRoundLimit(runMaxTurns),
31751
33281
  yes: flags.yes,
31752
33282
  dryRun: flags.dryRun,
31753
33283
  streaming: flags.streaming ?? config.streaming.enabled,
@@ -31921,7 +33451,7 @@ Flags:
31921
33451
  --agent <name> general, build, plan, execute, or a configured custom agent
31922
33452
  --provider <name> openai, anthropic, gemini, groq, azure, lmstudio, ollama-local, ollama-cloud, llamacpp, vllm, codex, claudecode
31923
33453
  --model <name> override configured model
31924
- --max-turns <n> maximum model/tool loop turns
33454
+ --max-turns <n> maximum high-level task turns; tool-use rounds use a separate safety limit
31925
33455
  --cwd <path> workspace directory
31926
33456
  --yes, -y auto-allow ask permissions; hard deny and hook block still apply
31927
33457
  --dry-run stop before write_file, edit_file, and bash execution