@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,874 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { inflateSync } from "node:zlib";
|
|
3
|
+
import {
|
|
4
|
+
evidenceKindMatches,
|
|
5
|
+
hasExistingNonEmptyArtifact,
|
|
6
|
+
hasTypedVerifiedReceipt,
|
|
7
|
+
isLiveSurfaceFamily,
|
|
8
|
+
isSubstantiveEvidence,
|
|
9
|
+
type JsonObject,
|
|
10
|
+
nonEmptyString,
|
|
11
|
+
nonEmptyStringArray,
|
|
12
|
+
normalizedEvidenceKind,
|
|
13
|
+
PASSED_STATUS,
|
|
14
|
+
qualityGateObject,
|
|
15
|
+
readArtifactBytes,
|
|
16
|
+
requiredStringField,
|
|
17
|
+
requireObjectArray,
|
|
18
|
+
requireQualityGateObject,
|
|
19
|
+
requireResolvedLinks,
|
|
20
|
+
requireStringLinks,
|
|
21
|
+
type SurfaceFamily,
|
|
22
|
+
surfaceFamily,
|
|
23
|
+
} from "./ultragoal-runtime";
|
|
24
|
+
|
|
25
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
26
|
+
const JPEG_START_OF_IMAGE = 0xd8;
|
|
27
|
+
const JPEG_END_OF_IMAGE = 0xd9;
|
|
28
|
+
const JPEG_START_OF_SCAN = 0xda;
|
|
29
|
+
const JPEG_STANDALONE_MARKERS = new Set([0x01, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7]);
|
|
30
|
+
const PNG_CRC_TABLE = new Uint32Array(256).map((_, index) => {
|
|
31
|
+
let crc = index;
|
|
32
|
+
for (let bit = 0; bit < 8; bit++) crc = crc & 1 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1;
|
|
33
|
+
return crc >>> 0;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export function pngCrc32(bytes: Buffer): number {
|
|
37
|
+
let crc = 0xffffffff;
|
|
38
|
+
for (const byte of bytes) crc = PNG_CRC_TABLE[(crc ^ byte) & 0xff]! ^ (crc >>> 8);
|
|
39
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parsePngDimensions(
|
|
43
|
+
bytes: Buffer,
|
|
44
|
+
): { width: number; height: number; headerBytes: number; sampleBytes?: Buffer } | null {
|
|
45
|
+
if (bytes.length < 45) return null;
|
|
46
|
+
if (!bytes.subarray(0, 8).equals(PNG_SIGNATURE)) return null;
|
|
47
|
+
let offset = 8;
|
|
48
|
+
let width = 0;
|
|
49
|
+
let height = 0;
|
|
50
|
+
let sawIhdr = false;
|
|
51
|
+
let sawIdat = false;
|
|
52
|
+
const idatChunks: Buffer[] = [];
|
|
53
|
+
while (offset + 12 <= bytes.length) {
|
|
54
|
+
const chunkStart = offset;
|
|
55
|
+
const length = bytes.readUInt32BE(offset);
|
|
56
|
+
offset += 4;
|
|
57
|
+
const type = bytes.toString("ascii", offset, offset + 4);
|
|
58
|
+
offset += 4;
|
|
59
|
+
if (offset + length + 4 > bytes.length) return null;
|
|
60
|
+
const data = bytes.subarray(offset, offset + length);
|
|
61
|
+
offset += length;
|
|
62
|
+
const expectedCrc = bytes.readUInt32BE(offset);
|
|
63
|
+
offset += 4;
|
|
64
|
+
if (pngCrc32(bytes.subarray(chunkStart + 4, offset - 4)) !== expectedCrc) return null;
|
|
65
|
+
if (!sawIhdr) {
|
|
66
|
+
if (type !== "IHDR" || length !== 13) return null;
|
|
67
|
+
width = data.readUInt32BE(0);
|
|
68
|
+
height = data.readUInt32BE(4);
|
|
69
|
+
if (
|
|
70
|
+
width === 0 ||
|
|
71
|
+
height === 0 ||
|
|
72
|
+
data[8] !== 8 ||
|
|
73
|
+
![2, 6].includes(data[9]!) ||
|
|
74
|
+
data[10] !== 0 ||
|
|
75
|
+
data[11] !== 0 ||
|
|
76
|
+
data[12] !== 0
|
|
77
|
+
)
|
|
78
|
+
return null;
|
|
79
|
+
sawIhdr = true;
|
|
80
|
+
} else if (type === "IHDR") return null;
|
|
81
|
+
if (type === "IDAT") {
|
|
82
|
+
if (!sawIhdr || length === 0) return null;
|
|
83
|
+
sawIdat = true;
|
|
84
|
+
idatChunks.push(data);
|
|
85
|
+
}
|
|
86
|
+
if (type === "IEND") {
|
|
87
|
+
if (length !== 0 || !sawIhdr || !sawIdat || offset !== bytes.length) return null;
|
|
88
|
+
try {
|
|
89
|
+
return { width, height, headerBytes: 8, sampleBytes: inflateSync(Buffer.concat(idatChunks)) };
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function parseJpegDimensions(
|
|
99
|
+
bytes: Buffer,
|
|
100
|
+
): { width: number; height: number; headerBytes: number; sampleBytes?: Buffer } | null {
|
|
101
|
+
if (bytes.length < 8 || bytes[0] !== 0xff || bytes[1] !== JPEG_START_OF_IMAGE) return null;
|
|
102
|
+
let offset = 2;
|
|
103
|
+
let dimensions: { width: number; height: number; headerBytes: number } | null = null;
|
|
104
|
+
let sawStartOfScan = false;
|
|
105
|
+
let scanStart = -1;
|
|
106
|
+
while (offset < bytes.length) {
|
|
107
|
+
if (bytes[offset] !== 0xff) return null;
|
|
108
|
+
while (offset < bytes.length && bytes[offset] === 0xff) offset++;
|
|
109
|
+
if (offset >= bytes.length) return null;
|
|
110
|
+
const marker = bytes[offset++];
|
|
111
|
+
if (marker === 0x00) return null;
|
|
112
|
+
if (marker === JPEG_END_OF_IMAGE) return null;
|
|
113
|
+
if (JPEG_STANDALONE_MARKERS.has(marker)) continue;
|
|
114
|
+
if (offset + 2 > bytes.length) return null;
|
|
115
|
+
const segmentLength = bytes.readUInt16BE(offset);
|
|
116
|
+
if (segmentLength < 2 || offset + segmentLength > bytes.length) return null;
|
|
117
|
+
const segmentDataEnd = offset + segmentLength;
|
|
118
|
+
if (marker === 0xc0 || marker === 0xc1 || marker === 0xc2) {
|
|
119
|
+
if (segmentLength < 8) return null;
|
|
120
|
+
dimensions = {
|
|
121
|
+
width: bytes.readUInt16BE(offset + 5),
|
|
122
|
+
height: bytes.readUInt16BE(offset + 3),
|
|
123
|
+
headerBytes: offset + segmentLength,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (marker === JPEG_START_OF_SCAN) {
|
|
127
|
+
if (!dimensions || segmentDataEnd >= bytes.length) return null;
|
|
128
|
+
sawStartOfScan = true;
|
|
129
|
+
scanStart = segmentDataEnd;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
offset += segmentLength;
|
|
133
|
+
}
|
|
134
|
+
if (!dimensions || !sawStartOfScan || scanStart < 0) return null;
|
|
135
|
+
let scanOffset = scanStart;
|
|
136
|
+
let entropyBytes = 0;
|
|
137
|
+
while (scanOffset < bytes.length) {
|
|
138
|
+
const byte = bytes[scanOffset++]!;
|
|
139
|
+
if (byte !== 0xff) {
|
|
140
|
+
entropyBytes++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (scanOffset >= bytes.length) return null;
|
|
144
|
+
const marker = bytes[scanOffset++]!;
|
|
145
|
+
if (marker === 0x00) {
|
|
146
|
+
entropyBytes++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (JPEG_STANDALONE_MARKERS.has(marker)) continue;
|
|
150
|
+
if (marker === JPEG_END_OF_IMAGE) {
|
|
151
|
+
if (scanOffset !== bytes.length || entropyBytes < 32) return null;
|
|
152
|
+
return { ...dimensions, sampleBytes: bytes.subarray(scanStart, scanOffset - 2) };
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function unsupportedScreenshotFormat(bytes: Buffer): string | null {
|
|
160
|
+
if (bytes.toString("ascii", 0, 6) === "GIF87a" || bytes.toString("ascii", 0, 6) === "GIF89a") return "GIF";
|
|
161
|
+
if (bytes.toString("ascii", 0, 2) === "BM") return "BMP";
|
|
162
|
+
if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP")
|
|
163
|
+
return "WebP";
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseImageDimensions(
|
|
168
|
+
bytes: Buffer,
|
|
169
|
+
): { width: number; height: number; headerBytes: number; sampleBytes?: Buffer } | null {
|
|
170
|
+
return parsePngDimensions(bytes) ?? parseJpegDimensions(bytes);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function hasNonUniformImageBytes(bytes: Buffer, headerBytes: number, sampleBytes?: Buffer): boolean {
|
|
174
|
+
const source = sampleBytes ?? bytes;
|
|
175
|
+
const sampleStart = sampleBytes ? 0 : Math.min(Math.max(headerBytes, 0), source.length);
|
|
176
|
+
const sampleLength = source.length - sampleStart;
|
|
177
|
+
if (sampleLength < 32) return false;
|
|
178
|
+
const windows: Buffer[] = [];
|
|
179
|
+
for (let index = 0; index < 64; index++) {
|
|
180
|
+
const offset = sampleStart + Math.floor(((sampleLength - 32) * index) / 63);
|
|
181
|
+
windows.push(source.subarray(offset, offset + 32));
|
|
182
|
+
}
|
|
183
|
+
const byteCounts = new Map<number, number>();
|
|
184
|
+
let total = 0;
|
|
185
|
+
for (const window of windows) {
|
|
186
|
+
for (const byte of window) {
|
|
187
|
+
byteCounts.set(byte, (byteCounts.get(byte) ?? 0) + 1);
|
|
188
|
+
total++;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const first = windows[0]!;
|
|
192
|
+
const differingWindows = windows.slice(1).filter(window => !window.equals(first)).length;
|
|
193
|
+
const maxCount = Math.max(...byteCounts.values());
|
|
194
|
+
return byteCounts.size >= 16 && differingWindows >= 8 && maxCount / total <= 0.95;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function validateScreenshotArtifact(cwd: string, row: JsonObject, fieldName: string): Promise<boolean> {
|
|
198
|
+
const bytes = await readArtifactBytes(cwd, row, fieldName);
|
|
199
|
+
if (!bytes) throw new Error(`qualityGate ${fieldName} screenshot artifact path must resolve to an existing file`);
|
|
200
|
+
if (bytes.length < 4096) throw new Error(`qualityGate ${fieldName} screenshot artifact must be at least 4096 bytes`);
|
|
201
|
+
const unsupportedFormat = unsupportedScreenshotFormat(bytes);
|
|
202
|
+
if (unsupportedFormat) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`qualityGate ${fieldName} unsupported/undecodable screenshot format ${unsupportedFormat}; use PNG or fully marker-validated JPEG`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const dimensions = parseImageDimensions(bytes);
|
|
208
|
+
if (!dimensions)
|
|
209
|
+
throw new Error(`qualityGate ${fieldName} screenshot artifact must be a decodable PNG or JPEG image`);
|
|
210
|
+
if (dimensions.width < 320 || dimensions.height < 180) {
|
|
211
|
+
throw new Error(`qualityGate ${fieldName} screenshot artifact must be at least 320x180 pixels`);
|
|
212
|
+
}
|
|
213
|
+
if (!hasNonUniformImageBytes(bytes, dimensions.headerBytes, dimensions.sampleBytes)) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`qualityGate ${fieldName} screenshot artifact must be non-uniform, not blank, solid, tiny, or placeholder imagery`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function normalizeTranscriptTimestamp(value: unknown): number | null {
|
|
222
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
223
|
+
if (typeof value !== "string" || value.trim().length === 0) return null;
|
|
224
|
+
const numeric = Number(value);
|
|
225
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
226
|
+
const parsed = Date.parse(value);
|
|
227
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function transcriptSurfaceCompatible(value: unknown, family: SurfaceFamily): boolean {
|
|
231
|
+
const surface = nonEmptyString(value);
|
|
232
|
+
return !surface || family === "unknown" || surfaceFamily(surface) === family;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function actionSelectorRequired(type: string): boolean {
|
|
236
|
+
return ["click", "fill", "press", "assert", "screenshot", "observe"].includes(type);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function validateAutomationTranscriptArtifact(
|
|
240
|
+
cwd: string,
|
|
241
|
+
row: JsonObject,
|
|
242
|
+
fieldName: string,
|
|
243
|
+
options: { surfaceFamily: SurfaceFamily },
|
|
244
|
+
): Promise<boolean> {
|
|
245
|
+
const bytes = await readArtifactBytes(cwd, row, fieldName);
|
|
246
|
+
if (!bytes) throw new Error(`qualityGate ${fieldName} automation transcript path must resolve to an existing file`);
|
|
247
|
+
let transcript: JsonObject;
|
|
248
|
+
try {
|
|
249
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
250
|
+
transcript = requireQualityGateObject(parsed, `${fieldName}.transcript`);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
throw new Error(`qualityGate ${fieldName} automation transcript must be valid JSON: ${String(error)}`);
|
|
253
|
+
}
|
|
254
|
+
if (transcript.schemaVersion !== 1)
|
|
255
|
+
throw new Error(`qualityGate ${fieldName} automation transcript schemaVersion must be 1`);
|
|
256
|
+
if (!transcriptSurfaceCompatible(transcript.surface, options.surfaceFamily)) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`qualityGate ${fieldName} automation transcript surface is not compatible with ${options.surfaceFamily}`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
if (!nonEmptyString(transcript.tool))
|
|
262
|
+
throw new Error(`qualityGate ${fieldName} automation transcript tool must be non-empty`);
|
|
263
|
+
const actions = requireObjectArray(transcript.actions, `${fieldName}.actions`);
|
|
264
|
+
if (actions.length < 1) throw new Error(`qualityGate ${fieldName} automation transcript actions must be non-empty`);
|
|
265
|
+
const assertionsValue = transcript.assertions;
|
|
266
|
+
const assertions =
|
|
267
|
+
assertionsValue === undefined ? [] : requireObjectArray(assertionsValue, `${fieldName}.assertions`);
|
|
268
|
+
const timestamps: number[] = [];
|
|
269
|
+
let hasSelectorBearingEntry = false;
|
|
270
|
+
for (const [index, action] of actions.entries()) {
|
|
271
|
+
const actionField = `${fieldName}.actions[${index}]`;
|
|
272
|
+
const type = requiredStringField(action, "type", actionField).toLowerCase();
|
|
273
|
+
const timestamp = normalizeTranscriptTimestamp(action.timestamp);
|
|
274
|
+
if (timestamp === null) throw new Error(`qualityGate ${actionField}.timestamp must be present and parseable`);
|
|
275
|
+
timestamps.push(timestamp);
|
|
276
|
+
const selector = nonEmptyString(action.selector);
|
|
277
|
+
if (actionSelectorRequired(type) && !selector)
|
|
278
|
+
throw new Error(`qualityGate ${actionField}.selector must be non-empty`);
|
|
279
|
+
if (type === "goto" && !nonEmptyString(action.url))
|
|
280
|
+
throw new Error(`qualityGate ${actionField}.url must be non-empty`);
|
|
281
|
+
if (type === "custom" && !selector && !nonEmptyString(action.target)) {
|
|
282
|
+
throw new Error(`qualityGate ${actionField}.selector or target must be non-empty`);
|
|
283
|
+
}
|
|
284
|
+
if (selector) hasSelectorBearingEntry = true;
|
|
285
|
+
}
|
|
286
|
+
for (const [index, assertion] of assertions.entries()) {
|
|
287
|
+
const assertionField = `${fieldName}.assertions[${index}]`;
|
|
288
|
+
const timestamp = normalizeTranscriptTimestamp(assertion.timestamp);
|
|
289
|
+
if (timestamp === null) throw new Error(`qualityGate ${assertionField}.timestamp must be present and parseable`);
|
|
290
|
+
timestamps.push(timestamp);
|
|
291
|
+
if (nonEmptyString(assertion.status)?.toLowerCase() !== PASSED_STATUS) {
|
|
292
|
+
throw new Error(`qualityGate ${assertionField}.status must be passed`);
|
|
293
|
+
}
|
|
294
|
+
if (nonEmptyString(assertion.selector)) hasSelectorBearingEntry = true;
|
|
295
|
+
}
|
|
296
|
+
for (let index = 1; index < timestamps.length; index++) {
|
|
297
|
+
if (timestamps[index]! < timestamps[index - 1]!) {
|
|
298
|
+
throw new Error(`qualityGate ${fieldName} automation transcript timestamps must be monotonic non-decreasing`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!hasSelectorBearingEntry) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`qualityGate ${fieldName} automation transcript must include at least one selector-bearing action or assertion`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function validatePtyCaptureArtifact(cwd: string, row: JsonObject, fieldName: string): Promise<boolean> {
|
|
310
|
+
const bytes = await readArtifactBytes(cwd, row, fieldName);
|
|
311
|
+
if (!bytes) throw new Error(`qualityGate ${fieldName} PTY capture path must resolve to an existing file`);
|
|
312
|
+
if (bytes.length < 512) throw new Error(`qualityGate ${fieldName} PTY capture must be at least 512 bytes`);
|
|
313
|
+
const text = bytes.toString("utf8");
|
|
314
|
+
const hasCsi = /\x1b\[[0-?]*[ -/]*[@-~]/.test(text);
|
|
315
|
+
const hasOsc = /\x1b\][^\x07]*(?:\x07|\x1b\\)/.test(text);
|
|
316
|
+
const hasAltOrCursor = /\x1b\[\?1049[hl]|\x1b\[H|\x1b\[2J/.test(text);
|
|
317
|
+
const hasRedraw = /[\r\b]/.test(text) && hasCsi;
|
|
318
|
+
if (!hasCsi && !hasOsc && !hasAltOrCursor && !hasRedraw) {
|
|
319
|
+
throw new Error(`qualityGate ${fieldName} PTY capture must contain terminal control sequences`);
|
|
320
|
+
}
|
|
321
|
+
if (!/[\x20-\x7e]{10,}/.test(text)) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`qualityGate ${fieldName} PTY capture must contain a printable text run of at least 10 characters`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function structuralArtifactKind(row: JsonObject): "screenshot" | "automation" | "pty" | null {
|
|
330
|
+
const kind = normalizedEvidenceKind(row);
|
|
331
|
+
if (evidenceKindMatches(kind, ["screenshot", "image", "visual"])) return "screenshot";
|
|
332
|
+
if (evidenceKindMatches(kind, ["browser", "playwright", "pandawright", "automation", "app-automation"]))
|
|
333
|
+
return "automation";
|
|
334
|
+
if (evidenceKindMatches(kind, ["pty", "tui", "terminal-capture"])) return "pty";
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export async function validateStructuralArtifact(
|
|
339
|
+
cwd: string,
|
|
340
|
+
row: JsonObject,
|
|
341
|
+
fieldName: string,
|
|
342
|
+
options: { surfaceFamily: SurfaceFamily; live: boolean },
|
|
343
|
+
): Promise<boolean> {
|
|
344
|
+
void options.live;
|
|
345
|
+
const kind = structuralArtifactKind(row);
|
|
346
|
+
if (!kind) return false;
|
|
347
|
+
if (kind === "screenshot") return validateScreenshotArtifact(cwd, row, fieldName);
|
|
348
|
+
if (kind === "automation") return validateAutomationTranscriptArtifact(cwd, row, fieldName, options);
|
|
349
|
+
if (kind === "pty") return validatePtyCaptureArtifact(cwd, row, fieldName);
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const CLI_REPLAY_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
354
|
+
const CLI_REPLAY_DEFAULT_TIMEOUT_MS = 10_000;
|
|
355
|
+
const CLI_REPLAY_MIN_TIMEOUT_MS = 1_000;
|
|
356
|
+
const CLI_REPLAY_MAX_TIMEOUT_MS = 30_000;
|
|
357
|
+
const CLI_REPLAY_EXEMPT_REASON_CODES = [
|
|
358
|
+
"unsafe_side_effect",
|
|
359
|
+
"requires_credentials",
|
|
360
|
+
"requires_network",
|
|
361
|
+
"non_deterministic_external",
|
|
362
|
+
"destructive",
|
|
363
|
+
"interactive_only",
|
|
364
|
+
"platform_unavailable",
|
|
365
|
+
] as const;
|
|
366
|
+
const CLI_REPLAY_EXEMPT_REASON_CODE_SET = new Set<string>(CLI_REPLAY_EXEMPT_REASON_CODES);
|
|
367
|
+
const CLI_REPLAY_ENV_BASE: Record<string, string> = { CI: "1", NO_COLOR: "1", GJC_ULTRAGOAL_REPLAY: "1" };
|
|
368
|
+
const CLI_REPLAY_EXEMPT_REASON_CODE_LIST = CLI_REPLAY_EXEMPT_REASON_CODES.join(", ");
|
|
369
|
+
const CLI_REPLAY_SAFE_ENV_NAMES = new Set(["LANG", "LC_ALL", "LC_CTYPE", "TZ"]);
|
|
370
|
+
const CLI_REPLAY_DANGEROUS_ENV_NAME_PATTERN =
|
|
371
|
+
/^(?:NODE_OPTIONS|GIT_EXTERNAL_DIFF|GIT_SSH|GIT_SSH_COMMAND|GIT_PAGER|PATH|LD_PRELOAD|LD_LIBRARY_PATH)$|^(?:GIT_CONFIG|DYLD_|BUN_|NPM_CONFIG_)|(?:^|_)OPTIONS$|PRELOAD$/;
|
|
372
|
+
const ANSI_ESCAPE_PATTERN = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\)|[@-Z\\-_])/g;
|
|
373
|
+
|
|
374
|
+
function clampCliReplayTimeout(value: unknown): number {
|
|
375
|
+
if (value === undefined) return CLI_REPLAY_DEFAULT_TIMEOUT_MS;
|
|
376
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
377
|
+
throw new Error("qualityGate CLI replay timeoutMs must be a finite number");
|
|
378
|
+
}
|
|
379
|
+
return Math.min(CLI_REPLAY_MAX_TIMEOUT_MS, Math.max(CLI_REPLAY_MIN_TIMEOUT_MS, Math.trunc(value)));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function basenameCommand(value: string): string {
|
|
383
|
+
return path.basename(value).toLowerCase();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function isDeterministicConsoleLogReplay(code: string): boolean {
|
|
387
|
+
let remaining = code.trim();
|
|
388
|
+
if (remaining.length === 0) return false;
|
|
389
|
+
let matched = false;
|
|
390
|
+
while (remaining.length > 0) {
|
|
391
|
+
const match =
|
|
392
|
+
/^console\.log\(\s*("(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\$])*`)\s*\)\s*;?\s*/.exec(
|
|
393
|
+
remaining,
|
|
394
|
+
);
|
|
395
|
+
if (!match) return false;
|
|
396
|
+
const statement = match[0]!;
|
|
397
|
+
const literal = match[1]!;
|
|
398
|
+
if (literal.startsWith("`") && literal.includes("${")) return false;
|
|
399
|
+
matched = true;
|
|
400
|
+
remaining = remaining.slice(statement.length);
|
|
401
|
+
}
|
|
402
|
+
return matched;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function hasShellRedirectionToken(value: string): boolean {
|
|
406
|
+
return /^(?:[<>]|\d?[<>]|\d?>&\d|\|\|?|&&|;)$/.test(value) || /(?:^|[^\w])-?>/.test(value);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function isSafeRefOrPathspec(value: string): boolean {
|
|
410
|
+
return value.length > 0 && !value.startsWith("-") && !/[\0\n\r]/.test(value) && !hasShellRedirectionToken(value);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function isAllowedGitReplayCommand(args: readonly string[]): boolean {
|
|
414
|
+
const subcommand = args[0];
|
|
415
|
+
const rest = args.slice(1);
|
|
416
|
+
if (subcommand === "status") return rest.every(arg => ["--short", "--porcelain", "--branch"].includes(arg));
|
|
417
|
+
if (subcommand === "rev-parse" || subcommand === "merge-base")
|
|
418
|
+
return rest.length > 0 && rest.every(isSafeRefOrPathspec);
|
|
419
|
+
if (subcommand !== "diff" && subcommand !== "show" && subcommand !== "log") return false;
|
|
420
|
+
let pathspecMode = false;
|
|
421
|
+
for (const arg of rest) {
|
|
422
|
+
if (arg === "--") {
|
|
423
|
+
pathspecMode = true;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (pathspecMode) {
|
|
427
|
+
if (!isSafeRefOrPathspec(arg)) return false;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (["--stat", "--name-only", "--oneline", "--no-ext-diff"].includes(arg)) continue;
|
|
431
|
+
if (!isSafeRefOrPathspec(arg)) return false;
|
|
432
|
+
}
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function isBareExecutableName(value: string): boolean {
|
|
437
|
+
// The allowlist is keyed on the basename, but the raw command[0] is what gets spawned.
|
|
438
|
+
// Reject path-qualified or case-spoofed executables (e.g. ./git, /tmp/npm, scripts/node, GIT)
|
|
439
|
+
// so an attacker-controlled binary cannot impersonate a trusted tool.
|
|
440
|
+
return (
|
|
441
|
+
value.length > 0 &&
|
|
442
|
+
!value.includes("/") &&
|
|
443
|
+
!value.includes("\\") &&
|
|
444
|
+
value === path.basename(value) &&
|
|
445
|
+
value === value.toLowerCase()
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function isAllowedCliReplayCommand(command: readonly string[]): boolean {
|
|
450
|
+
if (
|
|
451
|
+
command.length === 0 ||
|
|
452
|
+
command.some(arg => arg.trim() !== arg || arg.length === 0 || hasShellRedirectionToken(arg))
|
|
453
|
+
)
|
|
454
|
+
return false;
|
|
455
|
+
if (!isBareExecutableName(command[0]!)) return false;
|
|
456
|
+
const executable = basenameCommand(command[0]!);
|
|
457
|
+
const args = command.slice(1);
|
|
458
|
+
if (executable === "bun" || executable === "node") {
|
|
459
|
+
if (args.length === 1 && args[0] === "--version") return true;
|
|
460
|
+
return args.length === 2 && args[0] === "-e" && isDeterministicConsoleLogReplay(args[1]!);
|
|
461
|
+
}
|
|
462
|
+
if (executable === "npm" || executable === "pnpm" || executable === "yarn") {
|
|
463
|
+
return (args.length === 1 && args[0] === "--version") || (args.length === 1 && args[0] === "list");
|
|
464
|
+
}
|
|
465
|
+
if (executable === "git") return isAllowedGitReplayCommand(args);
|
|
466
|
+
if (executable === "gjc") return args.length === 1 && ["read", "status"].includes(args[0] ?? "");
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
function summarizeBlockedCliReplayCommand(command: readonly string[]): string {
|
|
470
|
+
const executable = command[0] ? basenameCommand(command[0]) : "<missing>";
|
|
471
|
+
const argCount = Math.max(0, command.length - 1);
|
|
472
|
+
return `${JSON.stringify(executable)} with ${argCount} arg${argCount === 1 ? "" : "s"}`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function cliReplayAllowlistDescription(): string {
|
|
476
|
+
return [
|
|
477
|
+
'`bun --version`, `node --version`, or deterministic `bun/node -e "console.log(...)"`',
|
|
478
|
+
"`npm|pnpm|yarn --version` or `npm|pnpm|yarn list`",
|
|
479
|
+
"read-only `git status|rev-parse|merge-base|diff|show|log` with safe args",
|
|
480
|
+
"`gjc read` or `gjc status`",
|
|
481
|
+
].join("; ");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function resolveCliReplayCommand(command: string[]): string[] {
|
|
485
|
+
if (basenameCommand(command[0]!) === "bun") return [process.execPath, ...command.slice(1)];
|
|
486
|
+
return command;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function resolveUnderCwd(cwd: string, replayCwd: unknown, fieldName: string): string {
|
|
490
|
+
const relative = replayCwd === undefined ? "." : nonEmptyString(replayCwd);
|
|
491
|
+
if (!relative) throw new Error(`qualityGate ${fieldName}.cwd must be a non-empty string when provided`);
|
|
492
|
+
const root = path.resolve(cwd);
|
|
493
|
+
const resolved = path.resolve(root, relative);
|
|
494
|
+
const relativeToRoot = path.relative(root, resolved);
|
|
495
|
+
if (relativeToRoot === ".." || relativeToRoot.startsWith(`..${path.sep}`) || path.isAbsolute(relativeToRoot)) {
|
|
496
|
+
throw new Error(`qualityGate ${fieldName}.cwd must resolve under the repository cwd`);
|
|
497
|
+
}
|
|
498
|
+
return resolved;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function buildCliReplayEnv(value: unknown, fieldName: string): Record<string, string> {
|
|
502
|
+
const env: Record<string, string> = { ...CLI_REPLAY_ENV_BASE };
|
|
503
|
+
if (value === undefined) return env;
|
|
504
|
+
const object = requireQualityGateObject(value, `${fieldName}.env`);
|
|
505
|
+
for (const [key, envValue] of Object.entries(object)) {
|
|
506
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(key))
|
|
507
|
+
throw new Error(`qualityGate ${fieldName}.env.${key} must be an uppercase environment key`);
|
|
508
|
+
if (CLI_REPLAY_DANGEROUS_ENV_NAME_PATTERN.test(key) || !CLI_REPLAY_SAFE_ENV_NAMES.has(key)) {
|
|
509
|
+
throw new Error(`qualityGate ${fieldName}.env.${key} is not in the CLI replay safe environment allowlist`);
|
|
510
|
+
}
|
|
511
|
+
if (typeof envValue !== "string") throw new Error(`qualityGate ${fieldName}.env.${key} must be a string`);
|
|
512
|
+
env[key] = envValue;
|
|
513
|
+
}
|
|
514
|
+
return env;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function normalizeCliReplayOutput(value: string, cwd: string): string {
|
|
518
|
+
let normalized = value.replace(ANSI_ESCAPE_PATTERN, "").replace(/\r\n?/g, "\n");
|
|
519
|
+
const home = process.env.HOME;
|
|
520
|
+
const replacements: Array<[RegExp, string]> = [
|
|
521
|
+
[/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\b/g, "<TIMESTAMP>"],
|
|
522
|
+
[/\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi, "<UUID>"],
|
|
523
|
+
[/\b[0-9a-f]{7,}\b/gi, "<HASH>"],
|
|
524
|
+
[/(?:\/private)?\/var\/folders\/[^\s"']+|\/tmp\/[^\s"']+|\/var\/tmp\/[^\s"']+/g, "<TMP>"],
|
|
525
|
+
];
|
|
526
|
+
for (const candidate of [path.resolve(cwd), home]) {
|
|
527
|
+
if (!candidate) continue;
|
|
528
|
+
const escaped = candidate.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
529
|
+
normalized = normalized.replace(new RegExp(escaped, "g"), candidate === home ? "<HOME>" : "<CWD>");
|
|
530
|
+
}
|
|
531
|
+
for (const [pattern, replacement] of replacements) normalized = normalized.replace(pattern, replacement);
|
|
532
|
+
const lines = normalized.split("\n").map(line => line.replace(/[ \t]+$/g, ""));
|
|
533
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
534
|
+
return lines.join("\n");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export async function readCliReplayRecord(cwd: string, row: JsonObject, fieldName: string): Promise<JsonObject | null> {
|
|
538
|
+
const inline = qualityGateObject(row.replay) ?? (row.kind === "cli-replay" ? row : null);
|
|
539
|
+
if (inline) return inline;
|
|
540
|
+
if (!evidenceKindMatches(normalizedEvidenceKind(row), ["cli-replay", "command-replay"])) return null;
|
|
541
|
+
const bytes = await readArtifactBytes(cwd, row, fieldName);
|
|
542
|
+
if (!bytes) return null;
|
|
543
|
+
try {
|
|
544
|
+
return requireQualityGateObject(JSON.parse(bytes.toString("utf8")), `${fieldName}.replay`);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
throw new Error(`qualityGate ${fieldName} CLI replay artifact must be valid JSON: ${String(error)}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function parseCliReplayRecord(
|
|
551
|
+
record: JsonObject,
|
|
552
|
+
fieldName: string,
|
|
553
|
+
): {
|
|
554
|
+
command: string[];
|
|
555
|
+
replayCwd: unknown;
|
|
556
|
+
env: Record<string, string>;
|
|
557
|
+
timeoutMs: number;
|
|
558
|
+
expectedExitCode: number;
|
|
559
|
+
recordedStdout: string;
|
|
560
|
+
invariants: JsonObject[];
|
|
561
|
+
} {
|
|
562
|
+
if (record.schemaVersion !== 1) throw new Error(`qualityGate ${fieldName}.schemaVersion must be 1`);
|
|
563
|
+
if (record.kind !== "cli-replay") throw new Error(`qualityGate ${fieldName}.kind must be cli-replay`);
|
|
564
|
+
if (record.command !== undefined && typeof record.command === "string") {
|
|
565
|
+
throw new Error(`qualityGate ${fieldName}.command must be an argv string array, not a shell string`);
|
|
566
|
+
}
|
|
567
|
+
const command = nonEmptyStringArray(record.command);
|
|
568
|
+
if (!command) throw new Error(`qualityGate ${fieldName}.command must be a non-empty string array`);
|
|
569
|
+
if (record.replaySafe !== true)
|
|
570
|
+
throw new Error(`qualityGate ${fieldName}.replaySafe must be true before CLI replay executes`);
|
|
571
|
+
if (!isAllowedCliReplayCommand(command)) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
`qualityGate ${fieldName}.command is not in the conservative CLI replay allowlist; command ${summarizeBlockedCliReplayCommand(command)} is blocked. Allowed replay commands: ${cliReplayAllowlistDescription()}. For other commands, provide audited replayExempt metadata with reasonCode, reason, approvedBy, and fallbackArtifactRefs that point to a structurally valid fallback artifact.`,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
if (record.normalization !== undefined && record.normalization !== "default") {
|
|
577
|
+
throw new Error(`qualityGate ${fieldName}.normalization must be default when provided`);
|
|
578
|
+
}
|
|
579
|
+
if (typeof record.recordedStdout !== "string")
|
|
580
|
+
throw new Error(`qualityGate ${fieldName}.recordedStdout must be a string`);
|
|
581
|
+
if (record.recordedStderr !== undefined && typeof record.recordedStderr !== "string") {
|
|
582
|
+
throw new Error(`qualityGate ${fieldName}.recordedStderr must be a string when provided`);
|
|
583
|
+
}
|
|
584
|
+
const expectedExitCode = record.expectedExitCode === undefined ? 0 : record.expectedExitCode;
|
|
585
|
+
if (typeof expectedExitCode !== "number" || !Number.isInteger(expectedExitCode)) {
|
|
586
|
+
throw new Error(`qualityGate ${fieldName}.expectedExitCode must be an integer`);
|
|
587
|
+
}
|
|
588
|
+
const invariants =
|
|
589
|
+
record.invariants === undefined ? [] : requireObjectArray(record.invariants, `${fieldName}.invariants`);
|
|
590
|
+
return {
|
|
591
|
+
command: command.map(item => item.trim()),
|
|
592
|
+
replayCwd: record.cwd,
|
|
593
|
+
env: buildCliReplayEnv(record.env, fieldName),
|
|
594
|
+
timeoutMs: clampCliReplayTimeout(record.timeoutMs),
|
|
595
|
+
expectedExitCode,
|
|
596
|
+
recordedStdout: record.recordedStdout,
|
|
597
|
+
invariants,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function isMeaningfulCliReplayInvariant(invariant: JsonObject, stdout: string, fieldName: string): boolean {
|
|
602
|
+
const type = requiredStringField(invariant, "type", fieldName);
|
|
603
|
+
const value = requiredStringField(invariant, "value", fieldName);
|
|
604
|
+
if (type === "substring") return value.trim().length >= 4 && stdout.includes(value);
|
|
605
|
+
if (type === "regex") {
|
|
606
|
+
const flags = invariant.flags === undefined ? "" : requiredStringField(invariant, "flags", fieldName);
|
|
607
|
+
if (!/^[im]*$/.test(flags)) throw new Error(`qualityGate ${fieldName}.flags may only contain i and m`);
|
|
608
|
+
const expression = new RegExp(value, flags);
|
|
609
|
+
if (expression.test("") || expression.test("gjc-replay-random-nonce-7f3a9c")) return false;
|
|
610
|
+
const match = expression.exec(stdout);
|
|
611
|
+
return match !== null && match[0].length >= 4;
|
|
612
|
+
}
|
|
613
|
+
if (type === "not_substring") return false;
|
|
614
|
+
throw new Error(`qualityGate ${fieldName}.type must be substring, regex, or not_substring`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function validateCliReplayInvariants(invariants: JsonObject[], stdout: string, fieldName: string): boolean {
|
|
618
|
+
let meaningfulPositiveInvariant = false;
|
|
619
|
+
for (const [index, invariant] of invariants.entries()) {
|
|
620
|
+
const invariantField = `${fieldName}.invariants[${index}]`;
|
|
621
|
+
const type = requiredStringField(invariant, "type", invariantField);
|
|
622
|
+
const value = requiredStringField(invariant, "value", invariantField);
|
|
623
|
+
if (type === "not_substring") {
|
|
624
|
+
if (stdout.includes(value))
|
|
625
|
+
throw new Error(`qualityGate ${invariantField} not_substring invariant matched stdout`);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (!isMeaningfulCliReplayInvariant(invariant, stdout, invariantField)) {
|
|
629
|
+
throw new Error(`qualityGate ${invariantField} must be a meaningful positive invariant that matches stdout`);
|
|
630
|
+
}
|
|
631
|
+
meaningfulPositiveInvariant = true;
|
|
632
|
+
}
|
|
633
|
+
return meaningfulPositiveInvariant;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function collectCliReplayOutput(
|
|
637
|
+
stream: ReadableStream<Uint8Array> | null,
|
|
638
|
+
): Promise<{ text: string; truncated: boolean }> {
|
|
639
|
+
if (!stream) return { text: "", truncated: false };
|
|
640
|
+
const reader = stream.getReader();
|
|
641
|
+
const chunks: Buffer[] = [];
|
|
642
|
+
let size = 0;
|
|
643
|
+
let truncated = false;
|
|
644
|
+
try {
|
|
645
|
+
while (true) {
|
|
646
|
+
const { done, value } = await reader.read();
|
|
647
|
+
if (done) break;
|
|
648
|
+
if (size < CLI_REPLAY_MAX_OUTPUT_BYTES) {
|
|
649
|
+
const remaining = CLI_REPLAY_MAX_OUTPUT_BYTES - size;
|
|
650
|
+
const chunk = Buffer.from(value.subarray(0, remaining));
|
|
651
|
+
chunks.push(chunk);
|
|
652
|
+
size += chunk.length;
|
|
653
|
+
}
|
|
654
|
+
if (value.length > 0 && size >= CLI_REPLAY_MAX_OUTPUT_BYTES) {
|
|
655
|
+
truncated = true;
|
|
656
|
+
await reader.cancel().catch(() => undefined);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
} finally {
|
|
661
|
+
reader.releaseLock();
|
|
662
|
+
}
|
|
663
|
+
return { text: Buffer.concat(chunks).toString("utf8"), truncated };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export interface ReplayProcessHandle {
|
|
667
|
+
readonly exited: Promise<number>;
|
|
668
|
+
kill(signal?: number | NodeJS.Signals): void;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export async function waitForReplayProcessWithTimeout(
|
|
672
|
+
process: ReplayProcessHandle,
|
|
673
|
+
timeoutMs: number,
|
|
674
|
+
graceMs = 2000,
|
|
675
|
+
): Promise<number> {
|
|
676
|
+
let timeoutTimer: NodeJS.Timeout | undefined;
|
|
677
|
+
let graceTimer: NodeJS.Timeout | undefined;
|
|
678
|
+
const timedOut = Symbol("timedOut");
|
|
679
|
+
const timeout = new Promise<typeof timedOut>(resolve => {
|
|
680
|
+
timeoutTimer = setTimeout(() => resolve(timedOut), timeoutMs);
|
|
681
|
+
});
|
|
682
|
+
const first = await Promise.race([process.exited, timeout]);
|
|
683
|
+
if (first !== timedOut) {
|
|
684
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
685
|
+
return first;
|
|
686
|
+
}
|
|
687
|
+
process.kill("SIGTERM");
|
|
688
|
+
const killed = Symbol("killed");
|
|
689
|
+
const grace = new Promise<typeof killed>(resolve => {
|
|
690
|
+
graceTimer = setTimeout(() => {
|
|
691
|
+
process.kill("SIGKILL");
|
|
692
|
+
resolve(killed);
|
|
693
|
+
}, graceMs);
|
|
694
|
+
});
|
|
695
|
+
await Promise.race([process.exited, grace]);
|
|
696
|
+
await process.exited.catch(() => undefined);
|
|
697
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
698
|
+
if (graceTimer) clearTimeout(graceTimer);
|
|
699
|
+
throw new Error("timeout");
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export async function validateReplayExemptFallback(
|
|
703
|
+
cwd: string,
|
|
704
|
+
record: JsonObject,
|
|
705
|
+
fieldName: string,
|
|
706
|
+
artifactRefs: Map<string, JsonObject>,
|
|
707
|
+
options: { surfaceFamily: SurfaceFamily; live: boolean },
|
|
708
|
+
): Promise<boolean> {
|
|
709
|
+
const exempt = qualityGateObject(record.replayExempt);
|
|
710
|
+
if (!exempt) return false;
|
|
711
|
+
const reasonCode = requiredStringField(exempt, "reasonCode", `${fieldName}.replayExempt`);
|
|
712
|
+
if (!CLI_REPLAY_EXEMPT_REASON_CODE_SET.has(reasonCode))
|
|
713
|
+
throw new Error(
|
|
714
|
+
`qualityGate ${fieldName}.replayExempt.reasonCode must be one of: ${CLI_REPLAY_EXEMPT_REASON_CODE_LIST}`,
|
|
715
|
+
);
|
|
716
|
+
const reason = requiredStringField(exempt, "reason", `${fieldName}.replayExempt`);
|
|
717
|
+
if (!isSubstantiveEvidence(reason) || reason.length < 30)
|
|
718
|
+
throw new Error(`qualityGate ${fieldName}.replayExempt.reason must be audited and substantive`);
|
|
719
|
+
requiredStringField(exempt, "approvedBy", `${fieldName}.replayExempt`);
|
|
720
|
+
const fallbackRefs = requireStringLinks(
|
|
721
|
+
exempt.fallbackArtifactRefs,
|
|
722
|
+
`${fieldName}.replayExempt.fallbackArtifactRefs`,
|
|
723
|
+
);
|
|
724
|
+
requireResolvedLinks(fallbackRefs, artifactRefs, `${fieldName}.replayExempt.fallbackArtifactRefs`);
|
|
725
|
+
let validFallback = false;
|
|
726
|
+
for (const fallbackRef of fallbackRefs) {
|
|
727
|
+
if (fallbackRef === requiredStringField(record, "id", fieldName)) {
|
|
728
|
+
throw new Error(`qualityGate ${fieldName}.replayExempt fallback must not reference the replay record itself`);
|
|
729
|
+
}
|
|
730
|
+
const fallback = artifactRefs.get(fallbackRef)!;
|
|
731
|
+
if (await validateStructuralArtifact(cwd, fallback, `executorQa.artifactRefs.${fallbackRef}`, options))
|
|
732
|
+
validFallback = true;
|
|
733
|
+
}
|
|
734
|
+
if (!validFallback)
|
|
735
|
+
throw new Error(
|
|
736
|
+
`qualityGate ${fieldName}.replayExempt requires at least one structurally-valid fallback artifact`,
|
|
737
|
+
);
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
export async function validateCliReplay(
|
|
741
|
+
cwd: string,
|
|
742
|
+
row: JsonObject,
|
|
743
|
+
fieldName: string,
|
|
744
|
+
options: { live: boolean },
|
|
745
|
+
): Promise<boolean> {
|
|
746
|
+
const record = await readCliReplayRecord(cwd, row, fieldName);
|
|
747
|
+
if (!record) return false;
|
|
748
|
+
if (record.replayExempt !== undefined) {
|
|
749
|
+
throw new Error(
|
|
750
|
+
`qualityGate ${fieldName}.replayExempt can only be validated from surfaceEvidence with fallback context`,
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
void options.live;
|
|
754
|
+
const replay = parseCliReplayRecord(record, fieldName);
|
|
755
|
+
const replayCwd = resolveUnderCwd(cwd, replay.replayCwd, fieldName);
|
|
756
|
+
const process = Bun.spawn(resolveCliReplayCommand(replay.command), {
|
|
757
|
+
cwd: replayCwd,
|
|
758
|
+
env: replay.env,
|
|
759
|
+
stdout: "pipe",
|
|
760
|
+
stderr: "pipe",
|
|
761
|
+
});
|
|
762
|
+
try {
|
|
763
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
764
|
+
collectCliReplayOutput(process.stdout),
|
|
765
|
+
collectCliReplayOutput(process.stderr),
|
|
766
|
+
waitForReplayProcessWithTimeout(process, replay.timeoutMs),
|
|
767
|
+
]);
|
|
768
|
+
if (stdout.truncated || stderr.truncated)
|
|
769
|
+
throw new Error(`qualityGate ${fieldName} CLI replay output exceeded 1 MiB buffer cap`);
|
|
770
|
+
if (exitCode !== replay.expectedExitCode) {
|
|
771
|
+
throw new Error(
|
|
772
|
+
`qualityGate ${fieldName} CLI replay exit code ${exitCode} did not match expected ${replay.expectedExitCode}`,
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
const actualStdout = normalizeCliReplayOutput(stdout.text, cwd);
|
|
776
|
+
const recordedStdout = normalizeCliReplayOutput(replay.recordedStdout, cwd);
|
|
777
|
+
if (!replay.invariants.length || !validateCliReplayInvariants(replay.invariants, actualStdout, fieldName)) {
|
|
778
|
+
if (actualStdout !== recordedStdout) {
|
|
779
|
+
throw new Error(
|
|
780
|
+
`qualityGate ${fieldName} CLI replay stdout did not match recordedStdout after normalization`,
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return true;
|
|
785
|
+
} catch (error) {
|
|
786
|
+
if (error instanceof Error && error.message === "timeout") {
|
|
787
|
+
throw new Error(`qualityGate ${fieldName} CLI replay timed out after ${replay.timeoutMs}ms`);
|
|
788
|
+
}
|
|
789
|
+
throw error;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
async function hasLiveProofPresence(
|
|
794
|
+
cwd: string,
|
|
795
|
+
row: JsonObject,
|
|
796
|
+
fieldName: string,
|
|
797
|
+
family: SurfaceFamily,
|
|
798
|
+
): Promise<boolean> {
|
|
799
|
+
if (await hasExistingNonEmptyArtifact(cwd, row.path)) return true;
|
|
800
|
+
if (family === "cli") {
|
|
801
|
+
const record = await readCliReplayRecord(cwd, row, fieldName);
|
|
802
|
+
if (record) return true;
|
|
803
|
+
}
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export async function validateLiveSurfaceProofPresence(
|
|
808
|
+
cwd: string,
|
|
809
|
+
family: SurfaceFamily,
|
|
810
|
+
artifactIds: string[],
|
|
811
|
+
artifactRefs: Map<string, JsonObject>,
|
|
812
|
+
): Promise<void> {
|
|
813
|
+
if (!isLiveSurfaceFamily(family)) return;
|
|
814
|
+
for (const artifactId of artifactIds) {
|
|
815
|
+
if (
|
|
816
|
+
await hasLiveProofPresence(cwd, artifactRefs.get(artifactId)!, `executorQa.artifactRefs.${artifactId}`, family)
|
|
817
|
+
)
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
throw new Error(
|
|
821
|
+
`qualityGate ${artifactIds.map(id => `executorQa.artifactRefs.${id}`).join(", ")} must reference a live proof artifact, structural capture, or CLI replay; inlineEvidence and typed verifiedReceipt do not prove live surfaces`,
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
export async function validateSurfaceStructuralRequirement(
|
|
825
|
+
cwd: string,
|
|
826
|
+
family: SurfaceFamily,
|
|
827
|
+
artifactIds: string[],
|
|
828
|
+
artifactRefs: Map<string, JsonObject>,
|
|
829
|
+
fieldName: string,
|
|
830
|
+
): Promise<void> {
|
|
831
|
+
if (family !== "web" && family !== "native") return;
|
|
832
|
+
let hasScreenshot = false;
|
|
833
|
+
let hasAutomation = false;
|
|
834
|
+
let hasPty = false;
|
|
835
|
+
for (const artifactId of artifactIds) {
|
|
836
|
+
const artifact = artifactRefs.get(artifactId)!;
|
|
837
|
+
const kind = structuralArtifactKind(artifact);
|
|
838
|
+
if (!kind) continue;
|
|
839
|
+
const valid = await validateStructuralArtifact(cwd, artifact, `executorQa.artifactRefs.${artifactId}`, {
|
|
840
|
+
surfaceFamily: family,
|
|
841
|
+
live: true,
|
|
842
|
+
});
|
|
843
|
+
if (kind === "screenshot" && valid) hasScreenshot = true;
|
|
844
|
+
if (kind === "automation" && valid) hasAutomation = true;
|
|
845
|
+
if (kind === "pty" && valid) hasPty = true;
|
|
846
|
+
}
|
|
847
|
+
if (family === "web" && (!hasScreenshot || !hasAutomation)) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
`qualityGate ${fieldName} for GUI/web surfaces must include a valid automation transcript and non-uniform screenshot`,
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
if (family === "native" && !hasScreenshot && !hasAutomation && !hasPty) {
|
|
853
|
+
throw new Error(
|
|
854
|
+
`qualityGate ${fieldName} for native surfaces must include a valid screenshot, PTY capture, or app-automation transcript`,
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export async function validateArtifactProof(
|
|
860
|
+
cwd: string,
|
|
861
|
+
row: JsonObject,
|
|
862
|
+
fieldName: string,
|
|
863
|
+
options: { surfaceFamily: SurfaceFamily; live: boolean },
|
|
864
|
+
): Promise<void> {
|
|
865
|
+
if (await hasExistingNonEmptyArtifact(cwd, row.path)) return;
|
|
866
|
+
if (await validateStructuralArtifact(cwd, row, fieldName, options)) return;
|
|
867
|
+
if (options.surfaceFamily === "cli" && (await validateCliReplay(cwd, row, fieldName, { live: options.live })))
|
|
868
|
+
return;
|
|
869
|
+
if (!options.live && (hasTypedVerifiedReceipt(row.verifiedReceipt) || hasTypedVerifiedReceipt(row.receipt))) return;
|
|
870
|
+
const proofLabel = options.live
|
|
871
|
+
? "a live proof artifact, structural capture, or CLI replay; inlineEvidence and typed verifiedReceipt do not prove live surfaces"
|
|
872
|
+
: "an existing non-empty artifact path or a typed verifiedReceipt; inlineEvidence alone is not sufficient";
|
|
873
|
+
throw new Error(`qualityGate ${fieldName} must reference ${proofLabel}`);
|
|
874
|
+
}
|