salmon-loop 0.2.3

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 (655) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/README.zh-CN.md +144 -0
  4. package/dist/cli/argv/headless-detection.js +60 -0
  5. package/dist/cli/argv/print-mode.js +60 -0
  6. package/dist/cli/authorization/allowlist.js +908 -0
  7. package/dist/cli/authorization/non-interactive.js +166 -0
  8. package/dist/cli/authorization/provider.js +416 -0
  9. package/dist/cli/chat-interface.js +83 -0
  10. package/dist/cli/chat.js +492 -0
  11. package/dist/cli/cli-runtime-context.js +12 -0
  12. package/dist/cli/commander-error-adapter.js +35 -0
  13. package/dist/cli/commander-error-meta.js +13 -0
  14. package/dist/cli/commands/allowlist.js +270 -0
  15. package/dist/cli/commands/chat.js +120 -0
  16. package/dist/cli/commands/config.js +250 -0
  17. package/dist/cli/commands/context.js +57 -0
  18. package/dist/cli/commands/dispatcher.js +53 -0
  19. package/dist/cli/commands/exit.js +9 -0
  20. package/dist/cli/commands/llm-output.js +135 -0
  21. package/dist/cli/commands/log-mode.js +143 -0
  22. package/dist/cli/commands/mode.js +136 -0
  23. package/dist/cli/commands/new.js +18 -0
  24. package/dist/cli/commands/parallel.js +256 -0
  25. package/dist/cli/commands/queue.js +130 -0
  26. package/dist/cli/commands/registry.js +85 -0
  27. package/dist/cli/commands/restore.js +26 -0
  28. package/dist/cli/commands/run/assistant-message.js +14 -0
  29. package/dist/cli/commands/run/config-resolution.js +37 -0
  30. package/dist/cli/commands/run/early-errors.js +108 -0
  31. package/dist/cli/commands/run/execute.js +73 -0
  32. package/dist/cli/commands/run/extensions-resolution.js +22 -0
  33. package/dist/cli/commands/run/handler.js +434 -0
  34. package/dist/cli/commands/run/headless-error-writer.js +182 -0
  35. package/dist/cli/commands/run/instruction-guard.js +24 -0
  36. package/dist/cli/commands/run/loop-params.js +46 -0
  37. package/dist/cli/commands/run/mode.js +8 -0
  38. package/dist/cli/commands/run/parse-options.js +67 -0
  39. package/dist/cli/commands/run/persist-session.js +35 -0
  40. package/dist/cli/commands/run/preflight.js +156 -0
  41. package/dist/cli/commands/run/reporter-factory.js +52 -0
  42. package/dist/cli/commands/run/runtime-llm.js +56 -0
  43. package/dist/cli/commands/run/runtime-options.js +30 -0
  44. package/dist/cli/commands/run/session.js +19 -0
  45. package/dist/cli/commands/run/structured-output.js +106 -0
  46. package/dist/cli/commands/run/types.js +2 -0
  47. package/dist/cli/commands/run/validate-options.js +28 -0
  48. package/dist/cli/commands/run/verbose.js +36 -0
  49. package/dist/cli/commands/run.js +2 -0
  50. package/dist/cli/commands/serve.js +323 -0
  51. package/dist/cli/commands/session.js +77 -0
  52. package/dist/cli/commands/snapshot-interactive.js +165 -0
  53. package/dist/cli/commands/snapshot.js +159 -0
  54. package/dist/cli/commands/status.js +17 -0
  55. package/dist/cli/commands/subagent.js +178 -0
  56. package/dist/cli/commands/subcommand-suggestions.js +63 -0
  57. package/dist/cli/commands/tool-names.js +155 -0
  58. package/dist/cli/commands/types.js +2 -0
  59. package/dist/cli/commands/utils.js +42 -0
  60. package/dist/cli/config.js +16 -0
  61. package/dist/cli/crash-reporter.js +5 -0
  62. package/dist/cli/headless/anthropic-stream-normalized-encoder.js +164 -0
  63. package/dist/cli/headless/anthropic-stream-protocol.js +62 -0
  64. package/dist/cli/headless/json-protocol.js +124 -0
  65. package/dist/cli/headless/native-stream-normalized-encoder.js +206 -0
  66. package/dist/cli/headless/openai-responses-canonical-applier.js +94 -0
  67. package/dist/cli/headless/openai-responses-state.js +294 -0
  68. package/dist/cli/headless/openai-stream-encoder.js +152 -0
  69. package/dist/cli/headless/stdout-writer.js +9 -0
  70. package/dist/cli/headless/stream-json-protocol.js +136 -0
  71. package/dist/cli/index.js +8 -0
  72. package/dist/cli/locales/en.js +409 -0
  73. package/dist/cli/locales/index.js +7 -0
  74. package/dist/cli/program-bootstrap.js +14 -0
  75. package/dist/cli/program-commands.js +106 -0
  76. package/dist/cli/program-options.js +15 -0
  77. package/dist/cli/program-output-mode.js +11 -0
  78. package/dist/cli/program-parse.js +24 -0
  79. package/dist/cli/reporters/anthropic-stream.js +77 -0
  80. package/dist/cli/reporters/base.js +2 -0
  81. package/dist/cli/reporters/json.js +69 -0
  82. package/dist/cli/reporters/openai-stream.js +72 -0
  83. package/dist/cli/reporters/standard.js +226 -0
  84. package/dist/cli/reporters/stderr-log-reporter.js +71 -0
  85. package/dist/cli/reporters/stream-json.js +111 -0
  86. package/dist/cli/run-cli.js +25 -0
  87. package/dist/cli/slash/runtime.js +240 -0
  88. package/dist/cli/ui/App.js +273 -0
  89. package/dist/cli/ui/authorization/bus.js +35 -0
  90. package/dist/cli/ui/components/CommandInput.js +200 -0
  91. package/dist/cli/ui/components/CommandSuggestionList.js +20 -0
  92. package/dist/cli/ui/components/Markdown.js +423 -0
  93. package/dist/cli/ui/components/MessageList.js +34 -0
  94. package/dist/cli/ui/components/StatusBannerLine.js +7 -0
  95. package/dist/cli/ui/components/TodoDrawer.js +60 -0
  96. package/dist/cli/ui/components/WelcomeMessage.js +14 -0
  97. package/dist/cli/ui/components/animations/StretchingThinking.js +51 -0
  98. package/dist/cli/ui/components/animations/ThinkingWave.js +15 -0
  99. package/dist/cli/ui/components/animations/TypeIndicator.js +30 -0
  100. package/dist/cli/ui/components/layout/SplitPane.js +11 -0
  101. package/dist/cli/ui/components/messageList/MessageItem.js +27 -0
  102. package/dist/cli/ui/components/messageList/QueuePreviewList.js +11 -0
  103. package/dist/cli/ui/components/messageList/items/EmphasisMessageItem.js +20 -0
  104. package/dist/cli/ui/components/messageList/items/InterruptMessageItem.js +10 -0
  105. package/dist/cli/ui/components/messageList/items/LightweightMessageItem.js +12 -0
  106. package/dist/cli/ui/components/messageList/items/StandardMessageItem.js +23 -0
  107. package/dist/cli/ui/components/messageList/items/WelcomeMessageItem.js +7 -0
  108. package/dist/cli/ui/components/messageList/messageListLayout.js +27 -0
  109. package/dist/cli/ui/components/messageList/streaming.js +51 -0
  110. package/dist/cli/ui/components/messageList/types.js +2 -0
  111. package/dist/cli/ui/components/messageList/utils.js +7 -0
  112. package/dist/cli/ui/components/sidebar/FileContext.js +8 -0
  113. package/dist/cli/ui/components/sidebar/MissionControl.js +8 -0
  114. package/dist/cli/ui/config.js +59 -0
  115. package/dist/cli/ui/hooks/useCommandLifecycle.js +110 -0
  116. package/dist/cli/ui/hooks/useCommandSuggestions.js +87 -0
  117. package/dist/cli/ui/hooks/useInputHistory.js +57 -0
  118. package/dist/cli/ui/hooks/useLoopEvents.js +382 -0
  119. package/dist/cli/ui/hooks/useLoopState.js +73 -0
  120. package/dist/cli/ui/hooks/useOnionExit.js +31 -0
  121. package/dist/cli/ui/hooks/useTerminalDimensions.js +34 -0
  122. package/dist/cli/ui/index.js +136 -0
  123. package/dist/cli/ui/selection/bus.js +35 -0
  124. package/dist/cli/ui/status/formatStatusBanner.js +8 -0
  125. package/dist/cli/ui/store/context.js +17 -0
  126. package/dist/cli/ui/store/reducer.js +264 -0
  127. package/dist/cli/ui/store/types.js +81 -0
  128. package/dist/cli/ui/styles/theme.js +295 -0
  129. package/dist/cli/ui/types.js +2 -0
  130. package/dist/cli/ui/utils/sanitizer.js +122 -0
  131. package/dist/cli/ui/utils/transcript.js +28 -0
  132. package/dist/cli/utils/asyncQueue.js +125 -0
  133. package/dist/cli/utils/audit-scope.js +10 -0
  134. package/dist/cli/utils/detectors/index.js +38 -0
  135. package/dist/cli/utils/llm-output.js +34 -0
  136. package/dist/cli/utils/outcome-reporter.js +17 -0
  137. package/dist/cli/utils/safe-fs.js +184 -0
  138. package/dist/cli/utils/verify-resolver.js +34 -0
  139. package/dist/cli/utils/worktree-prepare-resolver.js +18 -0
  140. package/dist/core/adapters/fs/atomic-file-writer.js +129 -0
  141. package/dist/core/adapters/fs/file-adapter.js +95 -0
  142. package/dist/core/adapters/fs/filesystem.js +31 -0
  143. package/dist/core/adapters/fs/index.js +5 -0
  144. package/dist/core/adapters/fs/node-fs.js +7 -0
  145. package/dist/core/adapters/fs/readonly-filesystem.js +23 -0
  146. package/dist/core/adapters/git/git-adapter.js +704 -0
  147. package/dist/core/adapters/git/git-runner.js +119 -0
  148. package/dist/core/adapters/git/lock-manager.js +314 -0
  149. package/dist/core/adapters/git/types.js +2 -0
  150. package/dist/core/adapters/path/index.js +2 -0
  151. package/dist/core/adapters/path/path-adapter.js +23 -0
  152. package/dist/core/ast/guard.js +116 -0
  153. package/dist/core/ast/index.js +4 -0
  154. package/dist/core/ast/parser.js +284 -0
  155. package/dist/core/ast/validator.js +46 -0
  156. package/dist/core/backends/salmon-loop/task-executor.js +68 -0
  157. package/dist/core/checkpoint-domain/manifest-store.js +379 -0
  158. package/dist/core/checkpoint-domain/service.js +84 -0
  159. package/dist/core/checkpoint-domain/types.js +2 -0
  160. package/dist/core/config/defaults.js +50 -0
  161. package/dist/core/config/errors.js +11 -0
  162. package/dist/core/config/file-format.js +108 -0
  163. package/dist/core/config/index.js +7 -0
  164. package/dist/core/config/limits.js +77 -0
  165. package/dist/core/config/load.js +34 -0
  166. package/dist/core/config/normalize.js +35 -0
  167. package/dist/core/config/paths.js +20 -0
  168. package/dist/core/config/redact.js +16 -0
  169. package/dist/core/config/resolve-env.js +43 -0
  170. package/dist/core/config/resolve-llm.js +130 -0
  171. package/dist/core/config/resolve.js +68 -0
  172. package/dist/core/config/resolvers/ast-validation.js +8 -0
  173. package/dist/core/config/resolvers/context.js +21 -0
  174. package/dist/core/config/resolvers/observability.js +45 -0
  175. package/dist/core/config/resolvers/output.js +8 -0
  176. package/dist/core/config/resolvers/permission-mode.js +6 -0
  177. package/dist/core/config/resolvers/security.js +14 -0
  178. package/dist/core/config/resolvers/server.js +36 -0
  179. package/dist/core/config/resolvers/tool-authorization.js +39 -0
  180. package/dist/core/config/resolvers/ui.js +26 -0
  181. package/dist/core/config/types/config-file.js +2 -0
  182. package/dist/core/config/types/primitives.js +9 -0
  183. package/dist/core/config/types/resolved.js +2 -0
  184. package/dist/core/config/types.js +4 -0
  185. package/dist/core/config/validate.js +852 -0
  186. package/dist/core/context/assembly/default-prompt-assembler.js +7 -0
  187. package/dist/core/context/assembly/prompt-assembler.js +2 -0
  188. package/dist/core/context/ast/import-extractor.js +28 -0
  189. package/dist/core/context/ast/module-resolver.js +61 -0
  190. package/dist/core/context/ast/source-outline.js +25 -0
  191. package/dist/core/context/audit-constants.js +23 -0
  192. package/dist/core/context/audit.js +54 -0
  193. package/dist/core/context/budget/dynamic-adjuster.js +149 -0
  194. package/dist/core/context/budget/example-integration.js +49 -0
  195. package/dist/core/context/budget/integration.js +93 -0
  196. package/dist/core/context/builder.js +289 -0
  197. package/dist/core/context/cache/errors.js +16 -0
  198. package/dist/core/context/cache/incremental-updater.js +131 -0
  199. package/dist/core/context/cache/index.js +25 -0
  200. package/dist/core/context/cache/path-resolver.js +127 -0
  201. package/dist/core/context/cache/prompt-caching.js +207 -0
  202. package/dist/core/context/cache/store-factory.js +63 -0
  203. package/dist/core/context/cache/store.js +193 -0
  204. package/dist/core/context/cache/types.js +15 -0
  205. package/dist/core/context/compression/js-like-comments.js +139 -0
  206. package/dist/core/context/compression/smart-compress.js +61 -0
  207. package/dist/core/context/compression/whitespace.js +26 -0
  208. package/dist/core/context/dependencies.js +102 -0
  209. package/dist/core/context/effectiveness/index.js +25 -0
  210. package/dist/core/context/effectiveness/tracker.js +253 -0
  211. package/dist/core/context/effectiveness/types.js +15 -0
  212. package/dist/core/context/formatters/index.js +7 -0
  213. package/dist/core/context/formatters/json-converter.js +662 -0
  214. package/dist/core/context/formatters/types.js +6 -0
  215. package/dist/core/context/formatters/xml-context.js +296 -0
  216. package/dist/core/context/gatherers/architecture-gatherer.js +75 -0
  217. package/dist/core/context/gatherers/artifact-gatherer.js +53 -0
  218. package/dist/core/context/gatherers/ast-gatherer.js +370 -0
  219. package/dist/core/context/gatherers/ghost-dependency-gatherer.js +46 -0
  220. package/dist/core/context/gatherers/git-diff-gatherer.js +91 -0
  221. package/dist/core/context/gatherers/git-history-gatherer.js +57 -0
  222. package/dist/core/context/gatherers/knowledge-gatherer.js +101 -0
  223. package/dist/core/context/gatherers/metadata-gatherer.js +59 -0
  224. package/dist/core/context/gatherers/primary-text-gatherer.js +36 -0
  225. package/dist/core/context/gatherers/ripgrep-gatherer.js +104 -0
  226. package/dist/core/context/hash.js +52 -0
  227. package/dist/core/context/index.js +3 -0
  228. package/dist/core/context/keywords.js +179 -0
  229. package/dist/core/context/policies/budget-policy.js +36 -0
  230. package/dist/core/context/policies/pack-until-full.js +419 -0
  231. package/dist/core/context/scoring/relevance.js +191 -0
  232. package/dist/core/context/service-deps.js +32 -0
  233. package/dist/core/context/service-helpers.js +32 -0
  234. package/dist/core/context/service.js +265 -0
  235. package/dist/core/context/steps/context-budget.js +157 -0
  236. package/dist/core/context/steps/context-gather.js +71 -0
  237. package/dist/core/context/steps/context-primary.js +19 -0
  238. package/dist/core/context/steps/context-promotion.js +78 -0
  239. package/dist/core/context/steps/context-targets.js +85 -0
  240. package/dist/core/context/steps/types.js +2 -0
  241. package/dist/core/context/summarization/index.js +27 -0
  242. package/dist/core/context/summarization/prompts.js +80 -0
  243. package/dist/core/context/summarization/summarizer.js +377 -0
  244. package/dist/core/context/summarization/types.js +29 -0
  245. package/dist/core/context/targeting/churn-policy.js +27 -0
  246. package/dist/core/context/targeting/target-resolver.js +491 -0
  247. package/dist/core/context/token/adaptive-budget.js +364 -0
  248. package/dist/core/context/token/cache.js +163 -0
  249. package/dist/core/context/token/counter.js +190 -0
  250. package/dist/core/context/token/encoding-registry.js +173 -0
  251. package/dist/core/context/token/index.js +31 -0
  252. package/dist/core/context/token/token-budget.js +213 -0
  253. package/dist/core/context/token/types.js +10 -0
  254. package/dist/core/context/truncation/index.js +23 -0
  255. package/dist/core/context/truncation/semantic-truncator.js +103 -0
  256. package/dist/core/context/truncation/strategies/error-stack.js +94 -0
  257. package/dist/core/context/truncation/strategies/generic.js +48 -0
  258. package/dist/core/context/truncation/strategies/git-diff.js +99 -0
  259. package/dist/core/context/truncation/strategies/index.js +10 -0
  260. package/dist/core/context/truncation/strategies/json.js +142 -0
  261. package/dist/core/context/truncation/strategies/log.js +131 -0
  262. package/dist/core/context/truncation/strategies/test-result.js +140 -0
  263. package/dist/core/context/truncation/type-detector.js +133 -0
  264. package/dist/core/context/truncation/types.js +16 -0
  265. package/dist/core/context/types.js +2 -0
  266. package/dist/core/extensions/index.js +118 -0
  267. package/dist/core/extensions/load.js +36 -0
  268. package/dist/core/extensions/merge.js +29 -0
  269. package/dist/core/extensions/paths.js +40 -0
  270. package/dist/core/extensions/redact.js +37 -0
  271. package/dist/core/extensions/schemas.js +70 -0
  272. package/dist/core/extensions/types.js +2 -0
  273. package/dist/core/facades/cli-authorization-allowlist.js +3 -0
  274. package/dist/core/facades/cli-authorization-non-interactive.js +3 -0
  275. package/dist/core/facades/cli-authorization-provider.js +2 -0
  276. package/dist/core/facades/cli-chat.js +11 -0
  277. package/dist/core/facades/cli-command-allowlist.js +3 -0
  278. package/dist/core/facades/cli-command-chat.js +8 -0
  279. package/dist/core/facades/cli-command-checkpoint.js +3 -0
  280. package/dist/core/facades/cli-command-config.js +10 -0
  281. package/dist/core/facades/cli-command-dispatcher.js +2 -0
  282. package/dist/core/facades/cli-command-parallel.js +8 -0
  283. package/dist/core/facades/cli-command-session.js +2 -0
  284. package/dist/core/facades/cli-command-tool-names.js +6 -0
  285. package/dist/core/facades/cli-context.js +8 -0
  286. package/dist/core/facades/cli-headless.js +3 -0
  287. package/dist/core/facades/cli-observability.js +3 -0
  288. package/dist/core/facades/cli-program-bootstrap.js +2 -0
  289. package/dist/core/facades/cli-reporters.js +5 -0
  290. package/dist/core/facades/cli-run-execute.js +3 -0
  291. package/dist/core/facades/cli-run-handler.js +7 -0
  292. package/dist/core/facades/cli-run-headless-error-writer.js +2 -0
  293. package/dist/core/facades/cli-run-loop-params.js +2 -0
  294. package/dist/core/facades/cli-run-persist-session.js +2 -0
  295. package/dist/core/facades/cli-run-runtime-llm.js +5 -0
  296. package/dist/core/facades/cli-serve.js +21 -0
  297. package/dist/core/facades/cli-slash-runtime.js +9 -0
  298. package/dist/core/facades/cli-subagent.js +2 -0
  299. package/dist/core/facades/cli-ui.js +5 -0
  300. package/dist/core/facades/cli-utils-llm-output.js +3 -0
  301. package/dist/core/facades/cli-utils-path.js +2 -0
  302. package/dist/core/facades/cli-utils-worktree.js +2 -0
  303. package/dist/core/failure/diagnostics.js +221 -0
  304. package/dist/core/feedback/index.js +28 -0
  305. package/dist/core/feedback/parsers.js +59 -0
  306. package/dist/core/feedback/patterns.js +26 -0
  307. package/dist/core/feedback/types.js +2 -0
  308. package/dist/core/grizzco/domain/grizzco-types.js +41 -0
  309. package/dist/core/grizzco/dsl/DecisionEngine.js +149 -0
  310. package/dist/core/grizzco/dsl/MicroTaskRunner.js +39 -0
  311. package/dist/core/grizzco/dsl/llm-strategy.js +80 -0
  312. package/dist/core/grizzco/dsl/strategies.js +69 -0
  313. package/dist/core/grizzco/dsl/types.js +2 -0
  314. package/dist/core/grizzco/engine/observability/event-adapter.js +41 -0
  315. package/dist/core/grizzco/engine/observability/index.js +3 -0
  316. package/dist/core/grizzco/engine/observability/loop-telemetry.js +51 -0
  317. package/dist/core/grizzco/engine/outcome/index.js +2 -0
  318. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +167 -0
  319. package/dist/core/grizzco/engine/pipeline/pipeline.js +335 -0
  320. package/dist/core/grizzco/engine/pipeline/types.js +2 -0
  321. package/dist/core/grizzco/engine/transaction/attempt-failure.js +242 -0
  322. package/dist/core/grizzco/engine/transaction/authorization-summary.js +44 -0
  323. package/dist/core/grizzco/engine/transaction/index.js +3 -0
  324. package/dist/core/grizzco/engine/transaction/report-mapper.js +50 -0
  325. package/dist/core/grizzco/engine/transaction/retry-policy.js +19 -0
  326. package/dist/core/grizzco/engine/transaction/runner-builder.js +45 -0
  327. package/dist/core/grizzco/engine/transaction/session.js +58 -0
  328. package/dist/core/grizzco/engine/transaction/transaction-runner.js +193 -0
  329. package/dist/core/grizzco/engine/transaction/types.js +2 -0
  330. package/dist/core/grizzco/execution/Executor.js +58 -0
  331. package/dist/core/grizzco/execution/RejectionManager.js +71 -0
  332. package/dist/core/grizzco/execution/WorkerFactory.js +31 -0
  333. package/dist/core/grizzco/flows/SalmonLoopFlow.js +102 -0
  334. package/dist/core/grizzco/runtime/apply-back-runtime.js +136 -0
  335. package/dist/core/grizzco/runtime/apply-back-utils.js +13 -0
  336. package/dist/core/grizzco/runtime/host/host-runner.js +99 -0
  337. package/dist/core/grizzco/runtime/host/index.js +2 -0
  338. package/dist/core/grizzco/runtime/host/types.js +2 -0
  339. package/dist/core/grizzco/services/CachedService.js +42 -0
  340. package/dist/core/grizzco/services/implementations/default/GitConfigService.js +38 -0
  341. package/dist/core/grizzco/services/implementations/mock/MockLockService.js +11 -0
  342. package/dist/core/grizzco/services/implementations/mock/MockUserQuotaService.js +11 -0
  343. package/dist/core/grizzco/services/registry.js +30 -0
  344. package/dist/core/grizzco/services/types.js +2 -0
  345. package/dist/core/grizzco/steps/answer.js +75 -0
  346. package/dist/core/grizzco/steps/apply-back.js +46 -0
  347. package/dist/core/grizzco/steps/apply.js +136 -0
  348. package/dist/core/grizzco/steps/ast-validate.js +37 -0
  349. package/dist/core/grizzco/steps/audit.js +311 -0
  350. package/dist/core/grizzco/steps/context.js +74 -0
  351. package/dist/core/grizzco/steps/display-answer.js +6 -0
  352. package/dist/core/grizzco/steps/display-report.js +158 -0
  353. package/dist/core/grizzco/steps/display-research.js +6 -0
  354. package/dist/core/grizzco/steps/displayReview.js +6 -0
  355. package/dist/core/grizzco/steps/explore.js +245 -0
  356. package/dist/core/grizzco/steps/extractIssues.js +27 -0
  357. package/dist/core/grizzco/steps/generateFixPlan.js +13 -0
  358. package/dist/core/grizzco/steps/generateReview.js +71 -0
  359. package/dist/core/grizzco/steps/patch.js +220 -0
  360. package/dist/core/grizzco/steps/plan.js +191 -0
  361. package/dist/core/grizzco/steps/preflight.js +93 -0
  362. package/dist/core/grizzco/steps/prepare-deps.js +49 -0
  363. package/dist/core/grizzco/steps/read-only-shrink.js +4 -0
  364. package/dist/core/grizzco/steps/research.js +188 -0
  365. package/dist/core/grizzco/steps/rollback.js +138 -0
  366. package/dist/core/grizzco/steps/shrink.js +64 -0
  367. package/dist/core/grizzco/steps/validate.js +40 -0
  368. package/dist/core/grizzco/steps/verify.js +136 -0
  369. package/dist/core/grizzco/validation/AstValidationService.js +133 -0
  370. package/dist/core/grizzco/validation/ContextValidator.js +17 -0
  371. package/dist/core/grizzco/validation/ast-validation-policy.js +11 -0
  372. package/dist/core/grizzco/workers/direct-write-worker.js +44 -0
  373. package/dist/core/grizzco/workers/git-apply-worker.js +75 -0
  374. package/dist/core/grizzco/workers/i-merge-worker.js +2 -0
  375. package/dist/core/grizzco/workers/mm-three-way-worker.js +117 -0
  376. package/dist/core/grizzco/workers/no-op-worker.js +18 -0
  377. package/dist/core/grizzco/workers/overwrite-binary-worker.js +29 -0
  378. package/dist/core/grizzco/workers/strata-sync-worker.js +69 -0
  379. package/dist/core/grizzco/workers/three-way-merge-worker.js +84 -0
  380. package/dist/core/grizzco/workers/three-way-staged-worker.js +93 -0
  381. package/dist/core/grizzco/workers/union-merge-worker.js +71 -0
  382. package/dist/core/history/input-history.js +55 -0
  383. package/dist/core/intent/chat-intent.js +250 -0
  384. package/dist/core/interaction/events/bus.js +52 -0
  385. package/dist/core/interaction/model/events.js +2 -0
  386. package/dist/core/interaction/model/index.js +3 -0
  387. package/dist/core/interaction/model/task-state.js +9 -0
  388. package/dist/core/interaction/model/transition-policy.js +50 -0
  389. package/dist/core/interaction/model/types.js +2 -0
  390. package/dist/core/interaction/orchestration/facade.js +190 -0
  391. package/dist/core/interaction/orchestration/index.js +2 -0
  392. package/dist/core/interaction/orchestration/store.js +32 -0
  393. package/dist/core/interaction/sync/task-sync-engine.js +57 -0
  394. package/dist/core/interaction/turn-stop-reason.js +27 -0
  395. package/dist/core/language-support/index.js +3 -0
  396. package/dist/core/language-support/orchestrator.js +37 -0
  397. package/dist/core/language-support/strategies/extension-candidate-strategy.js +27 -0
  398. package/dist/core/language-support/strategies/index.js +3 -0
  399. package/dist/core/language-support/strategies/language-query-strategy.js +26 -0
  400. package/dist/core/llm/ai-sdk/chat-executor.js +88 -0
  401. package/dist/core/llm/ai-sdk/langfuse-headers.js +28 -0
  402. package/dist/core/llm/ai-sdk/message-mapper.js +240 -0
  403. package/dist/core/llm/ai-sdk/observation-context.js +16 -0
  404. package/dist/core/llm/ai-sdk/provider-factory.js +29 -0
  405. package/dist/core/llm/ai-sdk/request-params.js +18 -0
  406. package/dist/core/llm/ai-sdk/request-runtime.js +168 -0
  407. package/dist/core/llm/ai-sdk/result-mapper.js +31 -0
  408. package/dist/core/llm/ai-sdk/retry-classifier.js +82 -0
  409. package/dist/core/llm/ai-sdk/retry-executor.js +38 -0
  410. package/dist/core/llm/ai-sdk.js +92 -0
  411. package/dist/core/llm/audit.js +2 -0
  412. package/dist/core/llm/base-url.js +18 -0
  413. package/dist/core/llm/contracts/repair.js +68 -0
  414. package/dist/core/llm/errors.js +172 -0
  415. package/dist/core/llm/factory.js +21 -0
  416. package/dist/core/llm/http/index.js +2 -0
  417. package/dist/core/llm/index.js +6 -0
  418. package/dist/core/llm/message-composition.js +25 -0
  419. package/dist/core/llm/openai.js +69 -0
  420. package/dist/core/llm/output-policy.js +192 -0
  421. package/dist/core/llm/phase-router.js +55 -0
  422. package/dist/core/llm/redact.js +37 -0
  423. package/dist/core/llm/registry.js +81 -0
  424. package/dist/core/llm/retry-utils.js +114 -0
  425. package/dist/core/llm/stream-utils.js +87 -0
  426. package/dist/core/llm/utils.js +82 -0
  427. package/dist/core/observability/audit-file.js +199 -0
  428. package/dist/core/observability/audit-trail.js +125 -0
  429. package/dist/core/observability/authorization-decisions.js +54 -0
  430. package/dist/core/observability/debug-artifacts.js +61 -0
  431. package/dist/core/observability/error-envelope.js +63 -0
  432. package/dist/core/observability/error-mapping.js +271 -0
  433. package/dist/core/observability/ignored-error.js +6 -0
  434. package/dist/core/observability/logger.js +457 -0
  435. package/dist/core/observability/loop-event-reporter.js +46 -0
  436. package/dist/core/observability/monitor.js +240 -0
  437. package/dist/core/observability/run-outcome-reporter.js +15 -0
  438. package/dist/core/observability/token-usage.js +36 -0
  439. package/dist/core/observability/ui-log-sanitize.js +35 -0
  440. package/dist/core/patch/aggregator.js +93 -0
  441. package/dist/core/patch/diff.js +298 -0
  442. package/dist/core/permission-gate/default-gate.js +115 -0
  443. package/dist/core/permission-gate/gate.js +2 -0
  444. package/dist/core/permission-gate/types.js +2 -0
  445. package/dist/core/plan/index.js +2 -0
  446. package/dist/core/plan/manager.js +123 -0
  447. package/dist/core/plan/markdown-editor.js +238 -0
  448. package/dist/core/plan/storage.js +75 -0
  449. package/dist/core/plan/types.js +2 -0
  450. package/dist/core/plugin/interface.js +2 -0
  451. package/dist/core/plugin/loader.js +130 -0
  452. package/dist/core/plugin/registry.js +90 -0
  453. package/dist/core/plugin/validator.js +98 -0
  454. package/dist/core/prompts/registry.js +189 -0
  455. package/dist/core/prompts/runtime.js +69 -0
  456. package/dist/core/prompts/schema.js +2 -0
  457. package/dist/core/prompts/templates/phases/explore_user.hbs +26 -0
  458. package/dist/core/prompts/templates/phases/patch_user.hbs +57 -0
  459. package/dist/core/prompts/templates/phases/plan_user.hbs +33 -0
  460. package/dist/core/prompts/templates/system/_context_json_legend.hbs +21 -0
  461. package/dist/core/prompts/templates/system/_tool_defs.hbs +60 -0
  462. package/dist/core/prompts/templates/system/explore_system.hbs +26 -0
  463. package/dist/core/prompts/templates/system/main_system.hbs +18 -0
  464. package/dist/core/prompts/templates/system/patch_system.hbs +10 -0
  465. package/dist/core/prompts/templates/system/plan_system.hbs +1 -0
  466. package/dist/core/prompts/templates/system/reflection.hbs +39 -0
  467. package/dist/core/protocols/a2a/agent-card.js +30 -0
  468. package/dist/core/protocols/a2a/mapper.js +14 -0
  469. package/dist/core/protocols/a2a/sdk/auth-middleware.js +31 -0
  470. package/dist/core/protocols/a2a/sdk/executor.js +301 -0
  471. package/dist/core/protocols/a2a/sdk/server.js +24 -0
  472. package/dist/core/protocols/a2a/task-projection.js +45 -0
  473. package/dist/core/protocols/acp/acp-command-runner.js +204 -0
  474. package/dist/core/protocols/acp/acp-filesystem.js +43 -0
  475. package/dist/core/protocols/acp/checkpoint-meta.js +2 -0
  476. package/dist/core/protocols/acp/formal-agent.js +1201 -0
  477. package/dist/core/protocols/acp/handlers.js +51 -0
  478. package/dist/core/protocols/acp/permission-provider.js +122 -0
  479. package/dist/core/protocols/acp/stdio-server.js +116 -0
  480. package/dist/core/reflection/engine.js +55 -0
  481. package/dist/core/reflection/types.js +2 -0
  482. package/dist/core/runtime/agent-server-runtime.js +88 -0
  483. package/dist/core/runtime/bun-runtime.js +26 -0
  484. package/dist/core/runtime/command-runner-context.js +16 -0
  485. package/dist/core/runtime/exit-codes.js +11 -0
  486. package/dist/core/runtime/fastify-fetch-bridge.js +51 -0
  487. package/dist/core/runtime/fastify-server-bundle.js +26 -0
  488. package/dist/core/runtime/initialize.js +132 -0
  489. package/dist/core/runtime/loop-finalize.js +71 -0
  490. package/dist/core/runtime/loop-run-lifecycle.js +73 -0
  491. package/dist/core/runtime/loop-run-reporter.js +19 -0
  492. package/dist/core/runtime/loop-runtime-config.js +26 -0
  493. package/dist/core/runtime/loop-session-runner.js +30 -0
  494. package/dist/core/runtime/loop.js +84 -0
  495. package/dist/core/runtime/paths.js +84 -0
  496. package/dist/core/runtime/process-runner.js +16 -0
  497. package/dist/core/runtime/process-types.js +2 -0
  498. package/dist/core/runtime/semaphore.js +41 -0
  499. package/dist/core/runtime/sidecar-fastify-plugin.js +35 -0
  500. package/dist/core/runtime/sidecar-paths.js +47 -0
  501. package/dist/core/runtime/sidecar-route-catalog.js +103 -0
  502. package/dist/core/runtime/spawn-command.js +392 -0
  503. package/dist/core/runtime/spawn-interactive.js +71 -0
  504. package/dist/core/security/redaction.js +160 -0
  505. package/dist/core/session/compression.js +323 -0
  506. package/dist/core/session/flow.js +85 -0
  507. package/dist/core/session/manager.js +313 -0
  508. package/dist/core/session/pruning-strategy.js +153 -0
  509. package/dist/core/session/session-context-builder.js +122 -0
  510. package/dist/core/session/summary-sync.js +82 -0
  511. package/dist/core/session/token-tracker.js +82 -0
  512. package/dist/core/session/types.js +2 -0
  513. package/dist/core/skills/bridge.js +33 -0
  514. package/dist/core/skills/index.js +8 -0
  515. package/dist/core/skills/loader.js +80 -0
  516. package/dist/core/skills/parser.js +66 -0
  517. package/dist/core/skills/runtime/MicroTaskRunner.js +102 -0
  518. package/dist/core/skills/runtime/SkillRunner.js +108 -0
  519. package/dist/core/skills/strategy.js +29 -0
  520. package/dist/core/skills/types.js +2 -0
  521. package/dist/core/slash/index.js +6 -0
  522. package/dist/core/slash/parser.js +33 -0
  523. package/dist/core/slash/registry.js +78 -0
  524. package/dist/core/slash/router.js +76 -0
  525. package/dist/core/slash/steps/slash-decide.js +19 -0
  526. package/dist/core/slash/steps/slash-execute.js +73 -0
  527. package/dist/core/slash/steps/types.js +2 -0
  528. package/dist/core/slash/strategy.js +33 -0
  529. package/dist/core/slash/types.js +2 -0
  530. package/dist/core/strata/checkpoint/manager.js +492 -0
  531. package/dist/core/strata/checkpoint/snapshot-audit.js +88 -0
  532. package/dist/core/strata/checkpoint/snapshot-create.js +79 -0
  533. package/dist/core/strata/checkpoint/snapshot-write-tree.js +72 -0
  534. package/dist/core/strata/engine/shadow-merge-engine.js +394 -0
  535. package/dist/core/strata/index.js +15 -0
  536. package/dist/core/strata/interaction/content-guardian.js +59 -0
  537. package/dist/core/strata/interaction/file-system-provider.js +89 -0
  538. package/dist/core/strata/layers/file-state-resolver.js +157 -0
  539. package/dist/core/strata/layers/immutable-git-layer.js +42 -0
  540. package/dist/core/strata/layers/shadow-driver/copy-backend.js +114 -0
  541. package/dist/core/strata/layers/shadow-driver/env.js +29 -0
  542. package/dist/core/strata/layers/shadow-driver/error-classifier.js +41 -0
  543. package/dist/core/strata/layers/shadow-driver/index.js +17 -0
  544. package/dist/core/strata/layers/shadow-driver/readonly-lock.js +221 -0
  545. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +234 -0
  546. package/dist/core/strata/layers/shadow-driver/strategy.js +86 -0
  547. package/dist/core/strata/layers/sidecar-layer.js +96 -0
  548. package/dist/core/strata/layers/worktree.js +240 -0
  549. package/dist/core/strata/runtime/environment.js +377 -0
  550. package/dist/core/strata/runtime/synchronizer.js +819 -0
  551. package/dist/core/strata/types.js +46 -0
  552. package/dist/core/streaming/canonical/canonical-responses-event-emitter.js +326 -0
  553. package/dist/core/streaming/canonical/function-call-item-id.js +13 -0
  554. package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +54 -0
  555. package/dist/core/streaming/canonical/responses-event-emitter.js +127 -0
  556. package/dist/core/streaming/canonical/responses-events.js +2 -0
  557. package/dist/core/streaming/normalized-events.js +9 -0
  558. package/dist/core/streaming/normalized-from-text.js +47 -0
  559. package/dist/core/streaming/stream-assembler.js +347 -0
  560. package/dist/core/structured-output/index.js +3 -0
  561. package/dist/core/structured-output/json-extract.js +70 -0
  562. package/dist/core/structured-output/json-schema-validator.js +90 -0
  563. package/dist/core/structured-output/types.js +2 -0
  564. package/dist/core/sub-agent/artifacts/store.js +141 -0
  565. package/dist/core/sub-agent/artifacts/types.js +2 -0
  566. package/dist/core/sub-agent/controller.js +69 -0
  567. package/dist/core/sub-agent/core/loop.js +79 -0
  568. package/dist/core/sub-agent/core/manager.js +246 -0
  569. package/dist/core/sub-agent/registry-defaults.js +52 -0
  570. package/dist/core/sub-agent/registry.js +35 -0
  571. package/dist/core/sub-agent/tools/task-spawn.js +29 -0
  572. package/dist/core/sub-agent/types.js +23 -0
  573. package/dist/core/target-runtime/command-resolver.js +42 -0
  574. package/dist/core/target-runtime/index.js +3 -0
  575. package/dist/core/target-runtime/profile.js +73 -0
  576. package/dist/core/testgen/detector.js +17 -0
  577. package/dist/core/testgen/index.js +38 -0
  578. package/dist/core/testgen/templates.js +46 -0
  579. package/dist/core/tools/audit.js +140 -0
  580. package/dist/core/tools/authorization/types.js +2 -0
  581. package/dist/core/tools/budget.js +118 -0
  582. package/dist/core/tools/builtin/artifact.js +29 -0
  583. package/dist/core/tools/builtin/ast-grep.js +107 -0
  584. package/dist/core/tools/builtin/ast.js +62 -0
  585. package/dist/core/tools/builtin/code-search/backends/powershell.js +84 -0
  586. package/dist/core/tools/builtin/code-search/backends/rg.js +85 -0
  587. package/dist/core/tools/builtin/code-search/executor.js +87 -0
  588. package/dist/core/tools/builtin/code-search/parse/plain-grep.js +59 -0
  589. package/dist/core/tools/builtin/code-search/parse/rg-json.js +31 -0
  590. package/dist/core/tools/builtin/code-search/spec.js +82 -0
  591. package/dist/core/tools/builtin/fs.js +243 -0
  592. package/dist/core/tools/builtin/git.js +118 -0
  593. package/dist/core/tools/builtin/index.js +80 -0
  594. package/dist/core/tools/builtin/interaction.js +120 -0
  595. package/dist/core/tools/builtin/knowledge.js +98 -0
  596. package/dist/core/tools/builtin/plan.js +148 -0
  597. package/dist/core/tools/builtin/proposal.js +207 -0
  598. package/dist/core/tools/builtin/shell.js +71 -0
  599. package/dist/core/tools/builtin/verify.js +41 -0
  600. package/dist/core/tools/capability/executor.js +84 -0
  601. package/dist/core/tools/capability/runner.js +50 -0
  602. package/dist/core/tools/capability/types.js +2 -0
  603. package/dist/core/tools/dispatcher.js +80 -0
  604. package/dist/core/tools/headless-payload.js +37 -0
  605. package/dist/core/tools/loader.js +100 -0
  606. package/dist/core/tools/mapper.js +142 -0
  607. package/dist/core/tools/mcp/client.js +308 -0
  608. package/dist/core/tools/mcp/loader.js +110 -0
  609. package/dist/core/tools/mcp/schema.js +54 -0
  610. package/dist/core/tools/mcp/streamable-http.js +101 -0
  611. package/dist/core/tools/mcp/types.js +26 -0
  612. package/dist/core/tools/parallel/isolation.js +25 -0
  613. package/dist/core/tools/parallel/lock-manager.js +124 -0
  614. package/dist/core/tools/parallel/persistence.js +126 -0
  615. package/dist/core/tools/parallel/plan-builder.js +66 -0
  616. package/dist/core/tools/parallel/plan.js +2 -0
  617. package/dist/core/tools/parallel/refs.js +7 -0
  618. package/dist/core/tools/parallel/resolve-args.js +50 -0
  619. package/dist/core/tools/parallel/resource-helpers.js +35 -0
  620. package/dist/core/tools/parallel/resources.js +2 -0
  621. package/dist/core/tools/parallel/scheduler.js +372 -0
  622. package/dist/core/tools/parser.js +89 -0
  623. package/dist/core/tools/permissions/permission-rules.js +503 -0
  624. package/dist/core/tools/plugins/loader.js +102 -0
  625. package/dist/core/tools/policy.js +87 -0
  626. package/dist/core/tools/registry.js +29 -0
  627. package/dist/core/tools/router.js +514 -0
  628. package/dist/core/tools/sanitize.js +78 -0
  629. package/dist/core/tools/schema-utils.js +71 -0
  630. package/dist/core/tools/session.js +1105 -0
  631. package/dist/core/tools/streaming/ToolCallAccumulator.js +64 -0
  632. package/dist/core/tools/types.js +2 -0
  633. package/dist/core/types/authorization.js +2 -0
  634. package/dist/core/types/context.js +2 -0
  635. package/dist/core/types/errors.js +29 -0
  636. package/dist/core/types/execution.js +65 -0
  637. package/dist/core/types/index.js +9 -0
  638. package/dist/core/types/llm.js +9 -0
  639. package/dist/core/types/loop.js +2 -0
  640. package/dist/core/types/planning.js +2 -0
  641. package/dist/core/types/runtime.js +2 -0
  642. package/dist/core/types/usage.js +2 -0
  643. package/dist/core/ui/kaomoji.js +5 -0
  644. package/dist/core/utils/path.js +116 -0
  645. package/dist/core/utils/platform-shell.js +10 -0
  646. package/dist/core/utils/sanitizer.js +107 -0
  647. package/dist/core/verification/runner.js +265 -0
  648. package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +272 -0
  649. package/dist/integrations/langfuse/outcome-proxy.js +68 -0
  650. package/dist/interfaces/cli/task-runner.js +11 -0
  651. package/dist/languages/typescript/index.js +178 -0
  652. package/dist/locales/en.js +679 -0
  653. package/dist/locales/index.js +11 -0
  654. package/dist/utils/eol.js +35 -0
  655. package/package.json +153 -0
@@ -0,0 +1,819 @@
1
+ import { createHash, randomBytes } from 'crypto';
2
+ import { tmpdir } from 'os';
3
+ import path from 'path';
4
+ import { text } from '../../../locales/index.js';
5
+ import { TextNormalizer } from '../../../utils/eol.js';
6
+ import { copyFile, lstat, mkdir, readFile, readdir, rm, stat, unlink, writeFile, } from '../../adapters/fs/node-fs.js';
7
+ import { GitAdapter } from '../../adapters/git/git-adapter.js';
8
+ import { logIgnoredError } from '../../observability/ignored-error.js';
9
+ import { getLogger } from '../../observability/logger.js';
10
+ import { getMonitor } from '../../observability/monitor.js';
11
+ import { detectDependencyPaths } from '../layers/shadow-driver/strategy.js';
12
+ const SECURITY_BLOCKLIST = [
13
+ /^\.git(\/|\\)/i,
14
+ /^\.env/i,
15
+ /id_rsa$/i,
16
+ /\.pem$/i,
17
+ /\.key$/i,
18
+ ];
19
+ const BINARY_EXTENSIONS = new Set([
20
+ '.png',
21
+ '.jpg',
22
+ '.jpeg',
23
+ '.gif',
24
+ '.bmp',
25
+ '.ico',
26
+ '.webp',
27
+ '.pdf',
28
+ '.zip',
29
+ '.gz',
30
+ '.tgz',
31
+ '.7z',
32
+ '.rar',
33
+ '.exe',
34
+ '.dll',
35
+ '.bin',
36
+ ]);
37
+ const DEFAULT_MAX_FILE_BYTES = Number(process.env.SALMONLOOP_SECURITY_MAX_FILE_BYTES) || 1024 * 1024;
38
+ const DEFAULT_DEPENDENCY_ROOT_CANDIDATES = ['node_modules'];
39
+ const DIRTY_BACKUP_PREFIX = 'salmon-loop-backup-';
40
+ const DEFAULT_DIRTY_BACKUP_RETENTION_MS = 24 * 60 * 60 * 1000;
41
+ var ApplyStrategy;
42
+ (function (ApplyStrategy) {
43
+ ApplyStrategy["ExplicitMerge"] = "ExplicitMerge";
44
+ ApplyStrategy["AtomicPatch"] = "AtomicPatch";
45
+ })(ApplyStrategy || (ApplyStrategy = {}));
46
+ export class WorkspaceSynchronizer {
47
+ checkpointManager;
48
+ constructor(checkpointManager) {
49
+ this.checkpointManager = checkpointManager;
50
+ }
51
+ normalizePath(value) {
52
+ return value.replace(/\\/g, '/');
53
+ }
54
+ isRenameOrCopyStatus(xy) {
55
+ const x = xy.charAt(0);
56
+ const y = xy.charAt(1);
57
+ return x === 'R' || x === 'C' || y === 'R' || y === 'C';
58
+ }
59
+ async pruneExpiredDirtyBackups() {
60
+ const retentionMs = this.getDirtyBackupRetentionMs();
61
+ if (retentionMs <= 0)
62
+ return;
63
+ const tempRoot = tmpdir();
64
+ let entries;
65
+ try {
66
+ entries = (await readdir(tempRoot, { withFileTypes: true }));
67
+ }
68
+ catch {
69
+ return;
70
+ }
71
+ const cutoffTs = Date.now() - retentionMs;
72
+ const pruneTargets = entries.filter((entry) => entry.isDirectory() && entry.name.startsWith(DIRTY_BACKUP_PREFIX));
73
+ await Promise.all(pruneTargets.map(async (entry) => {
74
+ const backupPath = path.join(tempRoot, entry.name);
75
+ try {
76
+ const backupStat = await stat(backupPath);
77
+ if (backupStat.mtimeMs < cutoffTs) {
78
+ await rm(backupPath, { recursive: true, force: true });
79
+ }
80
+ }
81
+ catch {
82
+ // Ignore stale cleanup failures; cleanup is best-effort.
83
+ }
84
+ }));
85
+ }
86
+ getDirtyBackupRetentionMs() {
87
+ const raw = process.env.SALMONLOOP_DIRTY_BACKUP_RETENTION_MS;
88
+ if (raw === undefined)
89
+ return DEFAULT_DIRTY_BACKUP_RETENTION_MS;
90
+ const parsed = Number(raw);
91
+ if (!Number.isFinite(parsed) || parsed < 0) {
92
+ return DEFAULT_DIRTY_BACKUP_RETENTION_MS;
93
+ }
94
+ return parsed;
95
+ }
96
+ sanitizeRelativePath(value) {
97
+ return this.normalizePath(value).replace(/^\.\//, '').replace(/\/+$/g, '');
98
+ }
99
+ isPathWithinRoots(relativePath, roots) {
100
+ const normalized = this.sanitizeRelativePath(relativePath);
101
+ if (!normalized)
102
+ return false;
103
+ for (const root of roots) {
104
+ if (normalized === root || normalized.startsWith(`${root}/`)) {
105
+ return true;
106
+ }
107
+ }
108
+ return false;
109
+ }
110
+ async getSymlinkedDependencyRoots(repoPath) {
111
+ let detectedDependencyPaths = [];
112
+ try {
113
+ detectedDependencyPaths = await detectDependencyPaths(repoPath);
114
+ }
115
+ catch (error) {
116
+ getLogger().debug(`[checkpoint] Failed to detect dependency paths: ${error instanceof Error ? error.message : String(error)}`);
117
+ }
118
+ const candidates = new Set([
119
+ ...DEFAULT_DEPENDENCY_ROOT_CANDIDATES,
120
+ ...detectedDependencyPaths,
121
+ ]);
122
+ const symlinkedRoots = new Set();
123
+ for (const candidate of candidates) {
124
+ const normalizedCandidate = this.sanitizeRelativePath(candidate);
125
+ if (!normalizedCandidate || normalizedCandidate.includes('/')) {
126
+ continue;
127
+ }
128
+ const candidatePath = path.join(repoPath, ...normalizedCandidate.split('/'));
129
+ try {
130
+ const entryStat = await lstat(candidatePath);
131
+ if (entryStat.isSymbolicLink()) {
132
+ symlinkedRoots.add(normalizedCandidate);
133
+ }
134
+ }
135
+ catch {
136
+ // Ignore non-existent dependency roots.
137
+ }
138
+ }
139
+ return symlinkedRoots;
140
+ }
141
+ async filterOutSymlinkedDependencyPaths(repoPath, relativePaths, logPrefix) {
142
+ const symlinkedRoots = await this.getSymlinkedDependencyRoots(repoPath);
143
+ if (symlinkedRoots.size === 0) {
144
+ return relativePaths;
145
+ }
146
+ const filtered = [];
147
+ for (const relativePath of relativePaths) {
148
+ if (this.isPathWithinRoots(relativePath, symlinkedRoots)) {
149
+ getLogger().debug(`[${logPrefix}] Skipping symlinked dependency path: ${relativePath}`);
150
+ continue;
151
+ }
152
+ filtered.push(relativePath);
153
+ }
154
+ return filtered;
155
+ }
156
+ isBlockedPath(relativePath) {
157
+ const normalized = this.normalizePath(relativePath);
158
+ return SECURITY_BLOCKLIST.some((pattern) => pattern.test(normalized));
159
+ }
160
+ isBinaryPath(relativePath) {
161
+ const ext = path.extname(relativePath).toLowerCase();
162
+ return BINARY_EXTENSIONS.has(ext);
163
+ }
164
+ async stagePathForCheckpoint(git, relativePath) {
165
+ const directAdd = await git.execMeta(['add', '--', relativePath]);
166
+ if (directAdd.ok) {
167
+ return 'staged';
168
+ }
169
+ // Use check-ignore (exit code) instead of stderr text matching, which is locale-dependent.
170
+ const pathIsIgnored = await git.checkIgnore(relativePath);
171
+ if (!pathIsIgnored) {
172
+ throw new Error(`Failed to stage path "${relativePath}": ${directAdd.stderr || `git add exited with code ${directAdd.code ?? 'unknown'}`}`);
173
+ }
174
+ // Tracked files that match ignore rules can fail on explicit path add.
175
+ // Fallback to `add -u` stages tracked changes without force-adding ignored untracked files.
176
+ const trackedFallback = await git.execMeta(['add', '-u', '--', relativePath]);
177
+ if (trackedFallback.ok) {
178
+ return 'staged';
179
+ }
180
+ const trackedProbe = await git.execMeta(['ls-files', '--error-unmatch', '--', relativePath]);
181
+ if (pathIsIgnored && !trackedProbe.ok && trackedProbe.code === 1) {
182
+ getLogger().debug(`[checkpoint] Skipping ignored untracked path during checkpoint staging: ${relativePath}`);
183
+ return 'skipped';
184
+ }
185
+ throw new Error(`Failed to stage path "${relativePath}" with tracked fallback: ${trackedFallback.stderr || `git add -u exited with code ${trackedFallback.code ?? 'unknown'}`}`);
186
+ }
187
+ async shouldAllowPath(repoPath, relativePath, options) {
188
+ if (this.isBlockedPath(relativePath)) {
189
+ return { allowed: false, reason: 'blocked-path' };
190
+ }
191
+ try {
192
+ const filePath = path.join(repoPath, ...relativePath.split('/'));
193
+ const fileStat = await stat(filePath);
194
+ if (fileStat.size > DEFAULT_MAX_FILE_BYTES) {
195
+ return { allowed: false, reason: 'size-limit' };
196
+ }
197
+ }
198
+ catch (error) {
199
+ const err = error;
200
+ if (err?.code === 'ENOENT') {
201
+ if (options?.allowMissing === false) {
202
+ return { allowed: false, reason: 'missing' };
203
+ }
204
+ return { allowed: true };
205
+ }
206
+ return { allowed: false, reason: 'stat-failed' };
207
+ }
208
+ return { allowed: true };
209
+ }
210
+ async getChangedPaths(repoPath) {
211
+ const git = new GitAdapter(repoPath);
212
+ const status = await git.query(['status', '--porcelain', '--untracked-files=all', '--ignored=no', '-z'], { trim: false });
213
+ if (!status)
214
+ return [];
215
+ const tokens = status.split('\0').filter((token) => token.length > 0);
216
+ const paths = [];
217
+ const extractPath = (entry) => {
218
+ const maybeSep = entry[2];
219
+ if (maybeSep === ' ' || maybeSep === '\t') {
220
+ return entry.slice(3);
221
+ }
222
+ return entry.slice(2);
223
+ };
224
+ for (let i = 0; i < tokens.length; i += 1) {
225
+ const entry = tokens[i];
226
+ const code = entry.slice(0, 2);
227
+ if (code === '!!')
228
+ continue;
229
+ const pathPart = extractPath(entry);
230
+ if (!pathPart)
231
+ continue;
232
+ if (this.isRenameOrCopyStatus(code)) {
233
+ const original = pathPart;
234
+ const renamed = tokens[i + 1];
235
+ if (original)
236
+ paths.push(original);
237
+ if (renamed)
238
+ paths.push(renamed);
239
+ i += 1;
240
+ continue;
241
+ }
242
+ paths.push(pathPart);
243
+ }
244
+ const unique = Array.from(new Set(paths.map((p) => this.normalizePath(p))));
245
+ const filteredPaths = await this.filterOutSymlinkedDependencyPaths(repoPath, unique, 'checkpoint');
246
+ const allowed = [];
247
+ for (const file of filteredPaths) {
248
+ const policy = await this.shouldAllowPath(repoPath, file);
249
+ if (!policy.allowed) {
250
+ getLogger().warn(text.loop.skipPathDueToPolicy(policy.reason, file));
251
+ continue;
252
+ }
253
+ allowed.push(file);
254
+ }
255
+ return allowed;
256
+ }
257
+ async createCheckpointCommit(worktreePath, taskId, stepId) {
258
+ const changedPaths = await this.getChangedPaths(worktreePath);
259
+ if (changedPaths.length === 0) {
260
+ return null;
261
+ }
262
+ const git = new GitAdapter(worktreePath);
263
+ let stagedCount = 0;
264
+ for (const changedPath of changedPaths) {
265
+ const result = await this.stagePathForCheckpoint(git, changedPath);
266
+ if (result === 'staged') {
267
+ stagedCount += 1;
268
+ }
269
+ }
270
+ if (stagedCount === 0) {
271
+ return null;
272
+ }
273
+ const stagedNames = await git.query(['diff', '--cached', '--name-only']);
274
+ if (!stagedNames.trim()) {
275
+ return null;
276
+ }
277
+ await git.exec([
278
+ '-c',
279
+ 'user.name=salmonloop',
280
+ '-c',
281
+ 'user.email=salmonloop@local',
282
+ 'commit',
283
+ '--no-verify',
284
+ '--no-gpg-sign',
285
+ '-m',
286
+ `checkpoint: ${stepId}`,
287
+ ]);
288
+ const head = await git.query(['rev-parse', 'HEAD']);
289
+ await git.exec(['update-ref', `refs/ai-agent/checkpoints/${taskId}/${stepId}`, head]);
290
+ return head;
291
+ }
292
+ async analyzeStrategy(shadowWorktreePath, initialRef, latestRef) {
293
+ const git = new GitAdapter(shadowWorktreePath);
294
+ // Parse diff status: A=Add, M=Modify, D=Delete, R=Rename, C=Copy, T=Type
295
+ const output = await git.query(['diff', '--name-status', '-z', initialRef, latestRef]);
296
+ if (!output)
297
+ return ApplyStrategy.ExplicitMerge; // No changes? Default safe
298
+ const tokens = output.split('\0').filter((t) => t.length > 0);
299
+ for (let i = 0; i < tokens.length; i++) {
300
+ const statusToken = tokens[i];
301
+ const status = statusToken.charAt(0);
302
+ // Check topology changes
303
+ if (['R', 'D', 'A', 'C', 'T'].includes(status)) {
304
+ getLogger().debug(`[SmartRoute] Topology change detected (${status}), upgrading to AtomicPatch.`);
305
+ return ApplyStrategy.AtomicPatch;
306
+ }
307
+ const filePath = tokens[i + 1];
308
+ i++; // Skip path
309
+ // Check binary files
310
+ if (filePath && this.isBinaryPath(filePath)) {
311
+ getLogger().debug(`[SmartRoute] Binary file detected (${filePath}), upgrading to AtomicPatch.`);
312
+ return ApplyStrategy.AtomicPatch;
313
+ }
314
+ }
315
+ getLogger().debug('[SmartRoute] Pure text modifications detected, using ExplicitMerge.');
316
+ return ApplyStrategy.ExplicitMerge;
317
+ }
318
+ async applyExplicitMerge(mainRepoPath, shadowWorktreePath, initialRef, latestRef) {
319
+ const shadowGit = new GitAdapter(shadowWorktreePath);
320
+ const mainGit = new GitAdapter(mainRepoPath);
321
+ const conflicts = [];
322
+ // Get list of modified files (we know they are 'M' only from analysis)
323
+ const output = await shadowGit.query(['diff', '--name-only', '-z', initialRef, latestRef]);
324
+ const files = output.split('\0').filter((f) => f.length > 0);
325
+ for (const relativePath of files) {
326
+ const tempBase = path.join(tmpdir(), `sl-base-${randomBytes(4).toString('hex')}`);
327
+ const tempTheirs = path.join(tmpdir(), `sl-theirs-${randomBytes(4).toString('hex')}`);
328
+ const tempOurs = path.join(tmpdir(), `sl-ours-${randomBytes(4).toString('hex')}`); // For normalization logic if needed
329
+ try {
330
+ // 1. Get Base Content (from Shadow ODB)
331
+ const baseContent = await shadowGit.show(initialRef, relativePath);
332
+ // 2. Get Theirs Content (from Shadow ODB)
333
+ const theirsContent = await shadowGit.show(latestRef, relativePath);
334
+ // 3. Get Ours Content (from Main Working Tree)
335
+ const mainAbsPath = path.join(mainRepoPath, ...relativePath.split('/'));
336
+ let oursContent;
337
+ try {
338
+ oursContent = await readFile(mainAbsPath);
339
+ }
340
+ catch {
341
+ // If file missing in main but modify in shadow -> conflict or re-create?
342
+ // Since we filtered for 'M', it implies it existed in Base. If missing in Main, User deleted it.
343
+ // Merge Modified vs Deleted -> Conflict.
344
+ conflicts.push(relativePath);
345
+ continue;
346
+ }
347
+ // --- EOL Normalization ---
348
+ const oursStr = oursContent.toString('utf8');
349
+ const { eol: targetEOL } = TextNormalizer.read(oursStr);
350
+ const normBase = TextNormalizer.read(baseContent.toString('utf8')).normalized;
351
+ const normTheirs = TextNormalizer.read(theirsContent.toString('utf8')).normalized;
352
+ const normOurs = TextNormalizer.read(oursStr).normalized;
353
+ await writeFile(tempBase, normBase);
354
+ await writeFile(tempTheirs, normTheirs);
355
+ await writeFile(tempOurs, normOurs); // Use normalized ours for merge-file
356
+ // 4. Perform 3-Way Merge
357
+ // git merge-file -p ours base theirs
358
+ const mergeResult = await mainGit.mergeFile(tempBase, tempOurs, tempTheirs);
359
+ // 5. Restore EOL
360
+ const mergedStr = mergeResult.content.toString('utf8');
361
+ const restoredStr = TextNormalizer.restore(mergedStr, targetEOL);
362
+ // 6. Write Back
363
+ await writeFile(mainAbsPath, restoredStr);
364
+ if (mergeResult.hasConflict) {
365
+ getLogger().warn(`[ExplicitMerge] Conflict detected in ${relativePath}`);
366
+ conflicts.push(relativePath);
367
+ }
368
+ }
369
+ catch (err) {
370
+ getLogger().error(`[ExplicitMerge] Failed to merge ${relativePath}: ${err}`);
371
+ throw err;
372
+ }
373
+ finally {
374
+ // Cleanup temps
375
+ await Promise.all([
376
+ unlink(tempBase).catch((error) => logIgnoredError(`[ExplicitMerge] cleanup ${tempBase}`, error)),
377
+ unlink(tempTheirs).catch((error) => logIgnoredError(`[ExplicitMerge] cleanup ${tempTheirs}`, error)),
378
+ unlink(tempOurs).catch((error) => logIgnoredError(`[ExplicitMerge] cleanup ${tempOurs}`, error)),
379
+ ]);
380
+ }
381
+ }
382
+ return { conflicts };
383
+ }
384
+ async applyAtomicPatch(mainRepoPath, shadowWorktreePath, initialRef, latestRef) {
385
+ const git = new GitAdapter(shadowWorktreePath);
386
+ const changedPathsOutput = await git.query(['diff', '--name-only', '-z', initialRef, latestRef], { trim: false });
387
+ if (!changedPathsOutput)
388
+ return;
389
+ const changedPaths = changedPathsOutput
390
+ .split('\0')
391
+ .filter((entry) => entry.length > 0)
392
+ .map((entry) => this.normalizePath(entry));
393
+ const uniqueChangedPaths = Array.from(new Set(changedPaths));
394
+ const filteredPaths = await this.filterOutSymlinkedDependencyPaths(shadowWorktreePath, uniqueChangedPaths, 'applyBack');
395
+ if (filteredPaths.length === 0) {
396
+ getLogger().info('[applyBack] Skipping AtomicPatch because only dependency projection paths changed.');
397
+ return;
398
+ }
399
+ const diffText = await git.query([
400
+ 'diff',
401
+ '--binary',
402
+ '--full-index',
403
+ '--no-color',
404
+ '--no-ext-diff',
405
+ initialRef,
406
+ latestRef,
407
+ '--',
408
+ ...filteredPaths,
409
+ ], { trim: false });
410
+ if (!diffText.trim())
411
+ return;
412
+ const mainGit = new GitAdapter(mainRepoPath);
413
+ try {
414
+ await mainGit.applyPatch(diffText, {
415
+ threeWay: true,
416
+ contextLines: 3,
417
+ preserveIndexLines: false,
418
+ });
419
+ }
420
+ catch (error) {
421
+ throw new Error(`Apply-back completed with conflicts (Atomic Patch). Rejection files (.rej) have been generated. Original error: ${error instanceof Error ? error.message : String(error)}`);
422
+ }
423
+ }
424
+ parseStatusEntries(statusPorcelainZ) {
425
+ if (!statusPorcelainZ)
426
+ return [];
427
+ const tokens = statusPorcelainZ.split('\0').filter((token) => token.length > 0);
428
+ const entries = [];
429
+ for (let i = 0; i < tokens.length; i += 1) {
430
+ const entry = tokens[i];
431
+ const xy = entry.slice(0, 2);
432
+ if (entry.length < 3)
433
+ continue;
434
+ const separator = entry[2];
435
+ const primaryPath = separator === ' ' || separator === '\t' ? entry.slice(3) : entry.slice(2);
436
+ if (!primaryPath)
437
+ continue;
438
+ if (this.isRenameOrCopyStatus(xy)) {
439
+ const renamedPath = tokens[i + 1];
440
+ if (renamedPath) {
441
+ entries.push({ xy, path: renamedPath, origPath: primaryPath });
442
+ i += 1;
443
+ continue;
444
+ }
445
+ }
446
+ entries.push({ xy, path: primaryPath });
447
+ }
448
+ return entries;
449
+ }
450
+ async applyBackToMainWorkspace(mainRepoPath, checkpointRef, diffText, applyBackOnDirty = '3way', verbose, changedFiles, shadowInitialRef, shadowLatestRef, _includePaths = [], telemetry) {
451
+ const startTime = Date.now();
452
+ if (telemetry) {
453
+ telemetry.startedAt = new Date().toISOString();
454
+ telemetry.policy = applyBackOnDirty;
455
+ telemetry.usedShadowRefs = Boolean(shadowInitialRef && shadowLatestRef);
456
+ telemetry.rollbackPath = 'none';
457
+ telemetry.dirtyBackupCreated = false;
458
+ telemetry.stagedRestoreAttempted = false;
459
+ telemetry.stagedRestoreSucceeded = false;
460
+ telemetry.stagedRestoreError = undefined;
461
+ telemetry.appliedToMain = false;
462
+ }
463
+ let applySuccess = false;
464
+ let applyError;
465
+ let appliedToMain = false;
466
+ let dirtyBackup = null;
467
+ let didBeginApply = false;
468
+ let fingerprintFn = null;
469
+ let originalFingerprint = null;
470
+ try {
471
+ // Smart Routing Logic: Determine Strategy
472
+ // If we have Shadow Refs, we can choose between Explicit Merge and Atomic Patch
473
+ let strategy = ApplyStrategy.AtomicPatch; // Fallback default
474
+ if (shadowInitialRef && shadowLatestRef) {
475
+ strategy = await this.analyzeStrategy(checkpointRef.worktreePath, shadowInitialRef, shadowLatestRef);
476
+ getLogger().info(`[applyBack] Smart Routing selected strategy: ${strategy}`);
477
+ }
478
+ if (telemetry) {
479
+ telemetry.selectedStrategy = strategy;
480
+ }
481
+ // Force AtomicPatch if applyBackOnDirty is 'abort' or other strict modes?
482
+ // Actually, ExplicitMerge is SAFER for dirty workspaces because it does 3-way content merge.
483
+ // But if user requested 'abort' on dirty, we check dirty below anyway.
484
+ // Pre-flight check for dirty workspace
485
+ const git = new GitAdapter(mainRepoPath);
486
+ // Use standard porcelain (v1) for compatibility
487
+ const status = await git.query(['status', '--porcelain', '-z'], { trim: false });
488
+ const trimmedStatus = status.replace(/\0/g, '').trim();
489
+ const printableStatus = status.replace(/\0/g, '\n').trim();
490
+ const isDirty = trimmedStatus.length > 0;
491
+ if (telemetry) {
492
+ telemetry.dirtyAtEntry = isDirty;
493
+ }
494
+ if (isDirty && applyBackOnDirty === 'abort') {
495
+ throw new Error(text.loop.applyBackAbortedDirty(printableStatus));
496
+ }
497
+ // --- Safety: Backup Dirty State ---
498
+ // We implement "Undo Log" pattern: Backup -> Apply -> Restore if Fail
499
+ // This applies to BOTH strategies to ensure atomicity via rollback
500
+ const hashContent = (value) => createHash('sha256').update(value).digest('hex');
501
+ const normalizeFilePath = (value) => value.replace(/\\/g, '/');
502
+ const computeFingerprint = async () => {
503
+ const head = await git.query(['rev-parse', 'HEAD']);
504
+ const index = await git.query(['write-tree']);
505
+ const workingDiff = await git.query(['diff', '--binary', '--no-color', '--no-ext-diff']);
506
+ const working = hashContent(workingDiff);
507
+ const untrackedOutput = await git.query(['ls-files', '--others', '--exclude-standard']);
508
+ const untrackedFiles = untrackedOutput
509
+ .split('\n')
510
+ .map((line) => line.trim())
511
+ .filter((line) => line.length > 0)
512
+ .sort();
513
+ let untracked = '';
514
+ if (untrackedFiles.length > 0) {
515
+ const entries = [];
516
+ for (const file of untrackedFiles) {
517
+ try {
518
+ const content = await readFile(path.join(mainRepoPath, ...file.split('/')));
519
+ entries.push(`${file}:${hashContent(content)}`);
520
+ }
521
+ catch {
522
+ entries.push(`${file}:missing`);
523
+ }
524
+ }
525
+ untracked = hashContent(entries.join('\n'));
526
+ }
527
+ else {
528
+ untracked = hashContent('');
529
+ }
530
+ return { head, index, working, untracked };
531
+ };
532
+ fingerprintFn = computeFingerprint;
533
+ originalFingerprint = await computeFingerprint();
534
+ // Always create dirty backup if dirty and policy is 3way
535
+ // This protects against partial failures in ExplicitMerge loop AND AtomicPatch
536
+ if (isDirty && applyBackOnDirty === '3way') {
537
+ const createDirtyBackup = async () => {
538
+ await this.pruneExpiredDirtyBackups();
539
+ const backupDir = path.join(tmpdir(), `${DIRTY_BACKUP_PREFIX}${Date.now()}-${randomBytes(4).toString('hex')}`);
540
+ await mkdir(backupDir, { recursive: true });
541
+ const stagedTree = (await git.query(['write-tree'])).trim();
542
+ const stagedPatch = await git.query(['diff', '--cached', '--binary'], { trim: false });
543
+ let stagedPatchPath;
544
+ if (stagedPatch.trim()) {
545
+ stagedPatchPath = path.join(backupDir, 'staged.patch');
546
+ await writeFile(stagedPatchPath, stagedPatch);
547
+ }
548
+ const unstagedPatch = await git.query(['diff', '--binary'], { trim: false });
549
+ if (unstagedPatch.trim()) {
550
+ await writeFile(path.join(backupDir, 'unstaged.patch'), unstagedPatch);
551
+ }
552
+ // Simple full file backup for dirty tracked files (robustness)
553
+ const statusEntries = this.parseStatusEntries(status);
554
+ const isDeleted = (xy) => xy.includes('D');
555
+ const dirtyFiles = statusEntries
556
+ .filter((e) => e.xy !== '??' && !isDeleted(e.xy))
557
+ .map((e) => normalizeFilePath(e.path));
558
+ const deletedFiles = statusEntries
559
+ .filter((e) => e.xy !== '??' && isDeleted(e.xy))
560
+ .map((e) => normalizeFilePath(e.path));
561
+ if (dirtyFiles.length > 0) {
562
+ const trackedDir = path.join(backupDir, 'tracked');
563
+ for (const file of dirtyFiles) {
564
+ const src = path.join(mainRepoPath, ...file.split('/'));
565
+ const dst = path.join(trackedDir, ...file.split('/'));
566
+ await mkdir(path.dirname(dst), { recursive: true });
567
+ try {
568
+ await copyFile(src, dst);
569
+ }
570
+ catch {
571
+ // Ignore backup failure for deleted files
572
+ }
573
+ }
574
+ }
575
+ // Backup untracked
576
+ const untrackedFiles = (await git.query(['ls-files', '--others', '--exclude-standard']))
577
+ .split('\n')
578
+ .map((l) => l.trim())
579
+ .filter((l) => l.length > 0);
580
+ if (untrackedFiles.length > 0) {
581
+ const untrackedDir = path.join(backupDir, 'untracked');
582
+ for (const file of untrackedFiles) {
583
+ const src = path.join(mainRepoPath, ...file.split('/'));
584
+ const dst = path.join(untrackedDir, ...file.split('/'));
585
+ await mkdir(path.dirname(dst), { recursive: true });
586
+ await copyFile(src, dst);
587
+ }
588
+ }
589
+ // Metadata
590
+ await writeFile(path.join(backupDir, 'status.txt'), printableStatus);
591
+ return {
592
+ dir: backupDir,
593
+ trackedFiles: dirtyFiles,
594
+ untrackedFiles,
595
+ deletedFiles,
596
+ stagedTree,
597
+ stagedPatchPath,
598
+ };
599
+ };
600
+ dirtyBackup = (await createDirtyBackup());
601
+ getLogger().info(text.loop.applyBackCheckpointCreated());
602
+ getLogger().info(text.loop.applyBackCheckpointLocation(dirtyBackup?.dir || ''));
603
+ if (telemetry) {
604
+ telemetry.dirtyBackupCreated = true;
605
+ telemetry.dirtyBackupDir = dirtyBackup?.dir || undefined;
606
+ }
607
+ }
608
+ // --- EXECUTION PHASE ---
609
+ try {
610
+ didBeginApply = true;
611
+ if (telemetry) {
612
+ telemetry.didBeginApply = true;
613
+ }
614
+ if (shadowInitialRef && shadowLatestRef) {
615
+ if (strategy === ApplyStrategy.ExplicitMerge) {
616
+ getLogger().info('[applyBack] Executing ExplicitMerge (Smart Routing)');
617
+ const result = await this.applyExplicitMerge(mainRepoPath, checkpointRef.worktreePath, shadowInitialRef, shadowLatestRef);
618
+ if (result.conflicts.length > 0) {
619
+ getLogger().warn(`[applyBack] ExplicitMerge completed with ${result.conflicts.length} conflicts.`);
620
+ // NOTE: We do NOT throw here. Markers are in the files.
621
+ // This is "Success with Conflicts".
622
+ }
623
+ }
624
+ else {
625
+ getLogger().info('[applyBack] Executing AtomicPatch (Smart Routing)');
626
+ await this.applyAtomicPatch(mainRepoPath, checkpointRef.worktreePath, shadowInitialRef, shadowLatestRef);
627
+ }
628
+ }
629
+ else {
630
+ // Fallback if no shadow refs (legacy flow or raw patch)
631
+ // Always use Atomic Patch for raw diffs
632
+ getLogger().info('[applyBack] Executing Raw Patch Apply');
633
+ const git = new GitAdapter(mainRepoPath);
634
+ await git.applyPatch(diffText, { threeWay: true });
635
+ }
636
+ appliedToMain = true;
637
+ applySuccess = true;
638
+ if (telemetry) {
639
+ telemetry.appliedToMain = true;
640
+ telemetry.error = undefined;
641
+ }
642
+ }
643
+ catch (err) {
644
+ applyError = err;
645
+ throw applyError;
646
+ }
647
+ }
648
+ catch (error) {
649
+ const err = error instanceof Error ? error : new Error(String(error));
650
+ if (telemetry) {
651
+ telemetry.error = err.message;
652
+ }
653
+ if (!didBeginApply) {
654
+ err.appliedToMain = appliedToMain;
655
+ throw err;
656
+ }
657
+ // If applyBack did not mutate the workspace, do not run rollback routines.
658
+ // This prevents "rollback" from becoming the thing that corrupts the user's state.
659
+ let workspaceChanged = true;
660
+ if (fingerprintFn && originalFingerprint) {
661
+ try {
662
+ const current = await fingerprintFn();
663
+ workspaceChanged =
664
+ current.head !== originalFingerprint.head ||
665
+ current.index !== originalFingerprint.index ||
666
+ current.working !== originalFingerprint.working ||
667
+ current.untracked !== originalFingerprint.untracked;
668
+ }
669
+ catch {
670
+ // If fingerprinting fails, assume changed to be safe.
671
+ workspaceChanged = true;
672
+ }
673
+ }
674
+ if (telemetry) {
675
+ telemetry.workspaceChangedAfterFailure = workspaceChanged;
676
+ }
677
+ if (!workspaceChanged) {
678
+ if (telemetry) {
679
+ telemetry.rollbackPath = 'skipped-no-change';
680
+ }
681
+ err.appliedToMain = appliedToMain;
682
+ throw err;
683
+ }
684
+ // Rollback Logic
685
+ if (dirtyBackup) {
686
+ if (telemetry) {
687
+ telemetry.rollbackPath = 'dirtyBackup';
688
+ }
689
+ getLogger().warn(text.loop.applyBackRollbackAttempt);
690
+ getLogger().warn(text.loop.checkpointLocation(dirtyBackup.dir));
691
+ const git = new GitAdapter(mainRepoPath);
692
+ // Best-effort cleanup: even if git is in a conflicted/unmerged state, we must still restore files.
693
+ await git.exec(['reset', '--hard', 'HEAD'], { allowError: true });
694
+ await git.exec(['clean', '-fd', '-e', '.salmonloop'], { allowError: true });
695
+ // Re-apply deletions from the original dirty state (T1).
696
+ for (const file of dirtyBackup.deletedFiles) {
697
+ await rm(path.join(mainRepoPath, ...file.split('/')), {
698
+ recursive: true,
699
+ force: true,
700
+ }).catch((error) => logIgnoredError(`[applyBack] cleanup ${file}`, error));
701
+ }
702
+ // Restore tracked files from the backup snapshot (authoritative for dirty preservation).
703
+ if (dirtyBackup.trackedFiles) {
704
+ const trackedDir = path.join(dirtyBackup.dir, 'tracked');
705
+ for (const file of dirtyBackup.trackedFiles) {
706
+ try {
707
+ await mkdir(path.dirname(path.join(mainRepoPath, ...file.split('/'))), {
708
+ recursive: true,
709
+ });
710
+ await copyFile(path.join(trackedDir, ...file.split('/')), path.join(mainRepoPath, ...file.split('/')));
711
+ }
712
+ catch (e) {
713
+ getLogger().error(`[applyBack] Failed to restore tracked file ${file}: ${e instanceof Error ? e.message : String(e)}`);
714
+ }
715
+ }
716
+ }
717
+ // Restore untracked files (best-effort).
718
+ if (dirtyBackup.untrackedFiles) {
719
+ const untrackedDir = path.join(dirtyBackup.dir, 'untracked');
720
+ for (const file of dirtyBackup.untrackedFiles) {
721
+ try {
722
+ await mkdir(path.dirname(path.join(mainRepoPath, ...file.split('/'))), {
723
+ recursive: true,
724
+ });
725
+ await copyFile(path.join(untrackedDir, ...file.split('/')), path.join(mainRepoPath, ...file.split('/')));
726
+ }
727
+ catch {
728
+ // Ignore restore errors for untracked files
729
+ }
730
+ }
731
+ }
732
+ // CRITICAL SAFETY: Restore staged/index state to preserve user intent.
733
+ if (dirtyBackup.stagedPatchPath) {
734
+ if (telemetry) {
735
+ telemetry.stagedRestoreAttempted = true;
736
+ }
737
+ try {
738
+ await git.exec(['apply', '--cached', '--binary', dirtyBackup.stagedPatchPath]);
739
+ if (telemetry) {
740
+ telemetry.stagedRestoreSucceeded = true;
741
+ telemetry.stagedRestoreError = undefined;
742
+ }
743
+ }
744
+ catch (e) {
745
+ const patchError = e instanceof Error ? e.message : String(e);
746
+ getLogger().error(`[applyBack] Failed to restore staged state from patch. ${patchError}. ` +
747
+ `Falling back to read-tree restore.`);
748
+ try {
749
+ await git.exec(['read-tree', dirtyBackup.stagedTree]);
750
+ if (telemetry) {
751
+ telemetry.stagedRestoreSucceeded = true;
752
+ telemetry.stagedRestoreError = undefined;
753
+ }
754
+ }
755
+ catch (fallbackError) {
756
+ const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
757
+ if (telemetry) {
758
+ telemetry.stagedRestoreSucceeded = false;
759
+ telemetry.stagedRestoreError = `${patchError}; fallback read-tree failed: ${fallbackMessage}`;
760
+ }
761
+ getLogger().error(`[applyBack] CRITICAL: Failed to restore staged state from backup. ` +
762
+ `Patch error: ${patchError}; fallback read-tree error: ${fallbackMessage}`);
763
+ }
764
+ }
765
+ }
766
+ await git.exec(['update-index', '--refresh'], { allowError: true });
767
+ }
768
+ else {
769
+ // Workspace was clean at entry, but applyBack still wrote something (e.g. conflict markers).
770
+ // Safety-first policy: prefer explicit snapshot restore; only fallback to HEAD reset if snapshot restore fails.
771
+ let restoredFromSnapshot = false;
772
+ try {
773
+ await this.checkpointManager.restoreToMain(mainRepoPath, checkpointRef.baseRef, true);
774
+ const git = new GitAdapter(mainRepoPath);
775
+ await git.exec(['update-index', '--refresh'], { allowError: true });
776
+ restoredFromSnapshot = true;
777
+ if (telemetry) {
778
+ telemetry.rollbackPath = 'cleanSnapshot';
779
+ }
780
+ }
781
+ catch (snapshotRestoreError) {
782
+ getLogger().error(`[applyBack] Snapshot restore failed during clean rollback. ` +
783
+ `baseRef=${checkpointRef.baseRef}; ` +
784
+ `error=${snapshotRestoreError instanceof Error ? snapshotRestoreError.message : String(snapshotRestoreError)}. ` +
785
+ `Falling back to clean reset.`);
786
+ }
787
+ if (!restoredFromSnapshot) {
788
+ if (telemetry) {
789
+ telemetry.rollbackPath = 'cleanReset';
790
+ }
791
+ const git = new GitAdapter(mainRepoPath);
792
+ await git.exec(['reset', '--hard', 'HEAD'], { allowError: true });
793
+ await git.exec(['clean', '-fd', '-e', '.salmonloop'], { allowError: true });
794
+ await git.exec(['update-index', '--refresh'], { allowError: true });
795
+ }
796
+ }
797
+ err.appliedToMain = appliedToMain;
798
+ throw err;
799
+ }
800
+ finally {
801
+ if (dirtyBackup?.dir && applySuccess) {
802
+ try {
803
+ await rm(dirtyBackup.dir, { recursive: true, force: true });
804
+ }
805
+ catch (cleanupError) {
806
+ getLogger().debug(`[applyBack] Failed to cleanup dirty backup ${dirtyBackup.dir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
807
+ }
808
+ }
809
+ if (telemetry) {
810
+ telemetry.finishedAt = new Date().toISOString();
811
+ }
812
+ // Record monitoring metrics
813
+ const duration = Date.now() - startTime;
814
+ getMonitor().recordApplyBack(applySuccess, duration);
815
+ getLogger().info(`applyBack completed in ${duration}ms, success: ${applySuccess}`);
816
+ }
817
+ }
818
+ }
819
+ //# sourceMappingURL=synchronizer.js.map