@mikode13/harness 4.0.1 → 5.0.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 (167) hide show
  1. package/README.md +278 -16
  2. package/dist/agent/domain/agent.d.ts +12 -0
  3. package/dist/agent/domain/agent.js.map +1 -1
  4. package/dist/agent/domain/approval.d.ts +9 -0
  5. package/dist/agent/domain/approval.js.map +1 -1
  6. package/dist/agent/domain/runContext.d.ts +31 -0
  7. package/dist/agent/domain/runContext.js +47 -0
  8. package/dist/agent/domain/runContext.js.map +1 -0
  9. package/dist/diff/domain/unifiedDiff.d.ts +19 -0
  10. package/dist/diff/domain/unifiedDiff.js +166 -0
  11. package/dist/diff/domain/unifiedDiff.js.map +1 -0
  12. package/dist/engines/domain/model/historyFollower.d.ts +41 -0
  13. package/dist/engines/domain/model/historyFollower.js +158 -0
  14. package/dist/engines/domain/model/historyFollower.js.map +1 -0
  15. package/dist/engines/domain/model/llmAgent.d.ts +42 -4
  16. package/dist/engines/domain/model/llmAgent.js +223 -23
  17. package/dist/engines/domain/model/llmAgent.js.map +1 -1
  18. package/dist/factory/infrastructure/agentFactory.d.ts +14 -0
  19. package/dist/factory/infrastructure/agentFactory.js +20 -2
  20. package/dist/factory/infrastructure/agentFactory.js.map +1 -1
  21. package/dist/factory/infrastructure/agentLLMFactory.d.ts +119 -14
  22. package/dist/factory/infrastructure/agentLLMFactory.js +284 -57
  23. package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
  24. package/dist/index.d.ts +11 -3
  25. package/dist/index.js +5 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/llm/domain/conversation.d.ts +44 -4
  28. package/dist/llm/domain/conversation.js +56 -7
  29. package/dist/llm/domain/conversation.js.map +1 -1
  30. package/dist/llm/domain/llm.d.ts +3 -1
  31. package/dist/llm/domain/llm.js.map +1 -1
  32. package/dist/llm/domain/tool.d.ts +13 -0
  33. package/dist/llm/domain/tool.js +12 -1
  34. package/dist/llm/domain/tool.js.map +1 -1
  35. package/dist/llm/infrastructure/claudeLLMClient.d.ts +3 -2
  36. package/dist/llm/infrastructure/claudeLLMClient.js +38 -10
  37. package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
  38. package/dist/llm/infrastructure/openAILLMClient.d.ts +2 -1
  39. package/dist/llm/infrastructure/openAILLMClient.js +66 -16
  40. package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
  41. package/dist/orchestration/domain/model/orchestratorAgent.d.ts +11 -2
  42. package/dist/orchestration/domain/model/orchestratorAgent.js +43 -4
  43. package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
  44. package/dist/recovery/domain/fileChanges.d.ts +31 -0
  45. package/dist/recovery/domain/fileChanges.js +48 -0
  46. package/dist/recovery/domain/fileChanges.js.map +1 -0
  47. package/dist/recovery/domain/history.d.ts +114 -0
  48. package/dist/recovery/domain/history.js +62 -0
  49. package/dist/recovery/domain/history.js.map +1 -0
  50. package/dist/recovery/domain/recoveryStore.d.ts +233 -0
  51. package/dist/recovery/domain/recoveryStore.js +32 -0
  52. package/dist/recovery/domain/recoveryStore.js.map +1 -0
  53. package/dist/recovery/domain/retention.d.ts +33 -0
  54. package/dist/recovery/domain/retention.js +90 -0
  55. package/dist/recovery/domain/retention.js.map +1 -0
  56. package/dist/recovery/domain/runChanges.d.ts +37 -0
  57. package/dist/recovery/domain/runChanges.js +109 -0
  58. package/dist/recovery/domain/runChanges.js.map +1 -0
  59. package/dist/recovery/domain/runTree.d.ts +32 -0
  60. package/dist/recovery/domain/runTree.js +90 -0
  61. package/dist/recovery/domain/runTree.js.map +1 -0
  62. package/dist/recovery/infrastructure/createHistory.d.ts +11 -0
  63. package/dist/recovery/infrastructure/createHistory.js +17 -0
  64. package/dist/recovery/infrastructure/createHistory.js.map +1 -0
  65. package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +133 -0
  66. package/dist/recovery/infrastructure/fileRecoveryStore.js +1043 -0
  67. package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -0
  68. package/dist/recovery/infrastructure/workspaceFiles.d.ts +30 -0
  69. package/dist/recovery/infrastructure/workspaceFiles.js +125 -0
  70. package/dist/recovery/infrastructure/workspaceFiles.js.map +1 -0
  71. package/dist/tools/domain/accessPolicy.d.ts +30 -0
  72. package/dist/tools/domain/accessPolicy.js +11 -0
  73. package/dist/tools/domain/accessPolicy.js.map +1 -0
  74. package/dist/tools/domain/commands.d.ts +93 -0
  75. package/dist/tools/domain/commands.js +191 -0
  76. package/dist/tools/domain/commands.js.map +1 -0
  77. package/dist/tools/domain/editResponse.d.ts +14 -0
  78. package/dist/tools/domain/editResponse.js +79 -0
  79. package/dist/tools/domain/editResponse.js.map +1 -0
  80. package/dist/tools/domain/fileEdits.d.ts +64 -0
  81. package/dist/tools/domain/fileEdits.js +21 -0
  82. package/dist/tools/domain/fileEdits.js.map +1 -0
  83. package/dist/tools/domain/lineLength.d.ts +7 -0
  84. package/dist/tools/domain/lineLength.js +26 -0
  85. package/dist/tools/domain/lineLength.js.map +1 -0
  86. package/dist/tools/domain/preparedCall.d.ts +31 -0
  87. package/dist/tools/domain/preparedCall.js +14 -0
  88. package/dist/tools/domain/preparedCall.js.map +1 -0
  89. package/dist/tools/domain/readRegistry.d.ts +46 -0
  90. package/dist/tools/domain/readRegistry.js +62 -0
  91. package/dist/tools/domain/readRegistry.js.map +1 -0
  92. package/dist/tools/domain/textLines.d.ts +26 -0
  93. package/dist/tools/domain/textLines.js +44 -0
  94. package/dist/tools/domain/textLines.js.map +1 -0
  95. package/dist/tools/domain/v4aPatch.d.ts +17 -0
  96. package/dist/tools/domain/v4aPatch.js +236 -0
  97. package/dist/tools/domain/v4aPatch.js.map +1 -0
  98. package/dist/tools/infrastructure/boundedWorkspace.d.ts +11 -2
  99. package/dist/tools/infrastructure/boundedWorkspace.js +9 -1
  100. package/dist/tools/infrastructure/boundedWorkspace.js.map +1 -1
  101. package/dist/tools/infrastructure/commandCheckpoint.d.ts +55 -0
  102. package/dist/tools/infrastructure/commandCheckpoint.js +178 -0
  103. package/dist/tools/infrastructure/commandCheckpoint.js.map +1 -0
  104. package/dist/tools/infrastructure/commandProcess.d.ts +47 -0
  105. package/dist/tools/infrastructure/commandProcess.js +217 -0
  106. package/dist/tools/infrastructure/commandProcess.js.map +1 -0
  107. package/dist/tools/infrastructure/commandTool.d.ts +36 -0
  108. package/dist/tools/infrastructure/commandTool.js +254 -0
  109. package/dist/tools/infrastructure/commandTool.js.map +1 -0
  110. package/dist/tools/infrastructure/createWorkspace.d.ts +10 -0
  111. package/dist/tools/infrastructure/createWorkspace.js +11 -3
  112. package/dist/tools/infrastructure/createWorkspace.js.map +1 -1
  113. package/dist/tools/infrastructure/defineTool.d.ts +14 -0
  114. package/dist/tools/infrastructure/defineTool.js +28 -5
  115. package/dist/tools/infrastructure/defineTool.js.map +1 -1
  116. package/dist/tools/infrastructure/editFormats.d.ts +22 -0
  117. package/dist/tools/infrastructure/editFormats.js +211 -0
  118. package/dist/tools/infrastructure/editFormats.js.map +1 -0
  119. package/dist/tools/infrastructure/fileEditor.d.ts +64 -0
  120. package/dist/tools/infrastructure/fileEditor.js +306 -0
  121. package/dist/tools/infrastructure/fileEditor.js.map +1 -0
  122. package/dist/tools/infrastructure/fileTools.d.ts +81 -0
  123. package/dist/tools/infrastructure/fileTools.js +152 -0
  124. package/dist/tools/infrastructure/fileTools.js.map +1 -0
  125. package/dist/tools/infrastructure/gitIgnoreRules.d.ts +15 -0
  126. package/dist/tools/infrastructure/gitIgnoreRules.js +74 -0
  127. package/dist/tools/infrastructure/gitIgnoreRules.js.map +1 -0
  128. package/dist/tools/infrastructure/gitWorkspace.d.ts +1 -1
  129. package/dist/tools/infrastructure/gitWorkspace.js +1 -1
  130. package/dist/tools/infrastructure/gitWorkspace.js.map +1 -1
  131. package/dist/tools/infrastructure/ignoreFileChanges.d.ts +14 -0
  132. package/dist/tools/infrastructure/ignoreFileChanges.js +62 -0
  133. package/dist/tools/infrastructure/ignoreFileChanges.js.map +1 -0
  134. package/dist/tools/infrastructure/policyWorkspace.d.ts +52 -0
  135. package/dist/tools/infrastructure/policyWorkspace.js +32 -0
  136. package/dist/tools/infrastructure/policyWorkspace.js.map +1 -0
  137. package/dist/tools/infrastructure/ripgrepWorkspace.d.ts +2 -1
  138. package/dist/tools/infrastructure/ripgrepWorkspace.js +1 -1
  139. package/dist/tools/infrastructure/ripgrepWorkspace.js.map +1 -1
  140. package/dist/tools/infrastructure/rootsAccessPolicy.d.ts +100 -0
  141. package/dist/tools/infrastructure/rootsAccessPolicy.js +301 -0
  142. package/dist/tools/infrastructure/rootsAccessPolicy.js.map +1 -0
  143. package/dist/tools/infrastructure/runProcess.d.ts +4 -1
  144. package/dist/tools/infrastructure/runProcess.js +4 -2
  145. package/dist/tools/infrastructure/runProcess.js.map +1 -1
  146. package/dist/tools/infrastructure/secretPaths.d.ts +8 -0
  147. package/dist/tools/infrastructure/secretPaths.js +76 -0
  148. package/dist/tools/infrastructure/secretPaths.js.map +1 -0
  149. package/dist/tools/infrastructure/showChangesTool.d.ts +12 -0
  150. package/dist/tools/infrastructure/showChangesTool.js +43 -0
  151. package/dist/tools/infrastructure/showChangesTool.js.map +1 -0
  152. package/dist/tools/infrastructure/trackedEdits.d.ts +33 -0
  153. package/dist/tools/infrastructure/trackedEdits.js +70 -0
  154. package/dist/tools/infrastructure/trackedEdits.js.map +1 -0
  155. package/dist/tools/infrastructure/trackedReadFile.d.ts +21 -0
  156. package/dist/tools/infrastructure/trackedReadFile.js +78 -0
  157. package/dist/tools/infrastructure/trackedReadFile.js.map +1 -0
  158. package/dist/tools/infrastructure/workspaceTools.d.ts +28 -0
  159. package/dist/tools/infrastructure/workspaceTools.js +53 -40
  160. package/dist/tools/infrastructure/workspaceTools.js.map +1 -1
  161. package/dist/tools/infrastructure/workspaceWrites.d.ts +23 -0
  162. package/dist/tools/infrastructure/workspaceWrites.js +42 -0
  163. package/dist/tools/infrastructure/workspaceWrites.js.map +1 -0
  164. package/dist/tools/infrastructure/writeSession.d.ts +35 -0
  165. package/dist/tools/infrastructure/writeSession.js +81 -0
  166. package/dist/tools/infrastructure/writeSession.js.map +1 -0
  167. package/package.json +1 -1
package/README.md CHANGED
@@ -39,9 +39,11 @@ chat loop itself never knows the difference.
39
39
  captured `session_id`, so the provider keeps the context.
40
40
  - **The model API path**: `LLMAgent` calls the OpenAI Responses API or the
41
41
  Anthropic Messages API and keeps the conversation itself, in a form that can move
42
- between providers. It runs its own tool loop over the tools it is given; the
43
- harness ships three that only read the repository (list, search, read), bounded
44
- by `.gitignore`.
42
+ between providers. It runs its own tool loop over the tools it is given. The
43
+ harness ships tools that read the repository (list, search, read), bounded by
44
+ `.gitignore`, and `createFileTools`, which also change files in each provider's
45
+ native format, every change recorded so it can be undone, and run the commands you
46
+ configure, such as the tests, recording what each one changed too.
45
47
  - **`OrchestratorAgent`**: coordinates a planner, an executor, and a reviewer
46
48
  (each an injected `Agent`) in a plan → execute → review loop. The reviewer's
47
49
  decision is a Zod-validated structured `{decision, feedback}`, not free text —
@@ -112,6 +114,9 @@ internal modules through the `imports` field of its `package.json`, so a TypeScr
112
114
  needs `moduleResolution` set to `node16`, `nodenext` or `bundler`; the legacy `node10`
113
115
  resolution cannot read that field.
114
116
 
117
+ It supports macOS and Linux. Windows is not supported for now: the tools that write files,
118
+ undo a run and run commands are built and tested on macOS and Linux only.
119
+
115
120
  An agent is built by a factory, then driven. The only output the package produces
116
121
  on its own is diagnostic warnings on stderr, from a default logger that both
117
122
  factories let you replace through their `logger` option:
@@ -152,6 +157,62 @@ example when the other one is out of quota.
152
157
  a role left out keeps the harness's own, and the reviewer must still answer with
153
158
  the JSON decision described on the option.
154
159
 
160
+ ### Validating factory options
161
+
162
+ The provider-aware guards validate command-line strings before a factory is built. Use the
163
+ catalog for help text rather than copying the accepted values; choose the catalog and guards for
164
+ the path you are using:
165
+
166
+ ```ts
167
+ import {
168
+ agentModels,
169
+ agentReasoningEfforts,
170
+ createAgent,
171
+ createLLMAgent,
172
+ isAgentProvider,
173
+ isLLMModelOf,
174
+ isLLMReasoningEffortOf,
175
+ isModelOf,
176
+ isReasoningEffortOf,
177
+ llmModels,
178
+ llmReasoningEfforts,
179
+ } from '@mikode13/harness';
180
+
181
+ const providerInput: string = process.argv[2] ?? '';
182
+ const modelInput: string = process.argv[3] ?? '';
183
+ const effortInput: string = process.argv[4] ?? '';
184
+ const useModelApi = false;
185
+
186
+ if (!isAgentProvider(providerInput)) throw new Error('Unknown provider');
187
+
188
+ if (useModelApi) {
189
+ if (
190
+ !isLLMModelOf(providerInput, modelInput) ||
191
+ !isLLMReasoningEffortOf(providerInput, effortInput)
192
+ ) {
193
+ throw new Error(
194
+ `Expected model ${llmModels[providerInput].join(', ')} and effort ${llmReasoningEfforts[providerInput].join(', ')}`,
195
+ );
196
+ }
197
+ createLLMAgent(providerInput, {
198
+ model: modelInput,
199
+ reasoningEffort: effortInput,
200
+ systemPrompt: 'Be concise.',
201
+ });
202
+ } else {
203
+ if (!isModelOf(providerInput, modelInput) || !isReasoningEffortOf(providerInput, effortInput)) {
204
+ throw new Error(
205
+ `Expected model ${agentModels[providerInput].join(', ')} and effort ${agentReasoningEfforts[providerInput].join(', ')}`,
206
+ );
207
+ }
208
+ createAgent(providerInput, { model: modelInput, reasoningEffort: effortInput });
209
+ }
210
+ ```
211
+
212
+ These guards describe the provider-wide catalogs. A client can still impose a model-specific
213
+ constraint, such as Claude Haiku accepting no reasoning effort, and the factory's client or
214
+ engine validation remains the final boundary for values that bypass the guards.
215
+
155
216
  Every agent a factory returns already retries recoverable failures. A model or
156
217
  reasoning effort the chosen provider does not support throws
157
218
  `InvalidAgentConfigError` when the agent is built. Building a Codex agent throws
@@ -164,17 +225,16 @@ The two paths authenticate differently:
164
225
 
165
226
  - `createAgent` and `createOrchestrator` use the Agent SDKs, which use the login of
166
227
  the Codex CLI and of Claude Code, so a run counts against that subscription.
167
- - `createLLMAgent` and the planner and reviewer of `createLLMOrchestrator` call the
168
- model APIs, which bill per token and need the key of each provider they use in
228
+ - `createLLMAgent` and every role of `createLLMOrchestrator`, the executor included, call
229
+ the model APIs, which bill per token and need the key of each provider they use in
169
230
  the environment: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or both for the
170
231
  orchestrator's default roles. A missing OpenAI key fails when the agent is built; a missing
171
232
  Anthropic key fails its first run.
172
233
 
173
234
  The Claude Agent SDK
174
235
  [authenticates with `ANTHROPIC_API_KEY`](https://code.claude.com/docs/en/agent-sdk) whenever
175
- it is set, so with that key in the environment a Claude agent from `createAgent`, or the executor of
176
- `createLLMOrchestrator({ provider: 'anthropic' })`, bills the key instead of the
177
- subscription.
236
+ it is set, so with that key in the environment a Claude agent from `createAgent` or `createOrchestrator`
237
+ bills the key instead of the subscription.
178
238
 
179
239
  ### Agents that own their conversation
180
240
 
@@ -236,9 +296,171 @@ neither run nor remembered. Its `key` option decides what counts as the same cal
236
296
  such as the tool and its path. The memory lives as long as the approver you created,
237
297
  so create one per session, per user or per run.
238
298
 
239
- `createLLMOrchestrator()`
240
- takes the same options as `createOrchestrator()` and returns a promise; its executor
241
- still runs on an Agent SDK, because the harness's own tools cannot change files.
299
+ ### Agents that change files
300
+
301
+ `createFileTools` gives an agent the file tools of its provider, over the folders you
302
+ declare, each `read` or `write`:
303
+
304
+ - **OpenAI** gets `listFiles`, `searchText`, `readFile` and its native `apply_patch`.
305
+ - **Anthropic** gets `listFiles`, `searchText`, its native text editor (whose `view`
306
+ reads) and `delete_file`.
307
+
308
+ Give the agent the same `workspace`, so it follows what the history undoes:
309
+
310
+ ```ts
311
+ import { createFileTools, createLLMAgent, type WorkspaceOptions } from '@mikode13/harness';
312
+
313
+ const workspace: WorkspaceOptions = {
314
+ roots: [
315
+ { path: process.cwd(), access: 'write' },
316
+ { path: '/srv/standards', access: 'read' },
317
+ ],
318
+ };
319
+ const agent = createLLMAgent('openai', {
320
+ systemPrompt: 'You fix bugs, with a test for each.',
321
+ tools: await createFileTools('openai', workspace),
322
+ workspace,
323
+ });
324
+ ```
325
+
326
+ Every tool goes through one access policy:
327
+
328
+ - it refuses a path outside the roots;
329
+ - it refuses a file `.gitignore` excludes;
330
+ - it refuses git's metadata, the history itself, and a secret such as `.env`. Add more
331
+ secrets with `secrets.protect`, or open exact files with `secrets.allow`.
332
+
333
+ A change happens only in a `write` root, and only from a version of the file the agent
334
+ read: editing a file it never read is `READ_REQUIRED`, and editing one that changed since
335
+ is `STALE_FILE`. Every change is recorded before it is made, so the history can undo the
336
+ run.
337
+
338
+ With `workspace`, the agent's system prompt is followed by the roots and how to name a file
339
+ in them. After the user undoes runs, the agent's next run starts from its conversation as it
340
+ was then, with a note on what was undone and why. By default that note carries a summary
341
+ from the provider's cheap model, Claude Haiku or `gpt-5.6-luna`, billed with the run;
342
+ `summarizeUndone: false` keeps to the undone prompts. A run that changed a file names its
343
+ run in `AgentResponse.runId`. Progress reports each change as a unified `diff` on the tool's
344
+ `completed` event.
345
+
346
+ `createLLMOrchestrator()` takes the options of `createOrchestrator()` plus `workspace`,
347
+ `maxSteps`, `commands` and `summarizeUndone`, and returns a promise. Every role runs on the
348
+ model APIs:
349
+
350
+ - the planner and the reviewer read, with a `showChanges` tool that gives the current task's
351
+ changes as far as each may read them;
352
+ - the executor gets its provider's file tools. `autoApprove` reaches only the executor;
353
+ - each role gets the commands whose `roles` name it; a command names only the executor unless
354
+ it says otherwise.
355
+
356
+ Each role's run stops after a number of calls to the model: 80 for the executor and 65 for
357
+ the planner and the reviewer by default. `maxSteps` changes it, as one number for every role
358
+ or per role, with no upper bound:
359
+
360
+ ```ts
361
+ createLLMOrchestrator({ workspace, maxSteps: { executor: 120 } });
362
+ ```
363
+
364
+ A run also stops sooner when five answers in a row ask for exactly the same tool calls,
365
+ which means the model is stuck. `createLLMAgent` takes one number, 65 by default.
366
+
367
+ Without `workspace` it works on the current directory, read only. One run of the
368
+ orchestrator is one run of the history, every round included.
369
+
370
+ ### Running the project's commands
371
+
372
+ With `commands`, `createFileTools` and `createLLMOrchestrator` add `runCommand`, which runs
373
+ one of the commands you configure, by name. Nothing goes through a shell, and the model
374
+ fills only the parameters a command declares:
375
+
376
+ ```ts
377
+ const tools = await createFileTools('openai', workspace, {
378
+ commands: {
379
+ test: {
380
+ description: 'Runs the tests. Give the test files to run only those.',
381
+ executable: 'pnpm',
382
+ args: ['vitest', 'run', '--', { type: 'paths', name: 'files', description: 'Test files' }],
383
+ },
384
+ format: { description: 'Formats the repository', executable: 'pnpm', args: ['format'] },
385
+ },
386
+ });
387
+ ```
388
+
389
+ - **Parameters:** `paths` inside the roots that the agent may read; a `choice` among the
390
+ values you list; or `text` that matches your pattern and never starts with `-`.
391
+ - **Approval:** every call asks the approver, which sees the exact program, arguments and
392
+ folder in `request.command`. `autoApprove` skips it.
393
+ - **Roles:** in `createLLMOrchestrator`, a command goes to the executor, or to the roles its
394
+ `roles` lists, such as `['planner', 'executor']` for one that reads an issue. Whoever runs it,
395
+ its files are captured around it. `createFileTools` offers every command.
396
+ - **Environment:** a command inherits only `PATH`, `HOME`, `TMPDIR`, `USER`, `SHELL`, `LANG`
397
+ and `LC_*`, plus its own `env`. The provider keys never reach it.
398
+ - **Limits:** 10 minutes, then its whole process group is stopped (`timeoutMs`). The model
399
+ reads the last 16 KB of its output (`outputBytes`).
400
+ - **History:** the files of the `write` roots are captured before each command and compared
401
+ after it. What it changed is recorded in the history, so `undo` takes it back.
402
+ - Not covered: what `.gitignore` excludes (`node_modules`, build output), secrets, files
403
+ outside the roots, the network and remote services.
404
+ - A timeout or a cancellation is never reported as a finished command.
405
+ - **Not on Windows yet.**
406
+
407
+ A command is trusted execution, not a sandbox. A test runs the repository's code, including
408
+ code the model just wrote, with everything your account can reach. The secret rules of the
409
+ file tools do not apply to it, so a command can print a secret into the model's context.
410
+ Configure only commands you would run yourself on that repository.
411
+
412
+ ### Going back through what an agent wrote
413
+
414
+ The harness's write tools record every run that changes files, outside the repository,
415
+ under the platform's state directory. `createHistory` reads that record and moves the
416
+ workspace through it:
417
+
418
+ ```ts
419
+ import { createHistory, historyStart } from '@mikode13/harness';
420
+
421
+ const history = await createHistory({ root: process.cwd() });
422
+ const { runId } = await agent.run('Rename the helper.', { signal: controller.signal });
423
+
424
+ if (runId) console.log(await history.changes(runId)); // a git-style diff
425
+ await history.undo({ reason: 'keep the old name' });
426
+ await history.redo();
427
+ await history.goTo(historyStart); // as it was before the agent's first run
428
+ ```
429
+
430
+ `list()` gives every run kept, oldest first, with its parent, status and the files it
431
+ changed, and the run the workspace is at (`head`), absent at the start. Runs form a tree:
432
+ going back and writing again starts a new branch, and `goTo` reaches the old one. `redo`
433
+ returns towards where the workspace was most recently. A run's `runId` is in its
434
+ `AgentResponse` when it changed a file, and a run that failed or was cancelled after
435
+ writing is still listed, with that status. A run that changed nothing is not kept.
436
+
437
+ A move writes a file only if it still holds what the history expects. A file you
438
+ changed since is left as it is and named in the move's `conflicts`, and the move is
439
+ then not `complete`. The reason is kept with the move.
440
+
441
+ A move can be tied to where the workspace is. Pass `from`, a run or `historyStart`, and
442
+ the move happens only if the workspace is still there; otherwise it fails with
443
+ `WorkspaceMovedError` and changes nothing. Pass it when you showed the user what a move
444
+ will do, so a run that ends while they answer cannot make it do something else:
445
+
446
+ ```ts
447
+ const { head } = await history.list();
448
+ // … show the user what undoing `head` does, and wait for a yes …
449
+ await history.undo({ from: head ?? historyStart, reason });
450
+ ```
451
+
452
+ A consumer branches on five errors: `WorkspaceBusyError` while a run or another move
453
+ holds the workspace, `WorkspaceMovedError` when it is not at `from`,
454
+ `NothingToMoveError` with nothing to undo or redo,
455
+ `UnknownRunError` for a run the workspace never had, and `HistoryExpiredError` for one
456
+ retention took. The history keeps 25 runs per workspace. Older runs are chained into
457
+ the next, so `historyStart` always restores the original state. For a chained run,
458
+ the error's `keptIn` names the run that now holds its changes, and that run's
459
+ `absorbed` in `list()` names it.
460
+
461
+ The history is not version control. It holds only what the harness's own tools wrote:
462
+ not your edits, not other programs', and not the Agent SDK engines' changes. The lock
463
+ keeps a second harness run out, but not an editor.
242
464
 
243
465
  ## Tests
244
466
 
@@ -258,19 +480,59 @@ pnpm run dev
258
480
  ```
259
481
 
260
482
  The agents work on the repository root, which is the directory the script runs in.
261
- `pnpm run dev --llm` plans and reviews on the model APIs instead, through
483
+ `pnpm run dev --llm` runs all three roles on the model APIs instead, through
262
484
  `createLLMOrchestrator`, and needs the API keys in the environment (see
263
485
  [Authentication](#authentication)).
264
486
 
265
487
  Type your prompt at `>`. Press Ctrl+C while idle at the prompt to exit; pressing
266
488
  it while an agent is running cancels only that turn and returns to the prompt.
267
489
 
490
+ A line that starts with `/` is a command for the CLI, not a prompt. Four commands
491
+ move through the [history of what the agents wrote](#going-back-through-what-an-agent-wrote),
492
+ without the model:
493
+
494
+ - `/history` shows the tree of runs and marks where the workspace is.
495
+ `/history --files` adds the files each run changed.
496
+ - `/undo` goes back to the run before the current one.
497
+ - `/redo` goes forward again.
498
+ - `/goto <run | start>` goes to a run, named by its id or its last six characters, or
499
+ to `start`, before any run.
500
+
501
+ Each move says what it will do and changes nothing until you answer `y`. It then
502
+ asks why, and records the reason, if you give one, with the move. A move that left
503
+ files as they were, because they changed since, names them. With `--llm` the executor
504
+ writes to the current directory through the harness's tools, so its runs are in that
505
+ history. The Agent SDK path changes files through its own tools, which the history does not
506
+ record.
507
+
268
508
  The CLI asks in the terminal before a destructive tool call runs: yes, always for
269
- that tool until the CLI exits, or no with an optional reason for the model. No
270
- agent it builds asks yet, because the harness's own tools only read and
271
- `autoApprove` is on.
509
+ that tool until the CLI exits, or no with an optional reason for the model. With
510
+ piped input nobody can answer, so such a call is denied: a line written for a later
511
+ question never approves one. Piped lines answer the prompt and the history commands in
512
+ order. With `--llm`, the executor asks before a destructive change, such as one that would
513
+ make `.gitignore` hide less.
514
+
515
+ With `--llm`, the agents can also [run commands](#running-the-projects-commands) you
516
+ configure for the repository, in `~/.config/mikode-harness/config.json` (or under
517
+ `$XDG_CONFIG_HOME`). The file maps each repository's real path to its `commands` and,
518
+ optionally, its `maxSteps`. It lives outside every repository, so neither a cloned repository
519
+ nor the agent decides what runs, or for how long:
520
+
521
+ ```json
522
+ {
523
+ "/Users/me/code/app": {
524
+ "commands": {
525
+ "test": { "description": "Runs the tests", "executable": "pnpm", "args": ["test"] }
526
+ },
527
+ "maxSteps": { "executor": 120 }
528
+ }
529
+ }
530
+ ```
531
+
532
+ The CLI shows each command as the line it will run before asking, and "always" allows
533
+ that command again, not every command.
272
534
 
273
- `cli/cli.ts` currently enables `autoApprove` for its trusted backend agents.
535
+ `cli/cli.ts` currently enables `autoApprove` for its Agent SDK agents, not for `--llm`.
274
536
  This maps to each provider's permission-bypass mode and grants those processes
275
537
  unrestricted command access. Keep it disabled when the host may receive untrusted
276
538
  prompts, or provide an approval workflow from the entry point.
@@ -14,6 +14,12 @@ export interface AgentResponse {
14
14
  tokens?: Tokens;
15
15
  /** Wall-clock seconds the consumer waited for the run, retries and every role included. */
16
16
  duration: number;
17
+ /**
18
+ * The run the workspace's history recorded, when this run wrote through the harness's write
19
+ * tools. Absent for a run that wrote nothing, and from the Agent SDK engines, whose changes
20
+ * the history does not record.
21
+ */
22
+ runId?: string;
17
23
  }
18
24
  export type Callback = (item: ProgressEvent) => void;
19
25
  /** What a run needs besides its prompt. Decorators pass it on whole, so a new field reaches every agent. */
@@ -71,6 +77,12 @@ export type ProgressEvent = {
71
77
  * it never ran because no one allowed it, with no `in_progress` before it.
72
78
  */
73
79
  status: 'in_progress' | 'completed' | 'error' | 'denied';
80
+ /**
81
+ * On `completed`, for a call that changed a file through the harness's write tools: the
82
+ * change as a unified diff, for the person watching. Narration only; the run's whole
83
+ * record is the history's `changes`.
84
+ */
85
+ diff?: string;
74
86
  } | {
75
87
  type: 'todoList';
76
88
  items: {
@@ -1 +1 @@
1
- {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"AAmCA,gDAAgD;AAChD,MAAM,CAAC,MAAM,cAAc,GAAa,GAAG,EAAE,CAAC,SAAS,CAAC","sourcesContent":["import type { Tokens } from '#src/shared/domain/tokens';\nimport type { Approver } from './approval.ts';\n\n/**\n * How a run ended when it did not fail. Every token a run spends travels with its end: here\n * when it finishes, in `RecoverableError.tokens` or `UnrecoverableError.tokens` when it fails.\n */\nexport interface AgentResponse {\n\t/** The final text. Empty when the run produced none, for example a turn that only edited files. */\n\tresponse: string;\n\t/**\n\t * Missing when any call in the run completed without reporting usage, which is not the same\n\t * as zero: a partial sum would look complete.\n\t */\n\ttokens?: Tokens;\n\t/** Wall-clock seconds the consumer waited for the run, retries and every role included. */\n\tduration: number;\n}\n\nexport type Callback = (item: ProgressEvent) => void;\n\n/** What a run needs besides its prompt. Decorators pass it on whole, so a new field reaches every agent. */\nexport interface RunOptions {\n\t/** Cancels the run, which then rejects with the signal's `AbortError`. */\n\tsignal: AbortSignal;\n\t/** Receives the run's live activity. Without it, the run is silent. */\n\tonProgress?: Callback | undefined;\n\t/**\n\t * Asked before a model-backed agent runs a `destructive` tool call. Without it, such a call\n\t * is denied. An agent built with `autoApprove` never asks, and the Agent SDK engines ignore\n\t * it: their own permission systems decide.\n\t */\n\tapprove?: Approver | undefined;\n}\n\n/** The `onProgress` of a run that gave none. */\nexport const ignoreProgress: Callback = () => undefined;\n\n/**\n * Live activity during a run, for narration only: the result travels in `AgentResponse`.\n * New event types can arrive in a minor release, so render the types you know and ignore\n * the rest instead of switching over them exhaustively with a `never` check.\n */\nexport type ProgressEvent =\n\t| { type: 'command'; command: string; exitCode?: number }\n\t| { type: 'reasoning'; message: string }\n\t| { type: 'search'; query: string }\n\t| { type: 'fileChange'; changes: { path: string; kind: 'add' | 'update' | 'delete' }[] }\n\t| { type: 'mcpTool'; server: string; tool: string; status: string }\n\t| { type: 'agentMessage'; message: string }\n\t/** `id` pairs a call's end with its start when one step calls the same tool more than once. */\n\t| {\n\t\t\ttype: 'tool';\n\t\t\tid: string;\n\t\t\tname: string;\n\t\t\t/**\n\t\t\t * `in_progress` only once the call is about to run, after any approval. `denied` when\n\t\t\t * it never ran because no one allowed it, with no `in_progress` before it.\n\t\t\t */\n\t\t\tstatus: 'in_progress' | 'completed' | 'error' | 'denied';\n\t }\n\t| { type: 'todoList'; items: { text: string; completed: boolean }[] }\n\t| { type: 'turnStarted' }\n\t| { type: 'turnEnded' };\n\n/**\n * The contract every engine (CodexAgent, ClaudeAgent, future providers) and every\n * decorator (RetryingAgent, OrchestratorAgent) is built against.\n *\n * Implementers MUST only ever reject with `RecoverableError` or `UnrecoverableError`\n * (see src/shared/domain/errors.ts) — never a raw SDK error, a plain `Error`, or anything else leaked\n * unclassified. Every consumer of `Agent` (RetryingAgent's retry decision,\n * OrchestratorAgent's failure handling) `instanceof`-checks against those two types to\n * decide what to do next; a leaked, unclassified error bypasses that decision\n * entirely — it gets retried when it shouldn't be, or crashes a run that a retry\n * would have recovered. Wrap every call into the underlying SDK so nothing escapes\n * unclassified, including failures the SDK itself doesn't model as a domain error\n * (network errors, malformed responses, etc.); `classifyProviderFailure` does this.\n *\n * Cancellation is the one exception: an `AbortError` must propagate unchanged, because\n * consumers check for it before either error type.\n */\nexport interface Agent {\n\trun(prompt: string, options: RunOptions): Promise<AgentResponse>;\n}\n"]}
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"AAyCA,gDAAgD;AAChD,MAAM,CAAC,MAAM,cAAc,GAAa,GAAG,EAAE,CAAC,SAAS,CAAC","sourcesContent":["import type { Tokens } from '#src/shared/domain/tokens';\nimport type { Approver } from './approval.ts';\n\n/**\n * How a run ended when it did not fail. Every token a run spends travels with its end: here\n * when it finishes, in `RecoverableError.tokens` or `UnrecoverableError.tokens` when it fails.\n */\nexport interface AgentResponse {\n\t/** The final text. Empty when the run produced none, for example a turn that only edited files. */\n\tresponse: string;\n\t/**\n\t * Missing when any call in the run completed without reporting usage, which is not the same\n\t * as zero: a partial sum would look complete.\n\t */\n\ttokens?: Tokens;\n\t/** Wall-clock seconds the consumer waited for the run, retries and every role included. */\n\tduration: number;\n\t/**\n\t * The run the workspace's history recorded, when this run wrote through the harness's write\n\t * tools. Absent for a run that wrote nothing, and from the Agent SDK engines, whose changes\n\t * the history does not record.\n\t */\n\trunId?: string;\n}\n\nexport type Callback = (item: ProgressEvent) => void;\n\n/** What a run needs besides its prompt. Decorators pass it on whole, so a new field reaches every agent. */\nexport interface RunOptions {\n\t/** Cancels the run, which then rejects with the signal's `AbortError`. */\n\tsignal: AbortSignal;\n\t/** Receives the run's live activity. Without it, the run is silent. */\n\tonProgress?: Callback | undefined;\n\t/**\n\t * Asked before a model-backed agent runs a `destructive` tool call. Without it, such a call\n\t * is denied. An agent built with `autoApprove` never asks, and the Agent SDK engines ignore\n\t * it: their own permission systems decide.\n\t */\n\tapprove?: Approver | undefined;\n}\n\n/** The `onProgress` of a run that gave none. */\nexport const ignoreProgress: Callback = () => undefined;\n\n/**\n * Live activity during a run, for narration only: the result travels in `AgentResponse`.\n * New event types can arrive in a minor release, so render the types you know and ignore\n * the rest instead of switching over them exhaustively with a `never` check.\n */\nexport type ProgressEvent =\n\t| { type: 'command'; command: string; exitCode?: number }\n\t| { type: 'reasoning'; message: string }\n\t| { type: 'search'; query: string }\n\t| { type: 'fileChange'; changes: { path: string; kind: 'add' | 'update' | 'delete' }[] }\n\t| { type: 'mcpTool'; server: string; tool: string; status: string }\n\t| { type: 'agentMessage'; message: string }\n\t/** `id` pairs a call's end with its start when one step calls the same tool more than once. */\n\t| {\n\t\t\ttype: 'tool';\n\t\t\tid: string;\n\t\t\tname: string;\n\t\t\t/**\n\t\t\t * `in_progress` only once the call is about to run, after any approval. `denied` when\n\t\t\t * it never ran because no one allowed it, with no `in_progress` before it.\n\t\t\t */\n\t\t\tstatus: 'in_progress' | 'completed' | 'error' | 'denied';\n\t\t\t/**\n\t\t\t * On `completed`, for a call that changed a file through the harness's write tools: the\n\t\t\t * change as a unified diff, for the person watching. Narration only; the run's whole\n\t\t\t * record is the history's `changes`.\n\t\t\t */\n\t\t\tdiff?: string;\n\t }\n\t| { type: 'todoList'; items: { text: string; completed: boolean }[] }\n\t| { type: 'turnStarted' }\n\t| { type: 'turnEnded' };\n\n/**\n * The contract every engine (CodexAgent, ClaudeAgent, future providers) and every\n * decorator (RetryingAgent, OrchestratorAgent) is built against.\n *\n * Implementers MUST only ever reject with `RecoverableError` or `UnrecoverableError`\n * (see src/shared/domain/errors.ts) — never a raw SDK error, a plain `Error`, or anything else leaked\n * unclassified. Every consumer of `Agent` (RetryingAgent's retry decision,\n * OrchestratorAgent's failure handling) `instanceof`-checks against those two types to\n * decide what to do next; a leaked, unclassified error bypasses that decision\n * entirely — it gets retried when it shouldn't be, or crashes a run that a retry\n * would have recovered. Wrap every call into the underlying SDK so nothing escapes\n * unclassified, including failures the SDK itself doesn't model as a domain error\n * (network errors, malformed responses, etc.); `classifyProviderFailure` does this.\n *\n * Cancellation is the one exception: an `AbortError` must propagate unchanged, because\n * consumers check for it before either error type.\n */\nexport interface Agent {\n\trun(prompt: string, options: RunOptions): Promise<AgentResponse>;\n}\n"]}
@@ -5,12 +5,21 @@
5
5
  * - `destructive` loses something, as with overwriting or deleting a file.
6
6
  */
7
7
  export type ToolRisk = 'safe' | 'mutating' | 'destructive';
8
+ /** A program a tool call will run, exactly as it will run: no shell reads it. */
9
+ export interface CommandLine {
10
+ executable: string;
11
+ args: string[];
12
+ /** Where it runs, as a real path. */
13
+ cwd: string;
14
+ }
8
15
  /** A tool call waiting for someone to allow it. */
9
16
  export interface ApprovalRequest {
10
17
  tool: string;
11
18
  /** What the model sent, unvalidated: the tool validates it only if the call is allowed. */
12
19
  input: unknown;
13
20
  risk: ToolRisk;
21
+ /** For a call that runs a configured command: the program, worked out from the input. */
22
+ command?: CommandLine;
14
23
  }
15
24
  /** A denial's `reason` reaches the model, so it can try something else. */
16
25
  export type ApprovalDecision = {
@@ -1 +1 @@
1
- {"version":3,"file":"approval.js","sourceRoot":"","sources":["../../../src/agent/domain/approval.ts"],"names":[],"mappings":"AAmCA;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAChC,GAGyD,EACzD,EAAE,GAAG,GAAG,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,KAAqD,EAAE;IAEtF,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAElC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;QAChC,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;QACxB,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAE/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1C,0FAA0F;QAC1F,MAAM,CAAC,cAAc,EAAE,CAAC;QACxB,8FAA8F;QAC9F,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,CAAC,QAAQ,CAAC,QAAQ;YAAE,OAAO,QAAQ,CAAC;QACxC,IAAI,YAAY,CAAC,MAAM,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1C,OAAO,QAAQ,CAAC;IACjB,CAAC,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAe;IAC3C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACnD,MAAM,IAAI,SAAS,CAAC,0CAA0C,CAAC,CAAC;IACjE,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAkD,CAAC;IAChF,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QACnC,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;IAC3E,CAAC;IAED,OAAO,QAAQ;QACd,CAAC,CAAC,EAAE,QAAQ,EAAE;QACd,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;AAC1E,CAAC;AAED,SAAS,YAAY,CAAC,MAAe;IACpC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAgC,CAAC;IACtD,IAAI,QAAQ,KAAK,SAAS,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC7D,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,QAAQ,KAAK,IAAI,CAAC;AAC1B,CAAC","sourcesContent":["/**\n * How much one tool call can hurt, judged by the tool from that call's input:\n * - `safe` changes nothing, as with a read.\n * - `mutating` makes a change git can undo, as with creating a file.\n * - `destructive` loses something, as with overwriting or deleting a file.\n */\nexport type ToolRisk = 'safe' | 'mutating' | 'destructive';\n\n/** A tool call waiting for someone to allow it. */\nexport interface ApprovalRequest {\n\ttool: string;\n\t/** What the model sent, unvalidated: the tool validates it only if the call is allowed. */\n\tinput: unknown;\n\trisk: ToolRisk;\n}\n\n/** A denial's `reason` reaches the model, so it can try something else. */\nexport type ApprovalDecision =\n\t{ approved: true } | { approved: false; reason?: string | undefined };\n\n/**\n * Decides one tool call at a time. A consumer that wants \"don't ask again\" remembers its own\n * answers, so the harness keeps no session state. A throw ends the run as an\n * `UnrecoverableError`.\n */\nexport type Approver = (\n\trequest: ApprovalRequest,\n\tsignal: AbortSignal,\n) => ApprovalDecision | Promise<ApprovalDecision>;\n\n/** What `rememberApprovals` asks for: `remember` allows the same call again without asking. */\nexport type RememberableDecision =\n\t| { approved: true; remember?: boolean | undefined }\n\t| { approved: false; reason?: string | undefined };\n\n/**\n * Wraps `ask` so a call the user allowed with `remember` is not asked about again. `key` decides\n * what counts as the same call; it defaults to the tool's name, so \"always\" allows every\n * destructive call to that tool. The memory lives in the returned approver, so its lifetime is\n * the consumer's choice: one per session, one per user, or one per run.\n */\nexport function rememberApprovals(\n\task: (\n\t\trequest: ApprovalRequest,\n\t\tsignal: AbortSignal,\n\t) => RememberableDecision | Promise<RememberableDecision>,\n\t{ key = request => request.tool }: { key?: (request: ApprovalRequest) => string } = {},\n): Approver {\n\tconst allowed = new Set<string>();\n\n\treturn async (request, signal) => {\n\t\tconst id = key(request);\n\t\tif (allowed.has(id)) return { approved: true };\n\n\t\tconst answer = await ask(request, signal);\n\t\t// The agent discards an answer that arrives after a cancellation, so it must not be kept.\n\t\tsignal.throwIfAborted();\n\t\t// Read before keeping anything: a truthy `'false'` would otherwise be allowed and remembered.\n\t\tconst decision = readDecision(answer);\n\t\tif (!decision.approved) return decision;\n\t\tif (readRemember(answer)) allowed.add(id);\n\t\treturn decision;\n\t};\n}\n\n/**\n * Reads an approver's answer without trusting its type, since a JavaScript consumer can return\n * anything: only an `approved` that is a boolean decides, and anything else throws.\n */\nexport function readDecision(answer: unknown): ApprovalDecision {\n\tif (typeof answer !== 'object' || answer === null) {\n\t\tthrow new TypeError('The approver answered without a decision');\n\t}\n\tconst { approved, reason } = answer as { approved?: unknown; reason?: unknown };\n\tif (typeof approved !== 'boolean') {\n\t\tthrow new TypeError('The approver answered without a boolean `approved`');\n\t}\n\n\treturn approved\n\t\t? { approved }\n\t\t: { approved, reason: typeof reason === 'string' ? reason : undefined };\n}\n\nfunction readRemember(answer: unknown): boolean {\n\tconst { remember } = answer as { remember?: unknown };\n\tif (remember !== undefined && typeof remember !== 'boolean') {\n\t\tthrow new TypeError('The approver answered without a boolean `remember`');\n\t}\n\treturn remember === true;\n}\n"]}
1
+ {"version":3,"file":"approval.js","sourceRoot":"","sources":["../../../src/agent/domain/approval.ts"],"names":[],"mappings":"AA6CA;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAChC,GAGyD,EACzD,EAAE,GAAG,GAAG,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,KAAqD,EAAE;IAEtF,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAElC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;QAChC,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;QACxB,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAE/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1C,0FAA0F;QAC1F,MAAM,CAAC,cAAc,EAAE,CAAC;QACxB,8FAA8F;QAC9F,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,CAAC,QAAQ,CAAC,QAAQ;YAAE,OAAO,QAAQ,CAAC;QACxC,IAAI,YAAY,CAAC,MAAM,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1C,OAAO,QAAQ,CAAC;IACjB,CAAC,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAe;IAC3C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACnD,MAAM,IAAI,SAAS,CAAC,0CAA0C,CAAC,CAAC;IACjE,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAkD,CAAC;IAChF,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QACnC,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;IAC3E,CAAC;IAED,OAAO,QAAQ;QACd,CAAC,CAAC,EAAE,QAAQ,EAAE;QACd,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;AAC1E,CAAC;AAED,SAAS,YAAY,CAAC,MAAe;IACpC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAgC,CAAC;IACtD,IAAI,QAAQ,KAAK,SAAS,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC7D,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,QAAQ,KAAK,IAAI,CAAC;AAC1B,CAAC","sourcesContent":["/**\n * How much one tool call can hurt, judged by the tool from that call's input:\n * - `safe` changes nothing, as with a read.\n * - `mutating` makes a change git can undo, as with creating a file.\n * - `destructive` loses something, as with overwriting or deleting a file.\n */\nexport type ToolRisk = 'safe' | 'mutating' | 'destructive';\n\n/** A program a tool call will run, exactly as it will run: no shell reads it. */\nexport interface CommandLine {\n\texecutable: string;\n\targs: string[];\n\t/** Where it runs, as a real path. */\n\tcwd: string;\n}\n\n/** A tool call waiting for someone to allow it. */\nexport interface ApprovalRequest {\n\ttool: string;\n\t/** What the model sent, unvalidated: the tool validates it only if the call is allowed. */\n\tinput: unknown;\n\trisk: ToolRisk;\n\t/** For a call that runs a configured command: the program, worked out from the input. */\n\tcommand?: CommandLine;\n}\n\n/** A denial's `reason` reaches the model, so it can try something else. */\nexport type ApprovalDecision =\n\t{ approved: true } | { approved: false; reason?: string | undefined };\n\n/**\n * Decides one tool call at a time. A consumer that wants \"don't ask again\" remembers its own\n * answers, so the harness keeps no session state. A throw ends the run as an\n * `UnrecoverableError`.\n */\nexport type Approver = (\n\trequest: ApprovalRequest,\n\tsignal: AbortSignal,\n) => ApprovalDecision | Promise<ApprovalDecision>;\n\n/** What `rememberApprovals` asks for: `remember` allows the same call again without asking. */\nexport type RememberableDecision =\n\t| { approved: true; remember?: boolean | undefined }\n\t| { approved: false; reason?: string | undefined };\n\n/**\n * Wraps `ask` so a call the user allowed with `remember` is not asked about again. `key` decides\n * what counts as the same call; it defaults to the tool's name, so \"always\" allows every\n * destructive call to that tool. The memory lives in the returned approver, so its lifetime is\n * the consumer's choice: one per session, one per user, or one per run.\n */\nexport function rememberApprovals(\n\task: (\n\t\trequest: ApprovalRequest,\n\t\tsignal: AbortSignal,\n\t) => RememberableDecision | Promise<RememberableDecision>,\n\t{ key = request => request.tool }: { key?: (request: ApprovalRequest) => string } = {},\n): Approver {\n\tconst allowed = new Set<string>();\n\n\treturn async (request, signal) => {\n\t\tconst id = key(request);\n\t\tif (allowed.has(id)) return { approved: true };\n\n\t\tconst answer = await ask(request, signal);\n\t\t// The agent discards an answer that arrives after a cancellation, so it must not be kept.\n\t\tsignal.throwIfAborted();\n\t\t// Read before keeping anything: a truthy `'false'` would otherwise be allowed and remembered.\n\t\tconst decision = readDecision(answer);\n\t\tif (!decision.approved) return decision;\n\t\tif (readRemember(answer)) allowed.add(id);\n\t\treturn decision;\n\t};\n}\n\n/**\n * Reads an approver's answer without trusting its type, since a JavaScript consumer can return\n * anything: only an `approved` that is a boolean decides, and anything else throws.\n */\nexport function readDecision(answer: unknown): ApprovalDecision {\n\tif (typeof answer !== 'object' || answer === null) {\n\t\tthrow new TypeError('The approver answered without a decision');\n\t}\n\tconst { approved, reason } = answer as { approved?: unknown; reason?: unknown };\n\tif (typeof approved !== 'boolean') {\n\t\tthrow new TypeError('The approver answered without a boolean `approved`');\n\t}\n\n\treturn approved\n\t\t? { approved }\n\t\t: { approved, reason: typeof reason === 'string' ? reason : undefined };\n}\n\nfunction readRemember(answer: unknown): boolean {\n\tconst { remember } = answer as { remember?: unknown };\n\tif (remember !== undefined && typeof remember !== 'boolean') {\n\t\tthrow new TypeError('The approver answered without a boolean `remember`');\n\t}\n\treturn remember === true;\n}\n"]}
@@ -0,0 +1,31 @@
1
+ import type { RunOptions } from './agent.ts';
2
+ export type RunEnd = 'completed' | 'failed' | 'cancelled';
3
+ /**
4
+ * What one top-level run shares with every agent and tool inside it, across all its roles,
5
+ * rounds and retries, such as the record of what it wrote. Whatever a tool starts for the run
6
+ * registers how to end it, and the agent that created the context ends it with the run.
7
+ *
8
+ * Internal: it travels in `RunOptions` under a symbol, so decorators, which pass the options on
9
+ * whole, carry it without knowing it exists, and no consumer can see or set it.
10
+ */
11
+ export declare class RunContext {
12
+ private readonly finishers;
13
+ /**
14
+ * The run the workspace's history recorded for this run, once it first wrote. The object is
15
+ * shared, not copied: a turn recorded before the first write still learns its run.
16
+ */
17
+ readonly historyRun: {
18
+ runId?: string;
19
+ };
20
+ /** Ends something the run started, when the run ends. */
21
+ onFinish(finisher: (end: RunEnd) => Promise<void>): void;
22
+ /**
23
+ * Ends everything registered, all of it even when one fails, and then throws the first
24
+ * failure.
25
+ */
26
+ finish(end: RunEnd): Promise<void>;
27
+ }
28
+ /** The context of the run these options belong to, if an outer agent created one. */
29
+ export declare function runContextOf(options: RunOptions): RunContext | undefined;
30
+ /** The same options, carrying `context` for every agent the run reaches. */
31
+ export declare function withRunContext(options: RunOptions, context: RunContext): RunOptions;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * What one top-level run shares with every agent and tool inside it, across all its roles,
3
+ * rounds and retries, such as the record of what it wrote. Whatever a tool starts for the run
4
+ * registers how to end it, and the agent that created the context ends it with the run.
5
+ *
6
+ * Internal: it travels in `RunOptions` under a symbol, so decorators, which pass the options on
7
+ * whole, carry it without knowing it exists, and no consumer can see or set it.
8
+ */
9
+ export class RunContext {
10
+ finishers = [];
11
+ /**
12
+ * The run the workspace's history recorded for this run, once it first wrote. The object is
13
+ * shared, not copied: a turn recorded before the first write still learns its run.
14
+ */
15
+ historyRun = {};
16
+ /** Ends something the run started, when the run ends. */
17
+ onFinish(finisher) {
18
+ this.finishers.push(finisher);
19
+ }
20
+ /**
21
+ * Ends everything registered, all of it even when one fails, and then throws the first
22
+ * failure.
23
+ */
24
+ async finish(end) {
25
+ const failures = [];
26
+ for (const finisher of this.finishers.splice(0)) {
27
+ try {
28
+ await finisher(end);
29
+ }
30
+ catch (error) {
31
+ failures.push(error);
32
+ }
33
+ }
34
+ if (failures.length > 0)
35
+ throw failures[0];
36
+ }
37
+ }
38
+ const runContextKey = Symbol('mikode-harness.runContext');
39
+ /** The context of the run these options belong to, if an outer agent created one. */
40
+ export function runContextOf(options) {
41
+ return options[runContextKey];
42
+ }
43
+ /** The same options, carrying `context` for every agent the run reaches. */
44
+ export function withRunContext(options, context) {
45
+ return { ...options, [runContextKey]: context };
46
+ }
47
+ //# sourceMappingURL=runContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runContext.js","sourceRoot":"","sources":["../../../src/agent/domain/runContext.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,MAAM,OAAO,UAAU;IACL,SAAS,GAAuC,EAAE,CAAC;IAEpE;;;OAGG;IACM,UAAU,GAAuB,EAAE,CAAC;IAE7C,yDAAyD;IACzD,QAAQ,CAAC,QAAwC;QAChD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC/B,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,MAAM,CAAC,GAAW;QACvB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YACjD,IAAI,CAAC;gBACJ,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;YACrB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC;QACF,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC;CACD;AAED,MAAM,aAAa,GAAG,MAAM,CAAC,2BAA2B,CAAC,CAAC;AAM1D,qFAAqF;AACrF,MAAM,UAAU,YAAY,CAAC,OAAmB;IAC/C,OAAQ,OAAgC,CAAC,aAAa,CAAC,CAAC;AACzD,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,cAAc,CAAC,OAAmB,EAAE,OAAmB;IACtE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,aAAa,CAAC,EAAE,OAAO,EAA0B,CAAC;AACzE,CAAC","sourcesContent":["import type { RunOptions } from './agent.ts';\n\nexport type RunEnd = 'completed' | 'failed' | 'cancelled';\n\n/**\n * What one top-level run shares with every agent and tool inside it, across all its roles,\n * rounds and retries, such as the record of what it wrote. Whatever a tool starts for the run\n * registers how to end it, and the agent that created the context ends it with the run.\n *\n * Internal: it travels in `RunOptions` under a symbol, so decorators, which pass the options on\n * whole, carry it without knowing it exists, and no consumer can see or set it.\n */\nexport class RunContext {\n\tprivate readonly finishers: ((end: RunEnd) => Promise<void>)[] = [];\n\n\t/**\n\t * The run the workspace's history recorded for this run, once it first wrote. The object is\n\t * shared, not copied: a turn recorded before the first write still learns its run.\n\t */\n\treadonly historyRun: { runId?: string } = {};\n\n\t/** Ends something the run started, when the run ends. */\n\tonFinish(finisher: (end: RunEnd) => Promise<void>): void {\n\t\tthis.finishers.push(finisher);\n\t}\n\n\t/**\n\t * Ends everything registered, all of it even when one fails, and then throws the first\n\t * failure.\n\t */\n\tasync finish(end: RunEnd): Promise<void> {\n\t\tconst failures: unknown[] = [];\n\t\tfor (const finisher of this.finishers.splice(0)) {\n\t\t\ttry {\n\t\t\t\tawait finisher(end);\n\t\t\t} catch (error) {\n\t\t\t\tfailures.push(error);\n\t\t\t}\n\t\t}\n\t\tif (failures.length > 0) throw failures[0];\n\t}\n}\n\nconst runContextKey = Symbol('mikode-harness.runContext');\n\ninterface ContextualRunOptions extends RunOptions {\n\t[runContextKey]?: RunContext;\n}\n\n/** The context of the run these options belong to, if an outer agent created one. */\nexport function runContextOf(options: RunOptions): RunContext | undefined {\n\treturn (options as ContextualRunOptions)[runContextKey];\n}\n\n/** The same options, carrying `context` for every agent the run reaches. */\nexport function withRunContext(options: RunOptions, context: RunContext): RunOptions {\n\treturn { ...options, [runContextKey]: context } as ContextualRunOptions;\n}\n"]}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Line diffs in the unified format, with Myers' algorithm. This module imports nothing, so it can
3
+ * move to a package of its own unchanged.
4
+ */
5
+ export interface LineDiffOptions {
6
+ /** Unchanged lines shown around each change. Defaults to 3, as in git. */
7
+ context?: number;
8
+ /**
9
+ * The most lines two texts may differ by before the diff stops looking for the smallest
10
+ * change and shows the differing middle as removed, then added. Memory grows with its square.
11
+ */
12
+ maxDistance?: number;
13
+ }
14
+ /**
15
+ * The hunks that turn `before` into `after`, each with its `@@` header, or `''` when the texts
16
+ * are equal. Lines are compared whole, a `\r` included, and a last line without a newline
17
+ * differs from the same line with one, as git shows it.
18
+ */
19
+ export declare function unifiedDiff(before: string, after: string, options?: LineDiffOptions): string;