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.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/README.zh-CN.md +144 -0
- package/dist/cli/argv/headless-detection.js +60 -0
- package/dist/cli/argv/print-mode.js +60 -0
- package/dist/cli/authorization/allowlist.js +908 -0
- package/dist/cli/authorization/non-interactive.js +166 -0
- package/dist/cli/authorization/provider.js +416 -0
- package/dist/cli/chat-interface.js +83 -0
- package/dist/cli/chat.js +492 -0
- package/dist/cli/cli-runtime-context.js +12 -0
- package/dist/cli/commander-error-adapter.js +35 -0
- package/dist/cli/commander-error-meta.js +13 -0
- package/dist/cli/commands/allowlist.js +270 -0
- package/dist/cli/commands/chat.js +120 -0
- package/dist/cli/commands/config.js +250 -0
- package/dist/cli/commands/context.js +57 -0
- package/dist/cli/commands/dispatcher.js +53 -0
- package/dist/cli/commands/exit.js +9 -0
- package/dist/cli/commands/llm-output.js +135 -0
- package/dist/cli/commands/log-mode.js +143 -0
- package/dist/cli/commands/mode.js +136 -0
- package/dist/cli/commands/new.js +18 -0
- package/dist/cli/commands/parallel.js +256 -0
- package/dist/cli/commands/queue.js +130 -0
- package/dist/cli/commands/registry.js +85 -0
- package/dist/cli/commands/restore.js +26 -0
- package/dist/cli/commands/run/assistant-message.js +14 -0
- package/dist/cli/commands/run/config-resolution.js +37 -0
- package/dist/cli/commands/run/early-errors.js +108 -0
- package/dist/cli/commands/run/execute.js +73 -0
- package/dist/cli/commands/run/extensions-resolution.js +22 -0
- package/dist/cli/commands/run/handler.js +434 -0
- package/dist/cli/commands/run/headless-error-writer.js +182 -0
- package/dist/cli/commands/run/instruction-guard.js +24 -0
- package/dist/cli/commands/run/loop-params.js +46 -0
- package/dist/cli/commands/run/mode.js +8 -0
- package/dist/cli/commands/run/parse-options.js +67 -0
- package/dist/cli/commands/run/persist-session.js +35 -0
- package/dist/cli/commands/run/preflight.js +156 -0
- package/dist/cli/commands/run/reporter-factory.js +52 -0
- package/dist/cli/commands/run/runtime-llm.js +56 -0
- package/dist/cli/commands/run/runtime-options.js +30 -0
- package/dist/cli/commands/run/session.js +19 -0
- package/dist/cli/commands/run/structured-output.js +106 -0
- package/dist/cli/commands/run/types.js +2 -0
- package/dist/cli/commands/run/validate-options.js +28 -0
- package/dist/cli/commands/run/verbose.js +36 -0
- package/dist/cli/commands/run.js +2 -0
- package/dist/cli/commands/serve.js +323 -0
- package/dist/cli/commands/session.js +77 -0
- package/dist/cli/commands/snapshot-interactive.js +165 -0
- package/dist/cli/commands/snapshot.js +159 -0
- package/dist/cli/commands/status.js +17 -0
- package/dist/cli/commands/subagent.js +178 -0
- package/dist/cli/commands/subcommand-suggestions.js +63 -0
- package/dist/cli/commands/tool-names.js +155 -0
- package/dist/cli/commands/types.js +2 -0
- package/dist/cli/commands/utils.js +42 -0
- package/dist/cli/config.js +16 -0
- package/dist/cli/crash-reporter.js +5 -0
- package/dist/cli/headless/anthropic-stream-normalized-encoder.js +164 -0
- package/dist/cli/headless/anthropic-stream-protocol.js +62 -0
- package/dist/cli/headless/json-protocol.js +124 -0
- package/dist/cli/headless/native-stream-normalized-encoder.js +206 -0
- package/dist/cli/headless/openai-responses-canonical-applier.js +94 -0
- package/dist/cli/headless/openai-responses-state.js +294 -0
- package/dist/cli/headless/openai-stream-encoder.js +152 -0
- package/dist/cli/headless/stdout-writer.js +9 -0
- package/dist/cli/headless/stream-json-protocol.js +136 -0
- package/dist/cli/index.js +8 -0
- package/dist/cli/locales/en.js +409 -0
- package/dist/cli/locales/index.js +7 -0
- package/dist/cli/program-bootstrap.js +14 -0
- package/dist/cli/program-commands.js +106 -0
- package/dist/cli/program-options.js +15 -0
- package/dist/cli/program-output-mode.js +11 -0
- package/dist/cli/program-parse.js +24 -0
- package/dist/cli/reporters/anthropic-stream.js +77 -0
- package/dist/cli/reporters/base.js +2 -0
- package/dist/cli/reporters/json.js +69 -0
- package/dist/cli/reporters/openai-stream.js +72 -0
- package/dist/cli/reporters/standard.js +226 -0
- package/dist/cli/reporters/stderr-log-reporter.js +71 -0
- package/dist/cli/reporters/stream-json.js +111 -0
- package/dist/cli/run-cli.js +25 -0
- package/dist/cli/slash/runtime.js +240 -0
- package/dist/cli/ui/App.js +273 -0
- package/dist/cli/ui/authorization/bus.js +35 -0
- package/dist/cli/ui/components/CommandInput.js +200 -0
- package/dist/cli/ui/components/CommandSuggestionList.js +20 -0
- package/dist/cli/ui/components/Markdown.js +423 -0
- package/dist/cli/ui/components/MessageList.js +34 -0
- package/dist/cli/ui/components/StatusBannerLine.js +7 -0
- package/dist/cli/ui/components/TodoDrawer.js +60 -0
- package/dist/cli/ui/components/WelcomeMessage.js +14 -0
- package/dist/cli/ui/components/animations/StretchingThinking.js +51 -0
- package/dist/cli/ui/components/animations/ThinkingWave.js +15 -0
- package/dist/cli/ui/components/animations/TypeIndicator.js +30 -0
- package/dist/cli/ui/components/layout/SplitPane.js +11 -0
- package/dist/cli/ui/components/messageList/MessageItem.js +27 -0
- package/dist/cli/ui/components/messageList/QueuePreviewList.js +11 -0
- package/dist/cli/ui/components/messageList/items/EmphasisMessageItem.js +20 -0
- package/dist/cli/ui/components/messageList/items/InterruptMessageItem.js +10 -0
- package/dist/cli/ui/components/messageList/items/LightweightMessageItem.js +12 -0
- package/dist/cli/ui/components/messageList/items/StandardMessageItem.js +23 -0
- package/dist/cli/ui/components/messageList/items/WelcomeMessageItem.js +7 -0
- package/dist/cli/ui/components/messageList/messageListLayout.js +27 -0
- package/dist/cli/ui/components/messageList/streaming.js +51 -0
- package/dist/cli/ui/components/messageList/types.js +2 -0
- package/dist/cli/ui/components/messageList/utils.js +7 -0
- package/dist/cli/ui/components/sidebar/FileContext.js +8 -0
- package/dist/cli/ui/components/sidebar/MissionControl.js +8 -0
- package/dist/cli/ui/config.js +59 -0
- package/dist/cli/ui/hooks/useCommandLifecycle.js +110 -0
- package/dist/cli/ui/hooks/useCommandSuggestions.js +87 -0
- package/dist/cli/ui/hooks/useInputHistory.js +57 -0
- package/dist/cli/ui/hooks/useLoopEvents.js +382 -0
- package/dist/cli/ui/hooks/useLoopState.js +73 -0
- package/dist/cli/ui/hooks/useOnionExit.js +31 -0
- package/dist/cli/ui/hooks/useTerminalDimensions.js +34 -0
- package/dist/cli/ui/index.js +136 -0
- package/dist/cli/ui/selection/bus.js +35 -0
- package/dist/cli/ui/status/formatStatusBanner.js +8 -0
- package/dist/cli/ui/store/context.js +17 -0
- package/dist/cli/ui/store/reducer.js +264 -0
- package/dist/cli/ui/store/types.js +81 -0
- package/dist/cli/ui/styles/theme.js +295 -0
- package/dist/cli/ui/types.js +2 -0
- package/dist/cli/ui/utils/sanitizer.js +122 -0
- package/dist/cli/ui/utils/transcript.js +28 -0
- package/dist/cli/utils/asyncQueue.js +125 -0
- package/dist/cli/utils/audit-scope.js +10 -0
- package/dist/cli/utils/detectors/index.js +38 -0
- package/dist/cli/utils/llm-output.js +34 -0
- package/dist/cli/utils/outcome-reporter.js +17 -0
- package/dist/cli/utils/safe-fs.js +184 -0
- package/dist/cli/utils/verify-resolver.js +34 -0
- package/dist/cli/utils/worktree-prepare-resolver.js +18 -0
- package/dist/core/adapters/fs/atomic-file-writer.js +129 -0
- package/dist/core/adapters/fs/file-adapter.js +95 -0
- package/dist/core/adapters/fs/filesystem.js +31 -0
- package/dist/core/adapters/fs/index.js +5 -0
- package/dist/core/adapters/fs/node-fs.js +7 -0
- package/dist/core/adapters/fs/readonly-filesystem.js +23 -0
- package/dist/core/adapters/git/git-adapter.js +704 -0
- package/dist/core/adapters/git/git-runner.js +119 -0
- package/dist/core/adapters/git/lock-manager.js +314 -0
- package/dist/core/adapters/git/types.js +2 -0
- package/dist/core/adapters/path/index.js +2 -0
- package/dist/core/adapters/path/path-adapter.js +23 -0
- package/dist/core/ast/guard.js +116 -0
- package/dist/core/ast/index.js +4 -0
- package/dist/core/ast/parser.js +284 -0
- package/dist/core/ast/validator.js +46 -0
- package/dist/core/backends/salmon-loop/task-executor.js +68 -0
- package/dist/core/checkpoint-domain/manifest-store.js +379 -0
- package/dist/core/checkpoint-domain/service.js +84 -0
- package/dist/core/checkpoint-domain/types.js +2 -0
- package/dist/core/config/defaults.js +50 -0
- package/dist/core/config/errors.js +11 -0
- package/dist/core/config/file-format.js +108 -0
- package/dist/core/config/index.js +7 -0
- package/dist/core/config/limits.js +77 -0
- package/dist/core/config/load.js +34 -0
- package/dist/core/config/normalize.js +35 -0
- package/dist/core/config/paths.js +20 -0
- package/dist/core/config/redact.js +16 -0
- package/dist/core/config/resolve-env.js +43 -0
- package/dist/core/config/resolve-llm.js +130 -0
- package/dist/core/config/resolve.js +68 -0
- package/dist/core/config/resolvers/ast-validation.js +8 -0
- package/dist/core/config/resolvers/context.js +21 -0
- package/dist/core/config/resolvers/observability.js +45 -0
- package/dist/core/config/resolvers/output.js +8 -0
- package/dist/core/config/resolvers/permission-mode.js +6 -0
- package/dist/core/config/resolvers/security.js +14 -0
- package/dist/core/config/resolvers/server.js +36 -0
- package/dist/core/config/resolvers/tool-authorization.js +39 -0
- package/dist/core/config/resolvers/ui.js +26 -0
- package/dist/core/config/types/config-file.js +2 -0
- package/dist/core/config/types/primitives.js +9 -0
- package/dist/core/config/types/resolved.js +2 -0
- package/dist/core/config/types.js +4 -0
- package/dist/core/config/validate.js +852 -0
- package/dist/core/context/assembly/default-prompt-assembler.js +7 -0
- package/dist/core/context/assembly/prompt-assembler.js +2 -0
- package/dist/core/context/ast/import-extractor.js +28 -0
- package/dist/core/context/ast/module-resolver.js +61 -0
- package/dist/core/context/ast/source-outline.js +25 -0
- package/dist/core/context/audit-constants.js +23 -0
- package/dist/core/context/audit.js +54 -0
- package/dist/core/context/budget/dynamic-adjuster.js +149 -0
- package/dist/core/context/budget/example-integration.js +49 -0
- package/dist/core/context/budget/integration.js +93 -0
- package/dist/core/context/builder.js +289 -0
- package/dist/core/context/cache/errors.js +16 -0
- package/dist/core/context/cache/incremental-updater.js +131 -0
- package/dist/core/context/cache/index.js +25 -0
- package/dist/core/context/cache/path-resolver.js +127 -0
- package/dist/core/context/cache/prompt-caching.js +207 -0
- package/dist/core/context/cache/store-factory.js +63 -0
- package/dist/core/context/cache/store.js +193 -0
- package/dist/core/context/cache/types.js +15 -0
- package/dist/core/context/compression/js-like-comments.js +139 -0
- package/dist/core/context/compression/smart-compress.js +61 -0
- package/dist/core/context/compression/whitespace.js +26 -0
- package/dist/core/context/dependencies.js +102 -0
- package/dist/core/context/effectiveness/index.js +25 -0
- package/dist/core/context/effectiveness/tracker.js +253 -0
- package/dist/core/context/effectiveness/types.js +15 -0
- package/dist/core/context/formatters/index.js +7 -0
- package/dist/core/context/formatters/json-converter.js +662 -0
- package/dist/core/context/formatters/types.js +6 -0
- package/dist/core/context/formatters/xml-context.js +296 -0
- package/dist/core/context/gatherers/architecture-gatherer.js +75 -0
- package/dist/core/context/gatherers/artifact-gatherer.js +53 -0
- package/dist/core/context/gatherers/ast-gatherer.js +370 -0
- package/dist/core/context/gatherers/ghost-dependency-gatherer.js +46 -0
- package/dist/core/context/gatherers/git-diff-gatherer.js +91 -0
- package/dist/core/context/gatherers/git-history-gatherer.js +57 -0
- package/dist/core/context/gatherers/knowledge-gatherer.js +101 -0
- package/dist/core/context/gatherers/metadata-gatherer.js +59 -0
- package/dist/core/context/gatherers/primary-text-gatherer.js +36 -0
- package/dist/core/context/gatherers/ripgrep-gatherer.js +104 -0
- package/dist/core/context/hash.js +52 -0
- package/dist/core/context/index.js +3 -0
- package/dist/core/context/keywords.js +179 -0
- package/dist/core/context/policies/budget-policy.js +36 -0
- package/dist/core/context/policies/pack-until-full.js +419 -0
- package/dist/core/context/scoring/relevance.js +191 -0
- package/dist/core/context/service-deps.js +32 -0
- package/dist/core/context/service-helpers.js +32 -0
- package/dist/core/context/service.js +265 -0
- package/dist/core/context/steps/context-budget.js +157 -0
- package/dist/core/context/steps/context-gather.js +71 -0
- package/dist/core/context/steps/context-primary.js +19 -0
- package/dist/core/context/steps/context-promotion.js +78 -0
- package/dist/core/context/steps/context-targets.js +85 -0
- package/dist/core/context/steps/types.js +2 -0
- package/dist/core/context/summarization/index.js +27 -0
- package/dist/core/context/summarization/prompts.js +80 -0
- package/dist/core/context/summarization/summarizer.js +377 -0
- package/dist/core/context/summarization/types.js +29 -0
- package/dist/core/context/targeting/churn-policy.js +27 -0
- package/dist/core/context/targeting/target-resolver.js +491 -0
- package/dist/core/context/token/adaptive-budget.js +364 -0
- package/dist/core/context/token/cache.js +163 -0
- package/dist/core/context/token/counter.js +190 -0
- package/dist/core/context/token/encoding-registry.js +173 -0
- package/dist/core/context/token/index.js +31 -0
- package/dist/core/context/token/token-budget.js +213 -0
- package/dist/core/context/token/types.js +10 -0
- package/dist/core/context/truncation/index.js +23 -0
- package/dist/core/context/truncation/semantic-truncator.js +103 -0
- package/dist/core/context/truncation/strategies/error-stack.js +94 -0
- package/dist/core/context/truncation/strategies/generic.js +48 -0
- package/dist/core/context/truncation/strategies/git-diff.js +99 -0
- package/dist/core/context/truncation/strategies/index.js +10 -0
- package/dist/core/context/truncation/strategies/json.js +142 -0
- package/dist/core/context/truncation/strategies/log.js +131 -0
- package/dist/core/context/truncation/strategies/test-result.js +140 -0
- package/dist/core/context/truncation/type-detector.js +133 -0
- package/dist/core/context/truncation/types.js +16 -0
- package/dist/core/context/types.js +2 -0
- package/dist/core/extensions/index.js +118 -0
- package/dist/core/extensions/load.js +36 -0
- package/dist/core/extensions/merge.js +29 -0
- package/dist/core/extensions/paths.js +40 -0
- package/dist/core/extensions/redact.js +37 -0
- package/dist/core/extensions/schemas.js +70 -0
- package/dist/core/extensions/types.js +2 -0
- package/dist/core/facades/cli-authorization-allowlist.js +3 -0
- package/dist/core/facades/cli-authorization-non-interactive.js +3 -0
- package/dist/core/facades/cli-authorization-provider.js +2 -0
- package/dist/core/facades/cli-chat.js +11 -0
- package/dist/core/facades/cli-command-allowlist.js +3 -0
- package/dist/core/facades/cli-command-chat.js +8 -0
- package/dist/core/facades/cli-command-checkpoint.js +3 -0
- package/dist/core/facades/cli-command-config.js +10 -0
- package/dist/core/facades/cli-command-dispatcher.js +2 -0
- package/dist/core/facades/cli-command-parallel.js +8 -0
- package/dist/core/facades/cli-command-session.js +2 -0
- package/dist/core/facades/cli-command-tool-names.js +6 -0
- package/dist/core/facades/cli-context.js +8 -0
- package/dist/core/facades/cli-headless.js +3 -0
- package/dist/core/facades/cli-observability.js +3 -0
- package/dist/core/facades/cli-program-bootstrap.js +2 -0
- package/dist/core/facades/cli-reporters.js +5 -0
- package/dist/core/facades/cli-run-execute.js +3 -0
- package/dist/core/facades/cli-run-handler.js +7 -0
- package/dist/core/facades/cli-run-headless-error-writer.js +2 -0
- package/dist/core/facades/cli-run-loop-params.js +2 -0
- package/dist/core/facades/cli-run-persist-session.js +2 -0
- package/dist/core/facades/cli-run-runtime-llm.js +5 -0
- package/dist/core/facades/cli-serve.js +21 -0
- package/dist/core/facades/cli-slash-runtime.js +9 -0
- package/dist/core/facades/cli-subagent.js +2 -0
- package/dist/core/facades/cli-ui.js +5 -0
- package/dist/core/facades/cli-utils-llm-output.js +3 -0
- package/dist/core/facades/cli-utils-path.js +2 -0
- package/dist/core/facades/cli-utils-worktree.js +2 -0
- package/dist/core/failure/diagnostics.js +221 -0
- package/dist/core/feedback/index.js +28 -0
- package/dist/core/feedback/parsers.js +59 -0
- package/dist/core/feedback/patterns.js +26 -0
- package/dist/core/feedback/types.js +2 -0
- package/dist/core/grizzco/domain/grizzco-types.js +41 -0
- package/dist/core/grizzco/dsl/DecisionEngine.js +149 -0
- package/dist/core/grizzco/dsl/MicroTaskRunner.js +39 -0
- package/dist/core/grizzco/dsl/llm-strategy.js +80 -0
- package/dist/core/grizzco/dsl/strategies.js +69 -0
- package/dist/core/grizzco/dsl/types.js +2 -0
- package/dist/core/grizzco/engine/observability/event-adapter.js +41 -0
- package/dist/core/grizzco/engine/observability/index.js +3 -0
- package/dist/core/grizzco/engine/observability/loop-telemetry.js +51 -0
- package/dist/core/grizzco/engine/outcome/index.js +2 -0
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +167 -0
- package/dist/core/grizzco/engine/pipeline/pipeline.js +335 -0
- package/dist/core/grizzco/engine/pipeline/types.js +2 -0
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +242 -0
- package/dist/core/grizzco/engine/transaction/authorization-summary.js +44 -0
- package/dist/core/grizzco/engine/transaction/index.js +3 -0
- package/dist/core/grizzco/engine/transaction/report-mapper.js +50 -0
- package/dist/core/grizzco/engine/transaction/retry-policy.js +19 -0
- package/dist/core/grizzco/engine/transaction/runner-builder.js +45 -0
- package/dist/core/grizzco/engine/transaction/session.js +58 -0
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +193 -0
- package/dist/core/grizzco/engine/transaction/types.js +2 -0
- package/dist/core/grizzco/execution/Executor.js +58 -0
- package/dist/core/grizzco/execution/RejectionManager.js +71 -0
- package/dist/core/grizzco/execution/WorkerFactory.js +31 -0
- package/dist/core/grizzco/flows/SalmonLoopFlow.js +102 -0
- package/dist/core/grizzco/runtime/apply-back-runtime.js +136 -0
- package/dist/core/grizzco/runtime/apply-back-utils.js +13 -0
- package/dist/core/grizzco/runtime/host/host-runner.js +99 -0
- package/dist/core/grizzco/runtime/host/index.js +2 -0
- package/dist/core/grizzco/runtime/host/types.js +2 -0
- package/dist/core/grizzco/services/CachedService.js +42 -0
- package/dist/core/grizzco/services/implementations/default/GitConfigService.js +38 -0
- package/dist/core/grizzco/services/implementations/mock/MockLockService.js +11 -0
- package/dist/core/grizzco/services/implementations/mock/MockUserQuotaService.js +11 -0
- package/dist/core/grizzco/services/registry.js +30 -0
- package/dist/core/grizzco/services/types.js +2 -0
- package/dist/core/grizzco/steps/answer.js +75 -0
- package/dist/core/grizzco/steps/apply-back.js +46 -0
- package/dist/core/grizzco/steps/apply.js +136 -0
- package/dist/core/grizzco/steps/ast-validate.js +37 -0
- package/dist/core/grizzco/steps/audit.js +311 -0
- package/dist/core/grizzco/steps/context.js +74 -0
- package/dist/core/grizzco/steps/display-answer.js +6 -0
- package/dist/core/grizzco/steps/display-report.js +158 -0
- package/dist/core/grizzco/steps/display-research.js +6 -0
- package/dist/core/grizzco/steps/displayReview.js +6 -0
- package/dist/core/grizzco/steps/explore.js +245 -0
- package/dist/core/grizzco/steps/extractIssues.js +27 -0
- package/dist/core/grizzco/steps/generateFixPlan.js +13 -0
- package/dist/core/grizzco/steps/generateReview.js +71 -0
- package/dist/core/grizzco/steps/patch.js +220 -0
- package/dist/core/grizzco/steps/plan.js +191 -0
- package/dist/core/grizzco/steps/preflight.js +93 -0
- package/dist/core/grizzco/steps/prepare-deps.js +49 -0
- package/dist/core/grizzco/steps/read-only-shrink.js +4 -0
- package/dist/core/grizzco/steps/research.js +188 -0
- package/dist/core/grizzco/steps/rollback.js +138 -0
- package/dist/core/grizzco/steps/shrink.js +64 -0
- package/dist/core/grizzco/steps/validate.js +40 -0
- package/dist/core/grizzco/steps/verify.js +136 -0
- package/dist/core/grizzco/validation/AstValidationService.js +133 -0
- package/dist/core/grizzco/validation/ContextValidator.js +17 -0
- package/dist/core/grizzco/validation/ast-validation-policy.js +11 -0
- package/dist/core/grizzco/workers/direct-write-worker.js +44 -0
- package/dist/core/grizzco/workers/git-apply-worker.js +75 -0
- package/dist/core/grizzco/workers/i-merge-worker.js +2 -0
- package/dist/core/grizzco/workers/mm-three-way-worker.js +117 -0
- package/dist/core/grizzco/workers/no-op-worker.js +18 -0
- package/dist/core/grizzco/workers/overwrite-binary-worker.js +29 -0
- package/dist/core/grizzco/workers/strata-sync-worker.js +69 -0
- package/dist/core/grizzco/workers/three-way-merge-worker.js +84 -0
- package/dist/core/grizzco/workers/three-way-staged-worker.js +93 -0
- package/dist/core/grizzco/workers/union-merge-worker.js +71 -0
- package/dist/core/history/input-history.js +55 -0
- package/dist/core/intent/chat-intent.js +250 -0
- package/dist/core/interaction/events/bus.js +52 -0
- package/dist/core/interaction/model/events.js +2 -0
- package/dist/core/interaction/model/index.js +3 -0
- package/dist/core/interaction/model/task-state.js +9 -0
- package/dist/core/interaction/model/transition-policy.js +50 -0
- package/dist/core/interaction/model/types.js +2 -0
- package/dist/core/interaction/orchestration/facade.js +190 -0
- package/dist/core/interaction/orchestration/index.js +2 -0
- package/dist/core/interaction/orchestration/store.js +32 -0
- package/dist/core/interaction/sync/task-sync-engine.js +57 -0
- package/dist/core/interaction/turn-stop-reason.js +27 -0
- package/dist/core/language-support/index.js +3 -0
- package/dist/core/language-support/orchestrator.js +37 -0
- package/dist/core/language-support/strategies/extension-candidate-strategy.js +27 -0
- package/dist/core/language-support/strategies/index.js +3 -0
- package/dist/core/language-support/strategies/language-query-strategy.js +26 -0
- package/dist/core/llm/ai-sdk/chat-executor.js +88 -0
- package/dist/core/llm/ai-sdk/langfuse-headers.js +28 -0
- package/dist/core/llm/ai-sdk/message-mapper.js +240 -0
- package/dist/core/llm/ai-sdk/observation-context.js +16 -0
- package/dist/core/llm/ai-sdk/provider-factory.js +29 -0
- package/dist/core/llm/ai-sdk/request-params.js +18 -0
- package/dist/core/llm/ai-sdk/request-runtime.js +168 -0
- package/dist/core/llm/ai-sdk/result-mapper.js +31 -0
- package/dist/core/llm/ai-sdk/retry-classifier.js +82 -0
- package/dist/core/llm/ai-sdk/retry-executor.js +38 -0
- package/dist/core/llm/ai-sdk.js +92 -0
- package/dist/core/llm/audit.js +2 -0
- package/dist/core/llm/base-url.js +18 -0
- package/dist/core/llm/contracts/repair.js +68 -0
- package/dist/core/llm/errors.js +172 -0
- package/dist/core/llm/factory.js +21 -0
- package/dist/core/llm/http/index.js +2 -0
- package/dist/core/llm/index.js +6 -0
- package/dist/core/llm/message-composition.js +25 -0
- package/dist/core/llm/openai.js +69 -0
- package/dist/core/llm/output-policy.js +192 -0
- package/dist/core/llm/phase-router.js +55 -0
- package/dist/core/llm/redact.js +37 -0
- package/dist/core/llm/registry.js +81 -0
- package/dist/core/llm/retry-utils.js +114 -0
- package/dist/core/llm/stream-utils.js +87 -0
- package/dist/core/llm/utils.js +82 -0
- package/dist/core/observability/audit-file.js +199 -0
- package/dist/core/observability/audit-trail.js +125 -0
- package/dist/core/observability/authorization-decisions.js +54 -0
- package/dist/core/observability/debug-artifacts.js +61 -0
- package/dist/core/observability/error-envelope.js +63 -0
- package/dist/core/observability/error-mapping.js +271 -0
- package/dist/core/observability/ignored-error.js +6 -0
- package/dist/core/observability/logger.js +457 -0
- package/dist/core/observability/loop-event-reporter.js +46 -0
- package/dist/core/observability/monitor.js +240 -0
- package/dist/core/observability/run-outcome-reporter.js +15 -0
- package/dist/core/observability/token-usage.js +36 -0
- package/dist/core/observability/ui-log-sanitize.js +35 -0
- package/dist/core/patch/aggregator.js +93 -0
- package/dist/core/patch/diff.js +298 -0
- package/dist/core/permission-gate/default-gate.js +115 -0
- package/dist/core/permission-gate/gate.js +2 -0
- package/dist/core/permission-gate/types.js +2 -0
- package/dist/core/plan/index.js +2 -0
- package/dist/core/plan/manager.js +123 -0
- package/dist/core/plan/markdown-editor.js +238 -0
- package/dist/core/plan/storage.js +75 -0
- package/dist/core/plan/types.js +2 -0
- package/dist/core/plugin/interface.js +2 -0
- package/dist/core/plugin/loader.js +130 -0
- package/dist/core/plugin/registry.js +90 -0
- package/dist/core/plugin/validator.js +98 -0
- package/dist/core/prompts/registry.js +189 -0
- package/dist/core/prompts/runtime.js +69 -0
- package/dist/core/prompts/schema.js +2 -0
- package/dist/core/prompts/templates/phases/explore_user.hbs +26 -0
- package/dist/core/prompts/templates/phases/patch_user.hbs +57 -0
- package/dist/core/prompts/templates/phases/plan_user.hbs +33 -0
- package/dist/core/prompts/templates/system/_context_json_legend.hbs +21 -0
- package/dist/core/prompts/templates/system/_tool_defs.hbs +60 -0
- package/dist/core/prompts/templates/system/explore_system.hbs +26 -0
- package/dist/core/prompts/templates/system/main_system.hbs +18 -0
- package/dist/core/prompts/templates/system/patch_system.hbs +10 -0
- package/dist/core/prompts/templates/system/plan_system.hbs +1 -0
- package/dist/core/prompts/templates/system/reflection.hbs +39 -0
- package/dist/core/protocols/a2a/agent-card.js +30 -0
- package/dist/core/protocols/a2a/mapper.js +14 -0
- package/dist/core/protocols/a2a/sdk/auth-middleware.js +31 -0
- package/dist/core/protocols/a2a/sdk/executor.js +301 -0
- package/dist/core/protocols/a2a/sdk/server.js +24 -0
- package/dist/core/protocols/a2a/task-projection.js +45 -0
- package/dist/core/protocols/acp/acp-command-runner.js +204 -0
- package/dist/core/protocols/acp/acp-filesystem.js +43 -0
- package/dist/core/protocols/acp/checkpoint-meta.js +2 -0
- package/dist/core/protocols/acp/formal-agent.js +1201 -0
- package/dist/core/protocols/acp/handlers.js +51 -0
- package/dist/core/protocols/acp/permission-provider.js +122 -0
- package/dist/core/protocols/acp/stdio-server.js +116 -0
- package/dist/core/reflection/engine.js +55 -0
- package/dist/core/reflection/types.js +2 -0
- package/dist/core/runtime/agent-server-runtime.js +88 -0
- package/dist/core/runtime/bun-runtime.js +26 -0
- package/dist/core/runtime/command-runner-context.js +16 -0
- package/dist/core/runtime/exit-codes.js +11 -0
- package/dist/core/runtime/fastify-fetch-bridge.js +51 -0
- package/dist/core/runtime/fastify-server-bundle.js +26 -0
- package/dist/core/runtime/initialize.js +132 -0
- package/dist/core/runtime/loop-finalize.js +71 -0
- package/dist/core/runtime/loop-run-lifecycle.js +73 -0
- package/dist/core/runtime/loop-run-reporter.js +19 -0
- package/dist/core/runtime/loop-runtime-config.js +26 -0
- package/dist/core/runtime/loop-session-runner.js +30 -0
- package/dist/core/runtime/loop.js +84 -0
- package/dist/core/runtime/paths.js +84 -0
- package/dist/core/runtime/process-runner.js +16 -0
- package/dist/core/runtime/process-types.js +2 -0
- package/dist/core/runtime/semaphore.js +41 -0
- package/dist/core/runtime/sidecar-fastify-plugin.js +35 -0
- package/dist/core/runtime/sidecar-paths.js +47 -0
- package/dist/core/runtime/sidecar-route-catalog.js +103 -0
- package/dist/core/runtime/spawn-command.js +392 -0
- package/dist/core/runtime/spawn-interactive.js +71 -0
- package/dist/core/security/redaction.js +160 -0
- package/dist/core/session/compression.js +323 -0
- package/dist/core/session/flow.js +85 -0
- package/dist/core/session/manager.js +313 -0
- package/dist/core/session/pruning-strategy.js +153 -0
- package/dist/core/session/session-context-builder.js +122 -0
- package/dist/core/session/summary-sync.js +82 -0
- package/dist/core/session/token-tracker.js +82 -0
- package/dist/core/session/types.js +2 -0
- package/dist/core/skills/bridge.js +33 -0
- package/dist/core/skills/index.js +8 -0
- package/dist/core/skills/loader.js +80 -0
- package/dist/core/skills/parser.js +66 -0
- package/dist/core/skills/runtime/MicroTaskRunner.js +102 -0
- package/dist/core/skills/runtime/SkillRunner.js +108 -0
- package/dist/core/skills/strategy.js +29 -0
- package/dist/core/skills/types.js +2 -0
- package/dist/core/slash/index.js +6 -0
- package/dist/core/slash/parser.js +33 -0
- package/dist/core/slash/registry.js +78 -0
- package/dist/core/slash/router.js +76 -0
- package/dist/core/slash/steps/slash-decide.js +19 -0
- package/dist/core/slash/steps/slash-execute.js +73 -0
- package/dist/core/slash/steps/types.js +2 -0
- package/dist/core/slash/strategy.js +33 -0
- package/dist/core/slash/types.js +2 -0
- package/dist/core/strata/checkpoint/manager.js +492 -0
- package/dist/core/strata/checkpoint/snapshot-audit.js +88 -0
- package/dist/core/strata/checkpoint/snapshot-create.js +79 -0
- package/dist/core/strata/checkpoint/snapshot-write-tree.js +72 -0
- package/dist/core/strata/engine/shadow-merge-engine.js +394 -0
- package/dist/core/strata/index.js +15 -0
- package/dist/core/strata/interaction/content-guardian.js +59 -0
- package/dist/core/strata/interaction/file-system-provider.js +89 -0
- package/dist/core/strata/layers/file-state-resolver.js +157 -0
- package/dist/core/strata/layers/immutable-git-layer.js +42 -0
- package/dist/core/strata/layers/shadow-driver/copy-backend.js +114 -0
- package/dist/core/strata/layers/shadow-driver/env.js +29 -0
- package/dist/core/strata/layers/shadow-driver/error-classifier.js +41 -0
- package/dist/core/strata/layers/shadow-driver/index.js +17 -0
- package/dist/core/strata/layers/shadow-driver/readonly-lock.js +221 -0
- package/dist/core/strata/layers/shadow-driver/shadow-driver.js +234 -0
- package/dist/core/strata/layers/shadow-driver/strategy.js +86 -0
- package/dist/core/strata/layers/sidecar-layer.js +96 -0
- package/dist/core/strata/layers/worktree.js +240 -0
- package/dist/core/strata/runtime/environment.js +377 -0
- package/dist/core/strata/runtime/synchronizer.js +819 -0
- package/dist/core/strata/types.js +46 -0
- package/dist/core/streaming/canonical/canonical-responses-event-emitter.js +326 -0
- package/dist/core/streaming/canonical/function-call-item-id.js +13 -0
- package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +54 -0
- package/dist/core/streaming/canonical/responses-event-emitter.js +127 -0
- package/dist/core/streaming/canonical/responses-events.js +2 -0
- package/dist/core/streaming/normalized-events.js +9 -0
- package/dist/core/streaming/normalized-from-text.js +47 -0
- package/dist/core/streaming/stream-assembler.js +347 -0
- package/dist/core/structured-output/index.js +3 -0
- package/dist/core/structured-output/json-extract.js +70 -0
- package/dist/core/structured-output/json-schema-validator.js +90 -0
- package/dist/core/structured-output/types.js +2 -0
- package/dist/core/sub-agent/artifacts/store.js +141 -0
- package/dist/core/sub-agent/artifacts/types.js +2 -0
- package/dist/core/sub-agent/controller.js +69 -0
- package/dist/core/sub-agent/core/loop.js +79 -0
- package/dist/core/sub-agent/core/manager.js +246 -0
- package/dist/core/sub-agent/registry-defaults.js +52 -0
- package/dist/core/sub-agent/registry.js +35 -0
- package/dist/core/sub-agent/tools/task-spawn.js +29 -0
- package/dist/core/sub-agent/types.js +23 -0
- package/dist/core/target-runtime/command-resolver.js +42 -0
- package/dist/core/target-runtime/index.js +3 -0
- package/dist/core/target-runtime/profile.js +73 -0
- package/dist/core/testgen/detector.js +17 -0
- package/dist/core/testgen/index.js +38 -0
- package/dist/core/testgen/templates.js +46 -0
- package/dist/core/tools/audit.js +140 -0
- package/dist/core/tools/authorization/types.js +2 -0
- package/dist/core/tools/budget.js +118 -0
- package/dist/core/tools/builtin/artifact.js +29 -0
- package/dist/core/tools/builtin/ast-grep.js +107 -0
- package/dist/core/tools/builtin/ast.js +62 -0
- package/dist/core/tools/builtin/code-search/backends/powershell.js +84 -0
- package/dist/core/tools/builtin/code-search/backends/rg.js +85 -0
- package/dist/core/tools/builtin/code-search/executor.js +87 -0
- package/dist/core/tools/builtin/code-search/parse/plain-grep.js +59 -0
- package/dist/core/tools/builtin/code-search/parse/rg-json.js +31 -0
- package/dist/core/tools/builtin/code-search/spec.js +82 -0
- package/dist/core/tools/builtin/fs.js +243 -0
- package/dist/core/tools/builtin/git.js +118 -0
- package/dist/core/tools/builtin/index.js +80 -0
- package/dist/core/tools/builtin/interaction.js +120 -0
- package/dist/core/tools/builtin/knowledge.js +98 -0
- package/dist/core/tools/builtin/plan.js +148 -0
- package/dist/core/tools/builtin/proposal.js +207 -0
- package/dist/core/tools/builtin/shell.js +71 -0
- package/dist/core/tools/builtin/verify.js +41 -0
- package/dist/core/tools/capability/executor.js +84 -0
- package/dist/core/tools/capability/runner.js +50 -0
- package/dist/core/tools/capability/types.js +2 -0
- package/dist/core/tools/dispatcher.js +80 -0
- package/dist/core/tools/headless-payload.js +37 -0
- package/dist/core/tools/loader.js +100 -0
- package/dist/core/tools/mapper.js +142 -0
- package/dist/core/tools/mcp/client.js +308 -0
- package/dist/core/tools/mcp/loader.js +110 -0
- package/dist/core/tools/mcp/schema.js +54 -0
- package/dist/core/tools/mcp/streamable-http.js +101 -0
- package/dist/core/tools/mcp/types.js +26 -0
- package/dist/core/tools/parallel/isolation.js +25 -0
- package/dist/core/tools/parallel/lock-manager.js +124 -0
- package/dist/core/tools/parallel/persistence.js +126 -0
- package/dist/core/tools/parallel/plan-builder.js +66 -0
- package/dist/core/tools/parallel/plan.js +2 -0
- package/dist/core/tools/parallel/refs.js +7 -0
- package/dist/core/tools/parallel/resolve-args.js +50 -0
- package/dist/core/tools/parallel/resource-helpers.js +35 -0
- package/dist/core/tools/parallel/resources.js +2 -0
- package/dist/core/tools/parallel/scheduler.js +372 -0
- package/dist/core/tools/parser.js +89 -0
- package/dist/core/tools/permissions/permission-rules.js +503 -0
- package/dist/core/tools/plugins/loader.js +102 -0
- package/dist/core/tools/policy.js +87 -0
- package/dist/core/tools/registry.js +29 -0
- package/dist/core/tools/router.js +514 -0
- package/dist/core/tools/sanitize.js +78 -0
- package/dist/core/tools/schema-utils.js +71 -0
- package/dist/core/tools/session.js +1105 -0
- package/dist/core/tools/streaming/ToolCallAccumulator.js +64 -0
- package/dist/core/tools/types.js +2 -0
- package/dist/core/types/authorization.js +2 -0
- package/dist/core/types/context.js +2 -0
- package/dist/core/types/errors.js +29 -0
- package/dist/core/types/execution.js +65 -0
- package/dist/core/types/index.js +9 -0
- package/dist/core/types/llm.js +9 -0
- package/dist/core/types/loop.js +2 -0
- package/dist/core/types/planning.js +2 -0
- package/dist/core/types/runtime.js +2 -0
- package/dist/core/types/usage.js +2 -0
- package/dist/core/ui/kaomoji.js +5 -0
- package/dist/core/utils/path.js +116 -0
- package/dist/core/utils/platform-shell.js +10 -0
- package/dist/core/utils/sanitizer.js +107 -0
- package/dist/core/verification/runner.js +265 -0
- package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +272 -0
- package/dist/integrations/langfuse/outcome-proxy.js +68 -0
- package/dist/interfaces/cli/task-runner.js +11 -0
- package/dist/languages/typescript/index.js +178 -0
- package/dist/locales/en.js +679 -0
- package/dist/locales/index.js +11 -0
- package/dist/utils/eol.js +35 -0
- 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
|