@moretea-labs/forge 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/CHANGELOG.md +39 -0
- package/CLAUDE.md +4 -0
- package/README.md +2 -2
- package/README.zh-CN.md +1 -1
- package/SKILL.md +2 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/hooks/hook-input.sh +11 -8
- package/assets/hooks/lib/session-state.sh +10 -7
- package/assets/hooks/lib/workflow-state.sh +13 -177
- package/assets/hooks/post-tool-observer.sh +8 -2
- package/assets/hooks/prompt-guard.sh +5 -81
- package/assets/hooks/session-start-context.sh +56 -1
- package/assets/partials/05-workflow.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +2 -0
- package/assets/partials-agents/03-orchestration.partial.md +1 -1
- package/assets/plugin-registry.v1.json +3 -3
- package/assets/reference-configs/document-generation.md +1 -1
- package/assets/reference-configs/harness-overview.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/contract.template.md +2 -7
- package/assets/templates/helpers/capture-plan.sh +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +17 -0
- package/assets/templates/helpers/check-task-workflow.sh +4 -4
- package/assets/templates/helpers/contract-worktree.sh +1 -21
- package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
- package/assets/templates/helpers/inspect-project-state.ts +8 -6
- package/assets/templates/helpers/migrate-project-template.sh +22 -45
- package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
- package/assets/templates/helpers/new-plan.sh +1 -1
- package/assets/templates/helpers/plan-to-todo.sh +12 -37
- package/assets/templates/helpers/ship-worktrees.sh +2 -29
- package/assets/templates/helpers/verify-contract.sh +1 -111
- package/assets/templates/helpers/verify-sprint.sh +3 -180
- package/assets/templates/helpers/workflow-contract.ts +5 -1
- package/assets/templates/plan.template.md +1 -1
- package/assets/templates/review.template.md +9 -34
- package/assets/workflow-contract.v1.json +10 -19
- package/docs/README.md +10 -10
- package/docs/architecture/CURRENT.md +178 -0
- package/docs/architecture/EVOLUTION.md +42 -0
- package/docs/architecture/current/README.md +4 -156
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
- package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
- package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
- package/docs/architecture/history/README.md +7 -0
- package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
- package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
- package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
- package/docs/architecture/history.md +3 -34
- package/docs/architecture/index.md +15 -98
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
- package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
- package/docs/architecture/versions/1.6.md +38 -0
- package/docs/architecture/versions/1.7.md +41 -0
- package/docs/forge-chatgpt-browser-engine.md +2 -2
- package/docs/forge-chatgpt-mcp-setup.md +4 -28
- package/docs/forge-chatgpt-supervised-automation.md +3 -3
- package/docs/forge-github-issue-launcher.md +1 -1
- package/docs/forge-plugin-management.md +1 -1
- package/docs/forge-runtime-self-healing-loop.md +1 -1
- package/docs/operations/controller-browser-plugin.md +26 -6
- package/docs/operations/controller-performance-and-502.md +143 -0
- package/docs/operations/controller-reliability-runbook.md +1 -1
- package/docs/operations/features.md +2 -2
- package/docs/operations/github-repository.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +5 -3
- package/docs/wiki/Architecture.md +6 -6
- package/docs/wiki/Implementation.md +2 -2
- package/docs/wiki/Integrations.md +2 -2
- package/docs/wiki/Work-Lifecycle.md +1 -1
- package/package.json +33 -11
- package/scripts/activate-source-baseline.ts +17 -13
- package/scripts/architecture-event.ts +2 -820
- package/scripts/architecture-queue.sh +2 -645
- package/scripts/archive-architecture-request.sh +2 -242
- package/scripts/archive-workflow.sh +1 -291
- package/scripts/benchmark-background-check-overlap.ts +228 -0
- package/scripts/benchmark-mcp-host.sh +117 -0
- package/scripts/benchmark-mcp-transport.ts +170 -0
- package/scripts/benchmark-quality-harness.ts +146 -0
- package/scripts/benchmark-route-session-concurrency.ts +207 -6
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
- package/scripts/benchmark-typescript-navigation.ts +186 -0
- package/scripts/benchmark-work-efficiency.ts +334 -0
- package/scripts/capability-config.ts +2 -392
- package/scripts/capability-resolver.ts +2 -503
- package/scripts/capture-plan.sh +1 -332
- package/scripts/check-agent-tooling.sh +2 -1558
- package/scripts/check-architecture-sync.sh +2 -371
- package/scripts/check-brain-manifest.sh +2 -253
- package/scripts/check-context-files.sh +2 -47
- package/scripts/check-deploy-sql-order.sh +2 -115
- package/scripts/check-mcp-compatibility.ts +14 -0
- package/scripts/check-runtime-architecture.mjs +349 -79
- package/scripts/check-skill-version.ts +3 -191
- package/scripts/check-source-duplication.mjs +61 -0
- package/scripts/check-stable-baseline.ts +92 -0
- package/scripts/check-task-sync.sh +2 -63
- package/scripts/check-task-workflow.sh +2 -1000
- package/scripts/codex-handoff-resume.sh +2 -267
- package/scripts/context-contract-sync.sh +2 -514
- package/scripts/contract-run.ts +2 -463
- package/scripts/contract-worktree.sh +1 -722
- package/scripts/ensure-task-workflow.sh +1 -1225
- package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
- package/scripts/inspect-project-state.ts +4 -279
- package/scripts/install-standalone-recovery.ts +24 -0
- package/scripts/lib/project-init-lib.sh +22 -46
- package/scripts/lib/run-forge-helper.sh +19 -0
- package/scripts/lib/run-forge-helper.ts +26 -0
- package/scripts/migrate-project-template.sh +1 -1189
- package/scripts/migrate-workflow-docs.ts +4 -412
- package/scripts/new-plan.sh +1 -180
- package/scripts/new-spec.sh +1 -47
- package/scripts/new-sprint.sh +1 -46
- package/scripts/plan-to-todo.sh +1 -796
- package/scripts/prepare-codex-handoff.sh +2 -201
- package/scripts/prepare-handoff.sh +1 -78
- package/scripts/refresh-current-status.sh +2 -467
- package/scripts/run-governed-gate.ts +5 -1
- package/scripts/select-agent-context-blocks.sh +2 -64
- package/scripts/ship-worktrees.sh +1 -558
- package/scripts/smoke-ios-physical-device.ts +4 -2
- package/scripts/smoke-runtime-recovery.ts +1 -35
- package/scripts/smoke-schedule-engine.ts +1 -20
- package/scripts/sprint-backlog.sh +1 -834
- package/scripts/stage-runtime-release.ts +2 -1
- package/scripts/summarize-failures.sh +2 -123
- package/scripts/switch-plan.sh +1 -150
- package/scripts/sync-brain-docs.sh +2 -379
- package/scripts/sync-generated-authority.mjs +175 -0
- package/scripts/verify-contract.sh +2 -703
- package/scripts/verify-forge-runtime.sh +0 -1
- package/scripts/verify-sprint.sh +1 -677
- package/scripts/workflow-contract.ts +4 -205
- package/scripts/workstream-sync.sh +2 -261
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/commands/init-hook.ts +3 -3
- package/src/cli/commands/mcp.ts +9 -5
- package/src/cli/commands/plugin.ts +12 -3
- package/src/cli/commands/recovery.ts +25 -3
- package/src/cli/commands/runtime.ts +55 -3
- package/src/cli/commands/security.ts +12 -1
- package/src/cli/commands/setup-profile.ts +31 -21
- package/src/cli/controller/check-runner.ts +113 -5
- package/src/cli/controller/composite-result.ts +1 -1
- package/src/cli/controller/context/focus.ts +51 -0
- package/src/cli/controller/context/instruction-context.ts +114 -0
- package/src/cli/controller/context/known-paths.ts +76 -0
- package/src/cli/controller/context/query-planning.ts +110 -0
- package/src/cli/controller/context/rule-resolution.ts +532 -0
- package/src/cli/controller/context/source-materializer.ts +278 -0
- package/src/cli/controller/context/structural.ts +16 -0
- package/src/cli/controller/context/types.ts +286 -0
- package/src/cli/controller/context-pack.ts +828 -487
- package/src/cli/controller/launch-agents.ts +77 -2
- package/src/cli/controller/operational-plan.ts +2 -2
- package/src/cli/controller/work-mode.ts +6 -4
- package/src/cli/editing/edit-session.ts +60 -7
- package/src/cli/github/contracts.ts +16 -0
- package/src/cli/github/github.ts +135 -16
- package/src/cli/github/session.ts +2 -1
- package/src/cli/installer/managed-entries.ts +15 -1
- package/src/cli/local-bridge/console-automations.ts +54 -78
- package/src/cli/local-bridge/console-view-models.ts +1 -35
- package/src/cli/local-bridge/facade-api.ts +37 -264
- package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
- package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
- package/src/cli/local-bridge/frontend/src/styles.css +1 -1
- package/src/cli/local-bridge/frontend/src/types.ts +5 -8
- package/src/cli/local-bridge/job-store.ts +1 -490
- package/src/cli/local-bridge/server.ts +2 -515
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
- package/src/cli/mcp/auth.ts +5 -5
- package/src/cli/mcp/instructions.ts +3 -2
- package/src/cli/mcp/legacy-tool-service.ts +10 -222
- package/src/cli/mcp/multi-repository.ts +16 -5
- package/src/cli/mcp/oauth.ts +175 -26
- package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
- package/src/cli/mcp/repository-tools.ts +282 -146
- package/src/cli/mcp/server.ts +586 -116
- package/src/cli/mcp/tool-capability-cache.ts +1 -1
- package/src/cli/mcp/tool-contract.ts +14 -0
- package/src/cli/mcp/toolset-selection.ts +1 -1
- package/src/cli/mcp/toolset.ts +44 -7
- package/src/cli/mcp/transports/http.ts +124 -14
- package/src/cli/mcp/transports/session-registry.ts +43 -5
- package/src/cli/mcp/types.ts +3 -3
- package/src/cli/repositories/command-classifier.ts +103 -21
- package/src/cli/repositories/command-executor.ts +78 -526
- package/src/cli/repositories/command-process.ts +108 -0
- package/src/cli/repositories/command-scope.ts +99 -41
- package/src/cli/repositories/controller-home.ts +7 -8
- package/src/cli/repositories/git-command-executor.ts +2 -0
- package/src/cli/repositories/locks.ts +53 -16
- package/src/cli/repositories/repository-snapshot.ts +124 -0
- package/src/cli/repositories/selected-path-actions.ts +17 -9
- package/src/cli/repositories/structured-git.ts +118 -6
- package/src/cli/repository/inspector.ts +563 -65
- package/src/cli/repository/session-cache.ts +58 -17
- package/src/cli/v81-entry.ts +1 -4
- package/src/effects/fs-transaction.ts +85 -17
- package/src/runtime/context/codegraph-read-provider.ts +233 -2
- package/src/runtime/context/codegraph-sidecar.cjs +74 -40
- package/src/runtime/context/generic-lsp-provider.ts +355 -0
- package/src/runtime/context/lsp-client.ts +283 -0
- package/src/runtime/context/semantic-navigation-contract.ts +61 -0
- package/src/runtime/context/semantic-navigation.ts +223 -0
- package/src/runtime/context/swift-navigation.ts +565 -0
- package/src/runtime/context/typescript-navigation.ts +226 -0
- package/src/runtime/control-plane/dispatch-priority.ts +28 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +48 -16
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
- package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
- package/src/runtime/control-plane/execution/session-store.ts +18 -0
- package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
- package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
- package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
- package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
- package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
- package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
- package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
- package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +22 -18
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +156 -11
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
- package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
- package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +90 -9
- package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
- package/src/runtime/control-plane/facade/controller-round-relay.ts +1118 -0
- package/src/runtime/control-plane/facade/controller-session-store.ts +302 -12
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +17 -1
- package/src/runtime/control-plane/facade/goal-workloop.ts +928 -190
- package/src/runtime/control-plane/facade/index.ts +3 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +275 -42
- package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
- package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
- package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
- package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
- package/src/runtime/control-plane/facade/types.ts +90 -10
- package/src/runtime/control-plane/facade/work-contract-store.ts +157 -261
- package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
- package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
- package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
- package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +152 -0
- package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +278 -543
- package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
- package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
- package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
- package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
- package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
- package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +63 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +365 -117
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +89 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +244 -11
- package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
- package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
- package/src/runtime/control-plane/release-retention.ts +62 -2
- package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
- package/src/runtime/control-plane/routing/route-policy.ts +24 -21
- package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
- package/src/runtime/control-plane/runtime-status-client.ts +0 -2
- package/src/runtime/diagnostics/mcp-timing.ts +13 -0
- package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
- package/src/runtime/diagnostics/performance.ts +24 -1
- package/src/runtime/evidence/event-ledger.ts +1 -1
- package/src/runtime/evidence/process-check-execution.ts +18 -0
- package/src/runtime/execution/jobs/child-reference.ts +6 -2
- package/src/runtime/execution/jobs/store.ts +58 -4
- package/src/runtime/execution/jobs/types.ts +1 -1
- package/src/runtime/execution/managed-workspace.ts +203 -1
- package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
- package/src/runtime/execution/process-runtime/check-result.ts +88 -0
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
- package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +367 -40
- package/src/runtime/execution/process-runtime/gc.ts +38 -6
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +1201 -0
- package/src/runtime/execution/process-runtime/persisted-check.ts +51 -5
- package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
- package/src/runtime/execution/process-runtime/runtime.ts +53 -3
- package/src/runtime/execution/process-runtime/store.ts +1 -0
- package/src/runtime/execution/process-runtime/types.ts +4 -12
- package/src/runtime/execution/thin-harness/async-process.ts +12 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
- package/src/runtime/execution/work-activity.ts +21 -0
- package/src/runtime/execution/workers/executor.ts +18 -61
- package/src/runtime/gateway/mcp/execution-tools.ts +35 -1852
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
- package/src/runtime/gateway/mcp/process-tools.ts +83 -37
- package/src/runtime/gateway/mcp/router.ts +186 -84
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +565 -0
- package/src/runtime/gateway/mcp/runtime-tools.ts +2378 -1947
- package/src/runtime/plugins/app-store-connect-adapter.ts +216 -47
- package/src/runtime/plugins/browser-adapter.ts +2662 -829
- package/src/runtime/plugins/browser-automation-service.ts +10 -0
- package/src/runtime/plugins/browser-handoff-host.ts +2 -25
- package/src/runtime/plugins/browser-handoff.ts +110 -5
- package/src/runtime/plugins/browser-macos-bridge.ts +701 -63
- package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
- package/src/runtime/plugins/browser-native-session-service.ts +222 -0
- package/src/runtime/plugins/browser-node-bridge.ts +6 -34
- package/src/runtime/plugins/browser-provider-registry.ts +109 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +154 -0
- package/src/runtime/plugins/browser-runtime.ts +193 -0
- package/src/runtime/plugins/browser-session-authority.ts +332 -0
- package/src/runtime/plugins/browser-session-store.ts +121 -0
- package/src/runtime/plugins/browser-session-types.ts +98 -0
- package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +10 -4
- package/src/runtime/plugins/external-adapter.ts +358 -3
- package/src/runtime/plugins/external-unix-socket.ts +7 -1
- package/src/runtime/plugins/first-party-registry.ts +2 -0
- package/src/runtime/plugins/github-adapter.ts +125 -0
- package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
- package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
- package/src/runtime/plugins/ios-adapter.ts +106 -16
- package/src/runtime/plugins/ios-agent-device.ts +234 -64
- package/src/runtime/plugins/ios-physical-device.ts +1143 -392
- package/src/runtime/plugins/lightweight-action.ts +153 -0
- package/src/runtime/plugins/local-system-adapter.ts +123 -6
- package/src/runtime/plugins/macos-capability-broker.ts +47 -9
- package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
- package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
- package/src/runtime/plugins/resend-adapter.ts +1 -1
- package/src/runtime/plugins/store.ts +330 -33
- package/src/runtime/plugins/types.ts +38 -3
- package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
- package/src/runtime/projections/git-status-sampler.ts +16 -16
- package/src/runtime/projections/materialized-view.ts +28 -10
- package/src/runtime/recovery/maintenance-executor.ts +426 -25
- package/src/runtime/root/mcp-transport.ts +180 -10
- package/src/runtime/root/package-connector-service.ts +230 -10
- package/src/runtime/root/package-runtime-release.ts +143 -17
- package/src/runtime/root/package-runtime-service.ts +461 -22
- package/src/runtime/root/release-manifest.ts +144 -18
- package/src/runtime/root/release-materialize.ts +125 -32
- package/src/runtime/root/release-store.ts +48 -11
- package/src/runtime/root/runtime.ts +23 -5
- package/src/runtime/root/service.ts +100 -29
- package/src/runtime/root/types.ts +12 -0
- package/src/runtime/safe-tooling/ios-development.ts +321 -63
- package/src/runtime/shared/trusted-executable.ts +62 -0
- package/src/runtime/shared/trusted-node-executable.ts +48 -0
- package/src/runtime/standalone-recovery/core.ts +1079 -240
- package/src/runtime/standalone-recovery/entry.ts +100 -24
- package/src/runtime/standalone-recovery/installer.ts +20 -4
- package/src/runtime/standalone-recovery/service-labels.ts +2 -0
- package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
- package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
- package/src/runtime/workflow/schedules/engine.ts +364 -202
- package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
- package/src/runtime/workflow/schedules/settlement.ts +64 -17
- package/src/runtime/workflow/schedules/store.ts +209 -19
- package/src/runtime/workflow/schedules/types.ts +2 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
- package/src/testing/test-governance.ts +8 -40
- package/assets/reference-configs/heartbeat-triage.md +0 -57
- package/assets/templates/helpers/harness-trace-grade.sh +0 -150
- package/assets/templates/helpers/heartbeat-triage.sh +0 -361
- package/assets/templates/helpers/maintenance-triage.sh +0 -167
- package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
- package/docs/architecture/current/architecture-invariants.md +0 -331
- package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
- package/docs/architecture/current/codegraph-context-provider.md +0 -238
- package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
- package/docs/architecture/current/controller-repository-decoupling.md +0 -148
- package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
- package/docs/architecture/current/entity-model.md +0 -612
- package/docs/architecture/current/external-plugin-governance.md +0 -396
- package/docs/architecture/current/failure-recovery.md +0 -447
- package/docs/architecture/current/governance.md +0 -213
- package/docs/architecture/current/human-interaction-plane.md +0 -87
- package/docs/architecture/current/implementation-status.md +0 -128
- package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
- package/docs/architecture/current/legacy-governance.md +0 -58
- package/docs/architecture/current/local-workspace-target-grants.md +0 -70
- package/docs/architecture/current/migration-roadmap.md +0 -105
- package/docs/architecture/current/multi-repository-execution.md +0 -371
- package/docs/architecture/current/operations-runbook.md +0 -45
- package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
- package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
- package/docs/architecture/current/repository-access-modes.md +0 -102
- package/docs/architecture/current/route-policy.md +0 -15
- package/docs/architecture/current/route-session-concurrency.md +0 -85
- package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
- package/docs/architecture/current/runtime-directory-map.md +0 -49
- package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
- package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
- package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
- package/docs/architecture/current/system-overview.md +0 -169
- package/docs/architecture/current/target-requirements-traceability.md +0 -202
- package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
- package/docs/architecture/current/thin-harness-v1.md +0 -321
- package/docs/architecture/current/verification-and-release-gates.md +0 -391
- package/docs/forge-autonomous-goal-loop.md +0 -9
- package/docs/forge-provider-configuration.md +0 -7
- package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
- package/docs/operations/gmail-assistant-routines.md +0 -186
- package/docs/operations/personal-assistant-operator-guide.md +0 -61
- package/docs/operations/provider-configuration.md +0 -31
- package/scripts/harness-trace-grade.sh +0 -150
- package/scripts/heartbeat-triage.sh +0 -361
- package/scripts/maintenance-triage.sh +0 -167
- package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
- package/src/cli/repositories/goal-registry.ts +0 -239
- package/src/runtime/assistant/action-proposals.ts +0 -279
- package/src/runtime/assistant/intent.ts +0 -395
- package/src/runtime/assistant/model-provider.ts +0 -460
- package/src/runtime/assistant/openapi.ts +0 -223
- package/src/runtime/assistant/policy.ts +0 -89
- package/src/runtime/assistant/readiness.ts +0 -258
- package/src/runtime/assistant/routine-runtime.ts +0 -477
- package/src/runtime/assistant/schedule-binding.ts +0 -171
- package/src/runtime/assistant/standing-grants.ts +0 -375
- package/src/runtime/assistant/store.ts +0 -243
- package/src/runtime/assistant/types.ts +0 -135
- package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
- package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
- package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
- package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
- package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
- package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
- package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
- package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
- package/src/runtime/control-plane/goal-loop/index.ts +0 -13
- package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
- package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
- package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
- package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
- package/src/runtime/control-plane/goal-loop/types.ts +0 -378
- package/src/runtime/control-plane/startup-recovery.ts +0 -198
- package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
- package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
- package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
- package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
- package/src/runtime/workflow/findings/store.ts +0 -204
- package/src/runtime/workflow/findings/types.ts +0 -58
- package/src/runtime/workflow/portfolio/engine.ts +0 -20
- package/src/runtime/workflow/portfolio/store.ts +0 -185
- package/src/runtime/workflow/portfolio/types.ts +0 -29
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'child_process';
|
|
2
|
+
import { capProcessOutput, redactProcessOutput } from '../../effects/process-runner';
|
|
3
|
+
import { repositoryChildProcessEnvironment } from '../../runtime/shared/process-environment';
|
|
4
|
+
import { terminateProcessTree } from '../../runtime/shared/process-tree';
|
|
5
|
+
import type { CanonicalRepositoryCommand } from './command-normalization';
|
|
6
|
+
|
|
7
|
+
export interface SpawnCommandResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
exitCode: number;
|
|
10
|
+
timedOut: boolean;
|
|
11
|
+
cancelled: boolean;
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RepositoryCommandAsyncHooks {
|
|
16
|
+
onSpawn?: (pid: number) => void;
|
|
17
|
+
onStdout?: (chunk: string) => void;
|
|
18
|
+
onStderr?: (chunk: string) => void;
|
|
19
|
+
/** Observe the exact child exit result before repository post-processing begins. */
|
|
20
|
+
onExit?: (result: SpawnCommandResult) => void;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function commandEnvironment(): NodeJS.ProcessEnv {
|
|
25
|
+
const allowed = ['PATH', 'HOME', 'USER', 'LOGNAME', 'TMPDIR', 'TMP', 'TEMP', 'SHELL', 'LANG', 'LC_ALL', 'TERM', 'SSH_AUTH_SOCK', 'GPG_TTY', 'XDG_CONFIG_HOME'];
|
|
26
|
+
const env: NodeJS.ProcessEnv = {};
|
|
27
|
+
for (const key of allowed) if (process.env[key] !== undefined) env[key] = process.env[key];
|
|
28
|
+
env.GIT_TERMINAL_PROMPT = '0';
|
|
29
|
+
env.CI = '1';
|
|
30
|
+
return repositoryChildProcessEnvironment(env);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function collectOutput(maxOutputBytes: number): { write(chunk: string | Buffer): void; complete(): string } {
|
|
34
|
+
const chunks: Buffer[] = [];
|
|
35
|
+
let totalBytes = 0;
|
|
36
|
+
let truncated = false;
|
|
37
|
+
return {
|
|
38
|
+
write(chunk) {
|
|
39
|
+
const buffer = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
|
|
40
|
+
if (buffer.length === 0) return;
|
|
41
|
+
if (totalBytes >= maxOutputBytes) { truncated = true; return; }
|
|
42
|
+
const remaining = maxOutputBytes - totalBytes;
|
|
43
|
+
chunks.push(buffer.length <= remaining ? buffer : buffer.subarray(0, remaining));
|
|
44
|
+
totalBytes += Math.min(buffer.length, remaining);
|
|
45
|
+
if (buffer.length > remaining) truncated = true;
|
|
46
|
+
},
|
|
47
|
+
complete() {
|
|
48
|
+
const redacted = redactProcessOutput(Buffer.concat(chunks).toString('utf8'));
|
|
49
|
+
return truncated ? capProcessOutput(redacted, maxOutputBytes) : redacted;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function killCommandTree(child: ChildProcess): Promise<void> {
|
|
55
|
+
const pid = child.pid;
|
|
56
|
+
if (!pid) return;
|
|
57
|
+
try {
|
|
58
|
+
if (process.platform !== 'win32') {
|
|
59
|
+
try { process.kill(-pid, 'SIGTERM'); } catch { child.kill('SIGTERM'); }
|
|
60
|
+
} else child.kill();
|
|
61
|
+
} catch { /* already exited */ }
|
|
62
|
+
await terminateProcessTree(pid, { gracePeriodMs: 200, killAfterMs: 1_000, pollIntervalMs: 25 });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function runCanonicalCommand(
|
|
66
|
+
command: CanonicalRepositoryCommand,
|
|
67
|
+
cwd: string,
|
|
68
|
+
timeoutMs: number,
|
|
69
|
+
maxOutputBytes: number,
|
|
70
|
+
hooks: RepositoryCommandAsyncHooks = {},
|
|
71
|
+
): Promise<SpawnCommandResult> {
|
|
72
|
+
if (hooks.signal?.aborted) return { ok: false, exitCode: 1, timedOut: false, cancelled: true, stdout: '', stderr: 'cancelled before spawn' };
|
|
73
|
+
const executable = command.kind === 'argv' ? command.executable! : process.platform === 'win32' ? 'cmd.exe' : '/bin/sh';
|
|
74
|
+
const args = command.kind === 'argv' ? [...(command.args ?? [])] : process.platform === 'win32' ? ['/d', '/s', '/c', command.shellCommand!] : ['-c', command.shellCommand!];
|
|
75
|
+
const display = typeof command.value === 'string' ? command.value : JSON.stringify(command.value);
|
|
76
|
+
const stdout = collectOutput(maxOutputBytes);
|
|
77
|
+
const stderr = collectOutput(maxOutputBytes);
|
|
78
|
+
return await new Promise<SpawnCommandResult>((resolve) => {
|
|
79
|
+
const child = spawn(executable, args, { cwd, env: commandEnvironment(), stdio: ['ignore', 'pipe', 'pipe'], detached: process.platform !== 'win32' });
|
|
80
|
+
if (child.pid) hooks.onSpawn?.(child.pid);
|
|
81
|
+
let settled = false;
|
|
82
|
+
let timedOut = false;
|
|
83
|
+
let cancelled = false;
|
|
84
|
+
let spawnError = '';
|
|
85
|
+
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
86
|
+
const finish = (exitCode: number) => {
|
|
87
|
+
if (settled) return;
|
|
88
|
+
settled = true;
|
|
89
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
90
|
+
hooks.signal?.removeEventListener('abort', onAbort);
|
|
91
|
+
const errors = [stderr.complete()];
|
|
92
|
+
if (timedOut) errors.push(`process timed out after ${timeoutMs}ms: ${redactProcessOutput(display)}`);
|
|
93
|
+
if (cancelled) errors.push('process cancelled');
|
|
94
|
+
if (spawnError) errors.push(redactProcessOutput(spawnError));
|
|
95
|
+
const result = { ok: exitCode === 0 && !timedOut && !cancelled && !spawnError, exitCode, timedOut, cancelled, stdout: stdout.complete(), stderr: capProcessOutput(errors.filter(Boolean).join('\n'), maxOutputBytes) };
|
|
96
|
+
try { hooks.onExit?.(result); } catch { /* observation hooks must never change the command outcome */ }
|
|
97
|
+
resolve(result);
|
|
98
|
+
};
|
|
99
|
+
const onAbort = () => { cancelled = true; void killCommandTree(child).finally(() => finish(1)); };
|
|
100
|
+
timeoutHandle = setTimeout(() => { timedOut = true; void killCommandTree(child).finally(() => finish(1)); }, timeoutMs);
|
|
101
|
+
timeoutHandle.unref();
|
|
102
|
+
hooks.signal?.addEventListener('abort', onAbort, { once: true });
|
|
103
|
+
child.stdout?.on('data', (chunk) => { stdout.write(chunk); hooks.onStdout?.(chunk.toString()); });
|
|
104
|
+
child.stderr?.on('data', (chunk) => { stderr.write(chunk); hooks.onStderr?.(chunk.toString()); });
|
|
105
|
+
child.on('error', (error) => { spawnError = error.message; });
|
|
106
|
+
child.on('close', (code) => finish(code ?? 1));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -50,64 +50,122 @@ export function resolveRepositoryCommandCwd(
|
|
|
50
50
|
return { root, cwd, relativeCwd };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
interface RepositoryCommandInputPolicyOptions {
|
|
54
|
+
/** Internal lane hint: preserve repository policy while allowing an already-classified local script shape. */
|
|
55
|
+
allowOpaqueLocalScript?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function assertRepositoryCommandAllowed(
|
|
59
|
+
command: string,
|
|
60
|
+
_options: RepositoryCommandInputPolicyOptions = {},
|
|
61
|
+
): string {
|
|
54
62
|
const normalized = command.trim();
|
|
55
63
|
if (!normalized) throw new Error('COMMAND_INVALID: command is required');
|
|
56
64
|
if (normalized.length > MAX_COMMAND_LENGTH) {
|
|
57
65
|
throw new Error(`COMMAND_INVALID: command exceeds ${MAX_COMMAND_LENGTH} characters`);
|
|
58
66
|
}
|
|
59
67
|
if (normalized.includes('\0')) throw new Error('COMMAND_INVALID: command contains a null byte');
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
[/\b(?:python\d*|node|ruby|perl)\b\s+(?:-[ce]\b|--eval\b)/i, 'inline interpreter execution is not allowed'],
|
|
66
|
-
[/(?:^|[\s'"=])(?:\.\.(?:\/|\\)|~(?:\/|\\)|[A-Za-z]:[\\/])/, 'ambiguous parent/home/drive paths are not allowed; use repo-relative paths or an authorized absolute external path'],
|
|
67
|
-
[/(?:^|[;&|]\s*)cd(?:\s|$)/i, 'use the cwd argument instead of cd'],
|
|
68
|
-
[/\b(?:env|printenv)\b|\bgh\s+auth\s+token\b|\bgit\s+credential\b|\bsecurity\s+find-(?:generic|internet)-password\b/i, 'credential or environment inspection is not allowed'],
|
|
69
|
-
[/(?:^|[\s'"/])(?:\.ssh|\.aws|\.gnupg|Library\/Keychains|login\.keychain|\.config\/gh\/hosts\.yml)(?:[\s'"/]|$)/i, 'sensitive credential paths are not allowed'],
|
|
70
|
-
[/(?:^|[;&|]\s*)(?:curl|wget|scp|sftp|ftp|nc|ncat|netcat|socat|ssh)(?:\s|$)/i, 'direct network or exfiltration utilities are not allowed'],
|
|
71
|
-
[/\bgit\s+(?:--git-dir|--work-tree|-C)\b|\b(?:GIT_DIR|GIT_WORK_TREE)\s*=/i, 'Git repository scope overrides are not allowed'],
|
|
72
|
-
[/\bgit\s+config\b[^\n]*(?:--global|--system)\b/i, 'global or system Git configuration changes are not allowed'],
|
|
73
|
-
];
|
|
74
|
-
for (const [pattern, reason] of denied) {
|
|
75
|
-
if (pattern.test(normalized)) throw new Error(`COMMAND_POLICY_DENIED: ${reason}`);
|
|
76
|
-
}
|
|
68
|
+
// Repository commands are an execution surface, not a shell-language policy
|
|
69
|
+
// engine. Do not maintain an enumerated command/program denylist here: it is
|
|
70
|
+
// necessarily incomplete and turns new CLI/subcommand shapes into false
|
|
71
|
+
// negatives. Structural repository/cwd/external-path authority is enforced
|
|
72
|
+
// separately after normalization.
|
|
77
73
|
return normalized;
|
|
78
74
|
}
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
const
|
|
76
|
+
function macOsTccSensitiveShellTool(command: string): string | undefined {
|
|
77
|
+
const match = command.match(/(?:^|[\n;&|])\s*(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+\s+)*(?:(?:command|exec|sudo|nohup)\s+)?(?:\/usr\/(?:bin|sbin)\/)?(osascript|screencapture)(?=\s|$)/i);
|
|
78
|
+
return match?.[1]?.toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function macOsTccSensitiveArgvTool(command: CanonicalRepositoryCommand): string | undefined {
|
|
82
|
+
if (command.kind !== 'argv') return undefined;
|
|
83
|
+
const executableName = command.executable!.split(/[\\/]/).at(-1)?.toLowerCase() ?? '';
|
|
84
|
+
if (executableName === 'osascript' || executableName === 'screencapture') return executableName;
|
|
85
|
+
if (!['bash', 'sh', 'zsh', 'fish', 'dash', 'cmd', 'powershell', 'pwsh'].includes(executableName)) return undefined;
|
|
86
|
+
const args = command.args ?? [];
|
|
87
|
+
const commandFlagIndex = args.findIndex((arg) => /^(?:-[^-]*c[^-]*|\/c|-Command)$/i.test(arg));
|
|
88
|
+
if (commandFlagIndex < 0) return undefined;
|
|
89
|
+
const shellPayload = args[commandFlagIndex + 1];
|
|
90
|
+
return typeof shellPayload === 'string' ? macOsTccSensitiveShellTool(shellPayload) : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const INLINE_INTERPRETERS = new Set(['node', 'nodejs', 'bun', 'deno']);
|
|
94
|
+
const SHELL_INTERPRETERS = new Set(['bash', 'sh', 'zsh', 'fish', 'dash']);
|
|
95
|
+
const FORGE_PLUGIN_ACTION_EXECUTOR = /\b(?:submitAssistantPluginAction|executeAssistantPluginAction|executeBrowserPluginAction)\b/;
|
|
96
|
+
const FORGE_RUNTIME_PLUGIN_IMPORT = /(?:\bfrom\s*|\bimport\s*\(|\brequire\s*\()\s*['"][^'"]*(?:^|[\\/])(?:src[\\/])?runtime[\\/]plugins[\\/][^'"]+['"]/m;
|
|
97
|
+
|
|
98
|
+
function inlineEvalPayload(command: CanonicalRepositoryCommand): string | undefined {
|
|
82
99
|
if (command.kind === 'shell') {
|
|
83
|
-
|
|
84
|
-
return
|
|
100
|
+
const shell = command.shellCommand ?? '';
|
|
101
|
+
return /(?:^|[;&|]\s*|\s)(?:node|nodejs|bun|deno)(?:\s+[^\n;&|]*)?\s(?:-e|--eval)(?:\s|=)/i.test(shell)
|
|
102
|
+
? shell
|
|
103
|
+
: undefined;
|
|
85
104
|
}
|
|
86
105
|
const executableName = command.executable!.split(/[\\/]/).at(-1)?.toLowerCase() ?? '';
|
|
87
106
|
const args = command.args ?? [];
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
if (SHELL_INTERPRETERS.has(executableName)) {
|
|
108
|
+
const commandFlagIndex = args.findIndex((arg) => /^(?:-[^-]*c[^-]*)$/i.test(arg));
|
|
109
|
+
const payload = commandFlagIndex >= 0 ? args[commandFlagIndex + 1] : undefined;
|
|
110
|
+
return typeof payload === 'string'
|
|
111
|
+
&& /(?:^|[;&|]\s*|\s)(?:node|nodejs|bun|deno)(?:\s+[^\n;&|]*)?\s(?:-e|--eval)(?:\s|=)/i.test(payload)
|
|
112
|
+
? payload
|
|
113
|
+
: undefined;
|
|
95
114
|
}
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
115
|
+
if (!INLINE_INTERPRETERS.has(executableName)) return undefined;
|
|
116
|
+
const evalIndex = args.findIndex((arg) => arg === '-e' || arg === '--eval');
|
|
117
|
+
if (evalIndex >= 0) return args[evalIndex + 1];
|
|
118
|
+
const inline = args.find((arg) => arg.startsWith('--eval='));
|
|
119
|
+
return inline?.slice('--eval='.length);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Process Runtime deliberately supports bounded direct argv eval for diagnostics
|
|
124
|
+
* and test helpers. What it must not support is using that escape hatch as a
|
|
125
|
+
* second plugin executor: typed plugin actions own authorization, resource
|
|
126
|
+
* claims, receipts, and recovery semantics through plugin_action_execute.
|
|
127
|
+
*/
|
|
128
|
+
export function assertRepositoryCommandNoPluginExecutionBypass(input: unknown): CanonicalRepositoryCommand {
|
|
129
|
+
const command = normalizeRepositoryCommand(input);
|
|
130
|
+
const payload = inlineEvalPayload(command);
|
|
131
|
+
if (payload && FORGE_PLUGIN_ACTION_EXECUTOR.test(payload) && FORGE_RUNTIME_PLUGIN_IMPORT.test(payload)) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
'PLUGIN_ACTION_EXECUTION_REQUIRES_TYPED_PLUGIN_TOOL: repository commands may not invoke Forge runtime plugin action internals; use plugin_action_execute so typed authorization, resource claims, receipts, and recovery remain authoritative',
|
|
134
|
+
);
|
|
104
135
|
}
|
|
105
|
-
|
|
106
|
-
|
|
136
|
+
return command;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Runtime releases live at release-specific executable paths. Never let those
|
|
141
|
+
* binaries become macOS TCC principals: GUI automation and screen capture must
|
|
142
|
+
* be owned by the stable Forge Desktop Operator / typed browser provider.
|
|
143
|
+
*/
|
|
144
|
+
export function assertRepositoryCommandStableHostIdentity(input: unknown): CanonicalRepositoryCommand {
|
|
145
|
+
const command = normalizeRepositoryCommand(input);
|
|
146
|
+
const tool = command.kind === 'shell'
|
|
147
|
+
? macOsTccSensitiveShellTool(command.shellCommand!)
|
|
148
|
+
: macOsTccSensitiveArgvTool(command);
|
|
149
|
+
if (tool) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`COMMAND_POLICY_DENIED: ${tool} is macOS TCC-sensitive and must run through the stable Forge Desktop Operator/browser capability; forge-runtime must not become the permission identity`,
|
|
152
|
+
);
|
|
107
153
|
}
|
|
108
|
-
|
|
109
|
-
|
|
154
|
+
return command;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function assertRepositoryCommandInputAllowed(
|
|
158
|
+
input: unknown,
|
|
159
|
+
options: RepositoryCommandInputPolicyOptions = {},
|
|
160
|
+
): CanonicalRepositoryCommand {
|
|
161
|
+
const command = assertRepositoryCommandStableHostIdentity(input);
|
|
162
|
+
if (command.kind === 'shell') {
|
|
163
|
+
assertRepositoryCommandAllowed(command.shellCommand!, options);
|
|
110
164
|
}
|
|
165
|
+
// Do not infer execution permission from executable names, shell flags, inline
|
|
166
|
+
// interpreter flags, network utilities, or Git subcommands. Those rules are
|
|
167
|
+
// not complete enough to be an authority boundary. Real repository and
|
|
168
|
+
// external-filesystem scope is validated by assertCommandPathOperandsStayInRepository.
|
|
111
169
|
return command;
|
|
112
170
|
}
|
|
113
171
|
|
|
@@ -12,20 +12,19 @@ export function resolveControllerHome(explicit?: string): string {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
* `
|
|
15
|
+
* Resolve the one installed Controller Home. A repository containing a retired
|
|
16
|
+
* `_ops/controller-home` is evidence for an explicit migration only; its mere
|
|
17
|
+
* presence must never redirect a package Runtime, CLI, or MCP caller away from
|
|
18
|
+
* the user-level authority.
|
|
19
|
+
*
|
|
20
|
+
* Repo-local storage remains available only through an explicit
|
|
21
|
+
* `--controller-home` argument or `FORGE_CONTROLLER_HOME` configuration.
|
|
17
22
|
*/
|
|
18
23
|
export function resolveRepoPreferredControllerHome(repoRoot?: string, explicit?: string): string {
|
|
19
24
|
const trimmedExplicit = explicit?.trim();
|
|
20
25
|
if (trimmedExplicit) return resolveControllerHome(trimmedExplicit);
|
|
21
26
|
const configured = process.env.FORGE_CONTROLLER_HOME?.trim();
|
|
22
27
|
if (configured) return resolveControllerHome(configured);
|
|
23
|
-
if (repoRoot?.trim()) {
|
|
24
|
-
const opsHome = join(resolve(repoRoot.trim()), '_ops', 'controller-home');
|
|
25
|
-
if (existsSync(join(opsHome, 'mcp', 'mcp.local.json')) || existsSync(opsHome)) {
|
|
26
|
-
return resolve(opsHome);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
28
|
return resolveControllerHome();
|
|
30
29
|
}
|
|
31
30
|
|
|
@@ -9,6 +9,7 @@ import { resolveRepositoryCommandCwd } from './command-scope';
|
|
|
9
9
|
import type { RepositoryRecord } from './types';
|
|
10
10
|
import { assertResolvedAuthorization, decideAuthorization, type AuthorizationDecision } from '../../runtime/control-plane/governance/authorization';
|
|
11
11
|
import { readRepositoryAccessPolicy } from '../../runtime/control-plane/governance/access-policy';
|
|
12
|
+
import { invalidateRepositoryReadCaches } from '../repository/inspector';
|
|
12
13
|
|
|
13
14
|
export interface ExecuteRepositoryGitCommandInput {
|
|
14
15
|
args: string[];
|
|
@@ -296,6 +297,7 @@ export function executeRepositoryGitCommand(
|
|
|
296
297
|
after,
|
|
297
298
|
repositoryChanged: snapshotChanged(before, after),
|
|
298
299
|
};
|
|
300
|
+
if (execution.repositoryChanged) invalidateRepositoryReadCaches(root);
|
|
299
301
|
auditExecution(controllerHome, repository, execution);
|
|
300
302
|
return execution;
|
|
301
303
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeSync, mkdirSync, openSync, readFileSync, rmSync,
|
|
1
|
+
import { closeSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
2
2
|
import { dirname, join } from 'path';
|
|
3
3
|
import { ensureControllerHome, repositoryControllerRoot } from './controller-home';
|
|
4
4
|
import { recordGatewayLatency } from '../../runtime/observability/gateway-contention-metrics';
|
|
@@ -39,7 +39,6 @@ export type ControllerLockTryResult =
|
|
|
39
39
|
const DEFAULT_ASYNC_LOCK_WAIT_MS = 500;
|
|
40
40
|
const MAX_ASYNC_LOCK_WAIT_MS = 5_000;
|
|
41
41
|
const LOCK_POLL_INTERVAL_MS = 10;
|
|
42
|
-
const CORRUPT_LOCK_GRACE_MS = 1_000;
|
|
43
42
|
|
|
44
43
|
function safe(value: string): string {
|
|
45
44
|
return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'unknown';
|
|
@@ -96,23 +95,59 @@ function isPidAlive(pid: number | undefined): boolean {
|
|
|
96
95
|
}
|
|
97
96
|
}
|
|
98
97
|
|
|
99
|
-
|
|
98
|
+
function readControllerLockRecord(path: string): ControllerLockRecord | undefined {
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(readFileSync(path, 'utf-8')) as ControllerLockRecord;
|
|
101
|
+
} catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Reap only the exact stale lock identity that the caller observed. The
|
|
108
|
+
* per-lockId guard serializes competing reapers, then the canonical path is
|
|
109
|
+
* re-read before unlink so an owner that replaced the stale record is never
|
|
110
|
+
* deleted from an old observation.
|
|
111
|
+
*/
|
|
112
|
+
export function reapControllerLockIfUnchanged(
|
|
113
|
+
controllerHome: string,
|
|
114
|
+
key: ControllerLockKey,
|
|
115
|
+
observed: ControllerLockRecord,
|
|
116
|
+
): boolean {
|
|
100
117
|
const path = controllerLockPath(controllerHome, key);
|
|
118
|
+
const guardPath = `${path}.reap-${safe(observed.lockId)}.guard`;
|
|
119
|
+
let guardFd: number | undefined;
|
|
101
120
|
try {
|
|
102
|
-
|
|
103
|
-
if (isExpired(record) || !isPidAlive(record.pid)) {
|
|
104
|
-
rmSync(path, { force: true });
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
return record;
|
|
121
|
+
guardFd = openSync(guardPath, 'wx');
|
|
108
122
|
} catch {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const current = readControllerLockRecord(path);
|
|
127
|
+
if (!current || current.lockId !== observed.lockId) return false;
|
|
128
|
+
if (!isExpired(current) && isPidAlive(current.pid)) return false;
|
|
129
|
+
rmSync(path, { force: true });
|
|
130
|
+
return true;
|
|
131
|
+
} finally {
|
|
132
|
+
if (guardFd !== undefined) closeSync(guardFd);
|
|
133
|
+
rmSync(guardPath, { force: true });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function readControllerLock(controllerHome: string, key: ControllerLockKey): ControllerLockRecord | undefined {
|
|
138
|
+
const path = controllerLockPath(controllerHome, key);
|
|
139
|
+
const record = readControllerLockRecord(path);
|
|
140
|
+
if (!record) {
|
|
141
|
+
// Canonical lock publication is atomic. A malformed file therefore means
|
|
142
|
+
// legacy/corrupt state; fail closed instead of deleting an identity that
|
|
143
|
+
// cannot be compared safely.
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
if (isExpired(record) || !isPidAlive(record.pid)) {
|
|
147
|
+
reapControllerLockIfUnchanged(controllerHome, key, record);
|
|
114
148
|
return undefined;
|
|
115
149
|
}
|
|
150
|
+
return record;
|
|
116
151
|
}
|
|
117
152
|
|
|
118
153
|
function metric(key: ControllerLockKey, waitedMs: number, outcome: 'success' | 'contention' | 'timeout'): void {
|
|
@@ -157,9 +192,11 @@ export function tryAcquireControllerLock(
|
|
|
157
192
|
};
|
|
158
193
|
let fd: number | undefined;
|
|
159
194
|
try {
|
|
195
|
+
// The exclusive create is the ownership primitive. A concurrent reader may
|
|
196
|
+
// briefly observe incomplete JSON, but malformed state is never deleted;
|
|
197
|
+
// competing open(..., 'wx') attempts therefore still fail closed.
|
|
160
198
|
fd = openSync(path, 'wx');
|
|
161
|
-
writeFileSync(fd, `${JSON.stringify(record, null, 2)}
|
|
162
|
-
`, 'utf-8');
|
|
199
|
+
writeFileSync(fd, `${JSON.stringify(record, null, 2)}\n`, 'utf-8');
|
|
163
200
|
const waitedMs = performance.now() - started;
|
|
164
201
|
if (recordMetrics) metric(key, waitedMs, 'success');
|
|
165
202
|
return { acquired: true, lock: record, waitedMs };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { spawn, spawnSync } from 'child_process';
|
|
5
|
+
import { capProcessOutput, redactProcessOutput } from '../../effects/process-runner';
|
|
6
|
+
import { commandEnvironment, collectOutput } from './command-process';
|
|
7
|
+
|
|
8
|
+
export interface RepositoryCommandSnapshot {
|
|
9
|
+
head: string | null;
|
|
10
|
+
branch: string | null;
|
|
11
|
+
status: string;
|
|
12
|
+
dirty: boolean;
|
|
13
|
+
refsHash: string;
|
|
14
|
+
paths: string[];
|
|
15
|
+
pathFingerprints: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function emptyWorkspaceSnapshot(): RepositoryCommandSnapshot {
|
|
19
|
+
return { head: null, branch: null, status: '', dirty: false, refsHash: createHash('sha256').update('').digest('hex'), paths: [], pathFingerprints: {} };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function commandOutput(command: string, args: string[], cwd: string, maxOutputBytes: number): { ok: boolean; stdout: string } {
|
|
23
|
+
const result = spawnSync(command, args, { cwd, env: commandEnvironment(), encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000, maxBuffer: Math.max(maxOutputBytes, 1024 * 1024) });
|
|
24
|
+
return { ok: result.status === 0 && !result.error, stdout: capProcessOutput(redactProcessOutput(typeof result.stdout === 'string' ? result.stdout : ''), maxOutputBytes) };
|
|
25
|
+
}
|
|
26
|
+
function gitText(root: string, args: string[]): string {
|
|
27
|
+
const output = commandOutput('git', ['-C', root, ...args], root, 256 * 1024);
|
|
28
|
+
return output.ok ? output.stdout.trim() : '';
|
|
29
|
+
}
|
|
30
|
+
const SNAPSHOT_PATHS = ['.', ':(exclude).ai/harness/**', ':(exclude)_ops/controller-home/**'];
|
|
31
|
+
function statusPath(line: string): string | undefined {
|
|
32
|
+
if (!line.trim() || line.startsWith('##')) return undefined;
|
|
33
|
+
const raw = line.length > 3 ? line.slice(3) : '';
|
|
34
|
+
const path = raw.includes(' -> ') ? raw.split(' -> ').at(-1) : raw;
|
|
35
|
+
return path?.replace(/^"|"$/g, '');
|
|
36
|
+
}
|
|
37
|
+
function pathFingerprint(root: string, relativePath: string, statusLines: string[]): string {
|
|
38
|
+
const hash = createHash('sha256').update(statusLines.join('\n'));
|
|
39
|
+
const absolute = resolve(root, relativePath);
|
|
40
|
+
if (!existsSync(absolute)) return hash.update('\nmissing').digest('hex');
|
|
41
|
+
try {
|
|
42
|
+
const stat = lstatSync(absolute);
|
|
43
|
+
if (stat.isSymbolicLink()) hash.update(`\nsymlink:${readlinkSync(absolute)}`);
|
|
44
|
+
else if (stat.isFile()) hash.update('\nfile:').update(readFileSync(absolute));
|
|
45
|
+
else hash.update(`\nmode:${stat.mode}:size:${stat.size}`);
|
|
46
|
+
} catch (error) { hash.update(`\nunreadable:${error instanceof Error ? error.message : String(error)}`); }
|
|
47
|
+
return hash.digest('hex');
|
|
48
|
+
}
|
|
49
|
+
function buildSnapshot(root: string, head: string | null, branch: string | null, status: string, refs: string): RepositoryCommandSnapshot {
|
|
50
|
+
const lines = status.split(/\r?\n/).filter((line) => line && !line.startsWith('##'));
|
|
51
|
+
const byPath = new Map<string, string[]>();
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
const path = statusPath(line); if (!path) continue;
|
|
54
|
+
byPath.set(path, [...(byPath.get(path) ?? []), line]);
|
|
55
|
+
}
|
|
56
|
+
const paths = [...byPath.keys()].sort();
|
|
57
|
+
return { head, branch, status, dirty: paths.length > 0, refsHash: createHash('sha256').update(refs).digest('hex'), paths, pathFingerprints: Object.fromEntries(paths.map((path) => [path, pathFingerprint(root, path, byPath.get(path) ?? [])])) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function repositorySnapshot(root: string): RepositoryCommandSnapshot {
|
|
61
|
+
return buildSnapshot(root, gitText(root, ['rev-parse', '--verify', 'HEAD']) || null, gitText(root, ['branch', '--show-current']) || null, gitText(root, ['status', '--porcelain=v1', '--branch', '--untracked-files=all', '--', ...SNAPSHOT_PATHS]), gitText(root, ['show-ref']));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface GitResult { ok: boolean; exitCode: number; stdout: string; stderr: string; timedOut: boolean; cancelled: boolean }
|
|
65
|
+
async function gitAsync(root: string, args: string[], signal?: AbortSignal): Promise<GitResult> {
|
|
66
|
+
if (signal?.aborted) return { ok: false, exitCode: 1, stdout: '', stderr: 'cancelled', timedOut: false, cancelled: true };
|
|
67
|
+
return await new Promise((resolveResult) => {
|
|
68
|
+
const child = spawn('git', ['-C', root, ...args], { cwd: root, env: commandEnvironment(), stdio: ['ignore', 'pipe', 'pipe'], detached: process.platform !== 'win32' });
|
|
69
|
+
const stdout = collectOutput(256 * 1024); const stderr = collectOutput(64 * 1024);
|
|
70
|
+
let settled = false; let timedOut = false; let cancelled = false;
|
|
71
|
+
const finish = (exitCode: number, forcedError?: string) => {
|
|
72
|
+
if (settled) return; settled = true; clearTimeout(timeout); signal?.removeEventListener('abort', abort);
|
|
73
|
+
resolveResult({ ok: exitCode === 0 && !timedOut && !cancelled, exitCode, stdout: stdout.complete().trim(), stderr: forcedError ?? stderr.complete(), timedOut, cancelled });
|
|
74
|
+
};
|
|
75
|
+
const timeout = setTimeout(() => { timedOut = true; try { child.kill(); } catch {} finish(1, `git ${args.join(' ')} timed out`); }, 10_000); timeout.unref();
|
|
76
|
+
const abort = () => { cancelled = true; try { child.kill(); } catch {} finish(1, 'cancelled'); };
|
|
77
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
78
|
+
child.stdout?.on('data', (chunk) => stdout.write(chunk)); child.stderr?.on('data', (chunk) => stderr.write(chunk));
|
|
79
|
+
child.on('error', (error) => finish(1, error.message)); child.on('close', (code) => finish(code ?? 1));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const MAX_DIRTY_PATHS = 200;
|
|
84
|
+
export async function repositorySnapshotAsync(
|
|
85
|
+
root: string,
|
|
86
|
+
signal?: AbortSignal,
|
|
87
|
+
options: { pathFingerprints?: boolean; fingerprintTimeoutMs?: number } = {},
|
|
88
|
+
): Promise<RepositoryCommandSnapshot> {
|
|
89
|
+
const [headResult, branchResult, statusResult, refsResult] = await Promise.all([
|
|
90
|
+
gitAsync(root, ['rev-parse', '--verify', 'HEAD'], signal), gitAsync(root, ['branch', '--show-current'], signal),
|
|
91
|
+
gitAsync(root, ['status', '--porcelain=v1', '--branch', '--untracked-files=all', '--', ...SNAPSHOT_PATHS], signal), gitAsync(root, ['show-ref'], signal),
|
|
92
|
+
]);
|
|
93
|
+
if ([headResult, branchResult, statusResult, refsResult].some((result) => result.cancelled)) throw new Error('CANCELLED: repository snapshot aborted');
|
|
94
|
+
if (statusResult.timedOut || headResult.timedOut) throw new Error('SNAPSHOT_TIMEOUT: git snapshot timed out');
|
|
95
|
+
if (!statusResult.ok) throw new Error(`SNAPSHOT_FAILED: git status exit ${statusResult.exitCode}: ${statusResult.stderr}`);
|
|
96
|
+
let head: string | null = null;
|
|
97
|
+
if (headResult.ok) head = headResult.stdout || null;
|
|
98
|
+
else if (!(headResult.exitCode === 128 || /unknown revision|bad revision|Needed a single revision|ambiguous argument|not a valid object name/i.test(headResult.stderr))) throw new Error(`SNAPSHOT_FAILED: git rev-parse HEAD exit ${headResult.exitCode}: ${headResult.stderr}`);
|
|
99
|
+
const refs = refsResult.ok || (refsResult.exitCode === 1 && (!refsResult.stderr.trim() || /expected|no match|no references/i.test(refsResult.stderr))) ? refsResult.stdout || '' : (() => { throw new Error(`SNAPSHOT_FAILED: git show-ref exit ${refsResult.exitCode}: ${refsResult.stderr}`); })();
|
|
100
|
+
if (signal?.aborted) throw new Error('CANCELLED: repository snapshot aborted');
|
|
101
|
+
const lines = statusResult.stdout.split(/\r?\n/).filter((line) => line && !line.startsWith('##'));
|
|
102
|
+
const includePathFingerprints = options.pathFingerprints !== false;
|
|
103
|
+
if (includePathFingerprints && lines.length > MAX_DIRTY_PATHS) throw new Error(`SNAPSHOT_TOO_DIRTY: ${lines.length} dirty paths exceeds Fast Path cap ${MAX_DIRTY_PATHS}`);
|
|
104
|
+
const byPath = new Map<string, string[]>();
|
|
105
|
+
for (const line of lines) { const path = statusPath(line); if (path) byPath.set(path, [...(byPath.get(path) ?? []), line]); }
|
|
106
|
+
const paths = [...byPath.keys()].sort();
|
|
107
|
+
let pathFingerprints: Record<string, string> = {};
|
|
108
|
+
if (includePathFingerprints) {
|
|
109
|
+
const { computePathFingerprintsAsync } = await import('../../runtime/execution/thin-harness/fingerprint-worker');
|
|
110
|
+
const fingerprintTimeoutMs = typeof options.fingerprintTimeoutMs === 'number' && Number.isFinite(options.fingerprintTimeoutMs)
|
|
111
|
+
? Math.max(1, Math.trunc(options.fingerprintTimeoutMs))
|
|
112
|
+
: 5_000;
|
|
113
|
+
const fingerprint = await computePathFingerprintsAsync({ root, paths, statusByPath: Object.fromEntries(byPath), maxFileBytes: 256 * 1024, maxTotalBytes: 8 * 1024 * 1024, maxPaths: MAX_DIRTY_PATHS }, { signal, timeoutMs: fingerprintTimeoutMs });
|
|
114
|
+
pathFingerprints = fingerprint.pathFingerprints;
|
|
115
|
+
}
|
|
116
|
+
return { head, branch: branchResult.ok ? branchResult.stdout || null : null, status: statusResult.stdout, dirty: paths.length > 0, refsHash: createHash('sha256').update(refs).digest('hex'), paths, pathFingerprints };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function changedSnapshotPaths(before: RepositoryCommandSnapshot, after: RepositoryCommandSnapshot): string[] {
|
|
120
|
+
return [...new Set([...before.paths, ...after.paths])].filter((path) => before.pathFingerprints[path] !== after.pathFingerprints[path]).sort();
|
|
121
|
+
}
|
|
122
|
+
export function snapshotChanged(before: RepositoryCommandSnapshot, after: RepositoryCommandSnapshot): boolean {
|
|
123
|
+
return before.head !== after.head || before.branch !== after.branch || before.refsHash !== after.refsHash || changedSnapshotPaths(before, after).length > 0;
|
|
124
|
+
}
|
|
@@ -4,6 +4,7 @@ import { join, relative, resolve } from 'path';
|
|
|
4
4
|
import { capProcessOutput, redactProcessOutput } from '../../effects/process-runner';
|
|
5
5
|
import { buildPatchHandoffArtifact } from '../../runtime/recovery/patch-handoff';
|
|
6
6
|
import { executeRepositoryGitCommand, type RepositoryGitExecution } from './git-command-executor';
|
|
7
|
+
import { withControllerLock } from './locks';
|
|
7
8
|
import type { RepositoryRecord } from './types';
|
|
8
9
|
|
|
9
10
|
const DEFAULT_MAX_OUTPUT_BYTES = 128 * 1024;
|
|
@@ -254,6 +255,11 @@ export function commitSelectedPaths(
|
|
|
254
255
|
const message = String(input.message ?? '').trim();
|
|
255
256
|
if (!message) throw new Error('COMMIT_MESSAGE_REQUIRED: message is required');
|
|
256
257
|
|
|
258
|
+
return withControllerLock(
|
|
259
|
+
controllerHome,
|
|
260
|
+
{ scope: 'worktree', repoId: repository.repoId, worktreeId: repository.activeCheckoutId },
|
|
261
|
+
`selected-path-commit:${repository.activeCheckoutId}`,
|
|
262
|
+
() => {
|
|
257
263
|
const stage = executeRepositoryGitCommand(controllerHome, repository, {
|
|
258
264
|
args: ['add', '--all', '--', ...paths],
|
|
259
265
|
authorization: 'explicit_user_request',
|
|
@@ -281,15 +287,17 @@ export function commitSelectedPaths(
|
|
|
281
287
|
args: ['commit', '--only', '-m', message, '--', ...paths],
|
|
282
288
|
authorization: 'explicit_user_request',
|
|
283
289
|
});
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
return {
|
|
291
|
+
paths,
|
|
292
|
+
stage,
|
|
293
|
+
stagedPaths: staged,
|
|
294
|
+
commit,
|
|
295
|
+
...(commit.status !== 'executed' || commit.ok !== true
|
|
296
|
+
? { error: { code: 'SELECTED_PATH_COMMIT_FAILED', message: commit.stderr || 'git commit failed for selected paths' } }
|
|
297
|
+
: {}),
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
);
|
|
293
301
|
}
|
|
294
302
|
|
|
295
303
|
export function prepareTransferArtifacts(
|