@gajae-code/coding-agent 0.11.1 → 0.11.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/CHANGELOG.md +54 -0
- package/dist/types/cli/config-cli.d.ts +11 -1
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/cli/session-picker.d.ts +2 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/mcp-serve.d.ts +33 -0
- package/dist/types/commands/mcp.d.ts +1 -1
- package/dist/types/commands/notify.d.ts +0 -2
- package/dist/types/commands/sdk.d.ts +1 -1
- package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
- package/dist/types/config/file-lock.d.ts +14 -4
- package/dist/types/config/keybindings.d.ts +42 -1
- package/dist/types/config/model-auth.d.ts +3 -0
- package/dist/types/config/model-bindings-applier.d.ts +13 -0
- package/dist/types/config/model-discovery-manager.d.ts +49 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-registry.d.ts +16 -15
- package/dist/types/config/model-resolver.d.ts +28 -18
- package/dist/types/config/models-config-schema.d.ts +11 -0
- package/dist/types/config/settings-schema.d.ts +56 -1
- package/dist/types/config/settings.d.ts +14 -11
- package/dist/types/coordinator-mcp/policy.d.ts +5 -0
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/discovery/helpers.d.ts +0 -6
- package/dist/types/edit/index.d.ts +3 -0
- package/dist/types/eval/py/env.d.ts +9 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +2 -2
- package/dist/types/extensibility/extensions/types.d.ts +34 -3
- package/dist/types/extensibility/hooks/runner.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
- package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
- package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
- package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
- package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
- package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
- package/dist/types/gjc-runtime/team-store.d.ts +160 -0
- package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
- package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
- package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/goals/state.d.ts +8 -0
- package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
- package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/state.d.ts +11 -4
- package/dist/types/lsp/lspmux.d.ts +5 -3
- package/dist/types/lsp/path-trust.d.ts +1 -0
- package/dist/types/main.d.ts +3 -2
- package/dist/types/memories/storage.d.ts +1 -0
- package/dist/types/modes/action-registry.d.ts +29 -0
- package/dist/types/modes/components/chrome.d.ts +14 -0
- package/dist/types/modes/components/command-palette.d.ts +27 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
- package/dist/types/modes/components/index.d.ts +2 -1
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
- package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
- package/dist/types/modes/components/queue-pane.d.ts +12 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
- package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/components/show-images-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +1 -94
- package/dist/types/modes/components/tasks-pane.d.ts +15 -0
- package/dist/types/modes/components/theme-selector.d.ts +1 -1
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/tool-status-header.d.ts +111 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
- package/dist/types/modes/controllers/input-controller.d.ts +8 -1
- package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
- package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
- package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
- package/dist/types/modes/interactive-mode.d.ts +19 -17
- package/dist/types/modes/jobs-observer.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
- package/dist/types/modes/tasks-aggregator.d.ts +39 -0
- package/dist/types/modes/transcript-item-registry.d.ts +60 -0
- package/dist/types/modes/types.d.ts +20 -13
- package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +78 -12
- package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
- package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
- package/dist/types/sdk/broker/session-index.d.ts +0 -1
- package/dist/types/sdk/broker/state-version.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
- package/dist/types/sdk/bus/config-commands.d.ts +1 -1
- package/dist/types/sdk/bus/config.d.ts +11 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/html-format.d.ts +3 -2
- package/dist/types/sdk/bus/index.d.ts +27 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/notification-service.d.ts +20 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
- package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
- package/dist/types/sdk/bus/rich-document.d.ts +7 -0
- package/dist/types/sdk/bus/rich-render.d.ts +18 -41
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
- package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/session-directory.d.ts +56 -0
- package/dist/types/session/agent-session.d.ts +82 -47
- package/dist/types/session/agent-storage.d.ts +7 -0
- package/dist/types/session/fallback-chain-controller.d.ts +10 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
- package/dist/types/session/messages.d.ts +16 -0
- package/dist/types/session/session-manager-internal.d.ts +14 -0
- package/dist/types/session/session-manager.d.ts +82 -12
- package/dist/types/session/session-storage.d.ts +43 -3
- package/dist/types/session/volatile-context-pruning.d.ts +15 -0
- package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
- package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
- package/dist/types/system-prompt.d.ts +2 -11
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/thinking.d.ts +7 -0
- package/dist/types/tool-discovery/tool-index.d.ts +1 -50
- package/dist/types/tools/ask.d.ts +64 -2
- package/dist/types/tools/bash.d.ts +3 -0
- package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
- package/dist/types/tools/cron.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -0
- package/dist/types/tools/index.d.ts +30 -15
- package/dist/types/tools/output-meta.d.ts +1 -0
- package/dist/types/tools/read.d.ts +3 -0
- package/dist/types/tools/search-tool-bm25.d.ts +3 -4
- package/dist/types/utils/event-bus.d.ts +1 -1
- package/dist/types/web/search/index.d.ts +3 -1
- package/package.json +21 -9
- package/scripts/build-sdk-package-smoke.ts +34 -1
- package/scripts/generate-hotkeys-docs.ts +74 -0
- package/scripts/generate-sdk-operation-inventory.ts +64 -20
- package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
- package/src/async/job-manager.ts +49 -6
- package/src/cli/config-cli.ts +75 -2
- package/src/cli/notify-cli.ts +92 -26
- package/src/cli/session-picker.ts +20 -3
- package/src/cli/setup-cli.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/config.ts +1 -1
- package/src/commands/harness.ts +34 -106
- package/src/commands/launch.ts +6 -4
- package/src/commands/mcp-serve.ts +85 -8
- package/src/commands/mcp.ts +2 -2
- package/src/commands/notify.ts +8 -4
- package/src/commands/sdk.ts +61 -40
- package/src/commands/ultragoal.ts +4 -1
- package/src/config/atomic-yaml-patch.ts +80 -13
- package/src/config/file-lock.ts +105 -43
- package/src/config/keybindings.ts +64 -20
- package/src/config/model-auth.ts +9 -0
- package/src/config/model-bindings-applier.ts +139 -0
- package/src/config/model-discovery-manager.ts +224 -0
- package/src/config/model-equivalence.ts +66 -0
- package/src/config/model-registry.ts +169 -302
- package/src/config/model-resolver.ts +361 -369
- package/src/config/models-config-schema.ts +5 -4
- package/src/config/settings-schema.ts +159 -2
- package/src/config/settings.ts +458 -482
- package/src/coordinator-mcp/policy.ts +76 -11
- package/src/coordinator-mcp/question-gate-codec.ts +274 -0
- package/src/coordinator-mcp/question-state.ts +778 -0
- package/src/coordinator-mcp/server.ts +1702 -238
- package/src/debug/index.ts +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
- package/src/defaults/gjc/skills/team/SKILL.md +16 -0
- package/src/discovery/helpers.ts +1 -50
- package/src/edit/index.ts +64 -0
- package/src/eval/py/env.ts +26 -0
- package/src/eval/py/kernel.ts +5 -3
- package/src/extensibility/extensions/loader.ts +9 -1
- package/src/extensibility/extensions/runner.ts +12 -3
- package/src/extensibility/extensions/types.ts +39 -1
- package/src/extensibility/gjc-plugins/installer.ts +16 -16
- package/src/extensibility/hooks/runner.ts +2 -2
- package/src/extensibility/runtime-skill-discovery.ts +33 -13
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
- package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
- package/src/gjc-runtime/deep-interview-state.ts +317 -5
- package/src/gjc-runtime/goal-mode-request.ts +55 -8
- package/src/gjc-runtime/launch-tmux.ts +2 -5
- package/src/gjc-runtime/launch-worktree.ts +5 -5
- package/src/gjc-runtime/linux-proc.ts +71 -0
- package/src/gjc-runtime/ralplan-runtime.ts +74 -90
- package/src/gjc-runtime/session-resolution.ts +18 -3
- package/src/gjc-runtime/session-state-sidecar.ts +6 -107
- package/src/gjc-runtime/state-migrations.ts +1 -1
- package/src/gjc-runtime/state-runtime.ts +320 -249
- package/src/gjc-runtime/state-writer.ts +36 -40
- package/src/gjc-runtime/team-launch.ts +319 -0
- package/src/gjc-runtime/team-notify.ts +326 -0
- package/src/gjc-runtime/team-runtime.ts +1099 -1482
- package/src/gjc-runtime/team-store.ts +828 -0
- package/src/gjc-runtime/team-workers.ts +962 -0
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
- package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
- package/src/gjc-runtime/tmux-sessions.ts +4 -21
- package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
- package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
- package/src/gjc-runtime/ultragoal-guard.ts +29 -5
- package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
- package/src/gjc-runtime/workflow-cli-common.ts +30 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +18 -0
- package/src/goals/continuation-timeout-guard.ts +79 -0
- package/src/goals/runtime.ts +7 -5
- package/src/goals/state.ts +18 -0
- package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
- package/src/harness-control-plane/phase-rollup.ts +4 -7
- package/src/harness-control-plane/receipt-ingest.ts +5 -0
- package/src/harness-control-plane/receipt-spool.ts +51 -7
- package/src/harness-control-plane/session-lease.ts +12 -130
- package/src/hindsight/backend.ts +26 -27
- package/src/hindsight/state.ts +101 -32
- package/src/hooks/skill-state.ts +17 -11
- package/src/internal-urls/docs-index.generated.ts +21 -18
- package/src/lsp/client.ts +1 -1
- package/src/lsp/config.ts +145 -26
- package/src/lsp/index.ts +1 -1
- package/src/lsp/lspmux.ts +39 -10
- package/src/lsp/path-trust.ts +81 -0
- package/src/main.ts +28 -8
- package/src/memories/index.ts +1 -0
- package/src/memories/storage.ts +6 -2
- package/src/modes/action-registry.ts +165 -0
- package/src/modes/components/chrome.ts +40 -0
- package/src/modes/components/command-palette.ts +168 -0
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/custom-provider-wizard.ts +51 -14
- package/src/modes/components/index.ts +2 -1
- package/src/modes/components/pet-selector.ts +11 -17
- package/src/modes/components/plan-preview-overlay.ts +295 -0
- package/src/modes/components/queue-mode-selector.ts +10 -27
- package/src/modes/components/queue-pane.ts +79 -0
- package/src/modes/components/read-tool-group.ts +13 -0
- package/src/modes/components/session-observer-overlay.ts +207 -772
- package/src/modes/components/sessions-dashboard.ts +181 -0
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/show-images-selector.ts +10 -24
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +1 -969
- package/src/modes/components/tasks-pane.ts +73 -0
- package/src/modes/components/theme-selector.ts +11 -31
- package/src/modes/components/tool-execution.ts +22 -0
- package/src/modes/components/tool-status-header.ts +1056 -0
- package/src/modes/components/transcript-viewer-overlay.ts +384 -0
- package/src/modes/controllers/btw-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +24 -1
- package/src/modes/controllers/goal-mode-controller.ts +474 -0
- package/src/modes/controllers/input-controller.ts +764 -229
- package/src/modes/controllers/mode-gate.ts +24 -0
- package/src/modes/controllers/plan-mode-controller.ts +517 -0
- package/src/modes/controllers/selector-controller.ts +225 -10
- package/src/modes/controllers/stt-controller.ts +125 -0
- package/src/modes/interactive-mode.ts +289 -1150
- package/src/modes/jobs-observer.ts +3 -0
- package/src/modes/runtime-init.ts +8 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
- package/src/modes/tasks-aggregator.ts +199 -0
- package/src/modes/transcript-item-registry.ts +229 -0
- package/src/modes/types.ts +45 -18
- package/src/modes/utils/ui-helpers.ts +17 -9
- package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
- package/src/prompts/agent-fragments/restricted-bash.md +1 -0
- package/src/prompts/agents/architect.md +2 -8
- package/src/prompts/agents/critic.md +2 -8
- package/src/prompts/agents/executor.md +3 -0
- package/src/prompts/agents/planner.md +4 -12
- package/src/prompts/system/custom-system-prompt.md +1 -23
- package/src/prompts/system/plan-mode-approved.md +8 -0
- package/src/prompts/system/system-prompt.md +17 -96
- package/src/prompts/tools/bash.md +4 -4
- package/src/prompts/tools/browser.md +15 -52
- package/src/prompts/tools/find.md +2 -2
- package/src/prompts/tools/hashline.md +13 -55
- package/src/prompts/tools/read.md +3 -6
- package/src/prompts/tools/search-tool-bm25.md +0 -2
- package/src/prompts/tools/search.md +2 -4
- package/src/prompts/tools/skill-discovery.md +1 -2
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-mcp/manager.ts +1 -1
- package/src/sdk/broker/broker.ts +206 -78
- package/src/sdk/broker/discovery.ts +1 -1
- package/src/sdk/broker/ensure.ts +3 -1
- package/src/sdk/broker/lifecycle-codec.ts +11 -0
- package/src/sdk/broker/lifecycle-ledger.ts +371 -74
- package/src/sdk/broker/lifecycle.ts +1399 -142
- package/src/sdk/broker/process-incarnation.ts +5 -13
- package/src/sdk/broker/session-index.ts +145 -56
- package/src/sdk/broker/state-version.ts +21 -0
- package/src/sdk/broker/transport.ts +1 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -0
- package/src/sdk/bus/chat-daemon-control.ts +8 -28
- package/src/sdk/bus/chat-effect-journal.ts +10 -2
- package/src/sdk/bus/config-commands.ts +5 -4
- package/src/sdk/bus/config.ts +98 -0
- package/src/sdk/bus/conversation-store.ts +5 -29
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/html-format.ts +20 -3
- package/src/sdk/bus/index.ts +778 -93
- package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
- package/src/sdk/bus/natives-augment.d.ts +26 -1
- package/src/sdk/bus/notification-service.ts +168 -20
- package/src/sdk/bus/rate-limit-pool.ts +58 -14
- package/src/sdk/bus/recent-activity.ts +188 -74
- package/src/sdk/bus/rich-document.ts +258 -0
- package/src/sdk/bus/rich-render.ts +62 -70
- package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
- package/src/sdk/bus/telegram-daemon.ts +1389 -237
- package/src/sdk/bus/threaded-inbound.ts +8 -3
- package/src/sdk/bus/threaded-render.ts +57 -1
- package/src/sdk/bus/topic-registry.ts +120 -13
- package/src/sdk/host/control/dispatch.ts +14 -2
- package/src/sdk/host/control/operations.ts +6 -1
- package/src/sdk/host/host.ts +31 -1
- package/src/sdk/host/query/handlers.ts +20 -22
- package/src/sdk/host/query/revision-store.ts +80 -57
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
- package/src/sdk/session-directory.ts +141 -0
- package/src/sdk/session.ts +458 -479
- package/src/session/agent-session.ts +1700 -583
- package/src/session/agent-storage.ts +18 -0
- package/src/session/cache-economics.ts +25 -8
- package/src/session/fallback-chain-controller.ts +53 -3
- package/src/session/internal/managed-session-scope.ts +2226 -0
- package/src/session/internal/managed-session-storage.ts +453 -0
- package/src/session/messages.ts +91 -2
- package/src/session/session-manager-internal.ts +33 -0
- package/src/session/session-manager.ts +1112 -333
- package/src/session/session-storage.ts +444 -39
- package/src/session/volatile-context-pruning.ts +78 -0
- package/src/setup/credential-auto-import.ts +2 -1
- package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
- package/src/setup/provider-onboarding.ts +36 -61
- package/src/setup/provider-presets.json +5 -0
- package/src/skill-state/active-state.ts +3 -13
- package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
- package/src/skill-state/workflow-state-contract.ts +6 -41
- package/src/slash-commands/builtin-registry.ts +46 -11
- package/src/system-prompt.ts +37 -44
- package/src/task/agents.ts +19 -1
- package/src/task/executor.ts +90 -6
- package/src/task/index.ts +36 -21
- package/src/thinking.ts +21 -6
- package/src/tool-discovery/tool-index.ts +2 -134
- package/src/tools/ask.ts +157 -21
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/bash.ts +38 -0
- package/src/tools/browser/dead-tab-recovery.ts +78 -0
- package/src/tools/browser/tab-supervisor.ts +282 -36
- package/src/tools/computer.ts +10 -3
- package/src/tools/cron.ts +30 -16
- package/src/tools/eval.ts +1 -2
- package/src/tools/fetch.ts +11 -4
- package/src/tools/index.ts +76 -24
- package/src/tools/output-meta.ts +13 -5
- package/src/tools/read.ts +37 -1
- package/src/tools/resource-gc.ts +2 -2
- package/src/tools/search-tool-bm25.ts +11 -74
- package/src/utils/event-bus.ts +9 -4
- package/src/web/search/index.ts +3 -3
- package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
- package/src/discovery/plugin-dir-roots.ts +0 -28
- package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
|
@@ -2,7 +2,6 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { getWorktreesDir } from "@gajae-code/utils/dirs";
|
|
5
|
-
import { SPAWN_PROVENANCE_ENV } from "../sdk/bus/config";
|
|
6
5
|
import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
7
6
|
import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
|
|
8
7
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
@@ -15,12 +14,66 @@ import {
|
|
|
15
14
|
appendJsonl as appendJsonlAudited,
|
|
16
15
|
appendText,
|
|
17
16
|
createJsonNoClobber,
|
|
18
|
-
deleteIfOwned,
|
|
19
17
|
removeFileAudited,
|
|
20
18
|
writeJsonAtomic,
|
|
21
19
|
writeReport,
|
|
22
20
|
writeWorkflowEnvelopeAtomic,
|
|
23
21
|
} from "./state-writer";
|
|
22
|
+
import { buildWorkerCommand, startGjcTeamLaunch } from "./team-launch";
|
|
23
|
+
import {
|
|
24
|
+
listTeamMailbox,
|
|
25
|
+
markTeamMailboxMessage,
|
|
26
|
+
reconcileTeamNotifications,
|
|
27
|
+
replayTeamNotifications,
|
|
28
|
+
sendTeamMessage,
|
|
29
|
+
summarizeTeamNotifications,
|
|
30
|
+
type TeamNotificationRuntime,
|
|
31
|
+
} from "./team-notify";
|
|
32
|
+
import type {
|
|
33
|
+
GjcTeamApiClaimResult,
|
|
34
|
+
GjcTeamMailboxDeliveryTransport,
|
|
35
|
+
GjcTeamNotification,
|
|
36
|
+
GjcTeamNotificationDeliveryState,
|
|
37
|
+
GjcTeamNotificationSummary,
|
|
38
|
+
GjcTeamPaneAttemptResult,
|
|
39
|
+
GjcTeamTask,
|
|
40
|
+
GjcTeamTaskClaim,
|
|
41
|
+
GjcTeamTaskMetadataInput,
|
|
42
|
+
GjcTeamTaskMutationCapability,
|
|
43
|
+
GjcTeamTaskStatus,
|
|
44
|
+
} from "./team-store";
|
|
45
|
+
import {
|
|
46
|
+
GjcTeamTaskStore,
|
|
47
|
+
isCanonicalPersistedGjcTeamTask,
|
|
48
|
+
isCanonicalPersistedGjcTeamTaskClaim,
|
|
49
|
+
isGjcTeamTaskCompletionVerified,
|
|
50
|
+
readGjcTeamTasksFromDir as readTasks,
|
|
51
|
+
taskMetadataFromInput,
|
|
52
|
+
withGjcTeamMutationFence,
|
|
53
|
+
withGjcTeamTaskMutation,
|
|
54
|
+
} from "./team-store";
|
|
55
|
+
|
|
56
|
+
import {
|
|
57
|
+
GJC_TEAM_CONTINUATION_PROMPT,
|
|
58
|
+
type GjcTeamWorkerOrchestrationRuntime,
|
|
59
|
+
type GjcTeamWorkerRuntime,
|
|
60
|
+
gjcContinuationReservationDigest,
|
|
61
|
+
isValidGjcContinuationOutcome,
|
|
62
|
+
isValidGjcContinuationReservation,
|
|
63
|
+
readGjcShutdownAuthority,
|
|
64
|
+
readWorkerLifecycleById as readLifecycleById,
|
|
65
|
+
readGjcShutdownAck as readShutdownAck,
|
|
66
|
+
readGjcWorkerHeartbeat as readWorkerHeartbeat,
|
|
67
|
+
readGjcWorkerStatus as readWorkerStatus,
|
|
68
|
+
reconcileGjcTeamStaleClaimsUnlocked,
|
|
69
|
+
shutdownGjcTeamWorkers,
|
|
70
|
+
updateGjcWorkerHeartbeat as updateWorkerHeartbeat,
|
|
71
|
+
updateGjcWorkerStatus as updateWorkerStatus,
|
|
72
|
+
writeWorkerLifecycleForConfig as writeLifecycleForConfig,
|
|
73
|
+
writeGjcShutdownRequest as writeShutdownRequest,
|
|
74
|
+
writeGjcWorkerStartupAck as writeWorkerStartupAck,
|
|
75
|
+
} from "./team-workers";
|
|
76
|
+
|
|
24
77
|
import {
|
|
25
78
|
buildGjcTmuxExactOptionTarget,
|
|
26
79
|
buildGjcTmuxUntaggedSessionHint,
|
|
@@ -31,18 +84,21 @@ import {
|
|
|
31
84
|
resolveGjcTmuxCommand,
|
|
32
85
|
} from "./tmux-common";
|
|
33
86
|
|
|
34
|
-
export type
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
87
|
+
export type {
|
|
88
|
+
GjcTeamApiClaimResult,
|
|
89
|
+
GjcTeamMailboxDeliveryInput,
|
|
90
|
+
GjcTeamMailboxDeliveryResult,
|
|
91
|
+
GjcTeamMailboxDeliveryTransport,
|
|
92
|
+
GjcTeamMailboxDeliveryTransportKind,
|
|
93
|
+
GjcTeamNotification,
|
|
94
|
+
GjcTeamNotificationDeliveryState,
|
|
95
|
+
GjcTeamNotificationSummary,
|
|
96
|
+
GjcTeamPaneAttemptResult,
|
|
97
|
+
GjcTeamTask,
|
|
98
|
+
GjcTeamTaskClaim,
|
|
99
|
+
GjcTeamTaskMetadataInput,
|
|
100
|
+
GjcTeamTaskStatus,
|
|
101
|
+
} from "./team-store";
|
|
46
102
|
|
|
47
103
|
export const GJC_TEAM_DEFAULT_WORKERS = 3;
|
|
48
104
|
export const GJC_TEAM_MAX_WORKERS = 20;
|
|
@@ -76,57 +132,17 @@ export interface GjcTeamWorker {
|
|
|
76
132
|
worktree_base_ref?: string;
|
|
77
133
|
team_state_root?: string;
|
|
78
134
|
}
|
|
79
|
-
|
|
80
|
-
export
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
status: GjcTeamTaskCompletionEvidenceStatus;
|
|
91
|
-
summary: string;
|
|
92
|
-
command?: string;
|
|
93
|
-
artifact?: string;
|
|
94
|
-
location?: string;
|
|
95
|
-
output?: string;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export interface GjcTeamTaskCompletionEvidence {
|
|
99
|
-
summary: string;
|
|
100
|
-
items: GjcTeamTaskCompletionEvidenceItem[];
|
|
101
|
-
files?: string[];
|
|
102
|
-
notes?: string;
|
|
103
|
-
recorded_by: string;
|
|
104
|
-
recorded_at: string;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export interface GjcTeamTask {
|
|
108
|
-
id: string;
|
|
109
|
-
subject: string;
|
|
110
|
-
description: string;
|
|
111
|
-
title: string;
|
|
112
|
-
objective: string;
|
|
113
|
-
status: GjcTeamTaskStatus;
|
|
114
|
-
assignee?: string;
|
|
115
|
-
owner?: string;
|
|
116
|
-
result?: string;
|
|
117
|
-
completion_evidence?: GjcTeamTaskCompletionEvidence;
|
|
118
|
-
error?: string;
|
|
119
|
-
blocked_by?: string[];
|
|
120
|
-
depends_on?: string[];
|
|
121
|
-
lane?: string;
|
|
122
|
-
required_role?: string;
|
|
123
|
-
allowed_roles?: string[];
|
|
124
|
-
version: number;
|
|
125
|
-
claim?: GjcTeamTaskClaim;
|
|
126
|
-
created_at: string;
|
|
127
|
-
updated_at: string;
|
|
128
|
-
completed_at?: string;
|
|
129
|
-
}
|
|
135
|
+
export type GjcTeamPhase = "starting" | "running" | "awaiting_integration" | "complete" | "failed" | "cancelled";
|
|
136
|
+
export type GjcWorkerStatusState = "idle" | "working" | "blocked" | "done" | "failed" | "draining" | "unknown";
|
|
137
|
+
export type GjcTeamWorkerLifecycleState =
|
|
138
|
+
| "starting"
|
|
139
|
+
| "ready"
|
|
140
|
+
| "working"
|
|
141
|
+
| "draining"
|
|
142
|
+
| "stopped"
|
|
143
|
+
| "failed"
|
|
144
|
+
| "unknown";
|
|
145
|
+
export type GjcTeamShutdownMode = "graceful" | "force" | "abort";
|
|
130
146
|
|
|
131
147
|
export type GjcTeamWorktreeMode =
|
|
132
148
|
| { enabled: false }
|
|
@@ -142,6 +158,7 @@ export interface GjcTeamConfig {
|
|
|
142
158
|
worker_count: number;
|
|
143
159
|
max_workers: number;
|
|
144
160
|
state_root: string;
|
|
161
|
+
gjc_session_id?: string;
|
|
145
162
|
worker_command: string;
|
|
146
163
|
worker_cli_plan: GjcTeamWorkerCli[];
|
|
147
164
|
tmux_command: string;
|
|
@@ -198,70 +215,6 @@ export interface GjcTeamWorkerLifecycle {
|
|
|
198
215
|
shutdown_mode?: GjcTeamShutdownMode;
|
|
199
216
|
}
|
|
200
217
|
|
|
201
|
-
export type GjcTeamNotificationDeliveryState =
|
|
202
|
-
| "pending"
|
|
203
|
-
| "sent"
|
|
204
|
-
| "queued"
|
|
205
|
-
| "deferred"
|
|
206
|
-
| "failed"
|
|
207
|
-
| "delivered"
|
|
208
|
-
| "acknowledged";
|
|
209
|
-
|
|
210
|
-
export type GjcTeamPaneAttemptResult = "sent" | "queued" | "deferred" | "failed";
|
|
211
|
-
export type GjcTeamMailboxDeliveryTransportKind = "sdk" | "pane";
|
|
212
|
-
|
|
213
|
-
export interface GjcTeamNotification {
|
|
214
|
-
id: string;
|
|
215
|
-
kind: "mailbox_message" | "worker_lifecycle" | "invalid_attempt";
|
|
216
|
-
team_name: string;
|
|
217
|
-
recipient: string;
|
|
218
|
-
source: { type: "message" | "task" | "worker" | "event"; id: string };
|
|
219
|
-
idempotency_key?: string;
|
|
220
|
-
delivery_state: GjcTeamNotificationDeliveryState;
|
|
221
|
-
pane_attempt_result?: GjcTeamPaneAttemptResult;
|
|
222
|
-
pane_attempt_reason?: string;
|
|
223
|
-
pane_attempt_at?: string;
|
|
224
|
-
created_at: string;
|
|
225
|
-
updated_at: string;
|
|
226
|
-
replay_count: number;
|
|
227
|
-
}
|
|
228
|
-
export interface GjcTeamMailboxDeliveryInput {
|
|
229
|
-
team_name: string;
|
|
230
|
-
state_dir: string;
|
|
231
|
-
config: GjcTeamConfig;
|
|
232
|
-
notification: GjcTeamNotification;
|
|
233
|
-
message: GjcTeamMailboxMessage;
|
|
234
|
-
cwd: string;
|
|
235
|
-
env: NodeJS.ProcessEnv;
|
|
236
|
-
}
|
|
237
|
-
export type GjcTeamMailboxDeliveryResult =
|
|
238
|
-
| { transport: "sdk"; state: GjcTeamNotificationDeliveryState; reason?: string }
|
|
239
|
-
| { transport: "pane"; state: GjcTeamPaneAttemptResult; reason?: string };
|
|
240
|
-
export interface GjcTeamMailboxDeliveryTransport {
|
|
241
|
-
deliverMailboxMessage(input: GjcTeamMailboxDeliveryInput): Promise<GjcTeamMailboxDeliveryResult | null>;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
let gjcTeamMailboxDeliveryTransport: GjcTeamMailboxDeliveryTransport | undefined;
|
|
245
|
-
|
|
246
|
-
export function setGjcTeamMailboxDeliveryTransport(transport: GjcTeamMailboxDeliveryTransport | undefined): () => void {
|
|
247
|
-
const previous = gjcTeamMailboxDeliveryTransport;
|
|
248
|
-
gjcTeamMailboxDeliveryTransport = transport;
|
|
249
|
-
return () => {
|
|
250
|
-
gjcTeamMailboxDeliveryTransport = previous;
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
export function setGjcTeamMailboxDeliveryTransportForTest(
|
|
254
|
-
transport: GjcTeamMailboxDeliveryTransport | undefined,
|
|
255
|
-
): () => void {
|
|
256
|
-
return setGjcTeamMailboxDeliveryTransport(transport);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export interface GjcTeamNotificationSummary {
|
|
260
|
-
total: number;
|
|
261
|
-
replay_eligible: number;
|
|
262
|
-
by_state: Record<GjcTeamNotificationDeliveryState, number>;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
218
|
export interface GjcTeamSnapshot {
|
|
266
219
|
team_name: string;
|
|
267
220
|
display_name: string;
|
|
@@ -295,13 +248,6 @@ export interface GjcTeamStartOptions {
|
|
|
295
248
|
mailboxDeliveryTransport?: GjcTeamMailboxDeliveryTransport;
|
|
296
249
|
}
|
|
297
250
|
|
|
298
|
-
export interface GjcTeamApiClaimResult {
|
|
299
|
-
ok: boolean;
|
|
300
|
-
task?: GjcTeamTask;
|
|
301
|
-
worker_id?: string;
|
|
302
|
-
claim_token?: string;
|
|
303
|
-
reason?: string;
|
|
304
|
-
}
|
|
305
251
|
export type GjcTeamLivenessRecoveryReason =
|
|
306
252
|
| "claim_expired"
|
|
307
253
|
| "stale_heartbeat"
|
|
@@ -370,7 +316,10 @@ function notificationReceiptFields(notification: GjcTeamNotification): Record<st
|
|
|
370
316
|
|
|
371
317
|
function notificationSummaryReceipt(
|
|
372
318
|
teamName: string,
|
|
373
|
-
result: {
|
|
319
|
+
result: {
|
|
320
|
+
notifications: GjcTeamNotification[];
|
|
321
|
+
summary: GjcTeamNotificationSummary;
|
|
322
|
+
},
|
|
374
323
|
): Record<string, unknown> {
|
|
375
324
|
return {
|
|
376
325
|
team_name: teamName,
|
|
@@ -475,13 +424,13 @@ export interface GjcTeamTraceEvent {
|
|
|
475
424
|
evidence_refs?: string[];
|
|
476
425
|
data?: Record<string, unknown>;
|
|
477
426
|
}
|
|
478
|
-
interface WorkerStatusFile {
|
|
427
|
+
export interface WorkerStatusFile {
|
|
479
428
|
state: GjcWorkerStatusState;
|
|
480
429
|
current_task_id?: string;
|
|
481
430
|
reason?: string;
|
|
482
431
|
updated_at: string;
|
|
483
432
|
}
|
|
484
|
-
interface WorkerHeartbeatFile {
|
|
433
|
+
export interface WorkerHeartbeatFile {
|
|
485
434
|
pid: number;
|
|
486
435
|
last_turn_at: string;
|
|
487
436
|
turn_count: number;
|
|
@@ -530,6 +479,10 @@ export interface GjcWorkerIntegrationAttemptRequestResult {
|
|
|
530
479
|
status?: GjcWorkerCheckpointClassification["kind"];
|
|
531
480
|
}
|
|
532
481
|
|
|
482
|
+
export interface GjcWorkerIntegrationAttemptOptions {
|
|
483
|
+
signal?: AbortSignal;
|
|
484
|
+
}
|
|
485
|
+
|
|
533
486
|
function isGjcTeamTaskStatus(value: string): value is GjcTeamTaskStatus {
|
|
534
487
|
return ["pending", "blocked", "in_progress", "completed", "failed"].includes(value);
|
|
535
488
|
}
|
|
@@ -581,9 +534,28 @@ export const GJC_TEAM_API_OPERATIONS = [
|
|
|
581
534
|
"write-task-approval",
|
|
582
535
|
] as const;
|
|
583
536
|
|
|
537
|
+
function currentTimeMs(): number {
|
|
538
|
+
return gjcTeamRuntimeTestSeams?.nowMs?.() ?? Date.now();
|
|
539
|
+
}
|
|
540
|
+
|
|
584
541
|
function now(): string {
|
|
585
|
-
return new Date().toISOString();
|
|
542
|
+
return new Date(currentTimeMs()).toISOString();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export interface GjcTeamRuntimeTestSeams {
|
|
546
|
+
nowMs?: () => number;
|
|
547
|
+
continuationTmuxDispatch?: (command: string, args: readonly string[]) => { exitCode?: number };
|
|
548
|
+
|
|
549
|
+
continuationBeforeDispatch?: () => Promise<void>;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
let gjcTeamRuntimeTestSeams: GjcTeamRuntimeTestSeams | undefined;
|
|
553
|
+
|
|
554
|
+
/** @internal Test-only seam; production code leaves the real clock and tmux dispatch intact. */
|
|
555
|
+
export function __setGjcTeamRuntimeTestSeamsForTests(seams: GjcTeamRuntimeTestSeams | undefined): void {
|
|
556
|
+
gjcTeamRuntimeTestSeams = seams;
|
|
586
557
|
}
|
|
558
|
+
|
|
587
559
|
function isEnoent(error: unknown): error is FsError {
|
|
588
560
|
return typeof error === "object" && error !== null && "code" in error && (error as FsError).code === "ENOENT";
|
|
589
561
|
}
|
|
@@ -599,7 +571,12 @@ function stateWriterOptions(filePath: string, category: "state" | "ledger" | "re
|
|
|
599
571
|
// Session-scoped audit requires a GJC session. When an explicit env-root override
|
|
600
572
|
// (e.g. GJC_TEAM_STATE_ROOT) is in effect with no resolvable session, omit the audit
|
|
601
573
|
// context entirely so the override write does not fail on a session-scoped audit.
|
|
602
|
-
return sessionId
|
|
574
|
+
return sessionId
|
|
575
|
+
? {
|
|
576
|
+
cwd,
|
|
577
|
+
audit: { category, verb, owner: "gjc-runtime" as const, sessionId },
|
|
578
|
+
}
|
|
579
|
+
: { cwd };
|
|
603
580
|
}
|
|
604
581
|
|
|
605
582
|
function sanitizeName(value: string): string {
|
|
@@ -646,9 +623,6 @@ function safePathSegment(kind: string, value: string): string {
|
|
|
646
623
|
assertSafeId(kind, value);
|
|
647
624
|
return value;
|
|
648
625
|
}
|
|
649
|
-
function taskPath(dir: string, taskId: string): string {
|
|
650
|
-
return path.join(dir, "tasks", `${safePathSegment("task_id", taskId)}.json`);
|
|
651
|
-
}
|
|
652
626
|
function mailboxPath(dir: string, worker: string): string {
|
|
653
627
|
return path.join(dir, "mailbox", `${safePathSegment("worker_id", worker)}.json`);
|
|
654
628
|
}
|
|
@@ -723,7 +697,9 @@ function workerIntegrationDedupePath(dir: string, worker: string): string {
|
|
|
723
697
|
export function resolveGjcTeamStateRoot(cwd = process.cwd(), env: NodeJS.ProcessEnv = process.env): string {
|
|
724
698
|
const explicit = env.GJC_TEAM_STATE_ROOT?.trim();
|
|
725
699
|
if (explicit) return path.resolve(cwd, explicit);
|
|
726
|
-
const session = resolveGjcSessionForWrite(cwd, {
|
|
700
|
+
const session = resolveGjcSessionForWrite(cwd, {
|
|
701
|
+
envSessionId: env.GJC_SESSION_ID,
|
|
702
|
+
});
|
|
727
703
|
return teamStateRoot(cwd, session.gjcSessionId);
|
|
728
704
|
}
|
|
729
705
|
|
|
@@ -735,6 +711,110 @@ async function readJsonFile<T>(filePath: string): Promise<T | null> {
|
|
|
735
711
|
throw error;
|
|
736
712
|
}
|
|
737
713
|
}
|
|
714
|
+
|
|
715
|
+
async function readContinuationJson<T>(filePath: string): Promise<T | null> {
|
|
716
|
+
try {
|
|
717
|
+
return await readJsonFile<T>(filePath);
|
|
718
|
+
} catch {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
type GjcContinuationAuthorityInventory =
|
|
723
|
+
| { valid: true; tasks: GjcTeamTask[]; claims: Map<string, GjcTeamTaskClaim> }
|
|
724
|
+
| { valid: false };
|
|
725
|
+
|
|
726
|
+
async function readGjcContinuationAuthorityInventory(dir: string): Promise<GjcContinuationAuthorityInventory> {
|
|
727
|
+
let taskNames: string[];
|
|
728
|
+
try {
|
|
729
|
+
taskNames = (await fs.readdir(path.join(dir, "tasks"), { withFileTypes: true }))
|
|
730
|
+
.filter(entry => entry.isFile() && entry.name.endsWith(".json") && !entry.name.endsWith(".evidence.json"))
|
|
731
|
+
.map(entry => entry.name);
|
|
732
|
+
} catch {
|
|
733
|
+
return { valid: false };
|
|
734
|
+
}
|
|
735
|
+
const tasks: GjcTeamTask[] = [];
|
|
736
|
+
const taskIds = new Set<string>();
|
|
737
|
+
for (const name of taskNames) {
|
|
738
|
+
const id = name.slice(0, -".json".length);
|
|
739
|
+
try {
|
|
740
|
+
const task = await Bun.file(path.join(dir, "tasks", name)).json();
|
|
741
|
+
if (!isCanonicalPersistedGjcTeamTask(task, id) || taskIds.has(task.id)) return { valid: false };
|
|
742
|
+
taskIds.add(task.id);
|
|
743
|
+
tasks.push(task);
|
|
744
|
+
} catch {
|
|
745
|
+
return { valid: false };
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
let claimNames: string[];
|
|
749
|
+
try {
|
|
750
|
+
claimNames = (await fs.readdir(path.join(dir, "claims"), { withFileTypes: true }))
|
|
751
|
+
.filter(entry => entry.isFile() && entry.name.endsWith(".json"))
|
|
752
|
+
.map(entry => entry.name);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
if (!isEnoent(error)) return { valid: false };
|
|
755
|
+
claimNames = [];
|
|
756
|
+
}
|
|
757
|
+
const claims = new Map<string, GjcTeamTaskClaim>();
|
|
758
|
+
for (const name of claimNames) {
|
|
759
|
+
const id = name.slice(0, -".json".length);
|
|
760
|
+
if (!taskIds.has(id) || claims.has(id)) return { valid: false };
|
|
761
|
+
try {
|
|
762
|
+
const claim = await Bun.file(path.join(dir, "claims", name)).json();
|
|
763
|
+
if (!isCanonicalPersistedGjcTeamTaskClaim(claim)) return { valid: false };
|
|
764
|
+
claims.set(id, claim);
|
|
765
|
+
} catch {
|
|
766
|
+
return { valid: false };
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
for (const task of tasks) {
|
|
770
|
+
const claim = claims.get(task.id);
|
|
771
|
+
if ((task.claim === undefined) !== (claim === undefined)) return { valid: false };
|
|
772
|
+
if (
|
|
773
|
+
task.claim !== undefined &&
|
|
774
|
+
(!claim ||
|
|
775
|
+
claim.owner !== task.claim.owner ||
|
|
776
|
+
claim.token !== task.claim.token ||
|
|
777
|
+
claim.leased_until !== task.claim.leased_until)
|
|
778
|
+
)
|
|
779
|
+
return { valid: false };
|
|
780
|
+
}
|
|
781
|
+
return { valid: true, tasks, claims };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
type GjcContinuationWorkerAuthority =
|
|
785
|
+
| { valid: true; task: GjcTeamTask & { claim: GjcTeamTaskClaim }; claim: GjcTeamTaskClaim }
|
|
786
|
+
| { valid: false; taskCount: number; claimCount: number };
|
|
787
|
+
|
|
788
|
+
function selectGjcContinuationWorkerAuthority(
|
|
789
|
+
inventory: Extract<GjcContinuationAuthorityInventory, { valid: true }>,
|
|
790
|
+
worker: string,
|
|
791
|
+
): GjcContinuationWorkerAuthority {
|
|
792
|
+
const tasks = inventory.tasks.filter(
|
|
793
|
+
task =>
|
|
794
|
+
task.status === "in_progress" &&
|
|
795
|
+
task.claim?.owner === worker &&
|
|
796
|
+
task.assignee === worker &&
|
|
797
|
+
task.owner === worker,
|
|
798
|
+
);
|
|
799
|
+
const claims = [...inventory.claims.entries()]
|
|
800
|
+
.filter(([, claim]) => claim.owner === worker)
|
|
801
|
+
.map(([taskId, claim]) => ({ taskId, claim }));
|
|
802
|
+
if (tasks.length !== 1 || claims.length !== 1)
|
|
803
|
+
return { valid: false, taskCount: tasks.length, claimCount: claims.length };
|
|
804
|
+
const task = tasks[0];
|
|
805
|
+
const stored = claims[0];
|
|
806
|
+
if (
|
|
807
|
+
!task?.claim ||
|
|
808
|
+
!stored ||
|
|
809
|
+
stored.taskId !== task.id ||
|
|
810
|
+
stored.claim.owner !== task.claim.owner ||
|
|
811
|
+
stored.claim.token !== task.claim.token ||
|
|
812
|
+
stored.claim.leased_until !== task.claim.leased_until
|
|
813
|
+
)
|
|
814
|
+
return { valid: false, taskCount: tasks.length, claimCount: claims.length };
|
|
815
|
+
return { valid: true, task: { ...task, claim: task.claim }, claim: stored.claim };
|
|
816
|
+
}
|
|
817
|
+
|
|
738
818
|
function isPositivePid(value: unknown): value is number {
|
|
739
819
|
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
740
820
|
}
|
|
@@ -763,7 +843,10 @@ interface TeamGcPidClassification {
|
|
|
763
843
|
*/
|
|
764
844
|
function classifyTeamGcWorkerPids(pids: number[], probe: GcPidProbe): TeamGcPidClassification {
|
|
765
845
|
if (pids.length === 0) return { removable: false, pidStatus: "none" };
|
|
766
|
-
const statuses = pids.map(pid => ({
|
|
846
|
+
const statuses = pids.map(pid => ({
|
|
847
|
+
pid,
|
|
848
|
+
status: gcProbeStatus(probe, pid),
|
|
849
|
+
}));
|
|
767
850
|
const kept = statuses.find(entry => entry.status !== "dead");
|
|
768
851
|
if (kept) return { removable: false, pidStatus: kept.status, pid: kept.pid };
|
|
769
852
|
return { removable: true, pidStatus: "dead", pid: statuses[0]?.pid };
|
|
@@ -795,7 +878,9 @@ export async function listTeamWorkerGcRecords(teamRoot: string, probe: GcPidProb
|
|
|
795
878
|
const teamDirPath = path.join(teamRoot, teamName);
|
|
796
879
|
let workerEntries: import("node:fs").Dirent[];
|
|
797
880
|
try {
|
|
798
|
-
workerEntries = await fs.readdir(path.join(teamDirPath, "workers"), {
|
|
881
|
+
workerEntries = await fs.readdir(path.join(teamDirPath, "workers"), {
|
|
882
|
+
withFileTypes: true,
|
|
883
|
+
});
|
|
799
884
|
} catch (error) {
|
|
800
885
|
if (isEnoent(error)) continue;
|
|
801
886
|
throw error;
|
|
@@ -863,6 +948,18 @@ export async function listTeamWorkerGcRecords(teamRoot: string, probe: GcPidProb
|
|
|
863
948
|
|
|
864
949
|
/** @internal */
|
|
865
950
|
export async function pruneTeamWorkerGcRecord(record: GcRecord, probe: GcPidProbe): Promise<boolean> {
|
|
951
|
+
if (!record.path || !record.id.includes("/")) return false;
|
|
952
|
+
const teamDirPath = path.dirname(path.dirname(record.path));
|
|
953
|
+
return withGjcTeamTaskMutation(taskStore(teamDirPath), capability =>
|
|
954
|
+
pruneTeamWorkerGcRecordUnlocked(record, probe, capability),
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
async function pruneTeamWorkerGcRecordUnlocked(
|
|
959
|
+
record: GcRecord,
|
|
960
|
+
probe: GcPidProbe,
|
|
961
|
+
capability: GjcTeamTaskMutationCapability,
|
|
962
|
+
): Promise<boolean> {
|
|
866
963
|
if (!record.path || !record.id.includes("/")) return false;
|
|
867
964
|
const [teamName, workerId] = record.id.split("/", 2);
|
|
868
965
|
if (!teamName || !workerId) return false;
|
|
@@ -888,7 +985,7 @@ export async function pruneTeamWorkerGcRecord(record: GcRecord, probe: GcPidProb
|
|
|
888
985
|
for (const task of await readTasks(teamDirPath)) {
|
|
889
986
|
if (task.claim?.owner !== workerId && task.assignee !== workerId) continue;
|
|
890
987
|
if (task.status === "completed" || task.status === "failed") continue;
|
|
891
|
-
await
|
|
988
|
+
await capability.writeRecovered({
|
|
892
989
|
...task,
|
|
893
990
|
status: "pending",
|
|
894
991
|
assignee: undefined,
|
|
@@ -1065,7 +1162,11 @@ async function appendTraceForEvent(dir: string, event: GjcTeamEvent): Promise<vo
|
|
|
1065
1162
|
}
|
|
1066
1163
|
}
|
|
1067
1164
|
async function appendEvent(dir: string, event: Omit<GjcTeamEvent, "ts" | "event_id">): Promise<GjcTeamEvent> {
|
|
1068
|
-
const full = {
|
|
1165
|
+
const full = {
|
|
1166
|
+
event_id: `evt-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
1167
|
+
ts: now(),
|
|
1168
|
+
...event,
|
|
1169
|
+
};
|
|
1069
1170
|
await appendJsonl(path.join(dir, "events.jsonl"), full);
|
|
1070
1171
|
await appendTraceForEvent(dir, full);
|
|
1071
1172
|
return full;
|
|
@@ -1094,7 +1195,11 @@ async function readConfig(dir: string): Promise<GjcTeamConfig> {
|
|
|
1094
1195
|
};
|
|
1095
1196
|
}
|
|
1096
1197
|
const WORKER_INTEGRATION_CONFIG_CACHE_TTL_MS = 100;
|
|
1097
|
-
type WorkerIntegrationConfigCacheEntry = {
|
|
1198
|
+
type WorkerIntegrationConfigCacheEntry = {
|
|
1199
|
+
checkedAt: number;
|
|
1200
|
+
mtimeMs: number;
|
|
1201
|
+
config: GjcTeamConfig;
|
|
1202
|
+
};
|
|
1098
1203
|
const workerIntegrationConfigCache = new Map<string, WorkerIntegrationConfigCacheEntry>();
|
|
1099
1204
|
|
|
1100
1205
|
async function readConfigForWorkerIntegration(dir: string): Promise<GjcTeamConfig> {
|
|
@@ -1105,142 +1210,80 @@ async function readConfigForWorkerIntegration(dir: string): Promise<GjcTeamConfi
|
|
|
1105
1210
|
if (cached && cached.mtimeMs === stat.mtimeMs && nowMs - cached.checkedAt <= WORKER_INTEGRATION_CONFIG_CACHE_TTL_MS)
|
|
1106
1211
|
return cached.config;
|
|
1107
1212
|
const config = await readConfig(dir);
|
|
1108
|
-
workerIntegrationConfigCache.set(configPath, {
|
|
1213
|
+
workerIntegrationConfigCache.set(configPath, {
|
|
1214
|
+
checkedAt: nowMs,
|
|
1215
|
+
mtimeMs: stat.mtimeMs,
|
|
1216
|
+
config,
|
|
1217
|
+
});
|
|
1109
1218
|
|
|
1110
1219
|
return config;
|
|
1111
1220
|
}
|
|
1112
1221
|
async function readPhase(dir: string): Promise<GjcTeamPhase> {
|
|
1113
|
-
|
|
1114
|
-
|
|
1222
|
+
try {
|
|
1223
|
+
const phase = await readJsonFile<{ current_phase?: GjcTeamPhase }>(path.join(dir, "phase.json"));
|
|
1224
|
+
return phase?.current_phase ?? "running";
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
if (error instanceof SyntaxError) return "running";
|
|
1227
|
+
throw error;
|
|
1228
|
+
}
|
|
1115
1229
|
}
|
|
1116
1230
|
async function writePhase(dir: string, phase: GjcTeamPhase): Promise<void> {
|
|
1117
|
-
await writeJsonFile(path.join(dir, "phase.json"), {
|
|
1231
|
+
await writeJsonFile(path.join(dir, "phase.json"), {
|
|
1232
|
+
current_phase: phase,
|
|
1233
|
+
updated_at: now(),
|
|
1234
|
+
});
|
|
1118
1235
|
}
|
|
1119
1236
|
function isGjcWorkerStatusState(value: string): value is GjcWorkerStatusState {
|
|
1120
1237
|
return ["idle", "working", "blocked", "done", "failed", "draining", "unknown"].includes(value);
|
|
1121
1238
|
}
|
|
1122
|
-
|
|
1123
|
-
function parseGjcWorkerStatusState(value: unknown): GjcWorkerStatusState {
|
|
1124
|
-
return typeof value === "string" && isGjcWorkerStatusState(value) ? value : "unknown";
|
|
1125
|
-
}
|
|
1126
1239
|
function parseRequiredGjcWorkerStatusState(value: unknown): GjcWorkerStatusState {
|
|
1127
1240
|
const raw = typeof value === "string" ? value.trim() : "";
|
|
1128
1241
|
if (isGjcWorkerStatusState(raw)) return raw;
|
|
1129
1242
|
throw new Error(`invalid_worker_status:${raw}`);
|
|
1130
1243
|
}
|
|
1131
|
-
|
|
1132
|
-
function lifecycleStateForWorkerStatus(status: GjcWorkerStatusState): GjcTeamWorkerLifecycleState {
|
|
1133
|
-
switch (status) {
|
|
1134
|
-
case "working":
|
|
1135
|
-
return "working";
|
|
1136
|
-
case "draining":
|
|
1137
|
-
return "draining";
|
|
1138
|
-
case "failed":
|
|
1139
|
-
return "failed";
|
|
1140
|
-
case "unknown":
|
|
1141
|
-
return "unknown";
|
|
1142
|
-
case "idle":
|
|
1143
|
-
case "blocked":
|
|
1144
|
-
case "done":
|
|
1145
|
-
return "ready";
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
1244
|
function parseGjcTeamShutdownMode(value: unknown): GjcTeamShutdownMode {
|
|
1150
1245
|
const raw = typeof value === "string" ? value.trim() : "graceful";
|
|
1151
1246
|
if (raw === "graceful" || raw === "force" || raw === "abort") return raw;
|
|
1152
1247
|
throw new Error(`invalid_shutdown_mode:${raw}`);
|
|
1153
1248
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
async function readWorkerLifecycleRecord(dir: string, worker: GjcTeamWorker): Promise<GjcTeamWorkerLifecycle> {
|
|
1173
|
-
const workerStatus = await readWorkerStatusFile(dir, worker.id);
|
|
1174
|
-
const heartbeat = await readJsonFile<WorkerHeartbeatFile>(path.join(workerDir(dir, worker.id), "heartbeat.json"));
|
|
1175
|
-
const rawLifecycle = await readJsonFile<Partial<GjcTeamWorkerLifecycle>>(workerLifecyclePath(dir, worker.id));
|
|
1176
|
-
const shutdownAck = await readJsonFile<Record<string, unknown>>(
|
|
1177
|
-
path.join(workerDir(dir, worker.id), "shutdown-ack.json"),
|
|
1178
|
-
);
|
|
1179
|
-
const lifecycle: GjcTeamWorkerLifecycle = {
|
|
1180
|
-
worker: worker.id,
|
|
1181
|
-
lifecycle_state: parseGjcTeamWorkerLifecycleState(rawLifecycle?.lifecycle_state),
|
|
1182
|
-
worker_status_state: parseGjcWorkerStatusState(workerStatus.state),
|
|
1183
|
-
pane_id: worker.pane_id ?? rawLifecycle?.pane_id,
|
|
1184
|
-
updated_at: rawLifecycle?.updated_at ?? workerStatus.updated_at ?? now(),
|
|
1185
|
-
};
|
|
1186
|
-
if (typeof rawLifecycle?.pid === "number") lifecycle.pid = rawLifecycle.pid;
|
|
1187
|
-
else if (typeof heartbeat?.pid === "number") lifecycle.pid = heartbeat.pid;
|
|
1188
|
-
if (rawLifecycle?.started_at) lifecycle.started_at = rawLifecycle.started_at;
|
|
1189
|
-
if (rawLifecycle?.stopped_at) lifecycle.stopped_at = rawLifecycle.stopped_at;
|
|
1190
|
-
if (rawLifecycle?.stop_reason) lifecycle.stop_reason = rawLifecycle.stop_reason;
|
|
1191
|
-
if (rawLifecycle?.shutdown_request_id) lifecycle.shutdown_request_id = rawLifecycle.shutdown_request_id;
|
|
1192
|
-
if (rawLifecycle?.shutdown_requested_at) lifecycle.shutdown_requested_at = rawLifecycle.shutdown_requested_at;
|
|
1193
|
-
if (
|
|
1194
|
-
rawLifecycle?.shutdown_mode === "graceful" ||
|
|
1195
|
-
rawLifecycle?.shutdown_mode === "force" ||
|
|
1196
|
-
rawLifecycle?.shutdown_mode === "abort"
|
|
1197
|
-
)
|
|
1198
|
-
lifecycle.shutdown_mode = rawLifecycle.shutdown_mode;
|
|
1199
|
-
if (typeof shutdownAck?.acknowledged_at === "string")
|
|
1200
|
-
lifecycle.shutdown_acknowledged_at = shutdownAck.acknowledged_at;
|
|
1201
|
-
if (typeof shutdownAck?.status === "string") lifecycle.shutdown_ack_status = shutdownAck.status;
|
|
1202
|
-
return lifecycle;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
async function readWorkerLifecycleById(
|
|
1206
|
-
dir: string,
|
|
1207
|
-
config: GjcTeamConfig,
|
|
1208
|
-
): Promise<Record<string, GjcTeamWorkerLifecycle>> {
|
|
1209
|
-
const entries = await Promise.all(config.workers.map(worker => readWorkerLifecycleRecord(dir, worker)));
|
|
1210
|
-
return Object.fromEntries(entries.map(entry => [entry.worker, entry]));
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
async function writeWorkerLifecycleRecord(
|
|
1214
|
-
dir: string,
|
|
1215
|
-
worker: GjcTeamWorker,
|
|
1216
|
-
lifecycleState: GjcTeamWorkerLifecycleState,
|
|
1217
|
-
updates: Partial<GjcTeamWorkerLifecycle> = {},
|
|
1218
|
-
): Promise<GjcTeamWorkerLifecycle> {
|
|
1219
|
-
const current = await readWorkerLifecycleRecord(dir, worker);
|
|
1220
|
-
const next: GjcTeamWorkerLifecycle = {
|
|
1221
|
-
...current,
|
|
1222
|
-
...updates,
|
|
1223
|
-
worker: worker.id,
|
|
1224
|
-
lifecycle_state: lifecycleState,
|
|
1225
|
-
worker_status_state: current.worker_status_state,
|
|
1226
|
-
pane_id: updates.pane_id ?? worker.pane_id ?? current.pane_id,
|
|
1227
|
-
updated_at: now(),
|
|
1228
|
-
};
|
|
1229
|
-
await writeJsonFile(workerLifecyclePath(dir, worker.id), next);
|
|
1230
|
-
return next;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
async function writeWorkerLifecycleForConfig(
|
|
1249
|
+
const workerRuntime: GjcTeamWorkerRuntime = {
|
|
1250
|
+
findTeamDir,
|
|
1251
|
+
readConfig,
|
|
1252
|
+
assertKnownWorker,
|
|
1253
|
+
assertKnownParticipant,
|
|
1254
|
+
findKnownWorker,
|
|
1255
|
+
workerDir,
|
|
1256
|
+
readJson: readJsonFile,
|
|
1257
|
+
writeJson: writeJsonFile,
|
|
1258
|
+
appendEvent,
|
|
1259
|
+
now,
|
|
1260
|
+
nowMs: currentTimeMs,
|
|
1261
|
+
stableHash,
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
const readWorkerLifecycleById = (dir: string, config: GjcTeamConfig) => readLifecycleById(workerRuntime, dir, config);
|
|
1265
|
+
const writeWorkerLifecycleForConfig = (
|
|
1234
1266
|
dir: string,
|
|
1235
1267
|
config: GjcTeamConfig,
|
|
1236
|
-
|
|
1268
|
+
state: GjcTeamWorkerLifecycleState,
|
|
1237
1269
|
updatesFor: (worker: GjcTeamWorker) => Partial<GjcTeamWorkerLifecycle> = () => ({}),
|
|
1238
|
-
)
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1270
|
+
) => writeLifecycleForConfig(workerRuntime, dir, config, state, updatesFor);
|
|
1271
|
+
|
|
1272
|
+
const workerOrchestrationRuntime: GjcTeamWorkerOrchestrationRuntime = {
|
|
1273
|
+
...workerRuntime,
|
|
1274
|
+
readTasks,
|
|
1275
|
+
withTaskMutation: (dir, fn) => withGjcTeamTaskMutation(taskStore(dir), fn),
|
|
1276
|
+
appendTelemetry,
|
|
1277
|
+
paneBelongsToTeamTarget,
|
|
1278
|
+
parseDurationEnv,
|
|
1279
|
+
parseHeartbeatStaleMs,
|
|
1280
|
+
killWorkerPanes,
|
|
1281
|
+
removeCleanCreatedWorktrees,
|
|
1282
|
+
readMonitorSnapshot: dir => readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir)),
|
|
1283
|
+
hasPendingIntegration: hasPendingGjcTeamIntegration,
|
|
1284
|
+
writePhase,
|
|
1285
|
+
readSnapshot: readGjcTeamSnapshot,
|
|
1286
|
+
};
|
|
1244
1287
|
|
|
1245
1288
|
function teamModeStatePath(cwd: string, sessionId: string): string {
|
|
1246
1289
|
return modeStatePath(cwd, sessionId, "team");
|
|
@@ -1249,7 +1292,9 @@ function teamModeStatePath(cwd: string, sessionId: string): string {
|
|
|
1249
1292
|
export async function persistGjcTeamModeStateSummary(snapshot: GjcTeamSnapshot, cwd = process.cwd()): Promise<void> {
|
|
1250
1293
|
const active = snapshot.phase !== "complete" && snapshot.phase !== "cancelled";
|
|
1251
1294
|
const updatedAt = now();
|
|
1252
|
-
const sessionId = resolveGjcSessionForWrite(cwd, {
|
|
1295
|
+
const sessionId = resolveGjcSessionForWrite(cwd, {
|
|
1296
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
1297
|
+
}).gjcSessionId;
|
|
1253
1298
|
const statePath = teamModeStatePath(cwd, sessionId);
|
|
1254
1299
|
await writeWorkflowEnvelopeAtomic(
|
|
1255
1300
|
statePath,
|
|
@@ -1272,25 +1317,24 @@ export async function persistGjcTeamModeStateSummary(snapshot: GjcTeamSnapshot,
|
|
|
1272
1317
|
sessionId,
|
|
1273
1318
|
nowIso: updatedAt,
|
|
1274
1319
|
},
|
|
1275
|
-
audit: {
|
|
1320
|
+
audit: {
|
|
1321
|
+
category: "state",
|
|
1322
|
+
verb: "sync-team-summary",
|
|
1323
|
+
owner: "gjc-runtime",
|
|
1324
|
+
skill: "team",
|
|
1325
|
+
sessionId,
|
|
1326
|
+
},
|
|
1276
1327
|
},
|
|
1277
1328
|
);
|
|
1278
|
-
await writeSessionActivityMarker(cwd, sessionId, {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
reasons: GjcTeamLivenessRecoveryReason[],
|
|
1283
|
-
reason: GjcTeamLivenessRecoveryReason,
|
|
1284
|
-
): void {
|
|
1285
|
-
if (!reasons.includes(reason)) reasons.push(reason);
|
|
1329
|
+
await writeSessionActivityMarker(cwd, sessionId, {
|
|
1330
|
+
writer: "team-runtime",
|
|
1331
|
+
path: statePath,
|
|
1332
|
+
});
|
|
1286
1333
|
}
|
|
1287
1334
|
|
|
1288
|
-
function
|
|
1289
|
-
|
|
1290
|
-
const timestamp = Date.parse(value);
|
|
1291
|
-
return Number.isFinite(timestamp) && timestamp <= Date.now();
|
|
1335
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1336
|
+
return typeof value === "object" && value != null;
|
|
1292
1337
|
}
|
|
1293
|
-
|
|
1294
1338
|
function readClaimRecord(value: unknown): GjcTeamTaskClaim | undefined {
|
|
1295
1339
|
if (!isRecord(value)) return undefined;
|
|
1296
1340
|
const owner = typeof value.owner === "string" ? value.owner : "";
|
|
@@ -1300,162 +1344,17 @@ function readClaimRecord(value: unknown): GjcTeamTaskClaim | undefined {
|
|
|
1300
1344
|
return { owner, token, leased_until: leasedUntil };
|
|
1301
1345
|
}
|
|
1302
1346
|
|
|
1303
|
-
function isWorkerHeartbeatStale(
|
|
1304
|
-
worker: GjcTeamWorker,
|
|
1305
|
-
heartbeat: WorkerHeartbeatFile | null,
|
|
1306
|
-
env: NodeJS.ProcessEnv,
|
|
1307
|
-
): boolean {
|
|
1308
|
-
const thresholdMs = parseDurationEnv(env, "GJC_TEAM_HEARTBEAT_STALE_MS", 120_000);
|
|
1309
|
-
if (thresholdMs <= 0) return false;
|
|
1310
|
-
const heartbeatAt = Date.parse(heartbeat?.last_turn_at ?? worker.last_heartbeat);
|
|
1311
|
-
return Number.isFinite(heartbeatAt) && Date.now() - heartbeatAt >= thresholdMs;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
async function detectGjcTeamWorkerLivenessReasons(
|
|
1315
|
-
dir: string,
|
|
1316
|
-
config: GjcTeamConfig,
|
|
1317
|
-
worker: GjcTeamWorker,
|
|
1318
|
-
env: NodeJS.ProcessEnv,
|
|
1319
|
-
): Promise<GjcTeamLivenessRecoveryReason[]> {
|
|
1320
|
-
const reasons: GjcTeamLivenessRecoveryReason[] = [];
|
|
1321
|
-
const lifecycle = await readWorkerLifecycleRecord(dir, worker);
|
|
1322
|
-
const heartbeat = await readJsonFile<WorkerHeartbeatFile>(path.join(workerDir(dir, worker.id), "heartbeat.json"));
|
|
1323
|
-
if (lifecycle.lifecycle_state === "failed") appendLivenessRecoveryReason(reasons, "worker_lifecycle_failed");
|
|
1324
|
-
if (lifecycle.lifecycle_state === "stopped") appendLivenessRecoveryReason(reasons, "worker_lifecycle_stopped");
|
|
1325
|
-
if (isWorkerHeartbeatStale(worker, heartbeat, env)) appendLivenessRecoveryReason(reasons, "stale_heartbeat");
|
|
1326
|
-
if (!config.dry_run && (!worker.pane_id?.startsWith("%") || !paneBelongsToTeamTarget(config, worker.pane_id)))
|
|
1327
|
-
appendLivenessRecoveryReason(reasons, "missing_pane");
|
|
1328
|
-
return reasons;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
async function reconcileGjcTeamStaleClaims(
|
|
1332
|
-
teamName: string,
|
|
1333
|
-
dir: string,
|
|
1334
|
-
config: GjcTeamConfig,
|
|
1335
|
-
env: NodeJS.ProcessEnv,
|
|
1336
|
-
): Promise<GjcTeamLivenessRecoveryResult> {
|
|
1337
|
-
const staleWorkers: Record<string, GjcTeamLivenessRecoveryReason[]> = {};
|
|
1338
|
-
for (const worker of config.workers) {
|
|
1339
|
-
const reasons = await detectGjcTeamWorkerLivenessReasons(dir, config, worker, env);
|
|
1340
|
-
if (reasons.length === 0) continue;
|
|
1341
|
-
staleWorkers[worker.id] = reasons;
|
|
1342
|
-
if (reasons.includes("missing_pane") && reasons.includes("worker_lifecycle_stopped") === false) {
|
|
1343
|
-
await writeWorkerLifecycleRecord(dir, worker, "failed", { stop_reason: "pane_missing" });
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
const recoveredClaims: GjcTeamRecoveredClaim[] = [];
|
|
1348
|
-
for (const task of await readTasks(dir)) {
|
|
1349
|
-
if (task.status === "completed" || task.status === "failed") continue;
|
|
1350
|
-
const claimPath = path.join(dir, "claims", `${task.id}.json`);
|
|
1351
|
-
const diskClaim = readClaimRecord(await readJsonFile<unknown>(claimPath));
|
|
1352
|
-
const claim = task.claim ?? diskClaim;
|
|
1353
|
-
if (!claim) continue;
|
|
1354
|
-
|
|
1355
|
-
const reasons = [...(staleWorkers[claim.owner] ?? [])];
|
|
1356
|
-
if (isPastTimestamp(claim.leased_until)) appendLivenessRecoveryReason(reasons, "claim_expired");
|
|
1357
|
-
if (reasons.length === 0) continue;
|
|
1358
|
-
|
|
1359
|
-
await fs.rm(claimPath, { force: true });
|
|
1360
|
-
recoveredClaims.push({ task_id: task.id, worker: claim.owner, reasons });
|
|
1361
|
-
if (task.status !== "in_progress") {
|
|
1362
|
-
await appendEvent(dir, {
|
|
1363
|
-
type: "task_claim_recovered",
|
|
1364
|
-
task_id: task.id,
|
|
1365
|
-
worker: claim.owner,
|
|
1366
|
-
message: "Removed stale task claim file",
|
|
1367
|
-
data: { reasons },
|
|
1368
|
-
});
|
|
1369
|
-
continue;
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
const recoveredTask = normalizeTask({
|
|
1373
|
-
...task,
|
|
1374
|
-
status: "pending",
|
|
1375
|
-
assignee: undefined,
|
|
1376
|
-
claim: undefined,
|
|
1377
|
-
version: task.version + 1,
|
|
1378
|
-
updated_at: now(),
|
|
1379
|
-
});
|
|
1380
|
-
await writeTask(dir, recoveredTask);
|
|
1381
|
-
await appendEvent(dir, {
|
|
1382
|
-
type: "task_claim_recovered",
|
|
1383
|
-
task_id: task.id,
|
|
1384
|
-
worker: claim.owner,
|
|
1385
|
-
message: "Recovered task from stale worker claim",
|
|
1386
|
-
data: { reasons },
|
|
1387
|
-
});
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
if (recoveredClaims.length > 0)
|
|
1391
|
-
await appendTelemetry(dir, {
|
|
1392
|
-
type: "team_liveness_recovery",
|
|
1393
|
-
message: `Recovered ${recoveredClaims.length} stale team task claim(s)`,
|
|
1394
|
-
data: { team_name: teamName, recovered_claims: recoveredClaims },
|
|
1395
|
-
});
|
|
1396
|
-
|
|
1397
|
-
return { recovered_claims: recoveredClaims, stale_workers: staleWorkers };
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
1347
|
export async function recoverGjcTeamStaleClaims(
|
|
1401
1348
|
teamName: string,
|
|
1402
1349
|
cwd = process.cwd(),
|
|
1403
1350
|
env: NodeJS.ProcessEnv = process.env,
|
|
1404
1351
|
): Promise<GjcTeamLivenessRecoveryResult> {
|
|
1405
1352
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
if (typeof value !== "string") return undefined;
|
|
1411
|
-
const trimmed = value.trim();
|
|
1412
|
-
return trimmed || undefined;
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
function normalizeOptionalTaskStringArray(value: unknown): string[] | undefined {
|
|
1416
|
-
if (!Array.isArray(value)) return undefined;
|
|
1417
|
-
const items = Array.from(
|
|
1418
|
-
new Set(value.map(item => (typeof item === "string" ? item.trim() : "")).filter(item => item.length > 0)),
|
|
1419
|
-
).sort();
|
|
1420
|
-
return items.length > 0 ? items : undefined;
|
|
1421
|
-
}
|
|
1422
|
-
type GjcTeamTaskMetadataInput = Partial<
|
|
1423
|
-
Pick<GjcTeamTask, "owner" | "lane" | "required_role" | "allowed_roles" | "depends_on" | "blocked_by">
|
|
1424
|
-
>;
|
|
1425
|
-
|
|
1426
|
-
function taskMetadataFromInput(input: Record<string, unknown>, includeOwner = false): GjcTeamTaskMetadataInput {
|
|
1427
|
-
const metadata: GjcTeamTaskMetadataInput = {};
|
|
1428
|
-
const owner = normalizeOptionalTaskString(input.owner);
|
|
1429
|
-
const lane = normalizeOptionalTaskString(input.lane);
|
|
1430
|
-
const requiredRole = normalizeOptionalTaskString(input.required_role ?? input.requiredRole);
|
|
1431
|
-
const allowedRoles = normalizeOptionalTaskStringArray(input.allowed_roles ?? input.allowedRoles);
|
|
1432
|
-
const dependsOn = normalizeOptionalTaskStringArray(input.depends_on ?? input.dependsOn);
|
|
1433
|
-
const blockedBy = normalizeOptionalTaskStringArray(input.blocked_by ?? input.blockedBy);
|
|
1434
|
-
if (includeOwner && owner) metadata.owner = owner;
|
|
1435
|
-
if (lane) metadata.lane = lane;
|
|
1436
|
-
if (requiredRole) metadata.required_role = requiredRole;
|
|
1437
|
-
if (allowedRoles) metadata.allowed_roles = allowedRoles;
|
|
1438
|
-
if (dependsOn) metadata.depends_on = dependsOn;
|
|
1439
|
-
if (blockedBy) metadata.blocked_by = blockedBy;
|
|
1440
|
-
return metadata;
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
function normalizeTask(raw: GjcTeamTask): GjcTeamTask {
|
|
1444
|
-
const status = raw.status === ("complete" as GjcTeamTaskStatus) ? "completed" : raw.status;
|
|
1445
|
-
return {
|
|
1446
|
-
...raw,
|
|
1447
|
-
status,
|
|
1448
|
-
subject: raw.subject ?? raw.title,
|
|
1449
|
-
description: raw.description ?? raw.objective,
|
|
1450
|
-
title: raw.title ?? raw.subject,
|
|
1451
|
-
objective: raw.objective ?? raw.description,
|
|
1452
|
-
version: raw.version ?? 1,
|
|
1453
|
-
lane: normalizeOptionalTaskString(raw.lane),
|
|
1454
|
-
required_role: normalizeOptionalTaskString(raw.required_role),
|
|
1455
|
-
allowed_roles: normalizeOptionalTaskStringArray(raw.allowed_roles),
|
|
1456
|
-
};
|
|
1353
|
+
return withGjcTeamTaskMutation(taskStore(dir), async capability => {
|
|
1354
|
+
const config = await readConfig(dir);
|
|
1355
|
+
return reconcileGjcTeamStaleClaimsUnlocked(workerOrchestrationRuntime, teamName, dir, config, env, capability);
|
|
1356
|
+
});
|
|
1457
1357
|
}
|
|
1458
|
-
|
|
1459
1358
|
const GJC_TEAM_INTEGRATION_ATTENTION_STATUSES = new Set<GjcTeamIntegrationStatus>([
|
|
1460
1359
|
"integration_failed",
|
|
1461
1360
|
"merge_conflict",
|
|
@@ -1500,230 +1399,18 @@ async function resolveGjcTeamSnapshotPhase(
|
|
|
1500
1399
|
return (await hasPendingGjcTeamIntegration(dir, config, monitor)) ? "awaiting_integration" : storedPhase;
|
|
1501
1400
|
}
|
|
1502
1401
|
|
|
1503
|
-
function
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1510
|
-
"
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
function completionEvidenceError(taskId: string, field: string): Error {
|
|
1516
|
-
return new Error(`invalid_completion_evidence:${taskId}:${field}`);
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
function trimRequiredCompletionEvidenceString(
|
|
1520
|
-
taskId: string,
|
|
1521
|
-
field: string,
|
|
1522
|
-
value: unknown,
|
|
1523
|
-
maxLength = GJC_TEAM_COMPLETION_EVIDENCE_SUMMARY_MAX,
|
|
1524
|
-
): string {
|
|
1525
|
-
if (typeof value !== "string") throw completionEvidenceError(taskId, field);
|
|
1526
|
-
const trimmed = value.trim();
|
|
1527
|
-
if (!trimmed || trimmed.length > maxLength) throw completionEvidenceError(taskId, field);
|
|
1528
|
-
return trimmed;
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
function trimOptionalCompletionEvidenceString(
|
|
1532
|
-
taskId: string,
|
|
1533
|
-
field: string,
|
|
1534
|
-
value: unknown,
|
|
1535
|
-
maxLength = GJC_TEAM_COMPLETION_EVIDENCE_OUTPUT_MAX,
|
|
1536
|
-
): string | undefined {
|
|
1537
|
-
if (value == null) return undefined;
|
|
1538
|
-
if (typeof value !== "string") throw completionEvidenceError(taskId, field);
|
|
1539
|
-
const trimmed = value.trim();
|
|
1540
|
-
if (!trimmed) return undefined;
|
|
1541
|
-
if (trimmed.length > maxLength) throw completionEvidenceError(taskId, field);
|
|
1542
|
-
return trimmed;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
function normalizeGjcTeamCompletionEvidenceStatus(
|
|
1546
|
-
taskId: string,
|
|
1547
|
-
kind: GjcTeamTaskCompletionEvidenceKind,
|
|
1548
|
-
value: unknown,
|
|
1549
|
-
): GjcTeamTaskCompletionEvidenceStatus {
|
|
1550
|
-
const status = trimRequiredCompletionEvidenceString(taskId, "items.status", value);
|
|
1551
|
-
const allowed = kind === "command" ? GJC_TEAM_COMMAND_EVIDENCE_STATUSES : GJC_TEAM_VERIFICATION_EVIDENCE_STATUSES;
|
|
1552
|
-
if (!allowed.has(status as GjcTeamTaskCompletionEvidenceStatus))
|
|
1553
|
-
throw completionEvidenceError(taskId, "items.status");
|
|
1554
|
-
return status as GjcTeamTaskCompletionEvidenceStatus;
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
function normalizeGjcTeamCompletionEvidenceItem(taskId: string, value: unknown): GjcTeamTaskCompletionEvidenceItem {
|
|
1558
|
-
if (!isRecord(value) || Array.isArray(value)) throw completionEvidenceError(taskId, "items");
|
|
1559
|
-
const kind = trimRequiredCompletionEvidenceString(taskId, "items.kind", value.kind);
|
|
1560
|
-
if (kind !== "command" && kind !== "inspection" && kind !== "artifact")
|
|
1561
|
-
throw completionEvidenceError(taskId, "items.kind");
|
|
1562
|
-
const status = normalizeGjcTeamCompletionEvidenceStatus(taskId, kind, value.status);
|
|
1563
|
-
const item: GjcTeamTaskCompletionEvidenceItem = {
|
|
1564
|
-
kind,
|
|
1565
|
-
status,
|
|
1566
|
-
summary: trimRequiredCompletionEvidenceString(taskId, "items.summary", value.summary),
|
|
1567
|
-
};
|
|
1568
|
-
const command = trimOptionalCompletionEvidenceString(taskId, "items.command", value.command);
|
|
1569
|
-
const artifact = trimOptionalCompletionEvidenceString(taskId, "items.artifact", value.artifact);
|
|
1570
|
-
const location = trimOptionalCompletionEvidenceString(taskId, "items.location", value.location);
|
|
1571
|
-
const output = trimOptionalCompletionEvidenceString(taskId, "items.output", value.output);
|
|
1572
|
-
if (kind === "command" && !command) throw completionEvidenceError(taskId, "items.command");
|
|
1573
|
-
if (command) item.command = command;
|
|
1574
|
-
if (artifact) item.artifact = artifact;
|
|
1575
|
-
if (location) item.location = location;
|
|
1576
|
-
if (output) item.output = output;
|
|
1577
|
-
return item;
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
function normalizeGjcTeamCompletionEvidenceFiles(taskId: string, value: unknown): string[] | undefined {
|
|
1581
|
-
if (value == null) return undefined;
|
|
1582
|
-
if (!Array.isArray(value)) throw completionEvidenceError(taskId, "files");
|
|
1583
|
-
const files = new Set<string>();
|
|
1584
|
-
for (const entry of value) {
|
|
1585
|
-
if (typeof entry !== "string") throw completionEvidenceError(taskId, "files");
|
|
1586
|
-
const filePath = entry.trim().replace(/\\/g, "/");
|
|
1587
|
-
if (!filePath || filePath.includes("\0") || path.isAbsolute(filePath) || filePath.split("/").includes("..")) {
|
|
1588
|
-
throw completionEvidenceError(taskId, "files");
|
|
1589
|
-
}
|
|
1590
|
-
files.add(filePath);
|
|
1591
|
-
}
|
|
1592
|
-
return files.size > 0 ? [...files].sort() : undefined;
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
function isGjcTeamCompletionEvidenceItemVerified(item: GjcTeamTaskCompletionEvidenceItem): boolean {
|
|
1596
|
-
return (
|
|
1597
|
-
(item.kind === "command" && item.status === "passed") ||
|
|
1598
|
-
((item.kind === "inspection" || item.kind === "artifact") && item.status === "verified")
|
|
1599
|
-
);
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
function normalizeGjcTeamTaskCompletionEvidence(
|
|
1603
|
-
taskId: string,
|
|
1604
|
-
owner: string,
|
|
1605
|
-
input: unknown,
|
|
1606
|
-
recordedAt = now(),
|
|
1607
|
-
): GjcTeamTaskCompletionEvidence {
|
|
1608
|
-
if (!isRecord(input) || Array.isArray(input)) throw new Error(`completion_evidence_required:${taskId}`);
|
|
1609
|
-
const itemsValue = input.items;
|
|
1610
|
-
if (!Array.isArray(itemsValue) || itemsValue.length === 0) throw completionEvidenceError(taskId, "items");
|
|
1611
|
-
const items = itemsValue.map(item => normalizeGjcTeamCompletionEvidenceItem(taskId, item));
|
|
1612
|
-
if (!items.some(isGjcTeamCompletionEvidenceItemVerified))
|
|
1613
|
-
throw new Error(`completion_evidence_no_verified_item:${taskId}`);
|
|
1614
|
-
const evidence: GjcTeamTaskCompletionEvidence = {
|
|
1615
|
-
summary: trimRequiredCompletionEvidenceString(taskId, "summary", input.summary),
|
|
1616
|
-
items,
|
|
1617
|
-
recorded_by: owner,
|
|
1618
|
-
recorded_at: recordedAt,
|
|
1619
|
-
};
|
|
1620
|
-
const files = normalizeGjcTeamCompletionEvidenceFiles(taskId, input.files);
|
|
1621
|
-
const notes = trimOptionalCompletionEvidenceString(taskId, "notes", input.notes);
|
|
1622
|
-
if (files) evidence.files = files;
|
|
1623
|
-
if (notes) evidence.notes = notes;
|
|
1624
|
-
return evidence;
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
function getGjcTeamTaskCompletionEvidenceFailure(task: GjcTeamTask): string | null {
|
|
1628
|
-
if (task.status !== "completed") return `task_not_completed:${task.id}`;
|
|
1629
|
-
const evidence = task.completion_evidence;
|
|
1630
|
-
if (!isRecord(evidence) || Array.isArray(evidence)) return `completion_evidence_required:${task.id}`;
|
|
1631
|
-
if (typeof evidence.recorded_by !== "string" || evidence.recorded_by.trim().length === 0)
|
|
1632
|
-
return `invalid_completion_evidence:${task.id}:recorded_by`;
|
|
1633
|
-
if (typeof evidence.recorded_at !== "string" || evidence.recorded_at.trim().length === 0)
|
|
1634
|
-
return `invalid_completion_evidence:${task.id}:recorded_at`;
|
|
1635
|
-
try {
|
|
1636
|
-
normalizeGjcTeamTaskCompletionEvidence(task.id, evidence.recorded_by.trim(), evidence, evidence.recorded_at);
|
|
1637
|
-
return null;
|
|
1638
|
-
} catch (error) {
|
|
1639
|
-
return error instanceof Error ? error.message : `invalid_completion_evidence:${task.id}:unknown`;
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
function isGjcTeamTaskCompletionVerified(task: GjcTeamTask): boolean {
|
|
1644
|
-
return getGjcTeamTaskCompletionEvidenceFailure(task) == null;
|
|
1645
|
-
}
|
|
1646
|
-
function roleValuesForWorker(worker: GjcTeamWorker): Set<string> {
|
|
1647
|
-
return new Set([worker.role, worker.agent_type].map(value => value.trim()).filter(value => value.length > 0));
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
function getGjcTeamTaskClaimEligibilityReason(
|
|
1651
|
-
task: GjcTeamTask,
|
|
1652
|
-
worker: GjcTeamWorker,
|
|
1653
|
-
tasks: GjcTeamTask[],
|
|
1654
|
-
): string | null {
|
|
1655
|
-
if (task.status !== "pending") return `task_not_pending:${task.id}`;
|
|
1656
|
-
if (task.owner && task.owner !== worker.id) return `task_owner_mismatch:${task.id}:${task.owner}`;
|
|
1657
|
-
if (task.assignee && task.assignee !== worker.id) return `task_assignee_mismatch:${task.id}:${task.assignee}`;
|
|
1658
|
-
|
|
1659
|
-
const workerRoles = roleValuesForWorker(worker);
|
|
1660
|
-
if (task.required_role && !workerRoles.has(task.required_role))
|
|
1661
|
-
return `task_role_mismatch:${task.id}:${task.required_role}`;
|
|
1662
|
-
if (task.allowed_roles?.length && !task.allowed_roles.some(role => workerRoles.has(role)))
|
|
1663
|
-
return `task_role_mismatch:${task.id}:${task.allowed_roles.join(",")}`;
|
|
1664
|
-
|
|
1665
|
-
if (task.blocked_by?.length) return `task_blocked:${task.id}:${task.blocked_by.join(",")}`;
|
|
1666
|
-
for (const dependencyId of task.depends_on ?? []) {
|
|
1667
|
-
const dependency = tasks.find(candidate => candidate.id === dependencyId);
|
|
1668
|
-
if (!dependency || !isGjcTeamTaskCompletionVerified(dependency))
|
|
1669
|
-
return `task_dependency_incomplete:${task.id}:${dependencyId}`;
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
return null;
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
async function getActiveClaimReason(dir: string, task: GjcTeamTask): Promise<string | null> {
|
|
1676
|
-
const claimPath = path.join(dir, "claims", `${task.id}.json`);
|
|
1677
|
-
const diskClaim = readClaimRecord(await readJsonFile<unknown>(claimPath));
|
|
1678
|
-
const claim = task.claim ?? diskClaim;
|
|
1679
|
-
if (!claim || isPastTimestamp(claim.leased_until)) return null;
|
|
1680
|
-
return `task_already_claimed:${task.id}`;
|
|
1681
|
-
}
|
|
1682
|
-
function isGjcTeamTaskRecord(value: unknown): value is GjcTeamTask {
|
|
1683
|
-
return (
|
|
1684
|
-
isRecord(value) &&
|
|
1685
|
-
typeof value.id === "string" &&
|
|
1686
|
-
typeof value.status === "string" &&
|
|
1687
|
-
(isGjcTeamTaskStatus(value.status) || value.status === "complete") &&
|
|
1688
|
-
(typeof value.subject === "string" || typeof value.title === "string") &&
|
|
1689
|
-
(typeof value.description === "string" || typeof value.objective === "string")
|
|
1690
|
-
);
|
|
1691
|
-
}
|
|
1692
|
-
function isGjcTeamTaskFile(entry: { isFile(): boolean; name: string }): boolean {
|
|
1693
|
-
return entry.isFile() && entry.name.endsWith(".json") && !entry.name.endsWith(".evidence.json");
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
async function readTasks(dir: string): Promise<GjcTeamTask[]> {
|
|
1697
|
-
try {
|
|
1698
|
-
const entries = await fs.readdir(path.join(dir, "tasks"), { withFileTypes: true });
|
|
1699
|
-
const tasks = await Promise.all(
|
|
1700
|
-
entries.filter(isGjcTeamTaskFile).map(entry => readJsonFile<unknown>(path.join(dir, "tasks", entry.name))),
|
|
1701
|
-
);
|
|
1702
|
-
return tasks
|
|
1703
|
-
.filter(isGjcTeamTaskRecord)
|
|
1704
|
-
.map(normalizeTask)
|
|
1705
|
-
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1706
|
-
} catch (error) {
|
|
1707
|
-
if (isEnoent(error)) return [];
|
|
1708
|
-
throw error;
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
async function writeTask(dir: string, task: GjcTeamTask): Promise<void> {
|
|
1712
|
-
await writeJsonFile(taskPath(dir, task.id), normalizeTask(task));
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
async function findTeamDir(
|
|
1716
|
-
teamName: string,
|
|
1717
|
-
cwd = process.cwd(),
|
|
1718
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
1719
|
-
): Promise<string> {
|
|
1720
|
-
const root = resolveGjcTeamStateRoot(cwd, env);
|
|
1721
|
-
const exact = teamDir(root, teamName);
|
|
1722
|
-
if (await readJsonFile<GjcTeamConfig>(path.join(exact, "config.json"))) return exact;
|
|
1723
|
-
const candidates = await listGjcTeams(cwd, env);
|
|
1724
|
-
const input = sanitizeName(teamName);
|
|
1725
|
-
const matches = candidates.filter(
|
|
1726
|
-
candidate => candidate.team_name === input || sanitizeName(candidate.display_name) === input,
|
|
1402
|
+
async function findTeamDir(
|
|
1403
|
+
teamName: string,
|
|
1404
|
+
cwd = process.cwd(),
|
|
1405
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
1406
|
+
): Promise<string> {
|
|
1407
|
+
const root = resolveGjcTeamStateRoot(cwd, env);
|
|
1408
|
+
const exact = teamDir(root, teamName);
|
|
1409
|
+
if (await readJsonFile<GjcTeamConfig>(path.join(exact, "config.json"))) return exact;
|
|
1410
|
+
const candidates = await listGjcTeams(cwd, env);
|
|
1411
|
+
const input = sanitizeName(teamName);
|
|
1412
|
+
const matches = candidates.filter(
|
|
1413
|
+
candidate => candidate.team_name === input || sanitizeName(candidate.display_name) === input,
|
|
1727
1414
|
);
|
|
1728
1415
|
if (matches.length === 1) return matches[0].state_dir;
|
|
1729
1416
|
if (matches.length > 1)
|
|
@@ -1750,28 +1437,43 @@ function sanitizePathToken(value: string): string {
|
|
|
1750
1437
|
return sanitizeName(value) || "default";
|
|
1751
1438
|
}
|
|
1752
1439
|
function runGitResult(cwd: string, args: string[]): GitResult {
|
|
1753
|
-
const result = Bun.spawnSync(["git", ...args], {
|
|
1440
|
+
const result = Bun.spawnSync(["git", ...args], {
|
|
1441
|
+
cwd,
|
|
1442
|
+
stdout: "pipe",
|
|
1443
|
+
stderr: "pipe",
|
|
1444
|
+
});
|
|
1754
1445
|
return {
|
|
1755
1446
|
ok: result.exitCode === 0,
|
|
1756
1447
|
stdout: result.stdout.toString().trim(),
|
|
1757
1448
|
stderr: result.stderr.toString().trim(),
|
|
1758
1449
|
};
|
|
1759
1450
|
}
|
|
1760
|
-
async function runGitResultAsync(cwd: string, args: string[]): Promise<GitResult> {
|
|
1451
|
+
async function runGitResultAsync(cwd: string, args: string[], signal?: AbortSignal): Promise<GitResult> {
|
|
1761
1452
|
const result = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
]);
|
|
1767
|
-
return {
|
|
1768
|
-
ok: exitCode === 0,
|
|
1769
|
-
stdout: stdout.trim(),
|
|
1770
|
-
stderr: stderr.trim(),
|
|
1453
|
+
const kill = () => {
|
|
1454
|
+
try {
|
|
1455
|
+
result.kill("SIGKILL");
|
|
1456
|
+
} catch {}
|
|
1771
1457
|
};
|
|
1458
|
+
if (signal?.aborted) kill();
|
|
1459
|
+
else signal?.addEventListener("abort", kill, { once: true });
|
|
1460
|
+
try {
|
|
1461
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
1462
|
+
result.exited,
|
|
1463
|
+
new Response(result.stdout).text(),
|
|
1464
|
+
new Response(result.stderr).text(),
|
|
1465
|
+
]);
|
|
1466
|
+
return {
|
|
1467
|
+
ok: exitCode === 0 && !signal?.aborted,
|
|
1468
|
+
stdout: stdout.trim(),
|
|
1469
|
+
stderr: signal?.aborted ? "worker integration request aborted" : stderr.trim(),
|
|
1470
|
+
};
|
|
1471
|
+
} finally {
|
|
1472
|
+
signal?.removeEventListener("abort", kill);
|
|
1473
|
+
}
|
|
1772
1474
|
}
|
|
1773
|
-
async function tryRunGitAsync(cwd: string, args: string[]): Promise<string | null> {
|
|
1774
|
-
const result = await runGitResultAsync(cwd, args);
|
|
1475
|
+
async function tryRunGitAsync(cwd: string, args: string[], signal?: AbortSignal): Promise<string | null> {
|
|
1476
|
+
const result = await runGitResultAsync(cwd, args, signal);
|
|
1775
1477
|
return result.ok ? result.stdout : null;
|
|
1776
1478
|
}
|
|
1777
1479
|
function runGit(cwd: string, args: string[]): string {
|
|
@@ -1787,7 +1489,10 @@ function isGitRepository(cwd: string): boolean {
|
|
|
1787
1489
|
return tryRunGit(cwd, ["rev-parse", "--show-toplevel"]) != null;
|
|
1788
1490
|
}
|
|
1789
1491
|
|
|
1790
|
-
function parseWorktreeMode(args: string[]): {
|
|
1492
|
+
function parseWorktreeMode(args: string[]): {
|
|
1493
|
+
mode: GjcTeamWorktreeMode;
|
|
1494
|
+
remainingArgs: string[];
|
|
1495
|
+
} {
|
|
1791
1496
|
let mode: GjcTeamWorktreeMode = { enabled: false };
|
|
1792
1497
|
const remainingArgs: string[] = [];
|
|
1793
1498
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -1960,7 +1665,10 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
|
|
|
1960
1665
|
const args = displayTarget
|
|
1961
1666
|
? ["display-message", "-p", "-t", displayTarget, "#S:#I #{pane_id}"]
|
|
1962
1667
|
: ["display-message", "-p", "#S:#I #{pane_id}"];
|
|
1963
|
-
const result = Bun.spawnSync([tmuxCommand, ...args], {
|
|
1668
|
+
const result = Bun.spawnSync([tmuxCommand, ...args], {
|
|
1669
|
+
stdout: "pipe",
|
|
1670
|
+
stderr: "pipe",
|
|
1671
|
+
});
|
|
1964
1672
|
if (result.exitCode !== 0) {
|
|
1965
1673
|
// Distinguish "you are not inside any tmux session" from a genuine tmux
|
|
1966
1674
|
// query failure so the caller gets actionable guidance instead of raw
|
|
@@ -1993,7 +1701,12 @@ function readCurrentTmuxLeaderContext(tmuxCommand: string, env: NodeJS.ProcessEn
|
|
|
1993
1701
|
),
|
|
1994
1702
|
);
|
|
1995
1703
|
}
|
|
1996
|
-
return {
|
|
1704
|
+
return {
|
|
1705
|
+
sessionName,
|
|
1706
|
+
windowIndex,
|
|
1707
|
+
leaderPaneId,
|
|
1708
|
+
target: `${sessionName}:${windowIndex}`,
|
|
1709
|
+
};
|
|
1997
1710
|
}
|
|
1998
1711
|
function isBunVirtualPath(candidate: string | undefined): boolean {
|
|
1999
1712
|
const normalized = candidate?.trim().replace(/\\/g, "/").toLowerCase();
|
|
@@ -2060,85 +1773,7 @@ export function resolveGjcWorkerCommand(
|
|
|
2060
1773
|
if (!executable) throw unresolvedWorkerAuthorityError();
|
|
2061
1774
|
return formatWorkerExecutable(platform, executable);
|
|
2062
1775
|
}
|
|
2063
|
-
|
|
2064
|
-
export function buildWorkerCommand(
|
|
2065
|
-
config: GjcTeamConfig,
|
|
2066
|
-
worker: GjcTeamWorker,
|
|
2067
|
-
platform: NodeJS.Platform = process.platform,
|
|
2068
|
-
): string {
|
|
2069
|
-
const quote = platform === "win32" ? powershellQuote : shellQuote;
|
|
2070
|
-
const envAssignment = (key: string, value: string): string =>
|
|
2071
|
-
platform === "win32" ? `$env:${key} = ${quote(value)};` : `${key}=${quote(value)}`;
|
|
2072
|
-
const workspace = worker.worktree_path
|
|
2073
|
-
? `Worker worktree: ${worker.worktree_path}.`
|
|
2074
|
-
: `Worker cwd: ${config.leader.cwd}.`;
|
|
2075
|
-
// The worker prompt body must stay single-line because fallback startup on
|
|
2076
|
-
// Windows/psmux dispatches the command through `tmux send-keys`, where
|
|
2077
|
-
// embedded LF characters are treated as Enter keypresses. POSIX tmux starts
|
|
2078
|
-
// workers by passing the command directly to `split-window`, but keeping the
|
|
2079
|
-
// body normalized there too makes fake-tmux logs and shell argv handling
|
|
2080
|
-
// deterministic. Strip a defensive U+FEFF in case a caller managed to inject
|
|
2081
|
-
// a UTF-8 BOM into the task text. Empty / whitespace-only bodies fall back to
|
|
2082
|
-
// a one-line placeholder so the worker never sits idle at an empty prompt.
|
|
2083
|
-
const normalizePrompt = (raw: string): string =>
|
|
2084
|
-
raw
|
|
2085
|
-
.replace(/[\uFEFF\u200B]/g, "")
|
|
2086
|
-
.replace(/\r?\n+/g, " ")
|
|
2087
|
-
.trim();
|
|
2088
|
-
const rawPrompt = [
|
|
2089
|
-
`You are ${worker.id} in gjc team ${config.team_name}.`,
|
|
2090
|
-
`Team state root: ${config.state_root}.`,
|
|
2091
|
-
workspace,
|
|
2092
|
-
`Team brief (context only): ${config.task}`,
|
|
2093
|
-
"Before implementation, claim your worker-owned task and treat the claimed task record as the source of truth. Do not implement directly from the broad team brief.",
|
|
2094
|
-
`Before claiming work, send startup ACK: gjc team api worker-startup-ack --input '{"team_name":"${config.team_name}","worker_id":"${worker.id}","protocol_version":"1"}' --json.`,
|
|
2095
|
-
`Use gjc team api update-worker-status to report task-local activity, then claim-task/transition-task-status with this worker id; keep heartbeat current during long work, record completion_evidence (summary plus a passed command or verified inspection/artifact item) before completed, and do not mutate leader-owned goal state.`,
|
|
2096
|
-
].join("\n");
|
|
2097
|
-
const prompt = normalizePrompt(rawPrompt) || `Worker ${worker.id} ready.`;
|
|
2098
|
-
const envLines = [
|
|
2099
|
-
envAssignment("GJC_TEAM_WORKER", `${config.team_name}/${worker.id}`),
|
|
2100
|
-
envAssignment("GJC_TEAM_INTERNAL_WORKER", `${config.team_name}/${worker.id}`),
|
|
2101
|
-
envAssignment("GJC_TEAM_NAME", config.team_name),
|
|
2102
|
-
envAssignment("GJC_TEAM_WORKER_ID", worker.id),
|
|
2103
|
-
envAssignment("GJC_TEAM_STATE_ROOT", config.state_root),
|
|
2104
|
-
envAssignment("GJC_TEAM_LEADER_CWD", config.leader.cwd),
|
|
2105
|
-
envAssignment("GJC_TEAM_DISPLAY_NAME", config.display_name),
|
|
2106
|
-
// Canonical GJC spawn-provenance marker so `notifications.sessionScope =
|
|
2107
|
-
// "primary"` suppresses the worker's own notification endpoint/topic. The
|
|
2108
|
-
// value is informational (leader session id, falling back to the team name
|
|
2109
|
-
// so it is always non-blank); presence is what marks the worker.
|
|
2110
|
-
envAssignment(SPAWN_PROVENANCE_ENV, config.leader.session_id.trim() || config.team_name),
|
|
2111
|
-
...(worker.worktree_path ? [envAssignment("GJC_TEAM_WORKTREE_PATH", worker.worktree_path)] : []),
|
|
2112
|
-
];
|
|
2113
|
-
const joined = platform === "win32" ? envLines.join(" ") : envLines.join(" ");
|
|
2114
|
-
// On Windows we wrap the worker command invocation in `& { & 'cmd' 'arg1' ... }`
|
|
2115
|
-
// so pwsh keeps the whole multi-statement body in command position. Two
|
|
2116
|
-
// failure modes this avoids:
|
|
2117
|
-
// 1. Bare `bun 'cli.ts' 'prompt'` after `$env:X = 'y'; ...` would be
|
|
2118
|
-
// parsed in expression position and PowerShell would reject the
|
|
2119
|
-
// second quoted token with "Unexpected token '<cli.ts>'".
|
|
2120
|
-
// 2. `& { 'cmd' 'arg1' 'arg2' }` (single & inside a block with adjacent
|
|
2121
|
-
// single-quoted tokens) is itself invalid because pwsh does not
|
|
2122
|
-
// concatenate adjacent single-quoted strings inside a script block.
|
|
2123
|
-
// The nested `&` inside the block forces command-position parsing for
|
|
2124
|
-
// the invocation. POSIX shells do not need this — they already treat
|
|
2125
|
-
// `cmd 'arg'` as a normal command invocation after `;`-separated
|
|
2126
|
-
// variable assignments.
|
|
2127
|
-
//
|
|
2128
|
-
// ASSUMPTION: this branch only fires when the worker pane is a PowerShell
|
|
2129
|
-
// shell. psmux launches pwsh by default on Windows, so the unset /
|
|
2130
|
-
// unset-and-replace cases are correct. If a user has explicitly set
|
|
2131
|
-
// `set -g default-shell "C:/Program Files/Git/bin/bash.exe"` (or any
|
|
2132
|
-
// other non-pwsh shell), this branch will send PowerShell syntax to a
|
|
2133
|
-
// bash pane and the worker will fail with a parse error. Detecting the
|
|
2134
|
-
// pane's shell at runtime and switching the quoting + invocation style
|
|
2135
|
-
// accordingly is a follow-up; for now the pwsh default is assumed.
|
|
2136
|
-
if (platform === "win32") {
|
|
2137
|
-
const invocation = `& ${config.worker_command} ${quote(prompt)}`;
|
|
2138
|
-
return `& { ${joined} ${invocation} }`;
|
|
2139
|
-
}
|
|
2140
|
-
return `${joined} ${config.worker_command} ${quote(prompt)}`;
|
|
2141
|
-
}
|
|
1776
|
+
export { buildWorkerCommand } from "./team-launch";
|
|
2142
1777
|
|
|
2143
1778
|
function shouldDispatchWorkerWithSendKeys(tmuxCommand: string, platform: NodeJS.Platform = process.platform): boolean {
|
|
2144
1779
|
return platform === "win32" || path.basename(tmuxCommand).toLowerCase() === "psmux";
|
|
@@ -2238,7 +1873,11 @@ async function startTmuxSession(
|
|
|
2238
1873
|
dryRun: boolean,
|
|
2239
1874
|
env: NodeJS.ProcessEnv = process.env,
|
|
2240
1875
|
): Promise<GjcTeamWorker[]> {
|
|
2241
|
-
if (dryRun)
|
|
1876
|
+
if (dryRun)
|
|
1877
|
+
return config.workers.map(worker => ({
|
|
1878
|
+
...worker,
|
|
1879
|
+
pane_id: `%dry-run-${worker.id}`,
|
|
1880
|
+
}));
|
|
2242
1881
|
const rollbackPaneIds: string[] = [];
|
|
2243
1882
|
try {
|
|
2244
1883
|
const workers: GjcTeamWorker[] = [];
|
|
@@ -2344,12 +1983,18 @@ async function startTmuxSession(
|
|
|
2344
1983
|
await appendTelemetry(dir, {
|
|
2345
1984
|
type: "tmux_started",
|
|
2346
1985
|
message: "Started gjc team worker panes in current tmux window",
|
|
2347
|
-
data: {
|
|
1986
|
+
data: {
|
|
1987
|
+
tmux_target: config.tmux_target,
|
|
1988
|
+
panes: workers.map(worker => worker.pane_id).filter(Boolean),
|
|
1989
|
+
},
|
|
2348
1990
|
});
|
|
2349
1991
|
return workers;
|
|
2350
1992
|
} catch (error) {
|
|
2351
1993
|
for (const paneId of rollbackPaneIds)
|
|
2352
|
-
Bun.spawnSync([config.tmux_command, "kill-pane", "-t", paneId], {
|
|
1994
|
+
Bun.spawnSync([config.tmux_command, "kill-pane", "-t", paneId], {
|
|
1995
|
+
stdout: "ignore",
|
|
1996
|
+
stderr: "ignore",
|
|
1997
|
+
});
|
|
2353
1998
|
throw error;
|
|
2354
1999
|
}
|
|
2355
2000
|
}
|
|
@@ -2400,7 +2045,9 @@ function commitHygieneLedgerPath(config: GjcTeamConfig): string {
|
|
|
2400
2045
|
return path.join(
|
|
2401
2046
|
sessionReportsDir(
|
|
2402
2047
|
config.leader_cwd,
|
|
2403
|
-
resolveGjcSessionForWrite(config.leader_cwd, {
|
|
2048
|
+
resolveGjcSessionForWrite(config.leader_cwd, {
|
|
2049
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
2050
|
+
}).gjcSessionId,
|
|
2404
2051
|
),
|
|
2405
2052
|
"team-commit-hygiene",
|
|
2406
2053
|
`${config.team_name}.ledger.json`,
|
|
@@ -2413,7 +2060,12 @@ function integrationNowState(
|
|
|
2413
2060
|
}
|
|
2414
2061
|
async function appendIntegrationReport(
|
|
2415
2062
|
dir: string,
|
|
2416
|
-
entry: {
|
|
2063
|
+
entry: {
|
|
2064
|
+
worker: string;
|
|
2065
|
+
operation: "merge" | "cherry-pick" | "rebase";
|
|
2066
|
+
files: string[];
|
|
2067
|
+
detail: string;
|
|
2068
|
+
},
|
|
2417
2069
|
): Promise<void> {
|
|
2418
2070
|
const line = `- [${now()}] ${entry.worker}: ${entry.operation}; files=${entry.files.join(",") || "unknown"}; ${entry.detail}\n`;
|
|
2419
2071
|
if (await pathExists(integrationReportPath(dir)))
|
|
@@ -2432,17 +2084,23 @@ async function appendIntegrationReport(
|
|
|
2432
2084
|
async function appendCommitHygieneEntries(config: GjcTeamConfig, entries: GjcTeamCommitHygieneEntry[]): Promise<void> {
|
|
2433
2085
|
if (entries.length === 0) return;
|
|
2434
2086
|
const ledgerPath = commitHygieneLedgerPath(config);
|
|
2435
|
-
const existing = (await readJsonFile<{
|
|
2087
|
+
const existing = (await readJsonFile<{
|
|
2088
|
+
version: number;
|
|
2089
|
+
entries: GjcTeamCommitHygieneEntry[];
|
|
2090
|
+
}>(ledgerPath)) ?? {
|
|
2436
2091
|
version: 1,
|
|
2437
2092
|
entries: [],
|
|
2438
2093
|
};
|
|
2439
|
-
await writeJsonFile(ledgerPath, {
|
|
2094
|
+
await writeJsonFile(ledgerPath, {
|
|
2095
|
+
version: 1,
|
|
2096
|
+
entries: [...existing.entries, ...entries],
|
|
2097
|
+
});
|
|
2440
2098
|
}
|
|
2441
2099
|
function resolveHead(cwd: string): string | null {
|
|
2442
2100
|
return tryRunGit(cwd, ["rev-parse", "HEAD"]);
|
|
2443
2101
|
}
|
|
2444
|
-
async function resolveHeadAsync(cwd: string): Promise<string | null> {
|
|
2445
|
-
return tryRunGitAsync(cwd, ["rev-parse", "HEAD"]);
|
|
2102
|
+
async function resolveHeadAsync(cwd: string, signal?: AbortSignal): Promise<string | null> {
|
|
2103
|
+
return tryRunGitAsync(cwd, ["rev-parse", "HEAD"], signal);
|
|
2446
2104
|
}
|
|
2447
2105
|
function isAncestor(cwd: string, ancestor: string, descendant: string): boolean {
|
|
2448
2106
|
return runGitResult(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]).ok;
|
|
@@ -2463,8 +2121,8 @@ function listConflictFiles(cwd: string): string[] {
|
|
|
2463
2121
|
.map(line => line.trim())
|
|
2464
2122
|
.filter(Boolean);
|
|
2465
2123
|
}
|
|
2466
|
-
async function listConflictFilesAsync(cwd: string): Promise<string[]> {
|
|
2467
|
-
const result = await runGitResultAsync(cwd, ["diff", "--name-only", "--diff-filter=U"]);
|
|
2124
|
+
async function listConflictFilesAsync(cwd: string, signal?: AbortSignal): Promise<string[]> {
|
|
2125
|
+
const result = await runGitResultAsync(cwd, ["diff", "--name-only", "--diff-filter=U"], signal);
|
|
2468
2126
|
if (!result.ok || !result.stdout) return [];
|
|
2469
2127
|
return result.stdout
|
|
2470
2128
|
.split(/\r?\n/)
|
|
@@ -2502,7 +2160,10 @@ function normalizeGitStatusPath(filePath: string): string {
|
|
|
2502
2160
|
return (filePath.split(" -> ").at(-1) ?? filePath).replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2503
2161
|
}
|
|
2504
2162
|
|
|
2505
|
-
export function classifyGjcTeamCheckpointFiles(files: string[]): {
|
|
2163
|
+
export function classifyGjcTeamCheckpointFiles(files: string[]): {
|
|
2164
|
+
eligible: string[];
|
|
2165
|
+
protected: string[];
|
|
2166
|
+
} {
|
|
2506
2167
|
const eligible: string[] = [];
|
|
2507
2168
|
const protectedFiles: string[] = [];
|
|
2508
2169
|
for (const file of files) {
|
|
@@ -2521,7 +2182,11 @@ export function classifyGjcTeamCheckpointFiles(files: string[]): { eligible: str
|
|
|
2521
2182
|
export function classifyWorkerCheckpointStatus(cwd: string): GjcWorkerCheckpointClassification {
|
|
2522
2183
|
const status = runGitResult(cwd, ["status", "--porcelain", "-uall"]);
|
|
2523
2184
|
if (!status.ok) {
|
|
2524
|
-
return {
|
|
2185
|
+
return {
|
|
2186
|
+
kind: "git_error",
|
|
2187
|
+
files: [],
|
|
2188
|
+
detail: status.stderr || status.stdout || "git status failed",
|
|
2189
|
+
};
|
|
2525
2190
|
}
|
|
2526
2191
|
if (!status.stdout.trim()) return { kind: "clean", files: [] };
|
|
2527
2192
|
const files = parsePorcelainStatusFiles(status.stdout);
|
|
@@ -2531,17 +2196,27 @@ export function classifyWorkerCheckpointStatus(cwd: string): GjcWorkerCheckpoint
|
|
|
2531
2196
|
.some(line => UNMERGED_GIT_STATUS_CODES.has(line.slice(0, 2)));
|
|
2532
2197
|
const conflictFiles = listConflictFiles(cwd);
|
|
2533
2198
|
if (hasUnmergedStatus || conflictFiles.length > 0) {
|
|
2534
|
-
return {
|
|
2199
|
+
return {
|
|
2200
|
+
kind: "conflicted",
|
|
2201
|
+
files: conflictFiles.length > 0 ? conflictFiles : files,
|
|
2202
|
+
};
|
|
2535
2203
|
}
|
|
2536
2204
|
const classified = classifyGjcTeamCheckpointFiles(files);
|
|
2537
2205
|
if (classified.eligible.length === 0 && classified.protected.length > 0)
|
|
2538
2206
|
return { kind: "protected_only", files: classified.protected };
|
|
2539
2207
|
return { kind: "eligible", files: classified.eligible };
|
|
2540
2208
|
}
|
|
2541
|
-
export async function classifyWorkerCheckpointStatusAsync(
|
|
2542
|
-
|
|
2209
|
+
export async function classifyWorkerCheckpointStatusAsync(
|
|
2210
|
+
cwd: string,
|
|
2211
|
+
signal?: AbortSignal,
|
|
2212
|
+
): Promise<GjcWorkerCheckpointClassification> {
|
|
2213
|
+
const status = await runGitResultAsync(cwd, ["status", "--porcelain", "-uall"], signal);
|
|
2543
2214
|
if (!status.ok) {
|
|
2544
|
-
return {
|
|
2215
|
+
return {
|
|
2216
|
+
kind: "git_error",
|
|
2217
|
+
files: [],
|
|
2218
|
+
detail: status.stderr || status.stdout || "git status failed",
|
|
2219
|
+
};
|
|
2545
2220
|
}
|
|
2546
2221
|
if (!status.stdout.trim()) return { kind: "clean", files: [] };
|
|
2547
2222
|
const files = parsePorcelainStatusFiles(status.stdout);
|
|
@@ -2549,9 +2224,12 @@ export async function classifyWorkerCheckpointStatusAsync(cwd: string): Promise<
|
|
|
2549
2224
|
.split(/\r?\n/)
|
|
2550
2225
|
.filter(Boolean)
|
|
2551
2226
|
.some(line => UNMERGED_GIT_STATUS_CODES.has(line.slice(0, 2)));
|
|
2552
|
-
const conflictFiles = await listConflictFilesAsync(cwd);
|
|
2227
|
+
const conflictFiles = await listConflictFilesAsync(cwd, signal);
|
|
2553
2228
|
if (hasUnmergedStatus || conflictFiles.length > 0) {
|
|
2554
|
-
return {
|
|
2229
|
+
return {
|
|
2230
|
+
kind: "conflicted",
|
|
2231
|
+
files: conflictFiles.length > 0 ? conflictFiles : files,
|
|
2232
|
+
};
|
|
2555
2233
|
}
|
|
2556
2234
|
const classified = classifyGjcTeamCheckpointFiles(files);
|
|
2557
2235
|
if (classified.eligible.length === 0 && classified.protected.length > 0)
|
|
@@ -2613,7 +2291,11 @@ function autoCommitDirtyWorker(worker: GjcTeamWorker): {
|
|
|
2613
2291
|
const message = `gjc(team): auto-checkpoint ${worker.id} [${worker.assigned_tasks[0] ?? "unknown"}]`;
|
|
2614
2292
|
if (!runGitResult(worker.worktree_path, ["commit", "--no-verify", "-m", message]).ok)
|
|
2615
2293
|
return { ...empty, classification };
|
|
2616
|
-
return {
|
|
2294
|
+
return {
|
|
2295
|
+
committed: true,
|
|
2296
|
+
commit: resolveHead(worker.worktree_path),
|
|
2297
|
+
classification,
|
|
2298
|
+
};
|
|
2617
2299
|
}
|
|
2618
2300
|
function workerMergeRef(worker: GjcTeamWorker, workerHead: string): string {
|
|
2619
2301
|
if (!worker.worktree_path) return workerHead;
|
|
@@ -2719,7 +2401,10 @@ async function integrateGjcWorkerCommits(
|
|
|
2719
2401
|
detail: "Leader created a runtime merge commit to integrate worker history.",
|
|
2720
2402
|
});
|
|
2721
2403
|
} else {
|
|
2722
|
-
integrationByWorker[worker.id] = {
|
|
2404
|
+
integrationByWorker[worker.id] = {
|
|
2405
|
+
...state,
|
|
2406
|
+
...integrationNowState("integration_failed"),
|
|
2407
|
+
};
|
|
2723
2408
|
hygieneEntries.push({
|
|
2724
2409
|
recorded_at: now(),
|
|
2725
2410
|
operation: "integration_merge",
|
|
@@ -2838,7 +2523,10 @@ async function integrateGjcWorkerCommits(
|
|
|
2838
2523
|
}
|
|
2839
2524
|
const newLeaderHead = resolveHead(leaderCwd);
|
|
2840
2525
|
if (!newLeaderHead || newLeaderHead === leaderHead) {
|
|
2841
|
-
integrationByWorker[worker.id] = {
|
|
2526
|
+
integrationByWorker[worker.id] = {
|
|
2527
|
+
...state,
|
|
2528
|
+
...integrationNowState("integration_failed"),
|
|
2529
|
+
};
|
|
2842
2530
|
hygieneEntries.push({
|
|
2843
2531
|
recorded_at: now(),
|
|
2844
2532
|
operation: "integration_cherry_pick",
|
|
@@ -3007,158 +2695,43 @@ async function integrateGjcWorkerCommits(
|
|
|
3007
2695
|
return integrationByWorker;
|
|
3008
2696
|
}
|
|
3009
2697
|
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
await fs.mkdir(path.join(dir, "mailbox"), { recursive: true });
|
|
3013
|
-
for (const worker of workers) {
|
|
3014
|
-
await fs.mkdir(mailboxDirPath(dir, worker.id), { recursive: true });
|
|
3015
|
-
await writeJsonFile(mailboxPath(dir, worker.id), { messages: [] });
|
|
3016
|
-
await writeJsonFile(path.join(workerDir(dir, worker.id), "status.json"), { state: "idle", updated_at: now() });
|
|
3017
|
-
await writeJsonFile(workerLifecyclePath(dir, worker.id), {
|
|
3018
|
-
worker: worker.id,
|
|
3019
|
-
lifecycle_state: "starting",
|
|
3020
|
-
worker_status_state: "idle",
|
|
3021
|
-
updated_at: now(),
|
|
3022
|
-
} satisfies GjcTeamWorkerLifecycle);
|
|
3023
|
-
await writeJsonFile(path.join(workerDir(dir, worker.id), "heartbeat.json"), {
|
|
3024
|
-
pid: 0,
|
|
3025
|
-
last_turn_at: now(),
|
|
3026
|
-
turn_count: 0,
|
|
3027
|
-
alive: true,
|
|
3028
|
-
});
|
|
3029
|
-
}
|
|
3030
|
-
// Empty leader mailbox directory is runtime state, so it must exist before messages arrive.
|
|
3031
|
-
await fs.mkdir(mailboxDirPath(dir, "leader-fixed"), { recursive: true });
|
|
3032
|
-
await writeJsonFile(mailboxPath(dir, "leader-fixed"), { messages: [] });
|
|
3033
|
-
}
|
|
2698
|
+
const writeInitialGjcTeamTask = (dir: string, task: GjcTeamTask) =>
|
|
2699
|
+
withGjcTeamTaskMutation(taskStore(dir), capability => capability.writeRecovered(task));
|
|
3034
2700
|
|
|
3035
2701
|
export async function startGjcTeam(options: GjcTeamStartOptions): Promise<GjcTeamSnapshot> {
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
throw error;
|
|
3067
|
-
}
|
|
3068
|
-
const config: GjcTeamConfig = {
|
|
3069
|
-
team_name: teamName,
|
|
3070
|
-
display_name: displayName,
|
|
3071
|
-
requested_name: options.teamName ?? displayName,
|
|
3072
|
-
task: options.task,
|
|
3073
|
-
agent_type: options.agentType,
|
|
3074
|
-
worker_count: options.workerCount,
|
|
3075
|
-
max_workers: GJC_TEAM_MAX_WORKERS,
|
|
3076
|
-
state_root: stateRoot,
|
|
3077
|
-
worker_command: resolveGjcWorkerCommand(cwd, env),
|
|
3078
|
-
worker_cli_plan: workerCliPlan,
|
|
3079
|
-
tmux_command: tmuxCommand,
|
|
3080
|
-
tmux_session: tmuxContext.sessionName,
|
|
3081
|
-
tmux_session_name: tmuxContext.sessionName,
|
|
3082
|
-
tmux_target: tmuxContext.target,
|
|
3083
|
-
workspace_mode: worktreeMode.enabled ? "worktree" : "direct",
|
|
3084
|
-
dry_run: options.dryRun ?? false,
|
|
3085
|
-
leader: { session_id: env.GJC_SESSION_ID ?? env.CODEX_SESSION_ID ?? "", pane_id: tmuxContext.leaderPaneId, cwd },
|
|
3086
|
-
leader_cwd: cwd,
|
|
3087
|
-
team_state_root: stateRoot,
|
|
3088
|
-
workers,
|
|
3089
|
-
created_at: createdAt,
|
|
3090
|
-
updated_at: createdAt,
|
|
3091
|
-
};
|
|
3092
|
-
await initializeStateDirs(dir, config.workers);
|
|
3093
|
-
await writeJsonFile(path.join(dir, "config.json"), config);
|
|
3094
|
-
await writeJsonFile(path.join(dir, "manifest.v2.json"), {
|
|
3095
|
-
version: 2,
|
|
3096
|
-
team_name: config.team_name,
|
|
3097
|
-
display_name: config.display_name,
|
|
3098
|
-
requested_name: config.requested_name,
|
|
3099
|
-
tmux_session: config.tmux_session,
|
|
3100
|
-
tmux_session_name: config.tmux_session_name,
|
|
3101
|
-
tmux_target: config.tmux_target,
|
|
3102
|
-
worker_command: config.worker_command,
|
|
3103
|
-
worker_cli_plan: config.worker_cli_plan,
|
|
3104
|
-
tmux_command: config.tmux_command,
|
|
3105
|
-
leader: config.leader,
|
|
3106
|
-
workers: config.workers,
|
|
3107
|
-
workspace_mode: config.workspace_mode,
|
|
3108
|
-
dry_run: config.dry_run,
|
|
3109
|
-
created_at: createdAt,
|
|
3110
|
-
updated_at: createdAt,
|
|
3111
|
-
});
|
|
3112
|
-
await writePhase(dir, "starting");
|
|
3113
|
-
for (const task of initialTasks) await writeTask(dir, task);
|
|
3114
|
-
await appendEvent(dir, {
|
|
3115
|
-
type: "team_started",
|
|
3116
|
-
message: options.dryRun
|
|
3117
|
-
? "Created native gjc team dry-run state without starting tmux workers"
|
|
3118
|
-
: "Started native gjc team runtime",
|
|
3119
|
-
data: {
|
|
3120
|
-
worker_count: options.workerCount,
|
|
3121
|
-
agent_type: options.agentType,
|
|
3122
|
-
workspace_mode: config.workspace_mode,
|
|
3123
|
-
dry_run: config.dry_run,
|
|
3124
|
-
},
|
|
3125
|
-
});
|
|
3126
|
-
await appendTelemetry(dir, {
|
|
3127
|
-
type: "team_runtime",
|
|
3128
|
-
message: options.dryRun ? "Native gjc team dry-run state initialized" : "Native gjc team runtime initialized",
|
|
3129
|
-
data: {
|
|
3130
|
-
state_root: stateRoot,
|
|
3131
|
-
worker_command: config.worker_command,
|
|
3132
|
-
worker_cli_plan: workerCliPlan,
|
|
3133
|
-
workspace_mode: config.workspace_mode,
|
|
3134
|
-
dry_run: config.dry_run,
|
|
2702
|
+
return startGjcTeamLaunch(
|
|
2703
|
+
{
|
|
2704
|
+
maxWorkers: GJC_TEAM_MAX_WORKERS,
|
|
2705
|
+
resolveWorkerCliPlan: resolveGjcTeamWorkerCliPlan,
|
|
2706
|
+
resolveStateRoot: resolveGjcTeamStateRoot,
|
|
2707
|
+
sanitizeName,
|
|
2708
|
+
makeTeamName,
|
|
2709
|
+
teamDir,
|
|
2710
|
+
resolveDefaultWorktreeMode,
|
|
2711
|
+
resolveTmuxBinary: resolveGjcTmuxBinary,
|
|
2712
|
+
readTmuxLeaderContext: readCurrentTmuxLeaderContext,
|
|
2713
|
+
buildWorkers,
|
|
2714
|
+
buildInitialTasks,
|
|
2715
|
+
ensureWorkerWorktree,
|
|
2716
|
+
rollbackCreatedWorktrees,
|
|
2717
|
+
resolveWorkerCommand: resolveGjcWorkerCommand,
|
|
2718
|
+
mailboxDirPath,
|
|
2719
|
+
mailboxPath,
|
|
2720
|
+
workerDir,
|
|
2721
|
+
workerLifecyclePath,
|
|
2722
|
+
writeJson: writeJsonFile,
|
|
2723
|
+
now,
|
|
2724
|
+
writePhase,
|
|
2725
|
+
writeTask: writeInitialGjcTeamTask,
|
|
2726
|
+
appendEvent,
|
|
2727
|
+
appendTelemetry,
|
|
2728
|
+
startTmuxSession,
|
|
2729
|
+
killWorkerPanes,
|
|
2730
|
+
writeWorkerLifecycleForConfig,
|
|
2731
|
+
readSnapshot: readGjcTeamSnapshot,
|
|
3135
2732
|
},
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
try {
|
|
3139
|
-
tmuxWorkers = await startTmuxSession(config, dir, options.dryRun ?? false, env);
|
|
3140
|
-
} catch (error) {
|
|
3141
|
-
await writePhase(dir, "failed");
|
|
3142
|
-
await appendEvent(dir, {
|
|
3143
|
-
type: "team_start_failed",
|
|
3144
|
-
message: error instanceof Error ? error.message : String(error),
|
|
3145
|
-
});
|
|
3146
|
-
killWorkerPanes(config);
|
|
3147
|
-
await rollbackCreatedWorktrees(config.workers);
|
|
3148
|
-
throw error;
|
|
3149
|
-
}
|
|
3150
|
-
const runningConfig = {
|
|
3151
|
-
...config,
|
|
3152
|
-
workers: tmuxWorkers.map(worker => ({ ...worker, status: "idle" as const, last_heartbeat: now() })),
|
|
3153
|
-
updated_at: now(),
|
|
3154
|
-
};
|
|
3155
|
-
await writeJsonFile(path.join(dir, "config.json"), runningConfig);
|
|
3156
|
-
await writeWorkerLifecycleForConfig(dir, runningConfig, "starting", worker => ({
|
|
3157
|
-
pane_id: worker.pane_id,
|
|
3158
|
-
started_at: runningConfig.created_at,
|
|
3159
|
-
}));
|
|
3160
|
-
await writePhase(dir, "running");
|
|
3161
|
-
return readGjcTeamSnapshot(teamName, cwd, env);
|
|
2733
|
+
options,
|
|
2734
|
+
);
|
|
3162
2735
|
}
|
|
3163
2736
|
|
|
3164
2737
|
export async function readGjcTeamSnapshot(
|
|
@@ -3183,8 +2756,8 @@ export async function readGjcTeamSnapshot(
|
|
|
3183
2756
|
const workerLifecycleById = await readWorkerLifecycleById(dir, config);
|
|
3184
2757
|
const notificationSummary =
|
|
3185
2758
|
options.reconcileNotifications === true
|
|
3186
|
-
? await reconcileTeamNotifications(dir, config)
|
|
3187
|
-
:
|
|
2759
|
+
? await reconcileTeamNotifications(teamNotificationRuntime, dir, config)
|
|
2760
|
+
: summarizeTeamNotifications(await listNotificationRecords(dir));
|
|
3188
2761
|
const phase = await resolveGjcTeamSnapshotPhase(dir, config, storedPhase, tasks, monitor);
|
|
3189
2762
|
return {
|
|
3190
2763
|
team_name: config.team_name,
|
|
@@ -3218,6 +2791,7 @@ function workerIntegrationFingerprint(head: string | null, classification: GjcWo
|
|
|
3218
2791
|
export async function requestGjcWorkerIntegrationAttempt(
|
|
3219
2792
|
cwd = process.cwd(),
|
|
3220
2793
|
env: NodeJS.ProcessEnv = process.env,
|
|
2794
|
+
options: GjcWorkerIntegrationAttemptOptions = {},
|
|
3221
2795
|
): Promise<GjcWorkerIntegrationAttemptRequestResult> {
|
|
3222
2796
|
const teamName = env.GJC_TEAM_NAME?.trim();
|
|
3223
2797
|
const worker = env.GJC_TEAM_WORKER_ID?.trim() || env.GJC_TEAM_INTERNAL_WORKER?.split("/").pop()?.trim();
|
|
@@ -3227,19 +2801,63 @@ export async function requestGjcWorkerIntegrationAttempt(
|
|
|
3227
2801
|
const configuredWorker = config.workers.find(candidate => candidate.id === worker);
|
|
3228
2802
|
const worktreePath = env.GJC_TEAM_WORKTREE_PATH?.trim() || configuredWorker?.worktree_path;
|
|
3229
2803
|
if (!worktreePath || !(await pathExists(worktreePath)))
|
|
3230
|
-
return {
|
|
2804
|
+
return {
|
|
2805
|
+
requested: false,
|
|
2806
|
+
reason: "missing_worktree",
|
|
2807
|
+
worker,
|
|
2808
|
+
team_name: teamName,
|
|
2809
|
+
};
|
|
3231
2810
|
const [classification, head] = await Promise.all([
|
|
3232
|
-
classifyWorkerCheckpointStatusAsync(worktreePath),
|
|
3233
|
-
resolveHeadAsync(worktreePath),
|
|
2811
|
+
classifyWorkerCheckpointStatusAsync(worktreePath, options.signal),
|
|
2812
|
+
resolveHeadAsync(worktreePath, options.signal),
|
|
3234
2813
|
]);
|
|
3235
2814
|
if (classification.kind === "git_error") {
|
|
2815
|
+
const reportWorker = configuredWorker ?? {
|
|
2816
|
+
id: worker,
|
|
2817
|
+
name: worker,
|
|
2818
|
+
index: 0,
|
|
2819
|
+
agent_type: "unknown",
|
|
2820
|
+
role: "unknown",
|
|
2821
|
+
status: "idle",
|
|
2822
|
+
last_heartbeat: now(),
|
|
2823
|
+
assigned_tasks: [],
|
|
2824
|
+
};
|
|
2825
|
+
const detail = classification.detail;
|
|
2826
|
+
await appendIntegrationEvent(dir, "worker_integration_attempt_failed", reportWorker, {
|
|
2827
|
+
worker_name: worker,
|
|
2828
|
+
worktree_path: worktreePath,
|
|
2829
|
+
detail,
|
|
2830
|
+
retryable: true,
|
|
2831
|
+
summary: `retryable worker integration request failure for ${worker}: ${detail}`,
|
|
2832
|
+
});
|
|
2833
|
+
await notifyLeader(
|
|
2834
|
+
config,
|
|
2835
|
+
reportWorker,
|
|
2836
|
+
`INTEGRATION RETRYABLE FAILURE: ${worker} git inspection failed (${detail}). A later turn can retry.`,
|
|
2837
|
+
cwd,
|
|
2838
|
+
env,
|
|
2839
|
+
);
|
|
3236
2840
|
return { requested: false, reason: "git_error", worker, team_name: teamName, head, status: classification.kind };
|
|
3237
2841
|
}
|
|
3238
2842
|
if (classification.kind === "protected_only") {
|
|
3239
|
-
return {
|
|
2843
|
+
return {
|
|
2844
|
+
requested: false,
|
|
2845
|
+
reason: "no_changes",
|
|
2846
|
+
worker,
|
|
2847
|
+
team_name: teamName,
|
|
2848
|
+
head,
|
|
2849
|
+
status: classification.kind,
|
|
2850
|
+
};
|
|
3240
2851
|
}
|
|
3241
2852
|
if (classification.kind === "clean" && configuredWorker?.worktree_base_ref === head) {
|
|
3242
|
-
return {
|
|
2853
|
+
return {
|
|
2854
|
+
requested: false,
|
|
2855
|
+
reason: "no_changes",
|
|
2856
|
+
worker,
|
|
2857
|
+
team_name: teamName,
|
|
2858
|
+
head,
|
|
2859
|
+
status: classification.kind,
|
|
2860
|
+
};
|
|
3243
2861
|
}
|
|
3244
2862
|
const fingerprint = workerIntegrationFingerprint(head, classification);
|
|
3245
2863
|
const dedupePath = workerIntegrationDedupePath(dir, worker);
|
|
@@ -3265,7 +2883,12 @@ export async function requestGjcWorkerIntegrationAttempt(
|
|
|
3265
2883
|
type: "worker_integration_attempt_requested",
|
|
3266
2884
|
worker,
|
|
3267
2885
|
message: `Worker ${worker} requested leader integration attempt`,
|
|
3268
|
-
data: {
|
|
2886
|
+
data: {
|
|
2887
|
+
worker_name: worker,
|
|
2888
|
+
worker_head: head,
|
|
2889
|
+
status: classification.kind,
|
|
2890
|
+
files: classification.files,
|
|
2891
|
+
},
|
|
3269
2892
|
});
|
|
3270
2893
|
await sendGjcTeamMessage(
|
|
3271
2894
|
teamName,
|
|
@@ -3315,6 +2938,403 @@ export async function buildTeamHudSummary(
|
|
|
3315
2938
|
});
|
|
3316
2939
|
}
|
|
3317
2940
|
|
|
2941
|
+
const GJC_TEAM_AUTO_CONTINUE_STALLED_WORKERS_ENV = "GJC_TEAM_AUTO_CONTINUE_STALLED_WORKERS";
|
|
2942
|
+
const GJC_TEAM_CONTINUATION_DISPATCH_TIMEOUT_MS = 5_000;
|
|
2943
|
+
|
|
2944
|
+
async function validateGjcContinuationEligibility(
|
|
2945
|
+
dir: string,
|
|
2946
|
+
config: GjcTeamConfig,
|
|
2947
|
+
worker: GjcTeamWorker,
|
|
2948
|
+
task: GjcTeamTask,
|
|
2949
|
+
heartbeatAt: string,
|
|
2950
|
+
staleMs: number,
|
|
2951
|
+
env: NodeJS.ProcessEnv,
|
|
2952
|
+
reservedHoldUntil?: string,
|
|
2953
|
+
): Promise<string | null> {
|
|
2954
|
+
const phase = await readContinuationJson<Record<string, unknown>>(path.join(dir, "phase.json"));
|
|
2955
|
+
if (
|
|
2956
|
+
phase?.current_phase !== "running" ||
|
|
2957
|
+
typeof phase.updated_at !== "string" ||
|
|
2958
|
+
!Number.isFinite(Date.parse(phase.updated_at))
|
|
2959
|
+
)
|
|
2960
|
+
return "invalid_or_absent_running_phase";
|
|
2961
|
+
if (
|
|
2962
|
+
!worker.pane_id ||
|
|
2963
|
+
!/^%\d+$/.test(worker.pane_id) ||
|
|
2964
|
+
worker.pane_id === config.leader.pane_id ||
|
|
2965
|
+
!paneBelongsToTeamTarget(config, worker.pane_id)
|
|
2966
|
+
)
|
|
2967
|
+
return "invalid_pane_authority";
|
|
2968
|
+
const shutdownAuthority = await readGjcShutdownAuthority(
|
|
2969
|
+
workerRuntime,
|
|
2970
|
+
path.join(workerDir(dir, worker.id), "shutdown-request.json"),
|
|
2971
|
+
);
|
|
2972
|
+
if (shutdownAuthority.state === "valid_present") return "shutdown_requested";
|
|
2973
|
+
if (shutdownAuthority.state === "invalid_or_unreadable") return "invalid_shutdown_authority";
|
|
2974
|
+
const heartbeat = await readWorkerHeartbeat(workerRuntime, config.team_name, worker.id, config.leader_cwd, env);
|
|
2975
|
+
if (!heartbeat || heartbeat.last_turn_at !== heartbeatAt || currentTimeMs() - Date.parse(heartbeatAt) < staleMs)
|
|
2976
|
+
return "heartbeat_changed";
|
|
2977
|
+
const lifecycle = (await readLifecycleById(workerRuntime, dir, config))[worker.id];
|
|
2978
|
+
const status = await readWorkerStatus(workerRuntime, config.team_name, worker.id, config.leader_cwd, env);
|
|
2979
|
+
if (
|
|
2980
|
+
(lifecycle?.lifecycle_state !== "ready" && lifecycle?.lifecycle_state !== "working") ||
|
|
2981
|
+
!["idle", "working", "blocked", "done"].includes(status.state) ||
|
|
2982
|
+
typeof status.updated_at !== "string" ||
|
|
2983
|
+
!Number.isFinite(Date.parse(status.updated_at))
|
|
2984
|
+
)
|
|
2985
|
+
return "invalid_worker_lifecycle_or_status";
|
|
2986
|
+
const inventory = await readGjcContinuationAuthorityInventory(dir);
|
|
2987
|
+
if (!inventory.valid) return "invalid_authority_inventory";
|
|
2988
|
+
const authority = selectGjcContinuationWorkerAuthority(inventory, worker.id);
|
|
2989
|
+
if (
|
|
2990
|
+
!authority.valid ||
|
|
2991
|
+
authority.task.id !== task.id ||
|
|
2992
|
+
authority.task.version !== task.version ||
|
|
2993
|
+
authority.task.claim?.owner !== task.claim?.owner ||
|
|
2994
|
+
authority.task.claim?.token !== task.claim?.token ||
|
|
2995
|
+
authority.task.claim?.leased_until !== task.claim?.leased_until
|
|
2996
|
+
)
|
|
2997
|
+
return "claim_changed";
|
|
2998
|
+
const leaseUntil = Date.parse(task.claim?.leased_until ?? "");
|
|
2999
|
+
const holdUntil = reservedHoldUntil === undefined ? undefined : Date.parse(reservedHoldUntil);
|
|
3000
|
+
if (!Number.isFinite(leaseUntil) || leaseUntil <= currentTimeMs()) return "invalid_or_expired_lease";
|
|
3001
|
+
if (holdUntil !== undefined && (!Number.isFinite(holdUntil) || leaseUntil < holdUntil))
|
|
3002
|
+
return "lease_does_not_cover_hold";
|
|
3003
|
+
if (reservedHoldUntil !== undefined && shouldDispatchWorkerWithSendKeys(config.tmux_command))
|
|
3004
|
+
return "unsupported_send_keys_transport";
|
|
3005
|
+
return null;
|
|
3006
|
+
}
|
|
3007
|
+
function normalizeGjcContinuationDispatchError(value: unknown, fallback: string, maxLength: number): string {
|
|
3008
|
+
try {
|
|
3009
|
+
const normalized = String(value ?? "")
|
|
3010
|
+
.replace(/(token|secret|password|authorization)\s*[:=]\s*\S+/gi, "$1=[redacted]")
|
|
3011
|
+
.trim()
|
|
3012
|
+
.slice(0, maxLength);
|
|
3013
|
+
return normalized || fallback;
|
|
3014
|
+
} catch {
|
|
3015
|
+
return fallback;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
function readGjcContinuationDispatchErrorField(error: unknown, field: "name" | "code" | "message"): unknown {
|
|
3020
|
+
try {
|
|
3021
|
+
return typeof error === "object" && error !== null ? (error as Record<string, unknown>)[field] : undefined;
|
|
3022
|
+
} catch {
|
|
3023
|
+
return undefined;
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
async function continueStalledGjcTeamWorkers(
|
|
3027
|
+
dir: string,
|
|
3028
|
+
config: GjcTeamConfig,
|
|
3029
|
+
env: NodeJS.ProcessEnv,
|
|
3030
|
+
): Promise<void> {
|
|
3031
|
+
if (env[GJC_TEAM_AUTO_CONTINUE_STALLED_WORKERS_ENV] !== "1" || config.dry_run) return;
|
|
3032
|
+
const phase = await readContinuationJson<unknown>(path.join(dir, "phase.json"));
|
|
3033
|
+
|
|
3034
|
+
if (
|
|
3035
|
+
typeof phase !== "object" ||
|
|
3036
|
+
phase === null ||
|
|
3037
|
+
Array.isArray(phase) ||
|
|
3038
|
+
(phase as Record<string, unknown>).current_phase !== "running" ||
|
|
3039
|
+
typeof (phase as Record<string, unknown>).updated_at !== "string" ||
|
|
3040
|
+
!Number.isFinite(Date.parse(String((phase as Record<string, unknown>).updated_at)))
|
|
3041
|
+
) {
|
|
3042
|
+
await appendEvent(dir, {
|
|
3043
|
+
type: "continuation_skipped",
|
|
3044
|
+
message: "Continuation requires a valid running team phase record",
|
|
3045
|
+
data: { reason: "invalid_or_absent_running_phase" },
|
|
3046
|
+
});
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
3049
|
+
const staleMs = parseHeartbeatStaleMs(env);
|
|
3050
|
+
if (staleMs <= 0) return;
|
|
3051
|
+
const inventory = await readGjcContinuationAuthorityInventory(dir);
|
|
3052
|
+
if (!inventory.valid) {
|
|
3053
|
+
await appendEvent(dir, {
|
|
3054
|
+
type: "continuation_skipped",
|
|
3055
|
+
message: "Continuation requires a complete canonical task and claim authority inventory",
|
|
3056
|
+
data: { reason: "invalid_authority_inventory" },
|
|
3057
|
+
});
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
for (const worker of config.workers) {
|
|
3061
|
+
const heartbeat = await readWorkerHeartbeat(workerRuntime, config.team_name, worker.id, config.leader_cwd, env);
|
|
3062
|
+
const heartbeatAt = Date.parse(heartbeat?.last_turn_at ?? "");
|
|
3063
|
+
if (!heartbeat || !Number.isFinite(heartbeatAt) || currentTimeMs() - heartbeatAt < staleMs) continue;
|
|
3064
|
+
if (!worker.pane_id || !/^%\d+$/.test(worker.pane_id) || worker.pane_id === config.leader.pane_id) continue;
|
|
3065
|
+
if (!paneBelongsToTeamTarget(config, worker.pane_id)) continue;
|
|
3066
|
+
const shutdownAuthority = await readGjcShutdownAuthority(
|
|
3067
|
+
workerRuntime,
|
|
3068
|
+
path.join(workerDir(dir, worker.id), "shutdown-request.json"),
|
|
3069
|
+
);
|
|
3070
|
+
if (shutdownAuthority.state !== "proven_absent") {
|
|
3071
|
+
await appendEvent(dir, {
|
|
3072
|
+
type: "continuation_skipped",
|
|
3073
|
+
worker: worker.id,
|
|
3074
|
+
message: "Continuation requires a readable absent shutdown authority record",
|
|
3075
|
+
data: {
|
|
3076
|
+
reason:
|
|
3077
|
+
shutdownAuthority.state === "valid_present" ? "shutdown_requested" : "invalid_shutdown_authority",
|
|
3078
|
+
},
|
|
3079
|
+
});
|
|
3080
|
+
continue;
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
const lifecycle = (await readLifecycleById(workerRuntime, dir, config))[worker.id];
|
|
3084
|
+
const status = await readWorkerStatus(workerRuntime, config.team_name, worker.id, config.leader_cwd, env);
|
|
3085
|
+
if (
|
|
3086
|
+
(lifecycle?.lifecycle_state !== "ready" && lifecycle?.lifecycle_state !== "working") ||
|
|
3087
|
+
!["idle", "working", "blocked", "done"].includes(status.state) ||
|
|
3088
|
+
typeof status.updated_at !== "string" ||
|
|
3089
|
+
!Number.isFinite(Date.parse(status.updated_at))
|
|
3090
|
+
) {
|
|
3091
|
+
await appendEvent(dir, {
|
|
3092
|
+
type: "continuation_skipped",
|
|
3093
|
+
worker: worker.id,
|
|
3094
|
+
message: "Continuation requires valid worker lifecycle and status records",
|
|
3095
|
+
data: { reason: "invalid_worker_lifecycle_or_status" },
|
|
3096
|
+
});
|
|
3097
|
+
continue;
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
const authority = selectGjcContinuationWorkerAuthority(inventory, worker.id);
|
|
3101
|
+
if (!authority.valid) {
|
|
3102
|
+
await appendEvent(dir, {
|
|
3103
|
+
type: "continuation_skipped",
|
|
3104
|
+
worker: worker.id,
|
|
3105
|
+
message: "Continuation requires exactly one canonical current in-progress claim",
|
|
3106
|
+
data: { reason: "invalid_claim_count", task_count: authority.taskCount, claim_count: authority.claimCount },
|
|
3107
|
+
});
|
|
3108
|
+
continue;
|
|
3109
|
+
}
|
|
3110
|
+
const task = authority.task;
|
|
3111
|
+
const eligibilityReason = await validateGjcContinuationEligibility(
|
|
3112
|
+
dir,
|
|
3113
|
+
config,
|
|
3114
|
+
worker,
|
|
3115
|
+
task,
|
|
3116
|
+
heartbeat.last_turn_at,
|
|
3117
|
+
staleMs,
|
|
3118
|
+
env,
|
|
3119
|
+
);
|
|
3120
|
+
if (eligibilityReason) {
|
|
3121
|
+
await appendEvent(dir, {
|
|
3122
|
+
type: "continuation_skipped",
|
|
3123
|
+
worker: worker.id,
|
|
3124
|
+
message: "Continuation eligibility changed or contains an invalid mutable record",
|
|
3125
|
+
data: { reason: eligibilityReason },
|
|
3126
|
+
});
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
const incident = stableHash(
|
|
3131
|
+
[
|
|
3132
|
+
config.team_name,
|
|
3133
|
+
worker.id,
|
|
3134
|
+
task.id,
|
|
3135
|
+
task.claim.owner,
|
|
3136
|
+
task.claim.token,
|
|
3137
|
+
task.version,
|
|
3138
|
+
heartbeat.last_turn_at,
|
|
3139
|
+
worker.pane_id,
|
|
3140
|
+
config.tmux_target,
|
|
3141
|
+
].join(":"),
|
|
3142
|
+
);
|
|
3143
|
+
const journalDir = path.join(workerDir(dir, worker.id), "continuations", incident);
|
|
3144
|
+
const firstReservationPath = path.join(journalDir, "attempt-01.reservation.json");
|
|
3145
|
+
const firstOutcomePath = path.join(journalDir, "attempt-01.outcome.json");
|
|
3146
|
+
const firstReservation = await readContinuationJson<Record<string, unknown>>(firstReservationPath);
|
|
3147
|
+
|
|
3148
|
+
const firstOutcome = await readContinuationJson<Record<string, unknown>>(firstOutcomePath);
|
|
3149
|
+
|
|
3150
|
+
let attempt = 1;
|
|
3151
|
+
if (firstReservation) {
|
|
3152
|
+
if (
|
|
3153
|
+
!isValidGjcContinuationReservation(
|
|
3154
|
+
firstReservation,
|
|
3155
|
+
incident,
|
|
3156
|
+
1,
|
|
3157
|
+
config,
|
|
3158
|
+
worker.id,
|
|
3159
|
+
task,
|
|
3160
|
+
task.claim,
|
|
3161
|
+
heartbeat.last_turn_at,
|
|
3162
|
+
worker.pane_id,
|
|
3163
|
+
)
|
|
3164
|
+
)
|
|
3165
|
+
continue;
|
|
3166
|
+
if (
|
|
3167
|
+
!isValidGjcContinuationOutcome(firstOutcome, firstReservation, incident, 1) ||
|
|
3168
|
+
firstOutcome.result !== "sent"
|
|
3169
|
+
)
|
|
3170
|
+
continue;
|
|
3171
|
+
const firstHold = Date.parse(String(firstOutcome.hold_until));
|
|
3172
|
+
const leaseUntil = Date.parse(task.claim.leased_until);
|
|
3173
|
+
if (
|
|
3174
|
+
!Number.isFinite(firstHold) ||
|
|
3175
|
+
!Number.isFinite(leaseUntil) ||
|
|
3176
|
+
leaseUntil < firstHold ||
|
|
3177
|
+
currentTimeMs() < firstHold
|
|
3178
|
+
)
|
|
3179
|
+
continue;
|
|
3180
|
+
if (await readContinuationJson(path.join(journalDir, "attempt-02.reservation.json"))) continue;
|
|
3181
|
+
|
|
3182
|
+
attempt = 2;
|
|
3183
|
+
}
|
|
3184
|
+
const holdMs = attempt === 1 ? 30_000 : 120_000;
|
|
3185
|
+
const reservedAtMs = currentTimeMs();
|
|
3186
|
+
const reservedAt = new Date(reservedAtMs).toISOString();
|
|
3187
|
+
const holdUntil = new Date(reservedAtMs + holdMs).toISOString();
|
|
3188
|
+
const leaseUntil = Date.parse(task.claim.leased_until);
|
|
3189
|
+
if (!Number.isFinite(leaseUntil) || leaseUntil < Date.parse(holdUntil)) continue;
|
|
3190
|
+
const reservation = {
|
|
3191
|
+
schema_version: 1,
|
|
3192
|
+
incident_hash: incident,
|
|
3193
|
+
team_name: config.team_name,
|
|
3194
|
+
worker: worker.id,
|
|
3195
|
+
task_id: task.id,
|
|
3196
|
+
owner: task.claim.owner,
|
|
3197
|
+
claim_token: task.claim.token,
|
|
3198
|
+
task_version: task.version,
|
|
3199
|
+
leased_until: task.claim.leased_until,
|
|
3200
|
+
heartbeat_at: heartbeat.last_turn_at,
|
|
3201
|
+
pane_id: worker.pane_id,
|
|
3202
|
+
tmux_target: config.tmux_target,
|
|
3203
|
+
attempt,
|
|
3204
|
+
reserved_at: reservedAt,
|
|
3205
|
+
hold_until: holdUntil,
|
|
3206
|
+
prompt_version: 1,
|
|
3207
|
+
prompt_sha256: createHash("sha256").update(GJC_TEAM_CONTINUATION_PROMPT).digest("hex"),
|
|
3208
|
+
dispatch_protocol: "tmux_command_sequence_v1",
|
|
3209
|
+
};
|
|
3210
|
+
const reservationPath = path.join(journalDir, `attempt-0${attempt}.reservation.json`);
|
|
3211
|
+
try {
|
|
3212
|
+
await createJsonNoClobber(
|
|
3213
|
+
reservationPath,
|
|
3214
|
+
reservation,
|
|
3215
|
+
stateWriterOptions(reservationPath, "state", "continuation-reservation"),
|
|
3216
|
+
);
|
|
3217
|
+
} catch (error) {
|
|
3218
|
+
if (error instanceof AlreadyExistsError) continue;
|
|
3219
|
+
throw error;
|
|
3220
|
+
}
|
|
3221
|
+
let result: "sent" | "unknown" = "unknown";
|
|
3222
|
+
let outcomeReason = "tmux_missing_exit_code";
|
|
3223
|
+
let tmuxExitCode: number | undefined;
|
|
3224
|
+
let tmuxError: { name: string; code?: string; message: string } | undefined;
|
|
3225
|
+
let dispatchedAt: string | undefined;
|
|
3226
|
+
let dispatchHoldUntil: string | undefined;
|
|
3227
|
+
if (gjcTeamRuntimeTestSeams?.continuationBeforeDispatch)
|
|
3228
|
+
await gjcTeamRuntimeTestSeams.continuationBeforeDispatch();
|
|
3229
|
+
const revalidationReason = await validateGjcContinuationEligibility(
|
|
3230
|
+
dir,
|
|
3231
|
+
config,
|
|
3232
|
+
worker,
|
|
3233
|
+
task,
|
|
3234
|
+
heartbeat.last_turn_at,
|
|
3235
|
+
staleMs,
|
|
3236
|
+
env,
|
|
3237
|
+
new Date(currentTimeMs() + GJC_TEAM_CONTINUATION_DISPATCH_TIMEOUT_MS + holdMs).toISOString(),
|
|
3238
|
+
);
|
|
3239
|
+
if (revalidationReason) {
|
|
3240
|
+
const skippedPath = path.join(journalDir, `attempt-0${attempt}.outcome.json`);
|
|
3241
|
+
await createJsonNoClobber(
|
|
3242
|
+
skippedPath,
|
|
3243
|
+
{
|
|
3244
|
+
schema_version: 1,
|
|
3245
|
+
incident_hash: incident,
|
|
3246
|
+
attempt,
|
|
3247
|
+
reservation_sha256: gjcContinuationReservationDigest(reservation),
|
|
3248
|
+
recorded_at: now(),
|
|
3249
|
+
result: "skipped",
|
|
3250
|
+
reason: revalidationReason,
|
|
3251
|
+
},
|
|
3252
|
+
stateWriterOptions(skippedPath, "state", "continuation-outcome"),
|
|
3253
|
+
);
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
try {
|
|
3257
|
+
const args = Object.freeze([
|
|
3258
|
+
"send-keys",
|
|
3259
|
+
"-l",
|
|
3260
|
+
"-t",
|
|
3261
|
+
worker.pane_id,
|
|
3262
|
+
GJC_TEAM_CONTINUATION_PROMPT,
|
|
3263
|
+
";",
|
|
3264
|
+
"send-keys",
|
|
3265
|
+
"-t",
|
|
3266
|
+
worker.pane_id,
|
|
3267
|
+
"Enter",
|
|
3268
|
+
]);
|
|
3269
|
+
const dispatch = gjcTeamRuntimeTestSeams?.continuationTmuxDispatch
|
|
3270
|
+
? gjcTeamRuntimeTestSeams.continuationTmuxDispatch(config.tmux_command, args)
|
|
3271
|
+
: Bun.spawnSync([config.tmux_command, ...args], {
|
|
3272
|
+
stdout: "ignore",
|
|
3273
|
+
stderr: "ignore",
|
|
3274
|
+
timeout: GJC_TEAM_CONTINUATION_DISPATCH_TIMEOUT_MS,
|
|
3275
|
+
});
|
|
3276
|
+
const dispatchAtMs = currentTimeMs();
|
|
3277
|
+
tmuxExitCode = dispatch.exitCode;
|
|
3278
|
+
if (dispatch.exitCode === 0) {
|
|
3279
|
+
result = "sent";
|
|
3280
|
+
outcomeReason = "tmux_sent";
|
|
3281
|
+
dispatchedAt = new Date(dispatchAtMs).toISOString();
|
|
3282
|
+
dispatchHoldUntil = new Date(dispatchAtMs + holdMs).toISOString();
|
|
3283
|
+
} else if (typeof dispatch.exitCode === "number") {
|
|
3284
|
+
outcomeReason = "tmux_nonzero_exit";
|
|
3285
|
+
}
|
|
3286
|
+
} catch (error) {
|
|
3287
|
+
outcomeReason = "tmux_dispatch_threw";
|
|
3288
|
+
const name = normalizeGjcContinuationDispatchError(
|
|
3289
|
+
readGjcContinuationDispatchErrorField(error, "name"),
|
|
3290
|
+
"Error",
|
|
3291
|
+
120,
|
|
3292
|
+
);
|
|
3293
|
+
const code = normalizeGjcContinuationDispatchError(
|
|
3294
|
+
readGjcContinuationDispatchErrorField(error, "code"),
|
|
3295
|
+
"",
|
|
3296
|
+
120,
|
|
3297
|
+
);
|
|
3298
|
+
tmuxError = {
|
|
3299
|
+
name,
|
|
3300
|
+
...(code ? { code } : {}),
|
|
3301
|
+
message: normalizeGjcContinuationDispatchError(
|
|
3302
|
+
readGjcContinuationDispatchErrorField(error, "message") ?? error,
|
|
3303
|
+
"tmux_dispatch_error",
|
|
3304
|
+
400,
|
|
3305
|
+
),
|
|
3306
|
+
};
|
|
3307
|
+
}
|
|
3308
|
+
const outcomePath = path.join(journalDir, `attempt-0${attempt}.outcome.json`);
|
|
3309
|
+
try {
|
|
3310
|
+
await createJsonNoClobber(
|
|
3311
|
+
outcomePath,
|
|
3312
|
+
{
|
|
3313
|
+
schema_version: 1,
|
|
3314
|
+
incident_hash: incident,
|
|
3315
|
+
attempt,
|
|
3316
|
+
reservation_sha256: gjcContinuationReservationDigest(reservation),
|
|
3317
|
+
recorded_at: now(),
|
|
3318
|
+
result,
|
|
3319
|
+
reason: outcomeReason,
|
|
3320
|
+
...(tmuxExitCode === undefined ? {} : { tmux_exit_code: tmuxExitCode }),
|
|
3321
|
+
...(dispatchedAt === undefined || dispatchHoldUntil === undefined
|
|
3322
|
+
? {}
|
|
3323
|
+
: { dispatched_at: dispatchedAt, hold_until: dispatchHoldUntil }),
|
|
3324
|
+
...(tmuxError ? { tmux_error: tmuxError } : {}),
|
|
3325
|
+
},
|
|
3326
|
+
stateWriterOptions(outcomePath, "state", "continuation-outcome"),
|
|
3327
|
+
);
|
|
3328
|
+
} catch (error) {
|
|
3329
|
+
if (!(error instanceof AlreadyExistsError)) throw error;
|
|
3330
|
+
const existing = await readContinuationJson<Record<string, unknown>>(outcomePath);
|
|
3331
|
+
if (!isValidGjcContinuationOutcome(existing, reservation, incident, attempt))
|
|
3332
|
+
throw new Error(`invalid_continuation_outcome:${incident}:${attempt}`);
|
|
3333
|
+
}
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3318
3338
|
export async function monitorGjcTeam(
|
|
3319
3339
|
teamName: string,
|
|
3320
3340
|
cwd = process.cwd(),
|
|
@@ -3323,11 +3343,18 @@ export async function monitorGjcTeam(
|
|
|
3323
3343
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
3324
3344
|
const config = await readConfig(dir);
|
|
3325
3345
|
const previous = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
|
|
3326
|
-
await
|
|
3346
|
+
await withGjcTeamTaskMutation(taskStore(dir), async capability => {
|
|
3347
|
+
const config = await readConfig(dir);
|
|
3348
|
+
await continueStalledGjcTeamWorkers(dir, config, env);
|
|
3349
|
+
await reconcileGjcTeamStaleClaimsUnlocked(workerOrchestrationRuntime, teamName, dir, config, env, capability);
|
|
3350
|
+
await computeLifecycleNudges(config, dir, cwd, env);
|
|
3351
|
+
});
|
|
3327
3352
|
const integrationByWorker = await integrateGjcWorkerCommits(config, dir, previous, cwd, env);
|
|
3328
|
-
await writeJsonFile(monitorSnapshotPath(dir), {
|
|
3353
|
+
await writeJsonFile(monitorSnapshotPath(dir), {
|
|
3354
|
+
integration_by_worker: integrationByWorker,
|
|
3355
|
+
updated_at: now(),
|
|
3356
|
+
});
|
|
3329
3357
|
await replayGjcTeamNotifications(teamName, cwd, env);
|
|
3330
|
-
await computeLifecycleNudges(config, dir, cwd, env);
|
|
3331
3358
|
return readGjcTeamSnapshot(teamName, cwd, env);
|
|
3332
3359
|
}
|
|
3333
3360
|
export async function listGjcTeams(
|
|
@@ -3361,23 +3388,7 @@ async function writeGjcWorkerStartupAck(
|
|
|
3361
3388
|
input: Record<string, unknown>,
|
|
3362
3389
|
): Promise<Record<string, unknown>> {
|
|
3363
3390
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
3364
|
-
|
|
3365
|
-
const teamWorker = findKnownWorker(config, worker);
|
|
3366
|
-
const ack = {
|
|
3367
|
-
worker,
|
|
3368
|
-
pid: typeof input.pid === "number" ? input.pid : undefined,
|
|
3369
|
-
session: typeof input.session === "string" ? input.session : undefined,
|
|
3370
|
-
protocol_version: String(input.protocol_version ?? "1"),
|
|
3371
|
-
ack_at: now(),
|
|
3372
|
-
};
|
|
3373
|
-
await writeJsonFile(path.join(workerDir(dir, worker), "startup-ack.json"), ack);
|
|
3374
|
-
await writeWorkerLifecycleRecord(dir, teamWorker, "ready", {
|
|
3375
|
-
pane_id: teamWorker.pane_id,
|
|
3376
|
-
pid: typeof input.pid === "number" ? input.pid : undefined,
|
|
3377
|
-
started_at: ack.ack_at,
|
|
3378
|
-
});
|
|
3379
|
-
await appendEvent(dir, { type: "worker_startup_ack", worker, message: `Worker ${worker} acknowledged startup` });
|
|
3380
|
-
return ack;
|
|
3391
|
+
return withGjcTeamMutationFence(dir, () => writeWorkerStartupAck(workerRuntime, teamName, worker, cwd, env, input));
|
|
3381
3392
|
}
|
|
3382
3393
|
function parseDurationEnv(env: NodeJS.ProcessEnv, name: string, fallbackMs: number): number {
|
|
3383
3394
|
const raw = env[name]?.trim();
|
|
@@ -3385,6 +3396,13 @@ function parseDurationEnv(env: NodeJS.ProcessEnv, name: string, fallbackMs: numb
|
|
|
3385
3396
|
const parsed = Number(raw);
|
|
3386
3397
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallbackMs;
|
|
3387
3398
|
}
|
|
3399
|
+
|
|
3400
|
+
function parseHeartbeatStaleMs(env: NodeJS.ProcessEnv): number {
|
|
3401
|
+
const raw = env.GJC_TEAM_HEARTBEAT_STALE_MS?.trim();
|
|
3402
|
+
if (!raw) return 120_000;
|
|
3403
|
+
const parsed = Number(raw);
|
|
3404
|
+
return Number.isFinite(parsed) ? parsed : 120_000;
|
|
3405
|
+
}
|
|
3388
3406
|
async function writeLifecycleNudge(
|
|
3389
3407
|
dir: string,
|
|
3390
3408
|
worker: string,
|
|
@@ -3414,7 +3432,12 @@ async function writeLifecycleNudge(
|
|
|
3414
3432
|
suggested_action: suggestedAction,
|
|
3415
3433
|
auto_action_taken: false,
|
|
3416
3434
|
};
|
|
3417
|
-
|
|
3435
|
+
try {
|
|
3436
|
+
await writeJsonFile(nudgePath, record);
|
|
3437
|
+
} catch (error) {
|
|
3438
|
+
if (isEnoent(error)) return;
|
|
3439
|
+
throw error;
|
|
3440
|
+
}
|
|
3418
3441
|
await appendEvent(dir, {
|
|
3419
3442
|
type: "worker_lifecycle_nudge",
|
|
3420
3443
|
worker,
|
|
@@ -3440,10 +3463,16 @@ async function computeLifecycleNudges(
|
|
|
3440
3463
|
env: NodeJS.ProcessEnv,
|
|
3441
3464
|
): Promise<void> {
|
|
3442
3465
|
const startupGraceMs = parseDurationEnv(env, "GJC_TEAM_STARTUP_GRACE_MS", 30_000);
|
|
3443
|
-
const heartbeatStaleMs =
|
|
3466
|
+
const heartbeatStaleMs = parseHeartbeatStaleMs(env);
|
|
3444
3467
|
const createdAt = Date.parse(config.created_at);
|
|
3445
3468
|
const ageMs = Date.now() - (Number.isFinite(createdAt) ? createdAt : Date.now());
|
|
3446
3469
|
for (const worker of config.workers) {
|
|
3470
|
+
try {
|
|
3471
|
+
await fs.access(workerDir(dir, worker.id));
|
|
3472
|
+
} catch (error) {
|
|
3473
|
+
if (isEnoent(error)) continue;
|
|
3474
|
+
throw error;
|
|
3475
|
+
}
|
|
3447
3476
|
const ack = await readJsonFile<Record<string, unknown>>(path.join(workerDir(dir, worker.id), "startup-ack.json"));
|
|
3448
3477
|
if (!ack && ageMs >= startupGraceMs) {
|
|
3449
3478
|
await writeLifecycleNudge(
|
|
@@ -3460,7 +3489,7 @@ async function computeLifecycleNudges(
|
|
|
3460
3489
|
GJC_TEAM_STATE_ROOT: config.state_root,
|
|
3461
3490
|
});
|
|
3462
3491
|
const heartbeatAt = Date.parse(heartbeat?.last_turn_at ?? worker.last_heartbeat);
|
|
3463
|
-
if (Number.isFinite(heartbeatAt) && Date.now() - heartbeatAt >= heartbeatStaleMs) {
|
|
3492
|
+
if (heartbeatStaleMs > 0 && Number.isFinite(heartbeatAt) && Date.now() - heartbeatAt >= heartbeatStaleMs) {
|
|
3464
3493
|
await writeLifecycleNudge(
|
|
3465
3494
|
dir,
|
|
3466
3495
|
worker.id,
|
|
@@ -3489,87 +3518,13 @@ export async function shutdownGjcTeam(
|
|
|
3489
3518
|
env: NodeJS.ProcessEnv = process.env,
|
|
3490
3519
|
): Promise<GjcTeamSnapshot> {
|
|
3491
3520
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
})
|
|
3499
|
-
.filter((failure): failure is { task_id: string; reason: string } => failure != null);
|
|
3500
|
-
const shutdownRequestId = `shutdown-${stableHash([config.team_name, now(), randomUUID()].join(":"))}`;
|
|
3501
|
-
const shutdownRequestedAt = now();
|
|
3502
|
-
await Promise.all(
|
|
3503
|
-
config.workers.map(worker =>
|
|
3504
|
-
writeGjcShutdownRequest(
|
|
3505
|
-
teamName,
|
|
3506
|
-
worker.id,
|
|
3507
|
-
"leader-fixed",
|
|
3508
|
-
cwd,
|
|
3509
|
-
env,
|
|
3510
|
-
shutdownRequestId,
|
|
3511
|
-
"graceful",
|
|
3512
|
-
shutdownRequestedAt,
|
|
3513
|
-
),
|
|
3514
|
-
),
|
|
3515
|
-
);
|
|
3516
|
-
const monitor = await readJsonFile<GjcTeamMonitorSnapshot>(monitorSnapshotPath(dir));
|
|
3517
|
-
const completionVerified = tasks.length === 0 || tasks.every(isGjcTeamTaskCompletionVerified);
|
|
3518
|
-
const pendingIntegration = completionVerified ? await hasPendingGjcTeamIntegration(dir, config, monitor) : false;
|
|
3519
|
-
killWorkerPanes(config);
|
|
3520
|
-
await removeCleanCreatedWorktrees(config.workers);
|
|
3521
|
-
const stopped = {
|
|
3522
|
-
...config,
|
|
3523
|
-
workers: config.workers.map(worker => ({ ...worker, status: "stopped" as const, last_heartbeat: now() })),
|
|
3524
|
-
updated_at: now(),
|
|
3525
|
-
};
|
|
3526
|
-
await writeJsonFile(path.join(dir, "config.json"), stopped);
|
|
3527
|
-
await writeWorkerLifecycleForConfig(dir, stopped, "stopped", worker => ({
|
|
3528
|
-
pane_id: worker.pane_id,
|
|
3529
|
-
stopped_at: stopped.updated_at,
|
|
3530
|
-
stop_reason: "graceful_shutdown",
|
|
3531
|
-
shutdown_request_id: shutdownRequestId,
|
|
3532
|
-
shutdown_requested_at: shutdownRequestedAt,
|
|
3533
|
-
shutdown_mode: "graceful",
|
|
3534
|
-
}));
|
|
3535
|
-
const workerLifecycleById = await readWorkerLifecycleById(dir, stopped);
|
|
3536
|
-
const gracefulShutdownComplete = stopped.workers.every(worker => {
|
|
3537
|
-
const lifecycle = workerLifecycleById[worker.id];
|
|
3538
|
-
return (
|
|
3539
|
-
lifecycle?.lifecycle_state === "stopped" &&
|
|
3540
|
-
lifecycle.shutdown_request_id === shutdownRequestId &&
|
|
3541
|
-
lifecycle.shutdown_mode === "graceful"
|
|
3542
|
-
);
|
|
3543
|
-
});
|
|
3544
|
-
const shutdownPhase: GjcTeamPhase =
|
|
3545
|
-
completionVerified && gracefulShutdownComplete
|
|
3546
|
-
? pendingIntegration
|
|
3547
|
-
? "awaiting_integration"
|
|
3548
|
-
: "complete"
|
|
3549
|
-
: evidenceFailures.length > 0 || tasks.some(task => task.status === "failed" || task.status === "blocked")
|
|
3550
|
-
? "failed"
|
|
3551
|
-
: "cancelled";
|
|
3552
|
-
await writePhase(dir, shutdownPhase);
|
|
3553
|
-
const shutdownData: Record<string, unknown> = {
|
|
3554
|
-
phase: shutdownPhase,
|
|
3555
|
-
shutdown_request_id: shutdownRequestId,
|
|
3556
|
-
graceful_shutdown_complete: gracefulShutdownComplete,
|
|
3557
|
-
};
|
|
3558
|
-
if (evidenceFailures.length > 0) shutdownData.evidence_failures = evidenceFailures;
|
|
3559
|
-
await appendEvent(dir, {
|
|
3560
|
-
type: "team_shutdown",
|
|
3561
|
-
message:
|
|
3562
|
-
shutdownPhase === "complete"
|
|
3563
|
-
? "Shut down native gjc team runtime after completed tasks"
|
|
3564
|
-
: "Shut down native gjc team runtime with incomplete tasks",
|
|
3565
|
-
data: shutdownData,
|
|
3566
|
-
});
|
|
3567
|
-
await appendTelemetry(dir, {
|
|
3568
|
-
type: "team_shutdown",
|
|
3569
|
-
message: `Native gjc team runtime stopped with phase ${shutdownPhase}`,
|
|
3570
|
-
data: { shutdown_request_id: shutdownRequestId, graceful_shutdown_complete: gracefulShutdownComplete },
|
|
3521
|
+
return withGjcTeamMutationFence(dir, () => shutdownGjcTeamWorkers(workerOrchestrationRuntime, teamName, cwd, env));
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
function taskStore(dir: string): GjcTeamTaskStore {
|
|
3525
|
+
return new GjcTeamTaskStore(dir, async event => {
|
|
3526
|
+
await appendEvent(dir, event);
|
|
3571
3527
|
});
|
|
3572
|
-
return readGjcTeamSnapshot(config.team_name, cwd, env);
|
|
3573
3528
|
}
|
|
3574
3529
|
|
|
3575
3530
|
export async function listGjcTeamTasks(
|
|
@@ -3577,7 +3532,7 @@ export async function listGjcTeamTasks(
|
|
|
3577
3532
|
cwd = process.cwd(),
|
|
3578
3533
|
env: NodeJS.ProcessEnv = process.env,
|
|
3579
3534
|
): Promise<GjcTeamTask[]> {
|
|
3580
|
-
return
|
|
3535
|
+
return taskStore(await findTeamDir(teamName, cwd, env)).list();
|
|
3581
3536
|
}
|
|
3582
3537
|
export async function readGjcTeamTask(
|
|
3583
3538
|
teamName: string,
|
|
@@ -3585,9 +3540,7 @@ export async function readGjcTeamTask(
|
|
|
3585
3540
|
cwd = process.cwd(),
|
|
3586
3541
|
env: NodeJS.ProcessEnv = process.env,
|
|
3587
3542
|
): Promise<GjcTeamTask> {
|
|
3588
|
-
|
|
3589
|
-
if (!task) throw new Error(`task_not_found:${taskId}`);
|
|
3590
|
-
return task;
|
|
3543
|
+
return taskStore(await findTeamDir(teamName, cwd, env)).read(taskId);
|
|
3591
3544
|
}
|
|
3592
3545
|
export async function createGjcTeamTask(
|
|
3593
3546
|
teamName: string,
|
|
@@ -3598,32 +3551,14 @@ export async function createGjcTeamTask(
|
|
|
3598
3551
|
taskOptions: GjcTeamTaskMetadataInput = {},
|
|
3599
3552
|
): Promise<GjcTeamTask> {
|
|
3600
3553
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
title: subject,
|
|
3610
|
-
objective: description,
|
|
3611
|
-
status: "pending",
|
|
3612
|
-
...(taskOptions.owner ? { owner: taskOptions.owner } : {}),
|
|
3613
|
-
...(taskOptions.lane ? { lane: taskOptions.lane } : {}),
|
|
3614
|
-
...(taskOptions.required_role ? { required_role: taskOptions.required_role } : {}),
|
|
3615
|
-
...(taskOptions.allowed_roles ? { allowed_roles: taskOptions.allowed_roles } : {}),
|
|
3616
|
-
...(taskOptions.depends_on ? { depends_on: taskOptions.depends_on } : {}),
|
|
3617
|
-
...(taskOptions.blocked_by ? { blocked_by: taskOptions.blocked_by } : {}),
|
|
3618
|
-
version: 1,
|
|
3619
|
-
created_at: now(),
|
|
3620
|
-
updated_at: now(),
|
|
3621
|
-
};
|
|
3622
|
-
await writeTask(dir, task);
|
|
3623
|
-
config.updated_at = now();
|
|
3624
|
-
await writeJsonFile(path.join(dir, "config.json"), config);
|
|
3625
|
-
await appendEvent(dir, { type: "task_created", task_id: task.id, message: subject });
|
|
3626
|
-
return task;
|
|
3554
|
+
return withGjcTeamTaskMutation(taskStore(dir), async capability => {
|
|
3555
|
+
const config = await readConfig(dir);
|
|
3556
|
+
if (taskOptions.owner) assertKnownWorker(config, taskOptions.owner);
|
|
3557
|
+
const task = await capability.create(subject, description, taskOptions);
|
|
3558
|
+
config.updated_at = now();
|
|
3559
|
+
await writeJsonFile(path.join(dir, "config.json"), config);
|
|
3560
|
+
return task;
|
|
3561
|
+
});
|
|
3627
3562
|
}
|
|
3628
3563
|
export async function updateGjcTeamTask(
|
|
3629
3564
|
teamName: string,
|
|
@@ -3638,18 +3573,7 @@ export async function updateGjcTeamTask(
|
|
|
3638
3573
|
env: NodeJS.ProcessEnv = process.env,
|
|
3639
3574
|
): Promise<GjcTeamTask> {
|
|
3640
3575
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
3641
|
-
|
|
3642
|
-
const updated = normalizeTask({
|
|
3643
|
-
...task,
|
|
3644
|
-
...updates,
|
|
3645
|
-
title: updates.subject ?? task.title,
|
|
3646
|
-
objective: updates.description ?? task.objective,
|
|
3647
|
-
version: task.version + 1,
|
|
3648
|
-
updated_at: now(),
|
|
3649
|
-
});
|
|
3650
|
-
await writeTask(dir, updated);
|
|
3651
|
-
await appendEvent(dir, { type: "task_updated", task_id: taskId, message: updated.subject });
|
|
3652
|
-
return updated;
|
|
3576
|
+
return taskStore(dir).update(taskId, updates);
|
|
3653
3577
|
}
|
|
3654
3578
|
export async function claimGjcTeamTask(
|
|
3655
3579
|
teamName: string,
|
|
@@ -3659,68 +3583,28 @@ export async function claimGjcTeamTask(
|
|
|
3659
3583
|
taskId?: string,
|
|
3660
3584
|
): Promise<GjcTeamApiClaimResult> {
|
|
3661
3585
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
const claimPath = path.join(dir, "claims", `${task.id}.json`);
|
|
3684
|
-
const created = await writeJsonFileNoClobber(claimPath, claim);
|
|
3685
|
-
if (!created) return { ok: false, reason: `task_already_claimed:${task.id}` };
|
|
3686
|
-
const current = await readGjcTeamTask(teamName, task.id, cwd, env);
|
|
3687
|
-
const currentEligibilityReason = getGjcTeamTaskClaimEligibilityReason(current, teamWorker, await readTasks(dir));
|
|
3688
|
-
if (currentEligibilityReason) {
|
|
3689
|
-
await fs.rm(claimPath, { force: true });
|
|
3690
|
-
return { ok: false, reason: currentEligibilityReason };
|
|
3691
|
-
}
|
|
3692
|
-
if (current.status !== "pending") {
|
|
3693
|
-
await deleteIfOwned(claimPath, {
|
|
3694
|
-
...stateWriterOptions(claimPath, "prune", "rollback"),
|
|
3695
|
-
predicate: current => (current as GjcTeamTaskClaim).token === token,
|
|
3696
|
-
});
|
|
3697
|
-
return { ok: false, reason: `task_not_pending:${task.id}` };
|
|
3698
|
-
}
|
|
3699
|
-
const updated: GjcTeamTask = {
|
|
3700
|
-
...current,
|
|
3701
|
-
status: "in_progress",
|
|
3702
|
-
assignee: workerId,
|
|
3703
|
-
owner: workerId,
|
|
3704
|
-
claim,
|
|
3705
|
-
version: current.version + 1,
|
|
3706
|
-
updated_at: now(),
|
|
3707
|
-
};
|
|
3708
|
-
try {
|
|
3709
|
-
await writeTask(dir, updated);
|
|
3710
|
-
} catch (error) {
|
|
3711
|
-
await deleteIfOwned(claimPath, {
|
|
3712
|
-
...stateWriterOptions(claimPath, "prune", "rollback"),
|
|
3713
|
-
predicate: current => (current as GjcTeamTaskClaim).token === token,
|
|
3714
|
-
});
|
|
3715
|
-
throw error;
|
|
3716
|
-
}
|
|
3717
|
-
await appendEvent(dir, {
|
|
3718
|
-
type: "task_claimed",
|
|
3719
|
-
task_id: updated.id,
|
|
3720
|
-
worker: workerId,
|
|
3721
|
-
message: "Worker claimed task",
|
|
3586
|
+
return withGjcTeamTaskMutation(taskStore(dir), async capability => {
|
|
3587
|
+
const config = await readConfig(dir);
|
|
3588
|
+
const worker = findKnownWorker(config, workerId);
|
|
3589
|
+
const recovered = await reconcileGjcTeamStaleClaimsUnlocked(
|
|
3590
|
+
workerOrchestrationRuntime,
|
|
3591
|
+
teamName,
|
|
3592
|
+
dir,
|
|
3593
|
+
config,
|
|
3594
|
+
env,
|
|
3595
|
+
capability,
|
|
3596
|
+
);
|
|
3597
|
+
const reasons = recovered.stale_workers[workerId];
|
|
3598
|
+
if (reasons?.length) return { ok: false, reason: `worker_not_live:${workerId}:${reasons.join(",")}` };
|
|
3599
|
+
const status = await readWorkerStatus(workerRuntime, teamName, workerId, cwd, env);
|
|
3600
|
+
if (
|
|
3601
|
+
!["idle", "working", "blocked", "done"].includes(status.state) ||
|
|
3602
|
+
typeof status.updated_at !== "string" ||
|
|
3603
|
+
!Number.isFinite(Date.parse(status.updated_at))
|
|
3604
|
+
)
|
|
3605
|
+
return { ok: false, reason: `worker_not_live:${workerId}:invalid_status` };
|
|
3606
|
+
return capability.claim(worker, taskId);
|
|
3722
3607
|
});
|
|
3723
|
-
return { ok: true, task: updated, worker_id: workerId, claim_token: token };
|
|
3724
3608
|
}
|
|
3725
3609
|
export async function transitionGjcTeamTaskStatus(
|
|
3726
3610
|
teamName: string,
|
|
@@ -3733,51 +3617,10 @@ export async function transitionGjcTeamTaskStatus(
|
|
|
3733
3617
|
completionEvidenceInput?: unknown,
|
|
3734
3618
|
): Promise<GjcTeamTask> {
|
|
3735
3619
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
if (status === "pending") throw new Error(`invalid_task_transition:${taskId}:pending_requires_release`);
|
|
3740
|
-
if (task.status === "completed" || task.status === "failed") throw new Error(`task_terminal:${taskId}`);
|
|
3741
|
-
if (!task.claim) throw new Error(`claim_token_required:${taskId}`);
|
|
3742
|
-
if (!claimToken) throw new Error(`claim_token_required:${taskId}`);
|
|
3743
|
-
if (task.claim.token !== claimToken) throw new Error(`claim_token_mismatch:${taskId}`);
|
|
3744
|
-
if (workerId && task.claim.owner !== workerId) throw new Error(`claim_owner_mismatch:${taskId}`);
|
|
3745
|
-
const terminal = status === "completed" || status === "failed";
|
|
3746
|
-
const transitionedAt = now();
|
|
3747
|
-
const completionEvidence =
|
|
3748
|
-
status === "completed"
|
|
3749
|
-
? normalizeGjcTeamTaskCompletionEvidence(taskId, task.claim.owner, completionEvidenceInput, transitionedAt)
|
|
3750
|
-
: undefined;
|
|
3751
|
-
const updated: GjcTeamTask = {
|
|
3752
|
-
...task,
|
|
3753
|
-
status,
|
|
3754
|
-
claim: terminal ? undefined : task.claim,
|
|
3755
|
-
version: task.version + 1,
|
|
3756
|
-
updated_at: transitionedAt,
|
|
3757
|
-
...(terminal ? { completed_at: transitionedAt } : {}),
|
|
3758
|
-
...(completionEvidence ? { completion_evidence: completionEvidence } : {}),
|
|
3759
|
-
};
|
|
3760
|
-
await writeTask(dir, updated);
|
|
3761
|
-
if (terminal) {
|
|
3762
|
-
const claimPath = path.join(dir, "claims", `${taskId}.json`);
|
|
3763
|
-
await removeFileAudited(claimPath, stateWriterOptions(claimPath, "prune", "terminal"));
|
|
3764
|
-
}
|
|
3765
|
-
const eventData: Record<string, unknown> = { status };
|
|
3766
|
-
if (completionEvidence) {
|
|
3767
|
-
eventData.completion_evidence = {
|
|
3768
|
-
recorded_by: completionEvidence.recorded_by,
|
|
3769
|
-
item_count: completionEvidence.items.length,
|
|
3770
|
-
verified_item_count: completionEvidence.items.filter(isGjcTeamCompletionEvidenceItemVerified).length,
|
|
3771
|
-
files_count: completionEvidence.files?.length ?? 0,
|
|
3772
|
-
};
|
|
3773
|
-
}
|
|
3774
|
-
await appendEvent(dir, {
|
|
3775
|
-
type: "task_transitioned",
|
|
3776
|
-
task_id: taskId,
|
|
3777
|
-
message: "Task status changed",
|
|
3778
|
-
data: eventData,
|
|
3620
|
+
return withGjcTeamTaskMutation(taskStore(dir), async capability => {
|
|
3621
|
+
if (workerId) assertKnownWorker(await readConfig(dir), workerId);
|
|
3622
|
+
return capability.transition(taskId, status, claimToken, workerId, completionEvidenceInput);
|
|
3779
3623
|
});
|
|
3780
|
-
return updated;
|
|
3781
3624
|
}
|
|
3782
3625
|
export async function transitionGjcTeamTask(
|
|
3783
3626
|
teamName: string,
|
|
@@ -3808,59 +3651,9 @@ export async function releaseGjcTeamTaskClaim(
|
|
|
3808
3651
|
env: NodeJS.ProcessEnv = process.env,
|
|
3809
3652
|
): Promise<GjcTeamTask> {
|
|
3810
3653
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
3811
|
-
|
|
3812
|
-
if (!task.claim || task.claim.token !== claimToken || task.claim.owner !== workerId)
|
|
3813
|
-
throw new Error(`claim_token_mismatch:${taskId}`);
|
|
3814
|
-
const updated: GjcTeamTask = {
|
|
3815
|
-
...task,
|
|
3816
|
-
status: "pending",
|
|
3817
|
-
assignee: undefined,
|
|
3818
|
-
claim: undefined,
|
|
3819
|
-
version: task.version + 1,
|
|
3820
|
-
updated_at: now(),
|
|
3821
|
-
};
|
|
3822
|
-
await writeTask(dir, updated);
|
|
3823
|
-
const claimPath = path.join(dir, "claims", `${taskId}.json`);
|
|
3824
|
-
await deleteIfOwned(claimPath, {
|
|
3825
|
-
...stateWriterOptions(claimPath, "prune", "release"),
|
|
3826
|
-
predicate: current => (current as GjcTeamTaskClaim).token === claimToken,
|
|
3827
|
-
});
|
|
3828
|
-
await appendEvent(dir, {
|
|
3829
|
-
type: "task_claim_released",
|
|
3830
|
-
task_id: taskId,
|
|
3831
|
-
worker: workerId,
|
|
3832
|
-
message: "Task claim released",
|
|
3833
|
-
});
|
|
3834
|
-
return updated;
|
|
3654
|
+
return taskStore(dir).release(taskId, claimToken, workerId);
|
|
3835
3655
|
}
|
|
3836
3656
|
|
|
3837
|
-
function emptyNotificationSummary(): GjcTeamNotificationSummary {
|
|
3838
|
-
return {
|
|
3839
|
-
total: 0,
|
|
3840
|
-
replay_eligible: 0,
|
|
3841
|
-
by_state: {
|
|
3842
|
-
pending: 0,
|
|
3843
|
-
sent: 0,
|
|
3844
|
-
queued: 0,
|
|
3845
|
-
deferred: 0,
|
|
3846
|
-
failed: 0,
|
|
3847
|
-
delivered: 0,
|
|
3848
|
-
acknowledged: 0,
|
|
3849
|
-
},
|
|
3850
|
-
};
|
|
3851
|
-
}
|
|
3852
|
-
function isReplayEligibleNotification(state: GjcTeamNotificationDeliveryState): boolean {
|
|
3853
|
-
return state === "pending" || state === "queued" || state === "deferred" || state === "failed";
|
|
3854
|
-
}
|
|
3855
|
-
function summarizeNotifications(notifications: GjcTeamNotification[]): GjcTeamNotificationSummary {
|
|
3856
|
-
const summary = emptyNotificationSummary();
|
|
3857
|
-
for (const notification of notifications) {
|
|
3858
|
-
summary.total += 1;
|
|
3859
|
-
summary.by_state[notification.delivery_state] += 1;
|
|
3860
|
-
if (isReplayEligibleNotification(notification.delivery_state)) summary.replay_eligible += 1;
|
|
3861
|
-
}
|
|
3862
|
-
return summary;
|
|
3863
|
-
}
|
|
3864
3657
|
async function listNotificationRecords(dir: string): Promise<GjcTeamNotification[]> {
|
|
3865
3658
|
const notificationsDir = path.join(dir, "notifications");
|
|
3866
3659
|
try {
|
|
@@ -3914,26 +3707,6 @@ async function writeNotificationRecord(dir: string, notification: GjcTeamNotific
|
|
|
3914
3707
|
await writeJsonFile(notificationPath(dir, merged.id), merged);
|
|
3915
3708
|
return merged;
|
|
3916
3709
|
}
|
|
3917
|
-
async function createMessageNotification(
|
|
3918
|
-
dir: string,
|
|
3919
|
-
teamName: string,
|
|
3920
|
-
message: GjcTeamMailboxMessage,
|
|
3921
|
-
state: GjcTeamNotificationDeliveryState = "pending",
|
|
3922
|
-
): Promise<GjcTeamNotification> {
|
|
3923
|
-
const id = messageNotificationId(teamName, message.to_worker, message.message_id);
|
|
3924
|
-
return writeNotificationRecord(dir, {
|
|
3925
|
-
id,
|
|
3926
|
-
kind: "mailbox_message",
|
|
3927
|
-
team_name: teamName,
|
|
3928
|
-
recipient: message.to_worker,
|
|
3929
|
-
source: { type: "message", id: message.message_id },
|
|
3930
|
-
idempotency_key: message.idempotency_key,
|
|
3931
|
-
delivery_state: state,
|
|
3932
|
-
created_at: message.created_at,
|
|
3933
|
-
updated_at: now(),
|
|
3934
|
-
replay_count: 0,
|
|
3935
|
-
});
|
|
3936
|
-
}
|
|
3937
3710
|
async function readLegacyMailbox(dir: string, worker: string): Promise<{ messages: GjcTeamMailboxMessage[] }> {
|
|
3938
3711
|
return (await readJsonFile<{ messages: GjcTeamMailboxMessage[] }>(mailboxPath(dir, worker))) ?? { messages: [] };
|
|
3939
3712
|
}
|
|
@@ -3942,7 +3715,9 @@ async function readMailbox(dir: string, worker: string): Promise<{ messages: Gjc
|
|
|
3942
3715
|
const byId = new Map<string, GjcTeamMailboxMessage>();
|
|
3943
3716
|
for (const message of (await readLegacyMailbox(dir, worker)).messages ?? []) byId.set(message.message_id, message);
|
|
3944
3717
|
try {
|
|
3945
|
-
const entries = await fs.readdir(mailboxDirPath(dir, worker), {
|
|
3718
|
+
const entries = await fs.readdir(mailboxDirPath(dir, worker), {
|
|
3719
|
+
withFileTypes: true,
|
|
3720
|
+
});
|
|
3946
3721
|
const records = await Promise.all(
|
|
3947
3722
|
entries
|
|
3948
3723
|
.filter(entry => entry.isFile() && entry.name.endsWith(".json"))
|
|
@@ -3952,7 +3727,9 @@ async function readMailbox(dir: string, worker: string): Promise<{ messages: Gjc
|
|
|
3952
3727
|
} catch (error) {
|
|
3953
3728
|
if (!isEnoent(error)) throw error;
|
|
3954
3729
|
}
|
|
3955
|
-
return {
|
|
3730
|
+
return {
|
|
3731
|
+
messages: [...byId.values()].sort((a, b) => a.created_at.localeCompare(b.created_at)),
|
|
3732
|
+
};
|
|
3956
3733
|
}
|
|
3957
3734
|
async function writeLegacyMailboxView(dir: string, worker: string): Promise<void> {
|
|
3958
3735
|
const current = await readMailbox(dir, worker);
|
|
@@ -3989,121 +3766,36 @@ async function writeMailboxMessage(
|
|
|
3989
3766
|
await writeLegacyMailboxView(dir, worker);
|
|
3990
3767
|
return message;
|
|
3991
3768
|
}
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
}
|
|
3769
|
+
const teamNotificationRuntime: TeamNotificationRuntime = {
|
|
3770
|
+
findTeamDir,
|
|
3771
|
+
readConfig,
|
|
3772
|
+
assertKnownParticipant,
|
|
3773
|
+
messageId: messageIdFor,
|
|
3774
|
+
messageNotificationId,
|
|
3775
|
+
now,
|
|
3776
|
+
randomId: randomUUID,
|
|
3777
|
+
appendEvent: async (dir, event) => {
|
|
3778
|
+
await appendEvent(dir, event);
|
|
3779
|
+
},
|
|
3780
|
+
readMailbox,
|
|
3781
|
+
writeMailboxMessage,
|
|
3782
|
+
listNotifications: listNotificationRecords,
|
|
3783
|
+
readNotification: readNotificationRecord,
|
|
3784
|
+
writeNotification: writeNotificationRecord,
|
|
3785
|
+
};
|
|
4010
3786
|
|
|
4011
|
-
async function attemptConfiguredMailboxTransport(
|
|
4012
|
-
dir: string,
|
|
4013
|
-
config: GjcTeamConfig,
|
|
4014
|
-
notification: GjcTeamNotification,
|
|
4015
|
-
cwd: string,
|
|
4016
|
-
env: NodeJS.ProcessEnv,
|
|
4017
|
-
): Promise<GjcTeamNotification | null> {
|
|
4018
|
-
if (!gjcTeamMailboxDeliveryTransport) return null;
|
|
4019
|
-
const message = await messageForNotification(dir, notification);
|
|
4020
|
-
if (!message) return null;
|
|
4021
|
-
try {
|
|
4022
|
-
const result = await gjcTeamMailboxDeliveryTransport.deliverMailboxMessage({
|
|
4023
|
-
team_name: config.team_name,
|
|
4024
|
-
state_dir: dir,
|
|
4025
|
-
config,
|
|
4026
|
-
notification,
|
|
4027
|
-
message,
|
|
4028
|
-
cwd,
|
|
4029
|
-
env,
|
|
4030
|
-
});
|
|
4031
|
-
if (!result) return null;
|
|
4032
|
-
if (result.transport === "sdk" && result.state === "failed") return null;
|
|
4033
|
-
return writeNotificationRecord(dir, {
|
|
4034
|
-
...notification,
|
|
4035
|
-
delivery_state: result.state,
|
|
4036
|
-
pane_attempt_result: result.transport === "pane" ? result.state : undefined,
|
|
4037
|
-
pane_attempt_reason: result.reason ?? result.transport,
|
|
4038
|
-
pane_attempt_at: now(),
|
|
4039
|
-
updated_at: now(),
|
|
4040
|
-
});
|
|
4041
|
-
} catch {
|
|
4042
|
-
return null;
|
|
4043
|
-
}
|
|
4044
|
-
}
|
|
4045
|
-
|
|
4046
|
-
async function attemptPaneNotification(
|
|
4047
|
-
dir: string,
|
|
4048
|
-
config: GjcTeamConfig,
|
|
4049
|
-
notification: GjcTeamNotification,
|
|
4050
|
-
env: NodeJS.ProcessEnv,
|
|
4051
|
-
cwd = process.cwd(),
|
|
4052
|
-
): Promise<GjcTeamNotification> {
|
|
4053
|
-
const transported = await attemptConfiguredMailboxTransport(dir, config, notification, cwd, env);
|
|
4054
|
-
if (transported) return transported;
|
|
4055
|
-
const paneId =
|
|
4056
|
-
notification.recipient === "leader-fixed"
|
|
4057
|
-
? config.leader.pane_id
|
|
4058
|
-
: config.workers.find(worker => worker.id === notification.recipient)?.pane_id;
|
|
4059
|
-
let result: GjcTeamPaneAttemptResult = "deferred";
|
|
4060
|
-
let reason = "pane_missing";
|
|
4061
|
-
if (paneId) {
|
|
4062
|
-
if (config.tmux_session === "dry-run" || env.GJC_TEAM_FAKE_PANE_ATTEMPT === "sent") {
|
|
4063
|
-
result = "sent";
|
|
4064
|
-
reason = "dry_run_or_fake_tmux";
|
|
4065
|
-
} else {
|
|
4066
|
-
result = "queued";
|
|
4067
|
-
reason = "tmux_delivery_recorded_without_injection";
|
|
4068
|
-
}
|
|
4069
|
-
}
|
|
4070
|
-
return writeNotificationRecord(dir, {
|
|
4071
|
-
...notification,
|
|
4072
|
-
delivery_state: result,
|
|
4073
|
-
pane_attempt_result: result,
|
|
4074
|
-
pane_attempt_reason: reason,
|
|
4075
|
-
pane_attempt_at: now(),
|
|
4076
|
-
updated_at: now(),
|
|
4077
|
-
});
|
|
4078
|
-
}
|
|
4079
3787
|
export async function replayGjcTeamNotifications(
|
|
4080
3788
|
teamName: string,
|
|
4081
3789
|
cwd = process.cwd(),
|
|
4082
3790
|
env: NodeJS.ProcessEnv = process.env,
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
if (!isReplayEligibleNotification(notification.delivery_state)) {
|
|
4090
|
-
next.push(notification);
|
|
4091
|
-
continue;
|
|
4092
|
-
}
|
|
4093
|
-
const attempted = await attemptPaneNotification(
|
|
4094
|
-
dir,
|
|
4095
|
-
config,
|
|
4096
|
-
{
|
|
4097
|
-
...notification,
|
|
4098
|
-
replay_count: (notification.replay_count ?? 0) + 1,
|
|
4099
|
-
},
|
|
4100
|
-
env,
|
|
4101
|
-
cwd,
|
|
4102
|
-
);
|
|
4103
|
-
next.push(attempted);
|
|
4104
|
-
}
|
|
4105
|
-
return { notifications: next, summary: summarizeNotifications(next) };
|
|
3791
|
+
transport?: GjcTeamMailboxDeliveryTransport,
|
|
3792
|
+
): Promise<{
|
|
3793
|
+
notifications: GjcTeamNotification[];
|
|
3794
|
+
summary: GjcTeamNotificationSummary;
|
|
3795
|
+
}> {
|
|
3796
|
+
return replayTeamNotifications(teamNotificationRuntime, teamName, cwd, env, transport);
|
|
4106
3797
|
}
|
|
3798
|
+
|
|
4107
3799
|
export async function sendGjcTeamMessage(
|
|
4108
3800
|
teamName: string,
|
|
4109
3801
|
fromWorker: string,
|
|
@@ -4112,36 +3804,21 @@ export async function sendGjcTeamMessage(
|
|
|
4112
3804
|
cwd = process.cwd(),
|
|
4113
3805
|
env: NodeJS.ProcessEnv = process.env,
|
|
4114
3806
|
idempotencyKey?: string,
|
|
3807
|
+
transport?: GjcTeamMailboxDeliveryTransport,
|
|
4115
3808
|
): Promise<GjcTeamMailboxMessage> {
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
const message: GjcTeamMailboxMessage = {
|
|
4122
|
-
message_id: messageIdFor({ teamName: config.team_name, fromWorker, toWorker, body, idempotencyKey, createdKey }),
|
|
4123
|
-
from_worker: fromWorker,
|
|
4124
|
-
to_worker: toWorker,
|
|
3809
|
+
return sendTeamMessage(
|
|
3810
|
+
teamNotificationRuntime,
|
|
3811
|
+
teamName,
|
|
3812
|
+
fromWorker,
|
|
3813
|
+
toWorker,
|
|
4125
3814
|
body,
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
const existingNotification = await readJsonFile<GjcTeamNotification>(
|
|
4131
|
-
notificationPath(dir, messageNotificationId(config.team_name, toWorker, written.message_id)),
|
|
3815
|
+
cwd,
|
|
3816
|
+
env,
|
|
3817
|
+
idempotencyKey,
|
|
3818
|
+
transport,
|
|
4132
3819
|
);
|
|
4133
|
-
const notification = await createMessageNotification(dir, config.team_name, written);
|
|
4134
|
-
if (!existingNotification) {
|
|
4135
|
-
await attemptPaneNotification(dir, config, notification, env, cwd);
|
|
4136
|
-
}
|
|
4137
|
-
await appendEvent(dir, {
|
|
4138
|
-
type: "message_sent",
|
|
4139
|
-
worker: fromWorker,
|
|
4140
|
-
message: body,
|
|
4141
|
-
data: { to_worker: toWorker, message_id: written.message_id },
|
|
4142
|
-
});
|
|
4143
|
-
return written;
|
|
4144
3820
|
}
|
|
3821
|
+
|
|
4145
3822
|
export async function broadcastGjcTeamMessage(
|
|
4146
3823
|
teamName: string,
|
|
4147
3824
|
fromWorker: string,
|
|
@@ -4165,17 +3842,16 @@ export async function broadcastGjcTeamMessage(
|
|
|
4165
3842
|
),
|
|
4166
3843
|
);
|
|
4167
3844
|
}
|
|
3845
|
+
|
|
4168
3846
|
export async function listGjcTeamMailbox(
|
|
4169
3847
|
teamName: string,
|
|
4170
3848
|
worker: string,
|
|
4171
3849
|
cwd = process.cwd(),
|
|
4172
3850
|
env: NodeJS.ProcessEnv = process.env,
|
|
4173
3851
|
): Promise<GjcTeamMailboxMessage[]> {
|
|
4174
|
-
|
|
4175
|
-
const config = await readConfig(dir);
|
|
4176
|
-
assertKnownParticipant(config, worker);
|
|
4177
|
-
return (await readMailbox(dir, worker)).messages;
|
|
3852
|
+
return listTeamMailbox(teamNotificationRuntime, teamName, worker, cwd, env);
|
|
4178
3853
|
}
|
|
3854
|
+
|
|
4179
3855
|
export async function markGjcTeamMailboxMessage(
|
|
4180
3856
|
teamName: string,
|
|
4181
3857
|
worker: string,
|
|
@@ -4185,40 +3861,14 @@ export async function markGjcTeamMailboxMessage(
|
|
|
4185
3861
|
env: NodeJS.ProcessEnv = process.env,
|
|
4186
3862
|
): Promise<GjcTeamMailboxMessage> {
|
|
4187
3863
|
assertSafeId("message_id", messageId);
|
|
4188
|
-
|
|
4189
|
-
const config = await readConfig(dir);
|
|
4190
|
-
assertKnownParticipant(config, worker);
|
|
4191
|
-
const mailbox = await readMailbox(dir, worker);
|
|
4192
|
-
const message = mailbox.messages.find(candidate => candidate.message_id === messageId);
|
|
4193
|
-
if (!message) throw new Error(`message_not_found:${messageId}`);
|
|
4194
|
-
const updated = { ...message, [field]: message[field] ?? now() };
|
|
4195
|
-
const written = await writeMailboxMessage(dir, worker, updated);
|
|
4196
|
-
const notificationId = messageNotificationId(config.team_name, worker, messageId);
|
|
4197
|
-
const existing =
|
|
4198
|
-
(await readJsonFile<GjcTeamNotification>(notificationPath(dir, notificationId))) ??
|
|
4199
|
-
(await createMessageNotification(dir, config.team_name, written));
|
|
4200
|
-
const nextState: GjcTeamNotificationDeliveryState = field === "delivered_at" ? "acknowledged" : "delivered";
|
|
4201
|
-
const before = existing.delivery_state;
|
|
4202
|
-
await writeNotificationRecord(dir, { ...existing, delivery_state: nextState, updated_at: now() });
|
|
4203
|
-
if (mergeNotificationState(before, nextState) !== before)
|
|
4204
|
-
await appendEvent(dir, {
|
|
4205
|
-
type: `message_${field === "delivered_at" ? "acknowledged" : "notified"}`,
|
|
4206
|
-
worker,
|
|
4207
|
-
message: messageId,
|
|
4208
|
-
});
|
|
4209
|
-
return written;
|
|
3864
|
+
return markTeamMailboxMessage(teamNotificationRuntime, teamName, worker, messageId, field, cwd, env);
|
|
4210
3865
|
}
|
|
4211
|
-
export
|
|
3866
|
+
export const readGjcWorkerStatus = (
|
|
4212
3867
|
teamName: string,
|
|
4213
3868
|
worker: string,
|
|
4214
3869
|
cwd = process.cwd(),
|
|
4215
3870
|
env: NodeJS.ProcessEnv = process.env,
|
|
4216
|
-
): Promise<WorkerStatusFile>
|
|
4217
|
-
const dir = await findTeamDir(teamName, cwd, env);
|
|
4218
|
-
const config = await readConfig(dir);
|
|
4219
|
-
assertKnownWorker(config, worker);
|
|
4220
|
-
return readWorkerStatusFile(dir, worker);
|
|
4221
|
-
}
|
|
3871
|
+
): Promise<WorkerStatusFile> => readWorkerStatus(workerRuntime, teamName, worker, cwd, env);
|
|
4222
3872
|
export async function updateGjcWorkerStatus(
|
|
4223
3873
|
teamName: string,
|
|
4224
3874
|
worker: string,
|
|
@@ -4229,43 +3879,16 @@ export async function updateGjcWorkerStatus(
|
|
|
4229
3879
|
reason?: string,
|
|
4230
3880
|
): Promise<WorkerStatusFile> {
|
|
4231
3881
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
const trimmedReason = reason?.trim();
|
|
4236
|
-
const value: WorkerStatusFile = {
|
|
4237
|
-
state: status,
|
|
4238
|
-
...(currentTaskId ? { current_task_id: currentTaskId } : {}),
|
|
4239
|
-
...(trimmedReason ? { reason: trimmedReason } : {}),
|
|
4240
|
-
updated_at: now(),
|
|
4241
|
-
};
|
|
4242
|
-
await writeJsonFile(path.join(workerDir(dir, worker), "status.json"), value);
|
|
4243
|
-
const currentLifecycle = await readWorkerLifecycleRecord(dir, teamWorker);
|
|
4244
|
-
const lifecycleState =
|
|
4245
|
-
currentLifecycle.lifecycle_state === "stopped" ? "stopped" : lifecycleStateForWorkerStatus(status);
|
|
4246
|
-
await writeWorkerLifecycleRecord(dir, teamWorker, lifecycleState);
|
|
4247
|
-
await appendEvent(dir, {
|
|
4248
|
-
type: "worker_status_updated",
|
|
4249
|
-
worker,
|
|
4250
|
-
message: `Worker ${worker} reported ${status}`,
|
|
4251
|
-
data: {
|
|
4252
|
-
status,
|
|
4253
|
-
current_task_id: currentTaskId,
|
|
4254
|
-
},
|
|
4255
|
-
});
|
|
4256
|
-
return value;
|
|
3882
|
+
return withGjcTeamMutationFence(dir, () =>
|
|
3883
|
+
updateWorkerStatus(workerRuntime, teamName, worker, status, cwd, env, currentTaskId, reason),
|
|
3884
|
+
);
|
|
4257
3885
|
}
|
|
4258
|
-
export
|
|
3886
|
+
export const readGjcWorkerHeartbeat = (
|
|
4259
3887
|
teamName: string,
|
|
4260
3888
|
worker: string,
|
|
4261
3889
|
cwd = process.cwd(),
|
|
4262
3890
|
env: NodeJS.ProcessEnv = process.env,
|
|
4263
|
-
): Promise<WorkerHeartbeatFile | null>
|
|
4264
|
-
const dir = await findTeamDir(teamName, cwd, env);
|
|
4265
|
-
const config = await readConfig(dir);
|
|
4266
|
-
assertKnownWorker(config, worker);
|
|
4267
|
-
return readJsonFile<WorkerHeartbeatFile>(path.join(workerDir(dir, worker), "heartbeat.json"));
|
|
4268
|
-
}
|
|
3891
|
+
): Promise<WorkerHeartbeatFile | null> => readWorkerHeartbeat(workerRuntime, teamName, worker, cwd, env);
|
|
4269
3892
|
export async function updateGjcWorkerHeartbeat(
|
|
4270
3893
|
teamName: string,
|
|
4271
3894
|
worker: string,
|
|
@@ -4274,11 +3897,9 @@ export async function updateGjcWorkerHeartbeat(
|
|
|
4274
3897
|
env: NodeJS.ProcessEnv = process.env,
|
|
4275
3898
|
): Promise<WorkerHeartbeatFile> {
|
|
4276
3899
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
await writeJsonFile(path.join(workerDir(dir, worker), "heartbeat.json"), value);
|
|
4281
|
-
return value;
|
|
3900
|
+
return withGjcTeamMutationFence(dir, () =>
|
|
3901
|
+
updateWorkerHeartbeat(workerRuntime, teamName, worker, heartbeat, cwd, env),
|
|
3902
|
+
);
|
|
4282
3903
|
}
|
|
4283
3904
|
export async function writeGjcWorkerInbox(
|
|
4284
3905
|
teamName: string,
|
|
@@ -4408,35 +4029,16 @@ export async function writeGjcShutdownRequest(
|
|
|
4408
4029
|
requestedAt = now(),
|
|
4409
4030
|
): Promise<Record<string, unknown>> {
|
|
4410
4031
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
const value = { worker, requested_by: requestedBy, request_id: requestId, mode, requested_at: requestedAt };
|
|
4415
|
-
await writeJsonFile(path.join(workerDir(dir, worker), "shutdown-request.json"), value);
|
|
4416
|
-
await writeWorkerLifecycleRecord(dir, teamWorker, "draining", {
|
|
4417
|
-
shutdown_request_id: requestId,
|
|
4418
|
-
shutdown_requested_at: requestedAt,
|
|
4419
|
-
shutdown_mode: mode,
|
|
4420
|
-
});
|
|
4421
|
-
await appendEvent(dir, {
|
|
4422
|
-
type: "worker_shutdown_requested",
|
|
4423
|
-
worker,
|
|
4424
|
-
message: `Worker ${worker} shutdown requested`,
|
|
4425
|
-
data: { requested_by: requestedBy, request_id: requestId, mode },
|
|
4426
|
-
});
|
|
4427
|
-
return value;
|
|
4032
|
+
return withGjcTeamMutationFence(dir, () =>
|
|
4033
|
+
writeShutdownRequest(workerRuntime, teamName, worker, requestedBy, cwd, env, requestId, mode, requestedAt),
|
|
4034
|
+
);
|
|
4428
4035
|
}
|
|
4429
|
-
export
|
|
4036
|
+
export const readGjcShutdownAck = (
|
|
4430
4037
|
teamName: string,
|
|
4431
4038
|
worker: string,
|
|
4432
4039
|
cwd = process.cwd(),
|
|
4433
4040
|
env: NodeJS.ProcessEnv = process.env,
|
|
4434
|
-
): Promise<Record<string, unknown> | null>
|
|
4435
|
-
const dir = await findTeamDir(teamName, cwd, env);
|
|
4436
|
-
const config = await readConfig(dir);
|
|
4437
|
-
assertKnownWorker(config, worker);
|
|
4438
|
-
return readJsonFile<Record<string, unknown>>(path.join(workerDir(dir, worker), "shutdown-ack.json"));
|
|
4439
|
-
}
|
|
4041
|
+
): Promise<Record<string, unknown> | null> => readShutdownAck(workerRuntime, teamName, worker, cwd, env);
|
|
4440
4042
|
|
|
4441
4043
|
export async function executeGjcTeamApiOperation(
|
|
4442
4044
|
operation: string,
|
|
@@ -4453,7 +4055,9 @@ export async function executeGjcTeamApiOperation(
|
|
|
4453
4055
|
case "list-tasks":
|
|
4454
4056
|
return { tasks: await listGjcTeamTasks(teamName, cwd, env) };
|
|
4455
4057
|
case "read-task":
|
|
4456
|
-
return {
|
|
4058
|
+
return {
|
|
4059
|
+
task: await readGjcTeamTask(teamName, String(input.task_id ?? input.taskId), cwd, env),
|
|
4060
|
+
};
|
|
4457
4061
|
case "create-task": {
|
|
4458
4062
|
const task = await createGjcTeamTask(
|
|
4459
4063
|
teamName,
|
|
@@ -4524,7 +4128,11 @@ export async function executeGjcTeamApiOperation(
|
|
|
4524
4128
|
cwd,
|
|
4525
4129
|
env,
|
|
4526
4130
|
);
|
|
4527
|
-
return {
|
|
4131
|
+
return {
|
|
4132
|
+
ok: true,
|
|
4133
|
+
...taskReceiptFields(teamName, task),
|
|
4134
|
+
worker_id: worker,
|
|
4135
|
+
};
|
|
4528
4136
|
}
|
|
4529
4137
|
case "send-message": {
|
|
4530
4138
|
const message = await sendGjcTeamMessage(
|
|
@@ -4586,9 +4194,12 @@ export async function executeGjcTeamApiOperation(
|
|
|
4586
4194
|
case "notification-list": {
|
|
4587
4195
|
const dir = await findTeamDir(teamName, cwd, env);
|
|
4588
4196
|
const config = await readConfig(dir);
|
|
4589
|
-
await reconcileTeamNotifications(dir, config);
|
|
4197
|
+
await reconcileTeamNotifications(teamNotificationRuntime, dir, config);
|
|
4590
4198
|
const notifications = await listNotificationRecords(dir);
|
|
4591
|
-
const result = {
|
|
4199
|
+
const result = {
|
|
4200
|
+
notifications,
|
|
4201
|
+
summary: summarizeTeamNotifications(notifications),
|
|
4202
|
+
};
|
|
4592
4203
|
return notificationSummaryReceipt(teamName, result);
|
|
4593
4204
|
}
|
|
4594
4205
|
case "notification-read":
|
|
@@ -4729,5 +4340,11 @@ export function parseTeamLaunchArgs(argv: string[]): GjcTeamStartOptions {
|
|
|
4729
4340
|
if (!task) throw new Error("missing_team_task");
|
|
4730
4341
|
if (!Number.isInteger(workerCount) || workerCount < 1 || workerCount > GJC_TEAM_MAX_WORKERS)
|
|
4731
4342
|
throw new Error(`invalid_team_worker_count:${workerCount}:expected_1_${GJC_TEAM_MAX_WORKERS}`);
|
|
4732
|
-
return {
|
|
4343
|
+
return {
|
|
4344
|
+
workerCount,
|
|
4345
|
+
agentType,
|
|
4346
|
+
task,
|
|
4347
|
+
dryRun,
|
|
4348
|
+
worktreeMode: resolveDefaultWorktreeMode(parsedWorktree.mode),
|
|
4349
|
+
};
|
|
4733
4350
|
}
|