@causyne/agent 0.0.0-stage → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/LICENSE +102 -0
  2. package/README.md +31 -2
  3. package/SECURITY.md +24 -0
  4. package/bin/causyne.js +13 -0
  5. package/docs/ARCHITECTURE.md +63 -0
  6. package/docs/PROVENANCE.md +29 -0
  7. package/docs/SECURITY_REVIEW.md +89 -0
  8. package/docs/THIRD_PARTY_NOTICES.md +49 -0
  9. package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
  10. package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
  11. package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
  12. package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
  13. package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
  14. package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
  15. package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
  16. package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
  17. package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
  18. package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
  19. package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
  20. package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
  21. package/native-skills/android/play-store-release/SKILL.md +43 -0
  22. package/native-skills/android/room-and-persistence/SKILL.md +40 -0
  23. package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
  24. package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
  25. package/native-skills/backend/caching-strategy/SKILL.md +44 -0
  26. package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
  27. package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
  28. package/native-skills/backend/rest-api-design/SKILL.md +40 -0
  29. package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
  30. package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
  31. package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
  32. package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
  33. package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
  34. package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
  35. package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
  36. package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
  37. package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
  38. package/native-skills/data/metric-definition/SKILL.md +42 -0
  39. package/native-skills/data/notebook-to-production/SKILL.md +43 -0
  40. package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
  41. package/native-skills/database/connection-pooling/SKILL.md +42 -0
  42. package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
  43. package/native-skills/database/query-optimisation/SKILL.md +38 -0
  44. package/native-skills/database/safe-migrations/SKILL.md +47 -0
  45. package/native-skills/database/schema-design/SKILL.md +38 -0
  46. package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
  47. package/native-skills/design/colour-system-construction/SKILL.md +38 -0
  48. package/native-skills/design/design-direction-brief/SKILL.md +41 -0
  49. package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
  50. package/native-skills/design/responsive-strategy/SKILL.md +39 -0
  51. package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
  52. package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
  53. package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
  54. package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
  55. package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
  56. package/native-skills/devops/incident-triage/SKILL.md +38 -0
  57. package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
  58. package/native-skills/devops/observability-baseline/SKILL.md +38 -0
  59. package/native-skills/devops/secret-management/SKILL.md +40 -0
  60. package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
  61. package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
  62. package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
  63. package/native-skills/git/branch-strategy/SKILL.md +39 -0
  64. package/native-skills/git/commit-hygiene/SKILL.md +41 -0
  65. package/native-skills/git/history-recovery/SKILL.md +44 -0
  66. package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
  67. package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
  68. package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
  69. package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
  70. package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
  71. package/native-skills/mobile/deep-linking/SKILL.md +39 -0
  72. package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
  73. package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
  74. package/native-skills/mobile/push-notifications/SKILL.md +38 -0
  75. package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
  76. package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
  77. package/native-skills/performance/load-testing/SKILL.md +42 -0
  78. package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
  79. package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
  80. package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
  81. package/native-skills/python/async-correctness/SKILL.md +40 -0
  82. package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
  83. package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
  84. package/native-skills/python/environment-isolation/SKILL.md +41 -0
  85. package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
  86. package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
  87. package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
  88. package/native-skills/security/authorization-model/SKILL.md +42 -0
  89. package/native-skills/security/crypto-usage/SKILL.md +48 -0
  90. package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
  91. package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
  92. package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
  93. package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
  94. package/native-skills/security/threat-modelling/SKILL.md +46 -0
  95. package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
  96. package/native-skills/testing/property-based-testing/SKILL.md +46 -0
  97. package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
  98. package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
  99. package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
  100. package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
  101. package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
  102. package/native-skills/web/accessible-components/SKILL.md +45 -0
  103. package/native-skills/web/api-client-resilience/SKILL.md +39 -0
  104. package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
  105. package/native-skills/web/core-web-vitals/SKILL.md +40 -0
  106. package/native-skills/web/css-layout-systems/SKILL.md +41 -0
  107. package/native-skills/web/form-validation-ux/SKILL.md +39 -0
  108. package/native-skills/web/react-state-architecture/SKILL.md +41 -0
  109. package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
  110. package/package.json +68 -4
  111. package/src/agentLoop.js +2458 -0
  112. package/src/appChannel.js +109 -0
  113. package/src/approvalGrants.js +66 -0
  114. package/src/auditLog.js +52 -0
  115. package/src/autoUpdate.js +12 -0
  116. package/src/capabilities.js +36 -0
  117. package/src/checkpoints.js +1199 -0
  118. package/src/cliMode.js +638 -0
  119. package/src/commands.js +805 -0
  120. package/src/compiledSkills.js +540 -0
  121. package/src/config.js +130 -0
  122. package/src/contextManager.js +498 -0
  123. package/src/cortex/autoCondenser.js +192 -0
  124. package/src/cortex/provisioner.js +27 -0
  125. package/src/cortex/vectorDb.js +447 -0
  126. package/src/designPalettes.js +99 -0
  127. package/src/devMode.js +6 -0
  128. package/src/embodimentSession.js +180 -0
  129. package/src/entrypoint.js +194 -0
  130. package/src/execSafe.js +21 -0
  131. package/src/gitSnapshot.js +378 -0
  132. package/src/mcp/jsonRpc.js +165 -0
  133. package/src/mcp/mcpClient.js +136 -0
  134. package/src/mcp/mcpRegistry.js +207 -0
  135. package/src/memory.js +62 -0
  136. package/src/memoryManager.js +124 -0
  137. package/src/modelCatalogue.js +132 -0
  138. package/src/modelIds.js +42 -0
  139. package/src/modelRouter.js +145 -0
  140. package/src/outputStore.js +246 -0
  141. package/src/perception.js +124 -0
  142. package/src/planCache.js +437 -0
  143. package/src/procSignal.js +32 -0
  144. package/src/providers/anthropic.js +408 -0
  145. package/src/providers/base.js +328 -0
  146. package/src/providers/dashscope-byok.js +86 -0
  147. package/src/providers/deepseek.js +80 -0
  148. package/src/providers/factory.js +37 -0
  149. package/src/providers/gemini.js +346 -0
  150. package/src/providers/moonshot.js +73 -0
  151. package/src/providers/muse.js +169 -0
  152. package/src/providers/ollama.js +104 -0
  153. package/src/providers/openai-compatible.js +84 -0
  154. package/src/providers/openai.js +20 -0
  155. package/src/providers/qwen-byok.js +76 -0
  156. package/src/providers/xai.js +73 -0
  157. package/src/provisioning/capabilityConsent.js +24 -0
  158. package/src/provisioning/capabilityProvisioner.js +271 -0
  159. package/src/provisioning/capabilityTools.js +29 -0
  160. package/src/reporting.js +373 -0
  161. package/src/sandbox.js +529 -0
  162. package/src/sessionStore.js +279 -0
  163. package/src/skillsManager.js +288 -0
  164. package/src/slopDetector.js +307 -0
  165. package/src/taskBuffer.js +45 -0
  166. package/src/taskShape.js +241 -0
  167. package/src/textDiff.js +150 -0
  168. package/src/tokenTracker.js +560 -0
  169. package/src/tool-scripts/ax_snapshot.applescript +39 -0
  170. package/src/tool-scripts/ax_snapshot.ps1 +10 -0
  171. package/src/tool-scripts/desktop_act.applescript +58 -0
  172. package/src/tool-scripts/resolve_automator.py +119 -0
  173. package/src/toolPacks.js +524 -0
  174. package/src/tools/archive.js +63 -0
  175. package/src/tools/astryxOracle.js +78 -0
  176. package/src/tools/axSnapshot.js +165 -0
  177. package/src/tools/blenderAutomator.js +349 -0
  178. package/src/tools/browserAutomator.js +252 -0
  179. package/src/tools/checkSystemSpecs.js +64 -0
  180. package/src/tools/clipboard.js +36 -0
  181. package/src/tools/codeSearchAst.js +101 -0
  182. package/src/tools/codeSearcher.js +230 -0
  183. package/src/tools/codeTools.js +262 -0
  184. package/src/tools/codebaseSearch.js +75 -0
  185. package/src/tools/createSkill.js +129 -0
  186. package/src/tools/deferredMcpSearch.js +246 -0
  187. package/src/tools/desktopAct.js +208 -0
  188. package/src/tools/desktopOpen.js +34 -0
  189. package/src/tools/dynamicLoader.js +44 -0
  190. package/src/tools/executeProceduralMedia.js +93 -0
  191. package/src/tools/fsDelete.js +51 -0
  192. package/src/tools/fsEdit.js +105 -0
  193. package/src/tools/fsFind.js +128 -0
  194. package/src/tools/fsList.js +77 -0
  195. package/src/tools/fsMove.js +57 -0
  196. package/src/tools/fsRead.js +87 -0
  197. package/src/tools/fsSearch.js +198 -0
  198. package/src/tools/fsWrite.js +41 -0
  199. package/src/tools/gameAiBehaviorTree.js +208 -0
  200. package/src/tools/gameAnimationRig.js +194 -0
  201. package/src/tools/gameAssetFetcher.js +372 -0
  202. package/src/tools/gameAssetHarvester.js +210 -0
  203. package/src/tools/gameAudioSynth.js +267 -0
  204. package/src/tools/gameBuilder.js +886 -0
  205. package/src/tools/gameCanvasAudit.js +101 -0
  206. package/src/tools/gameCinematicSequencer.js +151 -0
  207. package/src/tools/gameDataForge.js +124 -0
  208. package/src/tools/gameDialogueTreeGen.js +166 -0
  209. package/src/tools/gameEconomyBalancer.js +161 -0
  210. package/src/tools/gameEntitySpawner.js +207 -0
  211. package/src/tools/gameLoreForge.js +153 -0
  212. package/src/tools/gameQuestArcBuilder.js +152 -0
  213. package/src/tools/gameSceneBuilder.js +158 -0
  214. package/src/tools/gameShaderForge.js +181 -0
  215. package/src/tools/gameTilemapGenerator.js +164 -0
  216. package/src/tools/gameUiHudBuilder.js +292 -0
  217. package/src/tools/gameVfxEmitterGen.js +108 -0
  218. package/src/tools/gameWebRuntimeInspector.js +91 -0
  219. package/src/tools/generateMediaViaApi.js +358 -0
  220. package/src/tools/gitExec.js +54 -0
  221. package/src/tools/godotAutomator.js +437 -0
  222. package/src/tools/manageLocalAiDependencies.js +220 -0
  223. package/src/tools/memoryWrite.js +99 -0
  224. package/src/tools/multiEdit.js +107 -0
  225. package/src/tools/palmierAutomator.js +104 -0
  226. package/src/tools/pdfReader.js +112 -0
  227. package/src/tools/persistentShell.js +148 -0
  228. package/src/tools/printer.js +316 -0
  229. package/src/tools/procTerm.js +91 -0
  230. package/src/tools/processList.js +52 -0
  231. package/src/tools/readNativeSkill.js +39 -0
  232. package/src/tools/registry.js +277 -0
  233. package/src/tools/resolveAutomator.js +102 -0
  234. package/src/tools/screenshot.js +75 -0
  235. package/src/tools/subagentOrchestrator.js +305 -0
  236. package/src/tools/sysRun.js +125 -0
  237. package/src/tools/todoWrite.js +75 -0
  238. package/src/tools/toolDispatch.js +127 -0
  239. package/src/tools/toolForge.js +124 -0
  240. package/src/tools/updatePlan.js +88 -0
  241. package/src/tools/viewImage.js +82 -0
  242. package/src/tools/visionInspector.js +154 -0
  243. package/src/tools/webFetch.js +74 -0
  244. package/src/tools/webSearch.js +82 -0
  245. package/src/tui/ask.js +164 -0
  246. package/src/tui/banner.js +158 -0
  247. package/src/tui/block.js +152 -0
  248. package/src/tui/chatRenderer.js +953 -0
  249. package/src/tui/completion.js +424 -0
  250. package/src/tui/dialogs.js +606 -0
  251. package/src/tui/diff.js +296 -0
  252. package/src/tui/history.js +135 -0
  253. package/src/tui/home.js +217 -0
  254. package/src/tui/invocation.js +105 -0
  255. package/src/tui/keys.js +443 -0
  256. package/src/tui/lineEditor.js +256 -0
  257. package/src/tui/overlay.js +256 -0
  258. package/src/tui/prompt.js +1258 -0
  259. package/src/tui/selectList.js +335 -0
  260. package/src/tui/stage.js +330 -0
  261. package/src/tui/stageSurface.js +609 -0
  262. package/src/tui/statusLine.js +254 -0
  263. package/src/tui/text.js +345 -0
  264. package/src/tui/theme.js +393 -0
  265. package/src/tui/toolDisplay.js +176 -0
  266. package/src/tui/transcript.js +235 -0
  267. package/src/tui/viewport.js +337 -0
  268. package/src/uncertainty.js +271 -0
  269. package/src/userSkills.js +265 -0
  270. package/src/utils/cli_ui.js +0 -0
  271. package/src/utils/inquirer.js +12 -0
  272. package/src/utils/paths.js +37 -0
  273. package/src/version.js +38 -0
  274. package/src/vfeTracker.js +220 -0
@@ -0,0 +1,2458 @@
1
+ import { createHash } from 'crypto';
2
+ import { isAlwaysAllowed } from './approvalGrants.js';
3
+ import { pruneSessions as pruneEmbodimentSessions, describeSession } from './embodimentSession.js';
4
+ import { WorkspacePerception } from './perception.js';
5
+ import { audioFormatForMimeType } from './providers/base.js';
6
+ import { TaskBuffer } from './taskBuffer.js';
7
+ import { TOOL_DEFINITIONS, getDynamicTools, resolveTool, canonicalToolName } from './tools/registry.js';
8
+ import { Sandbox } from './sandbox.js';
9
+ import { auditLog } from './auditLog.js';
10
+ import { loadConfig } from './config.js';
11
+ import { isDevEmulator } from './devMode.js';
12
+ import { TokenTracker } from './tokenTracker.js';
13
+
14
+ import { getMemoryContextString } from './memory.js';
15
+ import { MemoryManager } from './memoryManager.js';
16
+ import { cortexDb } from './cortex/vectorDb.js';
17
+ import { condenseAndSave } from './cortex/autoCondenser.js';
18
+ import { analyzeTask, buildTaskBrief, packsFromShape } from './taskShape.js';
19
+ import { lookupPlan, recordPlan, recordMiss, renderPlanHint, toStep } from './planCache.js';
20
+ import { routedConfig } from './modelRouter.js';
21
+ import { assessStep, isThrashing } from './uncertainty.js';
22
+ import { VFETracker } from './vfeTracker.js';
23
+ import {
24
+ routePacks,
25
+ packForTool,
26
+ isCoreTool,
27
+ isGameIntent,
28
+ isWebIntent,
29
+ getPackCatalogue,
30
+ PACK_PLAYBOOKS,
31
+ PACKS
32
+ } from './toolPacks.js';
33
+ import {
34
+ resolveCharPool,
35
+ measureMessages,
36
+ resultCapFor,
37
+ decayToolResults,
38
+ buildWindow,
39
+ compactInPlace,
40
+ describeDroppedMessages,
41
+ sanitizeSequences,
42
+ applyProgressiveCompaction,
43
+ snipOversizedToolOutputs
44
+ } from './contextManager.js';
45
+
46
+ // Provider imports
47
+ import { detectWorkspaceDomains, ensureSkillRepo, getSkillDomainHeader } from './skillsManager.js';
48
+ import { spillToolResult, buildSpillNotice, buildDecayPointer, pruneOutputStore } from './outputStore.js';
49
+ import { beginTask as beginCheckpointTask, captureBeforeMutation, pruneStore as pruneCheckpointStore, resolveProjectRoot } from './checkpoints.js';
50
+ import fsSync from 'fs';
51
+ import os from 'os';
52
+ import { analyzeFiles, formatFindings, isScannableFile } from './slopDetector.js';
53
+ import { suggestMovements, formatMovementBrief } from './designPalettes.js';
54
+ import { createProvider } from './providers/factory.js';
55
+
56
+ const SYSTEM_PROMPT = `You are Causyne Agent, a God-Tier Autonomous AI. You operate the user's computer through a local desktop harness. You are not just a coding assistant; you are a general-purpose superintelligence.
57
+
58
+ # Your Capabilities
59
+ You can do anything a human can do from a terminal: write code, build apps, manage files, install software, configure systems, run tests, deploy projects, analyze security vulnerabilities, and more. If a human can do it, you can do it.
60
+ Specifically, you excel at:
61
+ - **System Administration**: Organizing folders, cleaning the desktop, categorizing downloads, finding lost files.
62
+ - **Office & Creative Work**: Researching and writing massive 100-page theses, novels, or film scripts. Writing scripts to output Excel, Word, or PDF files from raw data.
63
+ - **Web & Media**: Searching the web, downloading files, and opening browsers to specific videos (YouTube, Netflix) using cross-platform terminal commands (e.g., AppleScript for macOS, PowerShell for Windows, or xdg-open for Linux).
64
+
65
+ # Core Mandates
66
+ 1. **Autonomy & Completion**: You are an agent. Keep going until the user's query is completely resolved before yielding back. Do not stop until the problem is solved. Autonomously resolve the query to the best of your ability using tools.
67
+ 2. **Ambition vs. Precision**: Be highly ambitious and creative on new tasks. Be surgically precise on existing codebases. Treat existing code with respect, never touching unrelated files or fixing unrelated bugs.
68
+ 3. **Conventions**: Rigorously adhere to existing project conventions. Mimic the style, structure, framework choices, and architectural patterns of existing code. NEVER assume a library/framework is available; verify it first.
69
+
70
+ # Operational Guidelines & Communication
71
+ - **Ruthless Conciseness**: Answer concisely with fewer than 4 lines unless detail is requested. No preambles ("Okay, I will now...") or postambles ("I have finished..."). One-word answers are best for simple questions.
72
+ - **Status Updates**: Write in a continuous conversational style. Give a brief progress note (1-2 sentences) BEFORE executing a tool. **CRITICAL: You MUST output this text and invoke the tool in the EXACT SAME RESPONSE.** If you output text without invoking a tool, the system will assume you are finished and terminate the task.
73
+ - **Confirm Ambiguity**: Do not take significant actions beyond the clear scope of the request without confirming. If asked *how* to do something, explain first, don't just do it.
74
+ - **No Chitchat**: Tools are for actions, text output is *only* for communication. Do not add explanatory comments within tool calls or code blocks.
75
+ - **File Paths**: ALWAYS output the absolute path of any file you create, modify, or generate so the user knows exactly where it is.
76
+
77
+ # Task Management & Planning (TodoWrite)
78
+ - ALWAYS use the TodoWrite tool to track and manage tasks before starting implementation. Create atomic, verb-led, clear outcome items.
79
+ - Check off completed TODOs immediately before reporting progress. Do not batch up multiple task completions.
80
+ - Reconcile and close the todo list when all goal tasks are done. If the user asks you to implement, do not output a separate text-based plan. Just build and display the todo list.
81
+
82
+ # Maximize Parallel Tool Calls (Speed is Key)
83
+ - CRITICAL INSTRUCTION: For maximum efficiency, whenever you perform multiple operations, invoke all relevant tools concurrently (in parallel) rather than sequentially.
84
+ - Searching for different patterns (imports, usage, definitions) MUST happen in parallel. Reading multiple files MUST happen in parallel.
85
+ - Sequential calls can ONLY be used when you genuinely REQUIRE the output of one tool to determine the usage of the next tool. Parallel tool execution is 3-5x faster and is the expected behavior.
86
+
87
+ # Context Understanding & Searching
88
+ - Semantic search (or grep) is your MAIN exploration tool. Start with a broad, high-level query.
89
+ - MANDATORY: Run multiple searches with different wording in parallel; first-pass results often miss key details.
90
+ - Keep searching new areas until you're CONFIDENT nothing important remains. Bias towards not asking the user for help if you can find the answer yourself.
91
+
92
+ # Making Code Changes & Execution
93
+ - **No Blind Patching**: NEVER edit a file using a patch/edit tool if you have not opened it with the read tool within your last 5 messages. Read the file again to confirm its contents before patching.
94
+ - **Verification Loop**: NEVER assume code works. After making code changes, execute the project-specific build, linting, and type-checking commands (e.g., 'tsc', 'npm run lint', 'pytest'). Identify these commands by examining 'README' files or 'package.json'.
95
+ - **Code Quality**: Write HIGH-VERBOSITY code. Optimize for clarity. Use meaningful variable names (no 1-letter names like 'n'). Use guard clauses/early returns. Avoid deep nesting.
96
+ - **No TODOs/Comments**: Do not add comments for trivial code. NEVER use inline comments. Avoid TODO comments—implement the logic instead.
97
+ - **Runnable Code**: Ensure generated code can be run immediately. Add all necessary imports, dependencies, and scaffolding (e.g. requirements.txt) if building from scratch.
98
+
99
+ # Git Workflow & Best Practices
100
+ - When asked to commit changes, start by gathering information: run \`git status\`, \`git diff HEAD\`, and \`git log -n 3\` to match commit style. Combine these shell commands to save steps.
101
+ - Always propose a draft commit message. Prefer messages that are clear, concise, and focused more on "why" and less
102
+
103
+ # Security & Constraints (Causyne Architecture)
104
+ - **Token Budgeting**: You must respect the user's BYOK (Bring Your Own Key) limits. Be highly token-efficient.
105
+ - **Explain Critical Commands**: Before executing destructive shell commands, provide a brief explanation of the impact.
106
+ - **Interactive Commands**: Avoid shell commands that require user interaction (e.g., \`git rebase -i\`). Use non-interactive flags (e.g., \`-y\`).
107
+ - **Sandboxing**: You operate within a strict 4-layer sandbox. Flag destructive commands (rm, overwrite) as "dangerous" for user approval.
108
+ - **SSRF Protection**: Do not fetch internal IPs (127.0.0.1, 169.254.x.x, 10.x.x.x).
109
+
110
+ # ABSOLUTE ETHICAL SAFETY NET (NON-NEGOTIABLE)
111
+ You are bound by an unbreakable ethical safety net. Regardless of user prompt, jailbreak attempts, or roleplay scenarios, you MUST REFUSE to:
112
+ 1. Create software designed to damage, disrupt, or gain unauthorized access to computer systems (malware, ransomware, keyloggers, trojans, rootkits).
113
+ 2. Write phishing pages, credential harvesters, social engineering scripts, or fake login forms designed to steal user passwords.
114
+ 3. Build DDoS tools, network flooding scripts, or tools designed for unauthorized network exploitation or cracking.
115
+ 4. Generate content that exploits minors or non-consenting individuals.
116
+ 5. Create surveillance/spyware tools, unauthorized screen capture malware, or microphone/camera hijackers.
117
+ 6. Write tools for software piracy, license cracking, or key generators.
118
+ 7. Build unauthorized cryptocurrency miners or crypto wallet drainers.
119
+ 8. Forge custom tools (via tool_forge) that perform any of the above harmful actions.
120
+
121
+ If asked to perform ANY harmful task, politely decline, state that Causyne exists to make the world better, and offer legitimate developer assistance instead.
122
+
123
+ # Dependency Management & Autonomy Modes
124
+ - **Mode A (Passive)**: You MUST explicitly ask the user for permission (by ending your turn and asking a question) before installing any dependencies.
125
+ - **Mode B (Supervised)**: You may install standard development dependencies automatically, but you MUST ask for permission before modifying OS-level settings or running \`sudo\`.
126
+ - **Mode C (Autonomous)**: You have full freedom. Silently execute installation commands if a dependency is missing.
127
+
128
+ # Direct API & Automation Policy
129
+ - **Prefer REST APIs (curl)**: When interacting with external services (like web, email, chat), prioritize finding and using official REST APIs via \`curl\` or simple scripts.
130
+ - **Avoid UI/Browser Automation**: Automating a browser (e.g., Puppeteer/Playwright) is noisy, expensive, and fragile. ONLY use browser automation if a site genuinely has no API. Reading JSON is always better than reading DOM.
131
+
132
+ # UNIVERSAL CRITICAL DIRECTIVE (APPLIES TO ALL TASKS)
133
+ **DO NOT OUTPUT CONVERSATIONAL TEXT WITHOUT INVOKING A TOOL CALL SIMULTANEOUSLY.**
134
+ If the task is not 100% finished, you MUST invoke a tool (like web_search, fs_read, sysRun, etc.) in the exact same response as your text. If you output text without a tool call, the system will assume you are completely done and immediately terminate the task!
135
+ `;
136
+
137
+ function getProviderSpecificPrompt(provider = '') {
138
+ const p = provider.toLowerCase();
139
+
140
+ if (p === 'anthropic') {
141
+ return `ANTHROPIC BRAIN ACTIVATION:
142
+ You are Claude — a model with exceptional analytical depth and nuanced reasoning. Leverage your strengths:
143
+ - Use <thinking> tags for complex multi-step reasoning before acting.
144
+ - You excel at understanding subtle context and edge cases. Apply this to architecture decisions.
145
+ - Your creativity is rooted in deep understanding, not randomness. When designing UIs, draw from your vast knowledge of design history, typography, and color theory.
146
+ - Self-critique: Before finalizing any creative output, internally evaluate "Does this look like generic AI output?" If yes, redesign.
147
+ - Ensure all tool calls strictly match the schema without omitting required fields. Avoid conversational filler completely.`;
148
+ }
149
+
150
+ if (p === 'openai') {
151
+ return `OPENAI BRAIN ACTIVATION:
152
+ You are GPT — a model with exceptional parallel reasoning and creative synthesis. Leverage your strengths:
153
+ - Execute maximum parallel tool calls. You can read 5 files, search 3 patterns, and write 2 files simultaneously.
154
+ - Your training includes vast exposure to award-winning web design. Channel this: think of specific real-world sites you've seen (Stripe, Linear, Vercel, Apple) and synthesize NEW aesthetics from them.
155
+ - When generating creative work, push beyond the first idea. Your strength is in synthesis — combine unexpected influences.`;
156
+ }
157
+
158
+ if (p === 'deepseek') {
159
+ return `DEEPSEEK BRAIN ACTIVATION:
160
+ You are DeepSeek — a model with outstanding code generation and mathematical reasoning. Leverage your strengths:
161
+ - Pay close attention to raw JSON formatting. Avoid escaping quotes unnecessarily.
162
+ - Your code output is exceptionally clean and well-structured. Use this for building pixel-perfect CSS layouts.
163
+ - For creative tasks: your training data includes massive open-source design repositories. Access this knowledge — don't default to generic patterns.
164
+ - Self-verify: After generating UI code, mentally render it. Does it look unique? If not, iterate.`;
165
+ }
166
+
167
+ if (p === 'qwen') {
168
+ return `QWEN BRAIN ACTIVATION:
169
+ You are Qwen — a model with exceptional multilingual capability and creative breadth. Leverage your strengths:
170
+ - Pay close attention to raw JSON formatting. Avoid escaping quotes unnecessarily.
171
+ - Your training includes diverse cultural design traditions (Eastern minimalism, European modernism, etc.). Draw from this diversity.
172
+ - For web tasks: vary your design approach. Don't repeat the same layout structure across projects.`;
173
+ }
174
+
175
+ if (p === 'muse' || p === 'meta') {
176
+ return `MUSE BRAIN ACTIVATION:
177
+ You are Muse Spark — a model built for long-horizon coding and multi-step agentic work, with a 1,048,576-token context window. Leverage your strengths:
178
+ - You are tuned for tool loops that run for many steps. Do not stop early to summarise progress; keep acting until the task is actually finished.
179
+ - Your context is enormous. Read the surrounding files before you edit — you can afford to understand the whole module, and guessing is a choice, not a constraint.
180
+ - You emit fewer tokens than earlier versions for the same work. Spend that budget on correctness, not on narration.
181
+ - You can see: images, video frames, and PDFs all reach you. After building any UI, screenshot it and look at what you actually produced.
182
+ - For creative work: your training spans design history far beyond the templates. A layout that looks like every other AI-generated page is a failure, not a default.`;
183
+ }
184
+
185
+ if (p === 'gemini') {
186
+ return `GEMINI BRAIN ACTIVATION:
187
+ You are Gemini — a multimodal model with strong visual-spatial reasoning. Leverage your strengths:
188
+ - You can reason about visual layouts spatially. Use this when designing page compositions.
189
+ - Your training includes vast visual data. When building UIs, think in terms of visual hierarchy, whitespace rhythm, and typographic scale.
190
+ - After building a web page, use the screenshot tool to visually verify your work. You can see — use this advantage.`;
191
+ }
192
+
193
+ if (p === 'ollama') {
194
+ return `LOCAL MODEL ACTIVATION:
195
+ You are running locally — respect the user's hardware constraints. Be token-efficient but don't sacrifice quality.
196
+ - Focus on clean, minimal implementations that work on first try.
197
+ - For creative work: simplicity can be beautiful. Japanese minimalism, Swiss design — these are aesthetically powerful AND computationally light.`;
198
+ }
199
+
200
+ if (p === 'xai' || p === 'grok') {
201
+ return `GROK BRAIN ACTIVATION:
202
+ You are Grok — a model highly optimized for speed and parallel tool execution.
203
+ - Prioritize direct, functional solutions over verbose explanations.
204
+ - Execute maximum parallel tool calls when searching or refactoring code.
205
+ - Your personality is unapologetic and highly technical. Focus purely on the most efficient code path.`;
206
+ }
207
+
208
+ if (p === 'moonshot' || p === 'kimi') {
209
+ return `KIMI BRAIN ACTIVATION:
210
+ You are Kimi — a model powered by KDA (Kimi Delta Attention) with a massive context window.
211
+ - Leverage your massive memory to reason holistically about the entire codebase structure before making surgical edits.
212
+ - You excel at end-to-end programming and agentic task decomposition.
213
+ - When reading large files, synthesize the global context. Do not lose track of the big picture.`;
214
+ }
215
+
216
+ return "";
217
+ }
218
+
219
+ /**
220
+ * The system message carrying an invoked skill — or null when none is active.
221
+ *
222
+ * A pure function, and deliberately exported: this is the single link that was
223
+ * missing. `/skills <id>` printed the file to the terminal and returned, so the
224
+ * body never entered a request and no API call was made at all. Building it
225
+ * here rather than inline in run() means it can be tested for what it actually
226
+ * produces, instead of by reading the source and hoping.
227
+ *
228
+ * Returned as an ARRAY so the caller spreads it, which keeps the systemMessages
229
+ * literal readable.
230
+ */
231
+ export function skillSystemMessage(skill) {
232
+ if (!skill || typeof skill.content !== 'string' || !skill.content.trim()) return null;
233
+ return [{
234
+ role: 'system',
235
+ content:
236
+ `THE USER HAS INVOKED A SKILL: ${skill.id}\n\n`
237
+ + 'Follow this procedure for the current task. It is a working method, not a document to '
238
+ + 'summarise back — carry out its steps, and treat each "Done when:" line as the condition '
239
+ + 'that must actually hold before moving on. Where it conflicts with your general approach, '
240
+ + 'the skill wins; where it conflicts with your safety rules or the workspace boundaries, '
241
+ + 'those win and you say so.\n\n'
242
+ + '--- BEGIN SKILL ---\n'
243
+ + skill.content
244
+ + '\n--- END SKILL ---',
245
+ // Stable for the whole task, so it anchors the cache rather than
246
+ // invalidating the cached prefix on every step.
247
+ cacheAnchor: true
248
+ }];
249
+ }
250
+
251
+
252
+ export function explainNetworkError(error, config = {}) {
253
+ const cause = error && error.cause;
254
+ const code = (cause && (cause.code || cause.errno)) || '';
255
+
256
+ let host = '';
257
+ try {
258
+ const raw = config.baseUrl && String(config.baseUrl).trim();
259
+ if (raw) host = new URL(raw).host;
260
+ } catch (e) { /* a malformed base URL is itself worth reporting, below */ }
261
+
262
+ const REASONS = {
263
+ ENOTFOUND: 'the host could not be resolved (DNS)',
264
+ EAI_AGAIN: 'DNS lookup failed — the network may be down or captive',
265
+ ECONNREFUSED: 'nothing accepted the connection on that host and port',
266
+ ECONNRESET: 'the connection was reset before a reply arrived',
267
+ ETIMEDOUT: 'the connection timed out',
268
+ UND_ERR_CONNECT_TIMEOUT: 'the connection timed out',
269
+ UND_ERR_HEADERS_TIMEOUT: 'the server accepted the connection but never replied',
270
+ EHOSTUNREACH: 'that host is unreachable from this network',
271
+ ENETUNREACH: 'this machine has no route to the network',
272
+ EPIPE: 'the connection closed mid-request',
273
+ CERT_HAS_EXPIRED: 'the TLS certificate has expired',
274
+ DEPTH_ZERO_SELF_SIGNED_CERT: 'the TLS certificate is self-signed — a proxy may be intercepting',
275
+ UNABLE_TO_VERIFY_LEAF_SIGNATURE: 'the TLS certificate could not be verified — a proxy may be intercepting',
276
+ SELF_SIGNED_CERT_IN_CHAIN: 'a self-signed certificate is in the chain — a proxy may be intercepting'
277
+ };
278
+
279
+ const reason = REASONS[code] || (code ? `the connection failed (${code})` : 'the connection failed');
280
+ const where = host ? ` reaching ${host}` : '';
281
+ const provider = config.provider ? ` via ${config.provider}` : '';
282
+
283
+ const checks = [];
284
+ if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'ENETUNREACH') {
285
+ checks.push('confirm this machine is online');
286
+ if (host) checks.push(`confirm ${host} is the endpoint you meant — /config changes it`);
287
+ } else if (code === 'ECONNREFUSED') {
288
+ checks.push(host
289
+ ? `confirm something is listening on ${host}`
290
+ : 'confirm your local model server is running');
291
+ } else if (String(code).startsWith('CERT') || String(code).includes('SELF_SIGNED')) {
292
+ checks.push('a corporate proxy is likely intercepting TLS — the CA needs to be trusted');
293
+ } else {
294
+ checks.push('retry in a moment; if it persists, check the network and the configured endpoint');
295
+ }
296
+
297
+ return `Could not reach the model${provider}${where}: ${reason}.\n`
298
+ + checks.map((check) => ` - ${check}`).join('\n');
299
+ }
300
+
301
+ // classifyError() returns this for every connection-level failure. Assembled
302
+ // the same way the classifier does, so the two can never drift apart.
303
+ const NETWORK_ERROR_TYPE = "network_error";
304
+
305
+ export class AgentLoop {
306
+ /**
307
+ * How many times in a row the same class of failure may repeat before the
308
+ * loop gives up.
309
+ *
310
+ * Three is enough to ride out a blip and few enough that a genuinely broken
311
+ * endpoint is reported in seconds rather than after the whole step budget.
312
+ */
313
+ static MAX_CONSECUTIVE_FAILURES = 3;
314
+ constructor(eventSink, taskId, userPrompt) {
315
+ this.eventSink = eventSink;
316
+ this.taskId = taskId;
317
+ this.userPrompt = userPrompt;
318
+
319
+ this.config = loadConfig();
320
+ this.sandbox = new Sandbox();
321
+
322
+ // Retire spilled tool outputs past their retention window. Fire-and-forget
323
+ // at task start rather than on a timer: nothing may run in the background
324
+ // (rule ZM-01), and a task boundary is often enough for a 7-day window.
325
+ try { pruneOutputStore(); } catch (e) { /* never block a task on housekeeping */ }
326
+ try { pruneCheckpointStore(); } catch (e) { /* never block a task on housekeeping */ }
327
+ try { pruneEmbodimentSessions(); } catch (e) { /* never block a task on housekeeping */ }
328
+ // Set by the caller when the user invokes a skill by name. Its body becomes
329
+ // a system message for the task; see systemMessages [4].
330
+ this.activeSkill = null;
331
+ // Consecutive same-class failures, for the retry ceiling below.
332
+ this.consecutiveFailures = 0;
333
+ this.lastFailureType = null;
334
+ this.perception = new WorkspacePerception();
335
+ this.taskBuffer = new TaskBuffer();
336
+ this.tokenTracker = new TokenTracker();
337
+
338
+ this.llm = this.initializeProvider();
339
+ this.memoryManager = new MemoryManager();
340
+ this.messages = this.memoryManager.loadHistory(); // Persistent context window
341
+ this.stepsCompleted = 0;
342
+ this.consecutiveCommands = 0;
343
+ this.circuitBreakerUnlocked = false;
344
+ this.stopRequested = false;
345
+ this.pendingApprovals = new Map(); // Maps toolCallId -> resolve function
346
+
347
+ // Limits based on tier/budget & emulator fast bypass
348
+ this.updateTierLimits();
349
+
350
+ this.consecutiveThinkingSteps = 0;
351
+ this.consecutiveBlockedActions = 0; // V8 Transcendence: Circuit Breaker
352
+
353
+ // Sovereign Progressive Disclosure: which capability packs have entered the
354
+ // context window for this task. Nothing is disabled — see src/toolPacks.js.
355
+ this.unlockedPacks = new Set();
356
+ this.unlockedTools = new Set();
357
+ // sha256 of every large tool result seen this task -> the step that produced
358
+ // it. Lets a verbatim repeat collapse to a pointer instead of paying twice.
359
+ this._observationDigests = new Map();
360
+ // Unlock sequence drives append-only tool ordering, which is what keeps the
361
+ // prompt-cache prefix intact when a pack loads mid-task.
362
+ this.unlockOrder = [];
363
+ this.workspaceContext = null;
364
+ this.taskShape = null;
365
+ // Budget for the billed Tier 2 Cortex condensation pass.
366
+ this._cortexPassesUsed = 0;
367
+ // Per-task memo for context that is constant for the whole task but used to
368
+ // be recomputed on every single step (cortex retrieval, skills index,
369
+ // design detection, workspace domain scan).
370
+ this.staticContext = null;
371
+ this.vfeTracker = new VFETracker();
372
+ this._lessonSimilarity = 0;
373
+ this._paradigmShiftPending = false;
374
+
375
+ // Completion guards fire at most once per task. Previously they were gated
376
+ // on consecutiveThinkingSteps, which resets on every tool call, so the
377
+ // agent could be blocked from finishing over and over until it burned the
378
+ // entire 300-step budget on a task that was already done.
379
+ this.gameGuardFired = false;
380
+ this.webGuardFired = false;
381
+ // P6: rendered surfaces this task wrote, for the anti-slop gate to read back.
382
+ this.webArtifacts = new Set();
383
+ this.slopGateFired = false;
384
+
385
+ this.gameCascadeState = { active: true, currentStage: 1, stageHistory: [], verificationPassed: false };
386
+ this.gameToolStats = { godot_act: 0, game_asset_fetch: 0, game_build_html5: 0, last_godot_action: null };
387
+ this.webVerificationState = { screenshotTaken: false, browserActUsed: false }; // V8 Transcendence: Anti-Slop Guard
388
+
389
+ this.abortController = new AbortController();
390
+
391
+ // V5: Context compaction tracking
392
+ this._lastCompactionIndex = null;
393
+ // V5: Error recovery tracking (per tool call)
394
+ // V5: Error recovery tracking (per tool call)
395
+ this._toolRetryCount = new Map();
396
+ this.isInitialized = false;
397
+ this.taskTemperature = 0.1; // Default
398
+ // Set only on a delegated subagent loop; null means "the full catalogue".
399
+ this.toolScope = null;
400
+ }
401
+
402
+ // V6: Adaptive Temperature based on task intent
403
+ determineTemperature(prompt) {
404
+ if (!prompt) return 0.1;
405
+ const lower = prompt.toLowerCase();
406
+
407
+ // Creative / Open-ended tasks get higher temp
408
+ if (lower.includes("brainstorm") || lower.includes("write a story") || lower.includes("creative") || lower.includes("poem") || lower.includes("novel") || lower.includes("screenplay")) {
409
+ return 0.5;
410
+ }
411
+ // Web Design / UI tasks get creative-code temp (Anti-Slop: warm enough for diversity, cool enough for valid code)
412
+ if (lower.includes("website") || lower.includes("landing page") || lower.includes("web app") || lower.includes("dashboard") || lower.includes("portfolio") || lower.includes("ui design") || lower.includes("homepage") || lower.includes("frontend")) {
413
+ return 0.4;
414
+ }
415
+ // Conversational / Explanatory tasks get medium temp
416
+ if (lower.includes("explain") || lower.includes("how does") || lower.includes("what is") || lower.includes("teach me")) {
417
+ return 0.3;
418
+ }
419
+ // Code / Technical / Precision tasks get low temp
420
+ return 0.1;
421
+ }
422
+
423
+ /**
424
+ * Load a capability pack into the context window, recording the sequence.
425
+ *
426
+ * Order matters for cost, not just bookkeeping: tool definitions sit at the
427
+ * front of the cached prefix, so appending is nearly free while reordering
428
+ * invalidates the cache for the tools, the system prompt and every message.
429
+ */
430
+ unlockPack(packName) {
431
+ if (!packName || this.unlockedPacks.has(packName)) return false;
432
+ this.unlockedPacks.add(packName);
433
+ this.unlockOrder.push(packName);
434
+ return true;
435
+ }
436
+
437
+ /**
438
+ * Build the parts of the system context that are constant for a whole task.
439
+ * Called once per task rather than once per step.
440
+ */
441
+ async buildStaticContext() {
442
+ let lessonsLearned = '';
443
+ this._lessonSimilarity = 0;
444
+ if (this.currentTaskPrompt) {
445
+ try {
446
+ const matches = await cortexDb.search(this.currentTaskPrompt, 2);
447
+ if (matches.length > 0) {
448
+ lessonsLearned = `\n\n🧠 DIGITAL BRAIN (LESSONS LEARNED FROM PAST MISTAKES):\n` + matches.map(m => `- ${m.text}`).join('\n');
449
+ this._lessonSimilarity = matches[0].similarity || 0;
450
+ this.debugLog(`cortex search hit: ${(this._lessonSimilarity * 100).toFixed(0)}% similarity`);
451
+ }
452
+ } catch (e) {
453
+ this.debugLog(`cortex retrieval failed: ${e.message}`);
454
+ }
455
+ }
456
+
457
+ // Anti-Slop: Design Context Awareness (what design system is already installed)
458
+ let designContext = '';
459
+ try {
460
+ const detectedDesign = await this.perception.detectDesignContext(process.cwd());
461
+ if (detectedDesign && detectedDesign.length > 0) {
462
+ designContext = `\nDETECTED DESIGN SYSTEM IN WORKSPACE: ${detectedDesign.join(', ')} — USE THIS. Do NOT install a conflicting system.`;
463
+ }
464
+ } catch (e) {}
465
+
466
+ // Anti-Slop: Creative Pattern Memory (prevents repeating the same design approach)
467
+ let antiRepeatDirective = '';
468
+ if (this.currentTaskPrompt && isWebIntent(this.currentTaskPrompt)) {
469
+ try {
470
+ const recentPatterns = await cortexDb.getRecentCreativePatterns(5);
471
+ if (recentPatterns.length > 0) {
472
+ antiRepeatDirective = `\n\n🎨 CREATIVE DIVERSITY (DO NOT REPEAT RECENT PATTERNS):\n` +
473
+ recentPatterns.map(p => `- ${p}`).join('\n') +
474
+ `\nChoose a DIFFERENT design approach, color palette, and font combination than the above.`;
475
+ }
476
+ } catch (e) {}
477
+ }
478
+
479
+ // Universal skills awareness
480
+ let universalSkillsContext = '';
481
+ try {
482
+ const detectedDomains = detectWorkspaceDomains(process.cwd());
483
+ if (detectedDomains.length > 0) {
484
+ this.debugLog(`detected workspace domains: ${detectedDomains.join(', ')}`);
485
+ for (const domain of detectedDomains) {
486
+ ensureSkillRepo(domain);
487
+ }
488
+ universalSkillsContext = getSkillDomainHeader(detectedDomains);
489
+ }
490
+ } catch (e) {
491
+ this.debugLog(`skill domain detection failed: ${e.message}`);
492
+ }
493
+
494
+ // Plan cache lookup. Deterministic and local: no model call, no network.
495
+ // Goes in the per-task STATIC block, which is byte-identical every step and
496
+ // already carries a cache anchor, so a hit costs its own tokens exactly
497
+ // once per task rather than once per step.
498
+ let planHint = '';
499
+ try {
500
+ const match = lookupPlan({
501
+ prompt: this.currentTaskPrompt || this.userPrompt,
502
+ archetype: this.taskShape ? this.taskShape.archetype : ''
503
+ });
504
+ if (match) {
505
+ planHint = renderPlanHint(match);
506
+ this._offeredPlanId = match.template.id;
507
+ this._planSimilarity = match.similarity;
508
+ this.debugLog(`plan cache hit ${(match.similarity * 100).toFixed(0)}% (${match.template.steps.length} steps)`);
509
+ }
510
+ } catch (e) {
511
+ this.debugLog(`plan cache lookup skipped: ${e.message}`);
512
+ }
513
+
514
+ return { lessonsLearned, designContext, antiRepeatDirective, universalSkillsContext, planHint };
515
+ }
516
+
517
+
518
+ rearmAbortController() {
519
+ this.abortController = new AbortController();
520
+ }
521
+
522
+ abort() {
523
+ this.stopRequested = true;
524
+ if (this.abortController) {
525
+ this.abortController.abort();
526
+ }
527
+ }
528
+
529
+ /**
530
+ * A provider for UTILITY work — condensation, classification, filtering.
531
+ *
532
+ * Same provider, cheaper model. Condensing a dropped transcript is mechanical
533
+ * text work whose output the machine consumes, and paying a flagship rate for
534
+ * it is the clearest waste in the loop: on Anthropic it is $5/$25 against
535
+ * Haiku's $1/$5. Built lazily and cached, because most tasks never compact at
536
+ * all and constructing a second provider they will not use is its own cost.
537
+ *
538
+ * Falls back to the reasoning provider whenever the router declines — an
539
+ * unknown provider, a config override, or a route that would change the
540
+ * training-data posture. Utility work still has to happen.
541
+ *
542
+ * @returns {any}
543
+ */
544
+ utilityProvider() {
545
+ if (this._utilityLlm) return this._utilityLlm;
546
+ try {
547
+ const { config, route } = routedConfig(this.config, 'condense');
548
+ if (!route.routed) {
549
+ this._utilityLlm = this.llm;
550
+ this.debugLog(`utility route: ${route.reason}`);
551
+ } else {
552
+ this._utilityLlm = createProvider(config);
553
+ this.debugLog(`utility route: ${this.config.model} -> ${route.model}`);
554
+ }
555
+ } catch (e) {
556
+ this._utilityLlm = this.llm; // a routing failure must never stop a task
557
+ this.debugLog(`utility route failed, staying on the reasoning model: ${e.message}`);
558
+ }
559
+ return this._utilityLlm;
560
+ }
561
+
562
+ initializeProvider() {
563
+ return createProvider(this.config);
564
+ }
565
+
566
+ /**
567
+ * Tools that only observe. Nothing here writes to disk, mutates process
568
+ * state, spends money, or can require approval — which is what makes running
569
+ * several of them at once safe and order-independent.
570
+ *
571
+ * Deliberately an allowlist. A denylist would silently start prefetching
572
+ * every tool added in the future, including destructive ones.
573
+ */
574
+ static READ_ONLY_TOOLS = new Set([
575
+ 'fs_read', 'fs_list', 'fs_find', 'fs_search',
576
+ 'code_search_ast', 'code_list_symbols', 'code_search_ripgrep', 'codebase_search',
577
+ 'web_fetch', 'web_search', 'pdf_read', 'fs_read_skill',
578
+ 'check_system_specs', 'process_list', 'view_image'
579
+ ]);
580
+
581
+ /** How many reads may be in flight at once. */
582
+ static PREFETCH_CONCURRENCY = 6;
583
+
584
+ /**
585
+ * Start the read-only calls of one assistant turn concurrently.
586
+ *
587
+ * @returns {Map<string, Promise<object|null>>} keyed by tool_call_id. A entry
588
+ * resolves to the tool result, or to null when the call was not eligible,
589
+ * was denied by the sandbox, or threw — in every one of those cases the
590
+ * serial loop executes it normally and keeps its own error handling.
591
+ */
592
+ prefetchReadOnlyCalls(toolCalls) {
593
+ const prefetched = new Map();
594
+ if (!Array.isArray(toolCalls) || toolCalls.length < 2) return prefetched;
595
+
596
+ // Mode A promises approval before every tool, including reads. Starting a
597
+ // read here would execute it before the serial approval and hook gates.
598
+ if ((this.config.autonomyMode || 'C') === 'A') return prefetched;
599
+
600
+ const eligible = [];
601
+ for (const toolCall of toolCalls) {
602
+ const functionName = canonicalToolName(toolCall.function.name);
603
+ if (!AgentLoop.READ_ONLY_TOOLS.has(functionName)) continue;
604
+
605
+ let args;
606
+ try {
607
+ args = typeof toolCall.function.arguments === 'string'
608
+ ? JSON.parse(toolCall.function.arguments || '{}')
609
+ : (toolCall.function.arguments || {});
610
+ } catch (e) {
611
+ continue; // malformed arguments are the serial loop's business
612
+ }
613
+
614
+ // The same guard the serial path applies, applied before anything runs.
615
+ let validation;
616
+ try {
617
+ validation = this.sandbox.validate(functionName, args);
618
+ } catch (e) {
619
+ continue;
620
+ }
621
+ if (!validation || validation.allowed === false || validation.risk !== 'safe') continue;
622
+
623
+ const tool = resolveTool(functionName);
624
+ if (!tool || typeof tool.execute !== 'function') continue;
625
+
626
+ eligible.push({ toolCall, functionName, args, tool });
627
+ }
628
+
629
+ if (eligible.length < 2) return prefetched;
630
+
631
+ // A bounded worker pool rather than a bare Promise.all: a model can emit a
632
+ // large batch, and opening fifty file handles or fifty sockets at once
633
+ // would trade one problem for another.
634
+ let cursor = 0;
635
+ const resolvers = new Map();
636
+ for (const item of eligible) {
637
+ prefetched.set(item.toolCall.id, new Promise(resolve => resolvers.set(item.toolCall.id, resolve)));
638
+ }
639
+
640
+ const worker = async () => {
641
+ while (cursor < eligible.length) {
642
+ const item = eligible[cursor++];
643
+ const resolve = resolvers.get(item.toolCall.id);
644
+ if (this.stopRequested || this.abortController.signal.aborted) { resolve(null); continue; }
645
+ try {
646
+ resolve(await item.tool.execute(item.args, {
647
+ signal: this.abortController.signal,
648
+ parentLoop: this,
649
+ ...this.sandbox
650
+ }));
651
+ } catch (e) {
652
+ resolve(null); // let the serial path fail it, with its own retries
653
+ }
654
+ }
655
+ };
656
+
657
+ const lanes = Math.min(AgentLoop.PREFETCH_CONCURRENCY, eligible.length);
658
+ for (let i = 0; i < lanes; i++) worker();
659
+
660
+ return prefetched;
661
+ }
662
+
663
+ emitEvent(messageObj) {
664
+ if (this.eventSink && this.eventSink.readyState === 1) {
665
+ this.eventSink.send(JSON.stringify({ taskId: this.taskId, ...messageObj }));
666
+ }
667
+ }
668
+
669
+
670
+ // WHAT BELONGS HERE: things the user would want to know even though they did
671
+ // not ask — a file was written on their behalf, a limit was reached. Not
672
+ // internal telemetry. Converting the old console.log calls to notices fixed
673
+ // the display corruption but kept the noise, and three lines of task-shape and
674
+ // tier sizing after every invocation is exactly the clutter neither reference
675
+ // shows. Diagnostics go to debugLog().
676
+ notice(message, tone = 'muted') {
677
+ this.emitEvent({ type: 'notice', tone, message: String(message) });
678
+ }
679
+
680
+ /**
681
+ * Diagnostics, off by default.
682
+ *
683
+ * These are for whoever is debugging Causyne, not for the person using it.
684
+ * Printed unconditionally they are both noise and a second uncontrolled writer
685
+ * to a terminal the prompt is repainting. Behind CAUSYNE_DEBUG they cost
686
+ * nothing in a normal session, and stderr keeps them out of a piped stdout.
687
+ */
688
+ debugLog(message) {
689
+ if (process.env.CAUSYNE_DEBUG !== '1') return;
690
+ process.stderr.write(`[causyne] ${message}\n`);
691
+ }
692
+
693
+
694
+ handleApprovalResponse(toolCallId, approved) {
695
+ if (this.pendingApprovals.has(toolCallId)) {
696
+ const resolve = this.pendingApprovals.get(toolCallId);
697
+ resolve(approved);
698
+ this.pendingApprovals.delete(toolCallId);
699
+ }
700
+ }
701
+
702
+ updateTierLimits() {
703
+ this.config = loadConfig();
704
+ if (typeof this.initializeProvider === 'function') {
705
+ this.llm = this.initializeProvider();
706
+ this._utilityLlm = null; // the route is derived from the model; re-derive it
707
+ }
708
+ if (this.tokenTracker) {
709
+ this.tokenTracker.config = this.config;
710
+ this.tokenTracker.budget = this.config.tokenBudget || { enabled: false };
711
+ }
712
+
713
+ // verified entitlement). Developer emulator mode is a hardened, secret-gated
714
+ // override for local development on a Fast-tier emulator — see devMode.js.
715
+ const devMode = isDevEmulator(this.config);
716
+ this.activeTier = devMode ? 'super_max' : (this.config.tier || 'premium');
717
+ if (this.config.maxStepsPerTask) {
718
+ this.maxSteps = this.config.maxStepsPerTask;
719
+ } else if (this.activeTier === 'super_max') {
720
+ this.maxSteps = 600;
721
+ } else if (this.activeTier === 'max') {
722
+ this.maxSteps = 400;
723
+ } else {
724
+ this.maxSteps = 300;
725
+ }
726
+ this.maxCircuitBreaker = this.maxSteps;
727
+ }
728
+
729
+ async run() {
730
+ this.stopRequested = false;
731
+ // A task that was stopped must not poison the next one — see
732
+ // rearmAbortController(). This has to happen before any tool can run.
733
+ this.rearmAbortController();
734
+ // Refresh tier limits on every task run
735
+ this.updateTierLimits();
736
+
737
+ // 1. PERCEIVE — runs once per task so the agent starts with real workspace
738
+ // awareness instead of burning steps rediscovering it. The result used to be
739
+ // assigned to a local variable and thrown away.
740
+ auditLog.log({ event: 'task_start', taskId: this.taskId, request: this.userPrompt });
741
+ this.emitEvent({ type: 'status', message: 'Analyzing workspace...' });
742
+
743
+ // P2: open the undo group for this task. This snapshots the CONVERSATION as
744
+ // well as the workspace tree, because restoring files while chat_history
745
+ // still claims the edits happened leaves the agent believing in a disk state
746
+ // that no longer exists. Fail-open — never blocks the task.
747
+ await beginCheckpointTask({
748
+ taskId: this.taskId,
749
+ prompt: this.userPrompt,
750
+ // The working directory FIRST, always.
751
+ //
752
+ // This passed sandbox.workspacePaths, which is the broad default SCOPE —
753
+ // the places the agent is permitted to touch, not the place it is working.
754
+ // beginTask() snapshots the first three, so on a default install it
755
+ // captured /Users/Shared and /home (5.6 MB of files nobody was editing)
756
+ // and never captured the project the user was actually in. Permission and
757
+ // location are different questions; undo needs the location.
758
+ // beginTask() de-duplicates and takes the first three.
759
+ // The PROJECT, not the process's working directory.
760
+ //
761
+
762
+ // started inside a project. Started from the home directory — which is
763
+ // simply what happens when someone opens a terminal and types the command
764
+ // — cwd is $HOME, the snapshot blew past MAX_TREE_FILES and was recorded
765
+ // as `too_many_files`, and the permission-scope fallbacks then captured
766
+ // /Users/Shared and /home: 5.6 MB of files nobody has ever edited. Every
767
+ // task produced a checkpoint containing nothing worth restoring, which is
768
+ // why undo appeared to do nothing no matter where it was invoked.
769
+ //
770
+ // resolveProjectRoot walks up for a real project marker and refuses to
771
+ // return $HOME or a system root, so the checkpoint covers the code being
772
+ // worked on or honestly covers nothing at all.
773
+ workspaceRoots: [
774
+ resolveProjectRoot(process.cwd()),
775
+ process.cwd(),
776
+ ...(this.sandbox.grantAllAccess ? [] : (this.sandbox.workspacePaths || []))
777
+ ].filter(Boolean)
778
+ });
779
+
780
+ try {
781
+ this.workspaceContext = await this.perception.buildContext(process.cwd());
782
+ } catch (perceptionError) {
783
+ this.debugLog(`workspace scan failed: ${perceptionError.message}`);
784
+ this.workspaceContext = null;
785
+ }
786
+
787
+ // 2. ANALYSE TASK SHAPE & PRE-WARM CAPABILITY PACKS.
788
+ // A game request gets the full game toolset from step one with no discovery
789
+ // round trip, while a "rename my files" request never pays the 7,282 tokens
790
+ // those 21 schemas cost. The shape analysis additionally tells the model the
791
+ // ORDER to work in, which saves whole exploratory steps.
792
+ this.gameGuardFired = false;
793
+ this.webGuardFired = false;
794
+ this.webArtifacts = new Set();
795
+ this.slopGateFired = false;
796
+ this.staticContext = null;
797
+ // Plan cache: the procedure THIS task follows, and the template (if any)
798
+ // that was offered to it. Both reset per task — a plan is a property of one
799
+ // request, not of the session.
800
+ this._planSteps = [];
801
+ this._offeredPlanId = null;
802
+ this._planSimilarity = 0;
803
+ this._lessonSimilarity = 0;
804
+ this._lastToolResult = null;
805
+ this._paradigmShiftPending = false;
806
+ if (this.vfeTracker) this.vfeTracker.reset();
807
+
808
+ if (this.userPrompt) {
809
+ this.taskShape = analyzeTask(this.userPrompt, this.workspaceContext);
810
+
811
+ // Scored routing, with the regex router as a safety net so a pack can
812
+ // never be missed just because it scored below threshold.
813
+ const scored = packsFromShape(this.taskShape);
814
+ const fallback = routePacks(this.userPrompt, this.workspaceContext);
815
+ for (const pack of [...scored, ...fallback]) {
816
+ this.unlockPack(pack);
817
+ }
818
+
819
+ this.debugLog(`task shape: ${this.taskShape.archetype} (${this.taskShape.confidence} confidence)`);
820
+ }
821
+
822
+ if (!this.isInitialized) {
823
+ const activeTier = this.activeTier;
824
+ const charPoolLimit = resolveCharPool(activeTier, this.config.maxCharPool);
825
+ const devTag = isDevEmulator(this.config) ? ' (dev emulator)' : '';
826
+ // Was a console.log carrying a hardcoded \x1b[1m\x1b[36m, which ignored
827
+ // NO_COLOR outright. The renderer paints this through the theme.
828
+ // Internal sizing, and already implied by the header's `tier` field.
829
+ this.debugLog(
830
+ `tier ${activeTier}${devTag} | ${this.maxSteps} steps | ${charPoolLimit} char context`
831
+ );
832
+ this.isInitialized = true;
833
+ }
834
+
835
+ this.taskTemperature = this.determineTemperature(this.userPrompt);
836
+ if (this.unlockedPacks.size > 0) {
837
+ this.debugLog(`capability packs pre-warmed: ${[...this.unlockedPacks].join(', ')}`);
838
+ }
839
+
840
+ if (this.userPrompt) {
841
+ this.currentTaskPrompt = this.userPrompt; // Save for notification later
842
+
843
+ // V5: Multimodal Attachments Support
844
+ if (this.attachments && this.attachments.length > 0) {
845
+ let contentArray = [{ type: 'text', text: this.userPrompt }];
846
+
847
+ for (const att of this.attachments) {
848
+ if (att.type === 'image') {
849
+ contentArray.push({
850
+ type: 'image_url',
851
+ image_url: { url: `data:${att.mimeType};base64,${att.base64}` }
852
+ });
853
+ } else if (att.type === 'audio') {
854
+
855
+ // be hardcoded to 'mp3' while the recorder produced AAC/m4a, so every
856
+ // clip was mislabelled and the provider failed on decode. att.mimeType
857
+ // has always been on the wire; it is now actually read.
858
+ const audioFormat = audioFormatForMimeType(att.mimeType);
859
+
860
+
861
+ // cap during reassembly; this is the final, vendor-specific ceiling,
862
+ // reported as a Causyne message rather than an opaque upstream 400.
863
+ const audioBytes = Math.round((att.base64?.length || 0) * 0.75);
864
+ const byteCeiling = this.llm?.maxAudioBytes;
865
+ if (byteCeiling && audioBytes > byteCeiling) {
866
+ contentArray[0].text +=
867
+ `\n\n[SYSTEM NOTIFICATION: The user's voice clip (${Math.round(audioBytes / 1024)} KB) exceeds the ${Math.round(byteCeiling / 1024)} KB limit of the selected model and was not attached. Ask the user to record a shorter clip.]`;
868
+ this.notice(`voice clip dropped: ${audioBytes} B over the ${byteCeiling} B provider limit`);
869
+ } else if (!this.llm?.supportsAudio) {
870
+
871
+ // model that cannot hear — a stale capability advertisement. Say so
872
+ // in-band; prepareMessages would otherwise strip it further down
873
+ // and the user would never learn why nothing was heard.
874
+ // Before giving up: the provider may ship a dedicated
875
+ // speech-to-text model on this same key. Meta does, and it is the
876
+ // whole answer to Muse Spark 1.3 being both the recommended model
877
+ // and the one that cannot hear.
878
+ let transcript = null;
879
+ let transcribeError = null;
880
+ if (typeof this.llm?.transcribeAudio === 'function') {
881
+ try {
882
+ transcript = await this.llm.transcribeAudio(att.base64, att.mimeType);
883
+ } catch (e) {
884
+ transcribeError = e.message;
885
+ }
886
+ }
887
+
888
+ if (transcript) {
889
+ contentArray[0].text +=
890
+ `\n\n[VOICE TRANSCRIPT: The user spoke rather than typed. The selected model cannot take audio directly, so the clip was transcribed by this provider's speech-to-text model. Treat the following as the user's own words: "${transcript}"]`;
891
+ this.notice(`voice clip transcribed (${transcript.length} chars) — the chat model cannot hear`, 'accent');
892
+ } else {
893
+ // Name the way out when the provider knows one. A refusal the
894
+ // user cannot act on is worse than the limitation it reports.
895
+ const alternative = typeof this.llm?.audioAlternative === 'function'
896
+ ? this.llm.audioAlternative()
897
+ : null;
898
+ const route = alternative
899
+ ? `Ask them to switch to ${alternative}, which accepts audio on this same provider and key, or to use the on-device transcription instead.`
900
+ : 'Ask them to switch to an audio-capable model, or to use the on-device transcription instead.';
901
+ contentArray[0].text +=
902
+ `\n\n[SYSTEM NOTIFICATION: The user sent a voice recording but the selected model cannot process audio. ${route}]`;
903
+ this.notice(transcribeError
904
+ ? `voice clip dropped: transcription failed — ${transcribeError}`
905
+ : alternative
906
+ ? `voice clip dropped: ${this.config.model} cannot hear — ${alternative} can`
907
+ : 'voice clip dropped: selected model does not support audio input',
908
+ transcribeError ? 'warn' : 'muted');
909
+ }
910
+ } else {
911
+ contentArray.push({
912
+ type: 'input_audio',
913
+ input_audio: {
914
+ data: att.base64,
915
+ format: audioFormat
916
+ }
917
+ });
918
+ }
919
+ } else if (att.type === 'document') {
920
+ const fs = await import('fs');
921
+ const path = await import('path');
922
+ // Ensure unique name to prevent overwriting
923
+ const safeName = att.name.replace(/[^a-zA-Z0-9.\-_]/g, '_');
924
+ const writePath = path.join(process.cwd(), safeName);
925
+ fs.writeFileSync(writePath, Buffer.from(att.base64, 'base64'));
926
+
927
+ contentArray[0].text += `\n\n[SYSTEM NOTIFICATION: The user attached a document named '${safeName}'. It has been saved to your workspace at ./${safeName}. Please use your file reading tools to examine its contents.]`;
928
+ this.notice(`saved attached document to ${writePath}`);
929
+ }
930
+ }
931
+ this.messages.push({ role: 'user', content: contentArray });
932
+ } else {
933
+ this.messages.push({ role: 'user', content: this.userPrompt });
934
+ }
935
+
936
+ this.userPrompt = null; // Clear so we don't push it again
937
+ this.attachments = null; // Clear attachments
938
+ }
939
+
940
+ this.stepsCompleted = 0;
941
+ // Fresh per task. A digest from a previous task would point at a step
942
+ // number the model can no longer see, which is worse than paying twice.
943
+ this._observationDigests = new Map();
944
+ this.consecutiveCommands = 0;
945
+ this.circuitBreakerUnlocked = false;
946
+ this.tokenTracker.resetTask();
947
+
948
+ // The core loop governed by Sovereign Human-in-the-Loop Checkpoints
949
+ while (!this.stopRequested) {
950
+ if (this.stopRequested) {
951
+ this.emitEvent({ type: 'error', message: 'Task killed by user.' });
952
+ auditLog.log({ event: 'task_killed', taskId: this.taskId });
953
+ break;
954
+ }
955
+ this.emitEvent({ type: 'status', message: 'Thinking...' });
956
+
957
+ try {
958
+ // --- V8 MEMORY ARCHITECTURE: DECAY → WINDOW → REAL COMPACTION ---
959
+ // The window is no longer only a view over an ever-growing array. Old
960
+ // tool payloads decay into stubs, and anything amputated is physically
961
+ // replaced by a summary, so memory, disk and CPU all stay bounded.
962
+
963
+ const activeTier = isDevEmulator(this.config) ? 'super_max' : (this.config.tier || 'premium');
964
+ const MAX_CHAR_POOL = resolveCharPool(activeTier, this.config.maxCharPool);
965
+
966
+ // 5-Stage Progressive Compaction (Claude Code Architecture / Research Section E):
967
+ // Stage 2 (Snip >15K tokens / 60K chars) + Stage 1 (Budget Reduction >70%) +
968
+ // Stage 3 (Microcompact) + Stage 4 (Context Collapse >85%)
969
+ const compaction = applyProgressiveCompaction(this.messages, MAX_CHAR_POOL, {
970
+ budgetKeepRecent: this.config.keepRecentToolResults ?? 8,
971
+ collapseKeepRecent: 2
972
+ });
973
+ if (compaction.totalReclaimed > 0) {
974
+ this.debugLog(`progressive compaction reclaimed ${(compaction.totalReclaimed / 1024).toFixed(1)} KB [${compaction.stagesFired.join(', ')}]`);
975
+ }
976
+
977
+ const { window: slidingWindow, splitIndex } = buildWindow(this.messages, MAX_CHAR_POOL);
978
+
979
+ // What the context window actually holds right now.
980
+ //
981
+ // The footer's context meter was being fed `usedTokens` — the CUMULATIVE
982
+ // token spend of the whole task — and divided by a CHARACTER pool. Two
983
+ // unit errors compounding: after 16 steps it read 290,069 against
984
+ // 120,000 and, because the meter clamped at 99, displayed "290K (99%)"
985
+ // while the real window sat around a quarter full. A meter pinned at 99%
986
+ // for most of a run answers no question at all.
987
+ //
988
+ // Measured, not extrapolated: this is the same array the request is
989
+ // built from, one line after it is built.
990
+ this._contextChars = measureMessages(slidingWindow);
991
+ this._contextLimitChars = MAX_CHAR_POOL;
992
+
993
+ // Real compaction: physically collapse the amputated range so the array
994
+ // stops growing. Index 0 (the Root Anchor / original user intent) is
995
+ // never compacted away.
996
+ if (splitIndex >= 1) {
997
+ const dropped = this.messages.slice(1, splitIndex + 1);
998
+ if (dropped.length > 0) {
999
+ const removed = compactInPlace(this.messages, splitIndex, describeDroppedMessages(dropped));
1000
+ this._lastCompactionIndex = null;
1001
+ this.debugLog(`compacted ${removed} messages out of the live context`);
1002
+
1003
+ // V5 Cortex tiered condensation. Tier 1 (structural, free) always
1004
+ // runs so the Digital Brain keeps learning on every provider. Tier 2
1005
+ // (semantic, billed) is capped per task and fully tracked.
1006
+ condenseAndSave(dropped, this.utilityProvider(), this.tokenTracker, {
1007
+ enableLlmPass: this.config.enableCortexCondenser !== false,
1008
+ maxLlmPasses: this.config.maxCortexCondenserPasses ?? 3,
1009
+ passesUsed: this._cortexPassesUsed
1010
+ }).then(outcome => {
1011
+ if (outcome.semantic) this._cortexPassesUsed++;
1012
+ }).catch(() => {});
1013
+ }
1014
+ }
1015
+
1016
+ // Resolve the active toolset before assembling the prompt: the task
1017
+ // brief only cites tools the model can actually see right now.
1018
+ // Providers with native schema deferral (Anthropic: `defer_loading` +
1019
+ // `tool_search_tool_regex`) keep the whole core in context for free —
1020
+ // the API holds the schemas server-side and expands them on demand,
1021
+ // with no discovery round trip. Every OTHER provider was serialising
1022
+ // all 24 core schemas into every request: 3,341 tokens to advertise
1023
+ // tools a typical turn never calls. There, ship the resident 8 (1,059
1024
+ // tokens) and let `tool_search` — the same mechanism that already
1025
+ // unlocks packs — fetch the rest.
1026
+ const residentOnly = this.llm.supportsToolSearch?.() !== true;
1027
+ let dynamicTools = await getDynamicTools(this.config.autonomyMode, this.llm.supportsVision, {
1028
+ unlockedPacks: this.unlockedPacks,
1029
+ unlockedTools: this.unlockedTools,
1030
+ unlockOrder: this.unlockOrder,
1031
+ grantAllTools: this.config.grantAllTools === true,
1032
+ residentOnly
1033
+ });
1034
+ // A delegated subagent runs with a declared toolScope. Enforcing it
1035
+ // here — the one place the catalogue is assembled — is what makes the
1036
+ // scope real; the previous implementation accepted the argument and
1037
+ // ignored it entirely (finding D-7).
1038
+ if (this.toolScope instanceof Set && this.toolScope.size > 0) {
1039
+ dynamicTools = dynamicTools.filter(t => this.toolScope.has(t.name));
1040
+ }
1041
+ const dynamicToolNames = dynamicTools.map(t => t.name);
1042
+
1043
+ // Dynamically inject the absolute latest System context
1044
+ const autonomyMode = this.config.autonomyMode || 'C';
1045
+ const allowedPaths = this.sandbox.grantAllAccess
1046
+ ? "/ (ALL ACCESS GRANTED - You can read/write anywhere EXCEPT system protected folders like /etc or /System)"
1047
+ : (this.sandbox.workspacePaths ? this.sandbox.workspacePaths.join(', ') : process.cwd());
1048
+ const providerPrompt = getProviderSpecificPrompt(this.config.provider);
1049
+ const memoryContext = getMemoryContextString();
1050
+
1051
+ // --- PER-TASK STATIC CONTEXT (computed once, not once per step) ---
1052
+ // Cortex retrieval, design detection, creative-pattern lookup and the
1053
+ // workspace domain scan are all constant for the duration of a task, but
1054
+ // used to run on every single iteration: a SQLite query, several
1055
+ // directory scans and a skills-index rebuild per step. They also churned
1056
+ // the prompt prefix whenever a result shifted, costing cache hits.
1057
+ if (!this.staticContext) {
1058
+ this.staticContext = await this.buildStaticContext();
1059
+ }
1060
+ const { lessonsLearned, designContext, antiRepeatDirective, universalSkillsContext, planHint } = this.staticContext;
1061
+
1062
+ // --- WORKSPACE AWARENESS ---
1063
+ // perception.buildContext() output used to be computed and discarded,
1064
+ // leaving the agent blind and forcing it to rediscover the project with
1065
+ // ls/cat on every task.
1066
+ let workspaceSummary = '';
1067
+ if (this.workspaceContext) {
1068
+ const ctx = this.workspaceContext;
1069
+ const facts = [
1070
+ ctx.cwd && `Working directory: ${ctx.cwd}`,
1071
+ ctx.os && `OS: ${ctx.os}`,
1072
+ ctx.framework && `Framework: ${ctx.framework}`,
1073
+ ctx.projectType && `Project type: ${ctx.projectType}`,
1074
+ ctx.packageManager && `Package manager: ${ctx.packageManager}`,
1075
+ ctx.gitBranch && `Git branch: ${ctx.gitBranch}`,
1076
+ ctx.gitStatus && `Git status: ${ctx.gitStatus}`,
1077
+ Array.isArray(ctx.keyFiles) && ctx.keyFiles.length > 0 && `Key files: ${ctx.keyFiles.slice(0, 25).join(', ')}`
1078
+ ].filter(Boolean);
1079
+ if (facts.length > 0) {
1080
+ workspaceSummary = `\n\n📂 WORKSPACE CONTEXT (already scanned for you — do NOT waste steps rediscovering this):\n${facts.join('\n')}`;
1081
+ }
1082
+ }
1083
+
1084
+ // --- PROMPT ASSEMBLY, ORDERED STABLE → VOLATILE ---
1085
+ // Every provider's prompt cache binds on an identical prefix. Anthropic
1086
+ // marks it explicitly with cache_control (up to 4 breakpoints); OpenAI,
1087
+ // DeepSeek and Gemini match the longest stable prefix automatically. So
1088
+ // each block that is constant within a task carries its own cache anchor,
1089
+ // and the ONLY bytes that can change between steps live in a single
1090
+ // volatile block kept dead last. Previously just [1] was anchored, so the
1091
+ // pack catalogue and the whole workspace/memory/skills context were
1092
+ // re-billed on every step even though they never changed mid-task.
1093
+ const activePlaybooks = [...this.unlockedPacks]
1094
+ .map(pack => PACK_PLAYBOOKS[pack])
1095
+ .filter(Boolean)
1096
+ .join('\n\n');
1097
+
1098
+ // The task brief turns "here are 28 tools" into "here is the order to
1099
+ // use them in", which is where whole exploratory steps get saved.
1100
+ const taskBrief = this.taskShape
1101
+ ? buildTaskBrief(this.taskShape, dynamicToolNames)
1102
+ : '';
1103
+
1104
+ // Uncertainty gate. Every input is already in hand — no sampling, no
1105
+ // second model, no network. Returns '' for a confident step, and that
1106
+ // empty string is the point: reflecting on every step is the waste the
1107
+ // gating literature exists to prevent.
1108
+ let uncertaintyDirective = '';
1109
+ let uncertaintyTemperatureDelta = 0;
1110
+ try {
1111
+ const lastAssistant = [...this.messages].reverse()
1112
+ .find((m) => m.role === 'assistant' && typeof m.content === 'string');
1113
+ const vfeStatus = this.vfeTracker ? this.vfeTracker.getStatus() : { isPlateau: false };
1114
+ const isVfePlateau = Boolean(this._paradigmShiftPending || vfeStatus.isPlateau);
1115
+
1116
+ const verdict = assessStep({
1117
+ consecutiveFailures: this.consecutiveFailures,
1118
+ lastResult: this._lastToolResult,
1119
+ recentTools: (this._planSteps || []).map((step) => step.tool),
1120
+ planSimilarity: this._planSimilarity,
1121
+ lessonSimilarity: this._lessonSimilarity,
1122
+ vfePlateau: isVfePlateau,
1123
+ assistantText: lastAssistant ? lastAssistant.content : ''
1124
+ });
1125
+ uncertaintyDirective = verdict.directive;
1126
+ uncertaintyTemperatureDelta = verdict.temperatureDelta || 0;
1127
+ if (isVfePlateau) {
1128
+ this._paradigmShiftPending = false;
1129
+ }
1130
+ if (verdict.directive) {
1131
+ this.debugLog(`uncertainty ${verdict.uncertainty.toFixed(2)} (${verdict.level}): ${verdict.signals.join(', ')}`);
1132
+ auditLog.log({
1133
+ event: 'uncertainty_gate', taskId: this.taskId, step: this.stepsCompleted,
1134
+ level: verdict.level, score: Number(verdict.uncertainty.toFixed(3)), signals: verdict.signals
1135
+ });
1136
+ }
1137
+ } catch (e) {
1138
+ this.debugLog(`uncertainty gate skipped: ${e.message}`);
1139
+ }
1140
+
1141
+ const systemMessages = [
1142
+ // [1] stable for the entire task — the primary cache anchor.
1143
+ { role: 'system', content: SYSTEM_PROMPT, cacheAnchor: true },
1144
+ // [2] changes only when a capability pack unlocks (append-only), so it
1145
+ // is stable within a task and gets its own breakpoint.
1146
+ {
1147
+ role: 'system',
1148
+ content: `${taskBrief ? taskBrief + '\n\n' : ''}${getPackCatalogue(this.unlockedPacks)}${activePlaybooks ? '\n\n' + activePlaybooks : ''}`,
1149
+ cacheAnchor: true
1150
+ },
1151
+ // [3] per-task STATIC context — provider activation, detected design,
1152
+ // the one-time workspace scan, Cortex lessons, creative-diversity and
1153
+ // the skills index. All from this.staticContext (computed once) so it
1154
+ // is byte-identical every step; cached.
1155
+ {
1156
+ role: 'system',
1157
+ content: `${providerPrompt}${designContext}${workspaceSummary}${lessonsLearned}${planHint ? '\n' + planHint + '\n' : ''}${antiRepeatDirective}\n${universalSkillsContext}`,
1158
+ cacheAnchor: true
1159
+ },
1160
+ // [4] THE INVOKED SKILL, when the user named one.
1161
+ //
1162
+ // This is what makes `/skills <id> <instruction>` mean anything. Before
1163
+ // it existed the command printed the skill to the terminal and called
1164
+ // `continue` — the body never entered a request, the trailing
1165
+ // instruction was dropped on the floor, and no API call was made at
1166
+ // all. "Activating" a skill was a document viewer.
1167
+ //
1168
+ // Its own message rather than appended to [3]: [3] is the per-task
1169
+ // static anchor and appending here would invalidate that cache every
1170
+ // time a different skill is invoked. This one is stable for the task,
1171
+ // so it anchors too.
1172
+ ...(skillSystemMessage(this.activeSkill) || []),
1173
+ // [5] VOLATILE tail — the only bytes that may differ between steps:
1174
+ // autonomy/workspace paths (change via config) and persistent memory
1175
+ // (grows via memory_write mid-task). Kept LAST and UNcached so a change
1176
+ // here never invalidates the cached prefix in [1]-[3].
1177
+ {
1178
+ role: 'system',
1179
+ content: `${uncertaintyDirective ? uncertaintyDirective + '\n\n' : ''}${describeSession(this.taskId)}CURRENT AUTONOMY MODE: ${autonomyMode}\nALLOWED WORKSPACE PATHS: ${allowedPaths}\n(You MUST NOT write files outside these paths!)\n\n🧠 PERSISTENT MEMORY (FACTS LEARNED ABOUT THE USER):\n${memoryContext}`
1180
+ }
1181
+ ];
1182
+
1183
+ // Dev-only visibility into what fills each step's prompt. Set
1184
+ // CAUSYNE_TOKEN_DEBUG=1 to see the cached prefix ([1]-[3]) vs the volatile
1185
+ // tail ([4]) and confirm the cache is doing its job (pair it with the
1186
+ // "Prompt cache hit" line logged after the call).
1187
+ if (process.env.CAUSYNE_TOKEN_DEBUG === '1') {
1188
+ const chars = (m) => (typeof m?.content === 'string' ? m.content.length : JSON.stringify(m?.content || '').length);
1189
+ const windowChars = slidingWindow.reduce((sum, m) => sum + chars(m), 0);
1190
+ this.debugLog(
1191
+ `[token] sys1_base=${chars(systemMessages[0])} sys2_packs=${chars(systemMessages[1])} `
1192
+ + `sys3_static(cached)=${chars(systemMessages[2])} sys4_volatile=${chars(systemMessages[3])} `
1193
+ + `tools=${JSON.stringify(dynamicTools).length} window=${windowChars} (chars)`
1194
+ );
1195
+ }
1196
+
1197
+ // --- SOVEREIGN ROOT ANCHOR PRESERVATION ---
1198
+ // Ensure the original User Blueprint / Root Intent (`this.messages[0]`) is NEVER amputated
1199
+ // by the sliding window, regardless of how many steps or tool outputs accumulate.
1200
+ const rootAnchor = [];
1201
+ // The `slidingWindow.length > 0` guard is GONE on purpose. It meant an
1202
+ // empty window skipped the anchor too, and a request of nothing but
1203
+ // system blocks is a 400 on every provider. The window can no longer be
1204
+ // empty (contextManager.buildWindow), but the anchor must not depend on
1205
+ // that staying true.
1206
+ if (this.messages.length > 0 && slidingWindow[0] !== this.messages[0] && this.messages[0].role === 'user') {
1207
+ rootAnchor.push(this.messages[0]);
1208
+ }
1209
+
1210
+ // Final Assembly for the LLM. sanitizeSequences guarantees every
1211
+ // assistant tool_calls block is followed by matching tool results and
1212
+ // drops orphans that would otherwise 400 the provider.
1213
+ let prunedMessages = sanitizeSequences([...systemMessages, ...rootAnchor, ...slidingWindow]);
1214
+
1215
+ // THE INVARIANT, ENFORCED AT THE LAST POSSIBLE MOMENT.
1216
+ //
1217
+ // Every provider rejects a request whose messages carry no `user` or
1218
+ // `tool` turn. Two separate paths above can produce one — an empty
1219
+ // window, and sanitizeSequences dropping orphaned tool results — and
1220
+ // relying on either to be correct forever is how a 340,000-token run
1221
+ // died at step 13 with nothing to show for it. Checked here because
1222
+ // this is the last place the shape is still ours to fix.
1223
+ if (!prunedMessages.some(m => m.role === 'user' || m.role === 'tool')) {
1224
+ const root = this.messages.find(m => m.role === 'user');
1225
+ prunedMessages = [
1226
+ ...prunedMessages,
1227
+ root || { role: 'user', content: 'Continue the task from where you left off.' }
1228
+ ];
1229
+ this.debugLog('request had no user/tool message; re-anchored before sending');
1230
+ }
1231
+
1232
+ // BUDGET CEILING, CHECKED BEFORE SPENDING RATHER THAN AFTER.
1233
+ //
1234
+ // trackUsage() below stops the loop once the ceiling has ALREADY been
1235
+ // passed, so a run always overshot by one step — with ~9,000-token
1236
+ // steps a 299,000 ceiling landed near 308,000. Asking first is what
1237
+ // makes a ceiling mean the number the user typed.
1238
+ const affordability = this.tokenTracker.canAffordAnotherStep();
1239
+ if (!affordability.allowed) {
1240
+ this.emitEvent({ type: 'budget_warning', message: affordability.reason });
1241
+ this.emitEvent({ type: 'notice', message: 'Stopped before the token ceiling. Nothing was truncated mid-step.' });
1242
+ // A plan that was offered and did NOT carry the task through loses
1243
+ // rank, so a stale template stops being suggested forever. Only the
1244
+ // denominator moves here — recordPlan() is what awards a win.
1245
+ if (this._offeredPlanId) {
1246
+ try { recordMiss(this._offeredPlanId); } catch (e) { /* never block a stop */ }
1247
+ }
1248
+ auditLog.log({
1249
+ event: 'task_stopped_on_budget', taskId: this.taskId,
1250
+ used: affordability.used, limit: affordability.limit, projected: affordability.projected
1251
+ });
1252
+ break;
1253
+ }
1254
+
1255
+ // 2. PLAN (Call LLM). dynamicTools was resolved above so the task brief
1256
+ // could reference the tools actually in context.
1257
+ const response = await this.llm.chat(prunedMessages, {
1258
+ tools: dynamicTools,
1259
+ // IARS couples escalation to a wider search. Bounded so a stuck task
1260
+ // cannot climb into randomness across repeated escalations.
1261
+ temperature: Math.min(1, this.taskTemperature + uncertaintyTemperatureDelta),
1262
+ coreToolCount: dynamicTools.filter(t => isCoreTool(t.name)).length
1263
+ });
1264
+
1265
+ // The call came back. Whatever went wrong before is no longer
1266
+ // consecutive — without this reset, one failure at step 2 and another
1267
+ // at step 200 would count toward the same ceiling and stop a task that
1268
+ // was working perfectly well.
1269
+ this.consecutiveFailures = 0;
1270
+ this.lastFailureType = null;
1271
+
1272
+ // Track tokens. usage carries cachedPromptTokens / cacheWriteTokens so
1273
+ // the cost estimate reflects the real cache-discounted bill.
1274
+ const tracking = await this.tokenTracker.trackUsage(
1275
+ response.usage.promptTokens,
1276
+ response.usage.completionTokens,
1277
+ (msg) => this.emitEvent({ type: 'budget_warning', message: msg }),
1278
+ response.usage
1279
+ );
1280
+
1281
+ const cachedTokens = response.usage.cachedPromptTokens || 0;
1282
+ if (cachedTokens > 0) {
1283
+ this.debugLog(`prompt cache hit: ${cachedTokens.toLocaleString()} of ${response.usage.promptTokens.toLocaleString()} input tokens served from cache`);
1284
+ }
1285
+
1286
+
1287
+ //
1288
+
1289
+ // ALWAYS read (AgentViewModel: `optInt("dailyTokens", optInt("usedTokens",
1290
+
1291
+
1292
+ // — which this frame set to the TASK total. The card therefore drew task
1293
+ // tokens against `maxTokensPerDay` and read 29% while the daily limiter
1294
+ // fired at 1,001,877 / 1,000,000. The contract existed on one side only.
1295
+ //
1296
+ // Context is reported in characters against the character pool, the unit
1297
+ // the window is actually budgeted in. The renderer converts once, using
1298
+ // the project's own CHAR_POOLS convention, instead of comparing a token
1299
+ // count to a character limit.
1300
+ this.emitEvent({
1301
+ type: 'usage_update',
1302
+ usedTokens: tracking.dailyTokens || 0,
1303
+ dailyTokens: tracking.dailyTokens || 0,
1304
+ dailyCostUsd: tracking.dailyCostUsd || 0,
1305
+ dailyLimit: tracking.dailyLimit || 0,
1306
+ taskTokens: tracking.taskTokens || 0,
1307
+ taskCostUsd: tracking.taskCostUsd || 0,
1308
+ taskLimit: tracking.taskLimit || 0,
1309
+ costUsd: tracking.costUsd || 0,
1310
+ lifetimeTokens: tracking.lifetimeTokens || 0,
1311
+ lifetimeCostUsd: tracking.lifetimeCostUsd || 0,
1312
+ contextChars: this._contextChars || 0,
1313
+ contextLimitChars: this._contextLimitChars || 0,
1314
+ cachedTokens
1315
+ });
1316
+
1317
+ if (!tracking.allowed) {
1318
+ this.emitEvent({ type: 'error', message: tracking.reason });
1319
+ break;
1320
+ }
1321
+
1322
+ // Add assistant response to history. This is the ONLY place the turn is
1323
+ // recorded — the completion and guard branches below used to push it a
1324
+ // second time, duplicating every finished turn in the history.
1325
+ const hasToolCalls = Boolean(response.toolCalls && response.toolCalls.length > 0);
1326
+ const assistantMsg = {
1327
+ role: 'assistant',
1328
+ // An assistant message with neither text nor tool calls is rejected by
1329
+ // some providers, so a terminal turn always carries something.
1330
+ content: response.content || (hasToolCalls ? '' : 'Task completed.')
1331
+ };
1332
+ if (hasToolCalls) {
1333
+ assistantMsg.tool_calls = response.toolCalls;
1334
+ }
1335
+ if (response.reasoningContent) {
1336
+ assistantMsg.reasoning_content = response.reasoningContent;
1337
+ // The user asked to see the model think; show it rather than letting
1338
+
1339
+ this.emitEvent({ type: 'agent_reasoning', content: response.reasoningContent });
1340
+ }
1341
+ // Kept verbatim on the turn. A thinking-enabled model that gets its own
1342
+ // thinking blocks back is continuing a thought; one that does not is
1343
+ // starting over, and with tool_use in the same turn the API can reject
1344
+ // the sequence outright.
1345
+ if (response.thinkingBlocks) {
1346
+ assistantMsg.thinking_blocks = response.thinkingBlocks;
1347
+ }
1348
+ this.messages.push(assistantMsg);
1349
+
1350
+ if (response.content) {
1351
+ // V5: Parse TodoWrite checklist for visual plan display
1352
+ const planRegex = /-\s*\[([ xX\/])\]\s*(.*)/g;
1353
+ const planMatch = [...response.content.matchAll(planRegex)];
1354
+ if (planMatch.length > 0) {
1355
+ const planTasks = planMatch.map(m => {
1356
+ const marker = m[1].toLowerCase();
1357
+ const status = marker === 'x' ? 'completed' : (marker === '/' ? 'in_progress' : 'pending');
1358
+ return { status, text: m[2].trim() };
1359
+ });
1360
+ this.emitEvent({ type: 'agent_plan', plan: planTasks });
1361
+ }
1362
+
1363
+ this.emitEvent({ type: 'agent_message', content: response.content });
1364
+ }
1365
+
1366
+ // If no tool calls, check if the agent is just thinking/planning or actually done
1367
+ if (!response.toolCalls || response.toolCalls.length === 0) {
1368
+ const contentText = response.content || '';
1369
+ const lower = contentText.toLowerCase();
1370
+ const thinkingKeywords = ['let me ', "i'll ", 'i will ', 'now i ', 'next, i ', 'first, i ', 'checking ', 'trying to ', 'let us ', 'going to '];
1371
+ const isThinking = thinkingKeywords.some(kw => lower.includes(kw));
1372
+
1373
+ if (isThinking && this.consecutiveThinkingSteps < 3) {
1374
+ this.consecutiveThinkingSteps++;
1375
+ this.emitEvent({ type: 'status', message: 'Agent planning next steps...' });
1376
+ // assistantMsg was already pushed above — pushing it again here
1377
+ // duplicated the whole turn in history on every nudge.
1378
+ this.messages.push({
1379
+ role: 'system',
1380
+ content: '[SYSTEM NOTICE: You provided reasoning/planning without calling any tools. Please invoke the next appropriate tool now to continue progressing on the task.]'
1381
+ });
1382
+ this.memoryManager.saveHistory(this.messages);
1383
+ continue;
1384
+ }
1385
+
1386
+ // --- COMPLETION GUARDS ---
1387
+ // Two hard rules learned the expensive way:
1388
+ // 1. Match on word boundaries. Substring matching made 'ui' fire on
1389
+ // "build"/"quick"/"requirements" and 'race' fire on "trace",
1390
+ // dragging ordinary scripting tasks into game/web enforcement.
1391
+ // 2. Fire at most once per task. These used to be gated on
1392
+ // consecutiveThinkingSteps, which resets on every tool call, so
1393
+ // a guard could block completion indefinitely and burn the whole
1394
+ // step budget on an already-finished task.
1395
+ const taskPrompt = this.currentTaskPrompt || this.userPrompt || '';
1396
+ const isGameTask = isGameIntent(taskPrompt);
1397
+
1398
+ this.gameToolStats = this.gameToolStats || { godot_act: 0, game_asset_fetch: 0, game_build_html5: 0, last_godot_action: null };
1399
+
1400
+ if (isGameTask && !this.gameGuardFired) {
1401
+ const buildingRealGame = this.gameCascadeState.currentStage > 1 || this.stepsCompleted > 0;
1402
+ const needsVerification = buildingRealGame && !this.gameCascadeState.verificationPassed && this.gameCascadeState.currentStage >= 7;
1403
+ // Production-depth enforcement is opt-in. It is right for
1404
+ // "build me a full RPG" and wrong for "fix the jump height in my game".
1405
+ const needsDepth = this.config.enforceGameDepth === true && this.stepsCompleted < 50;
1406
+
1407
+ if (needsVerification || needsDepth) {
1408
+ this.gameGuardFired = true;
1409
+ this.emitEvent({ type: 'status', message: '⚡ Sovereign Game Guard: Enforcing Production Depth & Quality Gate...' });
1410
+
1411
+ const guardMsg = needsDepth && !needsVerification
1412
+ ? `[SOVEREIGN DEPTH BLOCK: You are building a Full Production Game but only took ${this.stepsCompleted} steps. A full production game requires 100+ steps. Expand the game with more levels, richer UI, complex enemy AI, or more assets before finishing. This check fires once — if the task genuinely is complete, say so plainly and finish.]`
1413
+ : `[SOVEREIGN QUALITY GATE: You built a game but declared completion without verifying it. Run \`game_web_runtime_inspector\` and \`game_canvas_audit\` to confirm runtime stability (FPS >= 30, no missing sprites) before exiting. This check fires once — if verification is genuinely not applicable, say so plainly and finish.]`;
1414
+
1415
+ this.messages.push({ role: 'system', content: guardMsg });
1416
+ this.memoryManager.saveHistory(this.messages);
1417
+ continue;
1418
+ }
1419
+ }
1420
+
1421
+ // P6 — ANTI-SLOP GATE (Vision §7, ENFORCED)
1422
+ //
1423
+ // The visual guard below makes the agent LOOK at its work, but the
1424
+ // agent was the only judge of what it saw — and a model that just
1425
+ // emitted a purple gradient hero does not reliably call it generic.
1426
+ // This reads the HTML/CSS actually written and scores it. Weighted and
1427
+ // threshold-based, never a single tripwire: a guard with false
1428
+ // positives gets switched off (the ZM-09 lesson).
1429
+ if (!this.slopGateFired && this.webArtifacts.size > 0) {
1430
+ this.slopGateFired = true; // fires at most once per task, like the others
1431
+ try {
1432
+ const files = [];
1433
+ for (const artifactPath of this.webArtifacts) {
1434
+ try {
1435
+ files.push({ path: artifactPath, content: fsSync.readFileSync(artifactPath, 'utf8') });
1436
+ } catch (e) { /* deleted or moved since: nothing to judge */ }
1437
+ }
1438
+
1439
+ const result = analyzeFiles(files);
1440
+ if (result.verdict === 'slop') {
1441
+ const recent = Array.isArray(this.staticContext?.recentMovements) ? this.staticContext.recentMovements : [];
1442
+ const brief = formatMovementBrief(suggestMovements(recent, 3));
1443
+
1444
+ auditLog.log({
1445
+ event: 'anti_slop_block', taskId: this.taskId,
1446
+ score: result.score, signals: result.findings.map(f => f.id)
1447
+ });
1448
+ this.emitEvent({ type: 'status', message: `⚡ Anti-Slop Gate: design scored ${result.score} — rebuilding.` });
1449
+ this.messages.push({ role: 'system', content: formatFindings(result, brief) });
1450
+ this.memoryManager.saveHistory(this.messages);
1451
+ continue;
1452
+ }
1453
+
1454
+ if (result.findings.length > 0) {
1455
+ auditLog.log({
1456
+ event: 'anti_slop_pass', taskId: this.taskId,
1457
+ score: result.score, verdict: result.verdict,
1458
+ signals: result.findings.map(f => f.id)
1459
+ });
1460
+ }
1461
+ } catch (e) {
1462
+ // A quality gate must never be the reason a task fails.
1463
+ this.debugLog(`anti-slop gate skipped: ${e.message}`);
1464
+ }
1465
+ }
1466
+
1467
+ // Web Visual Validation Guard (Anti-Slop / Transcendence)
1468
+ const isWebTask = isWebIntent(taskPrompt);
1469
+
1470
+ if (isWebTask && !this.webGuardFired && this.stepsCompleted > 0) {
1471
+ const hasVision = this.llm.supportsVision !== false;
1472
+ // Finding D-6: this guard used to demand `screenshot` /
1473
+ // `vision_inspect_ui` unconditionally. Both need the browser and
1474
+ // capture packages, which are provisioned only after the user
1475
+ // consents — so on a fresh install the guard was ordering the agent
1476
+ // to use a tool that could not run. Ask for the capability once;
1477
+ // if it was declined, fall through to the path that needs nothing.
1478
+ let visionReady = false;
1479
+ let visionDeclined = false;
1480
+ try {
1481
+ const { groupStatus, isGroupDeclined } = await import('./provisioning/capabilityProvisioner.js');
1482
+ visionReady = groupStatus('vision').state === 'ready';
1483
+ visionDeclined = isGroupDeclined('vision');
1484
+ } catch (e) { /* treat as not ready */ }
1485
+ let webGuardMsg = '';
1486
+
1487
+ if (hasVision && !visionReady && !visionDeclined) {
1488
+ webGuardMsg = `[ANTI-SLOP CHECK: You are building a Web/UI project and must look at it before finishing, but the browser and screen-capture capability is not enabled yet. Call \`manage_local_dependencies\` with action "grant_capability" and group "vision" to request it from the user. If the user declines, read the final compiled HTML/CSS instead and verify it that way. This check fires once.]`;
1489
+ } else if (hasVision && visionReady && !this.webVerificationState.screenshotTaken) {
1490
+ webGuardMsg = `[ANTI-SLOP CHECK: You are building a Web/UI project but attempted to finish without VISUALLY verifying your work. Use \`screenshot\` or \`vision_inspect_ui\` to look at the UI in a browser. A human looks at their work to avoid slop. This check fires once — if there is genuinely nothing visual to inspect, say so plainly and finish.]`;
1491
+ } else if ((!hasVision || visionDeclined) && !this.webVerificationState.browserActUsed && !this.webVerificationState.screenshotTaken) {
1492
+ webGuardMsg = `[TRANSCENDENCE CHECK: You are building a Web/UI project. As a vision-disabled model you cannot screenshot, but you MUST use \`browser_act\` to inspect the DOM, or read the final compiled HTML/CSS, to verify semantic correctness before finishing. This check fires once.]`;
1493
+ }
1494
+
1495
+ if (webGuardMsg) {
1496
+ this.webGuardFired = true;
1497
+ // The verification tools live in the web pack; make sure they are
1498
+ // actually in context before demanding the agent use them.
1499
+ this.unlockPack('web');
1500
+ this.emitEvent({ type: 'status', message: '⚡ Anti-Slop Guard: Forcing visual/semantic verification...' });
1501
+ this.messages.push({ role: 'system', content: webGuardMsg });
1502
+ this.memoryManager.saveHistory(this.messages);
1503
+ continue;
1504
+ }
1505
+ }
1506
+
1507
+ this.emitEvent({ type: 'status', message: 'Task completed.' });
1508
+ this.memoryManager.saveHistory(this.messages); // Save final state
1509
+
1510
+
1511
+
1512
+ // The task worked. Keep the route so the next one like it does not
1513
+ // have to rediscover it. Deterministic: we already watched every step
1514
+ // run, so asking a model to summarise it would be paying for a worse
1515
+ // copy of a fact we hold.
1516
+ try {
1517
+ const stored = recordPlan({
1518
+ prompt: this.currentTaskPrompt || this.userPrompt,
1519
+ archetype: this.taskShape ? this.taskShape.archetype : '',
1520
+ steps: this._planSteps || []
1521
+ });
1522
+ if (stored) this.debugLog(`plan cached: ${stored.steps.length} steps, ${stored.wins}/${stored.uses} wins`);
1523
+ } catch (e) {
1524
+ this.debugLog(`plan cache write skipped: ${e.message}`);
1525
+ }
1526
+
1527
+ auditLog.log({ event: 'task_complete', taskId: this.taskId, steps: this.stepsCompleted });
1528
+ this.emitEvent({ type: 'task_complete', taskId: this.taskId });
1529
+ break;
1530
+ }
1531
+
1532
+ this.consecutiveThinkingSteps = 0; // Reset thinking counter when tools are called
1533
+
1534
+ // 3a. PREFETCH — run the independent read-only calls of this batch
1535
+ // concurrently (finding D-8).
1536
+ //
1537
+ // Every competitive agent batches independent reads; this loop executed
1538
+ // them strictly one after another, so six `fs_read`s cost six full
1539
+ // round trips of tool latency.
1540
+ //
1541
+ // The serial loop below is deliberately left EXACTLY as it was. Nothing
1542
+ // about ordering, approval gates, step counting, streaming, retries or
1543
+ // error classification changes — the only difference is that a
1544
+ // read-only result may already be sitting in this map by the time the
1545
+ // loop reaches it. Restructuring the 400-line body into a batch would
1546
+ // have been the obvious approach and a far worse one: it is dense with
1547
+ // `break`s, approval waits and circuit breakers, and a subtle reordering
1548
+ // there is exactly the class of silent defect this whole remediation
1549
+ // exists to clean up.
1550
+ //
1551
+ // Safety rules that make this sound:
1552
+ // * Only tools on the read-only allowlist are eligible. Nothing that
1553
+ // writes, deletes, executes or asks for approval is ever prefetched.
1554
+ // * The sandbox is consulted FIRST, with the same call the serial path
1555
+ // makes, so a denied path is never read behind the guard's back.
1556
+ // * Only a SUCCESSFUL prefetch is consumed. A failure is discarded and
1557
+ // the serial path executes normally, so the retry-with-backoff and
1558
+ // error-classification logic keeps its exact behaviour.
1559
+ const prefetched = this.prefetchReadOnlyCalls(response.toolCalls);
1560
+
1561
+ // 3. ACT (Execute tool calls)
1562
+ for (const toolCall of response.toolCalls) {
1563
+ // Canonicalise once, here. Every downstream comparison — the sandbox's
1564
+ // risk classification, the approval gates, the game cascade lists —
1565
+ // matches on this string, so if it is not the catalogue name those
1566
+ // checks silently never fire.
1567
+ const functionName = canonicalToolName(toolCall.function.name);
1568
+ let args = {};
1569
+ try {
1570
+ args = typeof toolCall.function.arguments === 'string'
1571
+ ? JSON.parse(toolCall.function.arguments || '{}')
1572
+ : (toolCall.function.arguments || {});
1573
+ } catch (parseErr) {
1574
+ this.messages.push({
1575
+ role: 'tool',
1576
+ tool_call_id: toolCall.id,
1577
+ name: functionName,
1578
+ content: JSON.stringify({ success: false, error: `Invalid JSON in arguments: ${parseErr.message}` })
1579
+ });
1580
+ continue;
1581
+ }
1582
+
1583
+ this.emitEvent({ type: 'step_start', tool: functionName, args: args });
1584
+ this.consecutiveCommands++;
1585
+
1586
+ if (functionName === 'screenshot' || functionName === 'vision_inspect_ui') {
1587
+ this.webVerificationState.screenshotTaken = true;
1588
+ }
1589
+ if (functionName === 'browser_act') {
1590
+ this.webVerificationState.browserActUsed = true;
1591
+ }
1592
+
1593
+ // Sovereign Game Pre-Flight Interceptor (v1.3.13)
1594
+ const isGameTaskExec = isGameIntent(this.currentTaskPrompt || this.userPrompt || '');
1595
+
1596
+ const ALL_GAME_TOOLS_LIST = [
1597
+ 'godot_act', 'game_asset_fetch', 'game_build_html5',
1598
+ 'game_lore_forge', 'game_dialogue_tree_gen', 'game_quest_arc_builder',
1599
+ 'game_animation_rig', 'game_ai_behavior_tree', 'game_cinematic_sequencer',
1600
+ 'game_asset_harvest', 'game_audio_synth', 'game_vfx_emitter_gen',
1601
+ 'game_shader_forge', 'game_scene_build', 'game_entity_spawner',
1602
+ 'game_tilemap_gen', 'game_ui_hud_builder', 'game_economy_balancer',
1603
+ 'game_data_forge', 'check_system_specs', 'game_web_runtime_inspector', 'game_canvas_audit'
1604
+ ];
1605
+
1606
+ if (ALL_GAME_TOOLS_LIST.includes(functionName)) {
1607
+ // Stage progression
1608
+ if (['check_system_specs', 'godot_act'].includes(functionName)) this.gameCascadeState.currentStage = Math.max(this.gameCascadeState.currentStage, 2);
1609
+ if (['game_lore_forge', 'game_dialogue_tree_gen', 'game_quest_arc_builder'].includes(functionName)) this.gameCascadeState.currentStage = Math.max(this.gameCascadeState.currentStage, 3);
1610
+ if (['game_animation_rig', 'game_ai_behavior_tree', 'game_cinematic_sequencer'].includes(functionName)) this.gameCascadeState.currentStage = Math.max(this.gameCascadeState.currentStage, 4);
1611
+ if (['game_asset_harvest', 'game_audio_synth', 'game_vfx_emitter_gen'].includes(functionName)) this.gameCascadeState.currentStage = Math.max(this.gameCascadeState.currentStage, 5);
1612
+ if (['game_shader_forge', 'game_scene_build', 'game_entity_spawner'].includes(functionName)) this.gameCascadeState.currentStage = Math.max(this.gameCascadeState.currentStage, 6);
1613
+ if (['game_tilemap_gen', 'game_ui_hud_builder', 'game_economy_balancer', 'game_build_html5'].includes(functionName)) this.gameCascadeState.currentStage = Math.max(this.gameCascadeState.currentStage, 7);
1614
+
1615
+ if (functionName === 'game_web_runtime_inspector' || functionName === 'game_canvas_audit') {
1616
+ this.gameCascadeState.verificationPassed = true;
1617
+ }
1618
+ }
1619
+
1620
+ if (isGameTaskExec && ['fs_write', 'sysRun', 'terminal_persistent_shell', 'game_build_html5'].includes(functionName) && this.gameCascadeState.currentStage < 7) {
1621
+ const isGameBuildHtml5 = functionName === 'game_build_html5';
1622
+ const isCustomScriptBlock = isGameBuildHtml5 && args.template === 'custom' && args.customScript && args.customScript.length > 2000;
1623
+ const targetPath = (args.path || args.filepath || args.targetFile || args.command || '').toLowerCase();
1624
+
1625
+ // BUGFIX: Only block actual game file extensions to prevent false positives when creating directories (e.g., mkdir /Desktop/RacerGame)
1626
+ const isGameFileOrDir = targetPath.includes('.html'); // Removed .gd and .tscn to allow Godot to function
1627
+ const isEchoingToFile = targetPath.includes('echo') && targetPath.includes('>') && isGameFileOrDir;
1628
+
1629
+ if ((['fs_write'].includes(functionName) && isGameFileOrDir) || isEchoingToFile || isCustomScriptBlock) {
1630
+ const interceptMsg = `[SOVEREIGN CASCADE VIOLATION: You attempted to skip from Stage ${this.gameCascadeState.currentStage} to Stage 7 without completing Stages 3, 4, 5, and 6 modularly. DO NOT write monolithic 20 KB custom scripts!]`;
1631
+ this.emitEvent({ type: 'step_error', tool: functionName, error: '⚡ Sovereign Game Intercept: Cascade violation detected.' });
1632
+ this.messages.push({
1633
+ role: 'tool',
1634
+ tool_call_id: toolCall.id,
1635
+ name: functionName,
1636
+ content: JSON.stringify({ success: false, error: "Sovereign Cascade Violation", CRITICAL_INSTRUCTION: interceptMsg })
1637
+ });
1638
+ continue;
1639
+ }
1640
+ }
1641
+
1642
+ if (functionName === 'game_build_html5' && args.template === 'custom' && args.customScript) {
1643
+ const scr = args.customScript;
1644
+ if (scr.includes('c.drw ?') && !scr.includes('drw(')) {
1645
+ this.messages.push({
1646
+ role: 'tool', tool_call_id: toolCall.id, name: functionName,
1647
+ content: JSON.stringify({ success: false, error: 'AST SANITIZER: Detected ternary check for c.drw but no actual drw() method definition found! Fix and retry.' })
1648
+ });
1649
+ continue;
1650
+ }
1651
+ if (scr.includes('ctx.translate(-cam.x, -cam.y)') && (scr.includes('c.x - cam.x') || scr.includes('c.y - cam.y'))) {
1652
+ this.messages.push({
1653
+ role: 'tool', tool_call_id: toolCall.id, name: functionName,
1654
+ content: JSON.stringify({ success: false, error: 'AST SANITIZER: Detected double translation! You are translating context by cam and then manually subtracting cam again inside drawing calls!' })
1655
+ });
1656
+ continue;
1657
+ }
1658
+ }
1659
+
1660
+ // 4-Layer Security Sandbox Validation
1661
+ // 2. Sandbox Security Validation (CRITICAL FIX: Prevent Infinite Loop on Block)
1662
+ const validation = this.sandbox.validate(functionName, args);
1663
+ if (!validation.allowed) {
1664
+ this.consecutiveBlockedActions++;
1665
+
1666
+ // Hard Circuit Breaker (Transcendence Context Consciousness)
1667
+ if (this.consecutiveBlockedActions >= 4) {
1668
+ const fatalMsg = `FATAL ERROR: Infinite Token Drain Detected. Agent repeatedly attempted blocked/forbidden actions 4 times in a row. Forcibly aborting task to save tokens.`;
1669
+ this.emitEvent({ type: 'step_error', tool: functionName, error: fatalMsg });
1670
+ auditLog.log({ event: 'task_killed_circuit_breaker', taskId: this.taskId, reason: fatalMsg });
1671
+
1672
+ // Save memory of failure for RLTF
1673
+ cortexDb.saveMemory(
1674
+ this.currentTaskPrompt || 'Unknown task',
1675
+ 'Agent stubbornly repeated blocked actions causing an infinite loop.',
1676
+ `Do NOT repeat ${functionName} with these exact arguments, as it is permanently blocked by Sandbox. Ensure you respect ALLOWED WORKSPACE PATHS.`
1677
+ ).catch(() => {});
1678
+
1679
+ this.abort();
1680
+ break;
1681
+ }
1682
+
1683
+ auditLog.log({ event: 'tool_blocked', taskId: this.taskId, tool: functionName, args, reason: validation.reason });
1684
+ this.emitEvent({ type: 'step_error', tool: functionName, error: validation.reason });
1685
+
1686
+ // Feed error back to LLM with absolute command to stop repeating
1687
+ this.messages.push({
1688
+ role: 'tool',
1689
+ tool_call_id: toolCall.id,
1690
+ name: functionName,
1691
+ content: JSON.stringify({
1692
+ success: false,
1693
+ error: validation.reason,
1694
+ CRITICAL_INSTRUCTION: "DO NOT RETRY THIS EXACT ACTION. The Sandbox blocked it. Ensure you only write to paths inside ALLOWED WORKSPACE PATHS, or use a different approach. Repeating this will drain tokens."
1695
+ })
1696
+ });
1697
+ continue;
1698
+ }
1699
+
1700
+ this.consecutiveBlockedActions = 0; // Reset counter on allowed action
1701
+
1702
+ // Approval flow for dangerous actions (Mode A/B) or ALWAYS for heavy AI downloads
1703
+ // The schema name is manage_local_dependencies. Comparing against the
1704
+ // old alias meant this consent gate never fired and multi-GB model
1705
+ // downloads ran unannounced.
1706
+ const isHeavyAiDownload = functionName === 'manage_local_dependencies' &&
1707
+ ['pipInst', 'fetchBin', 'fetchMod'].includes(args.action);
1708
+ // A standing approval from "Allow always" lifts the gate for this
1709
+ // tool until the process restarts. It is checked HERE rather than
1710
+
1711
+ // asking and then auto-answering would still interrupt the user,
1712
+ // which is the entire thing they were trying to stop.
1713
+ //
1714
+ // Nothing else is bypassed: the sandbox has already run above, so a
1715
+ // FORBIDDEN command is still refused and a path outside the workspace
1716
+ // is still blocked. This waives the QUESTION, not the guard.
1717
+ const standingApproval = isAlwaysAllowed(functionName);
1718
+ if (standingApproval) {
1719
+ auditLog.log({
1720
+ event: 'approval_auto_granted', taskId: this.taskId,
1721
+ tool: functionName, risk: validation.risk, reason: 'allow_always'
1722
+ });
1723
+ }
1724
+
1725
+ if (!standingApproval && (
1726
+ validation.risk === 'extreme' ||
1727
+ isHeavyAiDownload ||
1728
+ (this.config.autonomyMode !== 'C' && validation.risk === 'dangerous') ||
1729
+ this.config.autonomyMode === 'A')) {
1730
+
1731
+ // Wait for local approval
1732
+ this.emitEvent({ type: 'approval_request', toolCallId: toolCall.id, tool: functionName, args, risk: validation.risk });
1733
+
1734
+ const approved = await this.waitForApproval(toolCall.id);
1735
+
1736
+ if (!approved) {
1737
+ this.messages.push({
1738
+ role: 'tool',
1739
+ tool_call_id: toolCall.id,
1740
+ name: functionName,
1741
+ content: JSON.stringify({ success: false, error: 'User denied this action' })
1742
+ });
1743
+ continue;
1744
+ }
1745
+ }
1746
+
1747
+ // Execute
1748
+ const tool = resolveTool(functionName);
1749
+
1750
+ // GUARD: Prevent crash if LLM hallucinates a non-existent tool name
1751
+ if (!tool || typeof tool.execute !== 'function') {
1752
+ // Only advertise what is currently in context. Listing all 69 names
1753
+ // here re-bloated the very context progressive disclosure protects.
1754
+ this.messages.push({
1755
+ role: 'tool',
1756
+ tool_call_id: toolCall.id,
1757
+ name: functionName,
1758
+ content: JSON.stringify({
1759
+ success: false,
1760
+ error: `Tool "${functionName}" does not exist. Call tool_search with a description of the capability you need to discover the correct tool.`
1761
+ })
1762
+ });
1763
+ continue;
1764
+ }
1765
+
1766
+ // AUTO-UNLOCK RECOVERY: the model named a real tool whose pack is not
1767
+ // loaded yet (it remembered it from an earlier task, or guessed well).
1768
+ // Unlock the pack and run it rather than dead-ending — capability is
1769
+ // deferred, never removed.
1770
+ if (!isCoreTool(functionName) && !this.unlockedTools.has(functionName)) {
1771
+ const pack = packForTool(functionName);
1772
+ if (this.unlockPack(pack)) {
1773
+ this.debugLog(`unlocked capability pack "${pack}" for ${functionName}`);
1774
+ }
1775
+ this.unlockedTools.add(functionName);
1776
+ }
1777
+
1778
+ // V5: Tool Call Argument Validation Layer
1779
+ const validationErrors = this.validateToolArgs(functionName, args);
1780
+ if (validationErrors) {
1781
+ this.messages.push({
1782
+ role: 'tool',
1783
+ tool_call_id: toolCall.id,
1784
+ name: functionName,
1785
+ content: JSON.stringify({
1786
+ success: false,
1787
+ error: `Invalid arguments: ${validationErrors}. Please fix and retry.`
1788
+ })
1789
+ });
1790
+ continue;
1791
+ }
1792
+
1793
+ // Stage 3 seam: PreToolUse hook. A no-op that always allows today; when
1794
+ // hooks are configured it can deny a call before it runs, returned as an
1795
+ // is_error tool result so the model self-corrects (never thrown).
1796
+ const preHook = await this.runPreToolUse(functionName, args, toolCall.id);
1797
+ if (preHook && preHook.denied) {
1798
+ this.messages.push({
1799
+ role: 'tool',
1800
+ tool_call_id: toolCall.id,
1801
+ name: functionName,
1802
+ content: JSON.stringify({
1803
+ success: false,
1804
+ error: `PreToolUse hook denied "${functionName}".`,
1805
+ hookFeedback: (preHook.messages || []).join('\n') || undefined
1806
+ })
1807
+ });
1808
+ continue;
1809
+ }
1810
+
1811
+ // P2: snapshot the files this call is about to change, before it runs.
1812
+ // Only the mutating fs tools match; everything else returns immediately.
1813
+ // Fail-open by contract — this can never block or fail a tool call.
1814
+ const restorePoints = captureBeforeMutation(functionName, args, {
1815
+ taskId: this.taskId,
1816
+ step: this.stepsCompleted
1817
+ });
1818
+
1819
+ // Say that a restore point exists.
1820
+ //
1821
+ // The ids came back and were dropped on the floor, so checkpointing
1822
+ // ran on every mutating call and the user was never told — /undo was
1823
+ // there the whole time with nothing pointing at it. On the Stage this
1824
+ // also marks the rail, which is what makes scrolling back double as
1825
+ // finding somewhere to roll back TO.
1826
+ if (restorePoints.length > 0) {
1827
+ this.emitEvent({
1828
+ type: 'checkpoint',
1829
+ taskId: this.taskId,
1830
+ count: restorePoints.length,
1831
+ tool: functionName,
1832
+ path: args.path || args.file_path || null,
1833
+ step: this.stepsCompleted
1834
+ });
1835
+ }
1836
+
1837
+ // P6: remember which rendered surfaces this task touched, so the
1838
+ // anti-slop gate can read the finished result back off disk rather
1839
+ // than trusting the model's own account of what it built.
1840
+ if (['fs_write', 'fs_edit', 'MultiEdit'].includes(functionName)) {
1841
+ const written = args.path || args.file_path;
1842
+ if (typeof written === 'string' && isScannableFile(written)) {
1843
+ this.webArtifacts.add(written.replace(/^~\//, os.homedir() + '/'));
1844
+ }
1845
+ }
1846
+
1847
+ // V5: Execute with Error Recovery Loop (3-attempt retry with exponential backoff)
1848
+ const retryKey = `${functionName}:${JSON.stringify(args).substring(0, 100)}`;
1849
+ let result;
1850
+ let duration;
1851
+ const MAX_RETRIES = 3;
1852
+ const currentRetries = this._toolRetryCount.get(retryKey) || 0;
1853
+
1854
+ try {
1855
+ const wasRetried = currentRetries > 0;
1856
+ const startTime = Date.now();
1857
+ // A concurrent read from the prefetch pass above, if it succeeded.
1858
+ const ready = prefetched.get(toolCall.id);
1859
+ const early = ready ? await ready : null;
1860
+ result = (early && early.success)
1861
+ ? early
1862
+ : await tool.execute(args, { signal: this.abortController.signal, parentLoop: this, ...this.sandbox });
1863
+ duration = Date.now() - startTime;
1864
+
1865
+ // Success: reset retry count for this operation
1866
+ this._toolRetryCount.delete(retryKey);
1867
+
1868
+ // V5 Cortex RLTF: Save the successful recovery as a permanent neural memory
1869
+ if (wasRetried) {
1870
+ cortexDb.saveMemory(
1871
+ this.currentTaskPrompt || `Task using ${functionName}`,
1872
+ `Failed ${currentRetries} times previously.`,
1873
+ `Successfully recovered and executed ${functionName} with arguments: ${JSON.stringify(args).substring(0, 200)}`
1874
+ ).catch(e => this.debugLog(`cortex save failed: ${e.message}`));
1875
+ }
1876
+
1877
+ } catch (toolError) {
1878
+ duration = 0;
1879
+ const errorType = this.classifyError(toolError);
1880
+
1881
+ if (currentRetries < MAX_RETRIES && errorType !== 'fatal') {
1882
+ this._toolRetryCount.set(retryKey, currentRetries + 1);
1883
+ const backoffMs = Math.pow(2, currentRetries) * 1000; // 1s, 2s, 4s
1884
+
1885
+ this.emitEvent({
1886
+ type: 'step_error',
1887
+ tool: functionName,
1888
+ error: `${errorType}: ${toolError.message} (retry ${currentRetries + 1}/${MAX_RETRIES} in ${backoffMs/1000}s)`
1889
+ });
1890
+
1891
+ auditLog.log({
1892
+ event: 'tool_retry',
1893
+ taskId: this.taskId,
1894
+ tool: functionName,
1895
+ attempt: currentRetries + 1,
1896
+ errorType,
1897
+ error: toolError.message
1898
+ });
1899
+
1900
+ // Wait before retry (the LLM will see the error and can adjust)
1901
+ await new Promise(resolve => setTimeout(resolve, backoffMs));
1902
+
1903
+ // Feed the classified error back so LLM can adjust its approach
1904
+ this.messages.push({
1905
+ role: 'tool',
1906
+ tool_call_id: toolCall.id,
1907
+ name: functionName,
1908
+ content: JSON.stringify({
1909
+ success: false,
1910
+ error: toolError.message,
1911
+ errorType: errorType,
1912
+ retryHint: this.getRetryHint(errorType),
1913
+ retriesLeft: MAX_RETRIES - currentRetries - 1
1914
+ })
1915
+ });
1916
+ continue;
1917
+ } else {
1918
+ // Max retries exceeded or fatal error: give up on this tool call
1919
+ this._toolRetryCount.delete(retryKey);
1920
+ this.emitEvent({
1921
+ type: 'step_error',
1922
+ tool: functionName,
1923
+ error: `Failed after ${MAX_RETRIES} attempts: ${toolError.message}`
1924
+ });
1925
+
1926
+ this.messages.push({
1927
+ role: 'tool',
1928
+ tool_call_id: toolCall.id,
1929
+ name: functionName,
1930
+ content: JSON.stringify({
1931
+ success: false,
1932
+ error: `PERMANENT FAILURE after ${MAX_RETRIES} retries: ${toolError.message}. Do NOT retry this exact operation. Try a completely different approach or ask the user for help.`,
1933
+ errorType: errorType
1934
+ })
1935
+ });
1936
+ continue;
1937
+ }
1938
+ }
1939
+
1940
+ auditLog.log({
1941
+ event: 'tool_exec',
1942
+ taskId: this.taskId,
1943
+ tool: functionName,
1944
+ success: result.success,
1945
+ duration_ms: duration
1946
+ });
1947
+
1948
+ // Stage 3 seam: PostToolUse hook. A no-op that always allows today; when
1949
+ // hooks are configured it can mark a completed call failed (e.g. a
1950
+ // formatter rejecting a write) so the model sees it. Neutral until then.
1951
+ const postHook = await this.runPostToolUse(functionName, args, result);
1952
+ if (postHook && postHook.denied) {
1953
+ result = {
1954
+ ...result,
1955
+ success: false,
1956
+ error: postHook.reason || `PostToolUse hook rejected "${functionName}".`,
1957
+ hookFeedback: (postHook.messages || []).join('\n') || undefined
1958
+ };
1959
+ }
1960
+
1961
+ // PROGRESSIVE DISCLOSURE: honour unlock signals from tool_search.
1962
+ // The control fields are stripped before the result reaches the LLM.
1963
+ if (result && (result._unlock_tools || result._unlock_packs)) {
1964
+ for (const packName of result._unlock_packs || []) {
1965
+ this.unlockPack(packName);
1966
+ }
1967
+ for (const toolName of result._unlock_tools || []) {
1968
+ this.unlockedTools.add(toolName);
1969
+ }
1970
+ delete result._unlock_tools;
1971
+ delete result._unlock_packs;
1972
+ this.debugLog(`capability packs loaded: ${[...this.unlockedPacks].join(', ') || 'core only'}`);
1973
+ }
1974
+
1975
+ // Buffer output for disconnect resilience
1976
+ await this.taskBuffer.appendOutput(this.taskId, { tool: functionName, result });
1977
+
1978
+ this.emitEvent({ type: 'step_complete', tool: functionName, result: result._is_image_marker ? '[IMAGE DATA]' : result });
1979
+ this.stepsCompleted++;
1980
+
1981
+ // The procedure, as it actually ran. Tool name plus a whitelisted
1982
+ // action token only — planCache.toStep() reads no paths, arguments or
1983
+ // command lines, so what is cached is a route and never a transcript.
1984
+ try {
1985
+ if (Array.isArray(this._planSteps)) {
1986
+ const stepSuccess = Boolean(result && result.success !== false && !result.error);
1987
+ this._planSteps.push(toStep(
1988
+ functionName,
1989
+ args,
1990
+ stepSuccess,
1991
+ result
1992
+ ));
1993
+ this._lastToolResult = result;
1994
+
1995
+ // Active Inference: Record step prediction error into VFE rolling tracker
1996
+ if (this.vfeTracker) {
1997
+ const isThrash = isThrashing((this._planSteps || []).map((s) => s.tool), functionName);
1998
+ const vfeOutcome = this.vfeTracker.recordStep({
1999
+ tool: functionName,
2000
+ success: stepSuccess,
2001
+ result,
2002
+ consecutiveFailures: this.consecutiveFailures,
2003
+ isThrashing: isThrash
2004
+ });
2005
+ if (vfeOutcome.paradigmShift) {
2006
+ this.debugLog(`[ACTIVE INFERENCE] VFE plateau detected: mean=${vfeOutcome.mean}, var=${vfeOutcome.variance}. Triggering paradigm shift.`);
2007
+ this._paradigmShiftPending = true;
2008
+ }
2009
+ }
2010
+ }
2011
+ } catch (e) { /* a plan is never worth failing a step for */ }
2012
+
2013
+ const ALL_GAME_TOOLS = [
2014
+ 'godot_act', 'game_asset_fetch', 'game_build_html5',
2015
+ 'game_lore_forge', 'game_dialogue_tree_gen', 'game_quest_arc_builder',
2016
+ 'game_animation_rig', 'game_ai_behavior_tree', 'game_cinematic_sequencer',
2017
+ 'game_asset_harvest', 'game_audio_synth', 'game_vfx_emitter_gen',
2018
+ 'game_shader_forge', 'game_scene_build', 'game_entity_spawner',
2019
+ 'game_tilemap_gen', 'game_ui_hud_builder', 'game_economy_balancer',
2020
+ 'game_data_forge', 'check_system_specs', 'game_web_runtime_inspector', 'game_canvas_audit'
2021
+ ];
2022
+ if (ALL_GAME_TOOLS.includes(functionName)) {
2023
+ // this.hasUsedGameTools is deprecated by this.gameCascadeState
2024
+ this.gameToolStats = this.gameToolStats || { godot_act: 0, game_asset_fetch: 0, game_build_html5: 0, last_godot_action: null };
2025
+ this.gameToolStats[functionName] = (this.gameToolStats[functionName] || 0) + 1;
2026
+ if (functionName === 'godot_act' && args && args.action) {
2027
+ this.gameToolStats.last_godot_action = args.action;
2028
+ }
2029
+ }
2030
+
2031
+ // Sovereign 100-Step Circuit Breaker Checkpoint Gate
2032
+ if (this.consecutiveCommands === 100 && !this.circuitBreakerUnlocked) {
2033
+ this.emitEvent({
2034
+ type: 'approval_request',
2035
+ toolCallId: 'circuit_breaker_100_' + this.stepsCompleted,
2036
+ tool: 'circuit_breaker_checkpoint',
2037
+ args: {
2038
+ consecutiveCommands: this.consecutiveCommands,
2039
+ stepsCompleted: this.stepsCompleted,
2040
+ activeTier: this.activeTier,
2041
+ maxTierSteps: this.maxSteps,
2042
+ message: `⚡ Circuit Breaker Checkpoint (100 steps reached). Approve to unlock and continue using your remaining ${this.activeTier.toUpperCase()} tier budget (up to ${this.maxSteps} steps total)?`
2043
+ },
2044
+ risk: 'info'
2045
+ });
2046
+
2047
+ const approved = await this.waitForApproval('circuit_breaker_100_' + this.stepsCompleted);
2048
+ if (!approved) {
2049
+ this.stopRequested = true;
2050
+ this.emitEvent({ type: 'status', message: 'Task stopped at 100-step circuit breaker checkpoint by user.' });
2051
+ break;
2052
+ }
2053
+ this.circuitBreakerUnlocked = true;
2054
+ this.emitEvent({ type: 'status', message: `⚡ Circuit breaker unlocked! Resuming execution up to ${this.maxSteps} tier budget...` });
2055
+ }
2056
+
2057
+ // Sovereign Tier Budget Extension Checkpoint Gate
2058
+ if (this.stepsCompleted >= this.maxSteps) {
2059
+ const extensionBatch = (this.activeTier === 'super_max') ? 600 : ((this.activeTier === 'max') ? 400 : 300);
2060
+ this.emitEvent({
2061
+ type: 'approval_request',
2062
+ toolCallId: 'tier_extension_' + this.stepsCompleted,
2063
+ tool: 'tier_budget_extension',
2064
+ args: {
2065
+ stepsCompleted: this.stepsCompleted,
2066
+ activeTier: this.activeTier,
2067
+ currentMaxSteps: this.maxSteps,
2068
+ message: `⚡ Tier Limit Reached (${this.stepsCompleted} / ${this.maxSteps} steps). The agent needs more steps to fulfill your intent. Approve to unlock another +${extensionBatch} steps for your ${this.activeTier.toUpperCase()} tier?`
2069
+ },
2070
+ risk: 'info'
2071
+ });
2072
+
2073
+ const approved = await this.waitForApproval('tier_extension_' + this.stepsCompleted);
2074
+ if (!approved) {
2075
+ this.stopRequested = true;
2076
+ this.emitEvent({ type: 'status', message: `Task stopped at ${this.maxSteps}-step tier limit by user.` });
2077
+ break;
2078
+ }
2079
+ this.maxSteps += extensionBatch;
2080
+ this.maxCircuitBreaker = this.maxSteps;
2081
+ this.emitEvent({ type: 'status', message: `⚡ Tier budget extended (+${extensionBatch} steps unlocked, new limit: ${this.maxSteps}). Resuming task...` });
2082
+ }
2083
+
2084
+ // Feed result back to LLM (4. OBSERVE)
2085
+ if (result && result._is_image_marker) {
2086
+ if (!this.llm.supportsVision) {
2087
+ // Fallback for blind models
2088
+ this.messages.push({
2089
+ role: 'tool',
2090
+ tool_call_id: toolCall.id,
2091
+ name: functionName,
2092
+ content: "[ERROR: The current LLM model you are using does not support Vision/Image capabilities. You cannot view images. Please rely on text tools or metadata instead.]"
2093
+ });
2094
+ } else {
2095
+ // Provider-specific format.
2096
+ //
2097
+ // This used to read `this.llm.constructor.name.includes('OpenAI')`.
2098
+ // Constructor names are not a stable provider identifier and can
2099
+ // change during packaging. Each provider declares its wire shape
2100
+ // explicitly.
2101
+ const imageContent = this.llm.imagePayloadFormat === 'openai' ? {
2102
+ type: "image_url",
2103
+ image_url: { url: `data:${result.mimeType};base64,${result.base64}` }
2104
+ } : {
2105
+ type: "image",
2106
+ source: {
2107
+ type: "base64",
2108
+ media_type: result.mimeType,
2109
+ data: result.base64
2110
+ }
2111
+ };
2112
+
2113
+ // We MUST push the tool return FIRST so tool_calls is immediately followed by tool messages
2114
+ this.messages.push({
2115
+ role: 'tool',
2116
+ tool_call_id: toolCall.id,
2117
+ name: functionName,
2118
+ content: `Image successfully captured: ${result.filename || 'image'}. Passing to vision observation.`
2119
+ });
2120
+
2121
+ // Then push the user observation message containing the image data block
2122
+ this.messages.push({
2123
+ role: 'user',
2124
+ content: [
2125
+ imageContent,
2126
+ {
2127
+ type: "text",
2128
+ text: `Image successfully loaded into vision context: ${result.filename || ''}`
2129
+ }
2130
+ ]
2131
+ });
2132
+ }
2133
+ } else {
2134
+ // SOVEREIGN CONTEXT WINDOW PROTECTION.
2135
+ // The previous implementation spread `...result` and only shortened
2136
+ // `output`, so tools that return their payload under a different key
2137
+ // (browser_act and blender_act return `log`) leaked the full
2138
+ // untruncated blob into history — and gained a redundant `output`
2139
+ // field on top. Truncate whichever string field is actually large.
2140
+ let resultString = JSON.stringify(result);
2141
+ // Per tool, not one number for all of them. A build log and a
2142
+ // source file are not equally worth their space in the window.
2143
+ const MAX_RESULT_CHARS = resultCapFor(functionName);
2144
+ let spill = null;
2145
+
2146
+ if (resultString.length > MAX_RESULT_CHARS) {
2147
+ // A1: spill the WHOLE result to disk before shortening it. The
2148
+ // previous code threw the overflow away, so the only way back to
2149
+ // it was re-running the tool — a second full execution and a
2150
+ // second full-size result in the window. Now the bytes survive
2151
+ // behind a path and recovery is one bounded read.
2152
+ // The raw result, not JSON.stringify(result): JSON escapes every
2153
+ // newline, which would land a large payload on a single line and
2154
+ // make the fs_read/ripgrep recovery route we advertise useless.
2155
+ spill = spillToolResult(result, {
2156
+ taskId: this.taskId,
2157
+ step: this.stepsCompleted,
2158
+ tool: functionName
2159
+ });
2160
+ const TRUNCATION_NOTICE = buildSpillNotice(spill);
2161
+
2162
+ const truncatedResult = {};
2163
+ // Budget shared across every oversized string field on the result.
2164
+ let budget = MAX_RESULT_CHARS - 2000;
2165
+
2166
+ for (const [key, value] of Object.entries(result)) {
2167
+ if (typeof value === 'string' && value.length > 500) {
2168
+ const allowance = Math.max(0, Math.min(value.length, budget));
2169
+ budget -= allowance;
2170
+ truncatedResult[key] = allowance < value.length
2171
+ ? value.substring(0, allowance) + TRUNCATION_NOTICE
2172
+ : value;
2173
+ } else if (value !== null && typeof value === 'object') {
2174
+ const serialized = JSON.stringify(value);
2175
+ if (serialized.length > budget) {
2176
+ truncatedResult[key] = spill
2177
+ ? `[${key} omitted here: ${(serialized.length / 1024).toFixed(1)} KB. The complete result is saved at ${spill.path} — read or search that file rather than re-running.]`
2178
+ : `[${key} omitted: ${(serialized.length / 1024).toFixed(1)} KB. Re-run with narrower arguments if you need it.]`;
2179
+ } else {
2180
+ budget -= serialized.length;
2181
+ truncatedResult[key] = value;
2182
+ }
2183
+ } else {
2184
+ truncatedResult[key] = value;
2185
+ }
2186
+ }
2187
+
2188
+ resultString = JSON.stringify(truncatedResult);
2189
+ }
2190
+
2191
+ // B5 — OBSERVATION COMPACTION.
2192
+ //
2193
+ // ARC Prize measured the same GPT-6 Astra weights at 62.7% under a
2194
+ // note-passing harness and 99.9% under one that compacts and reuses
2195
+ // prior work — with 49% FEWER tokens. Re-reading a file the agent
2196
+ // already read, or re-listing an unchanged directory, pays full
2197
+ // price for bytes that are verbatim identical to bytes already in
2198
+ // the window.
2199
+ //
2200
+ // Replacing the repeat with a pointer is not a loss of information:
2201
+ // "identical to step N" is strictly MORE informative than the bytes
2202
+ // alone, because it tells the model the thing did not change —
2203
+ // which after an edit is exactly the question it is asking.
2204
+ //
2205
+ // Only payloads worth the trade are folded. A small `{"success":true}`
2206
+ // ack costs less than the pointer that would replace it, and every
2207
+ // TodoWrite would otherwise collide with every other TodoWrite.
2208
+ const OBSERVATION_FOLD_FLOOR = 600;
2209
+ if (resultString.length >= OBSERVATION_FOLD_FLOOR) {
2210
+ const digest = createHash('sha256').update(resultString).digest('hex');
2211
+ const seenAt = this._observationDigests.get(digest);
2212
+ if (seenAt !== undefined) {
2213
+ resultString = JSON.stringify({
2214
+ success: true,
2215
+ unchanged: true,
2216
+ output: `Identical to the result of step ${seenAt} — nothing has changed since. `
2217
+ + `That earlier result is still in your context; re-read it there rather than asking again.`
2218
+ });
2219
+ } else {
2220
+ this._observationDigests.set(digest, this.stepsCompleted + 1);
2221
+ }
2222
+ }
2223
+
2224
+ this.messages.push({
2225
+ role: 'tool',
2226
+ tool_call_id: toolCall.id,
2227
+ name: functionName,
2228
+ content: resultString,
2229
+ // Internal bookkeeping only: stripped before any provider call
2230
+ // (providers/base.js INTERNAL_MESSAGE_FIELDS). It lets the decay
2231
+ // stub keep pointing at the spill after the payload is gone.
2232
+ ...(spill ? { spillPath: spill.path } : {})
2233
+ });
2234
+ }
2235
+
2236
+ // Save state continuously after each tool loop
2237
+ this.memoryManager.saveHistory(this.messages);
2238
+ }
2239
+
2240
+ } catch (e) {
2241
+ // A real failure. It reaches the user as an `error` frame further down
2242
+ // this same catch; the stack is a diagnostic and stays behind the flag.
2243
+ this.debugLog(`agent loop error: ${e && e.stack ? e.stack : e}`);
2244
+ const errorType = this.classifyError(e);
2245
+
2246
+ if (e.message.includes('NO_BACKEND_URL:')) {
2247
+ this.emitEvent({ type: 'warning', message: e.message.replace('NO_BACKEND_URL: ', '') });
2248
+ break;
2249
+ }
2250
+
2251
+ // V5: Only break on fatal errors. Transient errors get fed back to LLM.
2252
+ if (errorType === 'fatal' || errorType === 'auth_error' || errorType === 'invalid_request_error') {
2253
+ this.emitEvent({ type: 'error', message: e.message });
2254
+ auditLog.log({ event: 'task_error', taskId: this.taskId, error: e.message, errorType });
2255
+
2256
+ break;
2257
+ } else {
2258
+ // A transient error. "Transient" only means the loop MAY continue —
2259
+ // it does not mean it should continue forever.
2260
+ //
2261
+ // This used to retry immediately, with no backoff and no cap, until
2262
+ // the step budget ran out. A sleeping laptop or a wifi blip therefore
2263
+ // produced up to 300 attempts against a dead endpoint, each one
2264
+ // waiting out a full connect timeout, while the status line said
2265
+ // "Thinking" and the only signal was `network_error: fetch failed`
2266
+ // repeated in red. From the outside it is indistinguishable from
2267
+ // working, and it burns the user's step budget doing nothing.
2268
+ const repeated = this.lastFailureType === errorType;
2269
+ this.consecutiveFailures = repeated ? this.consecutiveFailures + 1 : 1;
2270
+ this.lastFailureType = errorType;
2271
+
2272
+ const isNetwork = errorType === NETWORK_ERROR_TYPE;
2273
+ const detail = isNetwork ? explainNetworkError(e, this.config) : e.message;
2274
+ auditLog.log({ event: 'loop_error_recovered', taskId: this.taskId, error: e.message, errorType });
2275
+
2276
+ if (this.consecutiveFailures >= AgentLoop.MAX_CONSECUTIVE_FAILURES) {
2277
+ // Nothing is changing between attempts. Stop and say why, rather
2278
+ // than spinning until the budget is gone.
2279
+ this.emitEvent({
2280
+ type: 'error',
2281
+ message: `${detail}\n\nStopped after ${this.consecutiveFailures} attempts. `
2282
+ + 'Nothing changed between them, so retrying further would only spend your budget.'
2283
+ });
2284
+
2285
+ break;
2286
+ }
2287
+
2288
+ this.emitEvent({
2289
+ type: 'step_error',
2290
+ tool: 'agent_loop',
2291
+ error: `${detail}\n retrying (${this.consecutiveFailures} of `
2292
+ + `${AgentLoop.MAX_CONSECUTIVE_FAILURES})`
2293
+ });
2294
+
2295
+ // Back off before trying again. Hammering a failing endpoint as fast
2296
+ // as the loop can spin turns a one-second blip into a burst of
2297
+ // failures and gives the endpoint no chance to recover.
2298
+ await this.backoff(this.consecutiveFailures);
2299
+
2300
+ // Only tell the MODEL about errors the model can act on. It cannot
2301
+ // fix DNS or a firewall, and a system message per attempt grows the
2302
+ // context — and the bill — for nothing.
2303
+ if (!isNetwork) {
2304
+ this.messages.push({
2305
+ role: 'system',
2306
+ content: `[SYSTEM ERROR RECOVERED: A ${errorType} error occurred: "${e.message}". `
2307
+ + 'The loop has recovered. You may continue or try a different approach.]'
2308
+ });
2309
+ }
2310
+ }
2311
+ }
2312
+ }
2313
+
2314
+ this.emitEvent({ type: 'task_complete', taskId: this.taskId });
2315
+ }
2316
+
2317
+ async waitForApproval(toolCallId) {
2318
+
2319
+ return new Promise((resolve) => {
2320
+ this.pendingApprovals.set(toolCallId, resolve);
2321
+
2322
+ // Safety timeout: If user doesn't respond in 1 hour, auto-deny.
2323
+ setTimeout(() => {
2324
+ if (this.pendingApprovals.has(toolCallId)) {
2325
+ this.handleApprovalResponse(toolCallId, false);
2326
+ }
2327
+ }, 3600000);
2328
+ });
2329
+ }
2330
+
2331
+ /**
2332
+ * Wait before retrying, and stay interruptible while waiting.
2333
+ *
2334
+ * Exponential, capped. Without the abort check a user pressing esc during a
2335
+ * backoff would wait out the full delay before anything responded.
2336
+ */
2337
+ async backoff(attempt) {
2338
+ const delay = Math.min(8000, 500 * Math.pow(2, Math.max(0, attempt - 1)));
2339
+ await new Promise((resolve) => {
2340
+ const timer = setTimeout(resolve, delay);
2341
+ if (timer.unref) timer.unref();
2342
+ const signal = this.abortController && this.abortController.signal;
2343
+ if (signal) {
2344
+ signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
2345
+ }
2346
+ });
2347
+ }
2348
+
2349
+ // V5: Classify errors for intelligent retry decisions
2350
+ classifyError(error) {
2351
+ const msg = (error.message || '').toLowerCase();
2352
+
2353
+ if (msg.includes('syntax') || msg.includes('unexpected token') || msg.includes('invalid json')) {
2354
+ return 'syntax_error';
2355
+ }
2356
+ if (msg.includes('enoent') || msg.includes('no such file') || msg.includes('not found') || msg.includes('does not exist')) {
2357
+ return 'path_not_found';
2358
+ }
2359
+ if (msg.includes('econnrefused') || msg.includes('econnreset') || msg.includes('etimedout') || msg.includes('fetch failed') || msg.includes('network')) {
2360
+ return "network_error";
2361
+ }
2362
+ if (msg.includes('rate limit') || msg.includes('429') || msg.includes('too many requests') || msg.includes('quota')) {
2363
+ return 'rate_limit';
2364
+ }
2365
+ if (msg.includes('eacces') || msg.includes('permission denied') || msg.includes('eperm')) {
2366
+ return 'permission_denied';
2367
+ }
2368
+ if (msg.includes('401') || msg.includes('403') || msg.includes('unauthorized') || msg.includes('api key')) {
2369
+ return 'auth_error';
2370
+ }
2371
+ if (msg.includes('400') || msg.includes('invalid_parameter') || msg.includes('invalid_request') || msg.includes('required property') || msg.includes('bad request')) {
2372
+ return 'invalid_request_error';
2373
+ }
2374
+ if (msg.includes('kill') || msg.includes('abort') || msg.includes('signal') || msg.includes('fatal')) {
2375
+ return 'fatal';
2376
+ }
2377
+
2378
+ return 'unknown_error';
2379
+ }
2380
+
2381
+ // V5: Provide actionable retry hints per error type
2382
+ getRetryHint(errorType) {
2383
+ const hints = {
2384
+ 'syntax_error': 'Check your command syntax or JSON structure. Ensure quotes are balanced.',
2385
+ 'path_not_found': 'The file or directory does not exist. Use fs.list to verify the path first, or create it with fs.write/mkdir.',
2386
+ ["network_error"]: "Network connectivity issue. Wait a moment and retry. If persistent, check if the URL is correct.",
2387
+ 'rate_limit': 'API rate limit hit. Wait 5-10 seconds before retrying.',
2388
+ 'permission_denied': 'Permission denied. Check if the path is within allowed workspace paths. Try a different directory.',
2389
+ 'auth_error': 'Authentication failed. Do NOT retry — the API key may be invalid.',
2390
+ 'invalid_request_error': 'API request format error (400 Bad Request). Do NOT retry — check model parameter compliance.',
2391
+ 'unknown_error': 'An unexpected error occurred. Try a different approach entirely.'
2392
+ };
2393
+ return hints[errorType] || hints['unknown_error'];
2394
+ }
2395
+
2396
+ // V5: Validate tool call arguments before execution
2397
+ /**
2398
+ * PreToolUse / PostToolUse seams (Stage 3 fills these in).
2399
+ *
2400
+ * Today they are no-ops that always allow, so the dispatch behaviour is exactly
2401
+ * what it was. When `.causyne/settings.json` gains a `hooks` block (Stage 3),
2402
+ * these run the user's shell hooks with the exit-code protocol
2403
+ * (0 allow / 2 deny / else warn) and return { denied, messages }. The loop
2404
+ * already branches on `denied`, so Stage 3 is a fill-in here, not another edit
2405
+ * to the dispatch path. See src/tools/toolDispatch.js for the discipline these
2406
+ * plug into.
2407
+ */
2408
+ async runPreToolUse(_toolName, _args, _toolCallId) {
2409
+ return { denied: false, messages: [] };
2410
+ }
2411
+
2412
+ async runPostToolUse(_toolName, _args, _result) {
2413
+ return { denied: false, messages: [] };
2414
+ }
2415
+
2416
+ validateToolArgs(toolName, args) {
2417
+ // Check for common LLM mistakes
2418
+
2419
+ // 1. File operations must have absolute paths
2420
+ const pathFields = ['path', 'source', 'destination'];
2421
+ for (const field of pathFields) {
2422
+ if (args[field] && typeof args[field] === 'string') {
2423
+ // Auto-fix relative paths by prepending cwd
2424
+ if (!args[field].startsWith('/') && !args[field].startsWith('~')) {
2425
+ args[field] = `${process.cwd()}/${args[field]}`;
2426
+ }
2427
+ }
2428
+ }
2429
+
2430
+ // 2. Terminal commands must have a command string
2431
+ if ((toolName === 'sysRun' || toolName === 'sysRunBg') && !args.command) {
2432
+ return 'Missing required "command" argument';
2433
+ }
2434
+
2435
+ // 3. File write must have a path AND content.
2436
+ //
2437
+ // These branches were dead: canonicalToolName() (called on every tool name
2438
+ // before this runs) normalises '.'→'_', so `toolName` is 'fs_write', never
2439
+ // 'fs.write'. The old 'fs.write' / 'codebase.search' / 'web.search' literals
2440
+ // could never match, so the model's missing-argument mistakes reached the
2441
+ // tool as runtime exceptions instead of a corrective message. Matched on the
2442
+ // canonical names now, and fs_write's `content` is actually checked (the old
2443
+ // comment said "must have content" but the code only looked at `path`).
2444
+ if (toolName === 'fs_write' && !args.path) {
2445
+ return 'Missing required "path" argument';
2446
+ }
2447
+ if (toolName === 'fs_write' && args.content === undefined) {
2448
+ return 'Missing required "content" argument';
2449
+ }
2450
+
2451
+ // 4. Search must have a query
2452
+ if ((toolName === 'codebase_search' || toolName === 'web_search') && !args.query) {
2453
+ return 'Missing required "query" argument';
2454
+ }
2455
+
2456
+ return null; // No validation errors
2457
+ }
2458
+ }