@mikode13/harness 11.0.1 → 11.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +186 -16
  2. package/dist/agent/domain/agent.d.ts +41 -3
  3. package/dist/agent/domain/agent.js.map +1 -1
  4. package/dist/agent/domain/approval.d.ts +20 -0
  5. package/dist/agent/domain/approval.js.map +1 -1
  6. package/dist/agent/domain/inbox.d.ts +42 -0
  7. package/dist/agent/domain/inbox.js +81 -0
  8. package/dist/agent/domain/inbox.js.map +1 -0
  9. package/dist/agent/domain/oneRunAtATime.d.ts +17 -0
  10. package/dist/agent/domain/oneRunAtATime.js +32 -0
  11. package/dist/agent/domain/oneRunAtATime.js.map +1 -0
  12. package/dist/agent/domain/runContext.d.ts +9 -0
  13. package/dist/agent/domain/runContext.js +5 -0
  14. package/dist/agent/domain/runContext.js.map +1 -1
  15. package/dist/agent/domain/session.d.ts +15 -0
  16. package/dist/agent/domain/session.js +80 -0
  17. package/dist/agent/domain/session.js.map +1 -0
  18. package/dist/engines/claude/infrastructure/model/claudeAgent.d.ts +6 -2
  19. package/dist/engines/claude/infrastructure/model/claudeAgent.js +20 -5
  20. package/dist/engines/claude/infrastructure/model/claudeAgent.js.map +1 -1
  21. package/dist/engines/codex/infrastructure/model/codexAgent.d.ts +5 -1
  22. package/dist/engines/codex/infrastructure/model/codexAgent.js +13 -2
  23. package/dist/engines/codex/infrastructure/model/codexAgent.js.map +1 -1
  24. package/dist/engines/domain/model/backgroundAgents.d.ts +5 -2
  25. package/dist/engines/domain/model/backgroundAgents.js +11 -3
  26. package/dist/engines/domain/model/backgroundAgents.js.map +1 -1
  27. package/dist/engines/domain/model/compactor.d.ts +40 -0
  28. package/dist/engines/domain/model/compactor.js +5 -0
  29. package/dist/engines/domain/model/compactor.js.map +1 -0
  30. package/dist/engines/domain/model/llmAgent.d.ts +53 -2
  31. package/dist/engines/domain/model/llmAgent.js +419 -24
  32. package/dist/engines/domain/model/llmAgent.js.map +1 -1
  33. package/dist/engines/domain/model/recentCompaction.d.ts +39 -0
  34. package/dist/engines/domain/model/recentCompaction.js +119 -0
  35. package/dist/engines/domain/model/recentCompaction.js.map +1 -0
  36. package/dist/engines/domain/model/stepContext.js +3 -0
  37. package/dist/engines/domain/model/stepContext.js.map +1 -1
  38. package/dist/engines/domain/model/triagedCompaction.d.ts +27 -0
  39. package/dist/engines/domain/model/triagedCompaction.js +60 -0
  40. package/dist/engines/domain/model/triagedCompaction.js.map +1 -0
  41. package/dist/factory/infrastructure/agentLLMFactory.d.ts +56 -15
  42. package/dist/factory/infrastructure/agentLLMFactory.js +144 -25
  43. package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
  44. package/dist/index.d.ts +6 -2
  45. package/dist/index.js +3 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/llm/domain/contextTriage.d.ts +26 -0
  48. package/dist/llm/domain/contextTriage.js +3 -0
  49. package/dist/llm/domain/contextTriage.js.map +1 -0
  50. package/dist/llm/domain/llm.d.ts +18 -1
  51. package/dist/llm/domain/llm.js.map +1 -1
  52. package/dist/llm/domain/message.d.ts +18 -1
  53. package/dist/llm/domain/message.js +20 -0
  54. package/dist/llm/domain/message.js.map +1 -1
  55. package/dist/llm/infrastructure/claudeLLMClient.d.ts +4 -5
  56. package/dist/llm/infrastructure/claudeLLMClient.js +39 -13
  57. package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
  58. package/dist/llm/infrastructure/jevContextTriage.d.ts +18 -0
  59. package/dist/llm/infrastructure/jevContextTriage.js +92 -0
  60. package/dist/llm/infrastructure/jevContextTriage.js.map +1 -0
  61. package/dist/llm/infrastructure/openAILLMClient.d.ts +4 -5
  62. package/dist/llm/infrastructure/openAILLMClient.js +20 -9
  63. package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
  64. package/dist/orchestration/domain/model/orchestratorAgent.d.ts +9 -1
  65. package/dist/orchestration/domain/model/orchestratorAgent.js +20 -3
  66. package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
  67. package/dist/recovery/domain/history.d.ts +10 -3
  68. package/dist/recovery/domain/history.js +1 -0
  69. package/dist/recovery/domain/history.js.map +1 -1
  70. package/dist/recovery/domain/recoveryStore.d.ts +14 -2
  71. package/dist/recovery/domain/recoveryStore.js.map +1 -1
  72. package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +21 -2
  73. package/dist/recovery/infrastructure/fileRecoveryStore.js +58 -6
  74. package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -1
  75. package/dist/recovery/infrastructure/folderClaims.d.ts +27 -0
  76. package/dist/recovery/infrastructure/folderClaims.js +124 -0
  77. package/dist/recovery/infrastructure/folderClaims.js.map +1 -0
  78. package/dist/shared/domain/session.d.ts +12 -0
  79. package/dist/shared/domain/session.js +2 -0
  80. package/dist/shared/domain/session.js.map +1 -0
  81. package/dist/tools/domain/preparedCall.d.ts +5 -2
  82. package/dist/tools/domain/preparedCall.js +7 -1
  83. package/dist/tools/domain/preparedCall.js.map +1 -1
  84. package/dist/tools/domain/readRegistry.d.ts +11 -0
  85. package/dist/tools/domain/readRegistry.js.map +1 -1
  86. package/dist/tools/infrastructure/agentAsTool.d.ts +6 -0
  87. package/dist/tools/infrastructure/agentAsTool.js +9 -0
  88. package/dist/tools/infrastructure/agentAsTool.js.map +1 -1
  89. package/dist/tools/infrastructure/commandTool.js +6 -1
  90. package/dist/tools/infrastructure/commandTool.js.map +1 -1
  91. package/dist/tools/infrastructure/exploreTool.js +2 -0
  92. package/dist/tools/infrastructure/exploreTool.js.map +1 -1
  93. package/dist/tools/infrastructure/fileTools.d.ts +2 -0
  94. package/dist/tools/infrastructure/fileTools.js +10 -1
  95. package/dist/tools/infrastructure/fileTools.js.map +1 -1
  96. package/dist/tools/infrastructure/removeWorktreeTool.d.ts +23 -0
  97. package/dist/tools/infrastructure/removeWorktreeTool.js +178 -0
  98. package/dist/tools/infrastructure/removeWorktreeTool.js.map +1 -0
  99. package/dist/tools/infrastructure/subagents.d.ts +3 -1
  100. package/dist/tools/infrastructure/subagents.js +2 -1
  101. package/dist/tools/infrastructure/subagents.js.map +1 -1
  102. package/dist/tools/infrastructure/trackedEdits.js +1 -0
  103. package/dist/tools/infrastructure/trackedEdits.js.map +1 -1
  104. package/dist/tools/infrastructure/workspaceWrites.d.ts +17 -1
  105. package/dist/tools/infrastructure/workspaceWrites.js +48 -17
  106. package/dist/tools/infrastructure/workspaceWrites.js.map +1 -1
  107. package/dist/tools/infrastructure/worktrees.d.ts +49 -0
  108. package/dist/tools/infrastructure/worktrees.js +154 -0
  109. package/dist/tools/infrastructure/worktrees.js.map +1 -0
  110. package/dist/tools/infrastructure/writeCeiling.d.ts +16 -0
  111. package/dist/tools/infrastructure/writeCeiling.js +49 -0
  112. package/dist/tools/infrastructure/writeCeiling.js.map +1 -0
  113. package/dist/tools/infrastructure/writeSession.d.ts +5 -1
  114. package/dist/tools/infrastructure/writeSession.js +7 -3
  115. package/dist/tools/infrastructure/writeSession.js.map +1 -1
  116. package/dist/workflow/domain/workflow.d.ts +3 -0
  117. package/dist/workflow/domain/workflow.js.map +1 -1
  118. package/dist/workflow/domain/workflowRunner.js +8 -2
  119. package/dist/workflow/domain/workflowRunner.js.map +1 -1
  120. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"exploreTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/exploreTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\nimport type { Agent } from '#src/agent/domain/agent';\nimport { RecoverableError, UnrecoverableError } from '#src/shared/domain/errors';\nimport type { DelegatedAnswer, DelegatingTool } from '../domain/preparedCall.ts';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\nimport { defineDelegatingTool } from './defineTool.ts';\nimport { cutAnswer, failureOf, subagentOptions } from './subagents.ts';\n\n/** The most explorers one call sends, so a single call cannot fan out without bound. */\nexport const maxExplorers = 4;\n\n/**\n * Sends one explorer, a new agent that starts from its question alone. What it spends counts in\n * the run's usage, whether it answered or failed; a failure becomes its answer. A cancellation\n * escapes.\n */\nasync function explore(\n\tquestion: string,\n\tsubagent: number,\n\texplorer: () => Agent,\n\tsignal: AbortSignal,\n\tcall: ToolCallContext,\n): Promise<DelegatedAnswer> {\n\t// Within the run that made the call, so the explorer's reads belong to it.\n\tconst options = subagentOptions(call, {\n\t\ttool: 'explore',\n\t\tname: 'explorer',\n\t\tsubagent,\n\t\ttask: question,\n\t\toptions: { signal },\n\t});\n\ttry {\n\t\tconst { response, tokens } = await explorer().run(question, options);\n\t\tcall.spend(tokens, tokens === undefined);\n\t\treturn { text: cutAnswer(response || 'It found nothing to report.') };\n\t} catch (error) {\n\t\t// Whatever it threw once the run was cancelled, it ends as cancelled.\n\t\tsignal.throwIfAborted();\n\t\tif (!(error instanceof RecoverableError || error instanceof UnrecoverableError)) throw error;\n\t\tcall.spend(error.tokens, error.usageUnreported);\n\t\treturn { text: `The explorer failed: ${failureOf(error)}`, failed: true };\n\t}\n}\n\n/**\n * `explore` for the planner and the reviewer: it sends cheaper, read-only explorers, created by\n * `explorer`, to find what an agent needs in the repository, so the agent that called it reads\n * their reports instead of filling its own context with the files.\n */\nexport function createExploreTool(explorer: () => Agent): DelegatingTool {\n\treturn defineDelegatingTool({\n\t\tname: 'explore',\n\t\tdescription: `Sends read-only explorers to find things in the repository for you, one for each question, all at once. They work in the background: keep working meanwhile, and each explorer's report reaches you in a message of its own once it finishes. Give your final answer only once every explorer you sent has reported: an answer while they work is taken as waiting for them, and only the answer you give after the last report is kept. Each explorer starts from nothing but its question, so make each one complete: what to find, and what to report about it, such as which files and lines implement something and how. Ask up to ${String(maxExplorers)} independent questions in one call. Each answer is an explorer's report, not the files: read a file yourself when you need its exact text.`,\n\t\tinput: z.object({\n\t\t\tquestions: z.array(z.string()).describe('One complete, independent question per explorer.'),\n\t\t}),\n\t\tsummarize: ({ questions }) =>\n\t\t\tquestions.length === 1 ? (questions[0] ?? '') : `${String(questions.length)} questions`,\n\t\tdelegate: ({ questions }, _, call) => {\n\t\t\tconst asked = questions.map(question => question.trim());\n\t\t\tif (asked.length === 0 || asked.length > maxExplorers) {\n\t\t\t\treturn Promise.reject(\n\t\t\t\t\tnew Error(`Ask between 1 and ${String(maxExplorers)} questions in one call.`),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (asked.some(question => question === '')) {\n\t\t\t\treturn Promise.reject(new Error('Every question needs some text.'));\n\t\t\t}\n\t\t\treturn Promise.resolve({\n\t\t\t\trisk: 'safe',\n\t\t\t\tstart: signal =>\n\t\t\t\t\tasked.map((question, index) => ({\n\t\t\t\t\t\tname: 'explorer',\n\t\t\t\t\t\tsubagent: index + 1,\n\t\t\t\t\t\ttask: question,\n\t\t\t\t\t\tanswer: explore(question, index + 1, explorer, signal, call),\n\t\t\t\t\t})),\n\t\t\t});\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"exploreTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/exploreTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\nimport type { Agent } from '#src/agent/domain/agent';\nimport { RecoverableError, UnrecoverableError } from '#src/shared/domain/errors';\nimport type { DelegatedAnswer, DelegatingTool } from '../domain/preparedCall.ts';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\nimport { defineDelegatingTool } from './defineTool.ts';\nimport { cutAnswer, failureOf, subagentOptions } from './subagents.ts';\n\n/** The most explorers one call sends, so a single call cannot fan out without bound. */\nexport const maxExplorers = 4;\n\n/**\n * Sends one explorer, a new agent that starts from its question alone. What it spends counts in\n * the run's usage, whether it answered or failed; a failure becomes its answer. A cancellation\n * escapes.\n */\nasync function explore(\n\tquestion: string,\n\tsubagent: number,\n\texplorer: () => Agent,\n\tsignal: AbortSignal,\n\tcall: ToolCallContext,\n): Promise<DelegatedAnswer> {\n\t// Within the run that made the call, so the explorer's reads belong to it.\n\tconst options = subagentOptions(call, {\n\t\ttool: 'explore',\n\t\tname: 'explorer',\n\t\tsubagent,\n\t\ttask: question,\n\t\toptions: { signal },\n\t\t// It only finds and reads.\n\t\twriteCeiling: [],\n\t});\n\ttry {\n\t\tconst { response, tokens } = await explorer().run(question, options);\n\t\tcall.spend(tokens, tokens === undefined);\n\t\treturn { text: cutAnswer(response || 'It found nothing to report.') };\n\t} catch (error) {\n\t\t// Whatever it threw once the run was cancelled, it ends as cancelled.\n\t\tsignal.throwIfAborted();\n\t\tif (!(error instanceof RecoverableError || error instanceof UnrecoverableError)) throw error;\n\t\tcall.spend(error.tokens, error.usageUnreported);\n\t\treturn { text: `The explorer failed: ${failureOf(error)}`, failed: true };\n\t}\n}\n\n/**\n * `explore` for the planner and the reviewer: it sends cheaper, read-only explorers, created by\n * `explorer`, to find what an agent needs in the repository, so the agent that called it reads\n * their reports instead of filling its own context with the files.\n */\nexport function createExploreTool(explorer: () => Agent): DelegatingTool {\n\treturn defineDelegatingTool({\n\t\tname: 'explore',\n\t\tdescription: `Sends read-only explorers to find things in the repository for you, one for each question, all at once. They work in the background: keep working meanwhile, and each explorer's report reaches you in a message of its own once it finishes. Give your final answer only once every explorer you sent has reported: an answer while they work is taken as waiting for them, and only the answer you give after the last report is kept. Each explorer starts from nothing but its question, so make each one complete: what to find, and what to report about it, such as which files and lines implement something and how. Ask up to ${String(maxExplorers)} independent questions in one call. Each answer is an explorer's report, not the files: read a file yourself when you need its exact text.`,\n\t\tinput: z.object({\n\t\t\tquestions: z.array(z.string()).describe('One complete, independent question per explorer.'),\n\t\t}),\n\t\tsummarize: ({ questions }) =>\n\t\t\tquestions.length === 1 ? (questions[0] ?? '') : `${String(questions.length)} questions`,\n\t\tdelegate: ({ questions }, _, call) => {\n\t\t\tconst asked = questions.map(question => question.trim());\n\t\t\tif (asked.length === 0 || asked.length > maxExplorers) {\n\t\t\t\treturn Promise.reject(\n\t\t\t\t\tnew Error(`Ask between 1 and ${String(maxExplorers)} questions in one call.`),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (asked.some(question => question === '')) {\n\t\t\t\treturn Promise.reject(new Error('Every question needs some text.'));\n\t\t\t}\n\t\t\treturn Promise.resolve({\n\t\t\t\trisk: 'safe',\n\t\t\t\tstart: signal =>\n\t\t\t\t\tasked.map((question, index) => ({\n\t\t\t\t\t\tname: 'explorer',\n\t\t\t\t\t\tsubagent: index + 1,\n\t\t\t\t\t\ttask: question,\n\t\t\t\t\t\tanswer: explore(question, index + 1, explorer, signal, call),\n\t\t\t\t\t})),\n\t\t\t});\n\t\t},\n\t});\n}\n"]}
@@ -42,6 +42,8 @@ export interface OpenedWorkspace {
42
42
  readonly writable: boolean;
43
43
  /** The real path of the first root. */
44
44
  readonly base: string;
45
+ /** Where the history is kept, as a real path. */
46
+ readonly stateDirectory: string;
45
47
  /** What a command's checkpoint captures and compares: see `CoveredFiles`. */
46
48
  readonly covered: CoveredFiles;
47
49
  }
@@ -43,7 +43,14 @@ export async function openStore(workspace, directory, logger) {
43
43
  if (!first)
44
44
  throw new InvalidAgentConfigError('A workspace needs at least one root');
45
45
  try {
46
- return await FileRecoveryStore.open({ root: resolve(first.path), directory, logger });
46
+ return await FileRecoveryStore.open({
47
+ root: resolve(first.path),
48
+ directory,
49
+ logger,
50
+ writeRoots: workspace.roots
51
+ .filter(({ access }) => access === 'write')
52
+ .map(({ path }) => resolve(path)),
53
+ });
47
54
  }
48
55
  catch (error) {
49
56
  throw new UnrecoverableError('The workspace history could not be opened', {
@@ -88,9 +95,11 @@ export async function openWorkspace(workspace, logger, { formatter } = {}) {
88
95
  policy,
89
96
  store,
90
97
  ...(fileFormatter ? { formatter: fileFormatter } : {}),
98
+ writeRoots: roots.filter(({ access }) => access === 'write').map(({ path }) => resolve(path)),
91
99
  }),
92
100
  writable: roots.some(({ access }) => access === 'write'),
93
101
  base: root,
102
+ stateDirectory,
94
103
  covered: coveredFiles(roots, policy, logger),
95
104
  };
96
105
  }
@@ -1 +1 @@
1
- {"version":3,"file":"fileTools.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/fileTools.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, realpath } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport type { ToolDefinition } from '#src/llm/domain/tool';\nimport type { RecoveryStore } from '#src/recovery/domain/recoveryStore';\nimport {\n\tdefaultStateDirectory,\n\tFileRecoveryStore,\n} from '#src/recovery/infrastructure/fileRecoveryStore';\nimport { InvalidAgentConfigError, UnrecoverableError } from '#src/shared/domain/errors';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport { describeFailure } from '#src/shared/domain/providerFailure';\nimport type { AccessPolicy, WorkspaceRoot } from '../domain/accessPolicy.ts';\nimport type { Commands, Formatter } from '../domain/commands.ts';\nimport type { AgentTool } from '../domain/preparedCall.ts';\nimport type { Workspace } from '../domain/workspace.ts';\nimport type { CoveredFiles } from './commandCheckpoint.ts';\nimport { createCommandTool } from './commandTool.ts';\nimport { chooseWorkspace } from './createWorkspace.ts';\nimport { createApplyPatchTool, createDeleteFileTool, createTextEditorTool } from './editFormats.ts';\nimport { FileFormatter } from './fileFormatter.ts';\nimport { GitIgnoreRules } from './gitIgnoreRules.ts';\nimport { PolicyWorkspace } from './policyWorkspace.ts';\nimport { RootsAccessPolicy, type SecretRules } from './rootsAccessPolicy.ts';\nimport { createTrackedReadFile, createTrackedReadFiles } from './trackedReadFile.ts';\nimport { createWorkspaceTools } from './workspaceTools.ts';\nimport { WorkspaceWrites } from './workspaceWrites.ts';\n\n/** The folders an agent works on, and what it may do in each. */\nexport interface WorkspaceOptions {\n\t/**\n\t * Each folder `read` or `write`. The first is where a relative path starts, and the one whose\n\t * history of runs is kept; a write to another root is recorded in it too.\n\t */\n\troots: WorkspaceRoot[];\n\t/** More files to keep closed as secrets, and exact files to open. */\n\tsecrets?: SecretRules;\n\t/**\n\t * Where the history of runs is kept, relative to the working directory if not absolute.\n\t * Defaults to the platform's state directory. No tool may reach it, even inside a root.\n\t */\n\tstateDirectory?: string;\n}\n\ndeclare const fileToolBrand: unique symbol;\n\n/**\n * A tool the harness builds for files, from `createFileTools`. It goes to an agent from\n * `createLLMAgent` beside the consumer's own tools; what it does inside is the harness's.\n */\nexport type FileTool = ToolDefinition & { readonly [fileToolBrand]: true };\n\n/** What the file tools of one workspace share: one policy, one history, one record of writes. */\nexport interface OpenedWorkspace {\n\treadonly roots: WorkspaceRoot[];\n\treadonly policy: AccessPolicy;\n\treadonly store: RecoveryStore;\n\t/** The read side, with the policy's word on every path. */\n\treadonly read: Workspace;\n\treadonly writes: WorkspaceWrites;\n\treadonly writable: boolean;\n\t/** The real path of the first root. */\n\treadonly base: string;\n\t/** What a command's checkpoint captures and compares: see `CoveredFiles`. */\n\treadonly covered: CoveredFiles;\n}\n\n/**\n * Where a workspace's history is kept, as one real path: made if missing, then resolved, links\n * included. The store and the access policy must name the same folder, or a tool could reach the\n * history the policy means to protect. A relative path is from the working directory.\n *\n * @throws {UnrecoverableError} when the folder cannot be made or reached.\n */\nexport async function stateDirectoryOf({ stateDirectory }: WorkspaceOptions): Promise<string> {\n\tconst directory = resolve(stateDirectory ?? defaultStateDirectory());\n\ttry {\n\t\tawait mkdir(directory, { recursive: true, mode: 0o700 });\n\t\treturn await realpath(directory);\n\t} catch (error) {\n\t\tthrow new UnrecoverableError('The folder for the workspace history could not be made', {\n\t\t\tcause: describeFailure(error),\n\t\t});\n\t}\n}\n\n/**\n * The recovery store of a workspace, at its first root.\n *\n * @throws {InvalidAgentConfigError} when the workspace has no root.\n * @throws {UnrecoverableError} when the store cannot be opened.\n */\nexport async function openStore(\n\tworkspace: WorkspaceOptions,\n\tdirectory: string,\n\tlogger: ILogger,\n): Promise<RecoveryStore> {\n\tconst [first] = workspace.roots;\n\tif (!first) throw new InvalidAgentConfigError('A workspace needs at least one root');\n\ttry {\n\t\treturn await FileRecoveryStore.open({ root: resolve(first.path), directory, logger });\n\t} catch (error) {\n\t\tthrow new UnrecoverableError('The workspace history could not be opened', {\n\t\t\tcause: describeFailure(error),\n\t\t});\n\t}\n}\n\n/**\n * Opens what the file tools of one workspace share. With `formatter`, its edits format each file\n * they write.\n *\n * @throws {InvalidAgentConfigError} for a workspace with no root, or an invalid formatter.\n */\nexport async function openWorkspace(\n\tworkspace: WorkspaceOptions,\n\tlogger: ILogger,\n\t{ formatter }: { formatter?: Formatter } = {},\n): Promise<OpenedWorkspace> {\n\tconst { roots, secrets } = workspace;\n\tconst stateDirectory = await stateDirectoryOf(workspace);\n\tconst policy = await RootsAccessPolicy.create({\n\t\troots,\n\t\t...(secrets ? { secrets } : {}),\n\t\tignoreRules: new GitIgnoreRules(),\n\t\t// The history holds the source a run replaced: no tool may read or write it.\n\t\tprotectedPaths: [stateDirectory],\n\t});\n\tconst store = await openStore(workspace, stateDirectory, logger);\n\t// Real, as the policy's paths are, so a file's path can be judged by name.\n\tconst root = await realpath(resolve(roots[0]?.path ?? '.'));\n\tconst inner = await chooseWorkspace({\n\t\troot,\n\t\tlogger,\n\t\t// Left out before a search counts or stores a match, so no total, limit or error can tell\n\t\t// that a secret matched. The policy still decides each path after it.\n\t\thidden: path => policy.hidesFromReading(join(root, path)),\n\t});\n\tconst read = new PolicyWorkspace({ inner, policy });\n\tconst fileFormatter =\n\t\tformatter && (await FileFormatter.create({ formatter, policy, base: root }));\n\treturn {\n\t\troots,\n\t\tpolicy,\n\t\tstore,\n\t\tread,\n\t\twrites: new WorkspaceWrites({\n\t\t\tpolicy,\n\t\t\tstore,\n\t\t\t...(fileFormatter ? { formatter: fileFormatter } : {}),\n\t\t}),\n\t\twritable: roots.some(({ access }) => access === 'write'),\n\t\tbase: root,\n\t\tcovered: coveredFiles(roots, policy, logger),\n\t};\n}\n\n/**\n * Lists every file of the `write` roots that `.gitignore` does not exclude, less what the policy\n * closes to writing by name, before anything is read. Each root is listed by its own program,\n * opened on the first listing.\n */\nfunction coveredFiles(\n\troots: WorkspaceRoot[],\n\tpolicy: RootsAccessPolicy,\n\tlogger: ILogger,\n): CoveredFiles {\n\tlet listers: Promise<{ root: string; workspace: Workspace }[]> | undefined;\n\tconst open = () =>\n\t\tPromise.all(\n\t\t\troots\n\t\t\t\t.filter(({ access }) => access === 'write')\n\t\t\t\t.map(async ({ path }) => {\n\t\t\t\t\tconst root = await realpath(resolve(path));\n\t\t\t\t\tconst workspace = await chooseWorkspace({\n\t\t\t\t\t\troot,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t\thidden: relativePath => policy.closedToWriting(join(root, relativePath)),\n\t\t\t\t\t});\n\t\t\t\t\treturn { root, workspace };\n\t\t\t\t}),\n\t\t);\n\n\treturn async signal => {\n\t\tlisters ??= open().catch((error: unknown) => {\n\t\t\t// Not kept: the next command tries again.\n\t\t\tlisters = undefined;\n\t\t\tthrow error;\n\t\t});\n\t\tconst files = new Set<string>();\n\t\tfor (const { root, workspace } of await listers) {\n\t\t\tconst listed = await workspace.listFiles({ limit: Number.MAX_SAFE_INTEGER }, signal);\n\t\t\tfor (const path of listed.files) files.add(join(root, path));\n\t\t}\n\t\treturn [...files];\n\t};\n}\n\n/**\n * `runCommand` over an opened workspace: see `createCommandTool`.\n *\n * @throws {InvalidAgentConfigError} for an invalid configuration, or on Windows.\n */\nexport function commandToolFor(workspace: OpenedWorkspace, commands: Commands): Promise<AgentTool> {\n\tconst { policy, writes, covered, base } = workspace;\n\treturn createCommandTool({ commands, policy, writes, covered, base });\n}\n\n/**\n * The file tools of a provider's executor, over one workspace. Every one reads through the access\n * policy, and an edit starts only from a version the conversation read.\n *\n * - OpenAI: `listFiles`, `searchText`, `readFile`, `readFiles`, and its native `apply_patch`.\n * - Anthropic: `listFiles`, `searchText`, its native text editor, whose `view` reads, and\n * `delete_file`.\n *\n * Without a `write` root they only read: `readFile` and `readFiles` in place of the editor and\n * the patch.\n */\nexport function fileToolsFor(\n\tprovider: 'anthropic' | 'openai',\n\tworkspace: OpenedWorkspace,\n): AgentTool[] {\n\tconst { policy, read, writes, writable } = workspace;\n\tconst [listFiles, searchText] = createWorkspaceTools(read).filter(\n\t\ttool => tool.name === 'listFiles' || tool.name === 'searchText',\n\t);\n\tconst finding: AgentTool[] = [listFiles, searchText].filter(tool => tool !== undefined);\n\tconst reading = [createTrackedReadFile(policy), createTrackedReadFiles(policy)];\n\tif (!writable) return [...finding, ...reading];\n\tif (provider === 'openai') return [...finding, ...reading, createApplyPatchTool(writes)];\n\treturn [\n\t\t...finding,\n\t\tcreateTextEditorTool({ writes, policy, workspace: read }),\n\t\tcreateDeleteFileTool(writes),\n\t];\n}\n"]}
1
+ {"version":3,"file":"fileTools.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/fileTools.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, realpath } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport type { ToolDefinition } from '#src/llm/domain/tool';\nimport type { RecoveryStore } from '#src/recovery/domain/recoveryStore';\nimport {\n\tdefaultStateDirectory,\n\tFileRecoveryStore,\n} from '#src/recovery/infrastructure/fileRecoveryStore';\nimport { InvalidAgentConfigError, UnrecoverableError } from '#src/shared/domain/errors';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport { describeFailure } from '#src/shared/domain/providerFailure';\nimport type { AccessPolicy, WorkspaceRoot } from '../domain/accessPolicy.ts';\nimport type { Commands, Formatter } from '../domain/commands.ts';\nimport type { AgentTool } from '../domain/preparedCall.ts';\nimport type { Workspace } from '../domain/workspace.ts';\nimport type { CoveredFiles } from './commandCheckpoint.ts';\nimport { createCommandTool } from './commandTool.ts';\nimport { chooseWorkspace } from './createWorkspace.ts';\nimport { createApplyPatchTool, createDeleteFileTool, createTextEditorTool } from './editFormats.ts';\nimport { FileFormatter } from './fileFormatter.ts';\nimport { GitIgnoreRules } from './gitIgnoreRules.ts';\nimport { PolicyWorkspace } from './policyWorkspace.ts';\nimport { RootsAccessPolicy, type SecretRules } from './rootsAccessPolicy.ts';\nimport { createTrackedReadFile, createTrackedReadFiles } from './trackedReadFile.ts';\nimport { createWorkspaceTools } from './workspaceTools.ts';\nimport { WorkspaceWrites } from './workspaceWrites.ts';\n\n/** The folders an agent works on, and what it may do in each. */\nexport interface WorkspaceOptions {\n\t/**\n\t * Each folder `read` or `write`. The first is where a relative path starts, and the one whose\n\t * history of runs is kept; a write to another root is recorded in it too.\n\t */\n\troots: WorkspaceRoot[];\n\t/** More files to keep closed as secrets, and exact files to open. */\n\tsecrets?: SecretRules;\n\t/**\n\t * Where the history of runs is kept, relative to the working directory if not absolute.\n\t * Defaults to the platform's state directory. No tool may reach it, even inside a root.\n\t */\n\tstateDirectory?: string;\n}\n\ndeclare const fileToolBrand: unique symbol;\n\n/**\n * A tool the harness builds for files, from `createFileTools`. It goes to an agent from\n * `createLLMAgent` beside the consumer's own tools; what it does inside is the harness's.\n */\nexport type FileTool = ToolDefinition & { readonly [fileToolBrand]: true };\n\n/** What the file tools of one workspace share: one policy, one history, one record of writes. */\nexport interface OpenedWorkspace {\n\treadonly roots: WorkspaceRoot[];\n\treadonly policy: AccessPolicy;\n\treadonly store: RecoveryStore;\n\t/** The read side, with the policy's word on every path. */\n\treadonly read: Workspace;\n\treadonly writes: WorkspaceWrites;\n\treadonly writable: boolean;\n\t/** The real path of the first root. */\n\treadonly base: string;\n\t/** Where the history is kept, as a real path. */\n\treadonly stateDirectory: string;\n\t/** What a command's checkpoint captures and compares: see `CoveredFiles`. */\n\treadonly covered: CoveredFiles;\n}\n\n/**\n * Where a workspace's history is kept, as one real path: made if missing, then resolved, links\n * included. The store and the access policy must name the same folder, or a tool could reach the\n * history the policy means to protect. A relative path is from the working directory.\n *\n * @throws {UnrecoverableError} when the folder cannot be made or reached.\n */\nexport async function stateDirectoryOf({ stateDirectory }: WorkspaceOptions): Promise<string> {\n\tconst directory = resolve(stateDirectory ?? defaultStateDirectory());\n\ttry {\n\t\tawait mkdir(directory, { recursive: true, mode: 0o700 });\n\t\treturn await realpath(directory);\n\t} catch (error) {\n\t\tthrow new UnrecoverableError('The folder for the workspace history could not be made', {\n\t\t\tcause: describeFailure(error),\n\t\t});\n\t}\n}\n\n/**\n * The recovery store of a workspace, at its first root.\n *\n * @throws {InvalidAgentConfigError} when the workspace has no root.\n * @throws {UnrecoverableError} when the store cannot be opened.\n */\nexport async function openStore(\n\tworkspace: WorkspaceOptions,\n\tdirectory: string,\n\tlogger: ILogger,\n): Promise<RecoveryStore> {\n\tconst [first] = workspace.roots;\n\tif (!first) throw new InvalidAgentConfigError('A workspace needs at least one root');\n\ttry {\n\t\treturn await FileRecoveryStore.open({\n\t\t\troot: resolve(first.path),\n\t\t\tdirectory,\n\t\t\tlogger,\n\t\t\twriteRoots: workspace.roots\n\t\t\t\t.filter(({ access }) => access === 'write')\n\t\t\t\t.map(({ path }) => resolve(path)),\n\t\t});\n\t} catch (error) {\n\t\tthrow new UnrecoverableError('The workspace history could not be opened', {\n\t\t\tcause: describeFailure(error),\n\t\t});\n\t}\n}\n\n/**\n * Opens what the file tools of one workspace share. With `formatter`, its edits format each file\n * they write.\n *\n * @throws {InvalidAgentConfigError} for a workspace with no root, or an invalid formatter.\n */\nexport async function openWorkspace(\n\tworkspace: WorkspaceOptions,\n\tlogger: ILogger,\n\t{ formatter }: { formatter?: Formatter } = {},\n): Promise<OpenedWorkspace> {\n\tconst { roots, secrets } = workspace;\n\tconst stateDirectory = await stateDirectoryOf(workspace);\n\tconst policy = await RootsAccessPolicy.create({\n\t\troots,\n\t\t...(secrets ? { secrets } : {}),\n\t\tignoreRules: new GitIgnoreRules(),\n\t\t// The history holds the source a run replaced: no tool may read or write it.\n\t\tprotectedPaths: [stateDirectory],\n\t});\n\tconst store = await openStore(workspace, stateDirectory, logger);\n\t// Real, as the policy's paths are, so a file's path can be judged by name.\n\tconst root = await realpath(resolve(roots[0]?.path ?? '.'));\n\tconst inner = await chooseWorkspace({\n\t\troot,\n\t\tlogger,\n\t\t// Left out before a search counts or stores a match, so no total, limit or error can tell\n\t\t// that a secret matched. The policy still decides each path after it.\n\t\thidden: path => policy.hidesFromReading(join(root, path)),\n\t});\n\tconst read = new PolicyWorkspace({ inner, policy });\n\tconst fileFormatter =\n\t\tformatter && (await FileFormatter.create({ formatter, policy, base: root }));\n\treturn {\n\t\troots,\n\t\tpolicy,\n\t\tstore,\n\t\tread,\n\t\twrites: new WorkspaceWrites({\n\t\t\tpolicy,\n\t\t\tstore,\n\t\t\t...(fileFormatter ? { formatter: fileFormatter } : {}),\n\t\t\twriteRoots: roots.filter(({ access }) => access === 'write').map(({ path }) => resolve(path)),\n\t\t}),\n\t\twritable: roots.some(({ access }) => access === 'write'),\n\t\tbase: root,\n\t\tstateDirectory,\n\t\tcovered: coveredFiles(roots, policy, logger),\n\t};\n}\n\n/**\n * Lists every file of the `write` roots that `.gitignore` does not exclude, less what the policy\n * closes to writing by name, before anything is read. Each root is listed by its own program,\n * opened on the first listing.\n */\nfunction coveredFiles(\n\troots: WorkspaceRoot[],\n\tpolicy: RootsAccessPolicy,\n\tlogger: ILogger,\n): CoveredFiles {\n\tlet listers: Promise<{ root: string; workspace: Workspace }[]> | undefined;\n\tconst open = () =>\n\t\tPromise.all(\n\t\t\troots\n\t\t\t\t.filter(({ access }) => access === 'write')\n\t\t\t\t.map(async ({ path }) => {\n\t\t\t\t\tconst root = await realpath(resolve(path));\n\t\t\t\t\tconst workspace = await chooseWorkspace({\n\t\t\t\t\t\troot,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t\thidden: relativePath => policy.closedToWriting(join(root, relativePath)),\n\t\t\t\t\t});\n\t\t\t\t\treturn { root, workspace };\n\t\t\t\t}),\n\t\t);\n\n\treturn async signal => {\n\t\tlisters ??= open().catch((error: unknown) => {\n\t\t\t// Not kept: the next command tries again.\n\t\t\tlisters = undefined;\n\t\t\tthrow error;\n\t\t});\n\t\tconst files = new Set<string>();\n\t\tfor (const { root, workspace } of await listers) {\n\t\t\tconst listed = await workspace.listFiles({ limit: Number.MAX_SAFE_INTEGER }, signal);\n\t\t\tfor (const path of listed.files) files.add(join(root, path));\n\t\t}\n\t\treturn [...files];\n\t};\n}\n\n/**\n * `runCommand` over an opened workspace: see `createCommandTool`.\n *\n * @throws {InvalidAgentConfigError} for an invalid configuration, or on Windows.\n */\nexport function commandToolFor(workspace: OpenedWorkspace, commands: Commands): Promise<AgentTool> {\n\tconst { policy, writes, covered, base } = workspace;\n\treturn createCommandTool({ commands, policy, writes, covered, base });\n}\n\n/**\n * The file tools of a provider's executor, over one workspace. Every one reads through the access\n * policy, and an edit starts only from a version the conversation read.\n *\n * - OpenAI: `listFiles`, `searchText`, `readFile`, `readFiles`, and its native `apply_patch`.\n * - Anthropic: `listFiles`, `searchText`, its native text editor, whose `view` reads, and\n * `delete_file`.\n *\n * Without a `write` root they only read: `readFile` and `readFiles` in place of the editor and\n * the patch.\n */\nexport function fileToolsFor(\n\tprovider: 'anthropic' | 'openai',\n\tworkspace: OpenedWorkspace,\n): AgentTool[] {\n\tconst { policy, read, writes, writable } = workspace;\n\tconst [listFiles, searchText] = createWorkspaceTools(read).filter(\n\t\ttool => tool.name === 'listFiles' || tool.name === 'searchText',\n\t);\n\tconst finding: AgentTool[] = [listFiles, searchText].filter(tool => tool !== undefined);\n\tconst reading = [createTrackedReadFile(policy), createTrackedReadFiles(policy)];\n\tif (!writable) return [...finding, ...reading];\n\tif (provider === 'openai') return [...finding, ...reading, createApplyPatchTool(writes)];\n\treturn [\n\t\t...finding,\n\t\tcreateTextEditorTool({ writes, policy, workspace: read }),\n\t\tcreateDeleteFileTool(writes),\n\t];\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import type { WorkspaceRoot } from '../domain/accessPolicy.ts';
2
+ import type { PreparingTool } from '../domain/preparedCall.ts';
3
+ /**
4
+ * `removeWorktree`: removes a git worktree of the repository the first root is in, its folder
5
+ * and every file in it, never its branch, which keeps its commits. Each call is `destructive`,
6
+ * so it is asked about, and the request names what is lost: changes no commit has, files
7
+ * `.gitignore` excludes, and with a detached HEAD the commits no branch, tag or remote holds. It
8
+ * refuses the repository's main folder, a worktree the agent works in, which holds one of its
9
+ * roots or lies inside one, and a locked one.
10
+ *
11
+ * Once approved it claims the worktree (`FolderClaims`), so no run that may write in it starts,
12
+ * and removes nothing while one works; it checks again that nothing it rests on changed while
13
+ * the user was asked, and otherwise removes nothing, so the model calls it again to have the
14
+ * user asked anew. A folder `.gitignore` excludes whole is checked as a folder, never looked
15
+ * into, and its result tells the model so.
16
+ */
17
+ export declare function createRemoveWorktreeTool({ base, roots, stateDirectory, }: {
18
+ /** The real path of the first root. */
19
+ base: string;
20
+ roots: readonly WorkspaceRoot[];
21
+ /** Where the histories are kept, as a real path: their runs' claims are there. */
22
+ stateDirectory: string;
23
+ }): PreparingTool;
@@ -0,0 +1,178 @@
1
+ import { lstat, realpath } from 'node:fs/promises';
2
+ import { join, resolve, sep } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { WorkspaceBusyError } from '#src/recovery/domain/recoveryStore';
5
+ import { FolderClaims } from '#src/recovery/infrastructure/folderClaims';
6
+ import { definePreparingTool } from "./defineTool.js";
7
+ import { ceilingRefusal } from "./writeCeiling.js";
8
+ import { git, listWorktrees } from "./worktrees.js";
9
+ function within(path, folder) {
10
+ return path === folder || path.startsWith(`${folder}${sep}`);
11
+ }
12
+ /**
13
+ * The paths `git status --porcelain -z` names, each with its two-letter code. A rename or copy
14
+ * names its source after it, which is skipped.
15
+ */
16
+ function statusEntries(status) {
17
+ const fields = status.split('\0');
18
+ const entries = [];
19
+ for (let index = 0; index < fields.length; index++) {
20
+ const field = fields[index] ?? '';
21
+ if (field.length < 4)
22
+ continue;
23
+ const code = field.slice(0, 2);
24
+ entries.push({ code, path: field.slice(3) });
25
+ if (code.startsWith('R') || code.startsWith('C'))
26
+ index++;
27
+ }
28
+ return entries;
29
+ }
30
+ async function gitOutput(args, cwd, signal) {
31
+ const { stdout, stderr, exitCode } = await git(args, cwd, signal);
32
+ if (exitCode !== 0) {
33
+ throw new Error(`git could not tell what removing ${cwd} would lose, so nothing was removed: ${stderr.trim()}`);
34
+ }
35
+ return stdout;
36
+ }
37
+ /**
38
+ * What removing the worktree at `path` would lose, and a fingerprint of all it rests on: its
39
+ * HEAD, its branch, the commits at risk, and each file `git status` names, by size and time of
40
+ * change. Throws when
41
+ * any of it cannot be read, since a removal must never be asked about with less than it loses.
42
+ */
43
+ async function inspect(path, branch, signal) {
44
+ const head = (await gitOutput(['rev-parse', '--verify', 'HEAD'], path, signal)).trim();
45
+ const changed = statusEntries(await gitOutput(['status', '--porcelain=v1', '-z', '--untracked-files=all'], path, signal));
46
+ // A folder `.gitignore` excludes whole is named once, however much it holds.
47
+ const ignored = statusEntries(await gitOutput(['status', '--porcelain=v1', '-z', '--untracked-files=normal', '--ignored=matching'], path, signal)).filter(({ code }) => code === '!!');
48
+ // The commits only a detached HEAD holds, which are lost with it.
49
+ const atRisk = [];
50
+ if (branch === undefined) {
51
+ const listed = (await gitOutput(['rev-list', 'HEAD', '--not', '--branches', '--tags', '--remotes'], path, signal))
52
+ .split('\n')
53
+ .filter(line => line !== '');
54
+ if (!listed.every(line => /^[0-9a-f]{40,64}$/.test(line))) {
55
+ throw new Error(`git could not list the commits only ${path} holds, so nothing was removed: it answered "${listed.join(' ')}".`);
56
+ }
57
+ atRisk.push(...listed);
58
+ }
59
+ const files = await Promise.all([...changed, ...ignored].map(async ({ code, path: file }) => {
60
+ const stats = await lstat(join(path, file)).catch(() => undefined);
61
+ return `${code} ${file} ${String(stats?.size)} ${String(stats?.mtimeMs)}`;
62
+ }));
63
+ return {
64
+ removal: {
65
+ path,
66
+ ...(branch === undefined ? {} : { branch }),
67
+ uncommitted: changed.length,
68
+ ignored: ignored.length,
69
+ commitsOnlyHere: atRisk.length,
70
+ },
71
+ fingerprint: JSON.stringify([head, branch ?? null, atRisk, files.sort()]),
72
+ };
73
+ }
74
+ /**
75
+ * The worktree of this repository at `target`, checked as removable: not the main folder, not
76
+ * one the agent works in, and not locked. Throws a message for the model otherwise.
77
+ */
78
+ async function removableWorktree(target, { base, roots }, signal) {
79
+ const worktrees = await listWorktrees(base, signal);
80
+ const [main] = worktrees;
81
+ if (main?.path === target) {
82
+ throw new Error(`${target} is the repository's main folder, not a worktree. Only a worktree can be removed.`);
83
+ }
84
+ const worktree = worktrees.find(listed => listed.path === target);
85
+ if (!worktree) {
86
+ const others = worktrees.slice(1).map(listed => listed.path);
87
+ throw new Error(`${target} is not a worktree of this repository. Its worktrees: ${others.join(', ') || 'none'}.`);
88
+ }
89
+ const own = await Promise.all(
90
+ // From the working directory, as the workspace reads them.
91
+ roots.map(root => realpath(resolve(root.path)).catch(() => resolve(root.path))));
92
+ if (own.some(root => within(root, target) || within(target, root))) {
93
+ throw new Error(`You work in ${target}, so it cannot be removed from this run.`);
94
+ }
95
+ if (worktree.locked) {
96
+ throw new Error(`${target} is locked. The user must unlock it to remove it.`);
97
+ }
98
+ return worktree;
99
+ }
100
+ /**
101
+ * `removeWorktree`: removes a git worktree of the repository the first root is in, its folder
102
+ * and every file in it, never its branch, which keeps its commits. Each call is `destructive`,
103
+ * so it is asked about, and the request names what is lost: changes no commit has, files
104
+ * `.gitignore` excludes, and with a detached HEAD the commits no branch, tag or remote holds. It
105
+ * refuses the repository's main folder, a worktree the agent works in, which holds one of its
106
+ * roots or lies inside one, and a locked one.
107
+ *
108
+ * Once approved it claims the worktree (`FolderClaims`), so no run that may write in it starts,
109
+ * and removes nothing while one works; it checks again that nothing it rests on changed while
110
+ * the user was asked, and otherwise removes nothing, so the model calls it again to have the
111
+ * user asked anew. A folder `.gitignore` excludes whole is checked as a folder, never looked
112
+ * into, and its result tells the model so.
113
+ */
114
+ export function createRemoveWorktreeTool({ base, roots, stateDirectory, }) {
115
+ return definePreparingTool({
116
+ name: 'removeWorktree',
117
+ description: 'Removes a git worktree of this repository: its folder and every file in it, committed or not. Its branch stays, with its commits. Only when the user asks for it.',
118
+ input: z.object({
119
+ path: z
120
+ .string()
121
+ .describe('The folder of the worktree, absolute or relative to the first root.'),
122
+ }),
123
+ summarize: ({ path }) => path,
124
+ prepare: async ({ path }, signal, { writeCeiling }) => {
125
+ const asked = resolve(base, path);
126
+ const target = await realpath(asked).catch(() => {
127
+ throw new Error(`There is no folder at ${asked}.`);
128
+ });
129
+ // Removing it writes there, which an agent another agent started may do only where that
130
+ // agent may.
131
+ const refusal = ceilingRefusal([target], writeCeiling);
132
+ if (refusal !== undefined)
133
+ throw new Error(refusal);
134
+ const worktree = await removableWorktree(target, { base, roots }, signal);
135
+ const { removal, fingerprint } = await inspect(target, worktree.branch, signal);
136
+ return {
137
+ risk: 'destructive',
138
+ worktree: removal,
139
+ run: async (runSignal) => {
140
+ let release;
141
+ try {
142
+ release = await new FolderClaims(stateDirectory).claimRemoval(target);
143
+ }
144
+ catch (error) {
145
+ if (!(error instanceof WorkspaceBusyError))
146
+ throw error;
147
+ throw new Error(`A run, or a move through a history, may write in ${target}, so nothing was removed. The harness removes no worktree a run may write in; tell the user, and try again once it ends.`, { cause: error });
148
+ }
149
+ try {
150
+ const now = await removableWorktree(target, { base, roots }, runSignal);
151
+ const current = await inspect(target, now.branch, runSignal);
152
+ if (current.fingerprint !== fingerprint) {
153
+ throw new Error(`${target} changed since the user was asked, so nothing was removed. Call removeWorktree again to have them asked with what it holds now.`);
154
+ }
155
+ // Forced: what it holds unsaved is what the user approved losing.
156
+ const removed = await git(['worktree', 'remove', '--force', '--', target], base, runSignal);
157
+ if (removed.exitCode !== 0) {
158
+ throw new Error(`git could not remove ${target}: ${removed.stderr.trim()}`);
159
+ }
160
+ }
161
+ finally {
162
+ await release();
163
+ }
164
+ const done = `Removed the worktree at ${target}.`;
165
+ const kept = removal.branch === undefined
166
+ ? ''
167
+ : ` Its branch ${removal.branch} stays, with its commits.`;
168
+ // Told to the model, which may have to mention it: an ignored folder went unchecked.
169
+ const ignored = removal.ignored === 0
170
+ ? ''
171
+ : ` It held ${String(removal.ignored)} ${removal.ignored === 1 ? 'path' : 'paths'} .gitignore excludes, removed whole without looking into them, so anything written in them while the user was asked went too.`;
172
+ return `${done}${kept}${ignored}`;
173
+ },
174
+ };
175
+ },
176
+ });
177
+ }
178
+ //# sourceMappingURL=removeWorktreeTool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"removeWorktreeTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/removeWorktreeTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { lstat, realpath } from 'node:fs/promises';\nimport { join, resolve, sep } from 'node:path';\nimport { z } from 'zod';\nimport type { WorktreeRemoval } from '#src/agent/domain/approval';\nimport { WorkspaceBusyError } from '#src/recovery/domain/recoveryStore';\nimport { FolderClaims } from '#src/recovery/infrastructure/folderClaims';\nimport type { WorkspaceRoot } from '../domain/accessPolicy.ts';\nimport type { PreparingTool } from '../domain/preparedCall.ts';\nimport { definePreparingTool } from './defineTool.ts';\nimport { ceilingRefusal } from './writeCeiling.ts';\nimport { git, listWorktrees } from './worktrees.ts';\n\nfunction within(path: string, folder: string): boolean {\n\treturn path === folder || path.startsWith(`${folder}${sep}`);\n}\n\n/**\n * The paths `git status --porcelain -z` names, each with its two-letter code. A rename or copy\n * names its source after it, which is skipped.\n */\nfunction statusEntries(status: string): { code: string; path: string }[] {\n\tconst fields = status.split('\\0');\n\tconst entries: { code: string; path: string }[] = [];\n\tfor (let index = 0; index < fields.length; index++) {\n\t\tconst field = fields[index] ?? '';\n\t\tif (field.length < 4) continue;\n\t\tconst code = field.slice(0, 2);\n\t\tentries.push({ code, path: field.slice(3) });\n\t\tif (code.startsWith('R') || code.startsWith('C')) index++;\n\t}\n\treturn entries;\n}\n\nasync function gitOutput(args: string[], cwd: string, signal: AbortSignal): Promise<string> {\n\tconst { stdout, stderr, exitCode } = await git(args, cwd, signal);\n\tif (exitCode !== 0) {\n\t\tthrow new Error(\n\t\t\t`git could not tell what removing ${cwd} would lose, so nothing was removed: ${stderr.trim()}`,\n\t\t);\n\t}\n\treturn stdout;\n}\n\n/**\n * What removing the worktree at `path` would lose, and a fingerprint of all it rests on: its\n * HEAD, its branch, the commits at risk, and each file `git status` names, by size and time of\n * change. Throws when\n * any of it cannot be read, since a removal must never be asked about with less than it loses.\n */\nasync function inspect(\n\tpath: string,\n\tbranch: string | undefined,\n\tsignal: AbortSignal,\n): Promise<{ removal: WorktreeRemoval; fingerprint: string }> {\n\tconst head = (await gitOutput(['rev-parse', '--verify', 'HEAD'], path, signal)).trim();\n\tconst changed = statusEntries(\n\t\tawait gitOutput(['status', '--porcelain=v1', '-z', '--untracked-files=all'], path, signal),\n\t);\n\t// A folder `.gitignore` excludes whole is named once, however much it holds.\n\tconst ignored = statusEntries(\n\t\tawait gitOutput(\n\t\t\t['status', '--porcelain=v1', '-z', '--untracked-files=normal', '--ignored=matching'],\n\t\t\tpath,\n\t\t\tsignal,\n\t\t),\n\t).filter(({ code }) => code === '!!');\n\t// The commits only a detached HEAD holds, which are lost with it.\n\tconst atRisk: string[] = [];\n\tif (branch === undefined) {\n\t\tconst listed = (\n\t\t\tawait gitOutput(\n\t\t\t\t['rev-list', 'HEAD', '--not', '--branches', '--tags', '--remotes'],\n\t\t\t\tpath,\n\t\t\t\tsignal,\n\t\t\t)\n\t\t)\n\t\t\t.split('\\n')\n\t\t\t.filter(line => line !== '');\n\t\tif (!listed.every(line => /^[0-9a-f]{40,64}$/.test(line))) {\n\t\t\tthrow new Error(\n\t\t\t\t`git could not list the commits only ${path} holds, so nothing was removed: it answered \"${listed.join(' ')}\".`,\n\t\t\t);\n\t\t}\n\t\tatRisk.push(...listed);\n\t}\n\tconst files = await Promise.all(\n\t\t[...changed, ...ignored].map(async ({ code, path: file }) => {\n\t\t\tconst stats = await lstat(join(path, file)).catch(() => undefined);\n\t\t\treturn `${code} ${file} ${String(stats?.size)} ${String(stats?.mtimeMs)}`;\n\t\t}),\n\t);\n\treturn {\n\t\tremoval: {\n\t\t\tpath,\n\t\t\t...(branch === undefined ? {} : { branch }),\n\t\t\tuncommitted: changed.length,\n\t\t\tignored: ignored.length,\n\t\t\tcommitsOnlyHere: atRisk.length,\n\t\t},\n\t\tfingerprint: JSON.stringify([head, branch ?? null, atRisk, files.sort()]),\n\t};\n}\n\n/**\n * The worktree of this repository at `target`, checked as removable: not the main folder, not\n * one the agent works in, and not locked. Throws a message for the model otherwise.\n */\nasync function removableWorktree(\n\ttarget: string,\n\t{ base, roots }: { base: string; roots: readonly WorkspaceRoot[] },\n\tsignal: AbortSignal,\n) {\n\tconst worktrees = await listWorktrees(base, signal);\n\tconst [main] = worktrees;\n\tif (main?.path === target) {\n\t\tthrow new Error(\n\t\t\t`${target} is the repository's main folder, not a worktree. Only a worktree can be removed.`,\n\t\t);\n\t}\n\tconst worktree = worktrees.find(listed => listed.path === target);\n\tif (!worktree) {\n\t\tconst others = worktrees.slice(1).map(listed => listed.path);\n\t\tthrow new Error(\n\t\t\t`${target} is not a worktree of this repository. Its worktrees: ${others.join(', ') || 'none'}.`,\n\t\t);\n\t}\n\tconst own = await Promise.all(\n\t\t// From the working directory, as the workspace reads them.\n\t\troots.map(root => realpath(resolve(root.path)).catch(() => resolve(root.path))),\n\t);\n\tif (own.some(root => within(root, target) || within(target, root))) {\n\t\tthrow new Error(`You work in ${target}, so it cannot be removed from this run.`);\n\t}\n\tif (worktree.locked) {\n\t\tthrow new Error(`${target} is locked. The user must unlock it to remove it.`);\n\t}\n\treturn worktree;\n}\n\n/**\n * `removeWorktree`: removes a git worktree of the repository the first root is in, its folder\n * and every file in it, never its branch, which keeps its commits. Each call is `destructive`,\n * so it is asked about, and the request names what is lost: changes no commit has, files\n * `.gitignore` excludes, and with a detached HEAD the commits no branch, tag or remote holds. It\n * refuses the repository's main folder, a worktree the agent works in, which holds one of its\n * roots or lies inside one, and a locked one.\n *\n * Once approved it claims the worktree (`FolderClaims`), so no run that may write in it starts,\n * and removes nothing while one works; it checks again that nothing it rests on changed while\n * the user was asked, and otherwise removes nothing, so the model calls it again to have the\n * user asked anew. A folder `.gitignore` excludes whole is checked as a folder, never looked\n * into, and its result tells the model so.\n */\nexport function createRemoveWorktreeTool({\n\tbase,\n\troots,\n\tstateDirectory,\n}: {\n\t/** The real path of the first root. */\n\tbase: string;\n\troots: readonly WorkspaceRoot[];\n\t/** Where the histories are kept, as a real path: their runs' claims are there. */\n\tstateDirectory: string;\n}): PreparingTool {\n\treturn definePreparingTool({\n\t\tname: 'removeWorktree',\n\t\tdescription:\n\t\t\t'Removes a git worktree of this repository: its folder and every file in it, committed or not. Its branch stays, with its commits. Only when the user asks for it.',\n\t\tinput: z.object({\n\t\t\tpath: z\n\t\t\t\t.string()\n\t\t\t\t.describe('The folder of the worktree, absolute or relative to the first root.'),\n\t\t}),\n\t\tsummarize: ({ path }) => path,\n\t\tprepare: async ({ path }, signal, { writeCeiling }) => {\n\t\t\tconst asked = resolve(base, path);\n\t\t\tconst target = await realpath(asked).catch(() => {\n\t\t\t\tthrow new Error(`There is no folder at ${asked}.`);\n\t\t\t});\n\t\t\t// Removing it writes there, which an agent another agent started may do only where that\n\t\t\t// agent may.\n\t\t\tconst refusal = ceilingRefusal([target], writeCeiling);\n\t\t\tif (refusal !== undefined) throw new Error(refusal);\n\t\t\tconst worktree = await removableWorktree(target, { base, roots }, signal);\n\t\t\tconst { removal, fingerprint } = await inspect(target, worktree.branch, signal);\n\t\t\treturn {\n\t\t\t\trisk: 'destructive',\n\t\t\t\tworktree: removal,\n\t\t\t\trun: async runSignal => {\n\t\t\t\t\tlet release: () => Promise<void>;\n\t\t\t\t\ttry {\n\t\t\t\t\t\trelease = await new FolderClaims(stateDirectory).claimRemoval(target);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tif (!(error instanceof WorkspaceBusyError)) throw error;\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`A run, or a move through a history, may write in ${target}, so nothing was removed. The harness removes no worktree a run may write in; tell the user, and try again once it ends.`,\n\t\t\t\t\t\t\t{ cause: error },\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst now = await removableWorktree(target, { base, roots }, runSignal);\n\t\t\t\t\t\tconst current = await inspect(target, now.branch, runSignal);\n\t\t\t\t\t\tif (current.fingerprint !== fingerprint) {\n\t\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t\t`${target} changed since the user was asked, so nothing was removed. Call removeWorktree again to have them asked with what it holds now.`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Forced: what it holds unsaved is what the user approved losing.\n\t\t\t\t\t\tconst removed = await git(\n\t\t\t\t\t\t\t['worktree', 'remove', '--force', '--', target],\n\t\t\t\t\t\t\tbase,\n\t\t\t\t\t\t\trunSignal,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (removed.exitCode !== 0) {\n\t\t\t\t\t\t\tthrow new Error(`git could not remove ${target}: ${removed.stderr.trim()}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tawait release();\n\t\t\t\t\t}\n\t\t\t\t\tconst done = `Removed the worktree at ${target}.`;\n\t\t\t\t\tconst kept =\n\t\t\t\t\t\tremoval.branch === undefined\n\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t: ` Its branch ${removal.branch} stays, with its commits.`;\n\t\t\t\t\t// Told to the model, which may have to mention it: an ignored folder went unchecked.\n\t\t\t\t\tconst ignored =\n\t\t\t\t\t\tremoval.ignored === 0\n\t\t\t\t\t\t\t? ''\n\t\t\t\t\t\t\t: ` It held ${String(removal.ignored)} ${removal.ignored === 1 ? 'path' : 'paths'} .gitignore excludes, removed whole without looking into them, so anything written in them while the user was asked went too.`;\n\t\t\t\t\treturn `${done}${kept}${ignored}`;\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
@@ -9,11 +9,13 @@ export declare function failureOf(error: RecoverableError | UnrecoverableError):
9
9
  * The options of a subagent a call starts: within the run that made the call, one delegation
10
10
  * deeper, and narrated as that call's `subagent`, so the person watching can tell it apart.
11
11
  */
12
- export declare function subagentOptions(call: ToolCallContext, { tool, name, subagent, task, options, }: {
12
+ export declare function subagentOptions(call: ToolCallContext, { tool, name, subagent, task, options, writeCeiling, }: {
13
13
  tool: string;
14
14
  name: string;
15
15
  subagent: number;
16
16
  task: string;
17
17
  /** What else it runs with, such as its signal. */
18
18
  options: RunOptions;
19
+ /** Where it may write, as real paths; see `delegateCeiling`. */
20
+ writeCeiling: readonly string[] | undefined;
19
21
  }): RunOptions;
@@ -15,7 +15,7 @@ export function failureOf(error) {
15
15
  * The options of a subagent a call starts: within the run that made the call, one delegation
16
16
  * deeper, and narrated as that call's `subagent`, so the person watching can tell it apart.
17
17
  */
18
- export function subagentOptions(call, { tool, name, subagent, task, options, }) {
18
+ export function subagentOptions(call, { tool, name, subagent, task, options, writeCeiling, }) {
19
19
  const onProgress = (event) => {
20
20
  call.onProgress({ type: 'subagent', callId: call.callId, subagent, name, task, event });
21
21
  };
@@ -24,6 +24,7 @@ export function subagentOptions(call, { tool, name, subagent, task, options, })
24
24
  callId: call.callId,
25
25
  subagent,
26
26
  depth: call.depth + 1,
27
+ ...(writeCeiling ? { writeCeiling } : {}),
27
28
  });
28
29
  }
29
30
  //# sourceMappingURL=subagents.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"subagents.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/subagents.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAI9E,qEAAqE;AACrE,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEnC,2FAA2F;AAC3F,MAAM,UAAU,SAAS,CAAC,MAAc;IACvC,IAAI,MAAM,CAAC,MAAM,IAAI,mBAAmB;QAAE,OAAO,MAAM,CAAC;IACxD,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC,4BAA4B,MAAM,CAAC,mBAAmB,CAAC,eAAe,CAAC;AACtH,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,SAAS,CAAC,KAA4C;IACrE,OAAO,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;AAC7F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC9B,IAAqB,EACrB,EACC,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,OAAO,GAQP;IAED,MAAM,UAAU,GAAG,CAAC,KAAoB,EAAE,EAAE;QAC3C,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzF,CAAC,CAAC;IACF,OAAO,cAAc,CAAC,cAAc,CAAC,EAAE,GAAG,OAAO,EAAE,UAAU,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE;QAC3E,IAAI;QACJ,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,QAAQ;QACR,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC;KACrB,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import type { ProgressEvent, RunOptions } from '#src/agent/domain/agent';\nimport { withDelegation, withRunContext } from '#src/agent/domain/runContext';\nimport type { RecoverableError, UnrecoverableError } from '#src/shared/domain/errors';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\n\n// An answer is read whole, like a read, which returns at most 16 KB.\nconst maxAnswerCharacters = 16_000;\n\n/** A subagent's answer as the agent that called it reads it, cut where a read would be. */\nexport function cutAnswer(answer: string): string {\n\tif (answer.length <= maxAnswerCharacters) return answer;\n\treturn `${answer.slice(0, maxAnswerCharacters)}\\n[The answer was cut at ${String(maxAnswerCharacters)} characters.]`;\n}\n\n/** Why a subagent failed, for the agent that called it. */\nexport function failureOf(error: RecoverableError | UnrecoverableError): string {\n\treturn typeof error.cause === 'string' ? `${error.message}: ${error.cause}` : error.message;\n}\n\n/**\n * The options of a subagent a call starts: within the run that made the call, one delegation\n * deeper, and narrated as that call's `subagent`, so the person watching can tell it apart.\n */\nexport function subagentOptions(\n\tcall: ToolCallContext,\n\t{\n\t\ttool,\n\t\tname,\n\t\tsubagent,\n\t\ttask,\n\t\toptions,\n\t}: {\n\t\ttool: string;\n\t\tname: string;\n\t\tsubagent: number;\n\t\ttask: string;\n\t\t/** What else it runs with, such as its signal. */\n\t\toptions: RunOptions;\n\t},\n): RunOptions {\n\tconst onProgress = (event: ProgressEvent) => {\n\t\tcall.onProgress({ type: 'subagent', callId: call.callId, subagent, name, task, event });\n\t};\n\treturn withDelegation(withRunContext({ ...options, onProgress }, call.run), {\n\t\ttool,\n\t\tcallId: call.callId,\n\t\tsubagent,\n\t\tdepth: call.depth + 1,\n\t});\n}\n"]}
1
+ {"version":3,"file":"subagents.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/subagents.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAI9E,qEAAqE;AACrE,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEnC,2FAA2F;AAC3F,MAAM,UAAU,SAAS,CAAC,MAAc;IACvC,IAAI,MAAM,CAAC,MAAM,IAAI,mBAAmB;QAAE,OAAO,MAAM,CAAC;IACxD,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC,4BAA4B,MAAM,CAAC,mBAAmB,CAAC,eAAe,CAAC;AACtH,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,SAAS,CAAC,KAA4C;IACrE,OAAO,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;AAC7F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC9B,IAAqB,EACrB,EACC,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,IAAI,EACJ,OAAO,EACP,YAAY,GAUZ;IAED,MAAM,UAAU,GAAG,CAAC,KAAoB,EAAE,EAAE;QAC3C,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzF,CAAC,CAAC;IACF,OAAO,cAAc,CAAC,cAAc,CAAC,EAAE,GAAG,OAAO,EAAE,UAAU,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE;QAC3E,IAAI;QACJ,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,QAAQ;QACR,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC;QACrB,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACzC,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import type { ProgressEvent, RunOptions } from '#src/agent/domain/agent';\nimport { withDelegation, withRunContext } from '#src/agent/domain/runContext';\nimport type { RecoverableError, UnrecoverableError } from '#src/shared/domain/errors';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\n\n// An answer is read whole, like a read, which returns at most 16 KB.\nconst maxAnswerCharacters = 16_000;\n\n/** A subagent's answer as the agent that called it reads it, cut where a read would be. */\nexport function cutAnswer(answer: string): string {\n\tif (answer.length <= maxAnswerCharacters) return answer;\n\treturn `${answer.slice(0, maxAnswerCharacters)}\\n[The answer was cut at ${String(maxAnswerCharacters)} characters.]`;\n}\n\n/** Why a subagent failed, for the agent that called it. */\nexport function failureOf(error: RecoverableError | UnrecoverableError): string {\n\treturn typeof error.cause === 'string' ? `${error.message}: ${error.cause}` : error.message;\n}\n\n/**\n * The options of a subagent a call starts: within the run that made the call, one delegation\n * deeper, and narrated as that call's `subagent`, so the person watching can tell it apart.\n */\nexport function subagentOptions(\n\tcall: ToolCallContext,\n\t{\n\t\ttool,\n\t\tname,\n\t\tsubagent,\n\t\ttask,\n\t\toptions,\n\t\twriteCeiling,\n\t}: {\n\t\ttool: string;\n\t\tname: string;\n\t\tsubagent: number;\n\t\ttask: string;\n\t\t/** What else it runs with, such as its signal. */\n\t\toptions: RunOptions;\n\t\t/** Where it may write, as real paths; see `delegateCeiling`. */\n\t\twriteCeiling: readonly string[] | undefined;\n\t},\n): RunOptions {\n\tconst onProgress = (event: ProgressEvent) => {\n\t\tcall.onProgress({ type: 'subagent', callId: call.callId, subagent, name, task, event });\n\t};\n\treturn withDelegation(withRunContext({ ...options, onProgress }, call.run), {\n\t\ttool,\n\t\tcallId: call.callId,\n\t\tsubagent,\n\t\tdepth: call.depth + 1,\n\t\t...(writeCeiling ? { writeCeiling } : {}),\n\t});\n}\n"]}
@@ -39,6 +39,7 @@ async function afterFormatting(writes, editor, { path, absolute, written }, { ru
39
39
  */
40
40
  export async function prepareTrackedEdit(writes, request, signal, call) {
41
41
  const { run, reads } = call;
42
+ await writes.checkCeiling(call);
42
43
  const editor = writes.editorFor(run);
43
44
  const { path } = request;
44
45
  if (request.kind === 'create') {
@@ -1 +1 @@
1
- {"version":3,"file":"trackedEdits.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/trackedEdits.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport {\n\tdescribeCreation,\n\tdescribeDeletion,\n\tdescribeFormatting,\n\tdescribeModification,\n\teditDiff,\n} from '../domain/editResponse.ts';\nimport { EditRefusedError } from '../domain/fileEdits.ts';\nimport type { PreparedCall } from '../domain/preparedCall.ts';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\nimport { editRisk, type FileEditor } from './fileEditor.ts';\nimport type { WorkspaceWrites } from './workspaceWrites.ts';\n\n/**\n * One edit, as an edit format asks for it, with the path as the model wrote it. A modification\n * works its new content out from what the file holds now, the version the model read; it throws\n * `EditRefusedError` with what the model needs to fix the call, such as a text that matched\n * twice.\n */\nexport type EditRequest =\n\t| { kind: 'create'; path: string; content: Buffer }\n\t| { kind: 'modify'; path: string; change: (current: Buffer) => Buffer }\n\t| { kind: 'delete'; path: string };\n\nfunction hashOf(content: Buffer): string {\n\treturn createHash('sha256').update(content).digest('hex');\n}\n\n/**\n * What the file holds once the formatter, if there is one, ran on what the edit wrote, and what\n * the model needs to know when it may not be formatted. A file as the edit wrote it is known to\n * the conversation, so it can edit it again without reading it. One the formatter rewrote is not:\n * the model would edit on from the lines it sent, so it must read the file again.\n */\nasync function afterFormatting(\n\twrites: WorkspaceWrites,\n\teditor: FileEditor,\n\t{ path, absolute, written }: { path: string; absolute: string; written: Buffer },\n\t{ run, reads }: ToolCallContext,\n\tsignal: AbortSignal,\n): Promise<{ content: Buffer | undefined; note: string }> {\n\tconst { formatter } = writes;\n\tif (!formatter) {\n\t\t// It wrote the content itself, so it has seen it.\n\t\treads.record(absolute, hashOf(written));\n\t\treturn { content: written, note: '' };\n\t}\n\tconst failure = await formatter.format(absolute, writes.sessionFor(run), signal);\n\tconst { content } = await editor.current(path, signal);\n\tif (content?.equals(written)) reads.record(absolute, hashOf(content));\n\telse reads.forget(absolute);\n\treturn { content, note: failure === undefined ? '' : `\\n${failure}` };\n}\n\n/**\n * Prepares an edit from the version of the file this conversation read, and only from it:\n *\n * - a file it never read is `READ_REQUIRED`;\n * - a file changed since it read it is `STALE_FILE`;\n * - a new file needs a path that does not exist.\n *\n * Once applied, the conversation knows the file as it wrote it, so it can edit it again without\n * reading it, unless the formatter rewrote it. What it returns to the model is short: the lines\n * around a change, or a confirmation, and the lines the formatter rewrote.\n */\nexport async function prepareTrackedEdit(\n\twrites: WorkspaceWrites,\n\trequest: EditRequest,\n\tsignal: AbortSignal,\n\tcall: ToolCallContext,\n): Promise<PreparedCall> {\n\tconst { run, reads } = call;\n\tconst editor = writes.editorFor(run);\n\tconst { path } = request;\n\n\tif (request.kind === 'create') {\n\t\tconst prepared = await editor.prepare(\n\t\t\t{ kind: 'create', path, content: request.content },\n\t\t\tsignal,\n\t\t);\n\t\treturn {\n\t\t\trisk: editRisk(prepared),\n\t\t\tdiff: editDiff(path, undefined, request.content.toString('utf8')),\n\t\t\trun: async runSignal => {\n\t\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\t\tconst { content, note } = await afterFormatting(\n\t\t\t\t\twrites,\n\t\t\t\t\teditor,\n\t\t\t\t\t{ path, absolute: prepared.target.absolute, written: request.content },\n\t\t\t\t\tcall,\n\t\t\t\t\trunSignal,\n\t\t\t\t);\n\t\t\t\treturn content === undefined\n\t\t\t\t\t? `${goneAfterFormatting(path)}${note}`\n\t\t\t\t\t: `${describeCreation(path, content)}${describeFormatting(path, request.content.toString('utf8'), content.toString('utf8'))}${note}`;\n\t\t\t},\n\t\t};\n\t}\n\n\tconst { target, state, content } = await editor.current(path, signal);\n\tif (!state.exists || content === undefined) {\n\t\tthrow new EditRefusedError(`\"${path}\" does not exist`);\n\t}\n\tconst seen = reads.versionOf(target.absolute);\n\tif (seen === undefined) {\n\t\tthrow new EditRefusedError(`READ_REQUIRED: read \"${path}\" before changing it`);\n\t}\n\tif (seen !== state.hash) {\n\t\tthrow new EditRefusedError(`STALE_FILE: \"${path}\" changed since you read it; read it again`);\n\t}\n\n\tif (request.kind === 'delete') {\n\t\tconst prepared = await editor.prepare({ kind: 'delete', path, expected: seen }, signal);\n\t\treturn {\n\t\t\trisk: editRisk(prepared),\n\t\t\tdiff: editDiff(path, content.toString('utf8'), undefined),\n\t\t\trun: async runSignal => {\n\t\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\t\treads.forget(target.absolute);\n\t\t\t\treturn describeDeletion(path);\n\t\t\t},\n\t\t};\n\t}\n\n\tconst next = request.change(content);\n\tconst prepared = await editor.prepare(\n\t\t{ kind: 'replace', path, content: next, expected: seen },\n\t\tsignal,\n\t);\n\treturn {\n\t\trisk: editRisk(prepared),\n\t\tdiff: editDiff(path, content.toString('utf8'), next.toString('utf8')),\n\t\trun: async runSignal => {\n\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\tconst { content: now, note } = await afterFormatting(\n\t\t\t\twrites,\n\t\t\t\teditor,\n\t\t\t\t{ path, absolute: target.absolute, written: next },\n\t\t\t\tcall,\n\t\t\t\trunSignal,\n\t\t\t);\n\t\t\tif (now === undefined) return `${goneAfterFormatting(path)}${note}`;\n\t\t\tconst formatted = now.toString('utf8');\n\t\t\treturn `${describeModification(path, content.toString('utf8'), formatted)}${describeFormatting(path, next.toString('utf8'), formatted)}${note}`;\n\t\t},\n\t};\n}\n\nfunction goneAfterFormatting(path: string): string {\n\treturn `Changed ${path}, but it no longer exists once the formatter ran.`;\n}\n"]}
1
+ {"version":3,"file":"trackedEdits.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/trackedEdits.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport {\n\tdescribeCreation,\n\tdescribeDeletion,\n\tdescribeFormatting,\n\tdescribeModification,\n\teditDiff,\n} from '../domain/editResponse.ts';\nimport { EditRefusedError } from '../domain/fileEdits.ts';\nimport type { PreparedCall } from '../domain/preparedCall.ts';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\nimport { editRisk, type FileEditor } from './fileEditor.ts';\nimport type { WorkspaceWrites } from './workspaceWrites.ts';\n\n/**\n * One edit, as an edit format asks for it, with the path as the model wrote it. A modification\n * works its new content out from what the file holds now, the version the model read; it throws\n * `EditRefusedError` with what the model needs to fix the call, such as a text that matched\n * twice.\n */\nexport type EditRequest =\n\t| { kind: 'create'; path: string; content: Buffer }\n\t| { kind: 'modify'; path: string; change: (current: Buffer) => Buffer }\n\t| { kind: 'delete'; path: string };\n\nfunction hashOf(content: Buffer): string {\n\treturn createHash('sha256').update(content).digest('hex');\n}\n\n/**\n * What the file holds once the formatter, if there is one, ran on what the edit wrote, and what\n * the model needs to know when it may not be formatted. A file as the edit wrote it is known to\n * the conversation, so it can edit it again without reading it. One the formatter rewrote is not:\n * the model would edit on from the lines it sent, so it must read the file again.\n */\nasync function afterFormatting(\n\twrites: WorkspaceWrites,\n\teditor: FileEditor,\n\t{ path, absolute, written }: { path: string; absolute: string; written: Buffer },\n\t{ run, reads }: ToolCallContext,\n\tsignal: AbortSignal,\n): Promise<{ content: Buffer | undefined; note: string }> {\n\tconst { formatter } = writes;\n\tif (!formatter) {\n\t\t// It wrote the content itself, so it has seen it.\n\t\treads.record(absolute, hashOf(written));\n\t\treturn { content: written, note: '' };\n\t}\n\tconst failure = await formatter.format(absolute, writes.sessionFor(run), signal);\n\tconst { content } = await editor.current(path, signal);\n\tif (content?.equals(written)) reads.record(absolute, hashOf(content));\n\telse reads.forget(absolute);\n\treturn { content, note: failure === undefined ? '' : `\\n${failure}` };\n}\n\n/**\n * Prepares an edit from the version of the file this conversation read, and only from it:\n *\n * - a file it never read is `READ_REQUIRED`;\n * - a file changed since it read it is `STALE_FILE`;\n * - a new file needs a path that does not exist.\n *\n * Once applied, the conversation knows the file as it wrote it, so it can edit it again without\n * reading it, unless the formatter rewrote it. What it returns to the model is short: the lines\n * around a change, or a confirmation, and the lines the formatter rewrote.\n */\nexport async function prepareTrackedEdit(\n\twrites: WorkspaceWrites,\n\trequest: EditRequest,\n\tsignal: AbortSignal,\n\tcall: ToolCallContext,\n): Promise<PreparedCall> {\n\tconst { run, reads } = call;\n\tawait writes.checkCeiling(call);\n\tconst editor = writes.editorFor(run);\n\tconst { path } = request;\n\n\tif (request.kind === 'create') {\n\t\tconst prepared = await editor.prepare(\n\t\t\t{ kind: 'create', path, content: request.content },\n\t\t\tsignal,\n\t\t);\n\t\treturn {\n\t\t\trisk: editRisk(prepared),\n\t\t\tdiff: editDiff(path, undefined, request.content.toString('utf8')),\n\t\t\trun: async runSignal => {\n\t\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\t\tconst { content, note } = await afterFormatting(\n\t\t\t\t\twrites,\n\t\t\t\t\teditor,\n\t\t\t\t\t{ path, absolute: prepared.target.absolute, written: request.content },\n\t\t\t\t\tcall,\n\t\t\t\t\trunSignal,\n\t\t\t\t);\n\t\t\t\treturn content === undefined\n\t\t\t\t\t? `${goneAfterFormatting(path)}${note}`\n\t\t\t\t\t: `${describeCreation(path, content)}${describeFormatting(path, request.content.toString('utf8'), content.toString('utf8'))}${note}`;\n\t\t\t},\n\t\t};\n\t}\n\n\tconst { target, state, content } = await editor.current(path, signal);\n\tif (!state.exists || content === undefined) {\n\t\tthrow new EditRefusedError(`\"${path}\" does not exist`);\n\t}\n\tconst seen = reads.versionOf(target.absolute);\n\tif (seen === undefined) {\n\t\tthrow new EditRefusedError(`READ_REQUIRED: read \"${path}\" before changing it`);\n\t}\n\tif (seen !== state.hash) {\n\t\tthrow new EditRefusedError(`STALE_FILE: \"${path}\" changed since you read it; read it again`);\n\t}\n\n\tif (request.kind === 'delete') {\n\t\tconst prepared = await editor.prepare({ kind: 'delete', path, expected: seen }, signal);\n\t\treturn {\n\t\t\trisk: editRisk(prepared),\n\t\t\tdiff: editDiff(path, content.toString('utf8'), undefined),\n\t\t\trun: async runSignal => {\n\t\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\t\treads.forget(target.absolute);\n\t\t\t\treturn describeDeletion(path);\n\t\t\t},\n\t\t};\n\t}\n\n\tconst next = request.change(content);\n\tconst prepared = await editor.prepare(\n\t\t{ kind: 'replace', path, content: next, expected: seen },\n\t\tsignal,\n\t);\n\treturn {\n\t\trisk: editRisk(prepared),\n\t\tdiff: editDiff(path, content.toString('utf8'), next.toString('utf8')),\n\t\trun: async runSignal => {\n\t\t\tawait editor.apply(prepared, runSignal);\n\t\t\tconst { content: now, note } = await afterFormatting(\n\t\t\t\twrites,\n\t\t\t\teditor,\n\t\t\t\t{ path, absolute: target.absolute, written: next },\n\t\t\t\tcall,\n\t\t\t\trunSignal,\n\t\t\t);\n\t\t\tif (now === undefined) return `${goneAfterFormatting(path)}${note}`;\n\t\t\tconst formatted = now.toString('utf8');\n\t\t\treturn `${describeModification(path, content.toString('utf8'), formatted)}${describeFormatting(path, next.toString('utf8'), formatted)}${note}`;\n\t\t},\n\t};\n}\n\nfunction goneAfterFormatting(path: string): string {\n\treturn `Changed ${path}, but it no longer exists once the formatter ran.`;\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import type { RunContext } from '#src/agent/domain/runContext';
2
2
  import type { RecoveryStore } from '#src/recovery/domain/recoveryStore';
3
3
  import type { AccessPolicy } from '../domain/accessPolicy.ts';
4
+ import type { ToolCallContext } from '../domain/readRegistry.ts';
4
5
  import { FileEditor } from './fileEditor.ts';
5
6
  import type { FileFormatter } from './fileFormatter.ts';
6
7
  import { WriteSession } from './writeSession.ts';
@@ -14,17 +15,32 @@ export declare class WorkspaceWrites {
14
15
  readonly formatter: FileFormatter | undefined;
15
16
  private readonly policy;
16
17
  private readonly store;
17
- constructor({ policy, store, formatter, }: {
18
+ private readonly writeRoots;
19
+ constructor({ policy, store, formatter, writeRoots, }: {
18
20
  policy: AccessPolicy;
19
21
  store: RecoveryStore;
20
22
  formatter?: FileFormatter;
23
+ /** The `write` roots of the tools' workspace. Defaults to the folders the store claims. */
24
+ writeRoots?: readonly string[];
21
25
  });
26
+ /**
27
+ * Checks, before anything is written or anyone is asked, that the agent that made the call
28
+ * may write wherever these tools may: an agent another agent started never writes more than
29
+ * that agent may.
30
+ *
31
+ * @throws {EditRefusedError} when these tools may write outside the call's `writeCeiling`.
32
+ */
33
+ checkCeiling({ writeCeiling }: Pick<ToolCallContext, 'writeCeiling'>): Promise<void>;
22
34
  /** An editor over this run's session, under this set of tools' policy. */
23
35
  editorFor(context: RunContext): FileEditor;
24
36
  /**
25
37
  * Everything this run writes to this history, its commands' changes included, from any set of
26
38
  * tools. Created on its first write, and registered before anything is awaited, so two tools
27
39
  * asking at once get the same one.
40
+ *
41
+ * @throws {EditRefusedError} for a set of tools that may write outside the folders of the set
42
+ * that started the session: the run claims those alone, so a worktree removed meanwhile could
43
+ * lose what it writes there.
28
44
  */
29
45
  sessionFor(context: RunContext): WriteSession;
30
46
  }