borgmcp 1.1.15 → 2.0.2
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/CONTRIBUTING.md +15 -0
- package/LICENSE +201 -33
- package/NOTICE +2 -0
- package/README.md +85 -36
- package/SECURITY.md +7 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +63 -1
- package/dist/agent-runtime.js.map +1 -0
- package/dist/assimilate-cmd.d.ts +96 -17
- package/dist/assimilate-cmd.d.ts.map +1 -0
- package/dist/assimilate-cmd.js +1396 -48
- package/dist/assimilate-cmd.js.map +1 -0
- package/dist/assimilate-deps.d.ts.map +1 -0
- package/dist/assimilate-deps.js +384 -3
- package/dist/assimilate-deps.js.map +1 -0
- package/dist/assimilate-guard.d.ts +5 -7
- package/dist/assimilate-guard.d.ts.map +1 -0
- package/dist/assimilate-guard.js +60 -2
- package/dist/assimilate-guard.js.map +1 -0
- package/dist/assimilate-welcome.d.ts +1 -1
- package/dist/assimilate-welcome.d.ts.map +1 -0
- package/dist/assimilate-welcome.js +32 -2
- package/dist/assimilate-welcome.js.map +1 -0
- package/dist/auth-env.d.ts +7 -50
- package/dist/auth-env.d.ts.map +1 -0
- package/dist/auth-env.js +22 -1
- package/dist/auth-env.js.map +1 -0
- package/dist/backends/launch-all-pastelist.d.ts.map +1 -0
- package/dist/backends/launch-all-pastelist.js +11 -4
- package/dist/backends/launch-all-pastelist.js.map +1 -0
- package/dist/backends/launch-all-tmux.d.ts.map +1 -0
- package/dist/backends/launch-all-tmux.js +49 -1
- package/dist/backends/launch-all-tmux.js.map +1 -0
- package/dist/backends/launch-all-windows.d.ts.map +1 -0
- package/dist/backends/launch-all-windows.js +86 -12
- package/dist/backends/launch-all-windows.js.map +1 -0
- package/dist/bare-launch-menu.d.ts.map +1 -0
- package/dist/bare-launch-menu.js +83 -6
- package/dist/bare-launch-menu.js.map +1 -0
- package/dist/claude-launch-args.d.ts.map +1 -0
- package/dist/claude-launch-args.js +34 -1
- package/dist/claude-launch-args.js.map +1 -0
- package/dist/claude.d.ts.map +1 -0
- package/dist/claude.js +486 -14
- package/dist/claude.js.map +1 -0
- package/dist/cleanup-cmd.d.ts +5 -5
- package/dist/cleanup-cmd.d.ts.map +1 -0
- package/dist/cleanup-cmd.js +453 -14
- package/dist/cleanup-cmd.js.map +1 -0
- package/dist/clear-rewake-core.d.ts.map +1 -0
- package/dist/clear-rewake-core.js +23 -2
- package/dist/clear-rewake-core.js.map +1 -0
- package/dist/clear-rewake.d.ts.map +1 -0
- package/dist/clear-rewake.js +19 -1
- package/dist/clear-rewake.js.map +1 -0
- package/dist/cli-help.d.ts +5 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +104 -56
- package/dist/cli-help.js.map +1 -0
- package/dist/cli-platform.d.ts.map +1 -0
- package/dist/cli-platform.js +106 -4
- package/dist/cli-platform.js.map +1 -0
- package/dist/cli-tool-approval.d.ts +84 -0
- package/dist/cli-tool-approval.d.ts.map +1 -0
- package/dist/cli-tool-approval.js +583 -0
- package/dist/cli-tool-approval.js.map +1 -0
- package/dist/codex-app-server.d.ts.map +1 -0
- package/dist/codex-app-server.js +228 -4
- package/dist/codex-app-server.js.map +1 -0
- package/dist/codex-app-wake.d.ts +9 -11
- package/dist/codex-app-wake.d.ts.map +1 -0
- package/dist/codex-app-wake.js +436 -2
- package/dist/codex-app-wake.js.map +1 -0
- package/dist/codex-launch.d.ts.map +1 -0
- package/dist/codex-launch.js +102 -2
- package/dist/codex-launch.js.map +1 -0
- package/dist/codex-remote.d.ts +6 -1
- package/dist/codex-remote.d.ts.map +1 -0
- package/dist/codex-remote.js +394 -1
- package/dist/codex-remote.js.map +1 -0
- package/dist/codex-wake-resolve.d.ts +4 -4
- package/dist/codex-wake-resolve.d.ts.map +1 -0
- package/dist/codex-wake-resolve.js +127 -1
- package/dist/codex-wake-resolve.js.map +1 -0
- package/dist/config-utils.d.ts +2 -2
- package/dist/config-utils.d.ts.map +1 -0
- package/dist/config-utils.js +766 -3
- package/dist/config-utils.js.map +1 -0
- package/dist/config.d.ts +98 -61
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +426 -1
- package/dist/config.js.map +1 -0
- package/dist/console-prefix.d.ts.map +1 -0
- package/dist/console-prefix.js +96 -1
- package/dist/console-prefix.js.map +1 -0
- package/dist/cube-name.d.ts.map +1 -0
- package/dist/cube-name.js +67 -1
- package/dist/cube-name.js.map +1 -0
- package/dist/cubes.d.ts +126 -18
- package/dist/cubes.d.ts.map +1 -0
- package/dist/cubes.js +412 -4
- package/dist/cubes.js.map +1 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +71 -1
- package/dist/debug.js.map +1 -0
- package/dist/direct-log.d.ts.map +1 -0
- package/dist/direct-log.js +11 -1
- package/dist/direct-log.js.map +1 -0
- package/dist/docs-sections.d.ts +11 -10
- package/dist/docs-sections.d.ts.map +1 -0
- package/dist/docs-sections.js +113 -3
- package/dist/docs-sections.js.map +1 -0
- package/dist/drone-lifecycle.d.ts +5 -21
- package/dist/drone-lifecycle.d.ts.map +1 -0
- package/dist/drone-lifecycle.js +57 -8
- package/dist/drone-lifecycle.js.map +1 -0
- package/dist/drone-management.d.ts +29 -0
- package/dist/drone-management.d.ts.map +1 -0
- package/dist/drone-management.js +109 -0
- package/dist/drone-management.js.map +1 -0
- package/dist/ensure-mcp-config.d.ts.map +1 -0
- package/dist/ensure-mcp-config.js +36 -1
- package/dist/ensure-mcp-config.js.map +1 -0
- package/dist/evict-drone.d.ts +10 -2
- package/dist/evict-drone.d.ts.map +1 -0
- package/dist/evict-drone.js +60 -1
- package/dist/evict-drone.js.map +1 -0
- package/dist/gc-orphan-inboxes.d.ts.map +1 -0
- package/dist/gc-orphan-inboxes.js +177 -1
- package/dist/gc-orphan-inboxes.js.map +1 -0
- package/dist/inbox-monitor.d.ts +1 -1
- package/dist/inbox-monitor.d.ts.map +1 -0
- package/dist/inbox-monitor.js +856 -3
- package/dist/inbox-monitor.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1034 -37
- package/dist/index.js.map +1 -0
- package/dist/launch-all-cmd.d.ts.map +1 -0
- package/dist/launch-all-cmd.js +377 -37
- package/dist/launch-all-cmd.js.map +1 -0
- package/dist/launch-all-command.d.ts.map +1 -0
- package/dist/launch-all-command.js +24 -1
- package/dist/launch-all-command.js.map +1 -0
- package/dist/launch-all-deps.d.ts +3 -8
- package/dist/launch-all-deps.d.ts.map +1 -0
- package/dist/launch-all-deps.js +99 -1
- package/dist/launch-all-deps.js.map +1 -0
- package/dist/launch-all-discovery.d.ts +1 -0
- package/dist/launch-all-discovery.d.ts.map +1 -0
- package/dist/launch-all-discovery.js +90 -4
- package/dist/launch-all-discovery.js.map +1 -0
- package/dist/launch-all-locks.d.ts.map +1 -0
- package/dist/launch-all-locks.js +54 -1
- package/dist/launch-all-locks.js.map +1 -0
- package/dist/launch-gate.d.ts +2 -2
- package/dist/launch-gate.d.ts.map +1 -0
- package/dist/launch-gate.js +73 -1
- package/dist/launch-gate.js.map +1 -0
- package/dist/lifecycle-log-guard.d.ts.map +1 -0
- package/dist/lifecycle-log-guard.js +93 -2
- package/dist/lifecycle-log-guard.js.map +1 -0
- package/dist/list-roles-render.d.ts.map +1 -0
- package/dist/list-roles-render.js +40 -6
- package/dist/list-roles-render.js.map +1 -0
- package/dist/local-log-routing.d.ts +19 -0
- package/dist/local-log-routing.d.ts.map +1 -0
- package/dist/local-log-routing.js +67 -0
- package/dist/local-log-routing.js.map +1 -0
- package/dist/local-manage-tool-result.d.ts +8 -0
- package/dist/local-manage-tool-result.d.ts.map +1 -0
- package/dist/local-manage-tool-result.js +10 -0
- package/dist/local-manage-tool-result.js.map +1 -0
- package/dist/local-server-cursor.d.ts +34 -0
- package/dist/local-server-cursor.d.ts.map +1 -0
- package/dist/local-server-cursor.js +151 -0
- package/dist/local-server-cursor.js.map +1 -0
- package/dist/log-audit.d.ts.map +1 -0
- package/dist/log-audit.js +194 -3
- package/dist/log-audit.js.map +1 -0
- package/dist/log-stream.d.ts +35 -18
- package/dist/log-stream.d.ts.map +1 -0
- package/dist/log-stream.js +1270 -12
- package/dist/log-stream.js.map +1 -0
- package/dist/model-presets.d.ts.map +1 -0
- package/dist/model-presets.js +19 -1
- package/dist/model-presets.js.map +1 -0
- package/dist/name-validator.d.ts.map +1 -0
- package/dist/name-validator.js +22 -1
- package/dist/name-validator.js.map +1 -0
- package/dist/opencode-drone.d.ts.map +1 -0
- package/dist/opencode-drone.js +438 -4
- package/dist/opencode-drone.js.map +1 -0
- package/dist/opencode-plugin.d.ts +1 -1
- package/dist/opencode-plugin.d.ts.map +1 -0
- package/dist/opencode-plugin.js +29 -4
- package/dist/opencode-plugin.js.map +1 -0
- package/dist/parse-assimilate-args.d.ts +1 -1
- package/dist/parse-assimilate-args.d.ts.map +1 -0
- package/dist/parse-assimilate-args.js +125 -1
- package/dist/parse-assimilate-args.js.map +1 -0
- package/dist/parse-launch-all-args.d.ts.map +1 -0
- package/dist/parse-launch-all-args.js +70 -1
- package/dist/parse-launch-all-args.js.map +1 -0
- package/dist/postinstall-banner.d.ts.map +1 -0
- package/dist/postinstall-banner.js +27 -2
- package/dist/postinstall-banner.js.map +1 -0
- package/dist/postinstall.d.ts.map +1 -0
- package/dist/postinstall.js +32 -5
- package/dist/postinstall.js.map +1 -0
- package/dist/readiness-probe.d.ts.map +1 -0
- package/dist/readiness-probe.js +9 -1
- package/dist/readiness-probe.js.map +1 -0
- package/dist/regen-format.d.ts.map +1 -0
- package/dist/regen-format.js +545 -13
- package/dist/regen-format.js.map +1 -0
- package/dist/regen.d.ts.map +1 -0
- package/dist/regen.js +141 -5
- package/dist/regen.js.map +1 -0
- package/dist/remote-client.d.ts +88 -173
- package/dist/remote-client.d.ts.map +1 -0
- package/dist/remote-client.js +997 -1
- package/dist/remote-client.js.map +1 -0
- package/dist/reset-local-seat-cmd.d.ts +50 -0
- package/dist/reset-local-seat-cmd.d.ts.map +1 -0
- package/dist/reset-local-seat-cmd.js +169 -0
- package/dist/reset-local-seat-cmd.js.map +1 -0
- package/dist/role-match.d.ts.map +1 -0
- package/dist/role-match.js +23 -1
- package/dist/role-match.js.map +1 -0
- package/dist/role-resolver.d.ts.map +1 -0
- package/dist/role-resolver.js +60 -1
- package/dist/role-resolver.js.map +1 -0
- package/dist/roster-render.d.ts.map +1 -0
- package/dist/roster-render.js +121 -3
- package/dist/roster-render.js.map +1 -0
- package/dist/seat-probe.d.ts +32 -13
- package/dist/seat-probe.d.ts.map +1 -0
- package/dist/seat-probe.js +78 -1
- package/dist/seat-probe.js.map +1 -0
- package/dist/seat-store.d.ts +76 -0
- package/dist/seat-store.d.ts.map +1 -0
- package/dist/seat-store.js +309 -0
- package/dist/seat-store.js.map +1 -0
- package/dist/seats.d.ts +375 -0
- package/dist/seats.d.ts.map +1 -0
- package/dist/seats.js +670 -0
- package/dist/seats.js.map +1 -0
- package/dist/self-path.d.ts +27 -0
- package/dist/self-path.d.ts.map +1 -0
- package/dist/self-path.js +43 -0
- package/dist/self-path.js.map +1 -0
- package/dist/server-endpoint.d.ts +9 -0
- package/dist/server-endpoint.d.ts.map +1 -0
- package/dist/server-endpoint.js +38 -0
- package/dist/server-endpoint.js.map +1 -0
- package/dist/server-errors.d.ts +50 -0
- package/dist/server-errors.d.ts.map +1 -0
- package/dist/server-errors.js +79 -0
- package/dist/server-errors.js.map +1 -0
- package/dist/server-handshake.d.ts +197 -0
- package/dist/server-handshake.d.ts.map +1 -0
- package/dist/server-handshake.js +533 -0
- package/dist/server-handshake.js.map +1 -0
- package/dist/server-response.d.ts +3 -0
- package/dist/server-response.d.ts.map +1 -0
- package/dist/server-response.js +67 -0
- package/dist/server-response.js.map +1 -0
- package/dist/server-trust.d.ts +14 -0
- package/dist/server-trust.d.ts.map +1 -0
- package/dist/server-trust.js +225 -0
- package/dist/server-trust.js.map +1 -0
- package/dist/session-continuity.d.ts +33 -0
- package/dist/session-continuity.d.ts.map +1 -0
- package/dist/session-continuity.js +220 -0
- package/dist/session-continuity.js.map +1 -0
- package/dist/setup-confirm.d.ts +3 -3
- package/dist/setup-confirm.d.ts.map +1 -0
- package/dist/setup-confirm.js +126 -2
- package/dist/setup-confirm.js.map +1 -0
- package/dist/setup.d.ts +5 -3
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +169 -46
- package/dist/setup.js.map +1 -0
- package/dist/shell-escape.d.ts.map +1 -0
- package/dist/shell-escape.js +22 -1
- package/dist/shell-escape.js.map +1 -0
- package/dist/spawn.d.ts +2 -2
- package/dist/spawn.d.ts.map +1 -0
- package/dist/spawn.js +29 -10
- package/dist/spawn.js.map +1 -0
- package/dist/startup-services.d.ts +0 -1
- package/dist/startup-services.d.ts.map +1 -0
- package/dist/startup-services.js +26 -1
- package/dist/startup-services.js.map +1 -0
- package/dist/stream-owner.d.ts +6 -2
- package/dist/stream-owner.d.ts.map +1 -0
- package/dist/stream-owner.js +547 -2
- package/dist/stream-owner.js.map +1 -0
- package/dist/stream-status.d.ts +1 -1
- package/dist/stream-status.d.ts.map +1 -0
- package/dist/stream-status.js +268 -3
- package/dist/stream-status.js.map +1 -0
- package/dist/sync-roles-render.d.ts.map +1 -0
- package/dist/sync-roles-render.js +118 -3
- package/dist/sync-roles-render.js.map +1 -0
- package/dist/sync.d.ts +1 -1
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +286 -22
- package/dist/sync.js.map +1 -0
- package/dist/terminal-title.d.ts.map +1 -0
- package/dist/terminal-title.js +68 -1
- package/dist/terminal-title.js.map +1 -0
- package/dist/token-store.d.ts +17 -111
- package/dist/token-store.d.ts.map +1 -0
- package/dist/token-store.js +81 -1
- package/dist/token-store.js.map +1 -0
- package/dist/tool-manifest.d.ts +4 -8
- package/dist/tool-manifest.d.ts.map +1 -0
- package/dist/tool-manifest.js +554 -1
- package/dist/tool-manifest.js.map +1 -0
- package/dist/tool-scope.d.ts +8 -12
- package/dist/tool-scope.d.ts.map +1 -0
- package/dist/tool-scope.js +97 -1
- package/dist/tool-scope.js.map +1 -0
- package/dist/types.d.ts +1 -5
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -0
- package/dist/unknown-subcommand.js +37 -1
- package/dist/unknown-subcommand.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +78 -2
- package/dist/version.js.map +1 -0
- package/dist/working-repo.d.ts.map +1 -0
- package/dist/working-repo.js +81 -1
- package/dist/working-repo.js.map +1 -0
- package/dist/worktree-lifecycle.d.ts.map +1 -0
- package/dist/worktree-lifecycle.js +205 -2
- package/dist/worktree-lifecycle.js.map +1 -0
- package/docs/EXTRACTION_PROVENANCE.md +42 -0
- package/docs/LOCAL_SERVER.md +103 -0
- package/docs/RELEASING.md +129 -0
- package/package.json +52 -34
- package/src/agent-runtime.ts +69 -0
- package/src/assimilate-cmd.ts +1864 -0
- package/src/assimilate-deps.ts +444 -0
- package/src/assimilate-guard.ts +84 -0
- package/src/assimilate-welcome.ts +39 -0
- package/src/auth-env.ts +21 -0
- package/src/backends/launch-all-pastelist.ts +18 -0
- package/src/backends/launch-all-tmux.ts +75 -0
- package/src/backends/launch-all-windows.ts +110 -0
- package/src/bare-launch-menu.ts +148 -0
- package/src/claude-launch-args.ts +35 -0
- package/src/claude.ts +561 -0
- package/src/cleanup-cmd.ts +581 -0
- package/src/clear-rewake-core.ts +34 -0
- package/src/clear-rewake.ts +18 -0
- package/src/cli-help.ts +116 -0
- package/src/cli-platform.ts +127 -0
- package/src/cli-tool-approval.ts +716 -0
- package/src/codex-app-server.ts +238 -0
- package/src/codex-app-wake.ts +524 -0
- package/src/codex-launch.ts +140 -0
- package/src/codex-remote.ts +500 -0
- package/src/codex-wake-resolve.ts +155 -0
- package/src/config-utils.ts +811 -0
- package/src/config.ts +595 -0
- package/src/console-prefix.ts +99 -0
- package/src/cube-name.ts +69 -0
- package/src/cubes.ts +583 -0
- package/src/debug.ts +73 -0
- package/src/direct-log.ts +9 -0
- package/src/docs-sections.ts +129 -0
- package/src/drone-lifecycle.ts +62 -0
- package/src/drone-management.ts +162 -0
- package/src/ensure-mcp-config.ts +54 -0
- package/src/evict-drone.ts +82 -0
- package/src/gc-orphan-inboxes.ts +229 -0
- package/src/inbox-monitor.ts +1005 -0
- package/src/index.ts +1201 -0
- package/src/launch-all-cmd.ts +450 -0
- package/src/launch-all-command.ts +30 -0
- package/src/launch-all-deps.ts +188 -0
- package/src/launch-all-discovery.ts +118 -0
- package/src/launch-all-locks.ts +74 -0
- package/src/launch-gate.ts +80 -0
- package/src/lifecycle-log-guard.ts +144 -0
- package/src/list-roles-render.ts +52 -0
- package/src/local-log-routing.ts +106 -0
- package/src/local-manage-tool-result.ts +10 -0
- package/src/local-server-cursor.ts +188 -0
- package/src/log-audit.ts +207 -0
- package/src/log-stream.ts +1567 -0
- package/src/model-presets.ts +22 -0
- package/src/name-validator.ts +23 -0
- package/src/opencode-drone.ts +528 -0
- package/src/opencode-plugin.ts +38 -0
- package/src/parse-assimilate-args.ts +125 -0
- package/src/parse-launch-all-args.ts +100 -0
- package/src/postinstall-banner.ts +37 -0
- package/src/postinstall.ts +36 -0
- package/src/readiness-probe.ts +12 -0
- package/src/regen-format.ts +651 -0
- package/src/regen.ts +150 -0
- package/src/remote-client.ts +1457 -0
- package/src/reset-local-seat-cmd.ts +227 -0
- package/src/role-match.ts +29 -0
- package/src/role-resolver.ts +93 -0
- package/src/roster-render.ts +196 -0
- package/src/seat-probe.ts +115 -0
- package/src/seat-store.ts +333 -0
- package/src/seats.ts +911 -0
- package/src/self-path.ts +51 -0
- package/src/server-endpoint.ts +38 -0
- package/src/server-errors.ts +95 -0
- package/src/server-handshake.ts +816 -0
- package/src/server-response.ts +76 -0
- package/src/server-trust.ts +245 -0
- package/src/session-continuity.ts +280 -0
- package/src/setup-confirm.ts +171 -0
- package/src/setup.ts +186 -0
- package/src/shell-escape.ts +22 -0
- package/src/spawn.ts +31 -0
- package/src/startup-services.ts +36 -0
- package/src/stream-owner.ts +686 -0
- package/src/stream-status.ts +337 -0
- package/src/sync-roles-render.ts +163 -0
- package/src/sync.ts +363 -0
- package/src/terminal-title.ts +76 -0
- package/src/token-store.ts +102 -0
- package/src/tool-manifest.ts +610 -0
- package/src/tool-scope.ts +110 -0
- package/src/types.ts +5 -0
- package/src/unknown-subcommand.ts +35 -0
- package/src/version.ts +82 -0
- package/src/working-repo.ts +99 -0
- package/src/worktree-lifecycle.ts +254 -0
- package/dist/auth-recovery.d.ts +0 -33
- package/dist/auth-recovery.js +0 -1
- package/dist/auth.d.ts +0 -103
- package/dist/auth.js +0 -39
- package/dist/device-auth.d.ts +0 -75
- package/dist/device-auth.js +0 -1
- package/dist/drone-address.d.ts +0 -26
- package/dist/drone-address.js +0 -1
- package/dist/get-started.d.ts +0 -20
- package/dist/get-started.js +0 -2
- package/dist/health-beat.d.ts +0 -134
- package/dist/health-beat.js +0 -1
- package/dist/role-section.d.ts +0 -110
- package/dist/role-section.js +0 -8
- package/dist/setup-action.d.ts +0 -20
- package/dist/setup-action.js +0 -1
- package/dist/stale-version-check.d.ts +0 -40
- package/dist/stale-version-check.js +0 -1
- package/dist/subscription-retry.d.ts +0 -40
- package/dist/subscription-retry.js +0 -1
- package/dist/templates.d.ts +0 -107
- package/dist/templates.js +0 -359
- package/dist/token-crypto.d.ts +0 -50
- package/dist/token-crypto.js +0 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `borg reset-local-seat [--host <host>] [--yes]` — the dedicated LOCAL/OFFLINE
|
|
3
|
+
* seat reset from the ratified client-seat-reset-state-model (Option W).
|
|
4
|
+
*
|
|
5
|
+
* Attach is PURE DIAGNOSIS on a pin-matched SESSION_REJECTED (it mutates
|
|
6
|
+
* nothing and points here). This command is the ONLY writer that intentionally
|
|
7
|
+
* clears a worktree's saved local seat. It performs ZERO network I/O: it never
|
|
8
|
+
* contacts the server, so it makes NO server-revocation claim. It clears ONLY
|
|
9
|
+
* this worktree's saved local seat — its credential and cube binding together —
|
|
10
|
+
* from the local seat store (keyed on findProjectRoot()); server, trust anchor,
|
|
11
|
+
* cube, and every sibling worktree are untouched.
|
|
12
|
+
*
|
|
13
|
+
* Flow (decision clause 2):
|
|
14
|
+
* S0 snapshot exact binding + token-safe credential observation
|
|
15
|
+
* (PRESENT(digest) | ABSENT); --host normalized-mismatch = no-op BEFORE
|
|
16
|
+
* any mutation; no local binding = honest no-op.
|
|
17
|
+
* S1 consent OUTSIDE any lock — TTY [y/N] defaulting to No; non-TTY requires
|
|
18
|
+
* the explicit --yes flag.
|
|
19
|
+
* S2/S3 re-acquire cube→seat store, re-verify the exact snapshot (any change /
|
|
20
|
+
* missing / same-ref replacement = honest no-op), then delete the
|
|
21
|
+
* CREDENTIAL FIRST and remove the binding.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { createInterface } from 'node:readline/promises';
|
|
25
|
+
import {
|
|
26
|
+
snapshotLocalSeat as cubesSnapshotLocalSeat,
|
|
27
|
+
resetLocalSeatBinding as cubesResetLocalSeatBinding,
|
|
28
|
+
findProjectRoot as cubesFindProjectRoot,
|
|
29
|
+
type LocalSeatSnapshot,
|
|
30
|
+
type ResetLocalSeatOutcome,
|
|
31
|
+
} from './cubes.js';
|
|
32
|
+
import { normalizeServerEndpoint } from './server-endpoint.js';
|
|
33
|
+
|
|
34
|
+
export interface ResetLocalSeatFlags {
|
|
35
|
+
host?: string;
|
|
36
|
+
yes?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ResetLocalSeatDeps {
|
|
40
|
+
snapshotLocalSeat: () => Promise<LocalSeatSnapshot | null>;
|
|
41
|
+
resetLocalSeatBinding: (expected: LocalSeatSnapshot) => Promise<ResetLocalSeatOutcome>;
|
|
42
|
+
findProjectRoot: (cwd: string) => string;
|
|
43
|
+
normalizeHost: (host: string) => string;
|
|
44
|
+
cwd: () => string;
|
|
45
|
+
isTTY: () => boolean;
|
|
46
|
+
prompt: (message: string) => Promise<string>;
|
|
47
|
+
stdout: (line: string) => void;
|
|
48
|
+
stderr: (line: string) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function reenrollCommand(apiUrl: string): string {
|
|
52
|
+
return `\`borg assimilate --host ${apiUrl} --enroll\``;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Recovery guidance shared by the success + honest-no-op copy. Makes NO
|
|
57
|
+
* server-revocation claim: it recommends the operator issue a LIVE scoped
|
|
58
|
+
* invitation (the server can stay running) and re-enroll from this worktree.
|
|
59
|
+
*/
|
|
60
|
+
function recoveryGuidance(apiUrl: string): string {
|
|
61
|
+
return (
|
|
62
|
+
'To rejoin: ask the server operator for a new enrollment invitation — the server can ' +
|
|
63
|
+
'stay running (`borg-mcp-server client-invite`, or `owner-invite` for an owner) — then ' +
|
|
64
|
+
`re-enroll from this worktree with ${reenrollCommand(apiUrl)}.\n`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function runResetLocalSeat(
|
|
69
|
+
flags: ResetLocalSeatFlags,
|
|
70
|
+
deps: ResetLocalSeatDeps,
|
|
71
|
+
): Promise<number> {
|
|
72
|
+
const worktree = deps.findProjectRoot(deps.cwd());
|
|
73
|
+
|
|
74
|
+
// Normalize --host up front so an unparseable value fails before any read.
|
|
75
|
+
let requestedHost: string | undefined;
|
|
76
|
+
if (flags.host !== undefined) {
|
|
77
|
+
try {
|
|
78
|
+
requestedHost = deps.normalizeHost(flags.host);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
deps.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ----- S0: snapshot -----
|
|
86
|
+
const snapshot = await deps.snapshotLocalSeat();
|
|
87
|
+
if (!snapshot) {
|
|
88
|
+
deps.stdout(
|
|
89
|
+
`No saved local seat was found for this worktree (${worktree}); nothing to reset.\n`,
|
|
90
|
+
);
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --host normalized-mismatch = honest no-op BEFORE any mutation. This worktree
|
|
95
|
+
// points at a different server than the one named, so it is not the seat the
|
|
96
|
+
// operator asked to reset.
|
|
97
|
+
if (requestedHost !== undefined && requestedHost !== snapshot.apiUrl) {
|
|
98
|
+
deps.stdout(
|
|
99
|
+
`This worktree's saved local seat is on ${snapshot.apiUrl}, not ${requestedHost}; ` +
|
|
100
|
+
`nothing was changed. Re-run \`borg reset-local-seat --host ${snapshot.apiUrl}\` to ` +
|
|
101
|
+
"reset this worktree's seat.\n",
|
|
102
|
+
);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const observed =
|
|
107
|
+
snapshot.observation.state !== 'absent'
|
|
108
|
+
? `a saved local session credential is present (${snapshot.observation.state})`
|
|
109
|
+
: 'the saved local session credential is already cleared (only the binding remains)';
|
|
110
|
+
deps.stderr(
|
|
111
|
+
`This will clear ONLY this worktree's saved local seat on ${snapshot.apiUrl} ` +
|
|
112
|
+
`(worktree ${worktree}) — ${observed}. Server, trust anchor, cube, and sibling ` +
|
|
113
|
+
'worktrees are left untouched. It makes no network call and revokes nothing server-side.\n',
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// ----- S1: consent OUTSIDE any lock -----
|
|
117
|
+
if (deps.isTTY()) {
|
|
118
|
+
const answer = await deps.prompt(
|
|
119
|
+
`Reset this worktree's saved local seat now? [y/N]: `,
|
|
120
|
+
);
|
|
121
|
+
const normalized = answer.trim().toLowerCase();
|
|
122
|
+
if (normalized !== 'y' && normalized !== 'yes') {
|
|
123
|
+
deps.stderr("audit: no changes made — this worktree's saved seat was left in place.\n");
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
} else if (flags.yes !== true) {
|
|
127
|
+
deps.stderr(
|
|
128
|
+
'audit: no changes made — stdin is non-interactive and --yes was not passed. Re-run ' +
|
|
129
|
+
'`borg reset-local-seat --yes` to clear this worktree\'s saved seat without a prompt.\n',
|
|
130
|
+
);
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ----- S2/S3: re-verify under lock, credential-FIRST delete, then binding -----
|
|
135
|
+
let outcome: ResetLocalSeatOutcome;
|
|
136
|
+
try {
|
|
137
|
+
outcome = await deps.resetLocalSeatBinding(snapshot);
|
|
138
|
+
} catch {
|
|
139
|
+
deps.stderr(
|
|
140
|
+
`audit: no changes made — the local seat reset for ${snapshot.apiUrl} (worktree ` +
|
|
141
|
+
`${worktree}) could not complete (local credential store error). Retry — it is ` +
|
|
142
|
+
'safe to re-run.\n',
|
|
143
|
+
);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (outcome.outcome === 'reset') {
|
|
148
|
+
deps.stderr(
|
|
149
|
+
`audit: this worktree's saved local seat for ${snapshot.apiUrl} (worktree ${worktree}) ` +
|
|
150
|
+
'was cleared; server, trust anchor, cube, and sibling worktrees unchanged.\n',
|
|
151
|
+
);
|
|
152
|
+
deps.stdout(recoveryGuidance(snapshot.apiUrl));
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (outcome.outcome === 'no-binding') {
|
|
157
|
+
deps.stdout(
|
|
158
|
+
`No saved local seat remained for this worktree (${worktree}); nothing to reset.\n`,
|
|
159
|
+
);
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 'changed': the seat drifted between the snapshot and the commit re-check
|
|
164
|
+
// (a concurrent re-enroll wrote a fresh bearer, or another process already
|
|
165
|
+
// reset it). Never clobber a replacement — report the honest no-op.
|
|
166
|
+
deps.stdout(
|
|
167
|
+
`This worktree's saved local seat on ${snapshot.apiUrl} changed since it was read ` +
|
|
168
|
+
"(a concurrent re-enroll or reset); nothing was changed. Re-run to observe the current " +
|
|
169
|
+
'state.\n',
|
|
170
|
+
);
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function buildDefaultResetLocalSeatDeps(): ResetLocalSeatDeps {
|
|
175
|
+
return {
|
|
176
|
+
snapshotLocalSeat: () => cubesSnapshotLocalSeat(),
|
|
177
|
+
resetLocalSeatBinding: (expected) => cubesResetLocalSeatBinding(expected),
|
|
178
|
+
findProjectRoot: (cwd) => cubesFindProjectRoot(cwd),
|
|
179
|
+
normalizeHost: (host) => normalizeServerEndpoint(host),
|
|
180
|
+
cwd: () => process.cwd(),
|
|
181
|
+
isTTY: () => process.stdin.isTTY === true,
|
|
182
|
+
prompt: async (message: string): Promise<string> => {
|
|
183
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
184
|
+
try {
|
|
185
|
+
return await rl.question(message);
|
|
186
|
+
} finally {
|
|
187
|
+
rl.close();
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
stdout: (line) => process.stdout.write(line),
|
|
191
|
+
stderr: (line) => process.stderr.write(line),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export type ResetLocalSeatParseResult =
|
|
196
|
+
| { ok: true; flags: ResetLocalSeatFlags }
|
|
197
|
+
| { ok: false; error: string };
|
|
198
|
+
|
|
199
|
+
/** Parse args after `borg reset-local-seat`. Supports `--host <h>` / `--host=<h>` / `--yes` / `-y`. */
|
|
200
|
+
export function parseResetLocalSeatArgs(rawArgs: string[]): ResetLocalSeatParseResult {
|
|
201
|
+
const flags: ResetLocalSeatFlags = {};
|
|
202
|
+
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
203
|
+
const arg = rawArgs[i];
|
|
204
|
+
if (arg === '--host') {
|
|
205
|
+
const next = rawArgs[i + 1];
|
|
206
|
+
if (typeof next !== 'string' || next.length === 0 || next.startsWith('-')) {
|
|
207
|
+
return { ok: false, error: '--host requires a host or URL (e.g. `--host localhost:7091`)' };
|
|
208
|
+
}
|
|
209
|
+
flags.host = next;
|
|
210
|
+
i += 1;
|
|
211
|
+
} else if (arg.startsWith('--host=')) {
|
|
212
|
+
const value = arg.slice('--host='.length);
|
|
213
|
+
if (!value) {
|
|
214
|
+
return { ok: false, error: '--host requires a host or URL (e.g. `--host localhost:7091`)' };
|
|
215
|
+
}
|
|
216
|
+
flags.host = value;
|
|
217
|
+
} else if (arg === '--yes' || arg === '-y') {
|
|
218
|
+
flags.yes = true;
|
|
219
|
+
} else {
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
error: `unexpected argument: ${arg}. Usage: borg reset-local-seat [--host <host>] [--yes]`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { ok: true, flags };
|
|
227
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const UUID_RE =
|
|
2
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rollout-compat guard. An old worker ignores the `?role=` param and
|
|
6
|
+
* returns the CALLER's own role, so a new client on a not-yet-deployed
|
|
7
|
+
* worker would silently show the wrong playbook. Verify the returned
|
|
8
|
+
* role actually matches the request: by id when the input was a uuid,
|
|
9
|
+
* otherwise by case-insensitive trimmed name.
|
|
10
|
+
*
|
|
11
|
+
* Benign residual: if the requested name/id equals the caller's OWN role,
|
|
12
|
+
* an old worker's echo passes the check — but that echo IS the correct role
|
|
13
|
+
* (names are unique per cube, ids globally unique), so no wrong playbook is
|
|
14
|
+
* ever shown. The guard only needs to catch a request for a DIFFERENT role.
|
|
15
|
+
*/
|
|
16
|
+
export function assertRoleMatches(
|
|
17
|
+
requested: string,
|
|
18
|
+
returned: { id?: string | null; name?: string | null }
|
|
19
|
+
): void {
|
|
20
|
+
const req = requested.trim();
|
|
21
|
+
const matches = UUID_RE.test(req)
|
|
22
|
+
? returned.id === req
|
|
23
|
+
: (returned.name ?? '').trim().toLowerCase() === req.toLowerCase();
|
|
24
|
+
if (!matches) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`server does not support named-role lookup yet (returned "${returned.name ?? '?'}" for "${requested}") — worker upgrade pending`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export interface Role {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
is_default: boolean;
|
|
5
|
+
// Optional so a newly published client remains compatible with an older
|
|
6
|
+
// worker that has not yet deployed the mandatory-role migration.
|
|
7
|
+
is_mandatory?: boolean;
|
|
8
|
+
is_human_seat: boolean;
|
|
9
|
+
// gh#899: present when the server includes it; used to give queen-class seats
|
|
10
|
+
// the full native tool surface. Optional — absent → treated as non-management
|
|
11
|
+
// (degrades to dispatcher access, never a capability loss).
|
|
12
|
+
role_class?: 'queen' | 'worker';
|
|
13
|
+
default_model?: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RoleOccupant {
|
|
17
|
+
role_id: string;
|
|
18
|
+
/** Server-derived advisory; absent remains occupied for rollout safety. */
|
|
19
|
+
presumed_abandoned?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the advisory occupancy set used only by role-less assimilation.
|
|
24
|
+
* Explicit role selection and server authorization do not consult it.
|
|
25
|
+
*/
|
|
26
|
+
export function occupiedRoleIdsForAutoRole(
|
|
27
|
+
drones: readonly RoleOccupant[],
|
|
28
|
+
): Set<string> {
|
|
29
|
+
return new Set(
|
|
30
|
+
drones
|
|
31
|
+
.filter((drone) => drone.presumed_abandoned !== true)
|
|
32
|
+
.map((drone) => drone.role_id),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a role-name argument or stored name into a slug used for
|
|
38
|
+
* both worktree-path construction and role lookup. Single source of
|
|
39
|
+
* truth so the path and the matched role always agree.
|
|
40
|
+
*
|
|
41
|
+
* Path-safe: strips characters outside `[a-z0-9-]` after the
|
|
42
|
+
* underscore/whitespace collapse. The no-arg path bypasses
|
|
43
|
+
* validateName (the matched role's name comes from the DB, which
|
|
44
|
+
* has no charset constraint on role names — CreateRoleSchema is
|
|
45
|
+
* `min(1).max(64)` only), so the safety has to live here.
|
|
46
|
+
*/
|
|
47
|
+
export function roleSlug(raw: string): string {
|
|
48
|
+
return raw.toLowerCase().replace(/[_\s]+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Case- and separator-insensitive lookup against the cube's roles.
|
|
53
|
+
*/
|
|
54
|
+
export function matchRoleByName(roles: Role[], query: string): Role | undefined {
|
|
55
|
+
const slug = roleSlug(query);
|
|
56
|
+
return roles.find((r) => roleSlug(r.name) === slug);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Role picker for the no-arg `borg assimilate` case:
|
|
61
|
+
* - First drone in cube + a human-seat role exists → that role.
|
|
62
|
+
* - Otherwise → the first UNOCCUPIED mandatory non-queen role in list
|
|
63
|
+
* order. This includes a mandatory human-seat role such as Coordinator;
|
|
64
|
+
* its active seat remains the occupancy guard against a double-fill.
|
|
65
|
+
* - Otherwise → the first UNOCCUPIED eligible worker role in list order
|
|
66
|
+
* (eligible = not queen-class, not human-seat). The default role is an
|
|
67
|
+
* equal candidate in this pass.
|
|
68
|
+
* - If every eligible worker role is occupied → the is_default role.
|
|
69
|
+
* - Neither available → undefined (caller errors out).
|
|
70
|
+
*
|
|
71
|
+
* `occupiedRoleIds` is the set of role_ids that still hold a live-enough active
|
|
72
|
+
* seat for automatic selection. Omitted → treated as empty (all roles
|
|
73
|
+
* unoccupied), which degrades to "first eligible worker role, else default".
|
|
74
|
+
*/
|
|
75
|
+
export function pickDefaultRole(
|
|
76
|
+
roles: Role[],
|
|
77
|
+
opts: { isFirstDrone: boolean; occupiedRoleIds?: ReadonlySet<string> }
|
|
78
|
+
): Role | undefined {
|
|
79
|
+
if (opts.isFirstDrone) {
|
|
80
|
+
const humanSeat = roles.find((r) => r.is_human_seat);
|
|
81
|
+
if (humanSeat) return humanSeat;
|
|
82
|
+
}
|
|
83
|
+
const occupied = opts.occupiedRoleIds ?? new Set<string>();
|
|
84
|
+
const mandatory = roles.filter(
|
|
85
|
+
(r) => r.is_mandatory === true && r.role_class !== 'queen'
|
|
86
|
+
);
|
|
87
|
+
const eligible = roles.filter(
|
|
88
|
+
(r) => r.role_class !== 'queen' && !r.is_human_seat
|
|
89
|
+
);
|
|
90
|
+
return mandatory.find((r) => !occupied.has(r.id))
|
|
91
|
+
?? eligible.find((r) => !occupied.has(r.id))
|
|
92
|
+
?? roles.find((r) => r.is_default);
|
|
93
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure renderer for the `borg_roster` MCP tool output.
|
|
3
|
+
*
|
|
4
|
+
* Split out from `index.ts` so the T2.1 sender-side liveness probe
|
|
5
|
+
* (the `awake`/`stale-since-X` column rendering when a `since` arg is
|
|
6
|
+
* passed) can be unit-tested without spinning up the MCP server. Same
|
|
7
|
+
* pure-function + injected-`humanAgo` pattern as `stream-status.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Two modes:
|
|
10
|
+
* - No `since` provided: classic roster — one line per drone with
|
|
11
|
+
* label, role, and `last seen` relative time.
|
|
12
|
+
* - `since` provided: each line additionally carries an `awake`
|
|
13
|
+
* marker if the drone's `last_seen` is after the resolved
|
|
14
|
+
* timestamp, otherwise a `stale-since-<relative>` marker derived
|
|
15
|
+
* from the resolved timestamp the server echoed back.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { formatDroneAddressToken } from 'borgmcp-shared/drone-address';
|
|
19
|
+
|
|
20
|
+
export interface RosterDrone {
|
|
21
|
+
id?: string;
|
|
22
|
+
label: string;
|
|
23
|
+
role_id: string;
|
|
24
|
+
last_seen: string | Date;
|
|
25
|
+
/** Only present when the request carried `since`. */
|
|
26
|
+
seen_since?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* gh#370 — which AI agent is running this drone. Null for drones that
|
|
29
|
+
* joined before the column existed or via a launcher path that didn't
|
|
30
|
+
* forward the kind. This is the agent CLI, not a model descriptor.
|
|
31
|
+
*/
|
|
32
|
+
agent_kind?: 'claude' | 'codex' | 'opencode' | null;
|
|
33
|
+
regen_count?: number | null;
|
|
34
|
+
wake_path?: 'live' | 'degraded' | 'deaf' | null;
|
|
35
|
+
wake_path_alert_class?:
|
|
36
|
+
| 'dead'
|
|
37
|
+
| 'post-blocked'
|
|
38
|
+
| 'presumed-dead'
|
|
39
|
+
| 'systemic-post-block'
|
|
40
|
+
| 'wake-path-deaf'
|
|
41
|
+
| 'independent'
|
|
42
|
+
| null;
|
|
43
|
+
/** Legacy launch descriptor retained on the wire for the Ollama launch path; not displayed. */
|
|
44
|
+
model?: string | null;
|
|
45
|
+
/** Advisory model string self-reported by the agent on regen. */
|
|
46
|
+
reported_model?: string | null;
|
|
47
|
+
/** Current cwd-derived repository identity, refreshed on regen. */
|
|
48
|
+
working_repo_name?: string | null;
|
|
49
|
+
working_repo_origin?: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RosterRole {
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
/**
|
|
56
|
+
* Default model descriptor for drones assigned to this role.
|
|
57
|
+
* Null = no role default (use cube default or null for Claude).
|
|
58
|
+
*/
|
|
59
|
+
default_model?: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* gh#503 — render-layer display labels for the wake-path wire enum.
|
|
64
|
+
*
|
|
65
|
+
* The gh#406 SLI MEASURES a streak of unanswered directed challenges; it
|
|
66
|
+
* cannot confirm a genuinely broken wake path ("deaf") — a seat that is
|
|
67
|
+
* alive but idle-and-text-only looks identical past the SLA. So the
|
|
68
|
+
* roster shows the measured-confidence label, not the unconfirmed verdict:
|
|
69
|
+
* degraded (1 miss) → `unverified` (signal seen, not yet corroborated)
|
|
70
|
+
* deaf (2+ miss) → `unresponsive` (no response past the SLA)
|
|
71
|
+
*
|
|
72
|
+
* The wire enum VALUES (`live`/`degraded`/`deaf`), computed server-side by
|
|
73
|
+
* `wakePathFromStreak`, are intentionally UNCHANGED — this is a display-only
|
|
74
|
+
* relabel within gh#503's copy-only scope (renaming the enum would be a
|
|
75
|
+
* wire + schema change). An operator who wants a confirmed read cross-checks
|
|
76
|
+
* `borg_stream-status` on the affected seat. Unknown wire values fall back
|
|
77
|
+
* to the raw value so a newer server can't break an older client's roster.
|
|
78
|
+
*/
|
|
79
|
+
const WAKE_PATH_DISPLAY: Record<string, string> = {
|
|
80
|
+
degraded: 'unverified',
|
|
81
|
+
deaf: 'unresponsive',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export function formatRoleAgentLabel(
|
|
85
|
+
roleName: string,
|
|
86
|
+
agentKind: RosterDrone['agent_kind']
|
|
87
|
+
): string {
|
|
88
|
+
const agentCli = agentKind === 'claude'
|
|
89
|
+
? 'Claude Code'
|
|
90
|
+
: agentKind === 'codex'
|
|
91
|
+
? 'Codex'
|
|
92
|
+
: agentKind === 'opencode'
|
|
93
|
+
? 'OpenCode'
|
|
94
|
+
: 'not reported';
|
|
95
|
+
return `Role: ${roleName} · Agent CLI: ${agentCli}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function formatWorkingRepoLabel(drone: Pick<RosterDrone, 'working_repo_name' | 'working_repo_origin'>): string {
|
|
99
|
+
if (drone.working_repo_name && drone.working_repo_origin) {
|
|
100
|
+
return `Working repo: ${drone.working_repo_name} · origin: ${drone.working_repo_origin}`;
|
|
101
|
+
}
|
|
102
|
+
if (drone.working_repo_name) return `Working repo: ${drone.working_repo_name}`;
|
|
103
|
+
if (drone.working_repo_origin) return `Working repo: origin: ${drone.working_repo_origin}`;
|
|
104
|
+
return 'Working repo: not reported';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface RenderRosterInputs {
|
|
108
|
+
cubeName: string;
|
|
109
|
+
drones: RosterDrone[];
|
|
110
|
+
roles: RosterRole[];
|
|
111
|
+
/**
|
|
112
|
+
* Server-echoed resolved timestamp (ISO-8601). Present iff caller
|
|
113
|
+
* passed `since` to `borg_roster`. Drives both the column header copy
|
|
114
|
+
* AND the relative-time label on `stale-since-X` cells.
|
|
115
|
+
*/
|
|
116
|
+
resolvedSince: string | null;
|
|
117
|
+
/** Relative-time formatter, injected so the renderer is pure. */
|
|
118
|
+
humanAgo: (d: Date | string) => string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function renderRoster(inputs: RenderRosterInputs): string {
|
|
122
|
+
const { cubeName, drones, roles, resolvedSince, humanAgo } = inputs;
|
|
123
|
+
const roleById = new Map<string, RosterRole>();
|
|
124
|
+
for (const r of roles) roleById.set(r.id, r);
|
|
125
|
+
|
|
126
|
+
const lines: string[] = [];
|
|
127
|
+
lines.push(`# Drones in cube: ${cubeName}`);
|
|
128
|
+
lines.push('');
|
|
129
|
+
|
|
130
|
+
if (resolvedSince) {
|
|
131
|
+
// Surface the liveness-probe context so the reader knows what the
|
|
132
|
+
// `awake`/`stale-since-X` column is measured against.
|
|
133
|
+
lines.push(
|
|
134
|
+
`_Liveness probe since ${resolvedSince} (${humanAgo(
|
|
135
|
+
resolvedSince
|
|
136
|
+
)}). \`awake\` = drone posted to the cube log after that point._`
|
|
137
|
+
);
|
|
138
|
+
lines.push('');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!drones.length) {
|
|
142
|
+
lines.push('_(no drones connected)_');
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const d of drones) {
|
|
147
|
+
const role = roleById.get(d.role_id);
|
|
148
|
+
const roleName = role?.name ?? 'unknown';
|
|
149
|
+
const roleLabel = formatRoleAgentLabel(roleName, d.agent_kind);
|
|
150
|
+
// gh#371: stable short-uuid address token beside the (renumber-prone) label.
|
|
151
|
+
const addr = d.id ? ` ${formatDroneAddressToken(d.id)}` : '';
|
|
152
|
+
const lastSeen = humanAgo(d.last_seen);
|
|
153
|
+
const reportedModelMarker = d.reported_model
|
|
154
|
+
? ` · \`Reported model: ${d.reported_model}\``
|
|
155
|
+
: ' · `Reported model: not reported`';
|
|
156
|
+
const workingRepo = formatWorkingRepoLabel(d);
|
|
157
|
+
const workingRepoMarker = ` · \`${workingRepo}\``;
|
|
158
|
+
const wakePathMarker =
|
|
159
|
+
d.wake_path && d.wake_path !== 'live'
|
|
160
|
+
? ` · \`wake-path:${WAKE_PATH_DISPLAY[d.wake_path] ?? d.wake_path}\``
|
|
161
|
+
: '';
|
|
162
|
+
const wakePathClassMarker =
|
|
163
|
+
d.wake_path_alert_class && d.wake_path_alert_class !== 'independent'
|
|
164
|
+
? ` · \`wake-path-class:${d.wake_path_alert_class}\``
|
|
165
|
+
: '';
|
|
166
|
+
const regenCountMarker =
|
|
167
|
+
typeof d.regen_count === 'number' ? ` · \`regen-count:${d.regen_count}\`` : '';
|
|
168
|
+
if (resolvedSince) {
|
|
169
|
+
// T2.1 awake/stale column. `seen_since === true` → drone called a
|
|
170
|
+
// tool after the resolved timestamp; treat as awake. False or
|
|
171
|
+
// missing → stale. Missing should not happen when the server
|
|
172
|
+
// echoed a since, but defending against a shape mismatch is
|
|
173
|
+
// cheap and matches the renderer's no-server-assumptions
|
|
174
|
+
// discipline established in stream-status.ts.
|
|
175
|
+
//
|
|
176
|
+
// Per drone-4's polish NIT on the original T2.1 ship: the marker
|
|
177
|
+
// is just `stale` (not `stale-since-<relative>`) because the
|
|
178
|
+
// header already anchors the reference point and per-row "since
|
|
179
|
+
// when" would be identical across all stale rows in a single
|
|
180
|
+
// probe call — pure redundancy. The per-row `last seen X ago`
|
|
181
|
+
// field carries the diagnostic detail for "how stale is this
|
|
182
|
+
// particular drone."
|
|
183
|
+
const isAwake = d.seen_since === true;
|
|
184
|
+
const marker = isAwake ? '`awake`' : '`stale`';
|
|
185
|
+
lines.push(
|
|
186
|
+
`- **${d.label}**${addr} (${roleLabel}) — last seen ${lastSeen} · ${marker}${regenCountMarker}${wakePathMarker}${wakePathClassMarker}${reportedModelMarker}${workingRepoMarker}`
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
lines.push(
|
|
190
|
+
`- **${d.label}**${addr} (${roleLabel}) — last seen ${lastSeen}${regenCountMarker}${wakePathMarker}${wakePathClassMarker}${reportedModelMarker}${workingRepoMarker}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return lines.join('\n');
|
|
196
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Shared per-seat liveness probe (gh#877 reuse).
|
|
2
|
+
//
|
|
3
|
+
// Extracted from cleanup-cmd.ts so BOTH `borg cleanup` (destructive → fail-SAFE)
|
|
4
|
+
// and `borg launch-all` (constructive → fail-OPEN) can classify a saved seat by
|
|
5
|
+
// ITS OWN token, without launch-all having to import cleanup-cmd's chalk/report
|
|
6
|
+
// graph. cleanup-cmd re-exports `SeatStatus` for backwards compatibility.
|
|
7
|
+
|
|
8
|
+
import { whoami } from './remote-client.js';
|
|
9
|
+
import { DroneEvictedError } from './drone-lifecycle.js';
|
|
10
|
+
import {
|
|
11
|
+
BorgServerError,
|
|
12
|
+
BorgServerHttpError,
|
|
13
|
+
BorgServerTrustError,
|
|
14
|
+
BorgServerUnreachableError,
|
|
15
|
+
} from './server-errors.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Eviction-probe verdict for ONE worktree's saved seat. Distinct CAUSES are
|
|
19
|
+
* preserved (CR #6 / CR5 — the probe must NOT collapse them). Each verdict is
|
|
20
|
+
* derived from the actual error TYPE/CODE, never from a mutable error-text regex:
|
|
21
|
+
* evicted ← 410 DRONE_EVICTED (terminal; the SOLE delete authority — gh#882 S1)
|
|
22
|
+
* rejected ← pin-matched drone-SESSION 401 carrying the EXACT typed
|
|
23
|
+
* SESSION_REJECTED code (revoked / taken over; recover via
|
|
24
|
+
* the offline `borg reset-local-seat` — never here)
|
|
25
|
+
* credential-rejected← any OTHER 401 on the drone session (bare/untyped or a
|
|
26
|
+
* non-SESSION typed code): the saved credential is no longer
|
|
27
|
+
* accepted, but this is NON-DESTRUCTIVE — re-enroll, NEVER a
|
|
28
|
+
* seat reset and never a "restart the server" blip
|
|
29
|
+
* trust-mismatch ← the pinned server identity no longer matches (TYPED
|
|
30
|
+
* BorgServerTrustError; terminal — restarting does not fix it)
|
|
31
|
+
* unreachable ← transport failure: connection refused/reset, DNS, or a
|
|
32
|
+
* request timeout (TYPED BorgServerUnreachableError or a stable
|
|
33
|
+
* transport errno) — genuinely transient
|
|
34
|
+
* endpoint-mismatch ← a 404 from a verified server: the drone endpoint/protocol is
|
|
35
|
+
* not recognized (a client/server version mismatch)
|
|
36
|
+
* server-failure ← a 5xx from a verified server (its internal error; transient)
|
|
37
|
+
* live ← 200 (resolves)
|
|
38
|
+
* indeterminate ← any other ambiguous/unknown failure — never authorizes a
|
|
39
|
+
* delete or a seat reset
|
|
40
|
+
*/
|
|
41
|
+
export type SeatStatus =
|
|
42
|
+
| 'evicted'
|
|
43
|
+
| 'rejected'
|
|
44
|
+
| 'live'
|
|
45
|
+
| 'credential-rejected'
|
|
46
|
+
| 'trust-mismatch'
|
|
47
|
+
| 'unreachable'
|
|
48
|
+
| 'endpoint-mismatch'
|
|
49
|
+
| 'server-failure'
|
|
50
|
+
| 'indeterminate';
|
|
51
|
+
|
|
52
|
+
// Stable transport-level errno / undici codes (a CODE check, not error-text). A
|
|
53
|
+
// pinned-CA TLS failure and a refused/reset/timeout all surface one of these.
|
|
54
|
+
const TRANSPORT_ERRNOS = new Set([
|
|
55
|
+
'ECONNREFUSED',
|
|
56
|
+
'ECONNRESET',
|
|
57
|
+
'ENOTFOUND',
|
|
58
|
+
'EHOSTUNREACH',
|
|
59
|
+
'ENETUNREACH',
|
|
60
|
+
'ETIMEDOUT',
|
|
61
|
+
'EAI_AGAIN',
|
|
62
|
+
'EPIPE',
|
|
63
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
64
|
+
'UND_ERR_HEADERS_TIMEOUT',
|
|
65
|
+
'UND_ERR_SOCKET',
|
|
66
|
+
'ABORT_ERR',
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
function isTransportFailure(err: unknown): boolean {
|
|
70
|
+
if (err instanceof BorgServerUnreachableError) return true;
|
|
71
|
+
const e = err as { name?: string; code?: string; cause?: { code?: string } };
|
|
72
|
+
if (e?.name === 'AbortError') return true;
|
|
73
|
+
const code = e?.code ?? e?.cause?.code;
|
|
74
|
+
return typeof code === 'string' && TRANSPORT_ERRNOS.has(code);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Default seat probe: a lightweight drone-authed `whoami` with the seat's OWN
|
|
79
|
+
* saved token. authedFetch throws TYPED errors on the authoritative outcomes
|
|
80
|
+
* (410→DroneEvictedError; pin-matched typed 401→BorgServerError('SESSION_REJECTED');
|
|
81
|
+
* every other 401→BorgServerError('CREDENTIAL_REJECTED'); a pinned-identity mismatch
|
|
82
|
+
* →BorgServerTrustError; a non-ok status→BorgServerHttpError(status); a transport
|
|
83
|
+
* failure/timeout→BorgServerUnreachableError). Each maps to a STABLE typed verdict
|
|
84
|
+
* so recovery copy is cause-accurate; the cleanup path must NEVER delete on anything
|
|
85
|
+
* but `evicted`; the launch path treats every non-`evicted`/non-terminal cause as
|
|
86
|
+
* fail-OPEN.
|
|
87
|
+
*/
|
|
88
|
+
export async function defaultProbeSeat(
|
|
89
|
+
sessionToken: string,
|
|
90
|
+
apiUrl: string,
|
|
91
|
+
serverTrustIdentity?: string,
|
|
92
|
+
): Promise<SeatStatus> {
|
|
93
|
+
try {
|
|
94
|
+
await whoami(sessionToken, apiUrl, serverTrustIdentity);
|
|
95
|
+
return 'live';
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (err instanceof DroneEvictedError) return 'evicted';
|
|
98
|
+
// A pinned-identity / CA mismatch is a TERMINAL trust verdict — classified from
|
|
99
|
+
// the error TYPE, never from message text (the security boundary).
|
|
100
|
+
if (err instanceof BorgServerTrustError) return 'trust-mismatch';
|
|
101
|
+
if (err instanceof BorgServerError) {
|
|
102
|
+
if (err.code === 'SESSION_REJECTED') return 'rejected';
|
|
103
|
+
// Every non-SESSION 401 (bare/untyped or a different typed code) is a
|
|
104
|
+
// credential rejection — non-destructive, distinct from a takeover.
|
|
105
|
+
if (err.code === 'CREDENTIAL_REJECTED') return 'credential-rejected';
|
|
106
|
+
}
|
|
107
|
+
if (err instanceof BorgServerHttpError) {
|
|
108
|
+
if (err.status === 404) return 'endpoint-mismatch';
|
|
109
|
+
if (err.status >= 500 && err.status <= 599) return 'server-failure';
|
|
110
|
+
return 'indeterminate';
|
|
111
|
+
}
|
|
112
|
+
if (isTransportFailure(err)) return 'unreachable';
|
|
113
|
+
return 'indeterminate';
|
|
114
|
+
}
|
|
115
|
+
}
|