@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,145 @@
1
+ import { UTILITY_MODEL_BY_PROVIDER } from './modelCatalogue.js';
2
+
3
+ // Hybrid model routing.
4
+ //
5
+ // Not every call an agent makes is reasoning. Condensing a dropped transcript,
6
+ // classifying an intent, filtering a log, extracting keywords — these are
7
+ // utility work, and paying a flagship rate for them is pure waste. The research
8
+ // spec puts the saving at the difference between the two rate cards, which for
9
+ // OpenAI is $5/$30 against $0.75/$4.50: roughly 6x on work that never needed
10
+ // the better model.
11
+ //
12
+ // THREE RULES THIS MODULE EXISTS TO ENFORCE, all of them things a naive router
13
+ // gets wrong:
14
+ //
15
+ // 1. NEVER CHANGE PROVIDER. Causyne is BYOK. The user holds one key. A router
16
+ // that sends utility work to "whatever is cheapest on the market" calls an
17
+ // endpoint there is no credential for and the task fails — having spent
18
+ // the tokens that got it there.
19
+ //
20
+ // 2. NEVER CHANGE THE PRIVACY POSTURE. Muse's cheap tier is `-contributor`,
21
+ // discounted because the vendor trains on the content. Routing into it to
22
+ // save money would opt the user into training silently. Cost is not a
23
+ // reason to make that choice for someone.
24
+ //
25
+ // 3. NEVER GUESS. A provider with no published second tier routes to nothing
26
+ // and pays full rate. An invented model id becomes a 404 mid-task, or
27
+ // worse, a real model at a price nobody predicted.
28
+ //
29
+ // The router returns a DESCRIPTION of where a call should go. It never
30
+ // constructs a provider and never performs a call, so it stays trivially
31
+ // testable and cannot become a second place where credentials are handled.
32
+
33
+ /**
34
+ * What a call is for.
35
+ *
36
+ * `reasoning` — planning, code, architecture, tool selection, anything the
37
+ * user's result quality depends on. Always the configured model.
38
+ * `utility` — mechanical text work whose output is consumed by the machine
39
+ * rather than by the user's judgement.
40
+ *
41
+ * @typedef {'reasoning' | 'utility'} RouteClass
42
+ */
43
+
44
+ /**
45
+ * @typedef {Object} Route
46
+ * @property {string} provider The provider to call. Always the configured one.
47
+ * @property {string} model The model id to request.
48
+ * @property {boolean} routed True when this differs from the configured model.
49
+ * @property {string} reason Why, in one line. Written to the audit trail.
50
+ */
51
+
52
+ /**
53
+ * Utility work that is cheap to get wrong and expensive to run on a flagship.
54
+ * Anything not listed is treated as reasoning — the safe default is to spend
55
+ * more, never to silently downgrade work the user is judging.
56
+ *
57
+ * @type {ReadonlySet<string>}
58
+ */
59
+ export const UTILITY_KINDS = new Set([
60
+ 'condense', // cortex/autoCondenser semantic pass
61
+ 'keywords', // plan-cache intent extraction
62
+ 'classify', // intent / archetype disambiguation
63
+ 'filter', // log and output filtering
64
+ 'summarise' // non-decision summaries
65
+ ]);
66
+
67
+ /**
68
+ * Decide where a call of a given kind should go.
69
+ *
70
+ * @param {{provider?: string, model?: string, modelRouting?: Record<string, string>, disableModelRouting?: boolean}} config
71
+ * The live agent config, as loaded from `~/.causyne/config.json`.
72
+ * @param {RouteClass|string} kind
73
+ * Either a RouteClass or one of UTILITY_KINDS.
74
+ * @returns {Route}
75
+ */
76
+ export function resolveRoute(config, kind) {
77
+ const provider = String(config?.provider || '').toLowerCase();
78
+ const configured = String(config?.model || '');
79
+
80
+ /** @type {Route} */
81
+ const stay = {
82
+ provider,
83
+ model: configured,
84
+ routed: false,
85
+ reason: 'reasoning work runs on the configured model'
86
+ };
87
+
88
+ if (!provider || !configured) {
89
+ return { ...stay, reason: 'no provider or model configured' };
90
+ }
91
+
92
+ const isUtility = kind === 'utility' || UTILITY_KINDS.has(String(kind));
93
+ if (!isUtility) return stay;
94
+
95
+ if (config?.disableModelRouting === true) {
96
+ return { ...stay, reason: 'model routing disabled by config' };
97
+ }
98
+
99
+ // An explicit choice by the user outranks the table, including a deliberate
100
+ // choice of a training tier — they made it, we did not make it for them.
101
+ const override = config?.modelRouting?.utility;
102
+ if (override) {
103
+ return {
104
+ provider,
105
+ model: String(override),
106
+ routed: String(override) !== configured,
107
+ reason: 'utility route set explicitly in config'
108
+ };
109
+ }
110
+
111
+ const cheap = UTILITY_MODEL_BY_PROVIDER[provider];
112
+ if (!cheap) {
113
+ return { ...stay, reason: `no published utility tier for ${provider}; paying full rate` };
114
+ }
115
+ if (cheap === configured) {
116
+ return { ...stay, reason: 'already on this provider\'s utility tier' };
117
+ }
118
+
119
+ // Rule 2. The configured model already carries a privacy posture; a route may
120
+ // not alter it in either direction.
121
+ if (/-contributor$/.test(configured) !== /-contributor$/.test(cheap)) {
122
+ return { ...stay, reason: 'a route may not change the training-data posture' };
123
+ }
124
+
125
+ return {
126
+ provider,
127
+ model: cheap,
128
+ routed: true,
129
+ reason: `utility work routed to ${provider}'s cheaper tier`
130
+ };
131
+ }
132
+
133
+ /**
134
+ * A config object for the routed call, leaving everything except the model
135
+ * untouched — same provider, same key, same base URL.
136
+ *
137
+ * @param {Record<string, any>} config
138
+ * @param {RouteClass|string} kind
139
+ * @returns {{config: Record<string, any>, route: Route}}
140
+ */
141
+ export function routedConfig(config, kind) {
142
+ const route = resolveRoute(config, kind);
143
+ if (!route.routed) return { config, route };
144
+ return { config: { ...config, model: route.model }, route };
145
+ }
@@ -0,0 +1,246 @@
1
+ // Sovereign Tool-Output Store
2
+ //
3
+ // WHY THIS EXISTS
4
+ // An oversized tool result used to be truncated to 32k chars and the remainder
5
+ // DESTROYED. The only route back to the missing part was to run the tool again:
6
+ // a second full execution, plus a second full-size result in the window. The
7
+ // decay stub in contextManager.js said so out loud — "Re-run the tool".
8
+ //
9
+ // Now the complete output is spilled to a file under ~/.causyne/outputs and the
10
+ // model is handed the path, so recovery is a bounded fs_read of a line range or
11
+ // a code_search_ripgrep over one file. A re-run costs a whole tool execution;
12
+ // a targeted read costs a few hundred tokens.
13
+ //
14
+ // ZERO-MALWARE: pure fs/path/os. No shell, no child process, no dependency, no
15
+ // dynamic eval — rules ZM-04/ZM-05 in .agents/rules.json hold by construction.
16
+ // Nothing here is obfuscated; it is plain readable code (Vision section 10).
17
+
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+ import os from 'os';
21
+ import { causyneHome } from './utils/paths.js';
22
+
23
+ const OUTPUT_DIR = path.join(causyneHome(), 'outputs');
24
+
25
+ // Matches the audit log's retention thinking: long enough that a spill is still
26
+ // there when the model comes back for it later in a long task, short enough
27
+ // that the directory cannot grow without bound.
28
+ const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
29
+
30
+ const FILE_PREFIX = 'tool_';
31
+
32
+ // How much of the spilled output travels back in the context window. The rest
33
+ // stays on disk behind the path.
34
+ const PREVIEW_LINES = 40;
35
+ const PREVIEW_CHARS = 4000;
36
+
37
+ /**
38
+ * The spill can be switched off entirely. Set CAUSYNE_OUTPUT_SPILL=0 to fall
39
+ * back to plain truncation.
40
+ */
41
+ export function isSpillEnabled() {
42
+ return process.env.CAUSYNE_OUTPUT_SPILL !== '0';
43
+ }
44
+
45
+ function ensureOutputDir() {
46
+ if (!fs.existsSync(OUTPUT_DIR)) {
47
+ // Owner-only: tool output routinely contains file contents and command
48
+ // output from the user's machine.
49
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true, mode: 0o700 });
50
+ }
51
+ }
52
+
53
+
54
+ function safeFragment(value, fallback) {
55
+ const cleaned = String(value == null ? '' : value).replace(/[^a-zA-Z0-9_-]/g, '');
56
+ return cleaned.length > 0 ? cleaned.slice(0, 64) : fallback;
57
+ }
58
+
59
+ function countLines(text) {
60
+ let lines = 1;
61
+ for (let i = 0; i < text.length; i++) {
62
+ if (text.charCodeAt(i) === 10) lines++;
63
+ }
64
+ return lines;
65
+ }
66
+
67
+ /**
68
+ * Take the head of `text` without splitting the whole string into an array —
69
+ * these payloads reach tens of megabytes and `split('\n')` on one of those is
70
+ * itself a memory problem.
71
+ */
72
+ function headPreview(text) {
73
+ const charLimit = Math.min(text.length, PREVIEW_CHARS);
74
+ let cut = charLimit;
75
+ let seen = 0;
76
+ for (let i = 0; i < charLimit; i++) {
77
+ if (text.charCodeAt(i) === 10) {
78
+ seen++;
79
+ if (seen >= PREVIEW_LINES) { cut = i; break; }
80
+ }
81
+ }
82
+ return text.slice(0, cut);
83
+ }
84
+
85
+ /**
86
+ * Write the full tool output to disk.
87
+ *
88
+ * FAIL-OPEN BY CONTRACT: never throws and never blocks the tool call. A
89
+ * read-only disk or a full volume degrades this feature back to plain
90
+ * truncation, it does not fail the task.
91
+ *
92
+ * @returns {{path: string, bytes: number, lines: number, preview: string}|null}
93
+ */
94
+ export function spillToolOutput(text, meta = {}) {
95
+ if (!isSpillEnabled()) return null;
96
+ if (typeof text !== 'string' || text.length === 0) return null;
97
+
98
+ try {
99
+ ensureOutputDir();
100
+
101
+ const taskId = safeFragment(meta.taskId, 'task');
102
+ const step = safeFragment(meta.step, '0');
103
+ const tool = safeFragment(meta.tool, 'tool');
104
+ const stamp = Date.now().toString(36);
105
+ const file = path.join(OUTPUT_DIR, `${FILE_PREFIX}${taskId}_${step}_${tool}_${stamp}.txt`);
106
+
107
+ fs.writeFileSync(file, text, { encoding: 'utf8', mode: 0o600 });
108
+
109
+ return {
110
+ path: file,
111
+ bytes: Buffer.byteLength(text, 'utf8'),
112
+ lines: countLines(text),
113
+ preview: headPreview(text)
114
+ };
115
+ } catch (e) {
116
+ console.warn(`[output-store] spill skipped: ${e.message}`);
117
+ return null;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Render a tool result into a document that is actually READABLE by the tools
123
+ * we tell the model to use on it.
124
+ *
125
+ * Spilling `JSON.stringify(result)` was the obvious move and the wrong one: JSON
126
+ * escapes every newline, so a 900 KB result lands as ONE line. `fs_read` with a
127
+ * line range and `code_search_ripgrep` are both line-oriented, so the notice
128
+ * would have been pointing at a file neither of them could work with. Writing
129
+ * the raw field values keeps the line structure the payload actually had.
130
+ */
131
+ export function renderSpillDocument(result, meta = {}) {
132
+ if (typeof result === 'string') return result;
133
+ if (result === null || typeof result !== 'object') return String(result);
134
+
135
+ const header = [
136
+ '=== CAUSYNE TOOL OUTPUT (full, untruncated) ===',
137
+ `tool : ${meta.tool || 'unknown'}`,
138
+ `task : ${meta.taskId || 'unknown'} step: ${meta.step == null ? 'unknown' : meta.step}`,
139
+ `saved: ${new Date().toISOString()}`
140
+ ];
141
+
142
+ const sections = [];
143
+ const scalars = [];
144
+
145
+ for (const [key, value] of Object.entries(result)) {
146
+ if (typeof value === 'string' && value.length > 500) {
147
+ // Raw, unescaped — this is the whole point.
148
+ sections.push(`\n--- field: ${key} (${value.length} chars) ---\n${value}`);
149
+ } else if (value !== null && typeof value === 'object') {
150
+ sections.push(`\n--- field: ${key} (JSON) ---\n${JSON.stringify(value, null, 2)}`);
151
+ } else {
152
+ scalars.push(`${key}: ${JSON.stringify(value)}`);
153
+ }
154
+ }
155
+
156
+ if (scalars.length > 0) header.push('', ...scalars);
157
+ return header.join('\n') + '\n' + sections.join('\n') + '\n';
158
+ }
159
+
160
+ /**
161
+ * Spill a full tool RESULT (object or string) as a readable document.
162
+ * This is what the agent loop calls; spillToolOutput stays the raw primitive.
163
+ */
164
+ export function spillToolResult(result, meta = {}) {
165
+ return spillToolOutput(renderSpillDocument(result, meta), meta);
166
+ }
167
+
168
+ /**
169
+ * The line the model reads in place of the bytes it did not get. It names the
170
+ * exact recovery route, because a notice that only says "truncated" is what
171
+ * drives the model back into a re-run.
172
+ */
173
+ export function buildSpillNotice(spill) {
174
+ if (!spill) {
175
+ return '\n\n[... OUTPUT TRUNCATED TO PROTECT THE CONTEXT WINDOW. Narrow your arguments and retry if you need more ...]';
176
+ }
177
+ const kb = (spill.bytes / 1024).toFixed(1);
178
+ return `\n\n[... OUTPUT TRUNCATED HERE (${kb} KB, ${spill.lines} lines total). `
179
+ + `THE COMPLETE OUTPUT IS SAVED AT: ${spill.path} — `
180
+ + `read a line range from that file with fs_read, or search it with code_search_ripgrep. `
181
+ + `DO NOT re-run the tool just to see the rest ...]`;
182
+ }
183
+
184
+ /** The one-line form the decay stub uses once the payload leaves the window. */
185
+ export function buildDecayPointer(spill) {
186
+ if (!spill || !spill.path) return '';
187
+ return ` The full output is still on disk at ${spill.path} — read or search that file instead of re-running the tool.`;
188
+ }
189
+
190
+ /**
191
+ * Delete spills past the retention window. Called fire-and-forget when a task
192
+ * starts, so the store self-maintains with no background timer and no installer
193
+ * behaviour (rule ZM-01).
194
+ *
195
+ * @returns {number} files removed
196
+ */
197
+ export function pruneOutputStore() {
198
+ try {
199
+ if (!fs.existsSync(OUTPUT_DIR)) return 0;
200
+ const cutoff = Date.now() - RETENTION_MS;
201
+ let removed = 0;
202
+
203
+ for (const name of fs.readdirSync(OUTPUT_DIR)) {
204
+ if (!name.startsWith(FILE_PREFIX)) continue;
205
+ const file = path.join(OUTPUT_DIR, name);
206
+ try {
207
+ if (fs.statSync(file).mtimeMs < cutoff) {
208
+ fs.unlinkSync(file);
209
+ removed++;
210
+ }
211
+ } catch (e) { /* a file that vanished under us needs no pruning */ }
212
+ }
213
+ return removed;
214
+ } catch (e) {
215
+ console.warn(`[output-store] prune skipped: ${e.message}`);
216
+ return 0;
217
+ }
218
+ }
219
+
220
+ /** Absolute path of the store, for the `causyne-agent context` surface. */
221
+ export function getOutputDir() {
222
+ return OUTPUT_DIR;
223
+ }
224
+
225
+ /** Current store contents, newest first — used by the CLI reporting surface. */
226
+ export function listSpills(limit = 20) {
227
+ try {
228
+ if (!fs.existsSync(OUTPUT_DIR)) return [];
229
+ return fs.readdirSync(OUTPUT_DIR)
230
+ .filter(name => name.startsWith(FILE_PREFIX))
231
+ .map(name => {
232
+ const file = path.join(OUTPUT_DIR, name);
233
+ try {
234
+ const stats = fs.statSync(file);
235
+ return { path: file, bytes: stats.size, modified: stats.mtimeMs };
236
+ } catch (e) {
237
+ return null;
238
+ }
239
+ })
240
+ .filter(Boolean)
241
+ .sort((a, b) => b.modified - a.modified)
242
+ .slice(0, limit);
243
+ } catch (e) {
244
+ return [];
245
+ }
246
+ }
@@ -0,0 +1,124 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ export class WorkspacePerception {
6
+ async buildContext(workspacePath) {
7
+ const context = {};
8
+ const resolvedPath = workspacePath.replace(/^~\//, os.homedir() + '/');
9
+
10
+ // 1. Directory structure (tree, max depth 2 to keep context size manageable)
11
+ context.structure = await this.getDirectoryTree(resolvedPath, 2);
12
+
13
+ // 2. Framework detection
14
+ context.framework = await this.detectFramework(resolvedPath);
15
+
16
+ // 2b. Design system detection (Anti-Slop: tells the brain what's already installed)
17
+ context.designSystem = await this.detectDesignContext(resolvedPath);
18
+
19
+ // 3. Package/dependency context
20
+ try {
21
+ const pkgPath = path.join(resolvedPath, 'package.json');
22
+ const pkgStats = await fs.stat(pkgPath);
23
+ if (pkgStats.isFile()) {
24
+ const pkgData = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
25
+ context.packageJson = {
26
+ name: pkgData.name,
27
+ dependencies: pkgData.dependencies || {},
28
+ devDependencies: pkgData.devDependencies || {},
29
+ scripts: pkgData.scripts || {}
30
+ };
31
+ }
32
+ } catch(e) {}
33
+
34
+ // 4. README (first 1000 chars)
35
+ try {
36
+ const readmePath = path.join(resolvedPath, 'README.md');
37
+ const readmeContent = await fs.readFile(readmePath, 'utf8');
38
+ context.readme = readmeContent.slice(0, 1000) + (readmeContent.length > 1000 ? '...' : '');
39
+ } catch(e) {}
40
+
41
+ // 5. OS & environment
42
+ context.os = { platform: os.platform(), arch: os.arch() };
43
+
44
+ return context;
45
+ }
46
+
47
+ async getDirectoryTree(dir, maxDepth, currentDepth = 0) {
48
+ if (currentDepth > maxDepth) return '... (max depth reached)';
49
+
50
+ try {
51
+ const items = await fs.readdir(dir, { withFileTypes: true });
52
+ const tree = {};
53
+
54
+ let count = 0;
55
+ for (const item of items) {
56
+ if (item.name.startsWith('.') || item.name === 'node_modules') continue;
57
+
58
+ if (count >= 30) {
59
+ tree['...'] = `(${items.length - count} more items hidden)`;
60
+ break;
61
+ }
62
+
63
+ count++;
64
+ if (item.isDirectory()) {
65
+ tree[item.name] = await this.getDirectoryTree(path.join(dir, item.name), maxDepth, currentDepth + 1);
66
+ } else {
67
+ tree[item.name] = 'file';
68
+ }
69
+ }
70
+ return tree;
71
+ } catch (e) {
72
+ return `Error: ${e.message}`;
73
+ }
74
+ }
75
+
76
+ async detectFramework(dir) {
77
+ const detectors = [
78
+ { file: 'next.config.js', type: 'nextjs', lang: 'javascript' },
79
+ { file: 'next.config.ts', type: 'nextjs', lang: 'typescript' },
80
+ { file: 'vite.config.ts', type: 'vite', lang: 'typescript' },
81
+ { file: 'tsconfig.json', type: 'node', lang: 'typescript' },
82
+ { file: 'package.json', type: 'node', lang: 'javascript' },
83
+ { file: 'manage.py', type: 'django', lang: 'python' },
84
+ { file: 'requirements.txt', type: 'python', lang: 'python' },
85
+ { file: 'build.gradle.kts', type: 'android', lang: 'kotlin' },
86
+ { file: 'docker-compose.yml', type: 'docker', lang: 'various' },
87
+ ];
88
+
89
+ for (const d of detectors) {
90
+ try {
91
+ await fs.access(path.join(dir, d.file));
92
+ return { type: d.type, language: d.lang };
93
+ } catch (e) {}
94
+ }
95
+
96
+ return { type: 'unknown', language: 'unknown' };
97
+ }
98
+
99
+ async detectDesignContext(dir) {
100
+ const designDetectors = [
101
+ { dep: 'tailwindcss', system: 'Tailwind CSS' },
102
+ { dep: '@astryxdesign/core', system: 'Astryx Design System' },
103
+ { dep: '@mui/material', system: 'Material-UI (MUI)' },
104
+ { dep: '@chakra-ui/react', system: 'Chakra UI' },
105
+ { dep: 'bootstrap', system: 'Bootstrap' },
106
+ { dep: 'styled-components', system: 'Styled Components' },
107
+ { dep: '@emotion/react', system: 'Emotion CSS-in-JS' },
108
+ ];
109
+
110
+ try {
111
+ const pkgPath = path.join(dir, 'package.json');
112
+ const pkgData = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
113
+ const allDeps = { ...pkgData.dependencies, ...pkgData.devDependencies };
114
+
115
+ const detected = designDetectors
116
+ .filter(d => allDeps[d.dep])
117
+ .map(d => d.system);
118
+
119
+ return detected.length > 0 ? detected : null;
120
+ } catch (e) {
121
+ return null;
122
+ }
123
+ }
124
+ }