@itaila/archetype 0.3.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +475 -0
- package/dist/audit/audit-persona.d.ts +163 -0
- package/dist/audit/audit-persona.d.ts.map +1 -0
- package/dist/audit/audit-persona.js +415 -0
- package/dist/audit/audit-persona.js.map +1 -0
- package/dist/audit/brain-reflection.d.ts +33 -0
- package/dist/audit/brain-reflection.d.ts.map +1 -0
- package/dist/audit/brain-reflection.js +148 -0
- package/dist/audit/brain-reflection.js.map +1 -0
- package/dist/audit/conversation-audit.d.ts +12 -0
- package/dist/audit/conversation-audit.d.ts.map +1 -0
- package/dist/audit/conversation-audit.js +76 -0
- package/dist/audit/conversation-audit.js.map +1 -0
- package/dist/audit/prompt-audit.d.ts +10 -0
- package/dist/audit/prompt-audit.d.ts.map +1 -0
- package/dist/audit/prompt-audit.js +153 -0
- package/dist/audit/prompt-audit.js.map +1 -0
- package/dist/audit/prompt-dump.d.ts +137 -0
- package/dist/audit/prompt-dump.d.ts.map +1 -0
- package/dist/audit/prompt-dump.js +269 -0
- package/dist/audit/prompt-dump.js.map +1 -0
- package/dist/audit/trace-integrity.d.ts +33 -0
- package/dist/audit/trace-integrity.d.ts.map +1 -0
- package/dist/audit/trace-integrity.js +109 -0
- package/dist/audit/trace-integrity.js.map +1 -0
- package/dist/audit/types.d.ts +92 -0
- package/dist/audit/types.d.ts.map +1 -0
- package/dist/audit/types.js +2 -0
- package/dist/audit/types.js.map +1 -0
- package/dist/audit/version.d.ts +14 -0
- package/dist/audit/version.d.ts.map +1 -0
- package/dist/audit/version.js +65 -0
- package/dist/audit/version.js.map +1 -0
- package/dist/brain.d.ts +7 -0
- package/dist/brain.d.ts.map +1 -0
- package/dist/brain.js +83 -0
- package/dist/brain.js.map +1 -0
- package/dist/builder/actions.d.ts +60 -0
- package/dist/builder/actions.d.ts.map +1 -0
- package/dist/builder/actions.js +257 -0
- package/dist/builder/actions.js.map +1 -0
- package/dist/builder/browser.d.ts +140 -0
- package/dist/builder/browser.d.ts.map +1 -0
- package/dist/builder/browser.js +232 -0
- package/dist/builder/browser.js.map +1 -0
- package/dist/builder/executor.d.ts +228 -0
- package/dist/builder/executor.d.ts.map +1 -0
- package/dist/builder/executor.js +1548 -0
- package/dist/builder/executor.js.map +1 -0
- package/dist/builder/index.d.ts +24 -0
- package/dist/builder/index.d.ts.map +1 -0
- package/dist/builder/index.js +24 -0
- package/dist/builder/index.js.map +1 -0
- package/dist/builder/node-test-discovery.d.ts +13 -0
- package/dist/builder/node-test-discovery.d.ts.map +1 -0
- package/dist/builder/node-test-discovery.js +45 -0
- package/dist/builder/node-test-discovery.js.map +1 -0
- package/dist/builder/sandbox.d.ts +172 -0
- package/dist/builder/sandbox.d.ts.map +1 -0
- package/dist/builder/sandbox.js +294 -0
- package/dist/builder/sandbox.js.map +1 -0
- package/dist/builder/workspace-files.d.ts +63 -0
- package/dist/builder/workspace-files.d.ts.map +1 -0
- package/dist/builder/workspace-files.js +190 -0
- package/dist/builder/workspace-files.js.map +1 -0
- package/dist/core/actions.d.ts +55 -0
- package/dist/core/actions.d.ts.map +1 -0
- package/dist/core/actions.js +311 -0
- package/dist/core/actions.js.map +1 -0
- package/dist/core/attachment-notes.d.ts +7 -0
- package/dist/core/attachment-notes.d.ts.map +1 -0
- package/dist/core/attachment-notes.js +38 -0
- package/dist/core/attachment-notes.js.map +1 -0
- package/dist/core/context.d.ts +10 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +108 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/crud-prompt.d.ts +16 -0
- package/dist/core/crud-prompt.d.ts.map +1 -0
- package/dist/core/crud-prompt.js +268 -0
- package/dist/core/crud-prompt.js.map +1 -0
- package/dist/core/crud-schema.d.ts +12 -0
- package/dist/core/crud-schema.d.ts.map +1 -0
- package/dist/core/crud-schema.js +42 -0
- package/dist/core/crud-schema.js.map +1 -0
- package/dist/core/effective-config.d.ts +13 -0
- package/dist/core/effective-config.d.ts.map +1 -0
- package/dist/core/effective-config.js +33 -0
- package/dist/core/effective-config.js.map +1 -0
- package/dist/core/entities.d.ts +82 -0
- package/dist/core/entities.d.ts.map +1 -0
- package/dist/core/entities.js +116 -0
- package/dist/core/entities.js.map +1 -0
- package/dist/core/entity-helpers.d.ts +47 -0
- package/dist/core/entity-helpers.d.ts.map +1 -0
- package/dist/core/entity-helpers.js +122 -0
- package/dist/core/entity-helpers.js.map +1 -0
- package/dist/core/entity-registry.d.ts +47 -0
- package/dist/core/entity-registry.d.ts.map +1 -0
- package/dist/core/entity-registry.js +54 -0
- package/dist/core/entity-registry.js.map +1 -0
- package/dist/core/eq.d.ts +13 -0
- package/dist/core/eq.d.ts.map +1 -0
- package/dist/core/eq.js +41 -0
- package/dist/core/eq.js.map +1 -0
- package/dist/core/focus-context.d.ts +19 -0
- package/dist/core/focus-context.d.ts.map +1 -0
- package/dist/core/focus-context.js +46 -0
- package/dist/core/focus-context.js.map +1 -0
- package/dist/core/focus-mode-actions.d.ts +23 -0
- package/dist/core/focus-mode-actions.d.ts.map +1 -0
- package/dist/core/focus-mode-actions.js +74 -0
- package/dist/core/focus-mode-actions.js.map +1 -0
- package/dist/core/greeting.d.ts +10 -0
- package/dist/core/greeting.d.ts.map +1 -0
- package/dist/core/greeting.js +41 -0
- package/dist/core/greeting.js.map +1 -0
- package/dist/core/identity.d.ts +13 -0
- package/dist/core/identity.d.ts.map +1 -0
- package/dist/core/identity.js +54 -0
- package/dist/core/identity.js.map +1 -0
- package/dist/core/knowledge.d.ts +10 -0
- package/dist/core/knowledge.d.ts.map +1 -0
- package/dist/core/knowledge.js +40 -0
- package/dist/core/knowledge.js.map +1 -0
- package/dist/core/memory-actions.d.ts +38 -0
- package/dist/core/memory-actions.d.ts.map +1 -0
- package/dist/core/memory-actions.js +181 -0
- package/dist/core/memory-actions.js.map +1 -0
- package/dist/core/memory.d.ts +35 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +168 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/peer-actions.d.ts +15 -0
- package/dist/core/peer-actions.d.ts.map +1 -0
- package/dist/core/peer-actions.js +33 -0
- package/dist/core/peer-actions.js.map +1 -0
- package/dist/core/prompt-builder.d.ts +46 -0
- package/dist/core/prompt-builder.d.ts.map +1 -0
- package/dist/core/prompt-builder.js +543 -0
- package/dist/core/prompt-builder.js.map +1 -0
- package/dist/core/prompt-mode.d.ts +3 -0
- package/dist/core/prompt-mode.d.ts.map +1 -0
- package/dist/core/prompt-mode.js +6 -0
- package/dist/core/prompt-mode.js.map +1 -0
- package/dist/core/prompted-turn.d.ts +6 -0
- package/dist/core/prompted-turn.d.ts.map +1 -0
- package/dist/core/prompted-turn.js +48 -0
- package/dist/core/prompted-turn.js.map +1 -0
- package/dist/core/request-builder.d.ts +14 -0
- package/dist/core/request-builder.d.ts.map +1 -0
- package/dist/core/request-builder.js +64 -0
- package/dist/core/request-builder.js.map +1 -0
- package/dist/core/session-routing.d.ts +23 -0
- package/dist/core/session-routing.d.ts.map +1 -0
- package/dist/core/session-routing.js +59 -0
- package/dist/core/session-routing.js.map +1 -0
- package/dist/core/voice.d.ts +6 -0
- package/dist/core/voice.d.ts.map +1 -0
- package/dist/core/voice.js +30 -0
- package/dist/core/voice.js.map +1 -0
- package/dist/engine/chat.d.ts +45 -0
- package/dist/engine/chat.d.ts.map +1 -0
- package/dist/engine/chat.js +308 -0
- package/dist/engine/chat.js.map +1 -0
- package/dist/engine/continuity.d.ts +107 -0
- package/dist/engine/continuity.d.ts.map +1 -0
- package/dist/engine/continuity.js +320 -0
- package/dist/engine/continuity.js.map +1 -0
- package/dist/engine/crud.d.ts +62 -0
- package/dist/engine/crud.d.ts.map +1 -0
- package/dist/engine/crud.js +260 -0
- package/dist/engine/crud.js.map +1 -0
- package/dist/engine/side-effects.d.ts +93 -0
- package/dist/engine/side-effects.d.ts.map +1 -0
- package/dist/engine/side-effects.js +271 -0
- package/dist/engine/side-effects.js.map +1 -0
- package/dist/engine/staging.d.ts +29 -0
- package/dist/engine/staging.d.ts.map +1 -0
- package/dist/engine/staging.js +159 -0
- package/dist/engine/staging.js.map +1 -0
- package/dist/engine/working-set.d.ts +18 -0
- package/dist/engine/working-set.d.ts.map +1 -0
- package/dist/engine/working-set.js +246 -0
- package/dist/engine/working-set.js.map +1 -0
- package/dist/evals/action-contracts.d.ts +40 -0
- package/dist/evals/action-contracts.d.ts.map +1 -0
- package/dist/evals/action-contracts.js +208 -0
- package/dist/evals/action-contracts.js.map +1 -0
- package/dist/evals/brain-bloat.d.ts +39 -0
- package/dist/evals/brain-bloat.d.ts.map +1 -0
- package/dist/evals/brain-bloat.js +167 -0
- package/dist/evals/brain-bloat.js.map +1 -0
- package/dist/evals/brain-prescriptions.d.ts +30 -0
- package/dist/evals/brain-prescriptions.d.ts.map +1 -0
- package/dist/evals/brain-prescriptions.js +148 -0
- package/dist/evals/brain-prescriptions.js.map +1 -0
- package/dist/evals/cross-layer-duplicates.d.ts +49 -0
- package/dist/evals/cross-layer-duplicates.d.ts.map +1 -0
- package/dist/evals/cross-layer-duplicates.js +289 -0
- package/dist/evals/cross-layer-duplicates.js.map +1 -0
- package/dist/evals/entity-visibility.d.ts +28 -0
- package/dist/evals/entity-visibility.d.ts.map +1 -0
- package/dist/evals/entity-visibility.js +216 -0
- package/dist/evals/entity-visibility.js.map +1 -0
- package/dist/evals/index.d.ts +19 -0
- package/dist/evals/index.d.ts.map +1 -0
- package/dist/evals/index.js +11 -0
- package/dist/evals/index.js.map +1 -0
- package/dist/evals/judge.d.ts +22 -0
- package/dist/evals/judge.d.ts.map +1 -0
- package/dist/evals/judge.js +337 -0
- package/dist/evals/judge.js.map +1 -0
- package/dist/evals/operational-contract.d.ts +40 -0
- package/dist/evals/operational-contract.d.ts.map +1 -0
- package/dist/evals/operational-contract.js +115 -0
- package/dist/evals/operational-contract.js.map +1 -0
- package/dist/evals/prompt-content.d.ts +14 -0
- package/dist/evals/prompt-content.d.ts.map +1 -0
- package/dist/evals/prompt-content.js +104 -0
- package/dist/evals/prompt-content.js.map +1 -0
- package/dist/evals/runtime.d.ts +4 -0
- package/dist/evals/runtime.d.ts.map +1 -0
- package/dist/evals/runtime.js +197 -0
- package/dist/evals/runtime.js.map +1 -0
- package/dist/evals/sample-projects.d.ts +143 -0
- package/dist/evals/sample-projects.d.ts.map +1 -0
- package/dist/evals/sample-projects.js +644 -0
- package/dist/evals/sample-projects.js.map +1 -0
- package/dist/evals/types.d.ts +88 -0
- package/dist/evals/types.d.ts.map +1 -0
- package/dist/evals/types.js +2 -0
- package/dist/evals/types.js.map +1 -0
- package/dist/foundation/index.d.ts +158 -0
- package/dist/foundation/index.d.ts.map +1 -0
- package/dist/foundation/index.js +256 -0
- package/dist/foundation/index.js.map +1 -0
- package/dist/index.d.ts +223 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +998 -0
- package/dist/index.js.map +1 -0
- package/dist/managed/autonomous-loop.d.ts +199 -0
- package/dist/managed/autonomous-loop.d.ts.map +1 -0
- package/dist/managed/autonomous-loop.js +451 -0
- package/dist/managed/autonomous-loop.js.map +1 -0
- package/dist/managed/conversation.d.ts +20 -0
- package/dist/managed/conversation.d.ts.map +1 -0
- package/dist/managed/conversation.js +40 -0
- package/dist/managed/conversation.js.map +1 -0
- package/dist/managed/knowledge.d.ts +7 -0
- package/dist/managed/knowledge.d.ts.map +1 -0
- package/dist/managed/knowledge.js +174 -0
- package/dist/managed/knowledge.js.map +1 -0
- package/dist/managed/memory-manager.d.ts +7 -0
- package/dist/managed/memory-manager.d.ts.map +1 -0
- package/dist/managed/memory-manager.js +18 -0
- package/dist/managed/memory-manager.js.map +1 -0
- package/dist/managed/memory-review.d.ts +45 -0
- package/dist/managed/memory-review.d.ts.map +1 -0
- package/dist/managed/memory-review.js +130 -0
- package/dist/managed/memory-review.js.map +1 -0
- package/dist/managed/storage.d.ts +2 -0
- package/dist/managed/storage.d.ts.map +1 -0
- package/dist/managed/storage.js +2 -0
- package/dist/managed/storage.js.map +1 -0
- package/dist/managed/work-history.d.ts +23 -0
- package/dist/managed/work-history.d.ts.map +1 -0
- package/dist/managed/work-history.js +31 -0
- package/dist/managed/work-history.js.map +1 -0
- package/dist/observability/index.d.ts +15 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +15 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/render-run-markdown.d.ts +90 -0
- package/dist/observability/render-run-markdown.d.ts.map +1 -0
- package/dist/observability/render-run-markdown.js +231 -0
- package/dist/observability/render-run-markdown.js.map +1 -0
- package/dist/observability/turn-reporter.d.ts +20 -0
- package/dist/observability/turn-reporter.d.ts.map +1 -0
- package/dist/observability/turn-reporter.js +106 -0
- package/dist/observability/turn-reporter.js.map +1 -0
- package/dist/persona.d.ts +49 -0
- package/dist/persona.d.ts.map +1 -0
- package/dist/persona.js +287 -0
- package/dist/persona.js.map +1 -0
- package/dist/playbook/defaults.d.ts +25 -0
- package/dist/playbook/defaults.d.ts.map +1 -0
- package/dist/playbook/defaults.js +108 -0
- package/dist/playbook/defaults.js.map +1 -0
- package/dist/playbook/invariants.d.ts +244 -0
- package/dist/playbook/invariants.d.ts.map +1 -0
- package/dist/playbook/invariants.js +259 -0
- package/dist/playbook/invariants.js.map +1 -0
- package/dist/playbook/templates.d.ts +7 -0
- package/dist/playbook/templates.d.ts.map +1 -0
- package/dist/playbook/templates.js +437 -0
- package/dist/playbook/templates.js.map +1 -0
- package/dist/providers/gemini.d.ts +73 -0
- package/dist/providers/gemini.d.ts.map +1 -0
- package/dist/providers/gemini.js +536 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/types.d.ts +2 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/providers/zod-to-gemini.d.ts +8 -0
- package/dist/providers/zod-to-gemini.d.ts.map +1 -0
- package/dist/providers/zod-to-gemini.js +148 -0
- package/dist/providers/zod-to-gemini.js.map +1 -0
- package/dist/samples/pm-spec-agent.d.ts +22 -0
- package/dist/samples/pm-spec-agent.d.ts.map +1 -0
- package/dist/samples/pm-spec-agent.js +53 -0
- package/dist/samples/pm-spec-agent.js.map +1 -0
- package/dist/types.d.ts +920 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coder-action executor — the dispatch layer that turns a parsed
|
|
3
|
+
* `{ name, params }` action (from an archetype persona) into real
|
|
4
|
+
* side-effects against a workspace, sandbox, and browser harness.
|
|
5
|
+
*
|
|
6
|
+
* The persona's LLM sees the contracts from `./actions.ts`; this module
|
|
7
|
+
* is what happens when it fires one. Architecture:
|
|
8
|
+
*
|
|
9
|
+
* ┌──────────────────────────────┐
|
|
10
|
+
* │ persona.chat → actions │
|
|
11
|
+
* └──────────────┬───────────────┘
|
|
12
|
+
* │
|
|
13
|
+
* ▼
|
|
14
|
+
* ┌──────────────────────────────┐
|
|
15
|
+
* │ executeCoderAction(action) │ ← this module
|
|
16
|
+
* │ ─────────────────────────── │
|
|
17
|
+
* │ fs ops → workspaceRoot │
|
|
18
|
+
* │ runTool → CoderSandbox │
|
|
19
|
+
* │ browser* → BrowserHarness │
|
|
20
|
+
* └──────────────┬───────────────┘
|
|
21
|
+
* │
|
|
22
|
+
* ▼
|
|
23
|
+
* ┌──────────────────────────────┐
|
|
24
|
+
* │ CoderActionResult │
|
|
25
|
+
* │ historyNote + log + hints │
|
|
26
|
+
* └──────────────────────────────┘
|
|
27
|
+
*
|
|
28
|
+
* Contract:
|
|
29
|
+
* • Return `null` when the action name is not a coder primitive — the
|
|
30
|
+
* host dispatches it on its own (benchmark-specific actions like
|
|
31
|
+
* markMilestone, createRole, sendInternalMemo, etc).
|
|
32
|
+
* • Return a `CoderActionResult` otherwise. The structured hints
|
|
33
|
+
* (`mutatedArtifact`, `capturedScreenshot`, `liveOrigin`,
|
|
34
|
+
* `sandboxToolCall`) let the host evolve its own book-keeping
|
|
35
|
+
* (metrics, evidence state, browser lifecycle) without this module
|
|
36
|
+
* knowing anything about benchmark internals.
|
|
37
|
+
*
|
|
38
|
+
* What belongs here:
|
|
39
|
+
* • File I/O (read/applyPatch/list/search) against workspaceRoot
|
|
40
|
+
* • Sandbox dispatch (runCommand + runTool)
|
|
41
|
+
* • Browser dispatch (open / click / type / key / screenshot / console)
|
|
42
|
+
* • Legacy write/edit/delete replay support for older traces.
|
|
43
|
+
*
|
|
44
|
+
* What does NOT belong here:
|
|
45
|
+
* • Benchmark metrics (toolCallsUsed, etc.)
|
|
46
|
+
* • Evidence revision tracking
|
|
47
|
+
* • Browser lifecycle (constructing BrowserHarness from a new origin
|
|
48
|
+
* — exposed via `liveOrigin` for the host to act on)
|
|
49
|
+
* • executionLog accumulation (the host builds trace output from
|
|
50
|
+
* `log` strings however it wants)
|
|
51
|
+
*
|
|
52
|
+
* Node-only: uses `node:fs` for file operations.
|
|
53
|
+
*/
|
|
54
|
+
import fs from 'node:fs';
|
|
55
|
+
import path from 'node:path';
|
|
56
|
+
import { execFileSync } from 'node:child_process';
|
|
57
|
+
import { listWorkspaceMountFileEntries, resolveWorkspaceMountPath, } from './workspace-files.js';
|
|
58
|
+
const SMALL_WORKSET_RESULT_TURNS = 4;
|
|
59
|
+
/**
|
|
60
|
+
* Return the text a host should carry into the immediately following
|
|
61
|
+
* continuity surface for a coder action.
|
|
62
|
+
*
|
|
63
|
+
* Use this instead of reading `continuity.staleText` directly. `staleText`
|
|
64
|
+
* is intentionally a later-turn tombstone; showing it immediately makes a
|
|
65
|
+
* successful read/search look as if the result was already unavailable.
|
|
66
|
+
*/
|
|
67
|
+
export function immediateCoderActionOutcome(result) {
|
|
68
|
+
return result.continuity?.resultText ?? result.historyNote;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Return the compact factual note safe to share beyond the actor's private
|
|
72
|
+
* workset. This preserves the important truth ("file X was written",
|
|
73
|
+
* "edit failed", "console was read") without carrying large read/search
|
|
74
|
+
* payloads into another participant's history.
|
|
75
|
+
*/
|
|
76
|
+
export function compactCoderActionOutcome(result) {
|
|
77
|
+
if (typeof result.toolExitCode === 'number' && result.toolExitCode !== 0) {
|
|
78
|
+
return historyCoderActionOutcome(result, { maxBytes: 2400 });
|
|
79
|
+
}
|
|
80
|
+
return result.continuity?.staleText ?? immediateCoderActionOutcome(result);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Return an outcome note suitable for stored chat history. Small factual
|
|
84
|
+
* results stay attached to the narrative that caused them ("I will list" is
|
|
85
|
+
* followed by the actual list result); large results decay to their compact
|
|
86
|
+
* recovery note so history does not become a file-content cache.
|
|
87
|
+
*/
|
|
88
|
+
export function historyCoderActionOutcome(result, options = {}) {
|
|
89
|
+
const maxBytes = options.maxBytes ?? 1600;
|
|
90
|
+
const immediate = immediateCoderActionOutcome(result).trim();
|
|
91
|
+
if (Buffer.byteLength(immediate, 'utf8') <= maxBytes)
|
|
92
|
+
return immediate;
|
|
93
|
+
return result.continuity?.staleText ?? `${immediate.slice(0, maxBytes)}\n<truncated; run the action again if exact output is needed>`;
|
|
94
|
+
}
|
|
95
|
+
export function coderActionOutcomeForLedger(action, result, options = {}) {
|
|
96
|
+
return {
|
|
97
|
+
action,
|
|
98
|
+
outcomeNote: historyCoderActionOutcome(result, options),
|
|
99
|
+
resultText: result.continuity?.resultText,
|
|
100
|
+
resultTurns: result.continuity?.resultTurns,
|
|
101
|
+
staleText: result.continuity?.staleText,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// ─── Dispatcher ──────────────────────────────────────────────────────
|
|
105
|
+
const SANDBOX_TOOL_NAMES = new Set([
|
|
106
|
+
'runInstall',
|
|
107
|
+
'runBuild',
|
|
108
|
+
'runTests',
|
|
109
|
+
'runLint',
|
|
110
|
+
'runStart',
|
|
111
|
+
]);
|
|
112
|
+
/**
|
|
113
|
+
* Execute one parsed coder action. Returns the structured result if the
|
|
114
|
+
* name matches a coder primitive, or `null` if the host should dispatch
|
|
115
|
+
* it (benchmark-specific actions like markMilestone).
|
|
116
|
+
*
|
|
117
|
+
* Never throws on user input — every failure is surfaced through the
|
|
118
|
+
* `historyNote` + `log` strings so the model sees what went wrong.
|
|
119
|
+
*/
|
|
120
|
+
export async function executeCoderAction(input) {
|
|
121
|
+
try {
|
|
122
|
+
const result = await executeCoderActionUnchecked(input);
|
|
123
|
+
return result ? normalizeCoderActionResult(input.action.name, result) : null;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
return failedCoderActionResult(input.action.name, error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Execute a same-turn batch of coder actions with truthful continuity.
|
|
131
|
+
*
|
|
132
|
+
* The model chose every action in the batch before seeing any tool result
|
|
133
|
+
* from that same batch. Successful actions remain durable facts; the model
|
|
134
|
+
* chose a sequence, not one hidden turn-wide transaction. After any failure,
|
|
135
|
+
* runTests and finishAttempt are skipped because their result would claim
|
|
136
|
+
* verification/completion for a turn where some intended tool work failed.
|
|
137
|
+
*
|
|
138
|
+
* Unknown actions return `null` so hosts can dispatch them normally.
|
|
139
|
+
*/
|
|
140
|
+
export async function executeCoderActions(input) {
|
|
141
|
+
const executions = [];
|
|
142
|
+
let failedThisTurn = false;
|
|
143
|
+
for (const action of input.actions) {
|
|
144
|
+
if (failedThisTurn && shouldSkipAfterSameTurnFailure(action.name)) {
|
|
145
|
+
executions.push({
|
|
146
|
+
action,
|
|
147
|
+
result: skippedAfterSameTurnFailureResult(action.name),
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const result = await executeCoderAction({ action, context: input.context });
|
|
152
|
+
const execution = { action, result };
|
|
153
|
+
executions.push(execution);
|
|
154
|
+
await input.onActionResult?.(execution, input.context);
|
|
155
|
+
if (!result)
|
|
156
|
+
continue;
|
|
157
|
+
const failed = !coderActionSucceeded(action.name, result);
|
|
158
|
+
if (failed)
|
|
159
|
+
failedThisTurn = true;
|
|
160
|
+
}
|
|
161
|
+
return executions;
|
|
162
|
+
}
|
|
163
|
+
function normalizeCoderActionResult(actionName, result) {
|
|
164
|
+
const kind = result.kind ?? inferCoderActionKind(actionName);
|
|
165
|
+
return {
|
|
166
|
+
...result,
|
|
167
|
+
kind,
|
|
168
|
+
ok: result.ok ?? inferCoderActionSuccess(actionName, result, kind),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function inferCoderActionKind(actionName) {
|
|
172
|
+
if (isFileMutationAction(actionName))
|
|
173
|
+
return 'fileMutation';
|
|
174
|
+
if (actionName === 'readFile' || actionName === 'listFiles' || actionName === 'searchInFiles')
|
|
175
|
+
return 'read';
|
|
176
|
+
if (actionName === 'runCommand' || SANDBOX_TOOL_NAMES.has(actionName))
|
|
177
|
+
return 'sandbox';
|
|
178
|
+
if (actionName.startsWith('browser'))
|
|
179
|
+
return 'browser';
|
|
180
|
+
return 'unhandled';
|
|
181
|
+
}
|
|
182
|
+
function inferCoderActionSuccess(actionName, result, kind) {
|
|
183
|
+
if (typeof result.toolExitCode === 'number')
|
|
184
|
+
return result.toolExitCode === 0;
|
|
185
|
+
if (kind === 'fileMutation')
|
|
186
|
+
return result.mutatedArtifact === true;
|
|
187
|
+
if (actionName === 'browserScreenshot' && result.capturedScreenshot !== true)
|
|
188
|
+
return false;
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
function coderActionSucceeded(actionName, result) {
|
|
192
|
+
return normalizeCoderActionResult(actionName, result).ok === true;
|
|
193
|
+
}
|
|
194
|
+
function isFileMutationAction(actionName) {
|
|
195
|
+
return actionName === 'applyPatch'
|
|
196
|
+
|| actionName === 'writeFile'
|
|
197
|
+
|| actionName === 'editFile'
|
|
198
|
+
|| actionName === 'deleteFile';
|
|
199
|
+
}
|
|
200
|
+
function shouldSkipAfterSameTurnFailure(actionName) {
|
|
201
|
+
return actionName === 'runTests' || actionName === 'finishAttempt';
|
|
202
|
+
}
|
|
203
|
+
function skippedAfterSameTurnFailureResult(actionName) {
|
|
204
|
+
const resultText = `${actionName} skipped — Error: didn't run because tools/actions failed this turn.`;
|
|
205
|
+
return {
|
|
206
|
+
log: `- ${actionName}\n skipped: tools/actions failed this turn`,
|
|
207
|
+
historyNote: resultText,
|
|
208
|
+
ok: false,
|
|
209
|
+
kind: inferCoderActionKind(actionName),
|
|
210
|
+
skipped: true,
|
|
211
|
+
continuity: {
|
|
212
|
+
resultText,
|
|
213
|
+
auditAnchors: [actionName, 'skipped', 'tools/actions failed this turn'],
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async function executeCoderActionUnchecked(input) {
|
|
218
|
+
const { action } = input;
|
|
219
|
+
const { workspaceRoot, browser } = input.context;
|
|
220
|
+
const sandbox = input.context.sandbox;
|
|
221
|
+
if (action.name === 'readFile') {
|
|
222
|
+
const targetPath = resolveCoderFilePath(input.context, String(action.params.path), { write: false });
|
|
223
|
+
const relativePath = targetPath.visiblePath;
|
|
224
|
+
const target = targetPath.absolutePath;
|
|
225
|
+
const content = safeReadWorkspaceText(target);
|
|
226
|
+
const rendered = renderReadFileContent(content);
|
|
227
|
+
return {
|
|
228
|
+
log: `- readFile: ${relativePath}\n ${indent(rendered)}`,
|
|
229
|
+
historyNote: `Tool result: readFile ${relativePath}\n${rendered}`,
|
|
230
|
+
continuity: {
|
|
231
|
+
resultText: `readFile ${relativePath}\n${rendered}`,
|
|
232
|
+
resultTurns: SMALL_WORKSET_RESULT_TURNS,
|
|
233
|
+
staleText: `<readFile result for ${relativePath} no longer carried in WORK HISTORY; read the file again only if exact contents are needed>`,
|
|
234
|
+
auditAnchors: buildAuditAnchors(relativePath, rendered),
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (action.name === 'writeFile') {
|
|
239
|
+
const targetPath = resolveCoderFilePath(input.context, String(action.params.path), { write: true });
|
|
240
|
+
if (!targetPath.writable)
|
|
241
|
+
return readonlyMountResult('writeFile', targetPath.visiblePath);
|
|
242
|
+
const relativePath = targetPath.visiblePath;
|
|
243
|
+
const target = targetPath.absolutePath;
|
|
244
|
+
const contentString = String(action.params.content ?? '');
|
|
245
|
+
const bytesWritten = Buffer.byteLength(contentString, 'utf8');
|
|
246
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
247
|
+
fs.writeFileSync(target, contentString, 'utf8');
|
|
248
|
+
const resultText = [
|
|
249
|
+
`writeFile ${relativePath}`,
|
|
250
|
+
`Successfully wrote ${renderTextSize(contentString)}.`,
|
|
251
|
+
'Exact file content is not carried in WORK HISTORY; use readFile if exact contents are needed.',
|
|
252
|
+
].join('\n');
|
|
253
|
+
return {
|
|
254
|
+
log: `- writeFile: ${relativePath} (${bytesWritten} bytes)`,
|
|
255
|
+
historyNote: `Successfully wrote ${renderTextSize(contentString)} to ${relativePath}.`,
|
|
256
|
+
mutatedArtifact: true,
|
|
257
|
+
continuity: {
|
|
258
|
+
resultText,
|
|
259
|
+
resultTurns: SMALL_WORKSET_RESULT_TURNS,
|
|
260
|
+
staleText: `writeFile ${relativePath}\nSuccessfully wrote ${renderTextSize(contentString)}. Exact file content is not carried in WORK HISTORY; read the file again only if exact contents are needed.`,
|
|
261
|
+
auditAnchors: [relativePath],
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
if (action.name === 'applyPatch') {
|
|
266
|
+
return executeApplyPatch({ action, context: input.context });
|
|
267
|
+
}
|
|
268
|
+
if (action.name === 'editFile') {
|
|
269
|
+
const targetPath = resolveCoderFilePath(input.context, String(action.params.path), { write: true });
|
|
270
|
+
if (!targetPath.writable)
|
|
271
|
+
return readonlyMountResult('editFile', targetPath.visiblePath);
|
|
272
|
+
return executeEditFile({ action, workspaceRoot: targetPath.root, visiblePath: targetPath.visiblePath, relativePath: targetPath.relativePath });
|
|
273
|
+
}
|
|
274
|
+
if (action.name === 'deleteFile') {
|
|
275
|
+
const targetPath = resolveCoderFilePath(input.context, String(action.params.path), { write: true });
|
|
276
|
+
if (!targetPath.writable)
|
|
277
|
+
return readonlyMountResult('deleteFile', targetPath.visiblePath);
|
|
278
|
+
const relativePath = targetPath.visiblePath;
|
|
279
|
+
const target = targetPath.absolutePath;
|
|
280
|
+
fs.rmSync(target, { force: true, recursive: true });
|
|
281
|
+
return {
|
|
282
|
+
log: `- deleteFile: ${relativePath}`,
|
|
283
|
+
historyNote: `Successfully deleted ${relativePath}.`,
|
|
284
|
+
mutatedArtifact: true,
|
|
285
|
+
continuity: {
|
|
286
|
+
resultText: `deleteFile ${relativePath}\nSuccessfully deleted the file.`,
|
|
287
|
+
auditAnchors: [relativePath],
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (action.name === 'listFiles') {
|
|
292
|
+
if (input.context.workspaceMounts?.length && (action.params.path == null || action.params.path === '.' || action.params.path === '')) {
|
|
293
|
+
const mountedEntries = listWorkspaceMountFileEntries(input.context.workspaceMounts);
|
|
294
|
+
const renderedMounted = mountedEntries.length > 0
|
|
295
|
+
? mountedEntries.map(entry => {
|
|
296
|
+
const linePart = entry.lines == null ? 'binary/unknown lines' : `${entry.lines} lines`;
|
|
297
|
+
return `${entry.path} (${linePart}, ${entry.bytes} bytes)`;
|
|
298
|
+
}).join('\n')
|
|
299
|
+
: '(empty)';
|
|
300
|
+
return {
|
|
301
|
+
log: `- listFiles: . (${mountedEntries.length} entries)\n ${indent(renderedMounted)}`,
|
|
302
|
+
historyNote: `Tool result: listFiles .\n${renderedMounted}`,
|
|
303
|
+
continuity: {
|
|
304
|
+
resultText: `listFiles .\n${renderedMounted}`,
|
|
305
|
+
staleText: '<listFiles result for . removed from continuity; run listFiles again to inspect the current tree>',
|
|
306
|
+
auditAnchors: buildAuditAnchors('.', renderedMounted),
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const targetPath = resolveCoderFilePath(input.context, String(action.params.path ?? '.'), { write: false });
|
|
311
|
+
const rel = targetPath.visiblePath || '.';
|
|
312
|
+
const base = targetPath.absolutePath;
|
|
313
|
+
const entries = [];
|
|
314
|
+
const walk = (dir, prefix) => {
|
|
315
|
+
let items = [];
|
|
316
|
+
try {
|
|
317
|
+
items = fs.readdirSync(dir, { withFileTypes: true });
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
items.sort((a, b) => a.name.localeCompare(b.name));
|
|
323
|
+
for (const it of items) {
|
|
324
|
+
if (it.name.startsWith('.'))
|
|
325
|
+
continue;
|
|
326
|
+
const full = path.join(dir, it.name);
|
|
327
|
+
const relPath = prefix ? `${prefix}/${it.name}` : it.name;
|
|
328
|
+
if (it.isDirectory()) {
|
|
329
|
+
entries.push(`${relPath}/`);
|
|
330
|
+
walk(full, relPath);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
let size = '';
|
|
334
|
+
try {
|
|
335
|
+
size = ` (${fs.statSync(full).size} bytes)`;
|
|
336
|
+
}
|
|
337
|
+
catch { /* ignore */ }
|
|
338
|
+
entries.push(`${relPath}${size}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
walk(base, '');
|
|
343
|
+
const rendered = entries.length > 0 ? entries.join('\n') : '(empty)';
|
|
344
|
+
return {
|
|
345
|
+
log: `- listFiles: ${rel} (${entries.length} entries)\n ${indent(rendered)}`,
|
|
346
|
+
historyNote: `Tool result: listFiles ${rel}\n${rendered}`,
|
|
347
|
+
continuity: {
|
|
348
|
+
resultText: `listFiles ${rel}\n${rendered}`,
|
|
349
|
+
staleText: `<listFiles result for ${rel} removed from continuity; run listFiles again to inspect the current tree>`,
|
|
350
|
+
auditAnchors: buildAuditAnchors(rel, rendered),
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
if (action.name === 'searchInFiles') {
|
|
355
|
+
return executeSearchInFiles({ action, context: input.context });
|
|
356
|
+
}
|
|
357
|
+
if (action.name === 'runCommand') {
|
|
358
|
+
const argv = Array.isArray(action.params.command)
|
|
359
|
+
? action.params.command.map(String)
|
|
360
|
+
: [];
|
|
361
|
+
const commandWorkspace = prepareRunCommandWorkspace(input.context);
|
|
362
|
+
const beforeFiles = snapshotCoderWorkspaceFiles(input.context);
|
|
363
|
+
const result = await sandbox.runCommand({
|
|
364
|
+
command: argv,
|
|
365
|
+
cwd: commandWorkspace.cwd,
|
|
366
|
+
extraReadPaths: commandWorkspace.extraReadPaths,
|
|
367
|
+
extraWritePaths: commandWorkspace.extraWritePaths,
|
|
368
|
+
});
|
|
369
|
+
const afterFiles = snapshotCoderWorkspaceFiles(input.context);
|
|
370
|
+
const fileChanges = renderWorkspaceFileChanges(beforeFiles, afterFiles);
|
|
371
|
+
const output = truncate([result.stdout, result.stderr].filter(Boolean).join('\n').trim(), 3000);
|
|
372
|
+
const commandSummary = renderCommandSummary(argv);
|
|
373
|
+
const resultText = [
|
|
374
|
+
`runCommand ${commandSummary}`,
|
|
375
|
+
`exit=${result.exitCode}`,
|
|
376
|
+
output || '(no output)',
|
|
377
|
+
fileChanges,
|
|
378
|
+
].filter(Boolean).join('\n');
|
|
379
|
+
const staleText = fileChanges
|
|
380
|
+
? `runCommand ${commandSummary} completed with exit=${result.exitCode}.\n${fileChanges}`
|
|
381
|
+
: `runCommand ${commandSummary} completed with exit=${result.exitCode}. Full output removed from continuity.`;
|
|
382
|
+
return {
|
|
383
|
+
log: `- runCommand: ${JSON.stringify(argv)}\n exit: ${result.exitCode}\n output:\n${indent(output || '(no output)')}`,
|
|
384
|
+
historyNote: `Tool result: ${resultText}`,
|
|
385
|
+
sandboxToolCall: true,
|
|
386
|
+
toolExitCode: result.exitCode,
|
|
387
|
+
continuity: {
|
|
388
|
+
resultText,
|
|
389
|
+
staleText,
|
|
390
|
+
auditAnchors: [commandSummary, `exit=${result.exitCode}`, ...(fileChanges ? ['Changed file state'] : [])],
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (SANDBOX_TOOL_NAMES.has(action.name)) {
|
|
395
|
+
const commandWorkspace = prepareRunCommandWorkspace(input.context);
|
|
396
|
+
const result = await sandbox.runTool(action.name, commandWorkspace);
|
|
397
|
+
const output = truncate([result.stdout, result.stderr].filter(Boolean).join('\n').trim(), 3000);
|
|
398
|
+
const liveOrigin = action.name === 'runStart' && result.ok && 'origin' in result
|
|
399
|
+
? result.origin
|
|
400
|
+
: undefined;
|
|
401
|
+
const commandDocumentation = renderSandboxToolDocumentation(result);
|
|
402
|
+
const staleText = liveOrigin
|
|
403
|
+
? [
|
|
404
|
+
`${action.name} completed with exit=${result.exitCode}; live origin remains ${liveOrigin}. Full output removed from continuity.`,
|
|
405
|
+
commandDocumentation,
|
|
406
|
+
].filter(Boolean).join('\n')
|
|
407
|
+
: [
|
|
408
|
+
`${action.name} completed with exit=${result.exitCode}. Full output removed from continuity.`,
|
|
409
|
+
commandDocumentation,
|
|
410
|
+
].filter(Boolean).join('\n');
|
|
411
|
+
const resultText = [
|
|
412
|
+
`${action.name}`,
|
|
413
|
+
`exit=${result.exitCode}`,
|
|
414
|
+
commandDocumentation,
|
|
415
|
+
output || '(no output)',
|
|
416
|
+
].filter(Boolean).join('\n');
|
|
417
|
+
return {
|
|
418
|
+
log: `- ${action.name}\n exit: ${result.exitCode}\n output:\n${indent(output || '(no output)')}`,
|
|
419
|
+
historyNote: `Tool result: ${resultText}`,
|
|
420
|
+
sandboxToolCall: true,
|
|
421
|
+
toolExitCode: result.exitCode,
|
|
422
|
+
liveOrigin,
|
|
423
|
+
continuity: {
|
|
424
|
+
resultText,
|
|
425
|
+
staleText,
|
|
426
|
+
auditAnchors: [
|
|
427
|
+
action.name,
|
|
428
|
+
`exit=${result.exitCode}`,
|
|
429
|
+
...(result.userFacingCommand ? [result.userFacingCommand] : []),
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (action.name === 'browserOpen') {
|
|
435
|
+
if (!browser) {
|
|
436
|
+
const resultText = 'browserOpen failed: there is no live server yet. runStart boots a local HTTP server at the workspace and returns a URL — the browser binds to that URL when it opens.';
|
|
437
|
+
return {
|
|
438
|
+
log: '- browserOpen\n blocked: local start has not succeeded yet',
|
|
439
|
+
historyNote: resultText,
|
|
440
|
+
ok: false,
|
|
441
|
+
continuity: {
|
|
442
|
+
resultText,
|
|
443
|
+
auditAnchors: ['browserOpen', 'no live server'],
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
let result;
|
|
448
|
+
try {
|
|
449
|
+
result = await browser.open(resolveBrowserOpenPath(action.params.path, input.context.browserMountPrefix ?? input.context.defaultMountPrefix));
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
const resultText = `browserOpen failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
453
|
+
return {
|
|
454
|
+
log: `- browserOpen\n failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
455
|
+
historyNote: resultText,
|
|
456
|
+
ok: false,
|
|
457
|
+
continuity: {
|
|
458
|
+
resultText,
|
|
459
|
+
auditAnchors: ['browserOpen', 'failed'],
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
log: `- browserOpen\n ok: ${result.ok}\n url: ${result.url}\n title: ${result.title}`,
|
|
465
|
+
historyNote: `Tool result: browserOpen\nok: ${result.ok}\nurl: ${result.url}\ntitle: ${result.title}`,
|
|
466
|
+
ok: result.ok,
|
|
467
|
+
continuity: {
|
|
468
|
+
resultText: `browserOpen\nok: ${result.ok}\nurl: ${result.url}\ntitle: ${result.title}`,
|
|
469
|
+
auditAnchors: [result.url, result.title],
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
if (action.name === 'browserScreenshot') {
|
|
474
|
+
if (!browser) {
|
|
475
|
+
const resultText = 'browserScreenshot failed: no browser is currently open — nothing rendered to capture.';
|
|
476
|
+
return {
|
|
477
|
+
log: '- browserScreenshot\n blocked: browser is not open',
|
|
478
|
+
historyNote: resultText,
|
|
479
|
+
continuity: {
|
|
480
|
+
resultText,
|
|
481
|
+
auditAnchors: ['browserScreenshot', 'browser is not open'],
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const label = String(action.params.label ?? 'page');
|
|
486
|
+
const result = await browser.screenshot(label);
|
|
487
|
+
const attachments = result.base64
|
|
488
|
+
? [{ type: 'image', mimeType: 'image/png', data: result.base64 }]
|
|
489
|
+
: [];
|
|
490
|
+
return {
|
|
491
|
+
log: `- browserScreenshot\n label: ${label}`,
|
|
492
|
+
historyNote: `Tool result: browserScreenshot (label="${label}")\nThe image is attached to this turn; on later turns you'll only see this text reference.`,
|
|
493
|
+
capturedScreenshot: true,
|
|
494
|
+
attachments,
|
|
495
|
+
continuity: {
|
|
496
|
+
resultText: `browserScreenshot (label="${label}")\nThe image is attached to this turn; on later turns you'll only see this text reference.`,
|
|
497
|
+
resultTurns: 1,
|
|
498
|
+
staleText: `browserScreenshot (label="${label}") captured successfully. The image attachment is no longer in continuity.`,
|
|
499
|
+
auditAnchors: [label],
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
if (action.name === 'browserClick') {
|
|
504
|
+
if (!browser) {
|
|
505
|
+
const resultText = 'browserClick failed: no browser is currently open — the live page has not been started.';
|
|
506
|
+
return {
|
|
507
|
+
log: '- browserClick\n blocked: browser is not open',
|
|
508
|
+
historyNote: resultText,
|
|
509
|
+
ok: false,
|
|
510
|
+
continuity: {
|
|
511
|
+
resultText,
|
|
512
|
+
auditAnchors: ['browserClick', 'browser is not open'],
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const text = action.params.text === undefined ? undefined : String(action.params.text);
|
|
517
|
+
const selector = action.params.selector === undefined ? undefined : String(action.params.selector);
|
|
518
|
+
const result = await browser.click({ text, selector });
|
|
519
|
+
return {
|
|
520
|
+
log: `- browserClick\n matched: ${result.matched}\n ok: ${result.ok}\n detail: ${result.detail}`,
|
|
521
|
+
historyNote: `Tool result: browserClick\nmatched: ${result.matched}\nok: ${result.ok}\ndetail: ${result.detail}`,
|
|
522
|
+
ok: result.ok,
|
|
523
|
+
mutatedArtifact: true,
|
|
524
|
+
continuity: {
|
|
525
|
+
resultText: `browserClick\nmatched: ${result.matched}\nok: ${result.ok}\ndetail: ${result.detail}`,
|
|
526
|
+
auditAnchors: [`matched: ${result.matched}`, `ok: ${result.ok}`],
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
if (action.name === 'browserType') {
|
|
531
|
+
if (!browser) {
|
|
532
|
+
const resultText = 'browserType failed: no browser is currently open — the live page has not been started.';
|
|
533
|
+
return {
|
|
534
|
+
log: '- browserType\n blocked: browser is not open',
|
|
535
|
+
historyNote: resultText,
|
|
536
|
+
ok: false,
|
|
537
|
+
continuity: {
|
|
538
|
+
resultText,
|
|
539
|
+
auditAnchors: ['browserType', 'browser is not open'],
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
const text = String(action.params.text ?? '');
|
|
544
|
+
const selector = action.params.selector === undefined ? undefined : String(action.params.selector);
|
|
545
|
+
const result = await browser.type({ text, selector });
|
|
546
|
+
return {
|
|
547
|
+
log: `- browserType\n ok: ${result.ok}\n detail: ${result.detail}`,
|
|
548
|
+
historyNote: `Tool result: browserType\nok: ${result.ok}\ndetail: ${result.detail}`,
|
|
549
|
+
ok: result.ok,
|
|
550
|
+
mutatedArtifact: true,
|
|
551
|
+
continuity: {
|
|
552
|
+
resultText: `browserType\nok: ${result.ok}\ndetail: ${result.detail}`,
|
|
553
|
+
auditAnchors: [`ok: ${result.ok}`],
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
if (action.name === 'browserKey') {
|
|
558
|
+
if (!browser) {
|
|
559
|
+
const resultText = 'browserKey failed: no browser is currently open — the live page has not been started.';
|
|
560
|
+
return {
|
|
561
|
+
log: '- browserKey\n blocked: browser is not open',
|
|
562
|
+
historyNote: resultText,
|
|
563
|
+
ok: false,
|
|
564
|
+
continuity: {
|
|
565
|
+
resultText,
|
|
566
|
+
auditAnchors: ['browserKey', 'browser is not open'],
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const key = String(action.params.key ?? '');
|
|
571
|
+
const selector = action.params.selector === undefined ? undefined : String(action.params.selector);
|
|
572
|
+
const result = await browser.key({ key, selector });
|
|
573
|
+
return {
|
|
574
|
+
log: `- browserKey\n ok: ${result.ok}\n detail: ${result.detail}`,
|
|
575
|
+
historyNote: `Tool result: browserKey\nok: ${result.ok}\ndetail: ${result.detail}`,
|
|
576
|
+
ok: result.ok,
|
|
577
|
+
mutatedArtifact: true,
|
|
578
|
+
continuity: {
|
|
579
|
+
resultText: `browserKey\nok: ${result.ok}\ndetail: ${result.detail}`,
|
|
580
|
+
auditAnchors: [`ok: ${result.ok}`],
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
if (action.name === 'browserConsole') {
|
|
585
|
+
if (!browser) {
|
|
586
|
+
const resultText = 'browserConsole failed: no browser is currently open — no console entries to return.';
|
|
587
|
+
return {
|
|
588
|
+
log: '- browserConsole\n blocked: browser is not open',
|
|
589
|
+
historyNote: resultText,
|
|
590
|
+
ok: false,
|
|
591
|
+
continuity: {
|
|
592
|
+
resultText,
|
|
593
|
+
auditAnchors: ['browserConsole', 'browser is not open'],
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const entries = browser.getConsoleEntries();
|
|
598
|
+
const rendered = entries.length === 0
|
|
599
|
+
? '(no console messages)'
|
|
600
|
+
: entries.map(renderBrowserConsoleEntry).join('\n');
|
|
601
|
+
return {
|
|
602
|
+
log: `- browserConsole\n entries:\n${indent(rendered)}`,
|
|
603
|
+
historyNote: `Tool result: browserConsole\n${rendered}`,
|
|
604
|
+
continuity: {
|
|
605
|
+
resultText: `browserConsole\n${rendered}`,
|
|
606
|
+
staleText: '<browserConsole result removed from continuity; run browserConsole again to inspect current entries>',
|
|
607
|
+
auditAnchors: buildAuditAnchors('browserConsole', rendered),
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
return null; // unknown to the coder-action surface; host dispatches
|
|
612
|
+
}
|
|
613
|
+
function failedCoderActionResult(actionName, error) {
|
|
614
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
615
|
+
const resultText = `${actionName} failed: ${message}`;
|
|
616
|
+
return {
|
|
617
|
+
log: `- ${actionName}\n failed: ${message}`,
|
|
618
|
+
historyNote: resultText,
|
|
619
|
+
ok: false,
|
|
620
|
+
kind: inferCoderActionKind(actionName),
|
|
621
|
+
continuity: {
|
|
622
|
+
resultText,
|
|
623
|
+
auditAnchors: [actionName, 'failed', message],
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function renderBrowserConsoleEntry(entry) {
|
|
628
|
+
const location = entry.location;
|
|
629
|
+
const url = location?.url?.trim();
|
|
630
|
+
const line = typeof location?.lineNumber === 'number' ? location.lineNumber : undefined;
|
|
631
|
+
const column = typeof location?.columnNumber === 'number' ? location.columnNumber : undefined;
|
|
632
|
+
const renderedLocation = [
|
|
633
|
+
url,
|
|
634
|
+
line === undefined ? undefined : `line ${line}`,
|
|
635
|
+
column === undefined ? undefined : `col ${column}`,
|
|
636
|
+
].filter(Boolean).join(' ');
|
|
637
|
+
return renderedLocation
|
|
638
|
+
? `${entry.type}: ${entry.text} (${renderedLocation})`
|
|
639
|
+
: `${entry.type}: ${entry.text}`;
|
|
640
|
+
}
|
|
641
|
+
export function collectCoderActionAttachmentsForNextTurn(results) {
|
|
642
|
+
const attachments = results
|
|
643
|
+
.filter(result => result.continuity?.resultTurns !== 0)
|
|
644
|
+
.flatMap(result => result.attachments ?? []);
|
|
645
|
+
const latest = attachments.at(-1);
|
|
646
|
+
return latest ? [latest] : [];
|
|
647
|
+
}
|
|
648
|
+
// ─── applyPatch helpers ─────────────────────────────────────────────
|
|
649
|
+
function executeApplyPatch(input) {
|
|
650
|
+
const hasPatch = typeof input.action.params.patch === 'string' && input.action.params.patch.trim().length > 0;
|
|
651
|
+
const patch = hasPatch ? String(input.action.params.patch) : '';
|
|
652
|
+
if (!patch.trim()) {
|
|
653
|
+
const resultText = 'applyPatch failed — patch was empty; no files were changed.';
|
|
654
|
+
return {
|
|
655
|
+
log: '- applyPatch\n blocked: empty patch',
|
|
656
|
+
historyNote: resultText,
|
|
657
|
+
continuity: {
|
|
658
|
+
resultText,
|
|
659
|
+
auditAnchors: ['applyPatch', 'empty patch'],
|
|
660
|
+
},
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
const plan = planGitPatchApplication(input.context, patch);
|
|
664
|
+
if (!plan.ok)
|
|
665
|
+
return plan.result;
|
|
666
|
+
const primaryApplyArgs = ['--recount', '--whitespace=nowarn'];
|
|
667
|
+
const inaccurateEofApplyArgs = [...primaryApplyArgs, '--inaccurate-eof'];
|
|
668
|
+
let applyArgs = primaryApplyArgs;
|
|
669
|
+
let check = runGitApply(plan.root, plan.patch, ['--check', ...primaryApplyArgs]);
|
|
670
|
+
if (!check.ok) {
|
|
671
|
+
const inaccurateEofCheck = runGitApply(plan.root, plan.patch, ['--check', ...inaccurateEofApplyArgs]);
|
|
672
|
+
if (inaccurateEofCheck.ok) {
|
|
673
|
+
check = inaccurateEofCheck;
|
|
674
|
+
applyArgs = inaccurateEofApplyArgs;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (!check.ok) {
|
|
678
|
+
const resultText = renderApplyPatchFailure({
|
|
679
|
+
reason: 'patch context did not match the current workspace state',
|
|
680
|
+
touchedFiles: plan.touchedFiles,
|
|
681
|
+
gitOutput: check.output,
|
|
682
|
+
});
|
|
683
|
+
return {
|
|
684
|
+
log: `- applyPatch\n blocked: git apply --check failed\n ${indent(check.output || '(no output)')}`,
|
|
685
|
+
historyNote: resultText,
|
|
686
|
+
continuity: {
|
|
687
|
+
resultText,
|
|
688
|
+
auditAnchors: ['applyPatch', 'git apply --check failed'],
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
const applied = runGitApply(plan.root, plan.patch, applyArgs);
|
|
693
|
+
if (!applied.ok) {
|
|
694
|
+
const resultText = renderApplyPatchFailure({
|
|
695
|
+
reason: 'git accepted the patch check but failed while applying it',
|
|
696
|
+
touchedFiles: plan.touchedFiles,
|
|
697
|
+
gitOutput: applied.output,
|
|
698
|
+
});
|
|
699
|
+
return {
|
|
700
|
+
log: `- applyPatch\n failed: git apply failed after check\n ${indent(applied.output || '(no output)')}`,
|
|
701
|
+
historyNote: resultText,
|
|
702
|
+
continuity: {
|
|
703
|
+
resultText,
|
|
704
|
+
auditAnchors: ['applyPatch', 'git apply failed'],
|
|
705
|
+
},
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
const touched = plan.touchedFiles;
|
|
709
|
+
const summary = `Successfully applied patch to ${touched.length} file${touched.length === 1 ? '' : 's'}: ${touched.join(', ')}.`;
|
|
710
|
+
const resultText = [
|
|
711
|
+
'applyPatch',
|
|
712
|
+
summary,
|
|
713
|
+
renderPatchCurrentFilesSummary(plan.root, plan.pathMappings),
|
|
714
|
+
].filter(Boolean).join('\n');
|
|
715
|
+
return {
|
|
716
|
+
log: `- applyPatch: ${touched.join(', ')}`,
|
|
717
|
+
historyNote: summary,
|
|
718
|
+
mutatedArtifact: true,
|
|
719
|
+
continuity: {
|
|
720
|
+
resultText,
|
|
721
|
+
resultTurns: SMALL_WORKSET_RESULT_TURNS,
|
|
722
|
+
staleText: `applyPatch\n${summary} Exact file contents are not carried in WORK HISTORY; read files again only if needed.`,
|
|
723
|
+
auditAnchors: ['applyPatch', ...touched.slice(0, 8)],
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function renderApplyPatchFailure(input) {
|
|
728
|
+
const files = input.touchedFiles.length > 0
|
|
729
|
+
? input.touchedFiles.map(file => `- ${file}`).join('\n')
|
|
730
|
+
: '- (no files identified)';
|
|
731
|
+
return [
|
|
732
|
+
'applyPatch failed; no files were changed.',
|
|
733
|
+
`Reason: ${input.reason}.`,
|
|
734
|
+
'The patch expected lines or file state that are not present exactly as written.',
|
|
735
|
+
'Files the patch tried to touch:',
|
|
736
|
+
files,
|
|
737
|
+
'Recovery: read the affected file(s) if exact current contents are not already in this prompt, then retry with a patch whose context matches that content.',
|
|
738
|
+
'Git detail:',
|
|
739
|
+
input.gitOutput?.trim() || '(git apply did not return details)',
|
|
740
|
+
].join('\n');
|
|
741
|
+
}
|
|
742
|
+
function renderCommandSummary(argv) {
|
|
743
|
+
const rendered = JSON.stringify(argv);
|
|
744
|
+
if (Buffer.byteLength(rendered, 'utf8') <= 240)
|
|
745
|
+
return rendered;
|
|
746
|
+
const head = argv.slice(0, 2);
|
|
747
|
+
const omitted = argv.length - head.length;
|
|
748
|
+
return `${JSON.stringify(head)} + ${omitted} omitted arg${omitted === 1 ? '' : 's'} (argv omitted from continuity; see audit for exact command)`;
|
|
749
|
+
}
|
|
750
|
+
function renderSandboxToolDocumentation(result) {
|
|
751
|
+
return [
|
|
752
|
+
result.userFacingCommand ? `User-facing command: ${result.userFacingCommand}` : '',
|
|
753
|
+
result.userFacingNote ? `User-facing note: ${result.userFacingNote}` : '',
|
|
754
|
+
].filter(Boolean).join('\n');
|
|
755
|
+
}
|
|
756
|
+
function planGitPatchApplication(context, patch) {
|
|
757
|
+
const visiblePaths = extractUnifiedDiffPaths(patch);
|
|
758
|
+
if (visiblePaths.length === 0) {
|
|
759
|
+
return {
|
|
760
|
+
ok: false,
|
|
761
|
+
result: {
|
|
762
|
+
log: '- applyPatch\n blocked: no file paths found in patch',
|
|
763
|
+
historyNote: 'applyPatch failed — no file paths were found in the patch; no files were changed.',
|
|
764
|
+
continuity: {
|
|
765
|
+
resultText: 'applyPatch failed — no file paths were found in the patch; no files were changed.',
|
|
766
|
+
auditAnchors: ['applyPatch', 'no file paths'],
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
if (!context.workspaceMounts?.length) {
|
|
772
|
+
const paths = new Map(visiblePaths.map(visiblePath => [visiblePath, visiblePath]));
|
|
773
|
+
const rewrittenPatch = rewriteUnifiedDiffPaths(patch, paths);
|
|
774
|
+
return {
|
|
775
|
+
ok: true,
|
|
776
|
+
root: context.workspaceRoot,
|
|
777
|
+
patch: normalizeUnifiedDiffForGitApply(rewrittenPatch),
|
|
778
|
+
touchedFiles: visiblePaths,
|
|
779
|
+
pathMappings: visiblePaths.map(visiblePath => ({ visiblePath, relativePath: visiblePath })),
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
const resolved = visiblePaths.map(visiblePath => ({
|
|
783
|
+
visiblePath,
|
|
784
|
+
resolved: resolveWorkspaceMountPath({
|
|
785
|
+
mounts: context.workspaceMounts ?? [],
|
|
786
|
+
requestPath: visiblePath,
|
|
787
|
+
defaultMountPrefix: context.defaultMountPrefix,
|
|
788
|
+
}),
|
|
789
|
+
}));
|
|
790
|
+
const readonly = resolved.find(item => item.resolved.mount.writable === false);
|
|
791
|
+
if (readonly) {
|
|
792
|
+
return {
|
|
793
|
+
ok: false,
|
|
794
|
+
result: readonlyMountResult('applyPatch', readonly.resolved.visiblePath),
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
const roots = [...new Set(resolved.map(item => path.resolve(item.resolved.mount.root)))];
|
|
798
|
+
if (roots.length !== 1) {
|
|
799
|
+
const resultText = [
|
|
800
|
+
'applyPatch failed — this patch spans multiple workspace mounts, so Archetype cannot apply it atomically in one git operation.',
|
|
801
|
+
`Touched files: ${resolved.map(item => item.resolved.visiblePath).join(', ')}`,
|
|
802
|
+
].join('\n');
|
|
803
|
+
return {
|
|
804
|
+
ok: false,
|
|
805
|
+
result: {
|
|
806
|
+
log: `- applyPatch\n blocked: multiple mounts ${roots.join(', ')}`,
|
|
807
|
+
historyNote: resultText,
|
|
808
|
+
continuity: {
|
|
809
|
+
resultText,
|
|
810
|
+
auditAnchors: ['applyPatch', 'multiple workspace mounts'],
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
const byVisiblePath = new Map(resolved.map(item => [item.visiblePath, item.resolved.relativePath]));
|
|
816
|
+
const rewrittenPatch = rewriteUnifiedDiffPaths(patch, byVisiblePath);
|
|
817
|
+
return {
|
|
818
|
+
ok: true,
|
|
819
|
+
root: roots[0],
|
|
820
|
+
patch: normalizeUnifiedDiffForGitApply(rewrittenPatch),
|
|
821
|
+
touchedFiles: resolved.map(item => item.resolved.visiblePath),
|
|
822
|
+
pathMappings: resolved.map(item => ({
|
|
823
|
+
visiblePath: item.resolved.visiblePath,
|
|
824
|
+
relativePath: item.resolved.relativePath,
|
|
825
|
+
})),
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
function runGitApply(root, patch, args) {
|
|
829
|
+
try {
|
|
830
|
+
execFileSync('git', ['apply', ...args], {
|
|
831
|
+
cwd: root,
|
|
832
|
+
input: patch,
|
|
833
|
+
encoding: 'utf8',
|
|
834
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
835
|
+
env: {
|
|
836
|
+
...process.env,
|
|
837
|
+
GIT_CEILING_DIRECTORIES: path.dirname(root),
|
|
838
|
+
},
|
|
839
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
840
|
+
});
|
|
841
|
+
return { ok: true, output: '' };
|
|
842
|
+
}
|
|
843
|
+
catch (err) {
|
|
844
|
+
const childError = err;
|
|
845
|
+
const output = [
|
|
846
|
+
childError.stdout ? String(childError.stdout).trim() : '',
|
|
847
|
+
childError.stderr ? String(childError.stderr).trim() : '',
|
|
848
|
+
].filter(Boolean).join('\n');
|
|
849
|
+
return { ok: false, output: output || childError.message || String(err) };
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function extractUnifiedDiffPaths(patch) {
|
|
853
|
+
const paths = new Set();
|
|
854
|
+
for (const line of patch.split(/\r?\n/u)) {
|
|
855
|
+
for (const value of extractPathsFromDiffLine(line)) {
|
|
856
|
+
if (value !== '/dev/null')
|
|
857
|
+
paths.add(stripGitPathPrefix(value));
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return [...paths];
|
|
861
|
+
}
|
|
862
|
+
function extractPathsFromDiffLine(line) {
|
|
863
|
+
if (line.startsWith('diff --git ')) {
|
|
864
|
+
const match = /^diff --git\s+(\S+)\s+(\S+)/u.exec(line);
|
|
865
|
+
return match ? [match[1], match[2]] : [];
|
|
866
|
+
}
|
|
867
|
+
if (line.startsWith('--- ') || line.startsWith('+++ ')) {
|
|
868
|
+
return [line.slice(4).split(/\t/u)[0].trim()];
|
|
869
|
+
}
|
|
870
|
+
if (line.startsWith('rename from '))
|
|
871
|
+
return [line.slice('rename from '.length).trim()];
|
|
872
|
+
if (line.startsWith('rename to '))
|
|
873
|
+
return [line.slice('rename to '.length).trim()];
|
|
874
|
+
if (line.startsWith('copy from '))
|
|
875
|
+
return [line.slice('copy from '.length).trim()];
|
|
876
|
+
if (line.startsWith('copy to '))
|
|
877
|
+
return [line.slice('copy to '.length).trim()];
|
|
878
|
+
return [];
|
|
879
|
+
}
|
|
880
|
+
function rewriteUnifiedDiffPaths(patch, paths) {
|
|
881
|
+
return patch.split(/\r?\n/u).map(line => rewriteUnifiedDiffLine(line, paths)).join('\n');
|
|
882
|
+
}
|
|
883
|
+
function normalizeUnifiedDiffForGitApply(patch) {
|
|
884
|
+
const lines = patch.split(/\r?\n/u);
|
|
885
|
+
const out = [];
|
|
886
|
+
let currentHeaderIndex = null;
|
|
887
|
+
let currentHeaderHasMode = false;
|
|
888
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
889
|
+
const line = lines[index];
|
|
890
|
+
const next = lines[index + 1];
|
|
891
|
+
if (line?.startsWith('diff --git ')) {
|
|
892
|
+
currentHeaderIndex = out.length;
|
|
893
|
+
currentHeaderHasMode = false;
|
|
894
|
+
out.push(line);
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (line === 'new file mode 100644' || line === 'deleted file mode 100644') {
|
|
898
|
+
currentHeaderHasMode = true;
|
|
899
|
+
out.push(line);
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (line?.startsWith('--- ') && next?.startsWith('+++ ') && !hasCurrentDiffHeader(out)) {
|
|
903
|
+
const oldToken = line.slice(4).split(/\t/u)[0].trim();
|
|
904
|
+
const newToken = next.slice(4).split(/\t/u)[0].trim();
|
|
905
|
+
const pathToken = oldToken === '/dev/null' ? newToken : oldToken;
|
|
906
|
+
const relativePath = stripGitPathPrefix(pathToken);
|
|
907
|
+
out.push(`diff --git a/${relativePath} b/${relativePath}`);
|
|
908
|
+
if (oldToken === '/dev/null')
|
|
909
|
+
out.push('new file mode 100644');
|
|
910
|
+
if (newToken === '/dev/null')
|
|
911
|
+
out.push('deleted file mode 100644');
|
|
912
|
+
currentHeaderIndex = null;
|
|
913
|
+
currentHeaderHasMode = false;
|
|
914
|
+
}
|
|
915
|
+
else if (line?.startsWith('--- ') && next?.startsWith('+++ ') && currentHeaderIndex !== null && !currentHeaderHasMode) {
|
|
916
|
+
const oldToken = line.slice(4).split(/\t/u)[0].trim();
|
|
917
|
+
const newToken = next.slice(4).split(/\t/u)[0].trim();
|
|
918
|
+
if (oldToken === '/dev/null') {
|
|
919
|
+
out.splice(currentHeaderIndex + 1, 0, 'new file mode 100644');
|
|
920
|
+
currentHeaderHasMode = true;
|
|
921
|
+
}
|
|
922
|
+
if (newToken === '/dev/null') {
|
|
923
|
+
out.splice(currentHeaderIndex + 1, 0, 'deleted file mode 100644');
|
|
924
|
+
currentHeaderHasMode = true;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
out.push(line);
|
|
928
|
+
}
|
|
929
|
+
const normalized = out.join('\n');
|
|
930
|
+
return normalized.endsWith('\n') ? normalized : `${normalized}\n`;
|
|
931
|
+
}
|
|
932
|
+
function hasCurrentDiffHeader(lines) {
|
|
933
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
934
|
+
const line = lines[index];
|
|
935
|
+
if (!line || line.trim() === '')
|
|
936
|
+
continue;
|
|
937
|
+
if (line.startsWith('diff --git '))
|
|
938
|
+
return true;
|
|
939
|
+
if (line.startsWith('@@ ') || line.startsWith('--- ') || line.startsWith('+++ '))
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
function rewriteUnifiedDiffLine(line, paths) {
|
|
945
|
+
if (line.startsWith('diff --git ')) {
|
|
946
|
+
const match = /^diff --git\s+(\S+)\s+(\S+)/u.exec(line);
|
|
947
|
+
if (!match)
|
|
948
|
+
return line;
|
|
949
|
+
return `diff --git ${rewriteGitToken(match[1], paths)} ${rewriteGitToken(match[2], paths)}`;
|
|
950
|
+
}
|
|
951
|
+
if (line.startsWith('--- ') || line.startsWith('+++ ')) {
|
|
952
|
+
const prefix = line.slice(0, 4);
|
|
953
|
+
const rest = line.slice(4);
|
|
954
|
+
const [token, ...suffix] = rest.split(/\t/u);
|
|
955
|
+
const rewritten = rewritePatchFileHeaderToken(token.trim(), paths, line.startsWith('--- ') ? 'a' : 'b');
|
|
956
|
+
return `${prefix}${rewritten}${suffix.length ? `\t${suffix.join('\t')}` : ''}`;
|
|
957
|
+
}
|
|
958
|
+
for (const prefix of ['rename from ', 'rename to ', 'copy from ', 'copy to ']) {
|
|
959
|
+
if (line.startsWith(prefix)) {
|
|
960
|
+
return `${prefix}${rewriteVisiblePath(line.slice(prefix.length).trim(), paths)}`;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return line;
|
|
964
|
+
}
|
|
965
|
+
function rewriteGitToken(token, paths) {
|
|
966
|
+
if (token === '/dev/null')
|
|
967
|
+
return token;
|
|
968
|
+
const prefix = token.startsWith('a/') ? 'a/' : token.startsWith('b/') ? 'b/' : '';
|
|
969
|
+
const visiblePath = stripGitPathPrefix(token);
|
|
970
|
+
return `${prefix}${rewriteVisiblePath(visiblePath, paths)}`;
|
|
971
|
+
}
|
|
972
|
+
function rewritePatchFileHeaderToken(token, paths, side) {
|
|
973
|
+
if (token === '/dev/null')
|
|
974
|
+
return token;
|
|
975
|
+
return `${side}/${rewriteVisiblePath(token, paths)}`;
|
|
976
|
+
}
|
|
977
|
+
function rewriteVisiblePath(visiblePath, paths) {
|
|
978
|
+
return paths.get(stripGitPathPrefix(visiblePath)) ?? visiblePath;
|
|
979
|
+
}
|
|
980
|
+
function stripGitPathPrefix(value) {
|
|
981
|
+
return value.replace(/^"(.*)"$/u, '$1').replace(/^[ab]\//u, '');
|
|
982
|
+
}
|
|
983
|
+
function renderPatchCurrentFilesSummary(root, pathMappings) {
|
|
984
|
+
const sections = ['Changed file state:'];
|
|
985
|
+
for (const mapping of pathMappings.slice(0, 8)) {
|
|
986
|
+
const absolutePath = resolveWorkspacePath(root, mapping.relativePath);
|
|
987
|
+
if (!fs.existsSync(absolutePath)) {
|
|
988
|
+
sections.push(`- ${mapping.visiblePath} — deleted`);
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
const content = safeReadWorkspaceText(absolutePath);
|
|
992
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
993
|
+
sections.push(`- ${mapping.visiblePath} — ${renderTextSize(content, bytes)}`);
|
|
994
|
+
}
|
|
995
|
+
if (pathMappings.length > 8)
|
|
996
|
+
sections.push(`- ${pathMappings.length - 8} additional file(s) changed; use listFiles/readFile if needed.`);
|
|
997
|
+
sections.push('Exact file contents are not carried in WORK HISTORY; use readFile if exact contents are needed.');
|
|
998
|
+
return sections.join('\n');
|
|
999
|
+
}
|
|
1000
|
+
function snapshotWorkspaceFiles(root) {
|
|
1001
|
+
const files = new Map();
|
|
1002
|
+
const resolvedRoot = path.resolve(root);
|
|
1003
|
+
const ignoredDirectories = new Set(['.git', '.sandbox-tmp', 'node_modules']);
|
|
1004
|
+
const walk = (directory, relativeDirectory) => {
|
|
1005
|
+
let entries = [];
|
|
1006
|
+
try {
|
|
1007
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
1008
|
+
}
|
|
1009
|
+
catch {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1013
|
+
for (const entry of entries) {
|
|
1014
|
+
if (entry.name.startsWith('.'))
|
|
1015
|
+
continue;
|
|
1016
|
+
if (entry.isDirectory() && ignoredDirectories.has(entry.name))
|
|
1017
|
+
continue;
|
|
1018
|
+
const relativePath = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
|
|
1019
|
+
const absolutePath = path.join(directory, entry.name);
|
|
1020
|
+
if (entry.isDirectory()) {
|
|
1021
|
+
walk(absolutePath, relativePath);
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (!entry.isFile())
|
|
1025
|
+
continue;
|
|
1026
|
+
let stat;
|
|
1027
|
+
try {
|
|
1028
|
+
stat = fs.statSync(absolutePath);
|
|
1029
|
+
}
|
|
1030
|
+
catch {
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
files.set(relativePath, {
|
|
1034
|
+
visiblePath: relativePath.replace(/\\/gu, '/'),
|
|
1035
|
+
absolutePath,
|
|
1036
|
+
bytes: stat.size,
|
|
1037
|
+
mtimeMs: stat.mtimeMs,
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
walk(resolvedRoot, '');
|
|
1042
|
+
return files;
|
|
1043
|
+
}
|
|
1044
|
+
function snapshotCoderWorkspaceFiles(context) {
|
|
1045
|
+
if (!context.workspaceMounts?.length) {
|
|
1046
|
+
return snapshotWorkspaceFiles(context.workspaceRoot);
|
|
1047
|
+
}
|
|
1048
|
+
const files = new Map();
|
|
1049
|
+
for (const mount of context.workspaceMounts) {
|
|
1050
|
+
const prefix = mount.prefix.replace(/^\/+|\/+$/gu, '');
|
|
1051
|
+
const mountFiles = snapshotWorkspaceFiles(path.resolve(mount.root));
|
|
1052
|
+
for (const [relativePath, signature] of mountFiles) {
|
|
1053
|
+
const visiblePath = prefix ? `${prefix}/${relativePath}` : relativePath;
|
|
1054
|
+
files.set(visiblePath, {
|
|
1055
|
+
...signature,
|
|
1056
|
+
visiblePath,
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return files;
|
|
1061
|
+
}
|
|
1062
|
+
function prepareRunCommandWorkspace(context) {
|
|
1063
|
+
if (!context.workspaceMounts?.length)
|
|
1064
|
+
return {};
|
|
1065
|
+
const visibleRoot = materializeVisibleWorkspaceRoot(context);
|
|
1066
|
+
const mountRoots = context.workspaceMounts.map(mount => path.resolve(mount.root));
|
|
1067
|
+
const writableRoots = context.workspaceMounts
|
|
1068
|
+
.filter(mount => mount.writable !== false)
|
|
1069
|
+
.map(mount => path.resolve(mount.root));
|
|
1070
|
+
return {
|
|
1071
|
+
cwd: visibleRoot,
|
|
1072
|
+
extraReadPaths: [visibleRoot, ...mountRoots],
|
|
1073
|
+
// The visible root itself is intentionally not writable; commands should
|
|
1074
|
+
// write through canonical mounted paths such as artifact/index.html.
|
|
1075
|
+
extraWritePaths: writableRoots,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function materializeVisibleWorkspaceRoot(context) {
|
|
1079
|
+
const visibleRoot = path.join(context.workspaceRoot, '.archetype-visible-workspace');
|
|
1080
|
+
fs.rmSync(visibleRoot, { recursive: true, force: true });
|
|
1081
|
+
fs.mkdirSync(visibleRoot, { recursive: true });
|
|
1082
|
+
for (const mount of context.workspaceMounts ?? []) {
|
|
1083
|
+
const prefix = mount.prefix.replace(/^\/+|\/+$/gu, '');
|
|
1084
|
+
if (!prefix)
|
|
1085
|
+
continue;
|
|
1086
|
+
const linkPath = path.join(visibleRoot, prefix);
|
|
1087
|
+
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
|
|
1088
|
+
fs.symlinkSync(path.resolve(mount.root), linkPath, 'dir');
|
|
1089
|
+
}
|
|
1090
|
+
return visibleRoot;
|
|
1091
|
+
}
|
|
1092
|
+
function renderWorkspaceFileChanges(before, after) {
|
|
1093
|
+
const changed = [];
|
|
1094
|
+
const paths = new Set([...before.keys(), ...after.keys()]);
|
|
1095
|
+
for (const relativePath of [...paths].sort((a, b) => a.localeCompare(b))) {
|
|
1096
|
+
const previous = before.get(relativePath);
|
|
1097
|
+
const next = after.get(relativePath);
|
|
1098
|
+
if (!previous && next) {
|
|
1099
|
+
changed.push(`- ${next.visiblePath} — created, ${renderWorkspaceSignatureSize(next)}`);
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
if (previous && !next) {
|
|
1103
|
+
changed.push(`- ${previous.visiblePath} — deleted`);
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
if (previous && next && (previous.bytes !== next.bytes || previous.mtimeMs !== next.mtimeMs)) {
|
|
1107
|
+
changed.push(`- ${next.visiblePath} — modified, ${renderWorkspaceSignatureSize(next)}`);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (changed.length === 0)
|
|
1111
|
+
return '';
|
|
1112
|
+
const lines = ['Changed file state:', ...changed.slice(0, 8)];
|
|
1113
|
+
if (changed.length > 8)
|
|
1114
|
+
lines.push(`- ${changed.length - 8} additional file(s) changed; use listFiles/readFile if needed.`);
|
|
1115
|
+
lines.push('Exact file contents are not carried in WORK HISTORY; use readFile if exact contents are needed.');
|
|
1116
|
+
return lines.join('\n');
|
|
1117
|
+
}
|
|
1118
|
+
function renderWorkspaceSignatureSize(signature) {
|
|
1119
|
+
const content = safeReadWorkspaceText(signature.absolutePath);
|
|
1120
|
+
return renderTextSize(content, signature.bytes);
|
|
1121
|
+
}
|
|
1122
|
+
function renderTextSize(content, bytes = Buffer.byteLength(content, 'utf8')) {
|
|
1123
|
+
const lines = countTextLines(content);
|
|
1124
|
+
return `${lines} line${lines === 1 ? '' : 's'}, ${bytes} bytes`;
|
|
1125
|
+
}
|
|
1126
|
+
function countTextLines(content) {
|
|
1127
|
+
if (content.length === 0)
|
|
1128
|
+
return 0;
|
|
1129
|
+
const lineBreaks = content.match(/\n/g)?.length ?? 0;
|
|
1130
|
+
return content.endsWith('\n') ? lineBreaks : lineBreaks + 1;
|
|
1131
|
+
}
|
|
1132
|
+
function resolveBrowserOpenPath(rawPath, defaultMountPrefix) {
|
|
1133
|
+
const raw = String(rawPath ?? '/').trim() || '/';
|
|
1134
|
+
if (/^[a-z][a-z0-9+.-]*:/iu.test(raw)) {
|
|
1135
|
+
try {
|
|
1136
|
+
const url = new URL(raw);
|
|
1137
|
+
url.pathname = normalizeBrowserOpenPath(url.pathname, defaultMountPrefix);
|
|
1138
|
+
return url.toString();
|
|
1139
|
+
}
|
|
1140
|
+
catch {
|
|
1141
|
+
return raw;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return normalizeBrowserOpenPath(raw, defaultMountPrefix);
|
|
1145
|
+
}
|
|
1146
|
+
function normalizeBrowserOpenPath(rawPath, defaultMountPrefix) {
|
|
1147
|
+
const withLeadingSlash = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
|
|
1148
|
+
const prefix = defaultMountPrefix?.replace(/^\/+|\/+$/gu, '');
|
|
1149
|
+
if (!prefix)
|
|
1150
|
+
return withLeadingSlash;
|
|
1151
|
+
const mountPrefix = `/${prefix}/`;
|
|
1152
|
+
if (withLeadingSlash === `/${prefix}`)
|
|
1153
|
+
return '/';
|
|
1154
|
+
if (withLeadingSlash.startsWith(mountPrefix)) {
|
|
1155
|
+
return `/${withLeadingSlash.slice(mountPrefix.length)}`;
|
|
1156
|
+
}
|
|
1157
|
+
return withLeadingSlash;
|
|
1158
|
+
}
|
|
1159
|
+
// ─── editFile helpers (multi-edit) ──────────────────────────────────
|
|
1160
|
+
function executeEditFile(input) {
|
|
1161
|
+
const { action, workspaceRoot } = input;
|
|
1162
|
+
const relativePath = input.visiblePath ?? String(action.params.path);
|
|
1163
|
+
const diskRelativePath = input.relativePath ?? relativePath;
|
|
1164
|
+
const target = resolveWorkspacePath(workspaceRoot, diskRelativePath);
|
|
1165
|
+
// Accept either the multi-edit shape `edits: [{oldText, newText}, ...]`
|
|
1166
|
+
// or the legacy single-edit shape `{oldText, newText}` (older traces,
|
|
1167
|
+
// pre-rename models).
|
|
1168
|
+
const rawEdits = Array.isArray(action.params.edits)
|
|
1169
|
+
? action.params.edits
|
|
1170
|
+
: (action.params.oldText !== undefined
|
|
1171
|
+
? [{ oldText: action.params.oldText, newText: action.params.newText ?? '' }]
|
|
1172
|
+
: []);
|
|
1173
|
+
const edits = rawEdits.map(e => ({
|
|
1174
|
+
oldText: String(e?.oldText ?? ''),
|
|
1175
|
+
newText: String(e?.newText ?? ''),
|
|
1176
|
+
occurrence: parseEditOccurrence(e?.occurrence),
|
|
1177
|
+
})).filter(e => e.oldText.length > 0);
|
|
1178
|
+
if (!fs.existsSync(target)) {
|
|
1179
|
+
return {
|
|
1180
|
+
log: `- editFile\n blocked: missing ${relativePath}`,
|
|
1181
|
+
historyNote: `editFile on ${relativePath} failed: the file does not exist at that path yet.`,
|
|
1182
|
+
continuity: {
|
|
1183
|
+
resultText: `editFile ${relativePath}\nfailed — the file does not exist at that path yet; no edits applied.`,
|
|
1184
|
+
auditAnchors: [relativePath, 'file does not exist'],
|
|
1185
|
+
},
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
if (edits.length === 0) {
|
|
1189
|
+
return {
|
|
1190
|
+
log: `- editFile\n blocked: no edits provided`,
|
|
1191
|
+
historyNote: `editFile on ${relativePath} failed: the edits[] array was empty — no edits were applied.`,
|
|
1192
|
+
continuity: {
|
|
1193
|
+
resultText: `editFile ${relativePath}\nfailed — edits[] was empty; no edits applied.`,
|
|
1194
|
+
auditAnchors: [relativePath, 'edits[] was empty'],
|
|
1195
|
+
},
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
const original = fs.readFileSync(target, 'utf8');
|
|
1199
|
+
const positions = [];
|
|
1200
|
+
const failures = [];
|
|
1201
|
+
for (let i = 0; i < edits.length; i++) {
|
|
1202
|
+
const { oldText, newText, occurrence } = edits[i];
|
|
1203
|
+
const matches = findMatchIndexes(original, oldText);
|
|
1204
|
+
if (matches.length === 0) {
|
|
1205
|
+
// Show the model a snippet of what IS in the file near the best
|
|
1206
|
+
// heuristic match point, so it can see why its oldText didn't hit
|
|
1207
|
+
// without needing a follow-up readFile. Saves a round-trip.
|
|
1208
|
+
const nearest = nearestContextForMissedEdit(original, oldText);
|
|
1209
|
+
failures.push({
|
|
1210
|
+
index: i,
|
|
1211
|
+
reason: nearest
|
|
1212
|
+
? `oldText did not match the original file content. ${nearest}`
|
|
1213
|
+
: 'oldText did not match the original file content',
|
|
1214
|
+
});
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
if (occurrence !== undefined) {
|
|
1218
|
+
if (occurrence > matches.length) {
|
|
1219
|
+
failures.push({
|
|
1220
|
+
index: i,
|
|
1221
|
+
reason: `oldText matched ${matches.length} time(s), but occurrence ${occurrence} is out of range. Match lines: ${formatMatchLines(original, matches)}`,
|
|
1222
|
+
});
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
const start = matches[occurrence - 1];
|
|
1226
|
+
positions.push({ index: i, start, end: start + oldText.length, newText, oldText });
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
if (matches.length > 1) {
|
|
1230
|
+
failures.push({
|
|
1231
|
+
index: i,
|
|
1232
|
+
reason: `oldText matches ${matches.length} times at ${formatMatchLines(original, matches)}; set occurrence to the 1-based match to replace, or use writeFile to replace the whole file`,
|
|
1233
|
+
});
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
const start = matches[0];
|
|
1237
|
+
positions.push({ index: i, start, end: start + oldText.length, newText, oldText });
|
|
1238
|
+
}
|
|
1239
|
+
// Detect overlapping edits.
|
|
1240
|
+
const sorted = [...positions].sort((a, b) => a.start - b.start);
|
|
1241
|
+
const rejected = new Set();
|
|
1242
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1243
|
+
if (sorted[i].start < sorted[i - 1].end) {
|
|
1244
|
+
const priorIdx = sorted[i - 1].index;
|
|
1245
|
+
failures.push({
|
|
1246
|
+
index: sorted[i].index,
|
|
1247
|
+
reason: `overlaps edits[${priorIdx}] in this call (their spans cover the same region of the file)`,
|
|
1248
|
+
});
|
|
1249
|
+
rejected.add(sorted[i].index);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
const applicable = positions.filter(p => !rejected.has(p.index));
|
|
1253
|
+
if (applicable.length === 0 || failures.length > 0) {
|
|
1254
|
+
const detail = failures.map(f => `edits[${f.index}]: ${f.reason}`).join('; ');
|
|
1255
|
+
const currentFileContext = renderCurrentFileContentContext(original);
|
|
1256
|
+
const resultText = [
|
|
1257
|
+
`editFile ${relativePath}`,
|
|
1258
|
+
`failed — no edits applied. ${detail}.`,
|
|
1259
|
+
currentFileContext,
|
|
1260
|
+
].filter(Boolean).join('\n');
|
|
1261
|
+
return {
|
|
1262
|
+
log: `- editFile\n blocked: edit set did not apply atomically in ${relativePath}`,
|
|
1263
|
+
historyNote: `editFile on ${relativePath} failed — no edits applied. ${detail}.`,
|
|
1264
|
+
continuity: {
|
|
1265
|
+
resultText,
|
|
1266
|
+
resultTurns: SMALL_WORKSET_RESULT_TURNS,
|
|
1267
|
+
staleText: `editFile ${relativePath}\nfailed — no edits applied. ${detail}. Current file content no longer carried in WORK HISTORY; read the file again only if exact contents are needed.`,
|
|
1268
|
+
auditAnchors: [relativePath],
|
|
1269
|
+
},
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
// Apply replacements in reverse position order so earlier positions are stable.
|
|
1273
|
+
const apply = [...applicable].sort((a, b) => b.start - a.start);
|
|
1274
|
+
let next = original;
|
|
1275
|
+
for (const p of apply) {
|
|
1276
|
+
next = next.slice(0, p.start) + p.newText + next.slice(p.end);
|
|
1277
|
+
}
|
|
1278
|
+
fs.writeFileSync(target, next, 'utf8');
|
|
1279
|
+
const newBytes = Buffer.byteLength(next, 'utf8');
|
|
1280
|
+
const applied = applicable.length;
|
|
1281
|
+
const baseNote = `Successfully replaced ${applied} block(s) in ${relativePath} (file is now ${newBytes} bytes).`;
|
|
1282
|
+
const resultText = [
|
|
1283
|
+
`editFile ${relativePath}`,
|
|
1284
|
+
baseNote,
|
|
1285
|
+
renderCurrentFileContentContext(next),
|
|
1286
|
+
].join('\n');
|
|
1287
|
+
return {
|
|
1288
|
+
log: `- editFile: ${relativePath} (${applied}/${edits.length} edits)`,
|
|
1289
|
+
historyNote: baseNote,
|
|
1290
|
+
mutatedArtifact: true,
|
|
1291
|
+
continuity: {
|
|
1292
|
+
resultText,
|
|
1293
|
+
resultTurns: SMALL_WORKSET_RESULT_TURNS,
|
|
1294
|
+
staleText: `editFile ${relativePath}\n${baseNote} Current file content no longer carried in WORK HISTORY; read the file again only if exact contents are needed.`,
|
|
1295
|
+
auditAnchors: [relativePath],
|
|
1296
|
+
},
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
// ─── searchInFiles ──────────────────────────────────────────────────
|
|
1300
|
+
function executeSearchInFiles(input) {
|
|
1301
|
+
const { action, context } = input;
|
|
1302
|
+
const pattern = String(action.params.pattern);
|
|
1303
|
+
const pathGlob = String(action.params.pathGlob ?? '');
|
|
1304
|
+
let re;
|
|
1305
|
+
try {
|
|
1306
|
+
re = new RegExp(pattern, 'gm');
|
|
1307
|
+
}
|
|
1308
|
+
catch (err) {
|
|
1309
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1310
|
+
return {
|
|
1311
|
+
log: `- searchInFiles\n blocked: invalid regex — ${msg}`,
|
|
1312
|
+
historyNote: `searchInFiles failed: the regex "${pattern}" did not parse (${msg}).`,
|
|
1313
|
+
ok: false,
|
|
1314
|
+
continuity: {
|
|
1315
|
+
resultText: `searchInFiles "${pattern}" failed: ${msg}`,
|
|
1316
|
+
auditAnchors: [pattern],
|
|
1317
|
+
},
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
const globRe = pathGlob
|
|
1321
|
+
? new RegExp('^' + pathGlob.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$')
|
|
1322
|
+
: null;
|
|
1323
|
+
const matches = [];
|
|
1324
|
+
let filesScanned = 0;
|
|
1325
|
+
const walk = (dir, prefix) => {
|
|
1326
|
+
let items = [];
|
|
1327
|
+
try {
|
|
1328
|
+
items = fs.readdirSync(dir, { withFileTypes: true });
|
|
1329
|
+
}
|
|
1330
|
+
catch {
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
for (const it of items) {
|
|
1334
|
+
if (it.name.startsWith('.') || it.name === 'node_modules' || it.name === 'dist')
|
|
1335
|
+
continue;
|
|
1336
|
+
const full = path.join(dir, it.name);
|
|
1337
|
+
const relPath = prefix ? `${prefix}/${it.name}` : it.name;
|
|
1338
|
+
if (it.isDirectory()) {
|
|
1339
|
+
walk(full, relPath);
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
if (globRe && !globRe.test(relPath))
|
|
1343
|
+
continue;
|
|
1344
|
+
let content = '';
|
|
1345
|
+
try {
|
|
1346
|
+
content = fs.readFileSync(full, 'utf8');
|
|
1347
|
+
}
|
|
1348
|
+
catch {
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
filesScanned += 1;
|
|
1352
|
+
const lines = content.split('\n');
|
|
1353
|
+
for (let i = 0; i < lines.length && matches.length < 200; i += 1) {
|
|
1354
|
+
if (re.test(lines[i])) {
|
|
1355
|
+
matches.push({ file: relPath, line: i + 1, text: lines[i].slice(0, 240) });
|
|
1356
|
+
}
|
|
1357
|
+
re.lastIndex = 0;
|
|
1358
|
+
}
|
|
1359
|
+
if (matches.length >= 200)
|
|
1360
|
+
break;
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
if (context.workspaceMounts?.length) {
|
|
1364
|
+
for (const mount of context.workspaceMounts) {
|
|
1365
|
+
const prefix = mount.prefix.replace(/^\/+|\/+$/gu, '');
|
|
1366
|
+
walk(path.resolve(mount.root), prefix);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
else {
|
|
1370
|
+
walk(context.workspaceRoot, '');
|
|
1371
|
+
}
|
|
1372
|
+
const rendered = matches.length > 0
|
|
1373
|
+
? matches.map((m) => `${m.file}:${m.line}: ${m.text}`).join('\n')
|
|
1374
|
+
: '(no matches)';
|
|
1375
|
+
return {
|
|
1376
|
+
log: `- searchInFiles: "${pattern}" (${matches.length} matches in ${filesScanned} files)\n ${indent(rendered)}`,
|
|
1377
|
+
historyNote: `Tool result: searchInFiles "${pattern}" → ${matches.length} literal matches in ${filesScanned} files\n${rendered}`,
|
|
1378
|
+
continuity: {
|
|
1379
|
+
resultText: `searchInFiles "${pattern}" → ${matches.length} literal matches in ${filesScanned} files\n${rendered}`,
|
|
1380
|
+
staleText: `<searchInFiles result for "${pattern}" removed from continuity; run searchInFiles again to inspect current matches>`,
|
|
1381
|
+
auditAnchors: buildAuditAnchors(pattern, rendered),
|
|
1382
|
+
},
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
function resolveCoderFilePath(context, requestPath, _options) {
|
|
1386
|
+
if (!context.workspaceMounts?.length) {
|
|
1387
|
+
const relativePath = requestPath === '.' ? '' : requestPath;
|
|
1388
|
+
return {
|
|
1389
|
+
root: context.workspaceRoot,
|
|
1390
|
+
absolutePath: resolveWorkspacePath(context.workspaceRoot, relativePath),
|
|
1391
|
+
relativePath,
|
|
1392
|
+
visiblePath: relativePath,
|
|
1393
|
+
writable: true,
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
const resolved = resolveWorkspaceMountPath({
|
|
1397
|
+
mounts: context.workspaceMounts,
|
|
1398
|
+
requestPath,
|
|
1399
|
+
defaultMountPrefix: context.defaultMountPrefix,
|
|
1400
|
+
});
|
|
1401
|
+
return {
|
|
1402
|
+
root: resolved.mount.root,
|
|
1403
|
+
absolutePath: resolved.absolutePath,
|
|
1404
|
+
relativePath: resolved.relativePath,
|
|
1405
|
+
visiblePath: resolved.visiblePath,
|
|
1406
|
+
writable: resolved.mount.writable !== false,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
function readonlyMountResult(actionName, visiblePath) {
|
|
1410
|
+
const resultText = `${actionName} ${visiblePath}\nfailed — ${visiblePath} is in a read-only workspace mount; no edits were applied.`;
|
|
1411
|
+
return {
|
|
1412
|
+
log: `- ${actionName}\n blocked: read-only mount ${visiblePath}`,
|
|
1413
|
+
historyNote: resultText,
|
|
1414
|
+
continuity: {
|
|
1415
|
+
resultText,
|
|
1416
|
+
auditAnchors: [visiblePath, 'read-only workspace mount'],
|
|
1417
|
+
},
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
function buildAuditAnchors(primary, content) {
|
|
1421
|
+
const anchors = [primary];
|
|
1422
|
+
const lines = content
|
|
1423
|
+
.split('\n')
|
|
1424
|
+
.map(line => line.trim())
|
|
1425
|
+
.filter(line => line.length > 0 && line !== '(no matches)' && line !== '(empty)' && !line.startsWith('['));
|
|
1426
|
+
for (const line of lines.slice(0, 2)) {
|
|
1427
|
+
anchors.push(line.slice(0, 120));
|
|
1428
|
+
}
|
|
1429
|
+
return anchors;
|
|
1430
|
+
}
|
|
1431
|
+
// ─── Workspace helpers ──────────────────────────────────────────────
|
|
1432
|
+
/**
|
|
1433
|
+
* Resolve a model-supplied relative path against the workspace root,
|
|
1434
|
+
* refusing any path that escapes the workspace. Throws — the model
|
|
1435
|
+
* should never see a path-traversal attempt succeed.
|
|
1436
|
+
*/
|
|
1437
|
+
export function resolveWorkspacePath(root, relativePath) {
|
|
1438
|
+
const resolved = path.resolve(root, relativePath);
|
|
1439
|
+
if (!resolved.startsWith(root)) {
|
|
1440
|
+
throw new Error(`Refusing to access outside workspace: ${relativePath}. Use a visible path from FILES; do not use ../ to cross mounts.`);
|
|
1441
|
+
}
|
|
1442
|
+
return resolved;
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Read a workspace file safely: returns a sentinel string for missing
|
|
1446
|
+
* / oversized / binary files instead of throwing. No silent truncation
|
|
1447
|
+
* for text files — if the model called readFile, it needs the full
|
|
1448
|
+
* content to form editFile.oldText correctly.
|
|
1449
|
+
*/
|
|
1450
|
+
export function safeReadWorkspaceText(filePath) {
|
|
1451
|
+
if (!fs.existsSync(filePath)) {
|
|
1452
|
+
return `(file missing: ${path.basename(filePath)})`;
|
|
1453
|
+
}
|
|
1454
|
+
const stat = fs.statSync(filePath);
|
|
1455
|
+
if (stat.size > 64 * 1024)
|
|
1456
|
+
return `(file skipped: ${path.basename(filePath)} is ${stat.size} bytes)`;
|
|
1457
|
+
const buffer = fs.readFileSync(filePath);
|
|
1458
|
+
if (buffer.subarray(0, 512).includes(0))
|
|
1459
|
+
return `(file skipped: ${path.basename(filePath)} appears to be binary)`;
|
|
1460
|
+
return buffer.toString('utf8');
|
|
1461
|
+
}
|
|
1462
|
+
function renderReadFileContent(value) {
|
|
1463
|
+
// No silent truncation. The model called readFile for a reason; showing it
|
|
1464
|
+
// a fraction of its own file and telling it the rest is hidden is a silent
|
|
1465
|
+
// bug (surfaced 2026-04-21: model read 13KB script.js, saw only 4KB, had
|
|
1466
|
+
// to form editFile.oldText from the visible portion, sometimes guessed
|
|
1467
|
+
// wrong). History compaction is a separate concern.
|
|
1468
|
+
return `content:\n${value}`;
|
|
1469
|
+
}
|
|
1470
|
+
function parseEditOccurrence(value) {
|
|
1471
|
+
if (value === undefined || value === null || value === '')
|
|
1472
|
+
return undefined;
|
|
1473
|
+
const numeric = typeof value === 'number' ? value : Number(value);
|
|
1474
|
+
if (!Number.isInteger(numeric) || numeric <= 0)
|
|
1475
|
+
return undefined;
|
|
1476
|
+
return numeric;
|
|
1477
|
+
}
|
|
1478
|
+
function findMatchIndexes(fileContent, oldText) {
|
|
1479
|
+
const matches = [];
|
|
1480
|
+
let cursor = 0;
|
|
1481
|
+
while (cursor <= fileContent.length) {
|
|
1482
|
+
const found = fileContent.indexOf(oldText, cursor);
|
|
1483
|
+
if (found === -1)
|
|
1484
|
+
break;
|
|
1485
|
+
matches.push(found);
|
|
1486
|
+
cursor = found + Math.max(oldText.length, 1);
|
|
1487
|
+
}
|
|
1488
|
+
return matches;
|
|
1489
|
+
}
|
|
1490
|
+
function formatMatchLines(fileContent, indexes) {
|
|
1491
|
+
return indexes
|
|
1492
|
+
.slice(0, 8)
|
|
1493
|
+
.map((index, matchIndex) => `#${matchIndex + 1} line ${lineNumberAtIndex(fileContent, index)}`)
|
|
1494
|
+
.join(', ');
|
|
1495
|
+
}
|
|
1496
|
+
function lineNumberAtIndex(fileContent, index) {
|
|
1497
|
+
let line = 1;
|
|
1498
|
+
for (let i = 0; i < index; i += 1) {
|
|
1499
|
+
if (fileContent.charCodeAt(i) === 10)
|
|
1500
|
+
line += 1;
|
|
1501
|
+
}
|
|
1502
|
+
return line;
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* When an editFile.oldText doesn't match the file, emit a short hint
|
|
1506
|
+
* pointing at the line in the file that shares the most prefix with the
|
|
1507
|
+
* attempted oldText. Saves the model a round-trip readFile when a ~40-
|
|
1508
|
+
* char nudge is enough to repair the call. Returns empty string when
|
|
1509
|
+
* no useful heuristic match exists.
|
|
1510
|
+
*/
|
|
1511
|
+
function nearestContextForMissedEdit(fileContent, oldText) {
|
|
1512
|
+
const firstLine = oldText.split('\n')[0].trim();
|
|
1513
|
+
if (firstLine.length < 8)
|
|
1514
|
+
return ''; // too short to be discriminating
|
|
1515
|
+
const prefix = firstLine.slice(0, 32);
|
|
1516
|
+
const lines = fileContent.split('\n');
|
|
1517
|
+
let bestIdx = -1;
|
|
1518
|
+
let bestLen = 0;
|
|
1519
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1520
|
+
const line = lines[i];
|
|
1521
|
+
for (let s = 0; s <= line.length - 4; s += 1) {
|
|
1522
|
+
let matched = 0;
|
|
1523
|
+
while (matched < prefix.length && s + matched < line.length && line[s + matched] === prefix[matched]) {
|
|
1524
|
+
matched += 1;
|
|
1525
|
+
}
|
|
1526
|
+
if (matched > bestLen) {
|
|
1527
|
+
bestLen = matched;
|
|
1528
|
+
bestIdx = i;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
if (bestIdx < 0 || bestLen < 6)
|
|
1533
|
+
return '';
|
|
1534
|
+
const lo = Math.max(0, bestIdx - 1);
|
|
1535
|
+
const hi = Math.min(lines.length - 1, bestIdx + 1);
|
|
1536
|
+
const snippet = lines.slice(lo, hi + 1).map((l, k) => ` ${lo + k + 1}: ${l.slice(0, 120)}`).join('\n');
|
|
1537
|
+
return `Nearest candidate in the file (line ${bestIdx + 1}):\n${snippet}`;
|
|
1538
|
+
}
|
|
1539
|
+
function renderCurrentFileContentContext(fileContent) {
|
|
1540
|
+
return `Current file content not carried (${renderTextSize(fileContent)}); use readFile if exact contents are needed.`;
|
|
1541
|
+
}
|
|
1542
|
+
function indent(value) {
|
|
1543
|
+
return value.split('\n').map(line => ` ${line}`).join('\n');
|
|
1544
|
+
}
|
|
1545
|
+
function truncate(value, limit) {
|
|
1546
|
+
return value.length <= limit ? value : `${value.slice(0, limit)}\n...[truncated]`;
|
|
1547
|
+
}
|
|
1548
|
+
//# sourceMappingURL=executor.js.map
|