@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,378 @@
1
+ // Shadow-git checkpoint backend.
2
+ //
3
+ // The file-copy store (checkpoints.js) works, but it copies bytes and therefore
4
+ // has to cap what it will look at — MAX_TREE_FILES is 5000, and a real home
5
+ // directory blows past that, which is how a user ended up with a workspace root
6
+ // silently marked `too_many_files` and NOT covered by any rollback.
7
+ //
8
+ // Git already solves this problem properly, so this backend borrows opencode's
9
+ // design (packages/core/src/snapshot.ts):
10
+ //
11
+ // * The repository lives in ~/.causyne/snapshot/<project>/<hash(worktree)>.
12
+ // The user's own .git is NEVER touched — no stash, no commit, no index
13
+ // mutation, no HEAD movement. Their history is theirs.
14
+ // * objects/info/alternates points at the user's object database, and their
15
+ // index is copied in, so blobs the project already stores are never
16
+ // re-hashed. This is what makes a checkpoint instant on a large tree
17
+ // instead of a minutes-long walk.
18
+ // * A checkpoint id IS a `write-tree` SHA. No commits, no refs, nothing to
19
+ // garbage collect by hand.
20
+ //
21
+ // ZERO-MALWARE: every git invocation goes through runGit(), which uses execFile
22
+ // with an ARRAY of arguments. There is no shell anywhere in this file, and no
23
+ // argument is ever built by string concatenation, so there is nothing for an
24
+ // injected filename to escape into. This is the same hardening applied to
25
+ // printer.js in v1.1.44 after its exec() shell-injection hole.
26
+
27
+ import { execFileSync, execFile } from 'child_process';
28
+ import { promisify } from 'util';
29
+ import fs from 'fs';
30
+ import os from 'os';
31
+ import path from 'path';
32
+ import crypto from 'crypto';
33
+ import { causyneHome } from './utils/paths.js';
34
+
35
+ /** A git call that hangs must not hang a task. */
36
+ const GIT_TIMEOUT_MS = 20000;
37
+
38
+ /** Untracked files above this are excluded rather than stored (opencode uses 2 MB). */
39
+ const MAX_UNTRACKED_BYTES = 2 * 1024 * 1024;
40
+
41
+ /**
42
+ * Run git.
43
+ *
44
+ * `args` is an ARRAY — execFile passes it straight to execve, so no shell ever
45
+ * parses it and a path containing a space, a quote or `$(...)` is just a path.
46
+ * Never change this to exec() or add `shell: true`.
47
+ */
48
+ function runGit(args, options = {}) {
49
+ if (!Array.isArray(args) || args.some((a) => typeof a !== 'string')) {
50
+ throw new Error('git arguments must be an array of strings');
51
+ }
52
+ // Synchronous on purpose. Everything in checkpoints.js is synchronous, and a
53
+ // checkpoint MUST complete before the agent is allowed to touch a file — an
54
+ // asynchronous capture would race the task's own writes and snapshot a state
55
+ // that already includes the edits it is supposed to protect against. The cost
56
+ // is nothing next to what this replaces: the native store walks and hashes
57
+ // every file in the tree, where git reuses its index and writes one object.
58
+ return execFileSync('git', args, {
59
+ encoding: 'utf8',
60
+ timeout: GIT_TIMEOUT_MS,
61
+ maxBuffer: 32 * 1024 * 1024,
62
+ windowsHide: true,
63
+ stdio: ['ignore', 'pipe', 'ignore'],
64
+ ...options
65
+ });
66
+ }
67
+
68
+ const execFileAsync = promisify(execFile);
69
+
70
+
71
+ async function runGitAsync(args, options = {}) {
72
+ if (!Array.isArray(args) || args.some((a) => typeof a !== 'string')) {
73
+ throw new Error('git arguments must be an array of strings');
74
+ }
75
+ const { stdout } = await execFileAsync('git', args, {
76
+ encoding: 'utf8',
77
+ timeout: GIT_TIMEOUT_MS,
78
+ maxBuffer: 32 * 1024 * 1024,
79
+ windowsHide: true,
80
+ ...options
81
+ });
82
+ return stdout;
83
+ }
84
+
85
+ /**
86
+ * Capture without blocking — the form the task hot path must use.
87
+ *
88
+ * beginTask runs at the START of every task, so it is the one capture that can
89
+ * never be synchronous. Preview and restore stay
90
+ * synchronous: they run only when the user has explicitly asked for an undo and
91
+ * is waiting for the answer.
92
+ */
93
+ export async function captureTreeAsync(worktree) {
94
+ try {
95
+ const gitDir = initShadow(worktree);
96
+ excludeOversizedUntracked(gitDir, worktree);
97
+ await runGitAsync(
98
+ ['--git-dir', gitDir, '--work-tree', worktree, 'add', '--all'],
99
+ { cwd: worktree }
100
+ );
101
+ const sha = (await runGitAsync(['--git-dir', gitDir, 'write-tree'])).trim();
102
+ return sha || null;
103
+ } catch (e) {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ /** Is a usable git binary on PATH? Cached — the answer cannot change mid-run. */
109
+ let gitAvailable = null;
110
+ export function isGitAvailable() {
111
+ if (gitAvailable !== null) return gitAvailable;
112
+ try {
113
+ runGit(['--version']);
114
+ gitAvailable = true;
115
+ } catch (e) {
116
+ gitAvailable = false;
117
+ }
118
+ return gitAvailable;
119
+ }
120
+
121
+ /** The worktree root for `dir`, or null when it is not inside a git repository. */
122
+ export function resolveWorktree(dir) {
123
+ try {
124
+ const out = runGit(['rev-parse', '--show-toplevel'], { cwd: dir });
125
+ const root = out.trim();
126
+ return root ? path.resolve(root) : null;
127
+ } catch (e) {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ /** Where this worktree's shadow repository lives. */
133
+ export function shadowDirFor(worktree) {
134
+ const hash = crypto.createHash('sha256').update(path.resolve(worktree)).digest('hex').slice(0, 16);
135
+ return path.join(causyneHome(), 'snapshot', path.basename(worktree), hash);
136
+ }
137
+
138
+ /**
139
+ * Create (or reuse) the shadow repository for a worktree.
140
+ *
141
+ * Returns the --git-dir path. Safe to call repeatedly.
142
+ */
143
+ export function initShadow(worktree) {
144
+ const gitDir = shadowDirFor(worktree);
145
+ const alreadyInitialised = fs.existsSync(path.join(gitDir, 'HEAD'));
146
+
147
+ if (!alreadyInitialised) {
148
+ fs.mkdirSync(gitDir, { recursive: true });
149
+ runGit(['--git-dir', gitDir, 'init', '--bare', '--quiet']);
150
+
151
+ // Tuning that matters on large trees, applied once at creation.
152
+ for (const [key, value] of [
153
+ ['feature.manyFiles', 'true'],
154
+ ['index.version', '4'],
155
+ ['index.threads', 'true'],
156
+ ['core.untrackedCache', 'true']
157
+ ]) {
158
+ try {
159
+ runGit(['--git-dir', gitDir, 'config', key, value]);
160
+ } catch (e) {
161
+ // Tuning is an optimisation; an older git that rejects a key must not
162
+ // stop the checkpoint from being taken.
163
+ }
164
+ }
165
+ }
166
+
167
+ // Point at the user's object database so blobs are never re-hashed, and adopt
168
+ // their index so already-hashed entries are reused. Both are best-effort: a
169
+ // missing or incompatible source just means a full, slower add.
170
+ const sourceObjects = path.join(worktree, '.git', 'objects');
171
+ if (fs.existsSync(sourceObjects)) {
172
+ try {
173
+ const infoDir = path.join(gitDir, 'objects', 'info');
174
+ fs.mkdirSync(infoDir, { recursive: true });
175
+ fs.writeFileSync(path.join(infoDir, 'alternates'), `${sourceObjects}\n`, 'utf8');
176
+ } catch (e) {}
177
+ }
178
+
179
+ if (!alreadyInitialised) {
180
+ const sourceIndex = path.join(worktree, '.git', 'index');
181
+ if (fs.existsSync(sourceIndex)) {
182
+ try {
183
+ fs.copyFileSync(sourceIndex, path.join(gitDir, 'index'));
184
+ } catch (e) {}
185
+ }
186
+ }
187
+
188
+ return gitDir;
189
+ }
190
+
191
+ /**
192
+ * Exclude untracked files too large to be worth storing.
193
+ *
194
+ * Written into the SHADOW repo's info/exclude, never the user's .gitignore.
195
+ */
196
+ function excludeOversizedUntracked(gitDir, worktree) {
197
+ let listing = '';
198
+ try {
199
+ listing = runGit(
200
+ ['--git-dir', gitDir, '--work-tree', worktree, 'ls-files', '--others', '--exclude-standard', '-z'],
201
+ { cwd: worktree }
202
+ );
203
+ } catch (e) {
204
+ return;
205
+ }
206
+
207
+ const oversized = [];
208
+ for (const rel of listing.split('\0')) {
209
+ if (!rel) continue;
210
+ try {
211
+ const stat = fs.statSync(path.join(worktree, rel));
212
+ if (stat.isFile() && stat.size > MAX_UNTRACKED_BYTES) oversized.push(rel);
213
+ } catch (e) {}
214
+ }
215
+ if (oversized.length === 0) return;
216
+
217
+ try {
218
+ const infoDir = path.join(gitDir, 'info');
219
+ fs.mkdirSync(infoDir, { recursive: true });
220
+ fs.writeFileSync(path.join(infoDir, 'exclude'), `${oversized.join('\n')}\n`, 'utf8');
221
+ } catch (e) {}
222
+ }
223
+
224
+ /**
225
+ * Capture the worktree's current state.
226
+ *
227
+ * @returns {string|null} a tree SHA, or null if the capture failed —
228
+ * a checkpoint is best-effort and must never fail the task it belongs to.
229
+ */
230
+ export function captureTree(worktree) {
231
+ try {
232
+ const gitDir = initShadow(worktree);
233
+ excludeOversizedUntracked(gitDir, worktree);
234
+
235
+ // The user's ignore rules decide what is worth keeping. --no-index keeps it
236
+ // pattern-based rather than dependent on what their index happens to track.
237
+ // NO --force here, deliberately.
238
+ //
239
+ // `--force` means "add otherwise ignored files". It was doing exactly that:
240
+ // node_modules, build outputs, caches — everything .gitignore exists to keep
241
+ // out. The first capture of this repository took 23 seconds and stalled task
242
+ // startup. The user's own ignore rules are the correct definition of "files
243
+ // worth being able to restore".
244
+ runGit(['--git-dir', gitDir, '--work-tree', worktree, 'add', '--all'], { cwd: worktree });
245
+
246
+ const sha = (runGit(['--git-dir', gitDir, 'write-tree'])).trim();
247
+ return sha || null;
248
+ } catch (e) {
249
+ return null;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * What restoring `files` would change — WITHOUT touching the worktree.
255
+ *
256
+ * The technique is opencode's (packages/core/src/git.ts), which built it and
257
+ * never wired it to a UI: point GIT_INDEX_FILE at a throwaway index, read-tree
258
+ * the current state into it, update-index the planned blobs over the top,
259
+ * write-tree that hypothetical, and diff it against now. Nothing anywhere near
260
+ * the user's files is written, and the temp index is removed in a finally.
261
+ *
262
+ * @param {string} worktree
263
+ * @param {string} fromTree tree SHA to restore from
264
+ * @returns {Array<{path,status,additions,deletions,patch}>}
265
+ */
266
+ export function previewRestore(worktree, fromTree) {
267
+ const gitDir = initShadow(worktree);
268
+ const tempIndex = path.join(gitDir, `preview-${crypto.randomUUID()}.index`);
269
+ const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
270
+
271
+ try {
272
+ const current = captureTree(worktree);
273
+ if (!current) return [];
274
+
275
+ runGit(['--git-dir', gitDir, 'read-tree', current], { env });
276
+ const raw = runGit(
277
+ ['--git-dir', gitDir, 'diff', '--numstat', '--find-renames', current, fromTree],
278
+ { env }
279
+ );
280
+
281
+ const changes = [];
282
+ for (const line of raw.split('\n')) {
283
+ if (!line.trim()) continue;
284
+ const [added, removed, file] = line.split('\t');
285
+ if (!file) continue;
286
+
287
+ let patch = '';
288
+ try {
289
+ patch = runGit(
290
+ ['--git-dir', gitDir, 'diff', '--unified=3', current, fromTree, '--', file],
291
+ { env }
292
+ );
293
+ } catch (e) {
294
+ // A binary file has no textual patch; the counts still describe it.
295
+ }
296
+
297
+ changes.push({
298
+ path: file,
299
+ // Restoring runs backwards: a file absent from the OLD tree was created
300
+ // by the task and would be deleted, and vice versa.
301
+ status: added === '-' && removed === '-' ? 'binary' : classify(added, removed),
302
+ additions: added === '-' ? 0 : Number(added) || 0,
303
+ deletions: removed === '-' ? 0 : Number(removed) || 0,
304
+ patch
305
+ });
306
+ }
307
+ return changes;
308
+ } finally {
309
+ try { fs.rmSync(tempIndex, { force: true }); } catch (e) {}
310
+ }
311
+ }
312
+
313
+ function classify(added, removed) {
314
+ const a = Number(added) || 0;
315
+ const d = Number(removed) || 0;
316
+ if (a > 0 && d === 0) return 'added';
317
+ if (d > 0 && a === 0) return 'deleted';
318
+ return 'modified';
319
+ }
320
+
321
+ /**
322
+ * Restore the worktree to `fromTree`.
323
+ *
324
+ * Only called after the user has approved a preview built from the same pair of
325
+ * trees, so what they were shown is what happens.
326
+ */
327
+ export function restoreTree(worktree, fromTree) {
328
+ const gitDir = initShadow(worktree);
329
+ const current = captureTree(worktree);
330
+
331
+ // Files the agent CREATED must be removed, not merely left behind.
332
+ //
333
+ // `git checkout <tree> -- .` restores every path the tree contains and is
334
+ // silent about every path it does not — so a file created after the
335
+ // checkpoint survived the rollback, and "restored to the previous state"
336
+ // quietly meant "mostly". Deletions are computed first, because the checkout
337
+ // below rewrites the index this diff is read from.
338
+ const toRemove = [];
339
+ if (current) {
340
+ let raw = '';
341
+ try {
342
+ raw = runGit(['--git-dir', gitDir, 'diff', '--name-status', '-z', current, fromTree]);
343
+ } catch (e) {
344
+ raw = '';
345
+ }
346
+ // -z output is NUL separated: status, path, status, path…
347
+ const parts = raw.split('\0').filter(Boolean);
348
+ for (let i = 0; i + 1 < parts.length; i += 2) {
349
+ const status = parts[i];
350
+ const rel = parts[i + 1];
351
+ // 'D' going current -> target means: present now, absent then.
352
+ if (status[0] !== 'D') continue;
353
+
354
+ // A path must never climb out of the worktree it belongs to.
355
+ const absolute = path.resolve(worktree, rel);
356
+ const root = path.resolve(worktree);
357
+ if (absolute !== root && !absolute.startsWith(root + path.sep)) continue;
358
+ toRemove.push(absolute);
359
+ }
360
+ }
361
+
362
+ runGit(
363
+ ['--git-dir', gitDir, '--work-tree', worktree, 'checkout', fromTree, '--', '.'],
364
+ { cwd: worktree }
365
+ );
366
+
367
+ for (const absolute of toRemove) {
368
+ try { fs.rmSync(absolute, { force: true }); } catch (e) {}
369
+ }
370
+ }
371
+
372
+ /** Retention: the shadow repo prunes itself rather than growing forever. */
373
+ export function pruneShadow(worktree) {
374
+ try {
375
+ const gitDir = initShadow(worktree);
376
+ runGit(['--git-dir', gitDir, 'gc', '--prune=7.days', '--quiet']);
377
+ } catch (e) {}
378
+ }
@@ -0,0 +1,165 @@
1
+
2
+
3
+ import { spawn } from 'child_process';
4
+
5
+ const DEFAULT_TIMEOUT_MS = 30000;
6
+
7
+ /**
8
+ * A JSON-RPC channel over a child process's stdin/stdout.
9
+ *
10
+ * MCP frames stdio traffic as newline-delimited JSON. stderr is deliberately
11
+ * NOT parsed — servers use it for human-readable logging, and treating it as
12
+ * protocol is how a chatty server becomes a hang.
13
+ */
14
+ export class StdioChannel {
15
+ constructor({ command, args = [], env = {}, cwd }) {
16
+ this.command = command;
17
+ this.args = args;
18
+ this.env = env;
19
+ this.cwd = cwd;
20
+ this.child = null;
21
+ this.buffer = '';
22
+ this.pending = new Map();
23
+ this.nextId = 1;
24
+ this.notificationHandler = null;
25
+ this.stderrTail = [];
26
+ }
27
+
28
+ async open() {
29
+ this.child = spawn(this.command, this.args, {
30
+ cwd: this.cwd || process.cwd(),
31
+ env: { ...process.env, ...this.env },
32
+ stdio: ['pipe', 'pipe', 'pipe']
33
+ });
34
+
35
+ this.child.stdout.setEncoding('utf-8');
36
+ this.child.stdout.on('data', chunk => this.#ingest(chunk));
37
+
38
+ // Kept only so a failure can be explained. Bounded, because a server that
39
+
40
+ this.child.stderr.setEncoding('utf-8');
41
+ this.child.stderr.on('data', chunk => {
42
+ this.stderrTail.push(String(chunk));
43
+ if (this.stderrTail.length > 20) this.stderrTail.shift();
44
+ });
45
+
46
+ this.child.on('exit', code => this.#failAll(new Error(`server exited (code ${code})${this.lastError()}`)));
47
+ this.child.on('error', err => this.#failAll(err));
48
+
49
+ // Surface a command that does not exist as an error rather than a timeout.
50
+ await new Promise((resolve, reject) => {
51
+ const onError = (err) => reject(err);
52
+ this.child.once('error', onError);
53
+ setTimeout(() => { this.child.removeListener('error', onError); resolve(); }, 150);
54
+ });
55
+ }
56
+
57
+ lastError() {
58
+ const tail = this.stderrTail.join('').trim();
59
+ return tail ? `: ${tail.slice(-400)}` : '';
60
+ }
61
+
62
+ #ingest(chunk) {
63
+ this.buffer += chunk;
64
+ let newline;
65
+ while ((newline = this.buffer.indexOf('\n')) !== -1) {
66
+ const line = this.buffer.slice(0, newline).trim();
67
+ this.buffer = this.buffer.slice(newline + 1);
68
+ if (!line) continue;
69
+ let frame;
70
+ try { frame = JSON.parse(line); } catch (e) { continue; }
71
+ this.#dispatch(frame);
72
+ }
73
+ }
74
+
75
+ #dispatch(frame) {
76
+ if (frame.id !== undefined && this.pending.has(frame.id)) {
77
+ const { resolve, reject, timer } = this.pending.get(frame.id);
78
+ this.pending.delete(frame.id);
79
+ clearTimeout(timer);
80
+ if (frame.error) reject(new Error(frame.error.message || 'JSON-RPC error'));
81
+ else resolve(frame.result);
82
+ return;
83
+ }
84
+ if (frame.method && this.notificationHandler) {
85
+ try { this.notificationHandler(frame); } catch (e) {}
86
+ }
87
+ }
88
+
89
+ #failAll(error) {
90
+ for (const [, { reject, timer }] of this.pending) {
91
+ clearTimeout(timer);
92
+ reject(error);
93
+ }
94
+ this.pending.clear();
95
+ }
96
+
97
+ send(method, params, timeoutMs = DEFAULT_TIMEOUT_MS) {
98
+ if (!this.child || this.child.killed) return Promise.reject(new Error('channel is closed'));
99
+ const id = this.nextId++;
100
+ const frame = { jsonrpc: '2.0', id, method, params: params ?? {} };
101
+
102
+ return new Promise((resolve, reject) => {
103
+ const timer = setTimeout(() => {
104
+ this.pending.delete(id);
105
+ reject(new Error(`${method} timed out after ${timeoutMs} ms`));
106
+ }, timeoutMs);
107
+ this.pending.set(id, { resolve, reject, timer });
108
+ try {
109
+ this.child.stdin.write(JSON.stringify(frame) + '\n');
110
+ } catch (e) {
111
+ clearTimeout(timer);
112
+ this.pending.delete(id);
113
+ reject(e);
114
+ }
115
+ });
116
+ }
117
+
118
+ notify(method, params) {
119
+ if (!this.child || this.child.killed) return;
120
+ try {
121
+ this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params: params ?? {} }) + '\n');
122
+ } catch (e) {}
123
+ }
124
+
125
+ close() {
126
+ this.#failAll(new Error('channel closed'));
127
+ try { if (this.child) this.child.kill('SIGTERM'); } catch (e) {}
128
+ this.child = null;
129
+ }
130
+ }
131
+
132
+ /** A JSON-RPC channel over HTTP POST. One request, one response, no session. */
133
+ export class HttpChannel {
134
+ constructor({ url, headers = {} }) {
135
+ this.url = url;
136
+ this.headers = headers;
137
+ this.nextId = 1;
138
+ this.notificationHandler = null;
139
+ }
140
+
141
+ async open() {
142
+ // Nothing to open. Validate the URL now so a typo fails at connect time
143
+ // rather than on the first tool call.
144
+ const parsed = new URL(this.url);
145
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
146
+ throw new Error(`unsupported transport protocol: ${parsed.protocol}`);
147
+ }
148
+ }
149
+
150
+ async send(method, params, timeoutMs = DEFAULT_TIMEOUT_MS) {
151
+ const response = await fetch(this.url, {
152
+ method: 'POST',
153
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', ...this.headers },
154
+ body: JSON.stringify({ jsonrpc: '2.0', id: this.nextId++, method, params: params ?? {} }),
155
+ signal: AbortSignal.timeout(timeoutMs)
156
+ });
157
+ if (!response.ok) throw new Error(`HTTP ${response.status} from ${this.url}`);
158
+ const frame = await response.json();
159
+ if (frame.error) throw new Error(frame.error.message || 'JSON-RPC error');
160
+ return frame.result;
161
+ }
162
+
163
+ notify() { /* HTTP transport has no notification channel */ }
164
+ close() { /* nothing to close */ }
165
+ }
@@ -0,0 +1,136 @@
1
+ // Model Context Protocol client.
2
+ //
3
+ // WHY THIS EXISTS
4
+ //
5
+ // `src/tools/deferredMcpSearch.js` carries "mcp" in its name and is not an MCP
6
+ // client — it searches Causyne's own tool registry. The only code in the tree
7
+ // that spoke JSON-RPC was `palmierAutomator.js`, against one hardcoded server.
8
+ // So the single largest extensibility lever every competing agent has was
9
+ // missing, while the name suggested it was present.
10
+ //
11
+ // SECURITY POSTURE — read before changing anything here
12
+ //
13
+ // A client that launches third-party servers is the highest-risk pattern in
14
+ // this codebase, so the boundary is drawn in exactly one place and stated
15
+ // plainly:
16
+ //
17
+ // * A server command NEVER comes from model output. Servers are read from
18
+ // `config.mcpServers`, which only the user writes. The model can ask for a
19
+ // configured server to be enabled; it cannot name a new one, cannot alter a
20
+ // command, and cannot pass arguments to a process launch.
21
+ // * Enabling a server requires the same human approval as any other
22
+ // consent-gated action (`sandbox.js` classifies `grant_mcp_server` with the
23
+ // download actions).
24
+ // * What the model DOES control is tool arguments, which is the entire point
25
+ // of the protocol — the same trust boundary as any other tool call.
26
+ //
27
+ // If a future change lets a server command originate anywhere other than user
28
+ // config, that is a remote-code-execution hole, not a feature.
29
+
30
+ import { StdioChannel, HttpChannel } from './jsonRpc.js';
31
+ import { getPackageVersion } from '../version.js';
32
+
33
+ const PROTOCOL_VERSION = '2025-06-18';
34
+ const CLIENT_INFO = { name: 'causyne-agent', version: getPackageVersion() };
35
+
36
+ export class McpClient {
37
+ /**
38
+ * @param {object} definition a user-configured server entry:
39
+ * stdio: { name, command, args?, env?, cwd? }
40
+ * http: { name, url, headers? }
41
+ */
42
+ constructor(definition) {
43
+ this.name = definition.name;
44
+ this.definition = definition;
45
+ this.channel = null;
46
+ this.tools = [];
47
+ this.serverInfo = null;
48
+ }
49
+
50
+ static validateDefinition(definition) {
51
+ if (!definition || typeof definition !== 'object') return 'not an object';
52
+ if (typeof definition.name !== 'string' || !/^[a-z0-9_-]{1,48}$/i.test(definition.name)) {
53
+ return 'name must be 1-48 characters of [A-Za-z0-9_-]';
54
+ }
55
+ const hasStdio = typeof definition.command === 'string' && definition.command.length > 0;
56
+ const hasHttp = typeof definition.url === 'string' && definition.url.length > 0;
57
+ if (hasStdio === hasHttp) return 'exactly one of `command` (stdio) or `url` (http) is required';
58
+ if (hasStdio && !Array.isArray(definition.args || [])) return '`args` must be an array';
59
+ return null;
60
+ }
61
+
62
+ async connect() {
63
+ const problem = McpClient.validateDefinition(this.definition);
64
+ if (problem) throw new Error(`invalid server definition: ${problem}`);
65
+
66
+ this.channel = this.definition.url
67
+ ? new HttpChannel({ url: this.definition.url, headers: this.definition.headers })
68
+ : new StdioChannel({
69
+ command: this.definition.command,
70
+ args: this.definition.args || [],
71
+ env: this.definition.env || {},
72
+ cwd: this.definition.cwd
73
+ });
74
+
75
+ await this.channel.open();
76
+
77
+ const result = await this.channel.send('initialize', {
78
+ protocolVersion: PROTOCOL_VERSION,
79
+ capabilities: { tools: {} },
80
+ clientInfo: CLIENT_INFO
81
+ });
82
+ this.serverInfo = result?.serverInfo || null;
83
+
84
+ // Required by the protocol: the server may not serve requests until it has
85
+ // seen this.
86
+ this.channel.notify('notifications/initialized');
87
+
88
+ await this.listTools();
89
+ return this;
90
+ }
91
+
92
+ async listTools() {
93
+ const result = await this.channel.send('tools/list', {});
94
+ const raw = Array.isArray(result?.tools) ? result.tools : [];
95
+
96
+ // Only keep entries that are actually callable. A malformed advertisement
97
+ // reaching the model produces a tool it can call and we cannot dispatch —
98
+ // the exact failure mode D-6 was about, arriving from someone else's code.
99
+ this.tools = raw
100
+ .filter(t => t && typeof t.name === 'string' && t.name.length > 0)
101
+ .map(t => ({
102
+ name: t.name,
103
+ description: typeof t.description === 'string' ? t.description : '',
104
+ inputSchema: (t.inputSchema && typeof t.inputSchema === 'object')
105
+ ? t.inputSchema
106
+ : { type: 'object', properties: {} }
107
+ }));
108
+ return this.tools;
109
+ }
110
+
111
+ async callTool(toolName, args) {
112
+ const result = await this.channel.send('tools/call', { name: toolName, arguments: args ?? {} });
113
+
114
+ // MCP returns content blocks. Flatten text for the agent loop, and report an
115
+ // error result as an error rather than as successful output.
116
+ const blocks = Array.isArray(result?.content) ? result.content : [];
117
+ const text = blocks
118
+ .map(b => (b?.type === 'text' && typeof b.text === 'string') ? b.text : '')
119
+ .filter(Boolean)
120
+ .join('\n');
121
+
122
+ if (result?.isError) {
123
+ return { success: false, error: text || 'the MCP server reported an error' };
124
+ }
125
+ return {
126
+ success: true,
127
+ output: text || JSON.stringify(result ?? {}, null, 2)
128
+ };
129
+ }
130
+
131
+ close() {
132
+ try { if (this.channel) this.channel.close(); } catch (e) {}
133
+ this.channel = null;
134
+ this.tools = [];
135
+ }
136
+ }