@wjarka/cezarion 0.15.1 → 0.16.0
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/README.md +167 -46
- package/dist/agent-config/catalog.d.ts +6 -2
- package/dist/agent-config/catalog.js +78 -3
- package/dist/agent-config/catalog.js.map +1 -1
- package/dist/agent-config/model-settings/shared.js +6 -3
- package/dist/agent-config/model-settings/shared.js.map +1 -1
- package/dist/agent-config/models.js +4 -0
- package/dist/agent-config/models.js.map +1 -1
- package/dist/agent-config/validate.js +6 -0
- package/dist/agent-config/validate.js.map +1 -1
- package/dist/artifacts/cli.js +10 -2
- package/dist/artifacts/cli.js.map +1 -1
- package/dist/automations/coordinator.d.ts +7 -0
- package/dist/automations/coordinator.js +22 -2
- package/dist/automations/coordinator.js.map +1 -1
- package/dist/automations/github-poller.d.ts +4 -4
- package/dist/automations/github-poller.js.map +1 -1
- package/dist/automations/schedule-runner.d.ts +130 -0
- package/dist/automations/schedule-runner.js +326 -0
- package/dist/automations/schedule-runner.js.map +1 -0
- package/dist/automations/scheduler.d.ts +38 -7
- package/dist/automations/scheduler.js +90 -22
- package/dist/automations/scheduler.js.map +1 -1
- package/dist/automations/store.d.ts +80 -3
- package/dist/automations/store.js +208 -16
- package/dist/automations/store.js.map +1 -1
- package/dist/automations/task-template.d.ts +42 -6
- package/dist/automations/task-template.js +152 -24
- package/dist/automations/task-template.js.map +1 -1
- package/dist/automations/types.d.ts +142 -6
- package/dist/automations/types.js +64 -6
- package/dist/automations/types.js.map +1 -1
- package/dist/autosave-holders.js +8 -12
- package/dist/autosave-holders.js.map +1 -1
- package/dist/ci-wait/client.d.ts +8 -1
- package/dist/ci-wait/client.js +43 -1
- package/dist/ci-wait/client.js.map +1 -1
- package/dist/ci-wait/controller.d.ts +83 -4
- package/dist/ci-wait/controller.js +76 -11
- package/dist/ci-wait/controller.js.map +1 -1
- package/dist/ci-wait/errors.d.ts +7 -1
- package/dist/ci-wait/errors.js +36 -0
- package/dist/ci-wait/errors.js.map +1 -1
- package/dist/ci-wait/github.d.ts +2 -0
- package/dist/ci-wait/github.js +3 -1
- package/dist/ci-wait/github.js.map +1 -1
- package/dist/ci-wait/mcp.d.ts +2 -98
- package/dist/ci-wait/mcp.js +11 -25
- package/dist/ci-wait/mcp.js.map +1 -1
- package/dist/ci-wait/supervisor.d.ts +2 -1
- package/dist/ci-wait/supervisor.js +2 -1
- package/dist/ci-wait/supervisor.js.map +1 -1
- package/dist/ci-wait/tools.d.ts +307 -0
- package/dist/ci-wait/tools.js +63 -0
- package/dist/ci-wait/tools.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/contract/agent-config.d.ts +5 -0
- package/dist/contract/agent-profiles.d.ts +15 -0
- package/dist/contract/automation-schedule.d.ts +60 -0
- package/dist/contract/automations.d.ts +208 -36
- package/dist/contract/delegation.d.ts +96 -0
- package/dist/contract/discovery.d.ts +7 -0
- package/dist/contract/github.d.ts +1 -0
- package/dist/contract/health.d.ts +6 -0
- package/dist/contract/index.d.ts +3 -0
- package/dist/contract/index.js +1445 -745
- package/dist/contract/preview.d.ts +508 -0
- package/dist/contract/runs.d.ts +716 -15
- package/dist/contract/skills.d.ts +39 -0
- package/dist/contract/workflows.d.ts +6 -0
- package/dist/contract/workspace.d.ts +65 -0
- package/dist/contract/zoned-time.d.ts +35 -0
- package/dist/core/agent-env.d.ts +11 -0
- package/dist/core/agent-env.js +27 -5
- package/dist/core/agent-env.js.map +1 -1
- package/dist/core/agent-profiles.js +7 -0
- package/dist/core/agent-profiles.js.map +1 -1
- package/dist/core/agent-runner.d.ts +49 -1
- package/dist/core/agent-runner.js +2 -1
- package/dist/core/agent-runner.js.map +1 -1
- package/dist/core/backend-detect.d.ts +3 -2
- package/dist/core/backend-detect.js +27 -1
- package/dist/core/backend-detect.js.map +1 -1
- package/dist/core/claude-cli-runner.d.ts +1 -0
- package/dist/core/claude-cli-runner.js +69 -19
- package/dist/core/claude-cli-runner.js.map +1 -1
- package/dist/core/codex-app-server-runner.d.ts +1 -0
- package/dist/core/codex-app-server-runner.js +200 -25
- package/dist/core/codex-app-server-runner.js.map +1 -1
- package/dist/core/codex-app-server-transport.d.ts +7 -3
- package/dist/core/codex-app-server-transport.js +11 -9
- package/dist/core/codex-app-server-transport.js.map +1 -1
- package/dist/core/cursor-acp-runner.d.ts +1 -0
- package/dist/core/cursor-acp-runner.js +24 -9
- package/dist/core/cursor-acp-runner.js.map +1 -1
- package/dist/core/host-model-catalog.js +2 -0
- package/dist/core/host-model-catalog.js.map +1 -1
- package/dist/core/managed-session.js +2 -0
- package/dist/core/managed-session.js.map +1 -1
- package/dist/core/model-identity.js +2 -0
- package/dist/core/model-identity.js.map +1 -1
- package/dist/core/model-presets.js +3 -0
- package/dist/core/model-presets.js.map +1 -1
- package/dist/core/omp-model-catalog.d.ts +43 -0
- package/dist/core/omp-model-catalog.js +205 -0
- package/dist/core/omp-model-catalog.js.map +1 -0
- package/dist/core/omp-runner.d.ts +73 -0
- package/dist/core/omp-runner.js +990 -0
- package/dist/core/omp-runner.js.map +1 -0
- package/dist/core/omp-ui-mapper.d.ts +106 -0
- package/dist/core/omp-ui-mapper.js +882 -0
- package/dist/core/omp-ui-mapper.js.map +1 -0
- package/dist/core/opencode-server-runner.d.ts +18 -4
- package/dist/core/opencode-server-runner.js +337 -96
- package/dist/core/opencode-server-runner.js.map +1 -1
- package/dist/core/opencode-session-error.d.ts +11 -0
- package/dist/core/opencode-session-error.js +26 -0
- package/dist/core/opencode-session-error.js.map +1 -0
- package/dist/core/opencode-ui-mapper.js +4 -36
- package/dist/core/opencode-ui-mapper.js.map +1 -1
- package/dist/core/pi-runner.d.ts +1 -0
- package/dist/core/pi-runner.js +75 -12
- package/dist/core/pi-runner.js.map +1 -1
- package/dist/core/provider-auth.d.ts +9 -2
- package/dist/core/provider-auth.js +43 -6
- package/dist/core/provider-auth.js.map +1 -1
- package/dist/core/runner-factory.js +3 -0
- package/dist/core/runner-factory.js.map +1 -1
- package/dist/core/runner-model-catalog.js +1 -0
- package/dist/core/runner-model-catalog.js.map +1 -1
- package/dist/core/runner-stderr.d.ts +2 -0
- package/dist/core/runner-stderr.js +20 -0
- package/dist/core/runner-stderr.js.map +1 -0
- package/dist/core/session-process.d.ts +22 -0
- package/dist/core/session-process.js +202 -0
- package/dist/core/session-process.js.map +1 -0
- package/dist/core/ui-events.d.ts +1 -1
- package/dist/core/usage-limit.js +3 -60
- package/dist/core/usage-limit.js.map +1 -1
- package/dist/data-gitignore.d.ts +6 -0
- package/dist/data-gitignore.js +57 -0
- package/dist/data-gitignore.js.map +1 -0
- package/dist/delegation/capacity.d.ts +19 -0
- package/dist/delegation/capacity.js +34 -0
- package/dist/delegation/capacity.js.map +1 -0
- package/dist/delegation/cli.js +13 -4
- package/dist/delegation/cli.js.map +1 -1
- package/dist/delegation/conversations.d.ts +4 -2
- package/dist/delegation/conversations.js +8 -12
- package/dist/delegation/conversations.js.map +1 -1
- package/dist/delegation/credentials.d.ts +2 -0
- package/dist/delegation/credentials.js +10 -0
- package/dist/delegation/credentials.js.map +1 -1
- package/dist/delegation/destroy-observation.d.ts +42 -0
- package/dist/delegation/destroy-observation.js +115 -0
- package/dist/delegation/destroy-observation.js.map +1 -0
- package/dist/delegation/execution-identity.d.ts +6 -0
- package/dist/delegation/execution-identity.js +1 -1
- package/dist/delegation/execution-identity.js.map +1 -1
- package/dist/delegation/policy.d.ts +3 -2
- package/dist/delegation/policy.js +7 -5
- package/dist/delegation/policy.js.map +1 -1
- package/dist/delegation/process-liveness.d.ts +57 -20
- package/dist/delegation/process-liveness.js +114 -92
- package/dist/delegation/process-liveness.js.map +1 -1
- package/dist/delegation/provision.js +6 -6
- package/dist/delegation/provision.js.map +1 -1
- package/dist/delegation/results.js +4 -2
- package/dist/delegation/results.js.map +1 -1
- package/dist/delegation/retry-backoff.d.ts +23 -0
- package/dist/delegation/retry-backoff.js +18 -0
- package/dist/delegation/retry-backoff.js.map +1 -0
- package/dist/delegation/routes.d.ts +64 -58
- package/dist/delegation/scratch-cleanup.d.ts +29 -0
- package/dist/delegation/scratch-cleanup.js +143 -0
- package/dist/delegation/scratch-cleanup.js.map +1 -0
- package/dist/delegation/service.d.ts +20 -1
- package/dist/delegation/service.js +233 -70
- package/dist/delegation/service.js.map +1 -1
- package/dist/delegation/transport.d.ts +12 -6
- package/dist/delegation/workspace.d.ts +23 -4
- package/dist/delegation/workspace.js +147 -31
- package/dist/delegation/workspace.js.map +1 -1
- package/dist/git-worktree-lock-helper.js +8 -3
- package/dist/git-worktree-lock-helper.js.map +1 -1
- package/dist/git-worktree-lock.d.ts +6 -1
- package/dist/git-worktree-lock.js +10 -4
- package/dist/git-worktree-lock.js.map +1 -1
- package/dist/git-worktree-release.d.ts +20 -0
- package/dist/git-worktree-release.js +27 -0
- package/dist/git-worktree-release.js.map +1 -0
- package/dist/git-worktree.d.ts +8 -2
- package/dist/git-worktree.js +19 -2
- package/dist/git-worktree.js.map +1 -1
- package/dist/handoff.d.ts +2 -2
- package/dist/handoff.js +1 -1
- package/dist/index.js +109 -76
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +1 -1
- package/dist/paths.js +5 -1
- package/dist/paths.js.map +1 -1
- package/dist/preview/cdp.d.ts +13 -0
- package/dist/preview/cdp.js +69 -0
- package/dist/preview/cdp.js.map +1 -0
- package/dist/preview/chromium.d.ts +75 -0
- package/dist/preview/chromium.js +394 -0
- package/dist/preview/chromium.js.map +1 -0
- package/dist/preview/dev-server.d.ts +68 -0
- package/dist/preview/dev-server.js +341 -0
- package/dist/preview/dev-server.js.map +1 -0
- package/dist/preview/host.d.ts +154 -0
- package/dist/preview/host.js +681 -0
- package/dist/preview/host.js.map +1 -0
- package/dist/preview/registration.d.ts +63 -0
- package/dist/preview/registration.js +109 -0
- package/dist/preview/registration.js.map +1 -0
- package/dist/preview/session.d.ts +73 -0
- package/dist/preview/session.js +347 -0
- package/dist/preview/session.js.map +1 -0
- package/dist/preview/url.d.ts +6 -0
- package/dist/preview/url.js +17 -0
- package/dist/preview/url.js.map +1 -0
- package/dist/runs/agent-tmpdir-removal.d.ts +15 -0
- package/dist/runs/agent-tmpdir-removal.js +60 -0
- package/dist/runs/agent-tmpdir-removal.js.map +1 -0
- package/dist/runs/agent-tmpdir.d.ts +12 -1
- package/dist/runs/agent-tmpdir.js +123 -34
- package/dist/runs/agent-tmpdir.js.map +1 -1
- package/dist/runs/delegation-state.d.ts +5 -1
- package/dist/runs/delegation-state.js +9 -0
- package/dist/runs/delegation-state.js.map +1 -1
- package/dist/runs/event-history.d.ts +4 -4
- package/dist/runs/event-history.js +97 -117
- package/dist/runs/event-history.js.map +1 -1
- package/dist/runs/history-compressor.d.ts +29 -0
- package/dist/runs/history-compressor.js +116 -0
- package/dist/runs/history-compressor.js.map +1 -0
- package/dist/runs/history-file.d.ts +36 -0
- package/dist/runs/history-file.js +463 -0
- package/dist/runs/history-file.js.map +1 -0
- package/dist/runs/human-ask-summary.js +18 -13
- package/dist/runs/human-ask-summary.js.map +1 -1
- package/dist/runs/legacy-index.d.ts +62 -0
- package/dist/runs/legacy-index.js +168 -0
- package/dist/runs/legacy-index.js.map +1 -0
- package/dist/runs/raw-record.d.ts +52 -0
- package/dist/runs/raw-record.js +145 -0
- package/dist/runs/raw-record.js.map +1 -0
- package/dist/runs/retention.d.ts +13 -6
- package/dist/runs/retention.js +42 -13
- package/dist/runs/retention.js.map +1 -1
- package/dist/runs/run-claims.d.ts +23 -0
- package/dist/runs/run-claims.js +24 -0
- package/dist/runs/run-claims.js.map +1 -0
- package/dist/runs/run-database.d.ts +385 -0
- package/dist/runs/run-database.js +930 -0
- package/dist/runs/run-database.js.map +1 -0
- package/dist/runs/run-index.d.ts +35 -9
- package/dist/runs/run-index.js +200 -25
- package/dist/runs/run-index.js.map +1 -1
- package/dist/runs/run-row.d.ts +44 -0
- package/dist/runs/run-row.js +84 -0
- package/dist/runs/run-row.js.map +1 -0
- package/dist/runs/run-search.d.ts +29 -0
- package/dist/runs/run-search.js +63 -0
- package/dist/runs/run-search.js.map +1 -0
- package/dist/runs/store-open-error.d.ts +24 -0
- package/dist/runs/store-open-error.js +61 -0
- package/dist/runs/store-open-error.js.map +1 -0
- package/dist/runs/store.d.ts +667 -32
- package/dist/runs/store.js +2153 -324
- package/dist/runs/store.js.map +1 -1
- package/dist/runs/transcript-facts.d.ts +77 -0
- package/dist/runs/transcript-facts.js +302 -0
- package/dist/runs/transcript-facts.js.map +1 -0
- package/dist/runs/webhook.js +5 -4
- package/dist/runs/webhook.js.map +1 -1
- package/dist/runs/worker-execution.d.ts +22 -0
- package/dist/runs/worker-execution.js +30 -0
- package/dist/runs/worker-execution.js.map +1 -0
- package/dist/server/capabilities.d.ts +1 -0
- package/dist/server/capabilities.js +2 -0
- package/dist/server/capabilities.js.map +1 -1
- package/dist/server/cockpit-ownership.d.ts +8 -0
- package/dist/server/cockpit-ownership.js +19 -0
- package/dist/server/cockpit-ownership.js.map +1 -1
- package/dist/server/forge/github.d.ts +1 -4
- package/dist/server/forge/github.js +47 -23
- package/dist/server/forge/github.js.map +1 -1
- package/dist/server/git.js +1 -1
- package/dist/server/git.js.map +1 -1
- package/dist/server/open-in-app.js +1 -0
- package/dist/server/open-in-app.js.map +1 -1
- package/dist/server/preview-socket.d.ts +36 -0
- package/dist/server/preview-socket.js +126 -0
- package/dist/server/preview-socket.js.map +1 -0
- package/dist/server/project-context.d.ts +31 -4
- package/dist/server/project-context.js +73 -9
- package/dist/server/project-context.js.map +1 -1
- package/dist/server/provider-action-gate.js +1 -0
- package/dist/server/provider-action-gate.js.map +1 -1
- package/dist/server/repo-branches.d.ts +4 -3
- package/dist/server/repo-branches.js +5 -4
- package/dist/server/repo-branches.js.map +1 -1
- package/dist/server/server.d.ts +4500 -2727
- package/dist/server/server.js +651 -202
- package/dist/server/server.js.map +1 -1
- package/dist/server/upgrade-router.d.ts +23 -0
- package/dist/server/upgrade-router.js +53 -0
- package/dist/server/upgrade-router.js.map +1 -0
- package/dist/server/validators.d.ts +6 -1
- package/dist/server/validators.js +4 -4
- package/dist/server/validators.js.map +1 -1
- package/dist/server/ws.d.ts +16 -16
- package/dist/server/ws.js +56 -54
- package/dist/server/ws.js.map +1 -1
- package/dist/skills-remote.d.ts +14 -0
- package/dist/skills-remote.js +91 -15
- package/dist/skills-remote.js.map +1 -1
- package/dist/task-cli/cli.js +40 -16
- package/dist/task-cli/cli.js.map +1 -1
- package/dist/task-cli/discovery.d.ts +4 -3
- package/dist/task-cli/discovery.js +4 -3
- package/dist/task-cli/discovery.js.map +1 -1
- package/dist/task-cli/http.d.ts +2 -0
- package/dist/task-cli/http.js +10 -4
- package/dist/task-cli/http.js.map +1 -1
- package/dist/task-cli/projections.d.ts +5 -2
- package/dist/task-cli/projections.js +13 -0
- package/dist/task-cli/projections.js.map +1 -1
- package/dist/task-cli/watch.d.ts +15 -2
- package/dist/task-cli/watch.js +31 -9
- package/dist/task-cli/watch.js.map +1 -1
- package/dist/ui-state.d.ts +4 -3
- package/dist/ui-state.js +8 -3
- package/dist/ui-state.js.map +1 -1
- package/dist/workflows/grouped-command.d.ts +31 -0
- package/dist/workflows/grouped-command.js +143 -0
- package/dist/workflows/grouped-command.js.map +1 -0
- package/dist/workflows/run.d.ts +223 -42
- package/dist/workflows/run.js +1316 -398
- package/dist/workflows/run.js.map +1 -1
- package/dist/workflows/types.d.ts +12 -0
- package/dist/workflows/types.js +20 -0
- package/dist/workflows/types.js.map +1 -1
- package/dist/workspace/agent-accounts.d.ts +5 -1
- package/dist/workspace/agent-accounts.js +1 -0
- package/dist/workspace/agent-accounts.js.map +1 -1
- package/dist/workspace/agent-profiles.js +1 -1
- package/dist/workspace/agent-profiles.js.map +1 -1
- package/dist/workspace/config.d.ts +3 -1
- package/dist/workspace/config.js +1 -0
- package/dist/workspace/config.js.map +1 -1
- package/dist/worktree-setup.d.ts +85 -0
- package/dist/worktree-setup.js +164 -0
- package/dist/worktree-setup.js.map +1 -0
- package/package.json +7 -5
- package/scripts/benchmark-run-store-fixture.ts +428 -0
- package/scripts/benchmark-run-store.ts +743 -0
- package/scripts/benchmark-stuck-destroys.ts +143 -0
- package/scripts/mock-autonomous.mjs +11 -0
- package/scripts/mock-ci-tool.mjs +20 -5
- package/scripts/mock-claude.mjs +110 -1
- package/scripts/mock-codex-app-server.mjs +117 -2
- package/scripts/mock-codex-coalesce-steer.mjs +59 -0
- package/scripts/mock-codex-reverse-steer.mjs +130 -0
- package/scripts/mock-cursor-acp.mjs +75 -1
- package/scripts/mock-omp-rpc.mjs +605 -0
- package/scripts/mock-opencode-serve.mjs +312 -22
- package/scripts/mock-pi-rpc.mjs +109 -3
- package/scripts/mock-runner-crash.mjs +42 -0
- package/scripts/mock-silent-tail.mjs +78 -0
- package/scripts/mock-turn-messages.mjs +5 -0
- package/scripts/omp-ci-wait.mjs +29 -0
- package/scripts/omp-restrict-delegation.yml +4 -0
- package/scripts/pi-ci-wait.mjs +12 -9
- package/scripts/seed-archive-profile.ts +3 -3
- package/web/dist/assets/agent-accounts-CFOBMVHc.js +1 -0
- package/web/dist/assets/alert-dialog-BrpNUwMt.js +1 -0
- package/web/dist/assets/app-window-CSHwDX9N.js +1 -0
- package/web/dist/assets/button-C_rd7CPp.js +43 -0
- package/web/dist/assets/compare-variants-D6WXhvXA.js +1 -0
- package/web/dist/assets/diff-DrEARXTV.js +3 -0
- package/web/dist/assets/diff-controls-B1-j7nFg.js +1 -0
- package/web/dist/assets/{diff-view-DPn6bQ5W.js → diff-view-DvGLjdTk.js} +3 -3
- package/web/dist/assets/dropdown-menu-BegOZFrj.js +1 -0
- package/web/dist/assets/format-DpNe3S0A.js +1 -0
- package/web/dist/assets/git-toolbar-Cw4ljmhB.js +1 -0
- package/web/dist/assets/github-CfV5sqL0.js +1 -0
- package/web/dist/assets/{image-preview-DJ8cgb1Z.js → image-preview-FOEfqpZP.js} +1 -1
- package/web/dist/assets/index-DgWWlOLy.js +13 -0
- package/web/dist/assets/index-fBFnuvxu.css +1 -0
- package/web/dist/assets/{nav-row-styles-3j64kH5s.js → nav-row-styles-vp2JbhuJ.js} +1 -1
- package/web/dist/assets/play-CmVAj_fu.js +1 -0
- package/web/dist/assets/popover-BohP6WTx.js +1 -0
- package/web/dist/assets/preview-pane-connected-qTA5KNoJ.js +3 -0
- package/web/dist/assets/preview-state-vuMviq6q.js +1 -0
- package/web/dist/assets/prompt-templates-Cubdut_C.js +15 -0
- package/web/dist/assets/repo-git-Cf4t5PD0.js +1 -0
- package/web/dist/assets/{run-diff-5-RRdcVz.js → run-diff-BpkUW37Z.js} +3 -3
- package/web/dist/assets/run-header-68WHf9wV.js +1 -0
- package/web/dist/assets/skills-DiVIgKeU.js +1 -0
- package/web/dist/assets/square-terminal-B6RYYieR.js +1 -0
- package/web/dist/assets/status-dot-CqTJHSP9.js +1 -0
- package/web/dist/assets/{tab-link-K0hjzEXR.js → tab-link-B07Hu4QX.js} +1 -1
- package/web/dist/assets/task-changes--lQF6EYf.js +1 -0
- package/web/dist/assets/task-commits-Cx0qy4DN.js +1 -0
- package/web/dist/assets/task-files-D50s-gKW.js +2 -0
- package/web/dist/assets/task-github-item-DaqmSwYT.js +1 -0
- package/web/dist/assets/task-thread-BPQ64lui.js +7 -0
- package/web/dist/assets/{task-thread-Bw_nQuTx.css → task-thread-zNE2Q9E-.css} +1 -1
- package/web/dist/assets/textarea-D20O4Pw0.js +1 -0
- package/web/dist/assets/thread-state-DWcFMrvb.js +4 -0
- package/web/dist/assets/use-hand-to-agent-state-BLIGmwbd.css +1 -0
- package/web/dist/assets/use-hand-to-agent-state-Cy9eyyfL.js +1 -0
- package/web/dist/assets/{utils-fzNfbPzA.js → utils-JsL3ZsJf.js} +1 -1
- package/web/dist/assets/workflows-jwG7uxqi.js +11 -0
- package/web/dist/assets/{zoomable-image-CDmZ_Rtl.js → zoomable-image-BJJ64KGc.js} +1 -1
- package/web/dist/cezarion-lockup-dark.svg +1 -13
- package/web/dist/cezarion-lockup-light.svg +1 -13
- package/web/dist/cezarion-mark-dark.svg +1 -12
- package/web/dist/cezarion-mark-light.svg +1 -12
- package/web/dist/index.html +16 -21
- package/web/dist/assets/agent-accounts-CU2ZDv24.js +0 -1
- package/web/dist/assets/alert-dialog-C-xYE_dg.js +0 -43
- package/web/dist/assets/chevron-right-BENZ-1ZN.js +0 -1
- package/web/dist/assets/collapsible-DD9J91Iv.js +0 -1
- package/web/dist/assets/compare-variants-5FmqpFSN.js +0 -1
- package/web/dist/assets/diff-controls-Bj0YrioE.js +0 -1
- package/web/dist/assets/diff-rE8rm75J2.js +0 -3
- package/web/dist/assets/diff-stat-BKDCSVfJ.js +0 -1
- package/web/dist/assets/dropdown-menu-Ch5BYQh_.js +0 -1
- package/web/dist/assets/format-dBiNDNVe.js +0 -1
- package/web/dist/assets/git-toolbar-ClVvf5iF.js +0 -1
- package/web/dist/assets/github-BkNOyour.css +0 -1
- package/web/dist/assets/github-DeZMeMwJ.js +0 -1
- package/web/dist/assets/github-item-detail-W5F6GVjF.js +0 -1
- package/web/dist/assets/github-sidebar-model-DcMu-IK2.js +0 -1
- package/web/dist/assets/highlighter-CdHqIwFr.js +0 -3
- package/web/dist/assets/index-CatemBEL.js +0 -8
- package/web/dist/assets/index-MvfD8bZJ.css +0 -1
- package/web/dist/assets/markdown-zdKeLIYA.js +0 -2
- package/web/dist/assets/mobile-run-bar-BCfmFi8V.js +0 -1
- package/web/dist/assets/prompt-templates-CBPp39IU.js +0 -15
- package/web/dist/assets/reference-chip-D6owncJT.js +0 -1
- package/web/dist/assets/repo-git-CtdkZFey.js +0 -1
- package/web/dist/assets/run-header-BcjoDi0W.js +0 -1
- package/web/dist/assets/sheet-ChnYVrp1.js +0 -1
- package/web/dist/assets/skills-C8YKp2vr.js +0 -1
- package/web/dist/assets/task-changes-BW8YdWG8.js +0 -1
- package/web/dist/assets/task-commits-q2kB2y6L.js +0 -1
- package/web/dist/assets/task-files-pPL6_3Cr.js +0 -2
- package/web/dist/assets/task-github-item-BXF5R52U.js +0 -1
- package/web/dist/assets/task-thread-DtkFL3EC.js +0 -10
- package/web/dist/assets/textarea-DxlkE-Yd.js +0 -1
- package/web/dist/assets/token-metrics-DIAjrPlM.js +0 -1
- package/web/dist/assets/use-desktop-CrDny2uG.js +0 -1
- package/web/dist/assets/use-submit-shortcut-KWJsC906.js +0 -1
- package/web/dist/assets/workflows-TmcPEj2K.js +0 -11
- /package/web/dist/assets/{lib-BQXq3kEf2.js → lib-BQXq3kEf.js} +0 -0
package/dist/server/server.js
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
|
+
import { sidebarLimitsSchema } from '../contract/index.js';
|
|
2
|
+
import { worktreeSetupConfigSchema } from '../contract/index.js';
|
|
3
|
+
import { ARCHIVED_RUNS_PAGE_DEFAULT, RUNS_SEARCH_PER_PROJECT_MAX, archivedRunsQuerySchema, runSummariesQuerySchema, runsSearchQuerySchema } from '../contract/index.js';
|
|
4
|
+
import { automationKindSchema, automationScheduleSchema, localTimeZone, nextOccurrence } from '../contract/index.js';
|
|
1
5
|
import { DelegationService } from '../delegation/service.js';
|
|
6
|
+
import { workerCapacity } from '../delegation/capacity.js';
|
|
2
7
|
import { delegationFailure } from '../delegation/routes.js';
|
|
3
|
-
import { githubItemParamsSchema, CLIENT_REQUEST_VARIANTS_ERROR, workerEmptyRequestSchema, runRelationshipsSchema
|
|
8
|
+
import { githubItemParamsSchema, CLIENT_REQUEST_VARIANTS_ERROR, workerEmptyRequestSchema, runRelationshipsSchema } from '../contract/index.js';
|
|
4
9
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
5
10
|
import { randomUUID } from 'node:crypto';
|
|
6
11
|
import { AutomationStore } from '../automations/store.js';
|
|
7
12
|
import { AutomationCoordinator } from '../automations/coordinator.js';
|
|
8
13
|
import { GithubPoller } from '../automations/github-poller.js';
|
|
9
14
|
import { ProjectAutomationScheduler, WorkspaceAutomationScheduler } from '../automations/scheduler.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
15
|
+
import { AutomationProjectUnavailableError, SCHEDULE_LEASE_HELD_REASON, ScheduleRunner } from '../automations/schedule-runner.js';
|
|
16
|
+
import { launchAutomationRun, launchScheduledRun, rebaselineIdleAutomations, reconcileAutomationReceipts, validateAutomationPrompt, } from '../automations/task-template.js';
|
|
17
|
+
import { automationEventSchema, automationFiltersSchema, automationLogResultSchema, automationTaskSchema, isGithubAutomation, isScheduleAutomation, } from '../automations/types.js';
|
|
12
18
|
import { access, constants as fsConstants, mkdir, readFile, realpath, stat, unlink, writeFile } from 'node:fs/promises';
|
|
13
19
|
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
14
20
|
import { fileURLToPath } from 'node:url';
|
|
15
21
|
import { Hono } from 'hono';
|
|
16
|
-
import { serve } from '@hono/node-server';
|
|
22
|
+
import { getRequestListener, serve } from '@hono/node-server';
|
|
17
23
|
import { bodyLimit } from 'hono/body-limit';
|
|
18
24
|
import { streamSSE } from 'hono/streaming';
|
|
25
|
+
import { matchedRoutes } from 'hono/route';
|
|
19
26
|
import { jsonZodValidator, paramZodValidator, queryZodValidator } from './validators.js';
|
|
20
27
|
import { deliverOnce, TaskWebhooks } from '../runs/webhook.js';
|
|
21
28
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
22
29
|
import { z } from 'zod';
|
|
23
|
-
import { repoPullInputSchema, deleteBranchesInputSchema, pinRunInputSchema, notifyRunInputSchema, githubSearchQuerySchema, githubProjectsQuerySchema, setWorkspaceUiStateInputSchema, } from '../contract/index.js';
|
|
30
|
+
import { repoPullInputSchema, deleteBranchesInputSchema, pinRunInputSchema, archiveFinishedRequestSchema, notifyRunInputSchema, githubSearchQuerySchema, githubProjectsQuerySchema, setWorkspaceUiStateInputSchema, } from '../contract/index.js';
|
|
24
31
|
// A contract VALUE, like `workspaceUiStateSchema` in workspace/migrations.ts — the request
|
|
25
32
|
// schema this route validates with is the same one the client compiles against.
|
|
26
33
|
import { MODEL_DISCOVERY_RUNNERS, attachmentInputSchema, attachmentFileParamSchema, modelDiscoveryRunnerSchema, openProjectInSchema, runnerDiscoversModels, updateProjectInputSchema, } from '../contract/index.js';
|
|
@@ -33,15 +40,18 @@ import { applyProviderEnablement } from '../core/provider-availability.js';
|
|
|
33
40
|
import { RunnerModelCatalog } from '../core/runner-model-catalog.js';
|
|
34
41
|
import { currentUsage, onUsage } from '../core/process-usage.js';
|
|
35
42
|
import { WORKFLOWS_DIR, loadWorkflows } from '../workflows/load.js';
|
|
36
|
-
import { QUICK_TASK_WORKFLOW, normalizeWorkflowDoc, skillStackOf, skillsToSteps, stepsIssue, workflowFileSchema, workflowStepSchema, } from '../workflows/types.js';
|
|
43
|
+
import { QUICK_TASK_WORKFLOW, normalizeWorkflowDoc, plannedWorkflow, skillStackOf, skillsToSteps, stepsIssue, workflowFileSchema, workflowStepSchema, } from '../workflows/types.js';
|
|
37
44
|
import { planChain, slugify } from '../planner.js';
|
|
38
45
|
import { discoverSkills } from '../skills.js';
|
|
39
46
|
import { SkillsUpdateConflictError, SkillsUpdateCoordinator, SkillsUpdateService } from '../skills-update.js';
|
|
40
47
|
import { getTeamSkillsCached, refreshTeamSkills, waitForTeamSkills } from '../skills-remote.js';
|
|
41
48
|
import { appendHandoffHeartbeat, handoffProgressExcerpt, readHandoff } from '../handoff.js';
|
|
42
49
|
import { markStarted, onTodosChanged, readTodos, removeTodo, todoTaskText } from '../todos.js';
|
|
50
|
+
import { ARCHIVED_WINDOW } from '../runs/store.js';
|
|
43
51
|
import { HistoryCursorError, deriveRunContextEvents, readEventsAfterLiveCursor, readRunHistoryPage, validateLiveCursor, } from '../runs/event-history.js';
|
|
44
|
-
import { readRunIndexFromDisk } from '../runs/run-index.js';
|
|
52
|
+
import { readRunIndexFromDisk, searchRunIndexFromDisk } from '../runs/run-index.js';
|
|
53
|
+
import { workerDestroyView } from '../runs/delegation-state.js';
|
|
54
|
+
import { searchTokens } from '../runs/run-search.js';
|
|
45
55
|
import { clientRequestHash } from '../runs/client-request.js';
|
|
46
56
|
import { ColdRepoHandles } from './cold-repo-handles.js';
|
|
47
57
|
import { isV2WireEventType } from '../runs/ui-event-sink.js';
|
|
@@ -51,13 +61,15 @@ import { artifactParamsSchema, fileLinkQuerySchema } from '../contract/index.js'
|
|
|
51
61
|
import { artifactDirectory, listArtifacts, readArtifact } from '../artifacts/store.js';
|
|
52
62
|
import { artifactPreview, loadFileLink, rasterMime } from '../artifacts/resolve.js';
|
|
53
63
|
import { isUntouchedCancelledRun, toPastedContent } from '../workflows/run.js';
|
|
54
|
-
import {
|
|
64
|
+
import { worktreeDiff, worktreeDiffStat, worktreeSizeBytes } from '../git-worktree.js';
|
|
65
|
+
import { releaseThenRemoveWorktree } from '../git-worktree-release.js';
|
|
55
66
|
import { isReclaimable, reclaimWorktree, reclaimWorktrees, rematerializeReclaimedWorktree, selectReclaimableWorktrees } from '../runs/retention.js';
|
|
56
67
|
import { getBranches, getCommit, getDiff, getLogWithParents, getRepoInfo, getStatus, getTracking } from './git.js';
|
|
57
68
|
import { attributeLog, deleteBranches, forgetRepoBranches, githubBranchForge, readRepoBranches } from './repo-branches.js';
|
|
58
69
|
import { claimRepoGitMutation, localPullBranches, pullRepoCheckout } from './repo-pull.js';
|
|
59
70
|
import { collectChanges, collectCommitChanges, collectRunCommits, commitAll, createOrSwitchBranch, imageMimeType, isOsOpenableImage, pushCurrentBranch, readWorktreePath, } from './git-changes.js';
|
|
60
71
|
import { gatedSkillsRepos, loadConfig, resolveWorktreeRetention } from '../config.js';
|
|
72
|
+
import { resolveWorktreeSetup } from '../worktree-setup.js';
|
|
61
73
|
import { findConfigFile } from '../agent-config/catalog.js';
|
|
62
74
|
import { readConfigFile, statConfigPath, writeConfigFile } from '../agent-config/files.js';
|
|
63
75
|
import { readAgentModelDefaults } from '../agent-config/models.js';
|
|
@@ -70,22 +82,26 @@ import { defaultAgentProfile, listAgentProfiles, profileDirState, resolveProfile
|
|
|
70
82
|
import { PROFILE_CAPABLE_PROVIDERS, profileEnv, supportsProfiles } from '../core/agent-profiles.js';
|
|
71
83
|
import { quoteExecutable, withEnvPrefix } from '../core/shell-env.js';
|
|
72
84
|
import { resolveCursorExecutable } from '../core/cursor-model-catalog.js';
|
|
85
|
+
import { resolveOmpExecutable } from '../core/omp-model-catalog.js';
|
|
73
86
|
import { allocateProjectSlug, listProjects, normalizeProjectTags, probeProjectStatus, toProjectListEntry, registerProject, removeProject, shouldRegisterProject, } from '../workspace/projects.js';
|
|
74
87
|
import { mergeWriteWorkspaceUiState, readWorkspaceUiState } from '../workspace/ui-state.js';
|
|
75
88
|
import { checkoutRepo } from './checkout.js';
|
|
76
|
-
import { ProjectContextError, ProjectContexts } from './project-context.js';
|
|
89
|
+
import { armPreview, ProjectContextError, ProjectContexts, sweepRegisteredPreviewLeftovers } from './project-context.js';
|
|
77
90
|
import { reviewGateEnabled } from '../runs/review-gate.js';
|
|
78
91
|
import { readUiState, uiStatePath } from '../ui-state.js';
|
|
79
92
|
import { agentHomePaths, expandTilde } from '../paths.js';
|
|
80
93
|
import { isLoopbackHostHeader, normalizeHostname, resolveCapabilities } from './capabilities.js';
|
|
81
94
|
import { applicationUpdateInputSchema } from '../contract/index.js';
|
|
82
95
|
import { ApplicationUpdateConflictError, ApplicationUpdateFailureError } from '../application-update/service.js';
|
|
83
|
-
import { createSocketHub } from './ws.js';
|
|
96
|
+
import { createSocketHub, socketHubRoute } from './ws.js';
|
|
97
|
+
import { attachUpgradeRouter } from './upgrade-router.js';
|
|
98
|
+
import { createPreviewSocket } from './preview-socket.js';
|
|
84
99
|
import { browseDirectory, isInsideBrowseRoot, isLexicallyInsideBrowseRoot, resolveBrowseRoot } from './fs-browse.js';
|
|
85
100
|
import { parseRemote, resolveForge } from './forge/index.js';
|
|
86
101
|
import { fetchGithub, fetchGithubProjects, fetchGithubChecks, fetchGithubComments, fetchGithubItem, forgetGithubItem, fetchGithubPrDiff, fetchGithubRefStatus, forgetRefStatus, readCachedRefStatuses, refNumberFromUrl, searchGithubItems, GithubPrNotFoundError, GH_CHECKS_MAX, GH_REF_STATUS_MAX } from './github.js';
|
|
87
102
|
import { ensureLaunchKey } from './launch-key.js';
|
|
88
103
|
import { CockpitAlreadyRunningError } from './cockpit-ownership.js';
|
|
104
|
+
import { RunStoreOpenError } from '../runs/store-open-error.js';
|
|
89
105
|
import { openInTerminal } from './open-in-terminal.js';
|
|
90
106
|
import { agentCliRunner, detectOpenTargets, openFileInDefaultApp, openInApp } from './open-in-app.js';
|
|
91
107
|
import { createDraftPr } from './pr.js';
|
|
@@ -150,14 +166,21 @@ const providerEnabledSchema = z.object({ enabled: z.boolean() }).strict();
|
|
|
150
166
|
const providerRetrySchema = z.object({
|
|
151
167
|
authFailureId: z.string().min(1).max(128),
|
|
152
168
|
}).strict();
|
|
169
|
+
/**
|
|
170
|
+
* The editable body of both kinds. Every kind-specific key is optional here because which ones a
|
|
171
|
+
* body must (or must not) carry depends on the kind, and on `PUT` the kind is the STORED one when
|
|
172
|
+
* the body omits it — `resolveAutomationKind` below owns those rules.
|
|
173
|
+
*/
|
|
153
174
|
const automationEditableSchema = z
|
|
154
175
|
.object({
|
|
155
176
|
name: z.string().trim().min(1).max(200),
|
|
156
177
|
description: z.string().max(2_000).optional(),
|
|
157
178
|
enabled: z.boolean().optional(),
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
179
|
+
kind: automationKindSchema.optional(),
|
|
180
|
+
events: z.array(automationEventSchema).min(1).max(4).optional(),
|
|
181
|
+
intervalSeconds: z.number().int().min(60).max(86_400).optional(),
|
|
182
|
+
filters: automationFiltersSchema.optional(),
|
|
183
|
+
schedule: automationScheduleSchema.optional(),
|
|
161
184
|
task: automationTaskSchema,
|
|
162
185
|
})
|
|
163
186
|
.strict();
|
|
@@ -177,17 +200,68 @@ const automationLogQuerySchema = z.object({
|
|
|
177
200
|
// never needed.
|
|
178
201
|
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
179
202
|
});
|
|
203
|
+
/** enable/pause carry no client revision, so a conflict is a race inside the write: 409, never 500. */
|
|
204
|
+
function toggleFailure(error) {
|
|
205
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
206
|
+
if (message === 'automation not found')
|
|
207
|
+
return { error: 'not found', status: 404 };
|
|
208
|
+
if (message === 'automation revision conflict')
|
|
209
|
+
return { error: 'the automation changed elsewhere; reload and try again', status: 409 };
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
180
212
|
function editableAutomation(definition) {
|
|
181
213
|
return {
|
|
182
214
|
name: definition.name,
|
|
183
215
|
description: definition.description,
|
|
184
216
|
enabled: definition.enabled,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
217
|
+
kind: definition.kind,
|
|
218
|
+
...(definition.events ? { events: definition.events } : {}),
|
|
219
|
+
...(definition.intervalSeconds !== undefined ? { intervalSeconds: definition.intervalSeconds } : {}),
|
|
220
|
+
...(definition.filters ? { filters: definition.filters } : {}),
|
|
221
|
+
...(definition.schedule ? { schedule: definition.schedule } : {}),
|
|
188
222
|
task: definition.task,
|
|
189
223
|
};
|
|
190
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Which kind a create/update body is, and whether its keys fit it (spec
|
|
227
|
+
* 2026-10-02-scheduled-automations § Data model). A body without `kind` is `github` on create —
|
|
228
|
+
* every pre-schedule client — and the STORED kind on update, so an old client editing a schedule
|
|
229
|
+
* edits a schedule. Changing the kind is refused: receipts and cursors are kind-specific.
|
|
230
|
+
*/
|
|
231
|
+
function resolveAutomationKind(body, stored) {
|
|
232
|
+
const kind = body.kind ?? stored?.kind ?? 'github';
|
|
233
|
+
if (stored && kind !== stored.kind)
|
|
234
|
+
return { error: 'change the kind by creating a new automation', status: 409 };
|
|
235
|
+
if (kind === 'schedule') {
|
|
236
|
+
if (body.events !== undefined || body.intervalSeconds !== undefined || body.filters !== undefined) {
|
|
237
|
+
return { error: 'a scheduled automation has no GitHub filter', status: 400 };
|
|
238
|
+
}
|
|
239
|
+
if (!body.schedule)
|
|
240
|
+
return { error: 'a scheduled automation needs a schedule', status: 400 };
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
if (body.schedule !== undefined)
|
|
244
|
+
return { error: 'a GitHub automation has no schedule', status: 400 };
|
|
245
|
+
if (!body.events || body.intervalSeconds === undefined || !body.filters) {
|
|
246
|
+
return { error: 'a GitHub automation needs events, intervalSeconds and filters', status: 400 };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { kind };
|
|
250
|
+
}
|
|
251
|
+
function sameSchedule(a, b) {
|
|
252
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The `nextRunAt` a schedule definition arms with: its next occurrence from now when enabled,
|
|
256
|
+
* cleared when paused. Passed to the store's `create`/`update` as `arm`, which writes it BEFORE
|
|
257
|
+
* the definition, so another cockpit's timer never pairs the new definition with the old instant.
|
|
258
|
+
*/
|
|
259
|
+
function armScheduleFromNow(definition) {
|
|
260
|
+
if (!isScheduleAutomation(definition))
|
|
261
|
+
return undefined;
|
|
262
|
+
const next = definition.enabled ? nextOccurrence(definition.schedule, Date.now(), localTimeZone()) : null;
|
|
263
|
+
return { nextRunAt: next === null ? undefined : new Date(next).toISOString() };
|
|
264
|
+
}
|
|
191
265
|
/**
|
|
192
266
|
* The public API surface (spec 2026-07-23-independent-server-web-packages).
|
|
193
267
|
*
|
|
@@ -225,7 +299,9 @@ export function projectRouteManifest(app) {
|
|
|
225
299
|
/** 409 body for the inbox mutators while the follow-up inbox is off (#471). */
|
|
226
300
|
const FOLLOWUPS_OFF = 'the follow-up inbox is disabled — set CEZ_FOLLOWUPS=1 to enable it';
|
|
227
301
|
/** 409 body for every automations route while GitHub automations are off (#801). */
|
|
228
|
-
const AUTOMATIONS_OFF = '
|
|
302
|
+
const AUTOMATIONS_OFF = 'Automations are disabled — set CEZ_AUTOMATIONS=1 to enable them';
|
|
303
|
+
/** 404 body for a run whose row is in runs.db but whose record this cezar cannot decode (#779). */
|
|
304
|
+
const UNREADABLE_RUN_ERROR = 'this run could not be read by this cezar: its record in runs.db does not fit this version (a newer cezar may have written it). It is left in the database untouched.';
|
|
229
305
|
/**
|
|
230
306
|
* The in-process bus for workspace-level SSE events. The registry-mutating
|
|
231
307
|
* routes (`POST /api/projects` — step 4.2, emits `project-added` for a
|
|
@@ -383,6 +459,7 @@ const appearanceSchema = z.object({
|
|
|
383
459
|
});
|
|
384
460
|
const uiStateSchema = z
|
|
385
461
|
.object({
|
|
462
|
+
sidebarLimits: sidebarLimitsSchema.optional(),
|
|
386
463
|
// `null` clears the recorded choice — the composer's "no skill, no workflow" state,
|
|
387
464
|
// which is a plain quick-task run. Written through to the file like any other value, so an
|
|
388
465
|
// older cockpit reading it falls back to its own default instead of restoring a stale skill.
|
|
@@ -842,6 +919,8 @@ export function createApp(deps) {
|
|
|
842
919
|
return listProjects(selector);
|
|
843
920
|
},
|
|
844
921
|
semaphore: deps.semaphore,
|
|
922
|
+
preview: deps.previewHost,
|
|
923
|
+
cezarPort: deps.cezarPort,
|
|
845
924
|
prepareManager: (project) => {
|
|
846
925
|
taskWebhooks.attach(project);
|
|
847
926
|
return deps.delegation?.attachProject(project);
|
|
@@ -987,34 +1066,52 @@ export function createApp(deps) {
|
|
|
987
1066
|
// `ProjectContextError` mapped to 404 (unknown) / 409 (missing root).
|
|
988
1067
|
// Named rather than inlined because both mounts share it — one function, so
|
|
989
1068
|
// the two spellings can never disagree about what `default` means.
|
|
1069
|
+
if (deps.projectLookup) {
|
|
1070
|
+
deps.projectLookup.resolve = async (raw) => {
|
|
1071
|
+
if (raw === undefined || raw === 'default' || raw === (await resolveBootProject()))
|
|
1072
|
+
return bootContext;
|
|
1073
|
+
if (!projectIdSchema.safeParse(raw).success)
|
|
1074
|
+
return undefined;
|
|
1075
|
+
return contexts.context(raw).catch(() => undefined);
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
990
1078
|
const resolveProjectScope = async (c, next) => {
|
|
991
1079
|
const raw = c.req.param('projectId');
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1080
|
+
let project = bootContext;
|
|
1081
|
+
if (raw !== undefined) {
|
|
1082
|
+
if (!projectIdSchema.safeParse(raw).success) {
|
|
1083
|
+
return c.json({ error: `unknown project: ${raw}` }, 404);
|
|
1084
|
+
}
|
|
1085
|
+
if (raw !== 'default' && raw !== (await resolveBootProject())) {
|
|
1086
|
+
try {
|
|
1087
|
+
project = await contexts.context(raw);
|
|
1088
|
+
}
|
|
1089
|
+
catch (err) {
|
|
1090
|
+
if (err instanceof CockpitAlreadyRunningError)
|
|
1091
|
+
return c.json({ error: err.message }, 409);
|
|
1092
|
+
// Nothing is cached for a failed build, so the next request opens the store again.
|
|
1093
|
+
if (err instanceof RunStoreOpenError)
|
|
1094
|
+
return c.json({ error: err.message }, 409);
|
|
1095
|
+
if (err instanceof ProjectContextError) {
|
|
1096
|
+
return err.reason === 'missing-root'
|
|
1097
|
+
? c.json({ error: `project folder not found: ${err.projectId}` }, 409)
|
|
1098
|
+
: c.json({ error: err.message }, 404);
|
|
1099
|
+
}
|
|
1100
|
+
throw err;
|
|
1101
|
+
}
|
|
1013
1102
|
}
|
|
1014
|
-
throw err;
|
|
1015
1103
|
}
|
|
1104
|
+
// The boot project's store could not open (#779): its runs are unknown, not absent, so its
|
|
1105
|
+
// routes answer with why rather than as an empty project. Only its routes: this resolver also
|
|
1106
|
+
// runs in front of every workspace route (`/health` above all), which must keep answering.
|
|
1107
|
+
if (project.store.unavailable && servesProjectRoute(c))
|
|
1108
|
+
return c.json({ error: project.store.unavailable.message }, 409);
|
|
1109
|
+
c.set('project', project);
|
|
1016
1110
|
return next();
|
|
1017
1111
|
};
|
|
1112
|
+
/** Every method+path of the project table, under both prefixes; filled once `v1` is built. */
|
|
1113
|
+
let projectRoutePaths = new Set();
|
|
1114
|
+
const servesProjectRoute = (c) => matchedRoutes(c).some((route) => route.method !== 'ALL' && projectRoutePaths.has(`${route.method} ${route.path}`));
|
|
1018
1115
|
// ---- static GUI ----------------------------------------------------------
|
|
1019
1116
|
const webDir = resolveWebDir();
|
|
1020
1117
|
const distDir = join(webDir, 'dist');
|
|
@@ -1462,7 +1559,7 @@ export function createApp(deps) {
|
|
|
1462
1559
|
workspaceEvents.emit('provider-status', row);
|
|
1463
1560
|
return c.json(result);
|
|
1464
1561
|
})
|
|
1465
|
-
.post('/providers/connect', jsonZodValidator(providerConnectSchema, { message: 'provider must be claude, codex, opencode, pi, or
|
|
1562
|
+
.post('/providers/connect', jsonZodValidator(providerConnectSchema, { message: 'provider must be claude, codex, opencode, pi, cursor, or omp' }), async (c) => {
|
|
1466
1563
|
const body = { data: c.req.valid('json') };
|
|
1467
1564
|
const provider = body.data.provider;
|
|
1468
1565
|
// A NAMED account is refused in hosted mode before anything is resolved, exactly like every
|
|
@@ -2727,6 +2824,7 @@ export function createApp(deps) {
|
|
|
2727
2824
|
codex: z.string().trim().min(1).max(200).nullable().optional(),
|
|
2728
2825
|
opencode: z.string().trim().min(1).max(200).nullable().optional(),
|
|
2729
2826
|
pi: z.string().trim().min(1).max(200).nullable().optional(),
|
|
2827
|
+
omp: z.string().trim().min(1).max(200).nullable().optional(),
|
|
2730
2828
|
cursor: z.string().trim().min(1).max(200).nullable().optional(),
|
|
2731
2829
|
})
|
|
2732
2830
|
.optional(),
|
|
@@ -2939,6 +3037,26 @@ export function createApp(deps) {
|
|
|
2939
3037
|
return c.json({ error: AUTOMATIONS_OFF }, 409);
|
|
2940
3038
|
await next();
|
|
2941
3039
|
};
|
|
3040
|
+
/** The schedule runner for a route's project: launches into that project's own run manager. */
|
|
3041
|
+
const scheduleHandle = (project) => {
|
|
3042
|
+
const timeZone = localTimeZone();
|
|
3043
|
+
return {
|
|
3044
|
+
projectId: project.id,
|
|
3045
|
+
store: project.automationStore,
|
|
3046
|
+
timeZone,
|
|
3047
|
+
launch: (definition, occurrence, receiptId) => launchScheduledRun({
|
|
3048
|
+
root: project.root,
|
|
3049
|
+
manager: project.manager,
|
|
3050
|
+
store: project.store,
|
|
3051
|
+
definition,
|
|
3052
|
+
occurrence,
|
|
3053
|
+
receiptId,
|
|
3054
|
+
projectName: basename(project.root),
|
|
3055
|
+
timeZone,
|
|
3056
|
+
}),
|
|
3057
|
+
onChange: (automationId, revision) => emitAutomationChange(project, automationId, revision),
|
|
3058
|
+
};
|
|
3059
|
+
};
|
|
2942
3060
|
// ---- chained family: GitHub automations (project-scoped) ----
|
|
2943
3061
|
// Every handler below reads `c.get('project')` — the definitions, their runtime state and the
|
|
2944
3062
|
// execution log are per-project files — so the family is project-scoped and mounted with the
|
|
@@ -2952,17 +3070,22 @@ export function createApp(deps) {
|
|
|
2952
3070
|
.get('/automations', async (c) => {
|
|
2953
3071
|
const { root, automationStore } = c.get('project');
|
|
2954
3072
|
const forge = resolveForge(await getRepoInfo(root));
|
|
2955
|
-
//
|
|
2956
|
-
//
|
|
2957
|
-
//
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
3073
|
+
// Awaits the driver's settled probe (per-repo cache + in-flight dedupe, 5 s timeout, never
|
|
3074
|
+
// throws): a "still being checked" answer would leave the cockpit's GitHub option disabled
|
|
3075
|
+
// because it does not refetch when the probe completes. `/health` keeps `detectCached`.
|
|
3076
|
+
// Annotated, so both branches are ONE shape: the route type is what
|
|
3077
|
+
// `contract/src/automations.ts` has to describe.
|
|
3078
|
+
const availability = forge
|
|
3079
|
+
? await forge.detect()
|
|
3080
|
+
: { available: false, reason: 'No GitHub remote is configured' };
|
|
2962
3081
|
const automations = automationStore.list().map((automation) => {
|
|
2963
3082
|
const logs = automationStore.logs({ automationId: automation.id, limit: 100 });
|
|
2964
3083
|
const state = automationStore.state(automation.id);
|
|
2965
3084
|
const latestLog = logs[0];
|
|
3085
|
+
// Enabled only: a paused definition's stored instant is stale, not a next run.
|
|
3086
|
+
const nextRunAt = automation.enabled
|
|
3087
|
+
? (automation.kind === 'schedule' ? state?.nextRunAt : state?.nextCheckAt)
|
|
3088
|
+
: undefined;
|
|
2966
3089
|
return {
|
|
2967
3090
|
...automation,
|
|
2968
3091
|
// Spread conditionally, never `state: maybeUndefined`: the latter types the key as
|
|
@@ -2970,17 +3093,19 @@ export function createApp(deps) {
|
|
|
2970
3093
|
// have to describe a key consumers never receive.
|
|
2971
3094
|
...(state ? { state } : {}),
|
|
2972
3095
|
...(latestLog ? { latestLog } : {}),
|
|
3096
|
+
...(nextRunAt ? { nextRunAt } : {}),
|
|
2973
3097
|
counts: {
|
|
2974
3098
|
matches: logs.filter((row) => row.result === 'launched' || row.result === 'duplicate').length,
|
|
2975
|
-
launched: logs.filter((row) => row.result === 'launched').length,
|
|
3099
|
+
launched: logs.filter((row) => row.result === 'launched' || row.result === 'manual' || row.result === 'catch-up').length,
|
|
2976
3100
|
duplicates: logs.filter((row) => row.result === 'duplicate').length,
|
|
2977
|
-
errors: logs.filter((row) => row.result === 'error' || row.result === 'rate-limited').length,
|
|
3101
|
+
errors: logs.filter((row) => row.result === 'error' || row.result === 'rate-limited' || row.result === 'failed').length,
|
|
2978
3102
|
},
|
|
2979
3103
|
};
|
|
2980
3104
|
});
|
|
2981
|
-
const nextDue = automations.map((item) => item.
|
|
3105
|
+
const nextDue = automations.map((item) => item.nextRunAt).filter(Boolean).sort()[0];
|
|
2982
3106
|
return c.json({
|
|
2983
3107
|
...availability,
|
|
3108
|
+
timeZone: localTimeZone(),
|
|
2984
3109
|
scheduler: {
|
|
2985
3110
|
// `as const` on both arms: in an object literal a conditional of two string literals
|
|
2986
3111
|
// widens to `string`, which would erase the two states this key can hold.
|
|
@@ -2991,22 +3116,30 @@ export function createApp(deps) {
|
|
|
2991
3116
|
});
|
|
2992
3117
|
})
|
|
2993
3118
|
.post('/automations', jsonZodValidator(() => automationCreateSchema), async (c) => {
|
|
2994
|
-
const { automationStore } = c.get('project');
|
|
3119
|
+
const { root, automationStore } = c.get('project');
|
|
2995
3120
|
const parsed = { data: c.req.valid('json') };
|
|
2996
|
-
const
|
|
3121
|
+
const resolved = resolveAutomationKind(parsed.data);
|
|
3122
|
+
if ('error' in resolved)
|
|
3123
|
+
return c.json({ error: resolved.error }, resolved.status);
|
|
3124
|
+
const promptIssue = validateAutomationPrompt(parsed.data.task.prompt, resolved.kind);
|
|
2997
3125
|
if (promptIssue)
|
|
2998
3126
|
return c.json({ error: promptIssue }, 400);
|
|
3127
|
+
// A poll needs a GitHub remote to poll; a schedule needs nothing but this project.
|
|
3128
|
+
if (resolved.kind === 'github' && !resolveForge(await getRepoInfo(root))) {
|
|
3129
|
+
return c.json({ error: 'No GitHub remote is configured' }, 400);
|
|
3130
|
+
}
|
|
2999
3131
|
const { enable, ...input } = parsed.data;
|
|
3000
3132
|
try {
|
|
3001
|
-
const automation = automationStore.create({ ...input, enabled: enable === true });
|
|
3002
|
-
if (enable) {
|
|
3133
|
+
const automation = automationStore.create({ ...input, kind: resolved.kind, enabled: enable === true }, undefined, enable ? armScheduleFromNow : undefined);
|
|
3134
|
+
if (enable && isGithubAutomation(automation)) {
|
|
3003
3135
|
const baselineAt = new Date().toISOString();
|
|
3004
|
-
automationStore.setState(automation.id, {
|
|
3136
|
+
automationStore.setState(automation.id, (current) => ({
|
|
3137
|
+
...current,
|
|
3005
3138
|
revision: automation.revision,
|
|
3006
3139
|
baselineAt,
|
|
3007
3140
|
cursor: { timestamp: baselineAt },
|
|
3008
3141
|
nextCheckAt: new Date(Date.now() + automation.intervalSeconds * 1_000).toISOString(),
|
|
3009
|
-
});
|
|
3142
|
+
}));
|
|
3010
3143
|
}
|
|
3011
3144
|
emitAutomationChange(c.get('project'), automation.id, automation.revision);
|
|
3012
3145
|
automationsChanged();
|
|
@@ -3032,14 +3165,24 @@ export function createApp(deps) {
|
|
|
3032
3165
|
.put('/automations/:id', jsonZodValidator(() => automationUpdateSchema), async (c) => {
|
|
3033
3166
|
const { automationStore } = c.get('project');
|
|
3034
3167
|
const parsed = { data: c.req.valid('json') };
|
|
3035
|
-
const promptIssue = validateAutomationPrompt(parsed.data.task.prompt);
|
|
3036
|
-
if (promptIssue)
|
|
3037
|
-
return c.json({ error: promptIssue }, 400);
|
|
3038
3168
|
const { expectedRevision, ...input } = parsed.data;
|
|
3039
|
-
|
|
3169
|
+
const stored = automationStore.get(c.req.param('id'));
|
|
3170
|
+
if (!stored)
|
|
3040
3171
|
return c.json({ error: 'not found' }, 404);
|
|
3172
|
+
const resolved = resolveAutomationKind(input, stored);
|
|
3173
|
+
if ('error' in resolved)
|
|
3174
|
+
return c.json({ error: resolved.error }, resolved.status);
|
|
3175
|
+
const promptIssue = validateAutomationPrompt(input.task.prompt, resolved.kind);
|
|
3176
|
+
if (promptIssue)
|
|
3177
|
+
return c.json({ error: promptIssue }, 400);
|
|
3041
3178
|
try {
|
|
3042
|
-
|
|
3179
|
+
// The armed instant belongs to the schedule it was computed from, and to an enabled
|
|
3180
|
+
// definition: a new schedule, or a resume through PUT, re-arms from now — never a catch-up
|
|
3181
|
+
// of an occurrence that passed while it was paused. Pausing clears it in the same write, so
|
|
3182
|
+
// another cockpit never reads the still-enabled definition beside a due instant. Armed in
|
|
3183
|
+
// the request, as `enable` does, rather than left to the timer: the list refetched on the
|
|
3184
|
+
// change event must already see it. Compared with the definition on disk, not the one read above.
|
|
3185
|
+
const automation = automationStore.update(c.req.param('id'), expectedRevision, { ...input, kind: resolved.kind, enabled: input.enabled ?? false }, (next, previous) => previous && (!sameSchedule(previous.schedule, next.schedule) || previous.enabled !== next.enabled) ? armScheduleFromNow(next) : undefined);
|
|
3043
3186
|
emitAutomationChange(c.get('project'), automation.id, automation.revision);
|
|
3044
3187
|
automationsChanged();
|
|
3045
3188
|
return c.json({ automation });
|
|
@@ -3061,32 +3204,79 @@ export function createApp(deps) {
|
|
|
3061
3204
|
})
|
|
3062
3205
|
.post('/automations/:id/enable', async (c) => {
|
|
3063
3206
|
const store = c.get('project').automationStore;
|
|
3207
|
+
// No client revision: the toggle applies to what is on disk now, not to this store's last read.
|
|
3208
|
+
store.reload();
|
|
3064
3209
|
const current = store.get(c.req.param('id'));
|
|
3065
3210
|
if (!current)
|
|
3066
3211
|
return c.json({ error: 'not found' }, 404);
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3212
|
+
try {
|
|
3213
|
+
if (isScheduleAutomation(current)) {
|
|
3214
|
+
// A schedule has no backlog to baseline against: it arms its next occurrence from now.
|
|
3215
|
+
const automation = store.update(current.id, current.revision, { ...editableAutomation(current), enabled: true }, armScheduleFromNow);
|
|
3216
|
+
emitAutomationChange(c.get('project'), automation.id, automation.revision);
|
|
3217
|
+
automationsChanged();
|
|
3218
|
+
return c.json({ automation });
|
|
3219
|
+
}
|
|
3220
|
+
if (!isGithubAutomation(current))
|
|
3221
|
+
return c.json({ error: 'automation definition is incomplete' }, 409);
|
|
3222
|
+
const automation = store.update(current.id, current.revision, { ...editableAutomation(current), enabled: true });
|
|
3223
|
+
const baselineAt = new Date().toISOString();
|
|
3224
|
+
store.setState(automation.id, (state) => ({
|
|
3225
|
+
...state,
|
|
3226
|
+
revision: automation.revision,
|
|
3227
|
+
baselineAt,
|
|
3228
|
+
cursor: { timestamp: baselineAt },
|
|
3229
|
+
nextCheckAt: new Date(Date.now() + current.intervalSeconds * 1_000).toISOString(),
|
|
3230
|
+
}));
|
|
3231
|
+
await store.appendLog({ automationId: automation.id, revision: automation.revision, result: 'baseline', reason: 'Enabled from a current-time baseline; existing records were not launched.' });
|
|
3232
|
+
emitAutomationChange(c.get('project'), automation.id, automation.revision);
|
|
3233
|
+
automationsChanged();
|
|
3234
|
+
return c.json({ automation });
|
|
3235
|
+
}
|
|
3236
|
+
catch (error) {
|
|
3237
|
+
const { status, ...body } = toggleFailure(error);
|
|
3238
|
+
return c.json(body, status);
|
|
3239
|
+
}
|
|
3080
3240
|
})
|
|
3081
3241
|
.post('/automations/:id/pause', (c) => {
|
|
3082
3242
|
const store = c.get('project').automationStore;
|
|
3243
|
+
store.reload();
|
|
3083
3244
|
const current = store.get(c.req.param('id'));
|
|
3084
3245
|
if (!current)
|
|
3085
3246
|
return c.json({ error: 'not found' }, 404);
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3247
|
+
try {
|
|
3248
|
+
const automation = store.update(current.id, current.revision, { ...editableAutomation(current), enabled: false }, armScheduleFromNow);
|
|
3249
|
+
emitAutomationChange(c.get('project'), automation.id, automation.revision);
|
|
3250
|
+
automationsChanged();
|
|
3251
|
+
return c.json({ automation });
|
|
3252
|
+
}
|
|
3253
|
+
catch (error) {
|
|
3254
|
+
const { status, ...body } = toggleFailure(error);
|
|
3255
|
+
return c.json(body, status);
|
|
3256
|
+
}
|
|
3257
|
+
})
|
|
3258
|
+
// Run now: a schedule fired once by hand, paused or not, outside the timer and under the same
|
|
3259
|
+
// lease. `nextRunAt` and `enabled` are untouched; a GitHub automation's equivalent is `check`.
|
|
3260
|
+
.post('/automations/:id/run', async (c) => {
|
|
3261
|
+
const project = c.get('project');
|
|
3262
|
+
const store = project.automationStore;
|
|
3263
|
+
const automation = store.get(c.req.param('id'));
|
|
3264
|
+
if (!automation)
|
|
3265
|
+
return c.json({ error: 'not found' }, 404);
|
|
3266
|
+
if (!isScheduleAutomation(automation))
|
|
3267
|
+
return c.json({ error: 'a GitHub automation runs on its events; use check with mode execute' }, 409);
|
|
3268
|
+
const outcome = await new ScheduleRunner(scheduleHandle(project)).runNow(automation);
|
|
3269
|
+
// Gone, or no longer a schedule, once re-read under the lease: nothing was launched.
|
|
3270
|
+
if (outcome.result === 'skipped')
|
|
3271
|
+
return c.json({ error: 'not found' }, 404);
|
|
3272
|
+
if (outcome.result === 'manual') {
|
|
3273
|
+
emitAutomationChange(project, automation.id, store.get(automation.id)?.revision ?? automation.revision);
|
|
3274
|
+
return c.json({ runId: outcome.runId }, 202);
|
|
3275
|
+
}
|
|
3276
|
+
if (outcome.result === 'lease-held')
|
|
3277
|
+
return c.json({ error: SCHEDULE_LEASE_HELD_REASON }, 409);
|
|
3278
|
+
const reason = store.logs({ automationId: automation.id, limit: 1 })[0]?.reason;
|
|
3279
|
+
return c.json({ error: reason ?? 'the run could not be launched' }, 409);
|
|
3090
3280
|
})
|
|
3091
3281
|
// The body is validated as MIDDLEWARE, which is what puts it in the route type — and moves
|
|
3092
3282
|
// the 400 ahead of this route's 404: `POST /automations/<unknown>/check` with a malformed
|
|
@@ -3097,6 +3287,8 @@ export function createApp(deps) {
|
|
|
3097
3287
|
const automation = store.get(c.req.param('id'));
|
|
3098
3288
|
if (!automation)
|
|
3099
3289
|
return c.json({ error: 'not found' }, 404);
|
|
3290
|
+
if (!isGithubAutomation(automation))
|
|
3291
|
+
return c.json({ error: 'a schedule has nothing to preview; use run' }, 409);
|
|
3100
3292
|
const parsed = { data: c.req.valid('json') };
|
|
3101
3293
|
// `string`, not `randomUUID`'s template-literal type: the wire carries an opaque id, and
|
|
3102
3294
|
// leaking `${string}-${string}-…` into the route type would make the contract describe the
|
|
@@ -3114,10 +3306,9 @@ export function createApp(deps) {
|
|
|
3114
3306
|
throw new Error('No GitHub remote is configured');
|
|
3115
3307
|
const scheduler = new ProjectAutomationScheduler({
|
|
3116
3308
|
projectId: project.id,
|
|
3117
|
-
owner: remote.owner,
|
|
3118
|
-
repo: remote.repo,
|
|
3119
3309
|
store,
|
|
3120
|
-
|
|
3310
|
+
timeZone: localTimeZone(),
|
|
3311
|
+
github: { owner: remote.owner, repo: remote.repo, poller: new GithubPoller() },
|
|
3121
3312
|
launch: parsed.data.mode === 'execute'
|
|
3122
3313
|
? (definition, candidate, receiptId) => launchAutomationRun({ root: project.root, manager: project.manager, store: project.store, definition, candidate, receiptId })
|
|
3123
3314
|
: undefined,
|
|
@@ -3143,11 +3334,32 @@ export function createApp(deps) {
|
|
|
3143
3334
|
return c.json({ error: 'not found' }, 404);
|
|
3144
3335
|
if (receipt.status !== 'launch-error' || receipt.runId)
|
|
3145
3336
|
return c.json({ error: 'receipt is not retryable' }, 409);
|
|
3337
|
+
const scheduled = store.get(receipt.automationId);
|
|
3338
|
+
// A schedule receipt carries its occurrence instead of a GitHub candidate; it is fired
|
|
3339
|
+
// again by hand through the runner, under the lease, as the same receipt.
|
|
3340
|
+
if (scheduled && isScheduleAutomation(scheduled) && receipt.occurrenceAt) {
|
|
3341
|
+
const outcome = await new ScheduleRunner(scheduleHandle(project)).retry(scheduled, receipt);
|
|
3342
|
+
// Gone, or no longer a schedule, once re-read under the lease: nothing was launched.
|
|
3343
|
+
if (outcome.result === 'skipped')
|
|
3344
|
+
return c.json({ error: 'not found' }, 404);
|
|
3345
|
+
if (outcome.result === 'manual') {
|
|
3346
|
+
emitAutomationChange(project, scheduled.id, store.get(scheduled.id)?.revision ?? scheduled.revision);
|
|
3347
|
+
return c.json({ receiptId: receipt.receiptId, runId: outcome.runId }, 202);
|
|
3348
|
+
}
|
|
3349
|
+
if (outcome.result === 'lease-held')
|
|
3350
|
+
return c.json({ error: SCHEDULE_LEASE_HELD_REASON }, 409);
|
|
3351
|
+
if (outcome.result === 'duplicate')
|
|
3352
|
+
return c.json({ error: 'receipt is not retryable' }, 409);
|
|
3353
|
+
const latest = store.latestReceipts().get(receipt.receiptKey);
|
|
3354
|
+
return c.json({ error: latest?.error ?? 'the run could not be launched' }, 409);
|
|
3355
|
+
}
|
|
3146
3356
|
if (!receipt.candidate)
|
|
3147
3357
|
return c.json({ error: 'receipt predates retry context and cannot be retried safely' }, 409);
|
|
3148
|
-
const definition =
|
|
3358
|
+
const definition = scheduled;
|
|
3149
3359
|
if (!definition)
|
|
3150
3360
|
return c.json({ error: 'automation not found' }, 404);
|
|
3361
|
+
if (!isGithubAutomation(definition))
|
|
3362
|
+
return c.json({ error: 'receipt is not retryable' }, 409);
|
|
3151
3363
|
const lease = store.acquireLease();
|
|
3152
3364
|
if (!lease)
|
|
3153
3365
|
return c.json({ error: 'automation polling lease is held by another process' }, 409);
|
|
@@ -3186,10 +3398,18 @@ export function createApp(deps) {
|
|
|
3186
3398
|
const sessionStep = [...run.steps].reverse().find(step => step.sessionId);
|
|
3187
3399
|
const sessionId = sessionStep?.sessionId;
|
|
3188
3400
|
const backend = sessionStep?.backend ?? run.runner ?? 'claude';
|
|
3189
|
-
|
|
3401
|
+
// Runners whose take-over command depends on a server-side binary override (CEZ_*_BIN).
|
|
3402
|
+
const command = (backend === 'cursor' || backend === 'omp') && sessionId ? resumeCommand(backend, sessionId) : null;
|
|
3190
3403
|
return { ...run, ...(usage ? { usage } : {}), ...(command ? { cliResumeCommand: command } : {}),
|
|
3191
3404
|
...(finishBlocked !== undefined ? { finishBlocked } : {}) };
|
|
3192
3405
|
};
|
|
3406
|
+
// A list row (#817) with the same live `usage` sample `withUsage` attaches. The sampler is
|
|
3407
|
+
// process-wide, so a workspace-level answer can carry it for every project's runs at once — a
|
|
3408
|
+
// cold project's stored summaries included. Appended last, exactly where `toRunSummary` puts it.
|
|
3409
|
+
const withLiveUsage = (summary) => {
|
|
3410
|
+
const usage = currentUsage(summary.id);
|
|
3411
|
+
return usage ? { ...summary, usage } : summary;
|
|
3412
|
+
};
|
|
3193
3413
|
// The inbox half of a composer launch (#374). Since the cockpit's "▶ Run"
|
|
3194
3414
|
// prefills `/new` instead of calling POST /api/todos/:id/start (never launch
|
|
3195
3415
|
// blind — #355), the todo id rides along on the composer's POST /api/runs and
|
|
@@ -3210,11 +3430,68 @@ export function createApp(deps) {
|
|
|
3210
3430
|
console.warn(`[cezar] could not mark inbox entry ${todoId} started: ${String(err)}`);
|
|
3211
3431
|
}
|
|
3212
3432
|
};
|
|
3433
|
+
/**
|
|
3434
|
+
* Every control on one run (any `/runs/:id` request that is not a read) first asks who owns it
|
|
3435
|
+
* (#779, plan step 3): a `serve` and a headless `cez run` can share a project's runs.
|
|
3436
|
+
* - Another live cezar process's run answers `409 { error }`, the shape these routes already
|
|
3437
|
+
* use for a run that exists but cannot be acted on now. Reads are never refused.
|
|
3438
|
+
* - A run whose owner is proven dead is adopted first — claimed, loaded and settled as
|
|
3439
|
+
* interrupted, never resumed (`settleOrphanedRun`) — and the control then applies. Otherwise
|
|
3440
|
+
* Stop on a crashed `cez run` would be a dead end until this cockpit restarted. Stop is the one
|
|
3441
|
+
* control the adoption carries out itself: its run settles as cancelled, and Stop answers that
|
|
3442
|
+
* it stopped it (`stoppedOnAdoption`).
|
|
3443
|
+
* - A run this process stopped writing after a conflicting write answers 409 too.
|
|
3444
|
+
*
|
|
3445
|
+
* Registered against explicit paths, like `requireAutomations`: `route()` re-registers it under
|
|
3446
|
+
* the mount prefix, and it gates every family's `/runs/:id` routes registered after it.
|
|
3447
|
+
*/
|
|
3448
|
+
const requireRunControl = async (c, next) => {
|
|
3449
|
+
const id = c.req.param('id');
|
|
3450
|
+
if (c.req.method === 'GET' || c.req.method === 'HEAD' || id === undefined)
|
|
3451
|
+
return next();
|
|
3452
|
+
const { store, manager } = c.get('project');
|
|
3453
|
+
if (store.runOwnership(id) === 'orphaned') {
|
|
3454
|
+
const stop = c.req.method === 'POST' && c.req.path.endsWith('/cancel');
|
|
3455
|
+
if (await manager.adoptOrphanedRun(id, { stop }) && stop && store.getRun(id)?.status === 'cancelled')
|
|
3456
|
+
c.set('stoppedOnAdoption', true);
|
|
3457
|
+
}
|
|
3458
|
+
const refusal = store.writeRefusal(id);
|
|
3459
|
+
if (!refusal)
|
|
3460
|
+
return next();
|
|
3461
|
+
// The delegation route in this family answers in the delegation error shape it always uses.
|
|
3462
|
+
return c.req.path.endsWith('/worker-destroy')
|
|
3463
|
+
? c.json({ code: 'incompatible_state', error: refusal }, 409)
|
|
3464
|
+
: c.json({ error: refusal }, 409);
|
|
3465
|
+
};
|
|
3213
3466
|
// ---- chained family: runs lifecycle + artifacts (project-scoped) ----
|
|
3214
3467
|
const delegationService = deps.delegation?.service ?? new DelegationService();
|
|
3215
3468
|
delegationService.setDiscovery({ models: modelCatalog, providers: providerStatus });
|
|
3216
3469
|
const runsRoutes = new Hono()
|
|
3217
|
-
.
|
|
3470
|
+
.use('/runs/:id', requireRunControl)
|
|
3471
|
+
.use('/runs/:id/*', requireRunControl)
|
|
3472
|
+
// Legacy (older clients): every full record. The cockpit and `cez task` read `/run-summaries`.
|
|
3473
|
+
.get('/runs', (c) => c.json(c.get('project').store.listAllRunsForLegacyRoute().map(run => withUsage(run))))
|
|
3474
|
+
// The slim list (#817): the same runs in the same order as `GET /runs`, from the stored
|
|
3475
|
+
// `toRunSummary` column with memory laid over it (#779), so the list decodes no record.
|
|
3476
|
+
// `?archived=recent` (#864) is the window the cockpit reads: every unarchived run plus the newest
|
|
3477
|
+
// `ARCHIVED_WINDOW` archived roots. The default stays every run, for clients that list them all.
|
|
3478
|
+
.get('/run-summaries', queryZodValidator(runSummariesQuerySchema), (c) => {
|
|
3479
|
+
const archivedWindow = c.req.valid('query').archived === 'recent' ? ARCHIVED_WINDOW : undefined;
|
|
3480
|
+
return c.json(c.get('project').store.listRunSummaries({ archivedWindow }).runs.map(withLiveUsage));
|
|
3481
|
+
})
|
|
3482
|
+
// The archived roots past that window, a page at a time, newest first (#864).
|
|
3483
|
+
.get('/run-summaries/archived', queryZodValidator(archivedRunsQuerySchema), (c) => {
|
|
3484
|
+
const { before, limit, q } = c.req.valid('query');
|
|
3485
|
+
const page = c.get('project').store.listArchivedRuns({
|
|
3486
|
+
limit: limit ?? ARCHIVED_RUNS_PAGE_DEFAULT,
|
|
3487
|
+
...(before !== undefined ? { before } : {}),
|
|
3488
|
+
...(q !== undefined ? { q } : {}),
|
|
3489
|
+
});
|
|
3490
|
+
if ('error' in page)
|
|
3491
|
+
return c.json({ error: page.error }, 400);
|
|
3492
|
+
const body = { ...page, runs: page.runs.map(withLiveUsage) };
|
|
3493
|
+
return c.json(body);
|
|
3494
|
+
})
|
|
3218
3495
|
.get('/runs/:id/relationships', paramZodValidator(runIdParamSchema), queryZodValidator(workerEmptyRequestSchema), (c) => {
|
|
3219
3496
|
const { store } = c.get('project');
|
|
3220
3497
|
const run = store.getRun(c.req.valid('param').id);
|
|
@@ -3225,18 +3502,31 @@ export function createApp(deps) {
|
|
|
3225
3502
|
// read has to cost O(workers) whatever the store holds — and nothing at all for the
|
|
3226
3503
|
// ordinary run that owns none, delegation on or off.
|
|
3227
3504
|
// Human reads use the bound project, never agent credentials or private evidence.
|
|
3228
|
-
const
|
|
3505
|
+
const ownedWorkers = store.listOwnedWorkers(run.id);
|
|
3506
|
+
const workers = ownedWorkers.flatMap(worker => {
|
|
3229
3507
|
const owned = worker.delegation;
|
|
3230
3508
|
if (owned?.role !== 'worker')
|
|
3231
3509
|
return [];
|
|
3232
3510
|
return [{ workerId: worker.id, parentRunId: run.id, status: worker.status, workspace: owned.workspace,
|
|
3233
3511
|
...(worker.currentStepId === undefined ? {} : { currentStepId: worker.currentStepId }),
|
|
3234
3512
|
...(worker.activity === undefined ? {} : { activity: worker.activity }),
|
|
3235
|
-
...(owned.destroy ? { destroy:
|
|
3513
|
+
...(owned.destroy ? { destroy: workerDestroyView(worker) } : {}),
|
|
3236
3514
|
}];
|
|
3237
|
-
})
|
|
3515
|
+
});
|
|
3516
|
+
// #864: the titles of the runs this answer names, since the cockpit's run list no longer
|
|
3517
|
+
// carries archived workers (or a parent past its archived window).
|
|
3518
|
+
const parent = run.delegation?.role === 'worker' ? store.getRun(run.delegation.parentRunId) : undefined;
|
|
3519
|
+
const titles = [...(parent ? [parent] : []), ...ownedWorkers].map((related) => ({
|
|
3520
|
+
id: related.id, title: related.title,
|
|
3521
|
+
...(related.titleSummary === undefined ? {} : { titleSummary: related.titleSummary }),
|
|
3522
|
+
...(related.titleOrigin === undefined ? {} : { titleOrigin: related.titleOrigin }),
|
|
3523
|
+
}));
|
|
3524
|
+
// #816: every owned worker, bounded by the 1,024 creation ceiling rather than sliced, and
|
|
3525
|
+
// the root's capacity from the same derivation the spawn policy enforces.
|
|
3238
3526
|
return c.json(runRelationshipsSchema.parse({
|
|
3239
3527
|
...(run.delegation?.role === 'worker' ? { parentRunId: run.delegation.parentRunId } : {}), workers,
|
|
3528
|
+
...(titles.length > 0 ? { titles } : {}),
|
|
3529
|
+
...(run.delegation?.role === 'root' ? { capacity: workerCapacity(run, id => store.getRun(id)) } : {}),
|
|
3240
3530
|
}));
|
|
3241
3531
|
})
|
|
3242
3532
|
.post('/runs/:id/worker-destroy', queryZodValidator(workerEmptyRequestSchema, { code: 'invalid_input', message: 'Invalid cleanup query' }), paramZodValidator(runIdParamSchema, { code: 'invalid_input', message: 'Invalid worker ID' }), jsonZodValidator(workerEmptyRequestSchema, { absent: {}, malformed: null, code: 'invalid_input', message: 'Invalid cleanup input' }), async (c) => {
|
|
@@ -3253,7 +3543,7 @@ export function createApp(deps) {
|
|
|
3253
3543
|
})
|
|
3254
3544
|
// Registered before the `/:id/...` routes so "archive-finished" and "read-all"
|
|
3255
3545
|
// never match as a run id.
|
|
3256
|
-
.post('/runs/archive-finished', (c) => c.json(
|
|
3546
|
+
.post('/runs/archive-finished', jsonZodValidator(archiveFinishedRequestSchema, { absent: {}, malformed: null }), (c) => c.json(c.get('project').store.archiveFinished(c.req.valid('json').scope)))
|
|
3257
3547
|
// The read-receipt sweep (#unread-done-items) — the mark-read twin of the archive
|
|
3258
3548
|
// sweep above, and under the same registration-order guard.
|
|
3259
3549
|
.post('/runs/read-all', (c) => c.json({ read: c.get('project').store.markAllRead() }))
|
|
@@ -3356,11 +3646,7 @@ export function createApp(deps) {
|
|
|
3356
3646
|
const issue = stepsIssue(parsed.data.steps);
|
|
3357
3647
|
if (issue)
|
|
3358
3648
|
return c.json({ error: issue }, 400);
|
|
3359
|
-
workflow =
|
|
3360
|
-
name: '(planned)',
|
|
3361
|
-
source: 'built-in',
|
|
3362
|
-
steps: parsed.data.steps,
|
|
3363
|
-
};
|
|
3649
|
+
workflow = plannedWorkflow(parsed.data.steps);
|
|
3364
3650
|
}
|
|
3365
3651
|
else {
|
|
3366
3652
|
const { workflows } = await loadWorkflows(repoRoot);
|
|
@@ -3442,8 +3728,12 @@ export function createApp(deps) {
|
|
|
3442
3728
|
})
|
|
3443
3729
|
.get('/runs/:id', (c) => {
|
|
3444
3730
|
const { store, manager } = c.get('project');
|
|
3445
|
-
const
|
|
3446
|
-
|
|
3731
|
+
const id = c.req.param('id');
|
|
3732
|
+
const run = store.getRun(id);
|
|
3733
|
+
if (run)
|
|
3734
|
+
return c.json(withUsage(run, manager.finishBlockedReason(run.id) ?? null));
|
|
3735
|
+
// A row in runs.db this version cannot decode (#779): listed until read, so say why it will not open.
|
|
3736
|
+
return c.json({ error: store.isUnreadable(id) ? UNREADABLE_RUN_ERROR : 'not found' }, 404);
|
|
3447
3737
|
})
|
|
3448
3738
|
.get('/runs/:id/history', paramZodValidator(runIdParamSchema), queryZodValidator(runHistoryQuerySchema), async (c) => {
|
|
3449
3739
|
const { store, dataDir } = c.get('project');
|
|
@@ -3451,7 +3741,7 @@ export function createApp(deps) {
|
|
|
3451
3741
|
if (!store.getRun(id))
|
|
3452
3742
|
return c.json({ error: 'not found' }, 404);
|
|
3453
3743
|
try {
|
|
3454
|
-
return c.json(await readRunHistoryPage(
|
|
3744
|
+
return c.json(await readRunHistoryPage(dataDir, id, c.req.valid('query').cursor));
|
|
3455
3745
|
}
|
|
3456
3746
|
catch (error) {
|
|
3457
3747
|
if (error instanceof HistoryCursorError)
|
|
@@ -3464,7 +3754,7 @@ export function createApp(deps) {
|
|
|
3464
3754
|
const { id } = c.req.valid('param');
|
|
3465
3755
|
if (!store.getRun(id))
|
|
3466
3756
|
return c.json({ error: 'not found' }, 404);
|
|
3467
|
-
return c.json(await deriveRunContextEvents(
|
|
3757
|
+
return c.json(await deriveRunContextEvents(dataDir, id));
|
|
3468
3758
|
})
|
|
3469
3759
|
// Editable titles (#389). The UI displays `titleSummary ?? title`, so a
|
|
3470
3760
|
// user edit sets BOTH: `title` (the record's own name — the raw task stops
|
|
@@ -3508,7 +3798,8 @@ export function createApp(deps) {
|
|
|
3508
3798
|
const id = c.req.param('id');
|
|
3509
3799
|
if (!store.getRun(id))
|
|
3510
3800
|
return c.json({ error: 'not found' }, 404);
|
|
3511
|
-
|
|
3801
|
+
// Still asked of the manager after an adoption stopped the run: a worker's stop bookkeeping is its.
|
|
3802
|
+
const cancelled = manager.cancel(id) || c.get('stoppedOnAdoption') === true;
|
|
3512
3803
|
return c.json({ cancelled });
|
|
3513
3804
|
})
|
|
3514
3805
|
// Live-session participation (spec 002): deliver a user message (text +
|
|
@@ -3809,9 +4100,13 @@ export function createApp(deps) {
|
|
|
3809
4100
|
// An id resumeCommand refuses (#431) degrades to a fresh CLI in the worktree,
|
|
3810
4101
|
// exactly like a run that never recorded a session.
|
|
3811
4102
|
const resume = sessionId && cliRunner === (sessionStep?.backend ?? run.runner ?? 'claude') ? resumeCommand(cliRunner, sessionId) : null;
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
4103
|
+
// Runners with a CEZ_*_BIN override launch the configured binary, not the PATH name.
|
|
4104
|
+
const command = resume ?? (cliRunner === 'cursor'
|
|
4105
|
+
? quoteExecutable(resolveCursorExecutable(), process.platform)
|
|
4106
|
+
: cliRunner === 'omp' ? quoteExecutable(resolveOmpExecutable(), process.platform) : cliRunner);
|
|
4107
|
+
if (command === null) {
|
|
4108
|
+
return c.json({ error: `the configured ${cliRunner === 'omp' ? 'OMP' : 'Cursor'} executable cannot be used in a terminal command` }, 409);
|
|
4109
|
+
}
|
|
3815
4110
|
// BOTH branches carry the account (spec 2026-07-29-agent-profiles): a resume needs the
|
|
3816
4111
|
// config dir that holds its session, and a FRESH CLI in this worktree should still open
|
|
3817
4112
|
// on the account the project works under — otherwise "Open in → Claude CLI" quietly
|
|
@@ -4175,7 +4470,7 @@ export function createApp(deps) {
|
|
|
4175
4470
|
if (run.delegation?.role === 'worker' || !store.canDeleteRun(id))
|
|
4176
4471
|
return c.json({ error: 'owned resources require verified worker cleanup' }, 409);
|
|
4177
4472
|
if (run.worktreePath)
|
|
4178
|
-
await
|
|
4473
|
+
await releaseThenRemoveWorktree({ previewHost: deps.previewHost }, id, repoRoot, run.worktreePath, run.branch);
|
|
4179
4474
|
store.updateRun(id, { worktreePath: undefined, branch: undefined });
|
|
4180
4475
|
return c.json({ removed: true });
|
|
4181
4476
|
})
|
|
@@ -4191,13 +4486,12 @@ export function createApp(deps) {
|
|
|
4191
4486
|
if (!store.canDeleteRun(id))
|
|
4192
4487
|
return c.json({ error: 'owned resources require verified worker cleanup and explicit child history deletion first' }, 409);
|
|
4193
4488
|
if (run.delegation?.role !== 'worker' && run.worktreePath)
|
|
4194
|
-
await
|
|
4489
|
+
await releaseThenRemoveWorktree({ previewHost: deps.previewHost }, id, repoRoot, run.worktreePath, run.branch);
|
|
4195
4490
|
return store.deleteRun(id) ? c.json({ deleted: true }) : c.json({ error: 'history cleanup incomplete — retry deletion' }, 409);
|
|
4196
4491
|
});
|
|
4197
4492
|
// ---- parallel variants (spec 010) -----------------------------------------
|
|
4198
4493
|
const groupRuns = (store, groupId) => store
|
|
4199
|
-
.
|
|
4200
|
-
.filter((r) => r.groupId === groupId)
|
|
4494
|
+
.listGroupRuns(groupId)
|
|
4201
4495
|
.sort((a, b) => (a.variant ?? '').localeCompare(b.variant ?? ''));
|
|
4202
4496
|
// ---- chained family: variant groups (project-scoped) ----
|
|
4203
4497
|
const groupsRoutes = new Hono()
|
|
@@ -4269,7 +4563,7 @@ export function createApp(deps) {
|
|
|
4269
4563
|
if (loser.delegation?.role === 'worker' || !store.canDeleteRun(loser.id))
|
|
4270
4564
|
continue;
|
|
4271
4565
|
if (loser.worktreePath)
|
|
4272
|
-
await
|
|
4566
|
+
await releaseThenRemoveWorktree({ previewHost: deps.previewHost }, loser.id, repoRoot, loser.worktreePath, loser.branch);
|
|
4273
4567
|
store.updateRun(loser.id, { worktreePath: undefined, branch: undefined });
|
|
4274
4568
|
store.setArchived(loser.id, true);
|
|
4275
4569
|
store.appendEvent(loser.id, {
|
|
@@ -4360,12 +4654,12 @@ export function createApp(deps) {
|
|
|
4360
4654
|
// The keep-limit the panel reports is the one the enforcer will actually
|
|
4361
4655
|
// apply — inherited from the workspace default when this repo sets none.
|
|
4362
4656
|
const keep = await resolveWorktreeRetention(repoRoot);
|
|
4363
|
-
const
|
|
4364
|
-
// Listing is on-disk dirs only; parent liveness for #575
|
|
4657
|
+
const withWorktree = store.listRunsWithWorktree();
|
|
4658
|
+
// Listing is on-disk dirs only; parent liveness for #575 reads the store by id
|
|
4365
4659
|
// so a live `worktree: false` parent is not treated as gone (#570 honesty).
|
|
4366
|
-
const runs =
|
|
4660
|
+
const runs = withWorktree.filter((r) => r.worktreePath && existsSync(r.worktreePath));
|
|
4367
4661
|
// The rows the enforcer would reclaim right now: reclaimable AND past the newest `keep`.
|
|
4368
|
-
const pastKeep = new Set(selectReclaimableWorktrees(
|
|
4662
|
+
const pastKeep = new Set(selectReclaimableWorktrees(withWorktree, keep, (id) => store.getRun(id)));
|
|
4369
4663
|
const worktrees = await Promise.all(runs.map(async (r) => ({
|
|
4370
4664
|
runId: r.id,
|
|
4371
4665
|
title: r.title ?? r.id,
|
|
@@ -4374,7 +4668,7 @@ export function createApp(deps) {
|
|
|
4374
4668
|
// POSIX `du` — degrades to null (Windows / du missing / error); never blocks.
|
|
4375
4669
|
sizeBytes: await worktreeSizeBytes(r.worktreePath),
|
|
4376
4670
|
finishedAt: r.finishedAt ?? null,
|
|
4377
|
-
reclaimable: isReclaimable(r,
|
|
4671
|
+
reclaimable: isReclaimable(r, (id) => store.getRun(id)),
|
|
4378
4672
|
pastKeep: pastKeep.has(r.id),
|
|
4379
4673
|
})));
|
|
4380
4674
|
// Total is null when any size degraded, so the panel never shows a wrong sum.
|
|
@@ -4390,6 +4684,7 @@ export function createApp(deps) {
|
|
|
4390
4684
|
const { manager } = c.get('project');
|
|
4391
4685
|
const reclaimed = await reclaimWorktrees(repoRoot, store, await resolveWorktreeRetention(repoRoot), {
|
|
4392
4686
|
claim: (run) => manager.claimWorktreeReclaim(run.id),
|
|
4687
|
+
previewHost: deps.previewHost,
|
|
4393
4688
|
});
|
|
4394
4689
|
if (reclaimed.length > 0)
|
|
4395
4690
|
forgetRepoBranches(repoRoot);
|
|
@@ -4408,7 +4703,7 @@ export function createApp(deps) {
|
|
|
4408
4703
|
if (!release) {
|
|
4409
4704
|
return c.json({ error: 'this worktree is in use or already reclaimed — only a finished task\'s worktree can be reclaimed' }, 409);
|
|
4410
4705
|
}
|
|
4411
|
-
const worktreeReclaimedAt = await reclaimWorktree(repoRoot, store, run, { requireClean: true }).finally(release);
|
|
4706
|
+
const worktreeReclaimedAt = await reclaimWorktree(repoRoot, store, run, { requireClean: true, previewHost: deps.previewHost }).finally(release);
|
|
4412
4707
|
if (!worktreeReclaimedAt) {
|
|
4413
4708
|
// Reclaim keeps only what is committed, so it leaves a dirty checkout alone; say which case this was.
|
|
4414
4709
|
const dirty = run.worktreePath && existsSync(run.worktreePath) ? (await getStatus(run.worktreePath).catch(() => [])).length : 0;
|
|
@@ -4526,10 +4821,9 @@ export function createApp(deps) {
|
|
|
4526
4821
|
if (!store.getRun(id))
|
|
4527
4822
|
return c.json({ error: 'not found' }, 404);
|
|
4528
4823
|
const query = c.req.valid('query');
|
|
4529
|
-
const eventsPath = join(dataDir, 'runs', `${id}.ndjson`);
|
|
4530
4824
|
if (query.cursor) {
|
|
4531
4825
|
try {
|
|
4532
|
-
await validateLiveCursor(
|
|
4826
|
+
await validateLiveCursor(dataDir, id, query.cursor);
|
|
4533
4827
|
}
|
|
4534
4828
|
catch (error) {
|
|
4535
4829
|
if (error instanceof HistoryCursorError)
|
|
@@ -4575,7 +4869,7 @@ export function createApp(deps) {
|
|
|
4575
4869
|
store.off('run', onRun);
|
|
4576
4870
|
});
|
|
4577
4871
|
const replay = query.cursor
|
|
4578
|
-
? await readEventsAfterLiveCursor(
|
|
4872
|
+
? await readEventsAfterLiveCursor(dataDir, id, query.cursor)
|
|
4579
4873
|
: { events: store.readEvents(id), boundarySeq: 0 };
|
|
4580
4874
|
maxSeq = Math.max(maxSeq, replay.boundarySeq);
|
|
4581
4875
|
for (const event of replay.events) {
|
|
@@ -4924,7 +5218,7 @@ export function createApp(deps) {
|
|
|
4924
5218
|
const config = await loadConfig(project.root);
|
|
4925
5219
|
return {
|
|
4926
5220
|
root: info.root,
|
|
4927
|
-
runs: project.store.
|
|
5221
|
+
runs: project.store.listBranchOwners(),
|
|
4928
5222
|
isActive: (id) => project.manager.isActive(id),
|
|
4929
5223
|
configuredBase: config.baseBranch,
|
|
4930
5224
|
currentBranch: info.branch,
|
|
@@ -4955,7 +5249,7 @@ export function createApp(deps) {
|
|
|
4955
5249
|
]);
|
|
4956
5250
|
const [tracking, sources] = await Promise.all([
|
|
4957
5251
|
getTracking(info.root, config.baseBranch ?? info.branch),
|
|
4958
|
-
attributeLog(info.root, rawLog, c.get('project').store.
|
|
5252
|
+
attributeLog(info.root, rawLog, c.get('project').store.listBranchOwners()),
|
|
4959
5253
|
]);
|
|
4960
5254
|
// `source` is spread conditionally: absent, never `null`, when no task is known.
|
|
4961
5255
|
const log = rawLog.map(({ parents: _parents, ...entry }, i) => {
|
|
@@ -5070,7 +5364,10 @@ export function createApp(deps) {
|
|
|
5070
5364
|
if (!info)
|
|
5071
5365
|
return c.json({ error: 'not a git repository' }, 409);
|
|
5072
5366
|
const config = await loadConfig(root);
|
|
5073
|
-
const result = await pullRepoCheckout(info.root, c.req.valid('json'), config.baseBranch,
|
|
5367
|
+
const result = await pullRepoCheckout(info.root, c.req.valid('json'), config.baseBranch,
|
|
5368
|
+
// Every id, not the live set: the manager may still hold a run whose record already
|
|
5369
|
+
// settled (a worker's execution winding down). Ids only, nothing decoded.
|
|
5370
|
+
() => store.listRunIds().some((id) => manager.isActive(id)));
|
|
5074
5371
|
if (!result.ok)
|
|
5075
5372
|
return c.json(result.value, 409);
|
|
5076
5373
|
return c.json(result.value, 200);
|
|
@@ -5095,10 +5392,13 @@ export function createApp(deps) {
|
|
|
5095
5392
|
release();
|
|
5096
5393
|
}
|
|
5097
5394
|
});
|
|
5395
|
+
/** #917: setup commands run on the host before every task, so only a local cockpit edits them. */
|
|
5396
|
+
const WORKTREE_SETUP_HOSTED_ERROR = 'Worktree setup commands can be edited only on the machine running cezar.';
|
|
5098
5397
|
// The Settings → Agents knobs in one read (R6 Step 1.5) — an ADDITIVE
|
|
5099
5398
|
// sibling of PUT /api/config below; /api/health keeps its protected shape.
|
|
5100
5399
|
const configAnswer = async (repoRoot, config) => {
|
|
5101
5400
|
const nativeModels = await readAgentModelDefaults(repoRoot);
|
|
5401
|
+
const setup = resolveWorktreeSetup(config.worktreeSetup);
|
|
5102
5402
|
const modelsLocked = agentModelsLocked(repoRoot);
|
|
5103
5403
|
return {
|
|
5104
5404
|
baseBranch: config.baseBranch ?? null,
|
|
@@ -5121,6 +5421,9 @@ export function createApp(deps) {
|
|
|
5121
5421
|
// Optional review gate (#489): tri-state — null means "no config key, the
|
|
5122
5422
|
// CEZ_REVIEW_GATE env default (OFF) decides".
|
|
5123
5423
|
reviewGate: config.reviewGate ?? null,
|
|
5424
|
+
// Worktree setup (#917): an invalid value is reported, never shown as "no setup".
|
|
5425
|
+
worktreeSetup: setup.kind === 'commands' ? { commands: setup.commands, timeoutSeconds: setup.timeoutSeconds } : null,
|
|
5426
|
+
worktreeSetupIssue: setup.kind === 'invalid' ? setup.issue : null,
|
|
5124
5427
|
};
|
|
5125
5428
|
};
|
|
5126
5429
|
// ---- chained family: per-repo config (project-scoped) ----
|
|
@@ -5135,6 +5438,11 @@ export function createApp(deps) {
|
|
|
5135
5438
|
if (agentModelsLocked(repoRoot) && parsed.data.defaultModels !== undefined) {
|
|
5136
5439
|
return c.json({ error: AGENT_MODELS_LOCKED_ERROR }, 409);
|
|
5137
5440
|
}
|
|
5441
|
+
// Setup commands run on the host before every task (#917): a local-machine capability, like
|
|
5442
|
+
// Agent config writes. Checked before any write, so a refused body changes nothing.
|
|
5443
|
+
if (parsed.data.worktreeSetup !== undefined && !capabilities().localHandoff) {
|
|
5444
|
+
return c.json({ error: WORKTREE_SETUP_HOSTED_ERROR }, 409);
|
|
5445
|
+
}
|
|
5138
5446
|
const configPath = join(dataDir, 'config.json');
|
|
5139
5447
|
let raw = {};
|
|
5140
5448
|
try {
|
|
@@ -5193,6 +5501,17 @@ export function createApp(deps) {
|
|
|
5193
5501
|
raw.memoryLimitMb = parsed.data.memoryLimitMb;
|
|
5194
5502
|
}
|
|
5195
5503
|
}
|
|
5504
|
+
if (parsed.data.worktreeSetup !== undefined) {
|
|
5505
|
+
// null or no commands = no setup: delete the key rather than store an empty list.
|
|
5506
|
+
const setup = parsed.data.worktreeSetup;
|
|
5507
|
+
if (setup === null || setup.commands.length === 0)
|
|
5508
|
+
delete raw.worktreeSetup;
|
|
5509
|
+
else
|
|
5510
|
+
raw.worktreeSetup = {
|
|
5511
|
+
commands: setup.commands,
|
|
5512
|
+
...(setup.timeoutSeconds !== undefined ? { timeoutSeconds: setup.timeoutSeconds } : {}),
|
|
5513
|
+
};
|
|
5514
|
+
}
|
|
5196
5515
|
if (parsed.data.defaultModels !== undefined) {
|
|
5197
5516
|
// Per-runner merge, so setting codex's preset never clobbers claude's.
|
|
5198
5517
|
const current = raw.defaultModels && typeof raw.defaultModels === 'object'
|
|
@@ -5237,6 +5556,7 @@ export function createApp(deps) {
|
|
|
5237
5556
|
codex: modelPresetSchema,
|
|
5238
5557
|
opencode: modelPresetSchema,
|
|
5239
5558
|
pi: modelPresetSchema,
|
|
5559
|
+
omp: modelPresetSchema,
|
|
5240
5560
|
cursor: modelPresetSchema,
|
|
5241
5561
|
})
|
|
5242
5562
|
.optional(),
|
|
@@ -5254,6 +5574,8 @@ export function createApp(deps) {
|
|
|
5254
5574
|
// Optional review gate toggle (Settings → Agents, #489): null clears the key
|
|
5255
5575
|
// back to the env-default behavior (OFF).
|
|
5256
5576
|
reviewGate: z.boolean().nullable().optional(),
|
|
5577
|
+
// Worktree setup (Settings → Worktrees, #917): null or [] deletes the key.
|
|
5578
|
+
worktreeSetup: worktreeSetupConfigSchema.nullable().optional(),
|
|
5257
5579
|
});
|
|
5258
5580
|
const setAgentConfigSchema = z.object({
|
|
5259
5581
|
content: z.string().max(2_000_000),
|
|
@@ -5338,15 +5660,18 @@ export function createApp(deps) {
|
|
|
5338
5660
|
.route('/', repoRoutes)
|
|
5339
5661
|
.route('/', configRoutes)
|
|
5340
5662
|
.route('/', agentConfigRoutes);
|
|
5663
|
+
projectRoutePaths = new Set(v1.routes.filter((route) => route.method !== 'ALL')
|
|
5664
|
+
.flatMap((route) => [V1_PREFIX, V1_SCOPED_PREFIX].map((prefix) => `${route.method} ${prefix}${route.path}`)));
|
|
5341
5665
|
// ---- chained family: the cross-project run index (workspace-level) -------
|
|
5342
5666
|
/**
|
|
5343
|
-
* How many runs each project may contribute, newest first. The index is a
|
|
5344
|
-
* listing:
|
|
5345
|
-
*
|
|
5346
|
-
*
|
|
5667
|
+
* How many ARCHIVED root runs each project may contribute, newest first (#864). The index is a
|
|
5668
|
+
* FINDER, not a listing: every unarchived ROOT run is in it (so a project's pills miss nothing
|
|
5669
|
+
* live or unread), and older archived runs are reached through `GET /workspace/runs-search` and
|
|
5670
|
+
* the project's own archived pages. Owned workers are never in it: the pills, ⌘K and the global
|
|
5671
|
+
* Tasks page list no workers, and a parent's wait label comes from its own delegation. `truncated` names the projects this bit, so a consumer never
|
|
5347
5672
|
* has to pretend the list is complete.
|
|
5348
5673
|
*/
|
|
5349
|
-
const RUNS_INDEX_PER_PROJECT =
|
|
5674
|
+
const RUNS_INDEX_PER_PROJECT = ARCHIVED_WINDOW;
|
|
5350
5675
|
/** `RunRecord` → the wire row. Optional keys are spread CONDITIONALLY: writing
|
|
5351
5676
|
* `titleSummary: run.titleSummary` types a key as always-present that `JSON.stringify` then
|
|
5352
5677
|
* drops when it is undefined, which is exactly the drift the parity guard fails on. */
|
|
@@ -5371,44 +5696,6 @@ export function createApp(deps) {
|
|
|
5371
5696
|
}
|
|
5372
5697
|
return numbers;
|
|
5373
5698
|
};
|
|
5374
|
-
const runIndexEntry = (projectId, run) => {
|
|
5375
|
-
const usage = currentUsage(run.id);
|
|
5376
|
-
return {
|
|
5377
|
-
projectId,
|
|
5378
|
-
id: run.id,
|
|
5379
|
-
title: run.title,
|
|
5380
|
-
...(run.titleSummary !== undefined ? { titleSummary: run.titleSummary } : {}),
|
|
5381
|
-
...(run.titleOrigin !== undefined ? { titleOrigin: run.titleOrigin } : {}),
|
|
5382
|
-
status: run.status,
|
|
5383
|
-
...(run.delegation ? { delegation: runDelegationSummarySchema.parse(run.delegation) } : {}),
|
|
5384
|
-
...(run.activity !== undefined ? { activity: run.activity } : {}),
|
|
5385
|
-
...(run.hasPendingHumanAsk !== undefined ? { hasPendingHumanAsk: run.hasPendingHumanAsk } : {}),
|
|
5386
|
-
createdAt: run.createdAt,
|
|
5387
|
-
...(run.finishedAt !== undefined ? { finishedAt: run.finishedAt } : {}),
|
|
5388
|
-
...(run.seenAt !== undefined ? { seenAt: run.seenAt } : {}),
|
|
5389
|
-
archived: run.archived,
|
|
5390
|
-
...(run.autoResumeAt !== undefined ? { autoResumeAt: run.autoResumeAt } : {}),
|
|
5391
|
-
workflow: run.workflow,
|
|
5392
|
-
...(run.branch !== undefined ? { branch: run.branch } : {}),
|
|
5393
|
-
...(run.startedAt !== undefined ? { startedAt: run.startedAt } : {}),
|
|
5394
|
-
// The tracker-reference inputs, verbatim — the cockpit's `taskReference()` owns the rule
|
|
5395
|
-
// that picks between them (see the schema's note).
|
|
5396
|
-
...(run.pullRequestUrl !== undefined ? { pullRequestUrl: run.pullRequestUrl } : {}),
|
|
5397
|
-
...(run.referencedPullRequestUrl !== undefined
|
|
5398
|
-
? { referencedPullRequestUrl: run.referencedPullRequestUrl }
|
|
5399
|
-
: {}),
|
|
5400
|
-
...(run.prNumber !== undefined ? { prNumber: run.prNumber } : {}),
|
|
5401
|
-
...(run.issueNumber !== undefined ? { issueNumber: run.issueNumber } : {}),
|
|
5402
|
-
...(run.referencedIssueUrl !== undefined ? { referencedIssueUrl: run.referencedIssueUrl } : {}),
|
|
5403
|
-
...(run.markerRefs !== undefined ? { markerRefs: run.markerRefs } : {}),
|
|
5404
|
-
...(run.costUsd !== undefined ? { costUsd: run.costUsd } : {}),
|
|
5405
|
-
...(run.peakRssBytes !== undefined ? { peakRssBytes: run.peakRssBytes } : {}),
|
|
5406
|
-
...(run.peakProcCount !== undefined ? { peakProcCount: run.peakProcCount } : {}),
|
|
5407
|
-
// The live sample, on the same terms as `GET /runs`: process-wide sampler, so a
|
|
5408
|
-
// workspace-level answer can carry it for every project's runs at once.
|
|
5409
|
-
...(usage ? { usage } : {}),
|
|
5410
|
-
};
|
|
5411
|
-
};
|
|
5412
5699
|
/**
|
|
5413
5700
|
* `GET /workspace/runs-index` — every registered project's recent tasks in one slim answer, so
|
|
5414
5701
|
* ⌘K can find a task without knowing which project it lives in.
|
|
@@ -5421,8 +5708,13 @@ export function createApp(deps) {
|
|
|
5421
5708
|
* search box must not resume work; see `runs/run-index.ts`.
|
|
5422
5709
|
*/
|
|
5423
5710
|
const coldRepoHandles = new ColdRepoHandles();
|
|
5424
|
-
|
|
5425
|
-
|
|
5711
|
+
/**
|
|
5712
|
+
* Every registered project whose runs can be read, and its owned store when this process holds
|
|
5713
|
+
* one (`bootContext` for the boot project, `contexts.peek` otherwise) — never a built context.
|
|
5714
|
+
* Without a store, the caller reads the project cold from `dataDir` with `handle`. The source
|
|
5715
|
+
* rule both workspace run routes share, so the index and the search can never disagree on it.
|
|
5716
|
+
*/
|
|
5717
|
+
const projectRunSources = async () => {
|
|
5426
5718
|
let projects = [];
|
|
5427
5719
|
try {
|
|
5428
5720
|
const selector = capabilities().singleProject
|
|
@@ -5431,13 +5723,37 @@ export function createApp(deps) {
|
|
|
5431
5723
|
projects = await listProjects(selector);
|
|
5432
5724
|
}
|
|
5433
5725
|
catch {
|
|
5434
|
-
// unreadable workspace —
|
|
5435
|
-
//
|
|
5726
|
+
// unreadable workspace — no rows, never a 500. The palette degrades to the active
|
|
5727
|
+
// project's own run list, which it holds either way.
|
|
5436
5728
|
}
|
|
5437
5729
|
const bootId = await resolveBootProject(projects);
|
|
5438
5730
|
coldRepoHandles.retainRoots(new Set(projects
|
|
5439
5731
|
.filter((project) => project.status !== 'missing' && project.id !== bootId && !contexts.peek(project.id))
|
|
5440
5732
|
.map((project) => project.root)));
|
|
5733
|
+
// No folder, no runs to read. `not-git` still has an `.ai/cezar` worth indexing.
|
|
5734
|
+
return projects.filter((project) => project.status !== 'missing').map((project) => {
|
|
5735
|
+
const owned = project.id === bootId ? bootContext : contexts.peek(project.id);
|
|
5736
|
+
// A cold project's repository identity only: asking for an owned one starts a discovery
|
|
5737
|
+
// nothing reads.
|
|
5738
|
+
return { project, store: owned?.store, dataDir: join(project.root, '.ai/cezar'), handle: owned ? undefined : coldRepoHandles.get(project.root) };
|
|
5739
|
+
});
|
|
5740
|
+
};
|
|
5741
|
+
/**
|
|
5742
|
+
* One project's contribution to a workspace run route. An owned store throws what its database
|
|
5743
|
+
* throws (busy, damaged); like a cold project that cannot be read, it then contributes nothing,
|
|
5744
|
+
* so one project never fails every project's index or search (#864).
|
|
5745
|
+
*/
|
|
5746
|
+
const readProjectRuns = (projectId, read) => {
|
|
5747
|
+
try {
|
|
5748
|
+
return read();
|
|
5749
|
+
}
|
|
5750
|
+
catch (error) {
|
|
5751
|
+
console.warn(`[cez] runs of project ${projectId} could not be read for the workspace run index: ${error instanceof Error ? error.message : String(error)}`);
|
|
5752
|
+
return { runs: [], truncated: false };
|
|
5753
|
+
}
|
|
5754
|
+
};
|
|
5755
|
+
const runsIndexRoutes = new Hono()
|
|
5756
|
+
.get('/workspace/runs-index', async (c) => {
|
|
5441
5757
|
const runs = [];
|
|
5442
5758
|
const truncated = [];
|
|
5443
5759
|
// Statuses the server already holds, shipped WITH the rows that carry the references. The
|
|
@@ -5446,24 +5762,24 @@ export function createApp(deps) {
|
|
|
5446
5762
|
// never touches `gh` and never slows the index down; anything cold stays absent and the
|
|
5447
5763
|
// lazy `/github/ref-status` route fills it in.
|
|
5448
5764
|
const referenceStatuses = {};
|
|
5449
|
-
for (const project of
|
|
5450
|
-
//
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
const owned = project.id === bootId ? bootContext : contexts.peek(project.id);
|
|
5454
|
-
// `listRuns()` already sorts newest-first; the disk reader returns file order, so both
|
|
5455
|
-
// paths get sorted below rather than trusting either.
|
|
5765
|
+
for (const { project, store: owned, dataDir, handle } of await projectRunSources()) {
|
|
5766
|
+
// Both sources answer the same window, newest first: every unarchived run, plus the
|
|
5767
|
+
// newest archived roots (#864). An owned project lays its held records over its rows; a
|
|
5768
|
+
// cold one reads its stored summaries, at most one archived root past the window.
|
|
5456
5769
|
//
|
|
5457
5770
|
// Archived runs are INCLUDED. The active project's rows reach the palette through
|
|
5458
|
-
// `GET /
|
|
5771
|
+
// `GET /run-summaries`, which carries them too, and excluding them here would mean a task
|
|
5459
5772
|
// is findable while you stand in its project and vanishes the moment you leave — the
|
|
5460
5773
|
// exact asymmetry a cross-project finder exists to remove.
|
|
5461
|
-
const
|
|
5462
|
-
|
|
5774
|
+
const window = readProjectRuns(project.id, () => owned
|
|
5775
|
+
? owned.listRunSummaries({ archivedWindow: RUNS_INDEX_PER_PROJECT, roots: true })
|
|
5776
|
+
: readRunIndexFromDisk(dataDir, { handle, archivedWindow: RUNS_INDEX_PER_PROJECT, roots: true }));
|
|
5777
|
+
if (window.truncated)
|
|
5463
5778
|
truncated.push(project.id);
|
|
5779
|
+
const recent = window.runs.map(withLiveUsage);
|
|
5464
5780
|
const mentioned = [];
|
|
5465
|
-
for (const run of recent
|
|
5466
|
-
runs.push(
|
|
5781
|
+
for (const run of recent) {
|
|
5782
|
+
runs.push({ projectId: project.id, ...run });
|
|
5467
5783
|
mentioned.push(...mentionedReferenceNumbers(run));
|
|
5468
5784
|
}
|
|
5469
5785
|
if (mentioned.length > 0) {
|
|
@@ -5484,6 +5800,35 @@ export function createApp(deps) {
|
|
|
5484
5800
|
referenceStatuses,
|
|
5485
5801
|
};
|
|
5486
5802
|
return c.json(body);
|
|
5803
|
+
})
|
|
5804
|
+
/**
|
|
5805
|
+
* `GET /workspace/runs-search` (#864) — every registered project's ROOT runs, archived or not,
|
|
5806
|
+
* that match `q`, newest first: what lets ⌘K find a run past a project's window. Same source
|
|
5807
|
+
* rule and the same side-effect-free contract as the index above.
|
|
5808
|
+
*/
|
|
5809
|
+
.get('/workspace/runs-search', queryZodValidator(runsSearchQuerySchema), async (c) => {
|
|
5810
|
+
const { q, limit = RUNS_SEARCH_PER_PROJECT_MAX } = c.req.valid('query');
|
|
5811
|
+
const runs = [];
|
|
5812
|
+
const truncated = [];
|
|
5813
|
+
const tokens = searchTokens(q);
|
|
5814
|
+
for (const { project, store: owned, dataDir, handle } of await projectRunSources()) {
|
|
5815
|
+
// The palette filters its rows by project name too, so a token naming this project is
|
|
5816
|
+
// already satisfied here: `shop database` searches shop for `database` (#864 review). A
|
|
5817
|
+
// query that only names the project adds nothing past the index's rows for it.
|
|
5818
|
+
const name = project.name.toLowerCase();
|
|
5819
|
+
const rest = tokens.filter((token) => !name.includes(token));
|
|
5820
|
+
if (rest.length === 0)
|
|
5821
|
+
continue;
|
|
5822
|
+
const scoped = rest.join(' ');
|
|
5823
|
+
const found = readProjectRuns(project.id, () => owned ? owned.searchRunSummaries(scoped, limit) : searchRunIndexFromDisk(dataDir, scoped, { handle, limit }));
|
|
5824
|
+
if (found.truncated)
|
|
5825
|
+
truncated.push(project.id);
|
|
5826
|
+
for (const run of found.runs)
|
|
5827
|
+
runs.push({ projectId: project.id, ...withLiveUsage(run) });
|
|
5828
|
+
}
|
|
5829
|
+
runs.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
5830
|
+
const body = { runs, truncated };
|
|
5831
|
+
return c.json(body);
|
|
5487
5832
|
});
|
|
5488
5833
|
// Workspace-level families answer for the whole workspace, so they are single-mount: never a
|
|
5489
5834
|
// project-scoped spelling, which would be a second surface to protect with no consumer.
|
|
@@ -5532,12 +5877,16 @@ function taskWebhookHost(bindHost) {
|
|
|
5532
5877
|
return bindHost.includes(':') ? `[${bindHost}]` : bindHost;
|
|
5533
5878
|
}
|
|
5534
5879
|
const NO_WEBHOOK_ERROR = 'this project has no task webhook — set one in Settings → General → Task webhook, or start without notify';
|
|
5535
|
-
export function startServer(deps, port) {
|
|
5880
|
+
export function startServer(deps, port, listener) {
|
|
5536
5881
|
const workspaceEvents = deps.workspaceEvents ?? new WorkspaceEventBus();
|
|
5537
5882
|
const skillsUpdate = deps.skillsUpdate ?? new SkillsUpdateService({ invalidateCatalog: refreshTeamSkills });
|
|
5538
5883
|
// The subscription hub rides the same HTTP server (one port, zero config):
|
|
5539
5884
|
// createApp registers the topics, the `upgrade` hook below owns the socket.
|
|
5540
5885
|
const socketHub = deps.socketHub ?? createSocketHub();
|
|
5886
|
+
// `cezar_preview_serve` refuses the cockpit's own port: the one the listener BOUND (`--port 0`).
|
|
5887
|
+
let listeningPort;
|
|
5888
|
+
const cezarPort = deps.cezarPort ?? (() => listeningPort);
|
|
5889
|
+
const projectLookup = {};
|
|
5541
5890
|
const automationCoordinator = new AutomationCoordinator({ listProjects });
|
|
5542
5891
|
const bootProjectId = deps.bootProjectId ?? 'default';
|
|
5543
5892
|
const bootAutomationStore = automationCoordinator.store(bootProjectId, deps.repoRoot);
|
|
@@ -5546,6 +5895,8 @@ export function startServer(deps, port) {
|
|
|
5546
5895
|
ownership: deps.ownership,
|
|
5547
5896
|
listProjects,
|
|
5548
5897
|
semaphore: deps.semaphore,
|
|
5898
|
+
preview: deps.previewHost,
|
|
5899
|
+
cezarPort,
|
|
5549
5900
|
automationStore: (projectId, root) => automationCoordinator.store(projectId, root),
|
|
5550
5901
|
prepareManager: (project) => {
|
|
5551
5902
|
taskWebhooks.attach(project);
|
|
@@ -5567,14 +5918,16 @@ export function startServer(deps, port) {
|
|
|
5567
5918
|
workspaceEvents,
|
|
5568
5919
|
skillsUpdate,
|
|
5569
5920
|
socketHub,
|
|
5921
|
+
cezarPort,
|
|
5922
|
+
projectLookup,
|
|
5570
5923
|
automationsChanged: () => rescheduleAutomations(),
|
|
5571
5924
|
});
|
|
5572
5925
|
// SECURITY: default to loopback. This server executes agents locally and its endpoints are
|
|
5573
5926
|
// same-origin-trusted (only /api/health is CORS-open); binding to a non-loopback host would
|
|
5574
5927
|
// expose an agent-executing box to the network. `bindHost` exists only for a deliberate
|
|
5575
5928
|
// hosted/VPS deployment (which also flips CEZ_REMOTE to gate the local-handoff endpoints) —
|
|
5576
|
-
//
|
|
5577
|
-
const
|
|
5929
|
+
// The CLI passes it only when explicitly requested; normal startup stays on loopback.
|
|
5930
|
+
const serverOptions = {
|
|
5578
5931
|
fetch: async (request, env) => {
|
|
5579
5932
|
const result = await app.fetch(request, env);
|
|
5580
5933
|
if (request.method === 'POST' && new URL(request.url).pathname === '/api/v1/workspace/application-update/restart'
|
|
@@ -5599,27 +5952,85 @@ export function startServer(deps, port) {
|
|
|
5599
5952
|
},
|
|
5600
5953
|
port,
|
|
5601
5954
|
hostname: deps.bindHost ?? '127.0.0.1',
|
|
5602
|
-
}
|
|
5955
|
+
};
|
|
5956
|
+
// The CLI already owns this listener before recovery. Replace its temporary 503
|
|
5957
|
+
// handler without closing/rebinding; direct server callers retain normal startup.
|
|
5958
|
+
if (listener) {
|
|
5959
|
+
listener.removeAllListeners('request');
|
|
5960
|
+
listener.on('request', getRequestListener(serverOptions.fetch, { hostname: serverOptions.hostname }));
|
|
5961
|
+
}
|
|
5962
|
+
const server = listener ?? serve(serverOptions);
|
|
5603
5963
|
const coordinator = new SkillsUpdateCoordinator(skillsUpdate, async () => effectiveSkillsAutoUpdate(await loadWorkspaceConfig()));
|
|
5964
|
+
// EVERY registered project, remote or not: a schedule needs none. `github` is present only for
|
|
5965
|
+
// a github.com remote, and only then does the project's handle poll.
|
|
5604
5966
|
const automationProjects = new Map();
|
|
5967
|
+
const rememberAutomationProject = async (id, root) => {
|
|
5968
|
+
const parsed = parseRemote((await getRepoInfo(root))?.remote ?? '');
|
|
5969
|
+
automationProjects.set(id, {
|
|
5970
|
+
root,
|
|
5971
|
+
...(parsed?.host === 'github.com' ? { github: { owner: parsed.owner, repo: parsed.repo } } : {}),
|
|
5972
|
+
});
|
|
5973
|
+
};
|
|
5974
|
+
const emitWorkspaceAutomationChange = (projectId) => (automationId, revision) => workspaceEvents.emit('automation-change', { project: projectId, automationId, revision });
|
|
5975
|
+
/**
|
|
5976
|
+
* The boot brake for one project, at boot and for a project registered after it: an enabled
|
|
5977
|
+
* poll idle past its lookback is re-baselined rather than resumed from a stale cursor. Only a
|
|
5978
|
+
* project with a github.com remote: its polls are the only ones the timer arms, so a poll
|
|
5979
|
+
* without one never succeeds and would be re-baselined on every boot, forever.
|
|
5980
|
+
*/
|
|
5981
|
+
const brakeIdleAutomations = (id, automationStore) => {
|
|
5982
|
+
if (!automationStore || !automationProjects.get(id)?.github)
|
|
5983
|
+
return;
|
|
5984
|
+
rebaselineIdleAutomations(automationStore, emitWorkspaceAutomationChange(id));
|
|
5985
|
+
};
|
|
5605
5986
|
const automationScheduler = new WorkspaceAutomationScheduler({
|
|
5606
5987
|
coordinator: automationCoordinator,
|
|
5607
5988
|
handle: (projectId, store) => {
|
|
5608
5989
|
const project = automationProjects.get(projectId);
|
|
5609
5990
|
if (!project)
|
|
5610
5991
|
return undefined;
|
|
5992
|
+
const timeZone = localTimeZone();
|
|
5993
|
+
const launchContext = async () => projectId === (deps.bootProjectId ?? 'default')
|
|
5994
|
+
? { root: deps.repoRoot, manager: deps.manager, store: deps.store }
|
|
5995
|
+
: await sharedContexts.context(projectId);
|
|
5611
5996
|
return {
|
|
5612
5997
|
projectId,
|
|
5613
|
-
owner: project.owner,
|
|
5614
|
-
repo: project.repo,
|
|
5615
5998
|
store,
|
|
5616
|
-
|
|
5617
|
-
|
|
5999
|
+
timeZone,
|
|
6000
|
+
...(project.github ? { github: { ...project.github, poller: new GithubPoller() } } : {}),
|
|
6001
|
+
onChange: emitWorkspaceAutomationChange(projectId),
|
|
6002
|
+
// Building a lazy project's context reconciles on its own; the explicit call covers the
|
|
6003
|
+
// boot project and a context built before another process left the reservation.
|
|
6004
|
+
reconcileReceipts: async () => { reconcileAutomationReceipts(store, (await launchContext()).store); },
|
|
6005
|
+
// A run store that cannot open (#779) launches nothing. Firing anyway would count every
|
|
6006
|
+
// refusal towards the auto-pause, and the pause would outlive the restart that fixed it.
|
|
6007
|
+
ready: async () => {
|
|
6008
|
+
try {
|
|
6009
|
+
const { store: runs } = await launchContext();
|
|
6010
|
+
if (runs.unavailable)
|
|
6011
|
+
throw runs.unavailable;
|
|
6012
|
+
}
|
|
6013
|
+
catch (error) {
|
|
6014
|
+
if (error instanceof RunStoreOpenError)
|
|
6015
|
+
throw new AutomationProjectUnavailableError(error.message, { cause: error });
|
|
6016
|
+
throw error;
|
|
6017
|
+
}
|
|
6018
|
+
},
|
|
6019
|
+
launchSchedule: async (definition, occurrence, receiptId) => {
|
|
6020
|
+
const context = await launchContext();
|
|
6021
|
+
return launchScheduledRun({
|
|
6022
|
+
root: context.root,
|
|
6023
|
+
manager: context.manager,
|
|
6024
|
+
store: context.store,
|
|
6025
|
+
definition,
|
|
6026
|
+
occurrence,
|
|
6027
|
+
receiptId,
|
|
6028
|
+
projectName: basename(context.root),
|
|
6029
|
+
timeZone,
|
|
6030
|
+
});
|
|
6031
|
+
},
|
|
5618
6032
|
launch: async (definition, candidate, receiptId) => {
|
|
5619
|
-
const
|
|
5620
|
-
const context = projectId === bootId
|
|
5621
|
-
? { root: deps.repoRoot, manager: deps.manager, store: deps.store }
|
|
5622
|
-
: await sharedContexts.context(projectId);
|
|
6033
|
+
const context = await launchContext();
|
|
5623
6034
|
return launchAutomationRun({
|
|
5624
6035
|
root: context.root,
|
|
5625
6036
|
manager: context.manager,
|
|
@@ -5645,12 +6056,13 @@ export function startServer(deps, port) {
|
|
|
5645
6056
|
const project = data.project;
|
|
5646
6057
|
if (project && typeof project.id === 'string' && typeof project.root === 'string' && project.status !== 'missing') {
|
|
5647
6058
|
coordinator.add(project.id, project.root);
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
if (
|
|
5651
|
-
|
|
6059
|
+
const { id, root } = project;
|
|
6060
|
+
void rememberAutomationProject(id, root).then(() => {
|
|
6061
|
+
if (!automationsEnabled())
|
|
6062
|
+
return;
|
|
6063
|
+
brakeIdleAutomations(id, automationCoordinator.store(id, root));
|
|
5652
6064
|
return rescheduleAutomations();
|
|
5653
|
-
});
|
|
6065
|
+
}).catch(() => undefined);
|
|
5654
6066
|
}
|
|
5655
6067
|
}
|
|
5656
6068
|
else if (event === 'project-removed') {
|
|
@@ -5664,41 +6076,72 @@ export function startServer(deps, port) {
|
|
|
5664
6076
|
}
|
|
5665
6077
|
}
|
|
5666
6078
|
});
|
|
5667
|
-
|
|
6079
|
+
const startBackgroundServices = () => {
|
|
5668
6080
|
void listProjects().then((projects) => {
|
|
5669
6081
|
const all = projects.some((project) => project.root === deps.repoRoot)
|
|
5670
6082
|
? projects : [{ id: deps.bootProjectId ?? 'default', root: deps.repoRoot, status: 'ok' }, ...projects];
|
|
5671
6083
|
coordinator.start(all);
|
|
6084
|
+
// #781: every registered project's preview leftovers, not only the boot project's.
|
|
6085
|
+
void sweepRegisteredPreviewLeftovers(all, deps.previewHost).catch(() => 0);
|
|
5672
6086
|
// #801: with automations off there is nothing to warm — no remote to resolve, no receipts
|
|
5673
6087
|
// to reconcile, and above all no scheduler to start. The skills-update coordinator above is
|
|
5674
6088
|
// a separate feature and starts either way.
|
|
5675
6089
|
if (!automationsEnabled())
|
|
5676
6090
|
return;
|
|
5677
6091
|
void Promise.all(all.map(async (project) => {
|
|
5678
|
-
|
|
5679
|
-
if (parsed?.host === 'github.com')
|
|
5680
|
-
automationProjects.set(project.id, { root: project.root, owner: parsed.owner, repo: parsed.repo });
|
|
6092
|
+
await rememberAutomationProject(project.id, project.root);
|
|
5681
6093
|
const automationStore = automationCoordinator.store(project.id, project.root);
|
|
5682
6094
|
const runStore = project.id === (deps.bootProjectId ?? 'default')
|
|
5683
6095
|
? deps.store
|
|
5684
6096
|
: sharedContexts.peek(project.id)?.store;
|
|
5685
|
-
|
|
6097
|
+
// A run store that could not open (#779) cannot say which receipts launched a run.
|
|
6098
|
+
if (automationStore && runStore && !runStore.unavailable)
|
|
5686
6099
|
reconcileAutomationReceipts(automationStore, runStore);
|
|
6100
|
+
// After reconciliation and before the timer arms.
|
|
6101
|
+
brakeIdleAutomations(project.id, automationStore);
|
|
5687
6102
|
})).then(() => automationScheduler.start()).catch(() => undefined);
|
|
5688
6103
|
}).catch(() => undefined);
|
|
6104
|
+
};
|
|
6105
|
+
if (server.listening)
|
|
6106
|
+
startBackgroundServices();
|
|
6107
|
+
else
|
|
6108
|
+
server.once('listening', startBackgroundServices);
|
|
6109
|
+
// The boot store's preview leftovers and run deletions; lazy projects arm theirs at build.
|
|
6110
|
+
armPreview(deps.store, join(deps.repoRoot, '.ai/cezar'), deps.previewHost);
|
|
6111
|
+
const previewSocket = createPreviewSocket({
|
|
6112
|
+
host: deps.previewHost,
|
|
6113
|
+
verify: (req) => verifyWsUpgrade(req, deps.bindHost),
|
|
6114
|
+
resolveProject: async (projectId) => projectLookup.resolve?.(projectId),
|
|
6115
|
+
});
|
|
6116
|
+
server.once('close', () => {
|
|
6117
|
+
unsubscribe();
|
|
6118
|
+
coordinator.stop();
|
|
6119
|
+
automationScheduler.stop();
|
|
6120
|
+
sharedContexts.disposeAll();
|
|
6121
|
+
void deps.delegation?.close();
|
|
6122
|
+
socketHub.close();
|
|
6123
|
+
previewSocket.close();
|
|
6124
|
+
void deps.previewHost?.close().catch(() => undefined);
|
|
5689
6125
|
});
|
|
5690
|
-
|
|
5691
|
-
|
|
6126
|
+
// The one `upgrade` listener: the subscription bus and the preview pane's socket (#781).
|
|
6127
|
+
attachUpgradeRouter(server, [socketHubRoute(socketHub, (req) => verifyWsUpgrade(req, deps.bindHost)), previewSocket.route]);
|
|
5692
6128
|
const shutdownForRestart = async () => {
|
|
5693
|
-
sharedContexts.disposeAll(); //
|
|
6129
|
+
sharedContexts.disposeAll(); // close every built secondary project before old process exits
|
|
5694
6130
|
deps.store.flush();
|
|
5695
6131
|
socketHub.close();
|
|
6132
|
+
// Dev servers run in their own process groups: they outlive cezar unless stopped here.
|
|
6133
|
+
// A failed release must not abort the restart halfway, with the store flushed and the port still bound.
|
|
6134
|
+
await deps.previewHost?.close().catch(() => undefined);
|
|
6135
|
+
previewSocket.close();
|
|
5696
6136
|
await deps.delegation?.close();
|
|
5697
6137
|
await new Promise((resolve) => {
|
|
5698
6138
|
server.close(() => resolve());
|
|
5699
6139
|
if ('closeAllConnections' in server)
|
|
5700
6140
|
server.closeAllConnections();
|
|
5701
6141
|
});
|
|
6142
|
+
// Last, with no request left to serve: write what is still pending and release every claim,
|
|
6143
|
+
// so the replacement process finds the live runs nobody's, as a clean stop leaves them (#779).
|
|
6144
|
+
deps.store.close();
|
|
5702
6145
|
};
|
|
5703
6146
|
// The task webhook's thread links name the port the listener BOUND (#594 review): `--port 0`
|
|
5704
6147
|
// asks for an ephemeral one, so the requested number is not an address. Deliveries queue
|
|
@@ -5706,6 +6149,7 @@ export function startServer(deps, port) {
|
|
|
5706
6149
|
const setTaskWebhookOrigin = () => {
|
|
5707
6150
|
const address = server.address();
|
|
5708
6151
|
const bound = address && typeof address === 'object' ? address.port : port;
|
|
6152
|
+
listeningPort = bound;
|
|
5709
6153
|
taskWebhooks.setOrigin(`http://${taskWebhookHost(deps.bindHost)}:${bound}`);
|
|
5710
6154
|
};
|
|
5711
6155
|
if (server.listening)
|
|
@@ -5872,6 +6316,11 @@ export function resumeCommand(runner, sessionId) {
|
|
|
5872
6316
|
}
|
|
5873
6317
|
case 'pi':
|
|
5874
6318
|
return `pi --session ${sessionId}`;
|
|
6319
|
+
case 'omp': {
|
|
6320
|
+
// CEZ_OMP_BIN may name a different install than `omp` on PATH; resume with the configured one.
|
|
6321
|
+
const executable = quoteExecutable(resolveOmpExecutable(), process.platform);
|
|
6322
|
+
return executable === null ? null : `${executable} --resume ${sessionId}`;
|
|
6323
|
+
}
|
|
5875
6324
|
default:
|
|
5876
6325
|
return `claude --resume ${sessionId}`;
|
|
5877
6326
|
}
|