@gajae-code/coding-agent 0.11.0 → 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 +63 -1
- package/dist/types/async/job-manager.d.ts +46 -7
- 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-receipt-freshness.d.ts +11 -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/command-controller.d.ts +2 -2
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -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 +21 -19
- 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 +21 -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 +22 -15
- 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/ensure.d.ts +9 -0
- 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/discord-daemon.d.ts +5 -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 +69 -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/tools/subagent-render.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +6 -1
- 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 +39 -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 +455 -69
- 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 +83 -12
- package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +156 -1319
- 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/command-controller.ts +9 -15
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +72 -25
- 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 +302 -1151
- 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 +78 -19
- 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 +47 -20
- 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 +21 -2
- package/src/sdk/broker/ensure.ts +85 -25
- 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 +14 -4
- 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/discord-daemon.ts +25 -8
- package/src/sdk/bus/html-format.ts +20 -3
- package/src/sdk/bus/index.ts +834 -113
- package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
- package/src/sdk/bus/native-runtime-compatibility.ts +1 -1
- 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 +1491 -247
- 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 +1901 -637
- 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 +62 -37
- 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/tools/subagent-render.ts +19 -8
- package/src/tools/subagent.ts +84 -32
- 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
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
/** File-backed team task store, claims, leases, and completion evidence. */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { sessionIdFromDirName } from "./session-layout";
|
|
6
|
+
import {
|
|
7
|
+
createJsonNoClobber,
|
|
8
|
+
deleteIfOwned,
|
|
9
|
+
removeFileAudited,
|
|
10
|
+
withWorkflowStateLock,
|
|
11
|
+
writeJsonAtomic,
|
|
12
|
+
} from "./state-writer";
|
|
13
|
+
import type { GjcTeamConfig, GjcTeamMailboxMessage } from "./team-runtime";
|
|
14
|
+
|
|
15
|
+
export type GjcTeamNotificationDeliveryState =
|
|
16
|
+
| "pending"
|
|
17
|
+
| "sent"
|
|
18
|
+
| "queued"
|
|
19
|
+
| "deferred"
|
|
20
|
+
| "failed"
|
|
21
|
+
| "delivered"
|
|
22
|
+
| "acknowledged";
|
|
23
|
+
export type GjcTeamPaneAttemptResult = "sent" | "queued" | "deferred" | "failed";
|
|
24
|
+
export type GjcTeamMailboxDeliveryTransportKind = "sdk" | "pane";
|
|
25
|
+
export interface GjcTeamNotification {
|
|
26
|
+
id: string;
|
|
27
|
+
kind: "mailbox_message" | "worker_lifecycle" | "invalid_attempt";
|
|
28
|
+
team_name: string;
|
|
29
|
+
recipient: string;
|
|
30
|
+
source: { type: "message" | "task" | "worker" | "event"; id: string };
|
|
31
|
+
idempotency_key?: string;
|
|
32
|
+
delivery_state: GjcTeamNotificationDeliveryState;
|
|
33
|
+
pane_attempt_result?: GjcTeamPaneAttemptResult;
|
|
34
|
+
pane_attempt_reason?: string;
|
|
35
|
+
pane_attempt_at?: string;
|
|
36
|
+
created_at: string;
|
|
37
|
+
updated_at: string;
|
|
38
|
+
replay_count: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface GjcTeamNotificationSummary {
|
|
42
|
+
total: number;
|
|
43
|
+
replay_eligible: number;
|
|
44
|
+
by_state: Record<GjcTeamNotificationDeliveryState, number>;
|
|
45
|
+
}
|
|
46
|
+
export interface GjcTeamMailboxDeliveryInput {
|
|
47
|
+
team_name: string;
|
|
48
|
+
state_dir: string;
|
|
49
|
+
config: GjcTeamConfig;
|
|
50
|
+
notification: GjcTeamNotification;
|
|
51
|
+
message: GjcTeamMailboxMessage;
|
|
52
|
+
cwd: string;
|
|
53
|
+
env: NodeJS.ProcessEnv;
|
|
54
|
+
}
|
|
55
|
+
export type GjcTeamMailboxDeliveryResult =
|
|
56
|
+
| {
|
|
57
|
+
transport: "sdk";
|
|
58
|
+
state: GjcTeamNotificationDeliveryState;
|
|
59
|
+
reason?: string;
|
|
60
|
+
}
|
|
61
|
+
| { transport: "pane"; state: GjcTeamPaneAttemptResult; reason?: string };
|
|
62
|
+
export interface GjcTeamMailboxDeliveryTransport {
|
|
63
|
+
deliverMailboxMessage(input: GjcTeamMailboxDeliveryInput): Promise<GjcTeamMailboxDeliveryResult | null>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type GjcTeamTaskStatus = "pending" | "blocked" | "in_progress" | "completed" | "failed";
|
|
67
|
+
export interface GjcTeamTaskClaim {
|
|
68
|
+
owner: string;
|
|
69
|
+
token: string;
|
|
70
|
+
leased_until: string;
|
|
71
|
+
}
|
|
72
|
+
export type GjcTeamTaskCompletionEvidenceKind = "command" | "inspection" | "artifact";
|
|
73
|
+
export type GjcTeamTaskCompletionEvidenceStatus = "passed" | "failed" | "not_run" | "verified" | "rejected";
|
|
74
|
+
export interface GjcTeamTaskCompletionEvidenceItem {
|
|
75
|
+
kind: GjcTeamTaskCompletionEvidenceKind;
|
|
76
|
+
status: GjcTeamTaskCompletionEvidenceStatus;
|
|
77
|
+
summary: string;
|
|
78
|
+
command?: string;
|
|
79
|
+
artifact?: string;
|
|
80
|
+
location?: string;
|
|
81
|
+
output?: string;
|
|
82
|
+
}
|
|
83
|
+
export interface GjcTeamTaskCompletionEvidence {
|
|
84
|
+
summary: string;
|
|
85
|
+
items: GjcTeamTaskCompletionEvidenceItem[];
|
|
86
|
+
files?: string[];
|
|
87
|
+
notes?: string;
|
|
88
|
+
recorded_by: string;
|
|
89
|
+
recorded_at: string;
|
|
90
|
+
}
|
|
91
|
+
export interface GjcTeamTask {
|
|
92
|
+
id: string;
|
|
93
|
+
subject: string;
|
|
94
|
+
description: string;
|
|
95
|
+
title: string;
|
|
96
|
+
objective: string;
|
|
97
|
+
status: GjcTeamTaskStatus;
|
|
98
|
+
assignee?: string;
|
|
99
|
+
owner?: string;
|
|
100
|
+
result?: string;
|
|
101
|
+
completion_evidence?: GjcTeamTaskCompletionEvidence;
|
|
102
|
+
error?: string;
|
|
103
|
+
blocked_by?: string[];
|
|
104
|
+
depends_on?: string[];
|
|
105
|
+
lane?: string;
|
|
106
|
+
required_role?: string;
|
|
107
|
+
allowed_roles?: string[];
|
|
108
|
+
version: number;
|
|
109
|
+
claim?: GjcTeamTaskClaim;
|
|
110
|
+
created_at: string;
|
|
111
|
+
updated_at: string;
|
|
112
|
+
completed_at?: string;
|
|
113
|
+
}
|
|
114
|
+
export type GjcTeamTaskMetadataInput = Partial<
|
|
115
|
+
Pick<GjcTeamTask, "owner" | "lane" | "required_role" | "allowed_roles" | "depends_on" | "blocked_by">
|
|
116
|
+
>;
|
|
117
|
+
export interface GjcTeamTaskWorker {
|
|
118
|
+
id: string;
|
|
119
|
+
role: string;
|
|
120
|
+
agent_type: string;
|
|
121
|
+
}
|
|
122
|
+
export interface GjcTeamApiClaimResult {
|
|
123
|
+
ok: boolean;
|
|
124
|
+
task?: GjcTeamTask;
|
|
125
|
+
worker_id?: string;
|
|
126
|
+
claim_token?: string;
|
|
127
|
+
reason?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type EventAppender = (event: {
|
|
131
|
+
type: string;
|
|
132
|
+
task_id?: string;
|
|
133
|
+
worker?: string;
|
|
134
|
+
message: string;
|
|
135
|
+
data?: Record<string, unknown>;
|
|
136
|
+
}) => Promise<void>;
|
|
137
|
+
const now = () => new Date().toISOString();
|
|
138
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
|
|
139
|
+
const isEnoent = (error: unknown): error is { code: string } => isRecord(error) && error.code === "ENOENT";
|
|
140
|
+
const canonicalTaskKeys = new Set([
|
|
141
|
+
"id",
|
|
142
|
+
"subject",
|
|
143
|
+
"description",
|
|
144
|
+
"title",
|
|
145
|
+
"objective",
|
|
146
|
+
"status",
|
|
147
|
+
"assignee",
|
|
148
|
+
"owner",
|
|
149
|
+
"result",
|
|
150
|
+
"completion_evidence",
|
|
151
|
+
"error",
|
|
152
|
+
"blocked_by",
|
|
153
|
+
"depends_on",
|
|
154
|
+
"lane",
|
|
155
|
+
"required_role",
|
|
156
|
+
"allowed_roles",
|
|
157
|
+
"version",
|
|
158
|
+
"claim",
|
|
159
|
+
"created_at",
|
|
160
|
+
"updated_at",
|
|
161
|
+
"completed_at",
|
|
162
|
+
]);
|
|
163
|
+
const canonicalClaimKeys = new Set(["owner", "token", "leased_until"]);
|
|
164
|
+
const canonicalEvidenceKeys = new Set(["summary", "items", "files", "notes", "recorded_by", "recorded_at"]);
|
|
165
|
+
const canonicalEvidenceItemKeys = new Set(["kind", "status", "summary", "command", "artifact", "location", "output"]);
|
|
166
|
+
const hasExactKeys = (value: Record<string, unknown>, keys: Set<string>) =>
|
|
167
|
+
Object.keys(value).every(key => keys.has(key));
|
|
168
|
+
const isNonEmptyString = (value: unknown): value is string => typeof value === "string" && value.trim().length > 0;
|
|
169
|
+
const isSafePersistedId = (value: unknown): value is string =>
|
|
170
|
+
isNonEmptyString(value) && /^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/.test(value) && !value.includes("..");
|
|
171
|
+
const isFiniteTimestamp = (value: unknown): value is string =>
|
|
172
|
+
typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
173
|
+
|
|
174
|
+
/** The sole strict schema gate for persisted task and claim authority records. */
|
|
175
|
+
export function isCanonicalPersistedGjcTeamTaskClaim(value: unknown): value is GjcTeamTaskClaim {
|
|
176
|
+
if (!isRecord(value) || Array.isArray(value) || !hasExactKeys(value, canonicalClaimKeys)) return false;
|
|
177
|
+
return isSafePersistedId(value.owner) && isNonEmptyString(value.token) && isFiniteTimestamp(value.leased_until);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isCanonicalCompletionEvidence(value: unknown): value is GjcTeamTaskCompletionEvidence {
|
|
181
|
+
if (!isRecord(value) || Array.isArray(value) || !hasExactKeys(value, canonicalEvidenceKeys)) return false;
|
|
182
|
+
if (
|
|
183
|
+
!isNonEmptyString(value.summary) ||
|
|
184
|
+
!isSafePersistedId(value.recorded_by) ||
|
|
185
|
+
!isFiniteTimestamp(value.recorded_at)
|
|
186
|
+
)
|
|
187
|
+
return false;
|
|
188
|
+
if (!Array.isArray(value.items) || value.items.length === 0) return false;
|
|
189
|
+
if (
|
|
190
|
+
!value.items.every(item => {
|
|
191
|
+
if (!isRecord(item) || Array.isArray(item) || !hasExactKeys(item, canonicalEvidenceItemKeys)) return false;
|
|
192
|
+
if (!isNonEmptyString(item.summary) || !["command", "inspection", "artifact"].includes(String(item.kind)))
|
|
193
|
+
return false;
|
|
194
|
+
if (
|
|
195
|
+
(item.kind === "command" && !["passed", "failed", "not_run"].includes(String(item.status))) ||
|
|
196
|
+
(item.kind !== "command" && !["verified", "rejected"].includes(String(item.status)))
|
|
197
|
+
)
|
|
198
|
+
return false;
|
|
199
|
+
if (item.command !== undefined && !isNonEmptyString(item.command)) return false;
|
|
200
|
+
if (item.artifact !== undefined && !isNonEmptyString(item.artifact)) return false;
|
|
201
|
+
if (item.location !== undefined && !isNonEmptyString(item.location)) return false;
|
|
202
|
+
if (item.output !== undefined && typeof item.output !== "string") return false;
|
|
203
|
+
return item.kind !== "command" || isNonEmptyString(item.command);
|
|
204
|
+
})
|
|
205
|
+
)
|
|
206
|
+
return false;
|
|
207
|
+
if (
|
|
208
|
+
value.files !== undefined &&
|
|
209
|
+
(!Array.isArray(value.files) || !value.files.every(file => typeof file === "string" && file.trim().length > 0))
|
|
210
|
+
)
|
|
211
|
+
return false;
|
|
212
|
+
return value.notes === undefined || typeof value.notes === "string";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function isCanonicalPersistedGjcTeamTask(value: unknown, fileId?: string): value is GjcTeamTask {
|
|
216
|
+
if (!isRecord(value) || Array.isArray(value) || !hasExactKeys(value, canonicalTaskKeys)) return false;
|
|
217
|
+
if (!isSafePersistedId(value.id) || (fileId !== undefined && value.id !== fileId)) return false;
|
|
218
|
+
if (!["pending", "blocked", "in_progress", "completed", "failed"].includes(String(value.status))) return false;
|
|
219
|
+
if (!["subject", "description", "title", "objective"].every(key => typeof value[key] === "string")) return false;
|
|
220
|
+
if (!Number.isInteger(value.version) || (value.version as number) <= 0) return false;
|
|
221
|
+
if (!isFiniteTimestamp(value.created_at) || !isFiniteTimestamp(value.updated_at)) return false;
|
|
222
|
+
if (value.completed_at !== undefined && !isFiniteTimestamp(value.completed_at)) return false;
|
|
223
|
+
if (value.owner !== undefined && !isSafePersistedId(value.owner)) return false;
|
|
224
|
+
if (value.assignee !== undefined && !isSafePersistedId(value.assignee)) return false;
|
|
225
|
+
if (value.result !== undefined && typeof value.result !== "string") return false;
|
|
226
|
+
if (value.error !== undefined && typeof value.error !== "string") return false;
|
|
227
|
+
if (value.lane !== undefined && !isNonEmptyString(value.lane)) return false;
|
|
228
|
+
if (value.required_role !== undefined && !isNonEmptyString(value.required_role)) return false;
|
|
229
|
+
if (
|
|
230
|
+
value.allowed_roles !== undefined &&
|
|
231
|
+
(!Array.isArray(value.allowed_roles) || !value.allowed_roles.every(isNonEmptyString))
|
|
232
|
+
)
|
|
233
|
+
return false;
|
|
234
|
+
if (
|
|
235
|
+
value.depends_on !== undefined &&
|
|
236
|
+
(!Array.isArray(value.depends_on) || !value.depends_on.every(isSafePersistedId))
|
|
237
|
+
)
|
|
238
|
+
return false;
|
|
239
|
+
if (
|
|
240
|
+
value.blocked_by !== undefined &&
|
|
241
|
+
(!Array.isArray(value.blocked_by) || !value.blocked_by.every(isSafePersistedId))
|
|
242
|
+
)
|
|
243
|
+
return false;
|
|
244
|
+
if (value.completion_evidence !== undefined && !isCanonicalCompletionEvidence(value.completion_evidence))
|
|
245
|
+
return false;
|
|
246
|
+
if (value.claim !== undefined) {
|
|
247
|
+
if (!isCanonicalPersistedGjcTeamTaskClaim(value.claim)) return false;
|
|
248
|
+
if (value.status !== "in_progress" || value.owner !== value.claim.owner || value.assignee !== value.claim.owner)
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
const safeId = (kind: string, value: string) => {
|
|
254
|
+
if (
|
|
255
|
+
!/^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/.test(value) ||
|
|
256
|
+
value.includes("..") ||
|
|
257
|
+
value.includes("/") ||
|
|
258
|
+
value.includes("\\")
|
|
259
|
+
)
|
|
260
|
+
throw new Error(`invalid_${kind}:${value}`);
|
|
261
|
+
return value;
|
|
262
|
+
};
|
|
263
|
+
const taskPath = (dir: string, id: string) => path.join(dir, "tasks", `${safeId("task_id", id)}.json`);
|
|
264
|
+
const claimPath = (dir: string, id: string) => path.join(dir, "claims", `${safeId("task_id", id)}.json`);
|
|
265
|
+
const writerOptions = (filePath: string, category: "state" | "prune", verb: string) => {
|
|
266
|
+
const resolved = path.resolve(filePath);
|
|
267
|
+
const marker = `${path.sep}.gjc${path.sep}`;
|
|
268
|
+
const markerIndex = resolved.indexOf(marker);
|
|
269
|
+
const cwd = markerIndex >= 0 ? resolved.slice(0, markerIndex) : process.cwd();
|
|
270
|
+
const sessionId =
|
|
271
|
+
resolved
|
|
272
|
+
.split(path.sep)
|
|
273
|
+
.map(segment => sessionIdFromDirName(segment))
|
|
274
|
+
.find((value): value is string => Boolean(value)) ?? process.env.GJC_SESSION_ID?.trim();
|
|
275
|
+
return sessionId
|
|
276
|
+
? {
|
|
277
|
+
cwd,
|
|
278
|
+
audit: { category, verb, owner: "gjc-runtime" as const, sessionId },
|
|
279
|
+
}
|
|
280
|
+
: { cwd };
|
|
281
|
+
};
|
|
282
|
+
async function readJson<T>(filePath: string): Promise<T | null> {
|
|
283
|
+
try {
|
|
284
|
+
return (await Bun.file(filePath).json()) as T;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (isEnoent(error)) return null;
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function writeJson(filePath: string, value: unknown): Promise<void> {
|
|
291
|
+
await writeJsonAtomic(filePath, value, writerOptions(filePath, "state", "write"));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Serializes mutable team facts across processes. Callers must not re-enter this
|
|
296
|
+
* fence; use unlocked helpers when composing operations under one transaction.
|
|
297
|
+
*/
|
|
298
|
+
export async function withGjcTeamMutationFence<T>(dir: string, fn: () => Promise<T>): Promise<T> {
|
|
299
|
+
return withWorkflowStateLock(
|
|
300
|
+
path.join(dir, "operations", "team-mutation.json"),
|
|
301
|
+
fn,
|
|
302
|
+
writerOptions(path.join(dir, "operations", "team-mutation.json"), "state", "mutation-fence"),
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
function optionalString(value: unknown): string | undefined {
|
|
306
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
307
|
+
}
|
|
308
|
+
function optionalStringArray(value: unknown): string[] | undefined {
|
|
309
|
+
if (!Array.isArray(value)) return undefined;
|
|
310
|
+
const values = [...new Set(value.map(optionalString).filter((value): value is string => Boolean(value)))].sort();
|
|
311
|
+
return values.length ? values : undefined;
|
|
312
|
+
}
|
|
313
|
+
export function taskMetadataFromInput(input: Record<string, unknown>, includeOwner = false): GjcTeamTaskMetadataInput {
|
|
314
|
+
const result: GjcTeamTaskMetadataInput = {};
|
|
315
|
+
const owner = optionalString(input.owner);
|
|
316
|
+
if (includeOwner && owner) result.owner = owner;
|
|
317
|
+
for (const [key, value] of [
|
|
318
|
+
["lane", optionalString(input.lane)],
|
|
319
|
+
["required_role", optionalString(input.required_role ?? input.requiredRole)],
|
|
320
|
+
["allowed_roles", optionalStringArray(input.allowed_roles ?? input.allowedRoles)],
|
|
321
|
+
["depends_on", optionalStringArray(input.depends_on ?? input.dependsOn)],
|
|
322
|
+
["blocked_by", optionalStringArray(input.blocked_by ?? input.blockedBy)],
|
|
323
|
+
] as const)
|
|
324
|
+
if (value) Object.assign(result, { [key]: value });
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
export function normalizeGjcTeamTask(raw: GjcTeamTask): GjcTeamTask {
|
|
328
|
+
return {
|
|
329
|
+
...raw,
|
|
330
|
+
status: raw.status === ("complete" as GjcTeamTaskStatus) ? "completed" : raw.status,
|
|
331
|
+
subject: raw.subject ?? raw.title,
|
|
332
|
+
description: raw.description ?? raw.objective,
|
|
333
|
+
title: raw.title ?? raw.subject,
|
|
334
|
+
objective: raw.objective ?? raw.description,
|
|
335
|
+
version: raw.version ?? 1,
|
|
336
|
+
lane: optionalString(raw.lane),
|
|
337
|
+
required_role: optionalString(raw.required_role),
|
|
338
|
+
allowed_roles: optionalStringArray(raw.allowed_roles),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function evidenceError(id: string, field: string) {
|
|
342
|
+
return new Error(`invalid_completion_evidence:${id}:${field}`);
|
|
343
|
+
}
|
|
344
|
+
function required(id: string, field: string, value: unknown, max = 4000): string {
|
|
345
|
+
if (typeof value !== "string" || !value.trim() || value.trim().length > max) throw evidenceError(id, field);
|
|
346
|
+
return value.trim();
|
|
347
|
+
}
|
|
348
|
+
function optional(id: string, field: string, value: unknown, max = 8000): string | undefined {
|
|
349
|
+
if (value == null) return undefined;
|
|
350
|
+
if (typeof value !== "string" || value.trim().length > max) throw evidenceError(id, field);
|
|
351
|
+
return value.trim() || undefined;
|
|
352
|
+
}
|
|
353
|
+
function verified(item: GjcTeamTaskCompletionEvidenceItem) {
|
|
354
|
+
return (
|
|
355
|
+
(item.kind === "command" && item.status === "passed") || (item.kind !== "command" && item.status === "verified")
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
export function normalizeGjcTeamTaskCompletionEvidence(
|
|
359
|
+
id: string,
|
|
360
|
+
owner: string,
|
|
361
|
+
input: unknown,
|
|
362
|
+
recordedAt = now(),
|
|
363
|
+
): GjcTeamTaskCompletionEvidence {
|
|
364
|
+
if (!isRecord(input) || Array.isArray(input)) throw new Error(`completion_evidence_required:${id}`);
|
|
365
|
+
if (!Array.isArray(input.items) || !input.items.length) throw evidenceError(id, "items");
|
|
366
|
+
const items = input.items.map(value => {
|
|
367
|
+
if (!isRecord(value) || Array.isArray(value)) throw evidenceError(id, "items");
|
|
368
|
+
const kind = required(id, "items.kind", value.kind);
|
|
369
|
+
if (kind !== "command" && kind !== "inspection" && kind !== "artifact") throw evidenceError(id, "items.kind");
|
|
370
|
+
const status = required(id, "items.status", value.status) as GjcTeamTaskCompletionEvidenceStatus;
|
|
371
|
+
if (
|
|
372
|
+
(kind === "command" && !["passed", "failed", "not_run"].includes(status)) ||
|
|
373
|
+
(kind !== "command" && !["verified", "rejected"].includes(status))
|
|
374
|
+
)
|
|
375
|
+
throw evidenceError(id, "items.status");
|
|
376
|
+
const item: GjcTeamTaskCompletionEvidenceItem = {
|
|
377
|
+
kind,
|
|
378
|
+
status,
|
|
379
|
+
summary: required(id, "items.summary", value.summary),
|
|
380
|
+
};
|
|
381
|
+
for (const [key, entry] of [
|
|
382
|
+
["command", optional(id, "items.command", value.command)],
|
|
383
|
+
["artifact", optional(id, "items.artifact", value.artifact)],
|
|
384
|
+
["location", optional(id, "items.location", value.location)],
|
|
385
|
+
["output", optional(id, "items.output", value.output)],
|
|
386
|
+
] as const)
|
|
387
|
+
if (entry) Object.assign(item, { [key]: entry });
|
|
388
|
+
if (kind === "command" && !item.command) throw evidenceError(id, "items.command");
|
|
389
|
+
return item;
|
|
390
|
+
});
|
|
391
|
+
if (!items.some(verified)) throw new Error(`completion_evidence_no_verified_item:${id}`);
|
|
392
|
+
let files: string[] | undefined;
|
|
393
|
+
if (input.files != null) {
|
|
394
|
+
if (!Array.isArray(input.files)) throw evidenceError(id, "files");
|
|
395
|
+
files = [
|
|
396
|
+
...new Set(
|
|
397
|
+
input.files.map(file => {
|
|
398
|
+
if (typeof file !== "string") throw evidenceError(id, "files");
|
|
399
|
+
const normalized = file.trim().replace(/\\/g, "/");
|
|
400
|
+
if (
|
|
401
|
+
!normalized ||
|
|
402
|
+
normalized.includes("\0") ||
|
|
403
|
+
path.isAbsolute(normalized) ||
|
|
404
|
+
normalized.split("/").includes("..")
|
|
405
|
+
)
|
|
406
|
+
throw evidenceError(id, "files");
|
|
407
|
+
return normalized;
|
|
408
|
+
}),
|
|
409
|
+
),
|
|
410
|
+
].sort();
|
|
411
|
+
if (!files.length) files = undefined;
|
|
412
|
+
}
|
|
413
|
+
const evidence: GjcTeamTaskCompletionEvidence = {
|
|
414
|
+
summary: required(id, "summary", input.summary),
|
|
415
|
+
items,
|
|
416
|
+
recorded_by: owner,
|
|
417
|
+
recorded_at: recordedAt,
|
|
418
|
+
};
|
|
419
|
+
if (files) evidence.files = files;
|
|
420
|
+
const notes = optional(id, "notes", input.notes);
|
|
421
|
+
if (notes) evidence.notes = notes;
|
|
422
|
+
return evidence;
|
|
423
|
+
}
|
|
424
|
+
export function getGjcTeamTaskCompletionEvidenceFailure(task: GjcTeamTask): string | null {
|
|
425
|
+
if (task.status !== "completed") return `task_not_completed:${task.id}`;
|
|
426
|
+
const evidence = task.completion_evidence;
|
|
427
|
+
if (!isRecord(evidence) || Array.isArray(evidence)) return `completion_evidence_required:${task.id}`;
|
|
428
|
+
if (typeof evidence.recorded_by !== "string" || !evidence.recorded_by.trim())
|
|
429
|
+
return `invalid_completion_evidence:${task.id}:recorded_by`;
|
|
430
|
+
if (typeof evidence.recorded_at !== "string" || !evidence.recorded_at.trim())
|
|
431
|
+
return `invalid_completion_evidence:${task.id}:recorded_at`;
|
|
432
|
+
try {
|
|
433
|
+
normalizeGjcTeamTaskCompletionEvidence(task.id, evidence.recorded_by.trim(), evidence, evidence.recorded_at);
|
|
434
|
+
return null;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
return error instanceof Error ? error.message : `invalid_completion_evidence:${task.id}:unknown`;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
export const isGjcTeamTaskCompletionVerified = (task: GjcTeamTask) =>
|
|
440
|
+
getGjcTeamTaskCompletionEvidenceFailure(task) === null;
|
|
441
|
+
function taskRecord(value: unknown): value is GjcTeamTask {
|
|
442
|
+
return (
|
|
443
|
+
isRecord(value) &&
|
|
444
|
+
typeof value.id === "string" &&
|
|
445
|
+
typeof value.status === "string" &&
|
|
446
|
+
["pending", "blocked", "in_progress", "completed", "failed", "complete"].includes(value.status) &&
|
|
447
|
+
(typeof value.subject === "string" || typeof value.title === "string") &&
|
|
448
|
+
(typeof value.description === "string" || typeof value.objective === "string")
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
export async function readGjcTeamTasksFromDir(dir: string): Promise<GjcTeamTask[]> {
|
|
452
|
+
try {
|
|
453
|
+
const entries = await fs.readdir(path.join(dir, "tasks"), {
|
|
454
|
+
withFileTypes: true,
|
|
455
|
+
});
|
|
456
|
+
const records = await Promise.all(
|
|
457
|
+
entries
|
|
458
|
+
.filter(entry => entry.isFile() && entry.name.endsWith(".json") && !entry.name.endsWith(".evidence.json"))
|
|
459
|
+
.map(entry => readJson<unknown>(path.join(dir, "tasks", entry.name))),
|
|
460
|
+
);
|
|
461
|
+
return records
|
|
462
|
+
.filter(taskRecord)
|
|
463
|
+
.map(normalizeGjcTeamTask)
|
|
464
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (isEnoent(error)) return [];
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async function writeGjcTeamTaskToDir(dir: string, task: GjcTeamTask): Promise<void> {
|
|
471
|
+
await writeJson(taskPath(dir, task.id), normalizeGjcTeamTask(task));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function eligibility(task: GjcTeamTask, worker: GjcTeamTaskWorker, tasks: GjcTeamTask[]): string | null {
|
|
475
|
+
if (task.status !== "pending") return `task_not_pending:${task.id}`;
|
|
476
|
+
if (task.owner && task.owner !== worker.id) return `task_owner_mismatch:${task.id}:${task.owner}`;
|
|
477
|
+
if (task.assignee && task.assignee !== worker.id) return `task_assignee_mismatch:${task.id}:${task.assignee}`;
|
|
478
|
+
const roles = new Set([worker.role, worker.agent_type].map(value => value.trim()).filter(Boolean));
|
|
479
|
+
if (task.required_role && !roles.has(task.required_role))
|
|
480
|
+
return `task_role_mismatch:${task.id}:${task.required_role}`;
|
|
481
|
+
if (task.allowed_roles?.length && !task.allowed_roles.some(role => roles.has(role)))
|
|
482
|
+
return `task_role_mismatch:${task.id}:${task.allowed_roles.join(",")}`;
|
|
483
|
+
if (task.blocked_by?.length) return `task_blocked:${task.id}:${task.blocked_by.join(",")}`;
|
|
484
|
+
for (const dependency of task.depends_on ?? [])
|
|
485
|
+
if (!tasks.find(candidate => candidate.id === dependency && isGjcTeamTaskCompletionVerified(candidate)))
|
|
486
|
+
return `task_dependency_incomplete:${task.id}:${dependency}`;
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
function claimRecord(value: unknown): GjcTeamTaskClaim | undefined {
|
|
490
|
+
if (
|
|
491
|
+
!isRecord(value) ||
|
|
492
|
+
typeof value.owner !== "string" ||
|
|
493
|
+
typeof value.token !== "string" ||
|
|
494
|
+
typeof value.leased_until !== "string" ||
|
|
495
|
+
!value.owner ||
|
|
496
|
+
!value.token ||
|
|
497
|
+
!value.leased_until
|
|
498
|
+
)
|
|
499
|
+
return undefined;
|
|
500
|
+
return {
|
|
501
|
+
owner: value.owner,
|
|
502
|
+
token: value.token,
|
|
503
|
+
leased_until: value.leased_until,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
const expired = (value: string | undefined) =>
|
|
507
|
+
Boolean(value && Number.isFinite(Date.parse(value)) && Date.parse(value) <= Date.now());
|
|
508
|
+
|
|
509
|
+
const teamTaskMutationCapability = Symbol("team-task-mutation-capability");
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* This is intentionally structural only: callers can invoke methods supplied by
|
|
513
|
+
* `withGjcTeamTaskMutation`, but cannot create an active capability. Each method
|
|
514
|
+
* closes over a private brand and is revoked when that callback settles.
|
|
515
|
+
*/
|
|
516
|
+
export interface GjcTeamTaskMutationCapability {
|
|
517
|
+
create(subject: string, description: string, options: GjcTeamTaskMetadataInput): Promise<GjcTeamTask>;
|
|
518
|
+
update(
|
|
519
|
+
id: string,
|
|
520
|
+
updates: Partial<
|
|
521
|
+
Pick<
|
|
522
|
+
GjcTeamTask,
|
|
523
|
+
"subject" | "description" | "blocked_by" | "depends_on" | "lane" | "required_role" | "allowed_roles"
|
|
524
|
+
>
|
|
525
|
+
>,
|
|
526
|
+
): Promise<GjcTeamTask>;
|
|
527
|
+
claim(worker: GjcTeamTaskWorker, id?: string): Promise<GjcTeamApiClaimResult>;
|
|
528
|
+
transition(
|
|
529
|
+
id: string,
|
|
530
|
+
status: GjcTeamTaskStatus,
|
|
531
|
+
token?: string,
|
|
532
|
+
workerId?: string,
|
|
533
|
+
evidenceInput?: unknown,
|
|
534
|
+
): Promise<GjcTeamTask>;
|
|
535
|
+
release(id: string, token: string, workerId: string): Promise<GjcTeamTask>;
|
|
536
|
+
writeRecovered(task: GjcTeamTask): Promise<void>;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export async function withGjcTeamTaskMutation<T>(
|
|
540
|
+
store: GjcTeamTaskStore,
|
|
541
|
+
fn: (capability: GjcTeamTaskMutationCapability) => Promise<T>,
|
|
542
|
+
): Promise<T> {
|
|
543
|
+
return withGjcTeamMutationFence(store.dir, () => store.withMutationCapability(teamTaskMutationCapability, fn));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export class GjcTeamTaskStore {
|
|
547
|
+
constructor(
|
|
548
|
+
readonly dir: string,
|
|
549
|
+
readonly appendEvent: EventAppender,
|
|
550
|
+
) {}
|
|
551
|
+
async withMutationCapability<T>(
|
|
552
|
+
capabilityBrand: typeof teamTaskMutationCapability,
|
|
553
|
+
fn: (capability: GjcTeamTaskMutationCapability) => Promise<T>,
|
|
554
|
+
): Promise<T> {
|
|
555
|
+
if (capabilityBrand !== teamTaskMutationCapability) throw new Error("team_mutation_capability_required");
|
|
556
|
+
const brand = Symbol("active-team-task-mutation");
|
|
557
|
+
let active = true;
|
|
558
|
+
const mutations: Promise<unknown>[] = [];
|
|
559
|
+
const assertActive = () => {
|
|
560
|
+
if (!active || brand.description !== "active-team-task-mutation")
|
|
561
|
+
throw new Error("team_mutation_capability_revoked");
|
|
562
|
+
};
|
|
563
|
+
const track = <T>(mutation: () => Promise<T>): Promise<T> => {
|
|
564
|
+
assertActive();
|
|
565
|
+
const pending = mutation();
|
|
566
|
+
mutations.push(pending);
|
|
567
|
+
void pending.catch(() => undefined);
|
|
568
|
+
return pending;
|
|
569
|
+
};
|
|
570
|
+
const capability = Object.freeze<GjcTeamTaskMutationCapability>({
|
|
571
|
+
create: (subject, description, options) => track(() => this.#createUnlocked(subject, description, options)),
|
|
572
|
+
update: (id, updates) => track(() => this.#updateUnlocked(id, updates)),
|
|
573
|
+
claim: (worker, id) => track(() => this.#claimUnlocked(worker, id)),
|
|
574
|
+
transition: (id, status, token, workerId, evidenceInput) =>
|
|
575
|
+
track(() => this.#transitionUnlocked(id, status, token, workerId, evidenceInput)),
|
|
576
|
+
release: (id, token, workerId) => track(() => this.#releaseUnlocked(id, token, workerId)),
|
|
577
|
+
writeRecovered: task => track(() => writeGjcTeamTaskToDir(this.dir, task)),
|
|
578
|
+
});
|
|
579
|
+
let callbackResult: T | undefined;
|
|
580
|
+
let callbackError: unknown;
|
|
581
|
+
let callbackFailed = false;
|
|
582
|
+
try {
|
|
583
|
+
callbackResult = await fn(capability);
|
|
584
|
+
} catch (error) {
|
|
585
|
+
callbackFailed = true;
|
|
586
|
+
callbackError = error;
|
|
587
|
+
}
|
|
588
|
+
// Callback settlement is the admission boundary. Drain only mutations that
|
|
589
|
+
// were admitted before it; an escaped capability cannot add work while draining.
|
|
590
|
+
active = false;
|
|
591
|
+
const settled: PromiseSettledResult<unknown>[] = [];
|
|
592
|
+
let drained = 0;
|
|
593
|
+
while (drained < mutations.length) {
|
|
594
|
+
const pending = mutations.slice(drained);
|
|
595
|
+
drained = mutations.length;
|
|
596
|
+
settled.push(...(await Promise.allSettled(pending)));
|
|
597
|
+
}
|
|
598
|
+
if (callbackFailed) throw callbackError;
|
|
599
|
+
const failed = settled.find((result): result is PromiseRejectedResult => result.status === "rejected");
|
|
600
|
+
if (failed) throw failed.reason;
|
|
601
|
+
return callbackResult as T;
|
|
602
|
+
}
|
|
603
|
+
async list() {
|
|
604
|
+
return readGjcTeamTasksFromDir(this.dir);
|
|
605
|
+
}
|
|
606
|
+
async read(id: string) {
|
|
607
|
+
const task = (await this.list()).find(candidate => candidate.id === id);
|
|
608
|
+
if (!task) throw new Error(`task_not_found:${id}`);
|
|
609
|
+
return task;
|
|
610
|
+
}
|
|
611
|
+
async create(subject: string, description: string, options: GjcTeamTaskMetadataInput) {
|
|
612
|
+
return withGjcTeamMutationFence(this.dir, () => this.#createUnlocked(subject, description, options));
|
|
613
|
+
}
|
|
614
|
+
async #createUnlocked(subject: string, description: string, options: GjcTeamTaskMetadataInput) {
|
|
615
|
+
const task: GjcTeamTask = {
|
|
616
|
+
id: `task-${(await this.list()).length + 1}`,
|
|
617
|
+
subject,
|
|
618
|
+
description,
|
|
619
|
+
title: subject,
|
|
620
|
+
objective: description,
|
|
621
|
+
status: "pending",
|
|
622
|
+
...options,
|
|
623
|
+
version: 1,
|
|
624
|
+
created_at: now(),
|
|
625
|
+
updated_at: now(),
|
|
626
|
+
};
|
|
627
|
+
await writeGjcTeamTaskToDir(this.dir, task);
|
|
628
|
+
await this.appendEvent({
|
|
629
|
+
type: "task_created",
|
|
630
|
+
task_id: task.id,
|
|
631
|
+
message: subject,
|
|
632
|
+
});
|
|
633
|
+
return task;
|
|
634
|
+
}
|
|
635
|
+
async update(
|
|
636
|
+
id: string,
|
|
637
|
+
updates: Partial<
|
|
638
|
+
Pick<
|
|
639
|
+
GjcTeamTask,
|
|
640
|
+
"subject" | "description" | "blocked_by" | "depends_on" | "lane" | "required_role" | "allowed_roles"
|
|
641
|
+
>
|
|
642
|
+
>,
|
|
643
|
+
) {
|
|
644
|
+
return withGjcTeamMutationFence(this.dir, () => this.#updateUnlocked(id, updates));
|
|
645
|
+
}
|
|
646
|
+
async #updateUnlocked(
|
|
647
|
+
id: string,
|
|
648
|
+
updates: Partial<
|
|
649
|
+
Pick<
|
|
650
|
+
GjcTeamTask,
|
|
651
|
+
"subject" | "description" | "blocked_by" | "depends_on" | "lane" | "required_role" | "allowed_roles"
|
|
652
|
+
>
|
|
653
|
+
>,
|
|
654
|
+
) {
|
|
655
|
+
const task = await this.read(id);
|
|
656
|
+
const updated = normalizeGjcTeamTask({
|
|
657
|
+
...task,
|
|
658
|
+
...updates,
|
|
659
|
+
title: updates.subject ?? task.title,
|
|
660
|
+
objective: updates.description ?? task.objective,
|
|
661
|
+
version: task.version + 1,
|
|
662
|
+
updated_at: now(),
|
|
663
|
+
});
|
|
664
|
+
await writeGjcTeamTaskToDir(this.dir, updated);
|
|
665
|
+
await this.appendEvent({
|
|
666
|
+
type: "task_updated",
|
|
667
|
+
task_id: id,
|
|
668
|
+
message: updated.subject,
|
|
669
|
+
});
|
|
670
|
+
return updated;
|
|
671
|
+
}
|
|
672
|
+
async claim(worker: GjcTeamTaskWorker, id?: string): Promise<GjcTeamApiClaimResult> {
|
|
673
|
+
return withGjcTeamMutationFence(this.dir, () => this.#claimUnlocked(worker, id));
|
|
674
|
+
}
|
|
675
|
+
async #claimUnlocked(worker: GjcTeamTaskWorker, id?: string): Promise<GjcTeamApiClaimResult> {
|
|
676
|
+
const tasks = await this.list();
|
|
677
|
+
const task = id
|
|
678
|
+
? tasks.find(candidate => candidate.id === id)
|
|
679
|
+
: tasks.find(candidate => eligibility(candidate, worker, tasks) === null);
|
|
680
|
+
if (!task)
|
|
681
|
+
return {
|
|
682
|
+
ok: false,
|
|
683
|
+
reason: id ? `task_not_found:${id}` : "no_pending_task",
|
|
684
|
+
};
|
|
685
|
+
const reason = eligibility(task, worker, tasks);
|
|
686
|
+
if (reason) return { ok: false, reason };
|
|
687
|
+
const existing = task.claim ?? claimRecord(await readJson<unknown>(claimPath(this.dir, task.id)));
|
|
688
|
+
if (existing && !expired(existing.leased_until)) return { ok: false, reason: `task_already_claimed:${task.id}` };
|
|
689
|
+
const claim: GjcTeamTaskClaim = {
|
|
690
|
+
owner: worker.id,
|
|
691
|
+
token: randomUUID(),
|
|
692
|
+
leased_until: new Date(Date.now() + 30 * 60_000).toISOString(),
|
|
693
|
+
};
|
|
694
|
+
try {
|
|
695
|
+
await createJsonNoClobber(
|
|
696
|
+
claimPath(this.dir, task.id),
|
|
697
|
+
claim,
|
|
698
|
+
writerOptions(claimPath(this.dir, task.id), "state", "claim"),
|
|
699
|
+
);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
if (isRecord(error) && error.code === "EEXIST")
|
|
702
|
+
return { ok: false, reason: `task_already_claimed:${task.id}` };
|
|
703
|
+
throw error;
|
|
704
|
+
}
|
|
705
|
+
const current = await this.read(task.id);
|
|
706
|
+
const currentReason = eligibility(current, worker, await this.list());
|
|
707
|
+
if (currentReason || current.status !== "pending") {
|
|
708
|
+
await deleteIfOwned(claimPath(this.dir, task.id), {
|
|
709
|
+
...writerOptions(claimPath(this.dir, task.id), "prune", "rollback"),
|
|
710
|
+
predicate: current => (current as GjcTeamTaskClaim).token === claim.token,
|
|
711
|
+
});
|
|
712
|
+
return {
|
|
713
|
+
ok: false,
|
|
714
|
+
reason: currentReason ?? `task_not_pending:${task.id}`,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
const updated = {
|
|
718
|
+
...current,
|
|
719
|
+
status: "in_progress" as const,
|
|
720
|
+
assignee: worker.id,
|
|
721
|
+
owner: worker.id,
|
|
722
|
+
claim,
|
|
723
|
+
version: current.version + 1,
|
|
724
|
+
updated_at: now(),
|
|
725
|
+
};
|
|
726
|
+
try {
|
|
727
|
+
await writeGjcTeamTaskToDir(this.dir, updated);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
await deleteIfOwned(claimPath(this.dir, task.id), {
|
|
730
|
+
...writerOptions(claimPath(this.dir, task.id), "prune", "rollback"),
|
|
731
|
+
predicate: current => (current as GjcTeamTaskClaim).token === claim.token,
|
|
732
|
+
});
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
await this.appendEvent({
|
|
736
|
+
type: "task_claimed",
|
|
737
|
+
task_id: updated.id,
|
|
738
|
+
worker: worker.id,
|
|
739
|
+
message: "Worker claimed task",
|
|
740
|
+
});
|
|
741
|
+
return {
|
|
742
|
+
ok: true,
|
|
743
|
+
task: updated,
|
|
744
|
+
worker_id: worker.id,
|
|
745
|
+
claim_token: claim.token,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
async transition(id: string, status: GjcTeamTaskStatus, token?: string, workerId?: string, evidenceInput?: unknown) {
|
|
749
|
+
return withGjcTeamMutationFence(this.dir, () =>
|
|
750
|
+
this.#transitionUnlocked(id, status, token, workerId, evidenceInput),
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
async #transitionUnlocked(
|
|
754
|
+
id: string,
|
|
755
|
+
status: GjcTeamTaskStatus,
|
|
756
|
+
token?: string,
|
|
757
|
+
workerId?: string,
|
|
758
|
+
evidenceInput?: unknown,
|
|
759
|
+
) {
|
|
760
|
+
const task = await this.read(id);
|
|
761
|
+
if (status === "pending") throw new Error(`invalid_task_transition:${id}:pending_requires_release`);
|
|
762
|
+
if (task.status === "completed" || task.status === "failed") throw new Error(`task_terminal:${id}`);
|
|
763
|
+
if (!task.claim || !token) throw new Error(`claim_token_required:${id}`);
|
|
764
|
+
if (task.claim.token !== token) throw new Error(`claim_token_mismatch:${id}`);
|
|
765
|
+
if (workerId && task.claim.owner !== workerId) throw new Error(`claim_owner_mismatch:${id}`);
|
|
766
|
+
const terminal = status === "completed" || status === "failed";
|
|
767
|
+
const transitionedAt = now();
|
|
768
|
+
const evidence =
|
|
769
|
+
status === "completed"
|
|
770
|
+
? normalizeGjcTeamTaskCompletionEvidence(id, task.claim.owner, evidenceInput, transitionedAt)
|
|
771
|
+
: undefined;
|
|
772
|
+
const updated: GjcTeamTask = {
|
|
773
|
+
...task,
|
|
774
|
+
status,
|
|
775
|
+
claim: terminal ? undefined : task.claim,
|
|
776
|
+
version: task.version + 1,
|
|
777
|
+
updated_at: transitionedAt,
|
|
778
|
+
...(terminal ? { completed_at: transitionedAt } : {}),
|
|
779
|
+
...(evidence ? { completion_evidence: evidence } : {}),
|
|
780
|
+
};
|
|
781
|
+
await writeGjcTeamTaskToDir(this.dir, updated);
|
|
782
|
+
if (terminal)
|
|
783
|
+
await removeFileAudited(claimPath(this.dir, id), writerOptions(claimPath(this.dir, id), "prune", "terminal"));
|
|
784
|
+
const data: Record<string, unknown> = { status };
|
|
785
|
+
if (evidence)
|
|
786
|
+
data.completion_evidence = {
|
|
787
|
+
recorded_by: evidence.recorded_by,
|
|
788
|
+
item_count: evidence.items.length,
|
|
789
|
+
verified_item_count: evidence.items.filter(verified).length,
|
|
790
|
+
files_count: evidence.files?.length ?? 0,
|
|
791
|
+
};
|
|
792
|
+
await this.appendEvent({
|
|
793
|
+
type: "task_transitioned",
|
|
794
|
+
task_id: id,
|
|
795
|
+
message: "Task status changed",
|
|
796
|
+
data,
|
|
797
|
+
});
|
|
798
|
+
return updated;
|
|
799
|
+
}
|
|
800
|
+
async release(id: string, token: string, workerId: string) {
|
|
801
|
+
return withGjcTeamMutationFence(this.dir, () => this.#releaseUnlocked(id, token, workerId));
|
|
802
|
+
}
|
|
803
|
+
async #releaseUnlocked(id: string, token: string, workerId: string) {
|
|
804
|
+
const task = await this.read(id);
|
|
805
|
+
if (!task.claim || task.claim.token !== token || task.claim.owner !== workerId)
|
|
806
|
+
throw new Error(`claim_token_mismatch:${id}`);
|
|
807
|
+
const updated: GjcTeamTask = {
|
|
808
|
+
...task,
|
|
809
|
+
status: "pending",
|
|
810
|
+
assignee: undefined,
|
|
811
|
+
claim: undefined,
|
|
812
|
+
version: task.version + 1,
|
|
813
|
+
updated_at: now(),
|
|
814
|
+
};
|
|
815
|
+
await writeGjcTeamTaskToDir(this.dir, updated);
|
|
816
|
+
await deleteIfOwned(claimPath(this.dir, id), {
|
|
817
|
+
...writerOptions(claimPath(this.dir, id), "prune", "release"),
|
|
818
|
+
predicate: current => (current as GjcTeamTaskClaim).token === token,
|
|
819
|
+
});
|
|
820
|
+
await this.appendEvent({
|
|
821
|
+
type: "task_claim_released",
|
|
822
|
+
task_id: id,
|
|
823
|
+
worker: workerId,
|
|
824
|
+
message: "Task claim released",
|
|
825
|
+
});
|
|
826
|
+
return updated;
|
|
827
|
+
}
|
|
828
|
+
}
|