@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,193 @@
|
|
|
1
|
+
import { AssistantPluginError } from './errors';
|
|
2
|
+
import type { AssistantPluginActionExecutionInput } from './types';
|
|
3
|
+
import {
|
|
4
|
+
validateBrowserTransaction,
|
|
5
|
+
type BrowserActionReplaySafety,
|
|
6
|
+
type BrowserForegroundRequirement,
|
|
7
|
+
type BrowserProviderCapability,
|
|
8
|
+
type BrowserTargetIdentity,
|
|
9
|
+
type BrowserTransaction,
|
|
10
|
+
} from './browser-runtime-contract';
|
|
11
|
+
import {
|
|
12
|
+
BrowserProviderRegistry,
|
|
13
|
+
BrowserProviderUnavailableBeforeActionError,
|
|
14
|
+
type BrowserRuntimeProvider,
|
|
15
|
+
} from './browser-provider-registry';
|
|
16
|
+
|
|
17
|
+
export const ALL_BROWSER_PROVIDER_CAPABILITIES = [
|
|
18
|
+
'dom.read',
|
|
19
|
+
'dom.interact',
|
|
20
|
+
'browser.internal_resources',
|
|
21
|
+
'browser.screenshot',
|
|
22
|
+
'browser.trusted_input',
|
|
23
|
+
'browser.foreground',
|
|
24
|
+
'browser.transaction',
|
|
25
|
+
'browser.persistent_handle',
|
|
26
|
+
] as const satisfies readonly BrowserProviderCapability[];
|
|
27
|
+
|
|
28
|
+
const READ_ACTIONS = new Set([
|
|
29
|
+
'list_sessions', 'reconcile_sessions', 'get_handoff_status',
|
|
30
|
+
'get_text', 'get_html', 'query_selector', 'query_all', 'get_attribute', 'list_frames', 'verify_state',
|
|
31
|
+
'extract_links', 'extract_tables', 'extract_forms', 'snapshot_interactive', 'get_console_errors', 'get_failed_requests',
|
|
32
|
+
'wait_for_load_state', 'wait_for_selector', 'await_file_transfer',
|
|
33
|
+
]);
|
|
34
|
+
const SCREENSHOT_ACTIONS = new Set(['screenshot']);
|
|
35
|
+
const TRUSTED_INPUT_ACTIONS = new Set(['trusted_input', 'activate_page']);
|
|
36
|
+
const DOM_INTERACTION_ACTIONS = new Set([
|
|
37
|
+
'create_session', 'open_page', 'navigate', 'reload', 'go_back',
|
|
38
|
+
'click', 'click_text', 'double_click', 'hover', 'focus', 'type', 'fill', 'select_option', 'check', 'uncheck',
|
|
39
|
+
'press', 'keyboard_shortcut', 'dispatch_event', 'attach_local_file', 'close_page',
|
|
40
|
+
]);
|
|
41
|
+
const IDEMPOTENT_ACTIONS = new Set(['configure', 'close_session', 'clear_session', 'request_human_handoff', 'resolve_handoff']);
|
|
42
|
+
|
|
43
|
+
interface BrowserRuntimeActionPolicy {
|
|
44
|
+
requiredCapabilities: readonly BrowserProviderCapability[];
|
|
45
|
+
foreground: BrowserForegroundRequirement;
|
|
46
|
+
replaySafety: BrowserActionReplaySafety;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const registries = new Map<string, BrowserProviderRegistry<AssistantPluginActionExecutionInput, Record<string, unknown>>>();
|
|
50
|
+
|
|
51
|
+
function registryFor(runtimeKey: string): BrowserProviderRegistry<AssistantPluginActionExecutionInput, Record<string, unknown>> {
|
|
52
|
+
let registry = registries.get(runtimeKey);
|
|
53
|
+
if (!registry) {
|
|
54
|
+
registry = new BrowserProviderRegistry();
|
|
55
|
+
registries.set(runtimeKey, registry);
|
|
56
|
+
}
|
|
57
|
+
return registry;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function invalidateBrowserRuntime(runtimeKey: string, reason: string): void {
|
|
61
|
+
registries.get(runtimeKey)?.invalidateAll(reason);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function browserRuntimeActionPolicy(actionId: string): BrowserRuntimeActionPolicy {
|
|
65
|
+
if (SCREENSHOT_ACTIONS.has(actionId)) {
|
|
66
|
+
return { requiredCapabilities: ['browser.screenshot', 'browser.transaction'], foreground: 'none', replaySafety: 'read_only' };
|
|
67
|
+
}
|
|
68
|
+
if (TRUSTED_INPUT_ACTIONS.has(actionId)) {
|
|
69
|
+
return {
|
|
70
|
+
requiredCapabilities: ['browser.trusted_input', 'browser.foreground', 'browser.transaction'],
|
|
71
|
+
foreground: 'explicit_required',
|
|
72
|
+
replaySafety: 'non_idempotent',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (READ_ACTIONS.has(actionId)) {
|
|
76
|
+
return { requiredCapabilities: ['dom.read', 'browser.transaction'], foreground: 'none', replaySafety: 'read_only' };
|
|
77
|
+
}
|
|
78
|
+
if (DOM_INTERACTION_ACTIONS.has(actionId)) {
|
|
79
|
+
return { requiredCapabilities: ['dom.interact', 'browser.transaction'], foreground: 'none', replaySafety: 'non_idempotent' };
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
requiredCapabilities: ['browser.transaction'],
|
|
83
|
+
foreground: 'none',
|
|
84
|
+
replaySafety: IDEMPOTENT_ACTIONS.has(actionId) ? 'idempotent' : 'non_idempotent',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Post-dispatch replay is a semantic BrowserRuntime decision. Only read-only
|
|
91
|
+
* actions fenced to an already-existing resource may be replayed after a
|
|
92
|
+
* transient provider failure; mutations retain unknown-outcome semantics.
|
|
93
|
+
*/
|
|
94
|
+
export function browserActionCanReplayAfterDispatch(actionId: string, requireExistingResource = true): boolean {
|
|
95
|
+
return requireExistingResource && browserRuntimeActionPolicy(actionId).replaySafety === 'read_only';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function stringArgument(args: Record<string, unknown>, key: string): string | undefined {
|
|
99
|
+
const value = args[key];
|
|
100
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function stableTargetFor(input: AssistantPluginActionExecutionInput, providerId: string): BrowserTargetIdentity {
|
|
104
|
+
const sessionId = stringArgument(input.args, 'session_id');
|
|
105
|
+
const nativeProduct = stringArgument(input.args, 'native_browser_product');
|
|
106
|
+
const nativeWindowId = stringArgument(input.args, 'native_window_id');
|
|
107
|
+
const nativeTabId = stringArgument(input.args, 'native_tab_id');
|
|
108
|
+
if (nativeWindowId && nativeTabId) {
|
|
109
|
+
return {
|
|
110
|
+
providerId,
|
|
111
|
+
resourceKind: 'tab',
|
|
112
|
+
resourceId: `window:${nativeWindowId}/tab:${nativeTabId}`,
|
|
113
|
+
...(nativeProduct ? { browserProduct: nativeProduct } : {}),
|
|
114
|
+
ownership: 'user_owned',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (sessionId) {
|
|
118
|
+
return {
|
|
119
|
+
providerId,
|
|
120
|
+
resourceKind: 'page',
|
|
121
|
+
resourceId: `session:${sessionId}`,
|
|
122
|
+
ownership: 'provider_owned',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
providerId,
|
|
127
|
+
resourceKind: 'page',
|
|
128
|
+
resourceId: `request:${input.requestId}:${input.actionId}`,
|
|
129
|
+
ownership: 'plugin_owned',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function transactionFor(
|
|
134
|
+
input: AssistantPluginActionExecutionInput,
|
|
135
|
+
providerId: string,
|
|
136
|
+
policy: BrowserRuntimeActionPolicy,
|
|
137
|
+
): BrowserTransaction {
|
|
138
|
+
return {
|
|
139
|
+
transactionId: input.requestId,
|
|
140
|
+
target: stableTargetFor(input, providerId),
|
|
141
|
+
requiredCapabilities: policy.requiredCapabilities,
|
|
142
|
+
foreground: policy.foreground,
|
|
143
|
+
replaySafety: policy.replaySafety,
|
|
144
|
+
action: {
|
|
145
|
+
kind: policy.replaySafety === 'read_only' ? 'read' : policy.foreground === 'explicit_required' ? 'trusted_input' : 'dom',
|
|
146
|
+
operation: input.actionId,
|
|
147
|
+
arguments: input.args,
|
|
148
|
+
},
|
|
149
|
+
...(policy.replaySafety === 'read_only'
|
|
150
|
+
? {}
|
|
151
|
+
: { postconditions: [{ kind: 'provider_assertion', assertion: 'semantic_action_result', expected: true }] as const }),
|
|
152
|
+
timeoutMs: typeof input.args.timeout_ms === 'number' && Number.isFinite(input.args.timeout_ms)
|
|
153
|
+
? Math.max(1, Math.trunc(input.args.timeout_ms))
|
|
154
|
+
: 60_000,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function executeBrowserRuntimeAction(options: {
|
|
159
|
+
runtimeKey: string;
|
|
160
|
+
input: AssistantPluginActionExecutionInput;
|
|
161
|
+
providers: readonly BrowserRuntimeProvider<AssistantPluginActionExecutionInput, Record<string, unknown>>[];
|
|
162
|
+
}): Promise<Record<string, unknown>> {
|
|
163
|
+
const registry = registryFor(options.runtimeKey);
|
|
164
|
+
for (const provider of options.providers) registry.register(provider);
|
|
165
|
+
|
|
166
|
+
const policy = browserRuntimeActionPolicy(options.input.actionId);
|
|
167
|
+
const excluded = new Set<string>();
|
|
168
|
+
while (true) {
|
|
169
|
+
const selection = await registry.select({
|
|
170
|
+
input: options.input,
|
|
171
|
+
requiredCapabilities: policy.requiredCapabilities,
|
|
172
|
+
foreground: policy.foreground,
|
|
173
|
+
excludedProviderIds: excluded,
|
|
174
|
+
});
|
|
175
|
+
const transaction = transactionFor(options.input, selection.provider.providerId, policy);
|
|
176
|
+
const validationErrors = validateBrowserTransaction(transaction);
|
|
177
|
+
if (validationErrors.length > 0) {
|
|
178
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_TRANSACTION_INVALID', validationErrors[0]!, {
|
|
179
|
+
retryable: false,
|
|
180
|
+
details: { transactionId: transaction.transactionId, errors: validationErrors },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
return await selection.provider.execute(options.input);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (!(error instanceof BrowserProviderUnavailableBeforeActionError)) throw error;
|
|
188
|
+
excluded.add(selection.provider.providerId);
|
|
189
|
+
// This is provider reselection, not action replay: the typed error proves the rejected provider
|
|
190
|
+
// never attempted the action. Unknown outcomes from non-idempotent actions always propagate.
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
4
|
+
import { join, resolve } from 'path';
|
|
5
|
+
import {
|
|
6
|
+
listControlPlaneRecords,
|
|
7
|
+
readControlPlaneRecord,
|
|
8
|
+
readControlPlaneRecordWithinTransaction,
|
|
9
|
+
withControlPlaneTransaction,
|
|
10
|
+
writeControlPlaneRecordWithinTransaction,
|
|
11
|
+
} from '../control-plane/persistence/sqlite-store';
|
|
12
|
+
|
|
13
|
+
const SESSION_NAMESPACE = 'browser_session';
|
|
14
|
+
const SESSION_SCOPE = 'controller';
|
|
15
|
+
const IMPORT_NAMESPACE = 'browser_session_legacy_import';
|
|
16
|
+
const MAX_AUTHORITY_RECORDS = 5_000;
|
|
17
|
+
export const DEFAULT_BROWSER_SESSION_LIST_LIMIT = 50;
|
|
18
|
+
export const MAX_BROWSER_SESSION_LIST_LIMIT = 200;
|
|
19
|
+
|
|
20
|
+
export interface BrowserSessionAuthorityContext {
|
|
21
|
+
controllerHome: string;
|
|
22
|
+
repoId: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BrowserSessionAuthoritySession {
|
|
26
|
+
schemaVersion: 1;
|
|
27
|
+
sessionId: string;
|
|
28
|
+
url: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
updatedAt: string;
|
|
32
|
+
browser?: {
|
|
33
|
+
provider?: string;
|
|
34
|
+
browserProduct?: string;
|
|
35
|
+
tab?: {
|
|
36
|
+
windowId?: string;
|
|
37
|
+
tabId?: string;
|
|
38
|
+
ownership?: string;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface BrowserSessionAuthorityEntry<T extends BrowserSessionAuthoritySession = BrowserSessionAuthoritySession> {
|
|
44
|
+
schemaVersion: 1;
|
|
45
|
+
status: 'active' | 'tombstoned';
|
|
46
|
+
session: T;
|
|
47
|
+
aliases: string[];
|
|
48
|
+
repositoryIds: string[];
|
|
49
|
+
nativeIdentity?: string;
|
|
50
|
+
tombstonedAt?: string;
|
|
51
|
+
importedFromLegacy?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface LegacyImportMarker {
|
|
55
|
+
schemaVersion: 1;
|
|
56
|
+
repoId: string;
|
|
57
|
+
repoRootFingerprint: string;
|
|
58
|
+
importedAt: string;
|
|
59
|
+
importedRecordCount: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface BrowserSessionAuthorityPage<T extends BrowserSessionAuthoritySession = BrowserSessionAuthoritySession> {
|
|
63
|
+
sessions: T[];
|
|
64
|
+
limit: number;
|
|
65
|
+
totalCount: number;
|
|
66
|
+
nextCursor?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const authorityContextStorage = new AsyncLocalStorage<BrowserSessionAuthorityContext>();
|
|
70
|
+
|
|
71
|
+
export function withBrowserSessionAuthorityContext<T>(
|
|
72
|
+
context: BrowserSessionAuthorityContext,
|
|
73
|
+
operation: () => T,
|
|
74
|
+
): T {
|
|
75
|
+
return authorityContextStorage.run(context, operation);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function currentBrowserSessionAuthorityContext(): BrowserSessionAuthorityContext | undefined {
|
|
79
|
+
return authorityContextStorage.getStore();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function digest(value: string): string {
|
|
83
|
+
return createHash('sha256').update(value).digest('hex');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function nativeIdentity(session: BrowserSessionAuthoritySession): string | undefined {
|
|
87
|
+
const browser = session.browser;
|
|
88
|
+
const tab = browser?.tab;
|
|
89
|
+
if (browser?.provider !== 'macos-apple-events' || !browser.browserProduct || !tab?.windowId || !tab.tabId) return undefined;
|
|
90
|
+
return `${browser.provider}:${browser.browserProduct}:${tab.windowId}:${tab.tabId}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function recordKey(repoId: string, session: BrowserSessionAuthoritySession): string {
|
|
94
|
+
const native = nativeIdentity(session);
|
|
95
|
+
return native ? `native-${digest(native).slice(0, 40)}` : `session-${digest(`${repoId}:${session.sessionId}`).slice(0, 40)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function importMarkerKey(repoRoot: string): string {
|
|
99
|
+
return digest(resolve(repoRoot)).slice(0, 40);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function unique(values: string[]): string[] {
|
|
103
|
+
return [...new Set(values.filter(Boolean))].sort();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseLegacySession(path: string): BrowserSessionAuthoritySession {
|
|
107
|
+
let parsed: unknown;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw new Error(`PLUGIN_BROWSER_SESSION_STATE_CORRUPT: ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
112
|
+
}
|
|
113
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
114
|
+
throw new Error(`PLUGIN_BROWSER_SESSION_STATE_CORRUPT: ${path}: expected object`);
|
|
115
|
+
}
|
|
116
|
+
const session = parsed as Partial<BrowserSessionAuthoritySession>;
|
|
117
|
+
if (session.schemaVersion !== 1 || typeof session.sessionId !== 'string' || typeof session.url !== 'string'
|
|
118
|
+
|| typeof session.createdAt !== 'string' || typeof session.updatedAt !== 'string') {
|
|
119
|
+
throw new Error(`PLUGIN_BROWSER_SESSION_STATE_CORRUPT: ${path}: required session fields are missing`);
|
|
120
|
+
}
|
|
121
|
+
return session as BrowserSessionAuthoritySession;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function legacySessions(repoRoot: string): BrowserSessionAuthoritySession[] {
|
|
125
|
+
const root = join(repoRoot, '.forge', 'browser', 'sessions');
|
|
126
|
+
let names: string[];
|
|
127
|
+
try {
|
|
128
|
+
names = readdirSync(root);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return [];
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
return names.filter((name) => name.endsWith('.json')).sort().map((name) => parseLegacySession(join(root, name)));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function mergeEntry<T extends BrowserSessionAuthoritySession>(
|
|
137
|
+
existing: BrowserSessionAuthorityEntry<T> | undefined,
|
|
138
|
+
repoId: string,
|
|
139
|
+
session: T,
|
|
140
|
+
options: { importedFromLegacy?: boolean } = {},
|
|
141
|
+
): BrowserSessionAuthorityEntry<T> {
|
|
142
|
+
const canonicalSessionId = existing?.session.sessionId ?? session.sessionId;
|
|
143
|
+
const canonicalCreatedAt = existing?.session.createdAt ?? session.createdAt;
|
|
144
|
+
const normalizedSession = {
|
|
145
|
+
...session,
|
|
146
|
+
sessionId: canonicalSessionId,
|
|
147
|
+
createdAt: canonicalCreatedAt,
|
|
148
|
+
} as T;
|
|
149
|
+
return {
|
|
150
|
+
schemaVersion: 1,
|
|
151
|
+
status: 'active',
|
|
152
|
+
session: normalizedSession,
|
|
153
|
+
aliases: unique([...(existing?.aliases ?? []), existing?.session.sessionId ?? '', session.sessionId, canonicalSessionId]),
|
|
154
|
+
repositoryIds: unique([...(existing?.repositoryIds ?? []), repoId]),
|
|
155
|
+
nativeIdentity: nativeIdentity(session) ?? existing?.nativeIdentity,
|
|
156
|
+
importedFromLegacy: options.importedFromLegacy === true || existing?.importedFromLegacy === true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function ensureLegacyBrowserSessionsImported(controllerHome: string, repoId: string, repoRoot: string): number {
|
|
161
|
+
const markerKey = importMarkerKey(repoRoot);
|
|
162
|
+
if (readControlPlaneRecord<LegacyImportMarker>(controllerHome, IMPORT_NAMESPACE, repoId, markerKey)) return 0;
|
|
163
|
+
const sessions = legacySessions(repoRoot);
|
|
164
|
+
return withControlPlaneTransaction(controllerHome, (database) => {
|
|
165
|
+
const marker = readControlPlaneRecordWithinTransaction<LegacyImportMarker>(database, IMPORT_NAMESPACE, repoId, markerKey);
|
|
166
|
+
if (marker) return 0;
|
|
167
|
+
let imported = 0;
|
|
168
|
+
for (const session of sessions) {
|
|
169
|
+
const key = recordKey(repoId, session);
|
|
170
|
+
const current = readControlPlaneRecordWithinTransaction<BrowserSessionAuthorityEntry>(database, SESSION_NAMESPACE, SESSION_SCOPE, key);
|
|
171
|
+
const next = mergeEntry(current?.value, repoId, session, { importedFromLegacy: true });
|
|
172
|
+
writeControlPlaneRecordWithinTransaction(database, {
|
|
173
|
+
namespace: SESSION_NAMESPACE,
|
|
174
|
+
scope: SESSION_SCOPE,
|
|
175
|
+
key,
|
|
176
|
+
schemaVersion: 1,
|
|
177
|
+
value: next,
|
|
178
|
+
expectedRevision: current?.revision ?? null,
|
|
179
|
+
action: current ? 'legacy_merge' : 'legacy_import',
|
|
180
|
+
});
|
|
181
|
+
imported += 1;
|
|
182
|
+
}
|
|
183
|
+
writeControlPlaneRecordWithinTransaction(database, {
|
|
184
|
+
namespace: IMPORT_NAMESPACE,
|
|
185
|
+
scope: repoId,
|
|
186
|
+
key: markerKey,
|
|
187
|
+
schemaVersion: 1,
|
|
188
|
+
value: {
|
|
189
|
+
schemaVersion: 1,
|
|
190
|
+
repoId,
|
|
191
|
+
repoRootFingerprint: markerKey,
|
|
192
|
+
importedAt: new Date().toISOString(),
|
|
193
|
+
importedRecordCount: imported,
|
|
194
|
+
} satisfies LegacyImportMarker,
|
|
195
|
+
expectedRevision: null,
|
|
196
|
+
action: 'legacy_import_complete',
|
|
197
|
+
});
|
|
198
|
+
return imported;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function authorityEntries(controllerHome: string): BrowserSessionAuthorityEntry[] {
|
|
203
|
+
return listControlPlaneRecords<BrowserSessionAuthorityEntry>(controllerHome, {
|
|
204
|
+
namespace: SESSION_NAMESPACE,
|
|
205
|
+
scope: SESSION_SCOPE,
|
|
206
|
+
limit: MAX_AUTHORITY_RECORDS,
|
|
207
|
+
}).map((record) => record.value);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function visibleToRepository(entry: BrowserSessionAuthorityEntry, repoId: string): boolean {
|
|
211
|
+
return Boolean(entry.nativeIdentity) || entry.repositoryIds.includes(repoId);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function activeEntries(controllerHome: string, repoId: string, repoRoot: string): BrowserSessionAuthorityEntry[] {
|
|
215
|
+
ensureLegacyBrowserSessionsImported(controllerHome, repoId, repoRoot);
|
|
216
|
+
return authorityEntries(controllerHome)
|
|
217
|
+
.filter((entry) => entry.status === 'active' && visibleToRepository(entry, repoId))
|
|
218
|
+
.sort((left, right) => {
|
|
219
|
+
const updated = right.session.updatedAt.localeCompare(left.session.updatedAt);
|
|
220
|
+
return updated !== 0 ? updated : left.session.sessionId.localeCompare(right.session.sessionId);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function saveBrowserSession<T extends BrowserSessionAuthoritySession>(
|
|
225
|
+
controllerHome: string,
|
|
226
|
+
repoId: string,
|
|
227
|
+
repoRoot: string,
|
|
228
|
+
session: T,
|
|
229
|
+
): T {
|
|
230
|
+
ensureLegacyBrowserSessionsImported(controllerHome, repoId, repoRoot);
|
|
231
|
+
const key = recordKey(repoId, session);
|
|
232
|
+
return withControlPlaneTransaction(controllerHome, (database) => {
|
|
233
|
+
const current = readControlPlaneRecordWithinTransaction<BrowserSessionAuthorityEntry<T>>(database, SESSION_NAMESPACE, SESSION_SCOPE, key);
|
|
234
|
+
const next = mergeEntry(current?.value, repoId, session);
|
|
235
|
+
const written = writeControlPlaneRecordWithinTransaction(database, {
|
|
236
|
+
namespace: SESSION_NAMESPACE,
|
|
237
|
+
scope: SESSION_SCOPE,
|
|
238
|
+
key,
|
|
239
|
+
schemaVersion: 1,
|
|
240
|
+
value: next,
|
|
241
|
+
expectedRevision: current?.revision ?? null,
|
|
242
|
+
action: current ? 'save' : 'create',
|
|
243
|
+
});
|
|
244
|
+
return written.value.session;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function findBrowserSession<T extends BrowserSessionAuthoritySession>(
|
|
249
|
+
controllerHome: string,
|
|
250
|
+
repoId: string,
|
|
251
|
+
repoRoot: string,
|
|
252
|
+
sessionId: string,
|
|
253
|
+
): T | undefined {
|
|
254
|
+
return activeEntries(controllerHome, repoId, repoRoot)
|
|
255
|
+
.find((entry) => entry.session.sessionId === sessionId || entry.aliases.includes(sessionId))?.session as T | undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function listBrowserSessions<T extends BrowserSessionAuthoritySession>(
|
|
259
|
+
controllerHome: string,
|
|
260
|
+
repoId: string,
|
|
261
|
+
repoRoot: string,
|
|
262
|
+
options: { limit?: number; cursor?: string } = {},
|
|
263
|
+
): BrowserSessionAuthorityPage<T> {
|
|
264
|
+
const entries = activeEntries(controllerHome, repoId, repoRoot);
|
|
265
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? DEFAULT_BROWSER_SESSION_LIST_LIMIT), MAX_BROWSER_SESSION_LIST_LIMIT));
|
|
266
|
+
let offset = 0;
|
|
267
|
+
if (options.cursor) {
|
|
268
|
+
try {
|
|
269
|
+
const parsed = JSON.parse(Buffer.from(options.cursor, 'base64url').toString('utf8')) as { offset?: unknown };
|
|
270
|
+
if (!Number.isInteger(parsed.offset) || Number(parsed.offset) < 0) throw new Error('invalid offset');
|
|
271
|
+
offset = Number(parsed.offset);
|
|
272
|
+
} catch {
|
|
273
|
+
throw new Error('PLUGIN_BROWSER_SESSION_CURSOR_INVALID: cursor is invalid or expired');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const page = entries.slice(offset, offset + limit);
|
|
277
|
+
const nextOffset = offset + page.length;
|
|
278
|
+
const nextCursor = nextOffset < entries.length
|
|
279
|
+
? Buffer.from(JSON.stringify({ offset: nextOffset }), 'utf8').toString('base64url')
|
|
280
|
+
: undefined;
|
|
281
|
+
return {
|
|
282
|
+
sessions: page.map((entry) => entry.session as T),
|
|
283
|
+
limit,
|
|
284
|
+
totalCount: entries.length,
|
|
285
|
+
nextCursor,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function listAllBrowserSessionsForRepository<T extends BrowserSessionAuthoritySession>(
|
|
290
|
+
controllerHome: string,
|
|
291
|
+
repoId: string,
|
|
292
|
+
repoRoot: string,
|
|
293
|
+
): T[] {
|
|
294
|
+
return activeEntries(controllerHome, repoId, repoRoot).map((entry) => entry.session as T);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function tombstoneBrowserSession(
|
|
298
|
+
controllerHome: string,
|
|
299
|
+
repoId: string,
|
|
300
|
+
repoRoot: string,
|
|
301
|
+
sessionId: string,
|
|
302
|
+
): boolean {
|
|
303
|
+
ensureLegacyBrowserSessionsImported(controllerHome, repoId, repoRoot);
|
|
304
|
+
const record = listControlPlaneRecords<BrowserSessionAuthorityEntry>(controllerHome, {
|
|
305
|
+
namespace: SESSION_NAMESPACE,
|
|
306
|
+
scope: SESSION_SCOPE,
|
|
307
|
+
limit: MAX_AUTHORITY_RECORDS,
|
|
308
|
+
}).find((candidate) => {
|
|
309
|
+
const entry = candidate.value;
|
|
310
|
+
return entry.status === 'active' && visibleToRepository(entry, repoId)
|
|
311
|
+
&& (entry.session.sessionId === sessionId || entry.aliases.includes(sessionId));
|
|
312
|
+
});
|
|
313
|
+
if (!record) return false;
|
|
314
|
+
return withControlPlaneTransaction(controllerHome, (database) => {
|
|
315
|
+
const current = readControlPlaneRecordWithinTransaction<BrowserSessionAuthorityEntry>(database, SESSION_NAMESPACE, SESSION_SCOPE, record.key);
|
|
316
|
+
if (!current || current.value.status !== 'active') return false;
|
|
317
|
+
writeControlPlaneRecordWithinTransaction(database, {
|
|
318
|
+
namespace: SESSION_NAMESPACE,
|
|
319
|
+
scope: SESSION_SCOPE,
|
|
320
|
+
key: record.key,
|
|
321
|
+
schemaVersion: 1,
|
|
322
|
+
value: {
|
|
323
|
+
...current.value,
|
|
324
|
+
status: 'tombstoned',
|
|
325
|
+
tombstonedAt: new Date().toISOString(),
|
|
326
|
+
},
|
|
327
|
+
expectedRevision: current.revision,
|
|
328
|
+
action: 'tombstone',
|
|
329
|
+
});
|
|
330
|
+
return true;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, rmSync } from 'fs';
|
|
2
|
+
import { basename, join } from 'path';
|
|
3
|
+
import { writeJsonAtomic } from '../shared/json-files';
|
|
4
|
+
import { AssistantPluginError } from './errors';
|
|
5
|
+
import {
|
|
6
|
+
currentBrowserSessionAuthorityContext,
|
|
7
|
+
findBrowserSession as findBrowserSessionAuthority,
|
|
8
|
+
listAllBrowserSessionsForRepository,
|
|
9
|
+
saveBrowserSession as saveBrowserSessionAuthority,
|
|
10
|
+
tombstoneBrowserSession,
|
|
11
|
+
} from './browser-session-authority';
|
|
12
|
+
|
|
13
|
+
const BROWSER_STATE_ROOT = '.forge/browser';
|
|
14
|
+
|
|
15
|
+
import type { BrowserSessionState } from './browser-session-types';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Repository-local browser paths remain compatibility storage only. During
|
|
19
|
+
* Runtime/plugin execution, BrowserSessionAuthority (SQLite) is the sole writer;
|
|
20
|
+
* the JSON files are read/written only when no Controller authority context is
|
|
21
|
+
* present (for legacy migration/tests/standalone helpers).
|
|
22
|
+
*/
|
|
23
|
+
export function browserStateDir(
|
|
24
|
+
repoRoot: string,
|
|
25
|
+
name: 'sessions' | 'screenshots' | 'profiles' | 'downloads' | 'diagnostics',
|
|
26
|
+
): string {
|
|
27
|
+
return join(repoRoot, BROWSER_STATE_ROOT, name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sessionPath(repoRoot: string, sessionId: string): string {
|
|
31
|
+
return join(browserStateDir(repoRoot, 'sessions'), `${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readLegacyBrowserSessionJson(path: string): BrowserSessionState | undefined {
|
|
35
|
+
let raw: string;
|
|
36
|
+
try {
|
|
37
|
+
raw = readFileSync(path, 'utf-8');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return undefined;
|
|
40
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_READ_FAILED', 'Saved browser session metadata could not be read.', {
|
|
41
|
+
retryable: true,
|
|
42
|
+
details: { fileName: basename(path), cause: error instanceof Error ? error.message : String(error) },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(raw) as BrowserSessionState;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_CORRUPT', 'Saved browser session metadata is malformed; refusing to treat corrupt state as a missing session.', {
|
|
49
|
+
retryable: false,
|
|
50
|
+
details: { fileName: basename(path), cause: error instanceof Error ? error.message : String(error) },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function saveBrowserSession(repoRoot: string, session: BrowserSessionState): BrowserSessionState {
|
|
56
|
+
const authority = currentBrowserSessionAuthorityContext();
|
|
57
|
+
if (!authority) {
|
|
58
|
+
writeJsonAtomic(sessionPath(repoRoot, session.sessionId), session);
|
|
59
|
+
return session;
|
|
60
|
+
}
|
|
61
|
+
const saved = saveBrowserSessionAuthority<BrowserSessionState>(authority.controllerHome, authority.repoId, repoRoot, session);
|
|
62
|
+
if (saved.sessionId === session.sessionId || !saved.browser?.sessionResume) return saved;
|
|
63
|
+
return {
|
|
64
|
+
...saved,
|
|
65
|
+
browser: {
|
|
66
|
+
...saved.browser,
|
|
67
|
+
sessionResume: { ...saved.browser.sessionResume, sessionId: saved.sessionId },
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function findBrowserSession(repoRoot: string, sessionId?: string): BrowserSessionState | undefined {
|
|
73
|
+
if (!sessionId) return undefined;
|
|
74
|
+
const authority = currentBrowserSessionAuthorityContext();
|
|
75
|
+
return authority
|
|
76
|
+
? findBrowserSessionAuthority<BrowserSessionState>(authority.controllerHome, authority.repoId, repoRoot, sessionId)
|
|
77
|
+
: readLegacyBrowserSessionJson(sessionPath(repoRoot, sessionId));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function listSavedBrowserSessions(repoRoot: string): BrowserSessionState[] {
|
|
81
|
+
const authority = currentBrowserSessionAuthorityContext();
|
|
82
|
+
if (authority) {
|
|
83
|
+
return listAllBrowserSessionsForRepository<BrowserSessionState>(authority.controllerHome, authority.repoId, repoRoot);
|
|
84
|
+
}
|
|
85
|
+
const root = browserStateDir(repoRoot, 'sessions');
|
|
86
|
+
let names: string[];
|
|
87
|
+
try {
|
|
88
|
+
names = readdirSync(root);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return [];
|
|
91
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_READ_FAILED', 'Saved browser session directory could not be read.', {
|
|
92
|
+
retryable: true,
|
|
93
|
+
details: { cause: error instanceof Error ? error.message : String(error) },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return names
|
|
97
|
+
.filter((name) => name.endsWith('.json'))
|
|
98
|
+
.map((name) => readLegacyBrowserSessionJson(join(root, name)))
|
|
99
|
+
.filter((session): session is BrowserSessionState => Boolean(session));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function removeBrowserSession(repoRoot: string, sessionId: string): void {
|
|
103
|
+
const authority = currentBrowserSessionAuthorityContext();
|
|
104
|
+
if (authority) {
|
|
105
|
+
tombstoneBrowserSession(authority.controllerHome, authority.repoId, repoRoot, sessionId);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
rmSync(sessionPath(repoRoot, sessionId), { force: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function loadBrowserSession(repoRoot: string, sessionId?: string): BrowserSessionState | undefined {
|
|
112
|
+
return sessionId ? findBrowserSession(repoRoot, sessionId) : undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function requireBrowserSession(repoRoot: string, sessionId: string): BrowserSessionState {
|
|
116
|
+
const session = findBrowserSession(repoRoot, sessionId);
|
|
117
|
+
if (!session) {
|
|
118
|
+
throw new AssistantPluginError('PLUGIN_SESSION_NOT_FOUND', `Browser session not found: ${sessionId}`, { retryable: false });
|
|
119
|
+
}
|
|
120
|
+
return session;
|
|
121
|
+
}
|