commandmate 0.24.0 → 0.26.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/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +266 -266
- package/.next/app-path-routes-manifest.json +15 -15
- package/.next/build-manifest.json +5 -5
- package/.next/prerender-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +3 -3
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/app/update-check/route.js +1 -1
- package/.next/server/app/api/app/update-check/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/conversation/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/current-output/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/messages/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/session/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/start/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/terminal/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/assistant/tools/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/auth/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/daily-summary/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/daily-summary/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/external-apps/[id]/health/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/external-apps/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/external-apps/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/fs/browse/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/fs/recent-paths/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/hooks/agent-event/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/hooks/claude-done/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/hooks/permission-request/route.js +1 -1
- package/.next/server/app/api/hooks/permission-request/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/metrics/vibe/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/ollama/models/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/push/escalation/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/push/subscriptions/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/push/vapid/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/[id]/todos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/clone/[jobId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/clone/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/excluded/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/restore/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/scan/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/sync/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/repositories/validate-path/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sidebar/group-order/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/installations/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/operations/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/reindex/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/slash-commands/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[taskId]/cancel/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[taskId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/templates/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/templates/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/todos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/verification/runs/[runId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/verification/runs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/auto-yes/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/capture/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/cli-tool/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/cmate/schedules/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route.js +1 -1
- package/.next/server/app/api/worktrees/[id]/current-output/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/[logId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/execution-logs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/files/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/branch/create/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/branch/delete/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/branches/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/checkout/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/commit/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/diff/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/fetch/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/log/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/pull/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/push/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/reset/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/revert/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/show/[commitHash]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stage/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/staged/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/[index]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/apply/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/pop/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/push/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/stash/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/status/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/unstage/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/git/working-diff/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/interrupt/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/kill-session/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/[filename]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/logs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/marp-render/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/memos/[memoId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/memos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/messages/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/prompt-response/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/respond/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/[scheduleId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/active/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/schedules/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/search/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/send/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/git-workflow/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/install/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/plan/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/uninstall-plan/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/[skillId]/update-plan/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/skills/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/slash-commands/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/special-keys/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/start-polling/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tasks/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/terminal/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/timers/history/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/timers/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/todos/[todoId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/todos/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/tree/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/upload/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/runs/[runId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/verify/runs/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/[id]/viewed/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/worktrees/route_client-reference-manifest.js +1 -1
- package/.next/server/app/chat/page.js +2 -2
- package/.next/server/app/chat/page.js.nft.json +1 -1
- package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login/page.js +2 -2
- package/.next/server/app/login/page.js.nft.json +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
- package/.next/server/app/more/page.js +2 -2
- package/.next/server/app/more/page.js.nft.json +1 -1
- package/.next/server/app/more/page_client-reference-manifest.js +1 -1
- package/.next/server/app/offline/page_client-reference-manifest.js +1 -1
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/proxy/[...path]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/repositories/page.js +2 -2
- package/.next/server/app/repositories/page.js.nft.json +1 -1
- package/.next/server/app/repositories/page_client-reference-manifest.js +1 -1
- package/.next/server/app/review/page.js +1 -1
- package/.next/server/app/review/page.js.nft.json +1 -1
- package/.next/server/app/review/page_client-reference-manifest.js +1 -1
- package/.next/server/app/sessions/page.js +1 -1
- package/.next/server/app/sessions/page.js.nft.json +1 -1
- package/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/.next/server/app/skills/[skillId]/page.js +2 -2
- package/.next/server/app/skills/[skillId]/page.js.nft.json +1 -1
- package/.next/server/app/skills/[skillId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/skills/installed/page.js +2 -2
- package/.next/server/app/skills/installed/page.js.nft.json +1 -1
- package/.next/server/app/skills/installed/page_client-reference-manifest.js +1 -1
- package/.next/server/app/skills/page.js +2 -2
- package/.next/server/app/skills/page.js.nft.json +1 -1
- package/.next/server/app/skills/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page.js +2 -2
- package/.next/server/app/worktrees/[id]/files/[...path]/page.js.nft.json +1 -1
- package/.next/server/app/worktrees/[id]/files/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/page.js +6 -6
- package/.next/server/app/worktrees/[id]/page.js.nft.json +1 -1
- package/.next/server/app/worktrees/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/worktrees/[id]/terminal/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +15 -15
- package/.next/server/chunks/2119.js +1 -1
- package/.next/server/chunks/2404.js +1 -1
- package/.next/server/chunks/2445.js +1 -1
- package/.next/server/chunks/3241.js +1 -1
- package/.next/server/chunks/3346.js +1 -1
- package/.next/server/chunks/4455.js +1 -1
- package/.next/server/chunks/5164.js +1 -1
- package/.next/server/chunks/5571.js +1 -1
- package/.next/server/chunks/5959.js +4 -4
- package/.next/server/chunks/6130.js +1 -1
- package/.next/server/chunks/6274.js +12 -9
- package/.next/server/chunks/6285.js +1 -1
- package/.next/server/chunks/7458.js +6 -6
- package/.next/server/chunks/848.js +1 -1
- package/.next/server/chunks/8730.js +1 -0
- package/.next/server/chunks/880.js +1 -1
- package/.next/server/chunks/9328.js +1 -1
- package/.next/server/chunks/9373.js +4 -3
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-manifest.json +5 -5
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/2053-dfac2d49227cc942.js +1 -0
- package/.next/static/chunks/3898-ef488bfa804507d9.js +1 -0
- package/.next/static/chunks/5229-35f9acab97a553c5.js +1 -0
- package/.next/static/chunks/{6381-18a6a02a94d00d76.js → 6381-9303430933084b56.js} +1 -1
- package/.next/static/chunks/{9599-d29d2f8eeaf7c249.js → 8649-f551b0010dba4986.js} +1 -1
- package/.next/static/chunks/9263.f2034ec525abcf72.js +1 -0
- package/.next/static/chunks/app/chat/page-439b35c9cb90dbe0.js +1 -0
- package/.next/static/chunks/app/login/page-eefd515d8c37e5ee.js +1 -0
- package/.next/static/chunks/app/more/page-889b7546a8a553dc.js +1 -0
- package/.next/static/chunks/app/page-a8d04ac0e3e4c9fc.js +1 -0
- package/.next/static/chunks/app/repositories/page-f3c4d8839b1ca203.js +1 -0
- package/.next/static/chunks/app/review/page-6dc88649fe88ac16.js +1 -0
- package/.next/static/chunks/app/sessions/page-2c9ed9e969097688.js +1 -0
- package/.next/static/chunks/app/skills/[skillId]/page-19beef6121c4c982.js +1 -0
- package/.next/static/chunks/app/skills/installed/page-d5b9cef6d21fabda.js +1 -0
- package/.next/static/chunks/app/skills/page-06ef637e07dd9377.js +1 -0
- package/.next/static/chunks/app/worktrees/[id]/files/[...path]/{page-9ddcb224594f620f.js → page-5fed3984fafcd910.js} +1 -1
- package/.next/static/chunks/app/worktrees/[id]/page-fbdf4621a2911ac1.js +1 -0
- package/.next/static/chunks/{webpack-3534d858912a6c92.js → webpack-9d892a9ff07c289d.js} +1 -1
- package/.next/static/css/e865c4979a41aa03.css +1 -0
- package/.next/trace +12 -12
- package/README.md +96 -45
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +2 -1
- package/dist/cli/commands/verify.d.ts +14 -0
- package/dist/cli/commands/verify.d.ts.map +1 -1
- package/dist/cli/commands/verify.js +109 -3
- package/dist/cli/commands/wait.d.ts +2 -0
- package/dist/cli/commands/wait.d.ts.map +1 -1
- package/dist/cli/commands/wait.js +184 -4
- package/dist/cli/types/api-responses.d.ts +172 -0
- package/dist/cli/types/api-responses.d.ts.map +1 -1
- package/dist/cli/types/index.d.ts +31 -0
- package/dist/cli/types/index.d.ts.map +1 -1
- package/dist/cli/types/index.js +26 -0
- package/dist/cli/utils/verify-runner.d.ts +36 -1
- package/dist/cli/utils/verify-runner.d.ts.map +1 -1
- package/dist/cli/utils/verify-runner.js +114 -6
- package/dist/server/src/config/activity-bar-config.js +5 -1
- package/dist/server/src/lib/auto-yes-poller.js +40 -4
- package/dist/server/src/lib/auto-yes-state.js +108 -3
- package/dist/server/src/lib/cli-tools/antigravity.js +8 -4
- package/dist/server/src/lib/cli-tools/codex.js +63 -30
- package/dist/server/src/lib/cli-tools/copilot.js +5 -1
- package/dist/server/src/lib/cli-tools/gemini.js +15 -15
- package/dist/server/src/lib/cli-tools/opencode.js +9 -3
- package/dist/server/src/lib/detection/cli-patterns.js +148 -6
- package/dist/server/src/lib/detection/status-detector.js +28 -0
- package/dist/server/src/lib/detection/upstream-faults.js +120 -0
- package/dist/server/src/lib/hooks/permission-decision-service.js +13 -5
- package/dist/server/src/lib/hooks/sources/antigravity/hooks-config.js +18 -11
- package/dist/server/src/lib/hooks/sources/antigravity/source.js +10 -9
- package/dist/server/src/lib/hooks/sources/claude/source.js +5 -2
- package/dist/server/src/lib/hooks/sources/codex/hooks-config.js +26 -18
- package/dist/server/src/lib/hooks/sources/codex/source.js +6 -9
- package/dist/server/src/lib/hooks/sources/copilot/hook-settings.js +22 -12
- package/dist/server/src/lib/hooks/sources/copilot/source.js +1 -1
- package/dist/server/src/lib/hooks/sources/define-source.js +6 -4
- package/dist/server/src/lib/hooks/sources/gemini/settings-generator.js +14 -8
- package/dist/server/src/lib/hooks/sources/gemini/source.js +29 -29
- package/dist/server/src/lib/hooks/sources/index.js +4 -1
- package/dist/server/src/lib/hooks/sources/launch-command.js +61 -0
- package/dist/server/src/lib/hooks/sources/legacy-relay.js +2 -1
- package/dist/server/src/lib/hooks/sources/opencode/source.js +10 -7
- package/dist/server/src/lib/polling/prompt-dedup-state.js +95 -0
- package/dist/server/src/lib/polling/response-checker.js +12 -0
- package/dist/server/src/lib/polling/response-poller-core.js +11 -1
- package/dist/server/src/lib/session/agent-session-lifecycle.js +28 -7
- package/dist/server/src/lib/session/claude-session.js +5 -1
- package/dist/server/src/lib/session/current-output-builder.js +34 -0
- package/dist/server/src/lib/verification/env-snapshot.js +21 -3
- package/dist/server/src/lib/verification/gate-runner.js +258 -7
- package/dist/server/src/lib/verification/machine-lock.js +204 -0
- package/dist/server/src/lib/verification/scope-gate.js +126 -13
- package/dist/server/src/lib/verification/verify-config.js +85 -3
- package/dist/server/src/lib/verification/worktree-index.js +140 -0
- package/package.json +1 -1
- package/.next/server/chunks/3451.js +0 -1
- package/.next/static/chunks/2053-52d3c82014e1cd54.js +0 -1
- package/.next/static/chunks/5229-0610b4e3288e777d.js +0 -1
- package/.next/static/chunks/744-3ad06d8c8ca482fc.js +0 -1
- package/.next/static/chunks/9263.5259f73f89220d64.js +0 -1
- package/.next/static/chunks/app/chat/page-15d13fc024d78f5d.js +0 -1
- package/.next/static/chunks/app/login/page-0247a3fb569bb22b.js +0 -1
- package/.next/static/chunks/app/more/page-b8ba537baeac8d5b.js +0 -1
- package/.next/static/chunks/app/page-afc64708362f31e5.js +0 -1
- package/.next/static/chunks/app/repositories/page-3469dacb6be7842f.js +0 -1
- package/.next/static/chunks/app/review/page-2d9cb7e068f8f35a.js +0 -1
- package/.next/static/chunks/app/sessions/page-7470c2bfd89e0df9.js +0 -1
- package/.next/static/chunks/app/skills/[skillId]/page-5e24481162f7ea78.js +0 -1
- package/.next/static/chunks/app/skills/installed/page-efd2d710a27f9735.js +0 -1
- package/.next/static/chunks/app/skills/page-0498adfc6e48684b.js +0 -1
- package/.next/static/chunks/app/worktrees/[id]/page-e611d30e47989e5d.js +0 -1
- package/.next/static/css/3ac44cfc082d02ad.css +0 -1
- /package/.next/static/{smu4mnL7qMf8w38n9dQJE → hNdNiFes2xLls68SWhAU9}/_buildManifest.js +0 -0
- /package/.next/static/{smu4mnL7qMf8w38n9dQJE → hNdNiFes2xLls68SWhAU9}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Machine-wide advisory lock for verification gates (Issue #1771).
|
|
4
|
+
*
|
|
5
|
+
* A gate that owns a fixed resource — a hard-coded port, a local database, an
|
|
6
|
+
* emulator — can only run once per machine. Two parallel worktrees running it
|
|
7
|
+
* at the same time make the second one fail on the resource, and the record
|
|
8
|
+
* says `GATE e2e FAIL exit=1`, which is indistinguishable from the change being
|
|
9
|
+
* broken. `mutex: <name>` in verify.yaml declares that constraint, and this
|
|
10
|
+
* module is what enforces it.
|
|
11
|
+
*
|
|
12
|
+
* Three properties decide the implementation:
|
|
13
|
+
*
|
|
14
|
+
* 1. **It must hold across processes.** The lock exists because CommandMate's
|
|
15
|
+
* runner and the standalone `verify-run.sh` can be started independently
|
|
16
|
+
* against the same machine, so an in-process mutex would not be a lock at
|
|
17
|
+
* all. The path convention (`~/.commandmate/locks/<name>.lock`) is part of
|
|
18
|
+
* the contract, not an implementation detail — see
|
|
19
|
+
* docs/design/verification-config.md §9.
|
|
20
|
+
* 2. **It must be portable.** macOS ships no `flock(1)`, so the primitive is
|
|
21
|
+
* `mkdir`, which is atomic on every POSIX filesystem and expressible in
|
|
22
|
+
* both bash and Node.
|
|
23
|
+
* 3. **A crashed holder must not wedge the machine.** The owner record names
|
|
24
|
+
* the pid and host, so a lock left behind by a process that no longer
|
|
25
|
+
* exists is broken by the next waiter rather than blocking every future
|
|
26
|
+
* run until someone deletes a directory by hand.
|
|
27
|
+
*
|
|
28
|
+
* Server-only: reads and writes the filesystem.
|
|
29
|
+
*
|
|
30
|
+
* @module lib/verification/machine-lock
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.DEFAULT_LOCK_POLL_INTERVAL_MS = exports.MACHINE_LOCK_ROOT_ENV = exports.MACHINE_LOCK_DIR_NAME = void 0;
|
|
34
|
+
exports.machineLockPath = machineLockPath;
|
|
35
|
+
exports.defaultMachineLockRoot = defaultMachineLockRoot;
|
|
36
|
+
exports.resolveMachineLockRoot = resolveMachineLockRoot;
|
|
37
|
+
exports.acquireMachineLock = acquireMachineLock;
|
|
38
|
+
const fs_1 = require("fs");
|
|
39
|
+
const os_1 = require("os");
|
|
40
|
+
const path_1 = require("path");
|
|
41
|
+
const crypto_1 = require("crypto");
|
|
42
|
+
const logger_1 = require("../../lib/logger");
|
|
43
|
+
const logger = (0, logger_1.createLogger)('lib/verification/machine-lock');
|
|
44
|
+
/**
|
|
45
|
+
* Directory under `~/.commandmate` that holds the locks.
|
|
46
|
+
*
|
|
47
|
+
* Exported because `env-snapshot` has to know this name: the env-clean gate
|
|
48
|
+
* lists the entries directly under `~/.commandmate` and would otherwise report
|
|
49
|
+
* the runner's own bookkeeping as pollution the delegation left behind.
|
|
50
|
+
*/
|
|
51
|
+
exports.MACHINE_LOCK_DIR_NAME = 'locks';
|
|
52
|
+
/**
|
|
53
|
+
* Override for the lock root.
|
|
54
|
+
*
|
|
55
|
+
* Tests must never take a lock under the real `~/.commandmate/locks`: parallel
|
|
56
|
+
* worktrees on a developer machine share that directory, so a suite that used
|
|
57
|
+
* it would serialize against — and be failed by — unrelated live runs. Also
|
|
58
|
+
* useful for isolating a CI runner from a shared home.
|
|
59
|
+
*/
|
|
60
|
+
exports.MACHINE_LOCK_ROOT_ENV = 'CM_VERIFY_LOCK_ROOT';
|
|
61
|
+
/** How often a waiter re-tries the claim. */
|
|
62
|
+
exports.DEFAULT_LOCK_POLL_INTERVAL_MS = 250;
|
|
63
|
+
/** File inside the lock directory naming who holds it. */
|
|
64
|
+
const OWNER_FILE = 'owner';
|
|
65
|
+
/** The convention both runners follow: `<root>/<name>.lock`. */
|
|
66
|
+
function machineLockPath(name, root) {
|
|
67
|
+
return (0, path_1.join)(root, `${name}.lock`);
|
|
68
|
+
}
|
|
69
|
+
function defaultMachineLockRoot() {
|
|
70
|
+
return (0, path_1.join)((0, os_1.homedir)(), '.commandmate', exports.MACHINE_LOCK_DIR_NAME);
|
|
71
|
+
}
|
|
72
|
+
function resolveMachineLockRoot(env = process.env) {
|
|
73
|
+
const override = env[exports.MACHINE_LOCK_ROOT_ENV];
|
|
74
|
+
return override && override.trim() !== '' ? override : defaultMachineLockRoot();
|
|
75
|
+
}
|
|
76
|
+
function sleep(ms) {
|
|
77
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
78
|
+
}
|
|
79
|
+
function readOwner(lockPath) {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(lockPath, OWNER_FILE), 'utf8'));
|
|
82
|
+
if (typeof parsed.pid !== 'number' || typeof parsed.token !== 'string')
|
|
83
|
+
return null;
|
|
84
|
+
return {
|
|
85
|
+
pid: parsed.pid,
|
|
86
|
+
host: typeof parsed.host === 'string' ? parsed.host : '',
|
|
87
|
+
token: parsed.token,
|
|
88
|
+
acquiredAt: typeof parsed.acquiredAt === 'number' ? parsed.acquiredAt : 0,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Either the holder has not written the record yet (it created the
|
|
93
|
+
// directory microseconds ago) or the file is corrupt. Both mean "held by
|
|
94
|
+
// someone we cannot identify", which is a reason to wait, never to break.
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function describeOwner(owner) {
|
|
99
|
+
return owner ? `pid ${owner.pid} on ${owner.host || 'unknown host'}` : null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Whether the recorded holder is gone.
|
|
103
|
+
*
|
|
104
|
+
* Only decidable on the machine that wrote the record: a pid from another host
|
|
105
|
+
* says nothing about a process here, and killing a lock on that guess would let
|
|
106
|
+
* two machines sharing a network home run the gate at once. Pid reuse can make
|
|
107
|
+
* a dead holder look alive, which only costs a wait — the safe direction.
|
|
108
|
+
*/
|
|
109
|
+
function isStaleOwner(owner) {
|
|
110
|
+
if (owner.host !== (0, os_1.hostname)())
|
|
111
|
+
return false;
|
|
112
|
+
try {
|
|
113
|
+
process.kill(owner.pid, 0);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
return error.code === 'ESRCH';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** @returns true when this call created the directory and now owns it */
|
|
121
|
+
function claim(lockPath, owner) {
|
|
122
|
+
try {
|
|
123
|
+
(0, fs_1.mkdirSync)(lockPath);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error.code === 'EEXIST')
|
|
127
|
+
return false;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(lockPath, OWNER_FILE), JSON.stringify(owner), { encoding: 'utf8' });
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
/** Remove a lock whose recorded holder is gone, but only if it is still that record. */
|
|
134
|
+
function breakStale(lockPath, owner) {
|
|
135
|
+
const current = readOwner(lockPath);
|
|
136
|
+
if (!current || current.token !== owner.token)
|
|
137
|
+
return;
|
|
138
|
+
try {
|
|
139
|
+
(0, fs_1.rmSync)(lockPath, { recursive: true, force: true });
|
|
140
|
+
logger.warn('machine-lock-broken-stale', { lockPath, pid: owner.pid });
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Someone else broke or re-took it first; the next attempt sorts it out.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Take the machine-wide lock named `name`, waiting up to `timeoutMs`.
|
|
148
|
+
*
|
|
149
|
+
* Never throws for the ordinary "someone else has it" case: the caller has to
|
|
150
|
+
* report *not acquired* differently from *the gate failed*, and an exception
|
|
151
|
+
* would collapse the two.
|
|
152
|
+
*/
|
|
153
|
+
async function acquireMachineLock(name, options) {
|
|
154
|
+
const root = options.root ?? resolveMachineLockRoot();
|
|
155
|
+
const pollIntervalMs = options.pollIntervalMs ?? exports.DEFAULT_LOCK_POLL_INTERVAL_MS;
|
|
156
|
+
const lockPath = machineLockPath(name, root);
|
|
157
|
+
const startedAt = Date.now();
|
|
158
|
+
const deadline = startedAt + Math.max(0, options.timeoutMs);
|
|
159
|
+
const token = (0, crypto_1.randomUUID)();
|
|
160
|
+
(0, fs_1.mkdirSync)(root, { recursive: true });
|
|
161
|
+
// A stale record is broken at most once per token, so a directory nobody can
|
|
162
|
+
// remove degrades into ordinary waiting instead of a spin loop.
|
|
163
|
+
const broken = new Set();
|
|
164
|
+
for (;;) {
|
|
165
|
+
const owner = { pid: process.pid, host: (0, os_1.hostname)(), token, acquiredAt: Date.now() };
|
|
166
|
+
if (claim(lockPath, owner)) {
|
|
167
|
+
return {
|
|
168
|
+
acquired: true,
|
|
169
|
+
waitedMs: Date.now() - startedAt,
|
|
170
|
+
handle: {
|
|
171
|
+
name,
|
|
172
|
+
path: lockPath,
|
|
173
|
+
release: () => {
|
|
174
|
+
const current = readOwner(lockPath);
|
|
175
|
+
// Ours was broken and re-taken while we ran. Deleting it now would
|
|
176
|
+
// evict a live holder.
|
|
177
|
+
if (current && current.token !== token)
|
|
178
|
+
return;
|
|
179
|
+
try {
|
|
180
|
+
(0, fs_1.rmSync)(lockPath, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
logger.warn('machine-lock-release-failed', {
|
|
184
|
+
lockPath,
|
|
185
|
+
error: error instanceof Error ? error.message : String(error),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const holder = readOwner(lockPath);
|
|
193
|
+
if (holder && !broken.has(holder.token) && isStaleOwner(holder)) {
|
|
194
|
+
broken.add(holder.token);
|
|
195
|
+
breakStale(lockPath, holder);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
if (now >= deadline) {
|
|
200
|
+
return { acquired: false, waitedMs: now - startedAt, heldBy: describeOwner(holder) };
|
|
201
|
+
}
|
|
202
|
+
await sleep(Math.min(pollIntervalMs, deadline - now));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* @module lib/verification/scope-gate
|
|
31
31
|
*/
|
|
32
32
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
-
exports.SCOPE_SKIP_NOT_REQUIRED = exports.SCOPE_SKIP_NO_CONTRACT = exports.ScopeMatcher = exports.CONTRACT_DIR_PREFIX = exports.ALWAYS_ALLOWED_PREFIX = exports.SCOPE_ALLOW_GUIDANCE = exports.MAX_REPORTED_VIOLATIONS = void 0;
|
|
33
|
+
exports.SCOPE_SKIP_NOT_REQUIRED = exports.SCOPE_SKIP_NO_CONTRACT = exports.ScopeMatcher = exports.CONTRACT_DIR_PREFIX = exports.SCOPE_EXEMPT_CONTRACT_PATH = exports.SCOPE_EXEMPT_ALWAYS_ALLOWED = exports.SCOPE_PATTERN_ARROW = exports.ALWAYS_ALLOWED_PREFIX = exports.SCOPE_ALLOW_GUIDANCE = exports.MAX_REPORTED_VIOLATIONS = void 0;
|
|
34
34
|
exports.isContractPath = isContractPath;
|
|
35
35
|
exports.parsePorcelainEntries = parsePorcelainEntries;
|
|
36
36
|
exports.collectChangedPaths = collectChangedPaths;
|
|
@@ -71,6 +71,31 @@ exports.SCOPE_ALLOW_GUIDANCE = "To allow this diff, add the paths above to the c
|
|
|
71
71
|
* this exemption prevents.
|
|
72
72
|
*/
|
|
73
73
|
exports.ALWAYS_ALLOWED_PREFIX = '.commandmate/';
|
|
74
|
+
/**
|
|
75
|
+
* Separator between a reported path and the pattern that decided it (#1841).
|
|
76
|
+
*
|
|
77
|
+
* Mirrored by the CLI's scope-evidence parser (`src/cli/commands/verify.ts`,
|
|
78
|
+
* SCOPE_PATTERN_ARROW): the gate's report is the only carrier for this evidence
|
|
79
|
+
* — `verification_gate_results` has columns for a status, an exit code and a log
|
|
80
|
+
* body, and nothing else — so the two constants have to agree. A round-trip
|
|
81
|
+
* test feeds a real `evaluateScope` report through that parser rather than
|
|
82
|
+
* trusting the two copies to be edited together.
|
|
83
|
+
*
|
|
84
|
+
* A path containing this exact three-character sequence would split wrongly on
|
|
85
|
+
* the way back. Nothing prevents such a filename; it is accepted because the
|
|
86
|
+
* alternative — encoding paths in a log a human reads first — costs every
|
|
87
|
+
* reader something to protect against a file no repository has.
|
|
88
|
+
*/
|
|
89
|
+
exports.SCOPE_PATTERN_ARROW = ' \u2190 ';
|
|
90
|
+
/**
|
|
91
|
+
* Stand-ins for the two ways a path is admitted without matching `allow`.
|
|
92
|
+
*
|
|
93
|
+
* Parenthesised so they cannot be mistaken for a pattern the contract declared:
|
|
94
|
+
* a reader who greps the contract for `(exempt: .commandmate/)` should find
|
|
95
|
+
* nothing, because nothing there put it in scope.
|
|
96
|
+
*/
|
|
97
|
+
exports.SCOPE_EXEMPT_ALWAYS_ALLOWED = `(exempt: ${exports.ALWAYS_ALLOWED_PREFIX})`;
|
|
98
|
+
exports.SCOPE_EXEMPT_CONTRACT_PATH = '(exempt: contract path)';
|
|
74
99
|
/**
|
|
75
100
|
* Execution contracts, dropped from both gates' change sets entirely (#1580).
|
|
76
101
|
*
|
|
@@ -189,6 +214,9 @@ function hasBalancedBraces(pattern) {
|
|
|
189
214
|
}
|
|
190
215
|
return depth === 0;
|
|
191
216
|
}
|
|
217
|
+
function compilePattern(pattern) {
|
|
218
|
+
return { pattern, regexp: globToRegExp(pattern) };
|
|
219
|
+
}
|
|
192
220
|
/**
|
|
193
221
|
* A scope declaration compiled once per run.
|
|
194
222
|
*
|
|
@@ -205,20 +233,43 @@ class ScopeMatcher {
|
|
|
205
233
|
*/
|
|
206
234
|
constructor(scope, contractPath = null) {
|
|
207
235
|
this.contractPath = contractPath;
|
|
208
|
-
this.allow = scope.allow.map(
|
|
209
|
-
this.deny = scope.deny.map(
|
|
236
|
+
this.allow = scope.allow.map(compilePattern);
|
|
237
|
+
this.deny = scope.deny.map(compilePattern);
|
|
210
238
|
}
|
|
211
239
|
/**
|
|
212
|
-
*
|
|
240
|
+
* Which declared rule decided `path`, and which way (#1841).
|
|
241
|
+
*
|
|
242
|
+
* The **first** matching pattern is reported, in the contract's declaration
|
|
243
|
+
* order, on both sides. Reporting the last would name a rule the reader
|
|
244
|
+
* cannot use: `allow: ["src/**", "src/lib/**"]` admits `src/lib/a.ts` under
|
|
245
|
+
* the first entry, and deleting the second changes nothing about the verdict.
|
|
213
246
|
*
|
|
214
247
|
* `path` is a repository-root-relative POSIX path as git reports it.
|
|
215
248
|
*/
|
|
249
|
+
classify(path) {
|
|
250
|
+
const denied = this.deny.find((entry) => entry.regexp.test(path));
|
|
251
|
+
if (denied)
|
|
252
|
+
return { admitted: false, pattern: denied.pattern };
|
|
253
|
+
if (path.startsWith(exports.ALWAYS_ALLOWED_PREFIX)) {
|
|
254
|
+
return { admitted: true, pattern: exports.SCOPE_EXEMPT_ALWAYS_ALLOWED };
|
|
255
|
+
}
|
|
256
|
+
if (path === this.contractPath) {
|
|
257
|
+
return { admitted: true, pattern: exports.SCOPE_EXEMPT_CONTRACT_PATH };
|
|
258
|
+
}
|
|
259
|
+
const allowed = this.allow.find((entry) => entry.regexp.test(path));
|
|
260
|
+
return allowed
|
|
261
|
+
? { admitted: true, pattern: allowed.pattern }
|
|
262
|
+
: { admitted: false, pattern: null };
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Whether `path` is a scope violation.
|
|
266
|
+
*
|
|
267
|
+
* Delegates to {@link classify} rather than repeating the rules: the verdict
|
|
268
|
+
* and the evidence for it must never be able to disagree, and two copies of
|
|
269
|
+
* "deny wins, then the exemptions, then allow" is exactly how they would.
|
|
270
|
+
*/
|
|
216
271
|
isViolation(path) {
|
|
217
|
-
|
|
218
|
-
return true;
|
|
219
|
-
if (path.startsWith(exports.ALWAYS_ALLOWED_PREFIX) || path === this.contractPath)
|
|
220
|
-
return false;
|
|
221
|
-
return !this.allow.some((pattern) => pattern.test(path));
|
|
272
|
+
return !this.classify(path).admitted;
|
|
222
273
|
}
|
|
223
274
|
}
|
|
224
275
|
exports.ScopeMatcher = ScopeMatcher;
|
|
@@ -328,6 +379,45 @@ async function collectChangedPaths(worktreePath, baseRef) {
|
|
|
328
379
|
function formatPatterns(patterns) {
|
|
329
380
|
return patterns.length > 0 ? patterns.join(', ') : '(none)';
|
|
330
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* One reported path, optionally naming the rule that decided it (#1841).
|
|
384
|
+
*
|
|
385
|
+
* Admitted paths lead with `+` rather than `-` so the two sections cannot be
|
|
386
|
+
* confused by a reader — or by a grep — that has only one line in front of it.
|
|
387
|
+
*/
|
|
388
|
+
function formatDecidedPath(marker, path, pattern) {
|
|
389
|
+
if (pattern === null)
|
|
390
|
+
return ` ${marker} ${path}`;
|
|
391
|
+
return ` ${marker} ${path}${exports.SCOPE_PATTERN_ARROW}${pattern}`;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* The `admitted:` section: what this run was allowed to change, and by which
|
|
395
|
+
* rule (#1841).
|
|
396
|
+
*
|
|
397
|
+
* With an `allow` of exact paths the pattern *is* the file and this adds
|
|
398
|
+
* nothing, but #1546 made globs legal, and `src/**` leaves no record of what it
|
|
399
|
+
* actually covered on the day it ran. The contract is the claim; this is the
|
|
400
|
+
* evidence for it, and it has to survive in `log_tail` because the run's own
|
|
401
|
+
* working tree will have moved on by the time anyone asks.
|
|
402
|
+
*
|
|
403
|
+
* Capped by the same rule as the violations, for the same reason (log_tail is
|
|
404
|
+
* stored per gate in SQLite), and the truncation is stated rather than silent —
|
|
405
|
+
* a list that stops at 100 with no marker reads as a complete one.
|
|
406
|
+
*/
|
|
407
|
+
function formatAdmitted(admitted) {
|
|
408
|
+
if (admitted.length === 0)
|
|
409
|
+
return [];
|
|
410
|
+
const listed = admitted.slice(0, exports.MAX_REPORTED_VIOLATIONS);
|
|
411
|
+
const remainder = admitted.length - listed.length;
|
|
412
|
+
return [
|
|
413
|
+
'admitted:',
|
|
414
|
+
...listed.map((entry) => formatDecidedPath('+', entry.path, entry.pattern)),
|
|
415
|
+
// Worded `(+N more)` rather than the violations' `... and N more`: the two
|
|
416
|
+
// sections are capped independently and a reader scanning a truncated
|
|
417
|
+
// report should not have to work out which count belongs to which list.
|
|
418
|
+
...(remainder > 0 ? [` ... (+${remainder} more)`] : []),
|
|
419
|
+
];
|
|
420
|
+
}
|
|
331
421
|
/**
|
|
332
422
|
* Reasons the gate has nothing to judge, phrased for `log_tail`.
|
|
333
423
|
*
|
|
@@ -381,9 +471,28 @@ async function evaluateScope(worktreePath, scope, requireScopeClean, baseRef, co
|
|
|
381
471
|
if ('error' in changed)
|
|
382
472
|
return done('error', `scope: ${changed.error}`, null);
|
|
383
473
|
const matcher = new ScopeMatcher(scope, contractPath);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
474
|
+
// Every changed path is classified, whether or not it will fit in the report:
|
|
475
|
+
// the cap below is a display rule, and a violation withheld from the listing
|
|
476
|
+
// still counts toward the verdict.
|
|
477
|
+
const admitted = [];
|
|
478
|
+
const violations = [];
|
|
479
|
+
const rejectedBy = new Map();
|
|
480
|
+
for (const path of changed.paths) {
|
|
481
|
+
const decision = matcher.classify(path);
|
|
482
|
+
if (decision.admitted) {
|
|
483
|
+
admitted.push({ path, pattern: decision.pattern });
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
violations.push(path);
|
|
487
|
+
if (decision.pattern !== null)
|
|
488
|
+
rejectedBy.set(path, decision.pattern);
|
|
489
|
+
}
|
|
490
|
+
const summary = [
|
|
491
|
+
`scope: baseRef=${baseRef} changed=${changed.paths.length} violations=${violations.length}`,
|
|
492
|
+
`allow: ${formatPatterns(scope.allow)}`,
|
|
493
|
+
`deny: ${formatPatterns(scope.deny)}`,
|
|
494
|
+
...formatAdmitted(admitted),
|
|
495
|
+
].join('\n');
|
|
387
496
|
if (violations.length === 0) {
|
|
388
497
|
// An empty change set passes: work-evidence is the gate that judges "nothing
|
|
389
498
|
// happened", and failing here too would report one problem as two.
|
|
@@ -394,7 +503,11 @@ async function evaluateScope(worktreePath, scope, requireScopeClean, baseRef, co
|
|
|
394
503
|
const report = [
|
|
395
504
|
summary,
|
|
396
505
|
'out of scope:',
|
|
397
|
-
|
|
506
|
+
// A deny match is named inline: the `deny:` line above lists what the
|
|
507
|
+
// contract declared, not which of those entries this path tripped, and a
|
|
508
|
+
// violation the reader must revert reads differently from one they could
|
|
509
|
+
// fix by widening `allow`.
|
|
510
|
+
...listed.map((path) => formatDecidedPath('-', path, rejectedBy.get(path) ?? null)),
|
|
398
511
|
...(remainder > 0 ? [` ... and ${remainder} more`] : []),
|
|
399
512
|
exports.SCOPE_ALLOW_GUIDANCE,
|
|
400
513
|
].join('\n');
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Server-only: reads from disk, so import this from API routes / CLI only.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.VerifyConfigError = exports.GATE_ID_PATTERN = exports.DEFAULT_MAX_LOG_TAIL_BYTES = exports.DEFAULT_TIMEOUT_SEC = exports.VERIFY_CONFIG_RELATIVE_PATH = exports.RESERVED_GATE_IDS = exports.ENV_CLEAN_GATE_ID = exports.SCOPE_GATE_ID = exports.WORK_EVIDENCE_GATE_ID = void 0;
|
|
11
|
+
exports.VerifyConfigError = exports.MAX_RETRY_ON_FAIL = exports.MAX_GATE_MUTEX_LENGTH = exports.GATE_MUTEX_PATTERN = exports.GATE_ID_PATTERN = exports.DEFAULT_MAX_LOG_TAIL_BYTES = exports.DEFAULT_TIMEOUT_SEC = exports.VERIFY_CONFIG_RELATIVE_PATH = exports.RESERVED_GATE_IDS = exports.ENV_CLEAN_GATE_ID = exports.SCOPE_GATE_ID = exports.WORK_EVIDENCE_GATE_ID = void 0;
|
|
12
12
|
exports.validateGateEntries = validateGateEntries;
|
|
13
13
|
exports.loadVerifyConfig = loadVerifyConfig;
|
|
14
14
|
const fs_1 = require("fs");
|
|
@@ -56,8 +56,29 @@ const MAX_LOG_TAIL_BYTES = 1048576;
|
|
|
56
56
|
* runner refuses to resolve is a completion criterion nothing can evaluate.
|
|
57
57
|
*/
|
|
58
58
|
exports.GATE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
59
|
+
/**
|
|
60
|
+
* Shape a `mutex` name may take (Issue #1771).
|
|
61
|
+
*
|
|
62
|
+
* Wider than {@link GATE_ID_PATTERN} because a mutex names a *resource*, not a
|
|
63
|
+
* gate: two repositories that both bind port 60303 should be able to agree on
|
|
64
|
+
* `port.60303` and be serialized against each other. Narrow enough that the
|
|
65
|
+
* name is safe as a path segment — no separator, no whitespace, nothing a shell
|
|
66
|
+
* re-interprets — because both runners turn it into
|
|
67
|
+
* `~/.commandmate/locks/<name>.lock`.
|
|
68
|
+
*/
|
|
69
|
+
exports.GATE_MUTEX_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
70
|
+
/** Keeps the lock directory name inside every filesystem's limit. */
|
|
71
|
+
exports.MAX_GATE_MUTEX_LENGTH = 64;
|
|
72
|
+
/**
|
|
73
|
+
* Largest `retryOnFail` a gate may declare (Issue #1772).
|
|
74
|
+
*
|
|
75
|
+
* Deliberately 1, and deliberately a named constant rather than a literal in
|
|
76
|
+
* one `if`: the ceiling is the feature. A gate that may re-run until it passes
|
|
77
|
+
* reports the machine's luck, not the work.
|
|
78
|
+
*/
|
|
79
|
+
exports.MAX_RETRY_ON_FAIL = 1;
|
|
59
80
|
const TOP_LEVEL_KEYS = ['version', 'gates', 'options'];
|
|
60
|
-
const GATE_KEYS = ['id', 'command', 'timeoutSec'];
|
|
81
|
+
const GATE_KEYS = ['id', 'command', 'timeoutSec', 'mutex', 'retryOnFail', 'flakyIsPass'];
|
|
61
82
|
const OPTION_KEYS = [
|
|
62
83
|
'baseRef',
|
|
63
84
|
'skipInPrimaryCheckout',
|
|
@@ -186,8 +207,69 @@ function validateGateEntry(entry, at, seenIds, gates, issues) {
|
|
|
186
207
|
timeoutSec = parsed;
|
|
187
208
|
}
|
|
188
209
|
}
|
|
210
|
+
let mutex;
|
|
211
|
+
if (entry.mutex !== undefined) {
|
|
212
|
+
if (typeof entry.mutex !== 'string' || entry.mutex === '') {
|
|
213
|
+
issues.push(`${at}.mutex: must be a non-empty string (got ${describe(entry.mutex)})`);
|
|
214
|
+
}
|
|
215
|
+
else if (entry.mutex.length > exports.MAX_GATE_MUTEX_LENGTH) {
|
|
216
|
+
issues.push(`${at}.mutex: must be at most ${exports.MAX_GATE_MUTEX_LENGTH} characters (got ${entry.mutex.length})`);
|
|
217
|
+
}
|
|
218
|
+
else if (!exports.GATE_MUTEX_PATTERN.test(entry.mutex)) {
|
|
219
|
+
issues.push(`${at}.mutex: "${entry.mutex}" must match ${exports.GATE_MUTEX_PATTERN.source}`);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
mutex = entry.mutex;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
let retryOnFail;
|
|
226
|
+
if (entry.retryOnFail !== undefined) {
|
|
227
|
+
const parsed = asInteger(entry.retryOnFail);
|
|
228
|
+
if (parsed === null) {
|
|
229
|
+
issues.push(`${at}.retryOnFail: must be an integer (got ${describe(entry.retryOnFail)})`);
|
|
230
|
+
}
|
|
231
|
+
else if (parsed < 0 || parsed > exports.MAX_RETRY_ON_FAIL) {
|
|
232
|
+
// Not "at most N": the range is the contract. See MAX_RETRY_ON_FAIL.
|
|
233
|
+
issues.push(`${at}.retryOnFail: must be 0 or ${exports.MAX_RETRY_ON_FAIL} (got ${parsed})`);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
retryOnFail = parsed;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
let flakyIsPass;
|
|
240
|
+
if (entry.flakyIsPass !== undefined) {
|
|
241
|
+
const parsed = asBoolean(entry.flakyIsPass);
|
|
242
|
+
if (parsed === null) {
|
|
243
|
+
issues.push(`${at}.flakyIsPass: must be true or false (got ${describe(entry.flakyIsPass)})`);
|
|
244
|
+
}
|
|
245
|
+
else if (parsed && retryOnFail !== exports.MAX_RETRY_ON_FAIL) {
|
|
246
|
+
// A knob that can never fire is a config bug, not a preference: without a
|
|
247
|
+
// retry the gate has no FLAKY outcome for this to reclassify, so the
|
|
248
|
+
// declaration reads as "flakes are tolerated here" while changing nothing.
|
|
249
|
+
issues.push(`${at}.flakyIsPass: true requires retryOnFail: ${exports.MAX_RETRY_ON_FAIL} ` +
|
|
250
|
+
'(without a retry a gate can never be FLAKY)');
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
flakyIsPass = parsed;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
189
256
|
if (issues.length === issuesBefore) {
|
|
190
|
-
|
|
257
|
+
const gate = {
|
|
258
|
+
id: entry.id,
|
|
259
|
+
command: entry.command,
|
|
260
|
+
timeoutSec,
|
|
261
|
+
};
|
|
262
|
+
// Only set when declared: an explicit `mutex: undefined` would serialize
|
|
263
|
+
// into a contract's stored JSON as a key, and "declared without a value"
|
|
264
|
+
// is not a state this field has. The same holds for the #1772 pair, whose
|
|
265
|
+
// absence has to keep meaning "this gate runs exactly once".
|
|
266
|
+
if (mutex !== undefined)
|
|
267
|
+
gate.mutex = mutex;
|
|
268
|
+
if (retryOnFail !== undefined)
|
|
269
|
+
gate.retryOnFail = retryOnFail;
|
|
270
|
+
if (flakyIsPass !== undefined)
|
|
271
|
+
gate.flakyIsPass = flakyIsPass;
|
|
272
|
+
gates.push(gate);
|
|
191
273
|
}
|
|
192
274
|
}
|
|
193
275
|
function validateGates(value, issues) {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A small, stable integer per worktree, handed to gates as `CM_WORKTREE_INDEX`
|
|
4
|
+
* (Issue #1771).
|
|
5
|
+
*
|
|
6
|
+
* The point is to let a repository *remove* the collision rather than serialize
|
|
7
|
+
* around it: a gate written as `E2E_PORT=$((60400+CM_WORKTREE_INDEX)) npm run
|
|
8
|
+
* test:e2e` gives every worktree its own port, so N parallel worktrees keep N-way
|
|
9
|
+
* parallelism. `mutex:` is the fallback for resources that cannot be
|
|
10
|
+
* parameterized at all; this is the one that preserves throughput.
|
|
11
|
+
*
|
|
12
|
+
* **CommandMate does not number worktrees.** The Issue assumed it did; it does
|
|
13
|
+
* not. `worktrees.id` is a TEXT primary key derived from the directory basename
|
|
14
|
+
* (`src/lib/git/worktree-id.ts:73`), the table has no ordinal column and no
|
|
15
|
+
* creation timestamp (`src/lib/db/migrations/v01-v05-initial-schema.ts:104`),
|
|
16
|
+
* and the only ordering any query applies is `updated_at DESC`
|
|
17
|
+
* (`src/lib/db/worktree-db.ts:113`), which reorders itself every time an agent
|
|
18
|
+
* writes a message. So the number is minted here, and the requirement it has to
|
|
19
|
+
* meet is stated rather than inherited:
|
|
20
|
+
*
|
|
21
|
+
* - **Same worktree, same number.** The assignment is persisted, so it
|
|
22
|
+
* survives restarts, re-registration and database resets.
|
|
23
|
+
* - **No two live worktrees share a number.** The claim is an `O_EXCL` file
|
|
24
|
+
* create, which is atomic, so two processes racing for the same slot cannot
|
|
25
|
+
* both win it.
|
|
26
|
+
*
|
|
27
|
+
* A hash of the worktree id would have satisfied only the first: 30 worktrees
|
|
28
|
+
* over 1024 slots collide about 35% of the time, and a collision here is exactly
|
|
29
|
+
* the port clash the feature exists to remove. It is kept only as the fallback
|
|
30
|
+
* for a machine where the registry cannot be written at all, because a gate
|
|
31
|
+
* expanding `$((60400+CM_WORKTREE_INDEX))` with the variable unset would give
|
|
32
|
+
* *every* worktree port 60400.
|
|
33
|
+
*
|
|
34
|
+
* Layout: `~/.commandmate/worktree-index/<n>` is a file whose content is the
|
|
35
|
+
* worktree id that owns `n`. Deleting a worktree leaves its slot claimed, which
|
|
36
|
+
* is deliberate — reusing it would move a live worktree's number.
|
|
37
|
+
*
|
|
38
|
+
* Server-only: reads and writes the filesystem.
|
|
39
|
+
*
|
|
40
|
+
* @module lib/verification/worktree-index
|
|
41
|
+
*/
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.MAX_WORKTREE_INDEX = exports.WORKTREE_INDEX_ROOT_ENV = exports.WORKTREE_INDEX_DIR_NAME = void 0;
|
|
44
|
+
exports.defaultWorktreeIndexRoot = defaultWorktreeIndexRoot;
|
|
45
|
+
exports.resolveWorktreeIndexRoot = resolveWorktreeIndexRoot;
|
|
46
|
+
exports.hashWorktreeIndex = hashWorktreeIndex;
|
|
47
|
+
exports.resolveWorktreeIndex = resolveWorktreeIndex;
|
|
48
|
+
const crypto_1 = require("crypto");
|
|
49
|
+
const fs_1 = require("fs");
|
|
50
|
+
const os_1 = require("os");
|
|
51
|
+
const path_1 = require("path");
|
|
52
|
+
const logger_1 = require("../../lib/logger");
|
|
53
|
+
const logger = (0, logger_1.createLogger)('lib/verification/worktree-index');
|
|
54
|
+
/**
|
|
55
|
+
* Directory under `~/.commandmate` holding the registry.
|
|
56
|
+
*
|
|
57
|
+
* Exported for the same reason as {@link MACHINE_LOCK_DIR_NAME}: env-clean
|
|
58
|
+
* lists `~/.commandmate` and must not read the runner's own bookkeeping as
|
|
59
|
+
* something the delegation left behind.
|
|
60
|
+
*/
|
|
61
|
+
exports.WORKTREE_INDEX_DIR_NAME = 'worktree-index';
|
|
62
|
+
/** Override for the registry root; tests and isolated runners use it. */
|
|
63
|
+
exports.WORKTREE_INDEX_ROOT_ENV = 'CM_VERIFY_WORKTREE_INDEX_ROOT';
|
|
64
|
+
/**
|
|
65
|
+
* Highest index the registry hands out.
|
|
66
|
+
*
|
|
67
|
+
* Also the modulus of the fallback hash, so both paths produce a number a
|
|
68
|
+
* repository can add to a port base without leaving the ephemeral range.
|
|
69
|
+
*/
|
|
70
|
+
exports.MAX_WORKTREE_INDEX = 1024;
|
|
71
|
+
function defaultWorktreeIndexRoot() {
|
|
72
|
+
return (0, path_1.join)((0, os_1.homedir)(), '.commandmate', exports.WORKTREE_INDEX_DIR_NAME);
|
|
73
|
+
}
|
|
74
|
+
function resolveWorktreeIndexRoot(env = process.env) {
|
|
75
|
+
const override = env[exports.WORKTREE_INDEX_ROOT_ENV];
|
|
76
|
+
return override && override.trim() !== '' ? override : defaultWorktreeIndexRoot();
|
|
77
|
+
}
|
|
78
|
+
/** Last-resort number for a machine whose registry cannot be written. */
|
|
79
|
+
function hashWorktreeIndex(worktreeId) {
|
|
80
|
+
const digest = (0, crypto_1.createHash)('sha256').update(worktreeId).digest('hex').slice(0, 8);
|
|
81
|
+
return Number.parseInt(digest, 16) % exports.MAX_WORKTREE_INDEX;
|
|
82
|
+
}
|
|
83
|
+
function readSlotOwner(root, slot) {
|
|
84
|
+
try {
|
|
85
|
+
return (0, fs_1.readFileSync)((0, path_1.join)(root, slot), 'utf8').trim();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function claimSlot(root, index, worktreeId) {
|
|
92
|
+
try {
|
|
93
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(root, String(index)), `${worktreeId}\n`, { encoding: 'utf8', flag: 'wx' });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error.code !== 'EEXIST')
|
|
98
|
+
throw error;
|
|
99
|
+
// Lost the race — unless the winner is us, which happens when two runners
|
|
100
|
+
// for the same worktree start together on different processes.
|
|
101
|
+
return readSlotOwner(root, String(index)) === worktreeId;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The index for `worktreeId`, claiming one if it has none yet.
|
|
106
|
+
*
|
|
107
|
+
* @param root registry directory; defaults to {@link resolveWorktreeIndexRoot}
|
|
108
|
+
* @returns a number in `[0, MAX_WORKTREE_INDEX)`; never throws
|
|
109
|
+
*/
|
|
110
|
+
function resolveWorktreeIndex(worktreeId, options = {}) {
|
|
111
|
+
const root = options.root ?? resolveWorktreeIndexRoot();
|
|
112
|
+
try {
|
|
113
|
+
(0, fs_1.mkdirSync)(root, { recursive: true });
|
|
114
|
+
const taken = new Set();
|
|
115
|
+
for (const name of (0, fs_1.readdirSync)(root)) {
|
|
116
|
+
if (!/^\d+$/.test(name))
|
|
117
|
+
continue;
|
|
118
|
+
if (readSlotOwner(root, name) === worktreeId)
|
|
119
|
+
return Number(name);
|
|
120
|
+
taken.add(Number(name));
|
|
121
|
+
}
|
|
122
|
+
for (let index = 0; index < exports.MAX_WORKTREE_INDEX; index++) {
|
|
123
|
+
if (taken.has(index))
|
|
124
|
+
continue;
|
|
125
|
+
if (claimSlot(root, index, worktreeId))
|
|
126
|
+
return index;
|
|
127
|
+
}
|
|
128
|
+
logger.warn('worktree-index-exhausted', { worktreeId, root });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
logger.warn('worktree-index-unavailable', {
|
|
132
|
+
worktreeId,
|
|
133
|
+
root,
|
|
134
|
+
error: error instanceof Error ? error.message : String(error),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
// Deterministic, so a machine stuck on the fallback still gives each worktree
|
|
138
|
+
// the *same* number every run — it just cannot promise they are all distinct.
|
|
139
|
+
return hashWorktreeIndex(worktreeId);
|
|
140
|
+
}
|
package/package.json
CHANGED