@sayknow-cli/coding-agent 0.4.7 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/cli.d.ts +9 -1
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +34 -4
- package/dist/types/config/model-profile-contract.d.ts +40 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +156 -10
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/extensibility/extensions/types.d.ts +43 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +33 -2
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +22 -9
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/tool-status-header.d.ts +4 -2
- package/dist/types/modes/components/welcome.d.ts +5 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +13 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +1 -1
- package/dist/types/runtime-mcp/manager.d.ts +9 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +13 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +9 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +52 -4
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +106 -0
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +33 -9
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +286 -12
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/host/control/operations.d.ts +3 -2
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/query/handlers.d.ts +14 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/sdk/startup-capability.d.ts +9 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +83 -24
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +70 -10
- package/dist/types/session/internal/managed-session-storage.d.ts +34 -12
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +89 -18
- package/dist/types/session/session-storage.d.ts +31 -0
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/skc-runtime/deep-interview-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/skc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/skc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/skc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/skc-runtime/ralplan-runtime.d.ts +112 -1
- package/dist/types/skc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/skc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/skc-runtime/state-writer.d.ts +5 -1
- package/dist/types/skc-runtime/team-launch.d.ts +1 -1
- package/dist/types/skc-runtime/team-runtime.d.ts +9 -2
- package/dist/types/skc-runtime/team-store.d.ts +12 -0
- package/dist/types/skc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +6 -5
- package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +28 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +43 -5
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +133 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +22 -9
- package/dist/types/tools/image-gen.d.ts +63 -1
- package/dist/types/tools/index.d.ts +7 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +23 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -12
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/compile-args.ts +0 -1
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-docs-index.ts +4 -1
- package/scripts/generate-sdk-operation-inventory.ts +9 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/scripts/verify-skc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +58 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/args.ts +16 -100
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +6 -38
- package/src/cli/read-cli.ts +2 -1
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +133 -11
- package/src/commands/daemon.ts +22 -5
- package/src/commands/deep-interview.ts +26 -1
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/sdk.ts +134 -17
- package/src/commands/team.ts +1 -1
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/keybindings.ts +158 -25
- package/src/config/model-profile-contract.ts +174 -0
- package/src/config/model-profiles.ts +51 -10
- package/src/config/model-registry.ts +24 -4
- package/src/config/model-resolver.ts +2 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +157 -10
- package/src/config/settings.ts +254 -108
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/skc/skills/deep-interview/SKILL.md +147 -24
- package/src/defaults/skc/skills/ralplan/SKILL.md +91 -34
- package/src/defaults/skc/skills/team/SKILL.md +1 -1
- package/src/defaults/skc/skills/ultragoal/SKILL.md +68 -17
- package/src/discovery/ssh.ts +12 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/eval/py/executor.ts +37 -13
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +42 -4
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/extensibility/shared-events.ts +5 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +2 -1
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/internal-urls/docs-index.generated.ts +17 -16
- package/src/internal-urls/local-protocol.ts +87 -25
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +120 -42
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +52 -0
- package/src/modes/acp/acp-agent.ts +647 -83
- package/src/modes/acp/acp-event-mapper.ts +106 -12
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/sayknow-pet-widget.ts +20 -8
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +26 -8
- package/src/modes/components/welcome.ts +50 -23
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +101 -54
- package/src/modes/controllers/event-controller.ts +119 -27
- package/src/modes/controllers/extension-ui-controller.ts +23 -12
- package/src/modes/controllers/input-controller.ts +100 -8
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +4 -1
- package/src/modes/interactive-mode.ts +201 -60
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/runtime-init.ts +20 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +3 -3
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +6 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +99 -4
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +17 -3
- package/src/prompts/agents/critic.md +17 -2
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -3
- package/src/prompts/tools/cron.md +3 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/discoverable-tool-metadata.ts +1 -1
- package/src/runtime-mcp/json-rpc.ts +28 -11
- package/src/runtime-mcp/manager.ts +77 -59
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/redaction.ts +30 -0
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/runtime-mcp/transports/http.ts +63 -22
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/lifecycle.ts +293 -43
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +22 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +10 -2
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +793 -121
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/lifecycle-commands.ts +63 -10
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +87 -25
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/prompt-reconciliation.ts +240 -0
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +34 -9
- package/src/sdk/bus/telegram-daemon-control.ts +161 -40
- package/src/sdk/bus/telegram-daemon.ts +3649 -593
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +8 -2
- package/src/sdk/host/control/operations.ts +3 -2
- package/src/sdk/host/host.ts +27 -9
- package/src/sdk/host/query/handlers.ts +125 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +145 -0
- package/src/sdk/protocol/operation-registry.ts +24 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +69 -8
- package/src/sdk/startup-capability.ts +15 -1
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +1112 -314
- package/src/session/blob-store.ts +25 -7
- package/src/session/btw-contract.ts +59 -0
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +1426 -259
- package/src/session/internal/managed-session-storage.ts +414 -238
- package/src/session/internal/native-publish-outcome.ts +271 -0
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +2079 -469
- package/src/session/session-storage.ts +151 -11
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +49 -5
- package/src/setup/provider-presets.json +2 -1
- package/src/skc-runtime/deep-interview-runtime.ts +303 -36
- package/src/skc-runtime/deep-interview-stage.ts +1016 -0
- package/src/skc-runtime/deep-interview-state.ts +8 -5
- package/src/skc-runtime/gc-render.ts +1 -0
- package/src/skc-runtime/gc-runtime.ts +11 -1
- package/src/skc-runtime/linux-proc.ts +70 -36
- package/src/skc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/skc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/skc-runtime/ralplan-runtime.ts +1057 -75
- package/src/skc-runtime/repository-binding.ts +267 -0
- package/src/skc-runtime/session-state-sidecar.ts +25 -9
- package/src/skc-runtime/state-runtime.ts +50 -12
- package/src/skc-runtime/state-writer.ts +68 -56
- package/src/skc-runtime/team-launch.ts +34 -5
- package/src/skc-runtime/team-runtime.ts +41 -1
- package/src/skc-runtime/team-store.ts +32 -0
- package/src/skc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/skc-runtime/team-workers.ts +1 -0
- package/src/skc-runtime/tmux-common.ts +13 -2
- package/src/skc-runtime/tmux-owner-isolation.ts +20 -1
- package/src/skc-runtime/tmux-sessions.ts +45 -6
- package/src/skc-runtime/ultragoal-guard.ts +93 -15
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +123 -0
- package/src/skc-runtime/ultragoal-runtime.ts +469 -38
- package/src/skc-runtime/workflow-manifest.generated.json +195 -5
- package/src/skc-runtime/workflow-manifest.ts +67 -3
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +104 -12
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/task/executor.ts +165 -47
- package/src/task/index.ts +123 -15
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/receipt.ts +3 -0
- package/src/task/render.ts +45 -26
- package/src/task/skc-command.ts +2 -2
- package/src/task/types.ts +55 -10
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/ask.ts +1 -1
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +127 -154
- package/src/tools/image-gen.ts +476 -23
- package/src/tools/index.ts +7 -1
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1544 -310
- package/src/tools/resource-gc.ts +665 -125
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/src/tools/tool-result.ts +14 -1
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +140 -27
- package/src/web/scrapers/docs-rs.ts +30 -5
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
- package/src/web/search/providers/codex.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +4 -1
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
4
|
+
import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
|
|
5
|
+
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
6
|
+
import { applyAmbiguityFloorToEnvelope } from "./deep-interview-ambiguity";
|
|
7
|
+
import {
|
|
8
|
+
assertDeepInterviewEnvelopeInputLimits,
|
|
9
|
+
assertDeepInterviewIntentManifest,
|
|
10
|
+
assertDeepInterviewStructuredResponseWithinLimit,
|
|
11
|
+
mergeDeepInterviewEnvelope,
|
|
12
|
+
normalizeDeepInterviewEnvelope,
|
|
13
|
+
} from "./deep-interview-state";
|
|
14
|
+
import { sessionStateDir } from "./session-layout";
|
|
15
|
+
import { resolveSkcSessionForWrite, SessionResolutionError, writeSessionActivityMarker } from "./session-resolution";
|
|
16
|
+
import { runNativeStateCommand } from "./state-runtime";
|
|
17
|
+
import {
|
|
18
|
+
persistedStateRevision,
|
|
19
|
+
readExistingStateForMutation,
|
|
20
|
+
StateWriteConflictError,
|
|
21
|
+
withWorkflowStateLock,
|
|
22
|
+
workflowEnvelopeContentSha256,
|
|
23
|
+
writeGuardedWorkflowEnvelopeAtomic,
|
|
24
|
+
writeJsonAtomic,
|
|
25
|
+
} from "./state-writer";
|
|
26
|
+
import { CommandError, flagValue, hasFlag, isPlainObject } from "./workflow-cli-common";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Staged JSON transitions for deep-interview state (`skc deep-interview stage|check|apply|discard`).
|
|
30
|
+
*
|
|
31
|
+
* Design contract (post-#3040 revert; deliberately NOT the typed-flag surface):
|
|
32
|
+
* - The payload is one JSON document supplied whole (`--input '<json>'` or `@file`),
|
|
33
|
+
* merged into current state through the same lossless envelope merge every other
|
|
34
|
+
* sanctioned deep-interview writer uses. There is no per-field flag grammar.
|
|
35
|
+
* - Exactly one pending draft exists per session at a fixed session-scoped path,
|
|
36
|
+
* so no `--draft-id` is needed; the session resolves from `SKC_SESSION_ID` (or
|
|
37
|
+
* payload `session_id`), so no identity flags are needed.
|
|
38
|
+
* - The draft records the `state_revision` it was staged against; `apply` enforces
|
|
39
|
+
* that runtime-side (CAS). A stale draft is auto-invalidated with typed recovery
|
|
40
|
+
* guidance — the agent never does revision arithmetic.
|
|
41
|
+
* - `check` dry-runs the identical merge `apply` performs. Validation is core-schema
|
|
42
|
+
* only (envelope shape, bounded input sizes, locked intent-contract immutability);
|
|
43
|
+
* free-form interview fields pass through untouched.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import * as path from "node:path";
|
|
47
|
+
|
|
48
|
+
export const DEEP_INTERVIEW_STAGE_TRANSITIONS = [
|
|
49
|
+
"initialize-context",
|
|
50
|
+
"record-round",
|
|
51
|
+
"update-facts",
|
|
52
|
+
"merge-state",
|
|
53
|
+
] as const;
|
|
54
|
+
export type DeepInterviewStageTransition = (typeof DEEP_INTERVIEW_STAGE_TRANSITIONS)[number];
|
|
55
|
+
|
|
56
|
+
const DRAFT_VERSION = 1;
|
|
57
|
+
const DRAFT_FILE = "deep-interview-draft.json";
|
|
58
|
+
|
|
59
|
+
export type DeepInterviewStageErrorCode =
|
|
60
|
+
| "DI_STAGE_USAGE"
|
|
61
|
+
| "DI_STAGE_INPUT_INVALID"
|
|
62
|
+
| "DI_STAGE_SESSION_REQUIRED"
|
|
63
|
+
| "DI_STAGE_DRAFT_EXISTS"
|
|
64
|
+
| "DI_STAGE_NO_DRAFT"
|
|
65
|
+
| "DI_STAGE_DRAFT_CORRUPT"
|
|
66
|
+
| "DI_STAGE_STATE_MISSING"
|
|
67
|
+
| "DI_STAGE_STATE_CORRUPT"
|
|
68
|
+
| "DI_STAGE_REVISION_CONFLICT"
|
|
69
|
+
| "DI_STAGE_MERGE_REJECTED";
|
|
70
|
+
|
|
71
|
+
export class DeepInterviewStageError extends CommandError {
|
|
72
|
+
constructor(
|
|
73
|
+
readonly code: DeepInterviewStageErrorCode,
|
|
74
|
+
message: string,
|
|
75
|
+
readonly recovery?: string,
|
|
76
|
+
) {
|
|
77
|
+
super(2, message);
|
|
78
|
+
this.name = "DeepInterviewStageError";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface DeepInterviewStageDraft {
|
|
83
|
+
version: typeof DRAFT_VERSION;
|
|
84
|
+
draft_id: string;
|
|
85
|
+
session_id: string;
|
|
86
|
+
transition: DeepInterviewStageTransition;
|
|
87
|
+
/** State revision the draft was staged against; `apply` CAS-checks this. */
|
|
88
|
+
staged_against_revision: number;
|
|
89
|
+
/**
|
|
90
|
+
* Canonical content SHA-256 of the state the draft was staged against. Revision
|
|
91
|
+
* alone cannot catch sanctioned writers that do not bump `state_revision`
|
|
92
|
+
* (seed/spec-persistence), so `apply` requires BOTH to match.
|
|
93
|
+
*/
|
|
94
|
+
staged_against_sha256: string;
|
|
95
|
+
payload: Record<string, unknown>;
|
|
96
|
+
created_at: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface DeepInterviewStageCommandResult {
|
|
100
|
+
status: number;
|
|
101
|
+
stdout?: string;
|
|
102
|
+
stderr?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function deepInterviewDraftPath(cwd: string, sessionId: string): string {
|
|
106
|
+
return path.join(sessionStateDir(cwd, sessionId), DRAFT_FILE);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function statePathFor(cwd: string, sessionId: string): string {
|
|
110
|
+
return path.join(sessionStateDir(cwd, sessionId), "deep-interview-state.json");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// -----------------------------------------------------------------------------
|
|
114
|
+
// Input parsing
|
|
115
|
+
// -----------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
/** Bytes cap for `@file` payloads — conservative for the 100k-char structured-response limit. */
|
|
118
|
+
const MAX_INPUT_FILE_BYTES = 1_000_000;
|
|
119
|
+
|
|
120
|
+
async function parseJsonInput(rawInput: string, cwd: string): Promise<Record<string, unknown>> {
|
|
121
|
+
let text = rawInput;
|
|
122
|
+
if (rawInput.startsWith("@")) {
|
|
123
|
+
const filePath = path.resolve(cwd, rawInput.slice(1));
|
|
124
|
+
try {
|
|
125
|
+
const stat = await fs.stat(filePath);
|
|
126
|
+
if (!stat.isFile()) {
|
|
127
|
+
throw new DeepInterviewStageError(
|
|
128
|
+
"DI_STAGE_INPUT_INVALID",
|
|
129
|
+
`--input file is not a regular file: ${filePath}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (stat.size > MAX_INPUT_FILE_BYTES) {
|
|
133
|
+
throw new DeepInterviewStageError(
|
|
134
|
+
"DI_STAGE_INPUT_INVALID",
|
|
135
|
+
`--input file exceeds ${MAX_INPUT_FILE_BYTES} bytes (${stat.size}); staged payloads are bounded`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
text = await fs.readFile(filePath, "utf-8");
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error instanceof DeepInterviewStageError) throw error;
|
|
141
|
+
throw new DeepInterviewStageError(
|
|
142
|
+
"DI_STAGE_INPUT_INVALID",
|
|
143
|
+
`failed to read --input file ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
let parsed: unknown;
|
|
148
|
+
try {
|
|
149
|
+
parsed = JSON.parse(text);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw new DeepInterviewStageError(
|
|
152
|
+
"DI_STAGE_INPUT_INVALID",
|
|
153
|
+
`--input is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (!isPlainObject(parsed)) {
|
|
157
|
+
throw new DeepInterviewStageError("DI_STAGE_INPUT_INVALID", "--input must be a JSON object");
|
|
158
|
+
}
|
|
159
|
+
return parsed;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Envelope lifecycle fields the runtime owns exclusively. A staged payload may
|
|
164
|
+
* carry interview data only; phase transitions go through their dedicated verbs
|
|
165
|
+
* (`--write`, `skc state handoff/clear`), never through a staged patch.
|
|
166
|
+
*/
|
|
167
|
+
const RUNTIME_OWNED_ENVELOPE_KEYS = [
|
|
168
|
+
"current_phase",
|
|
169
|
+
"active",
|
|
170
|
+
"skill",
|
|
171
|
+
"version",
|
|
172
|
+
"state_revision",
|
|
173
|
+
"source_state_revision",
|
|
174
|
+
"receipt",
|
|
175
|
+
"updated_at",
|
|
176
|
+
"last_applied_draft_id",
|
|
177
|
+
] as const;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Nested `state.*` keys owned by the Round-0 ask recorder. A staged/write
|
|
181
|
+
* payload can never set them: a fabricated contract (missing digest/
|
|
182
|
+
* confirmation binding) would poison state so every later merge fails
|
|
183
|
+
* `invalid intent contract`, bricking the interview until a destructive
|
|
184
|
+
* `clear --force`. The recorder is the only writer that can lock intent.
|
|
185
|
+
*/
|
|
186
|
+
const RECORDER_OWNED_STATE_KEYS = ["intent_contract", "intent_review"] as const;
|
|
187
|
+
|
|
188
|
+
/** Strip runtime-owned keys from a staged payload; returns the ignored key names. */
|
|
189
|
+
function sanitizeStagedPayload(payload: Record<string, unknown>): {
|
|
190
|
+
payload: Record<string, unknown>;
|
|
191
|
+
ignoredKeys: string[];
|
|
192
|
+
} {
|
|
193
|
+
const next = { ...payload };
|
|
194
|
+
const ignoredKeys: string[] = [];
|
|
195
|
+
for (const key of RUNTIME_OWNED_ENVELOPE_KEYS) {
|
|
196
|
+
if (key in next) {
|
|
197
|
+
delete next[key];
|
|
198
|
+
ignoredKeys.push(key);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (isPlainObject(next.state)) {
|
|
202
|
+
const state = { ...(next.state as Record<string, unknown>) };
|
|
203
|
+
for (const key of RECORDER_OWNED_STATE_KEYS) {
|
|
204
|
+
if (key in state) {
|
|
205
|
+
delete state[key];
|
|
206
|
+
ignoredKeys.push(`state.${key}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
next.state = state;
|
|
210
|
+
}
|
|
211
|
+
return { payload: next, ignoredKeys };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Self-heal a poisoned merge base: a persisted `state.intent_contract` that
|
|
216
|
+
* fails canonical validation can only come from a pre-guard poisoned write
|
|
217
|
+
* (the recorder always persists valid manifests). Left in place it makes
|
|
218
|
+
* every merge throw, bricking the interview. Drop it (and any equally
|
|
219
|
+
* unverifiable intent_review) from the base and report the repair.
|
|
220
|
+
*/
|
|
221
|
+
function healPoisonedIntentContract(base: Record<string, unknown>): {
|
|
222
|
+
base: Record<string, unknown>;
|
|
223
|
+
healed: boolean;
|
|
224
|
+
} {
|
|
225
|
+
if (!isPlainObject(base.state)) return { base, healed: false };
|
|
226
|
+
const state = base.state as Record<string, unknown>;
|
|
227
|
+
if (state.intent_contract === undefined) return { base, healed: false };
|
|
228
|
+
try {
|
|
229
|
+
assertDeepInterviewIntentManifest(state.intent_contract);
|
|
230
|
+
return { base, healed: false };
|
|
231
|
+
} catch {
|
|
232
|
+
const healedState = { ...state };
|
|
233
|
+
delete healedState.intent_contract;
|
|
234
|
+
delete healedState.intent_review;
|
|
235
|
+
return { base: { ...base, state: healedState }, healed: true };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function parseTransition(raw: string | undefined): DeepInterviewStageTransition {
|
|
240
|
+
if (!raw || !(DEEP_INTERVIEW_STAGE_TRANSITIONS as readonly string[]).includes(raw)) {
|
|
241
|
+
throw new DeepInterviewStageError(
|
|
242
|
+
"DI_STAGE_USAGE",
|
|
243
|
+
`--for must be one of: ${DEEP_INTERVIEW_STAGE_TRANSITIONS.join(", ")}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return raw as DeepInterviewStageTransition;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// -----------------------------------------------------------------------------
|
|
250
|
+
// Core-schema validation (flexible by design: bounds + envelope shape only)
|
|
251
|
+
// -----------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Validate only what the write gate and durability contract require:
|
|
255
|
+
* JSON-serializable, bounded total size, bounded free-text prose fields, and no
|
|
256
|
+
* attempt to smuggle envelope-reserved keys as interview state. Free-form fields
|
|
257
|
+
* (rounds, facts, notes, anything unknown) pass through — flexibility is the
|
|
258
|
+
* contract; the merge preserves them verbatim.
|
|
259
|
+
*/
|
|
260
|
+
function assertCorePayloadSchema(payload: Record<string, unknown>): void {
|
|
261
|
+
try {
|
|
262
|
+
assertDeepInterviewStructuredResponseWithinLimit(payload);
|
|
263
|
+
assertDeepInterviewEnvelopeInputLimits(normalizeDeepInterviewEnvelope(payload) as Record<string, unknown>);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
throw new DeepInterviewStageError(
|
|
266
|
+
"DI_STAGE_INPUT_INVALID",
|
|
267
|
+
error instanceof Error ? error.message : String(error),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// -----------------------------------------------------------------------------
|
|
273
|
+
// Draft persistence
|
|
274
|
+
// -----------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
type DraftReadResult =
|
|
277
|
+
| { kind: "absent" }
|
|
278
|
+
| { kind: "corrupt"; error: string }
|
|
279
|
+
| { kind: "valid"; draft: DeepInterviewStageDraft };
|
|
280
|
+
|
|
281
|
+
async function readDraft(cwd: string, sessionId: string): Promise<DraftReadResult> {
|
|
282
|
+
const draftPath = deepInterviewDraftPath(cwd, sessionId);
|
|
283
|
+
let raw: string;
|
|
284
|
+
try {
|
|
285
|
+
raw = await fs.readFile(draftPath, "utf-8");
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { kind: "absent" };
|
|
288
|
+
return { kind: "corrupt", error: error instanceof Error ? error.message : String(error) };
|
|
289
|
+
}
|
|
290
|
+
let parsed: unknown;
|
|
291
|
+
try {
|
|
292
|
+
parsed = JSON.parse(raw);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
return { kind: "corrupt", error: error instanceof Error ? error.message : String(error) };
|
|
295
|
+
}
|
|
296
|
+
if (
|
|
297
|
+
!isPlainObject(parsed) ||
|
|
298
|
+
parsed.version !== DRAFT_VERSION ||
|
|
299
|
+
typeof parsed.draft_id !== "string" ||
|
|
300
|
+
typeof parsed.session_id !== "string" ||
|
|
301
|
+
typeof parsed.transition !== "string" ||
|
|
302
|
+
!(DEEP_INTERVIEW_STAGE_TRANSITIONS as readonly string[]).includes(parsed.transition) ||
|
|
303
|
+
typeof parsed.staged_against_revision !== "number" ||
|
|
304
|
+
typeof parsed.staged_against_sha256 !== "string" ||
|
|
305
|
+
!isPlainObject(parsed.payload)
|
|
306
|
+
) {
|
|
307
|
+
return { kind: "corrupt", error: "draft file does not match the staged-draft shape" };
|
|
308
|
+
}
|
|
309
|
+
return { kind: "valid", draft: parsed as unknown as DeepInterviewStageDraft };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Remove the session draft only when it still holds `draftId`. Prevents a stale
|
|
314
|
+
* handle (e.g. a concurrent apply/discard that already consumed the draft and a
|
|
315
|
+
* new one was staged) from deleting a draft it never read.
|
|
316
|
+
*/
|
|
317
|
+
async function removeDraftIfMatches(cwd: string, sessionId: string, draftId?: string): Promise<boolean> {
|
|
318
|
+
if (draftId !== undefined) {
|
|
319
|
+
const current = await readDraft(cwd, sessionId);
|
|
320
|
+
if (current.kind === "valid" && current.draft.draft_id !== draftId) return false;
|
|
321
|
+
if (current.kind === "absent") return false;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
await fs.rm(deepInterviewDraftPath(cwd, sessionId));
|
|
325
|
+
return true;
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// -----------------------------------------------------------------------------
|
|
333
|
+
// State reading + merge preview
|
|
334
|
+
// -----------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
interface CurrentState {
|
|
337
|
+
value: Record<string, unknown>;
|
|
338
|
+
revision: number;
|
|
339
|
+
sha256: string;
|
|
340
|
+
exists: boolean;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function readCurrentState(cwd: string, sessionId: string): Promise<CurrentState> {
|
|
344
|
+
const read = await readExistingStateForMutation(statePathFor(cwd, sessionId));
|
|
345
|
+
if (read.kind === "corrupt") {
|
|
346
|
+
throw new DeepInterviewStageError(
|
|
347
|
+
"DI_STAGE_STATE_CORRUPT",
|
|
348
|
+
`deep-interview state is corrupt or tampered: ${read.error}`,
|
|
349
|
+
'repair or clear it with `skc state clear --force --mode deep-interview`, then re-seed with `skc deep-interview "<idea>"`',
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
if (read.kind === "absent")
|
|
353
|
+
return { value: {}, revision: 0, sha256: workflowEnvelopeContentSha256({}), exists: false };
|
|
354
|
+
return {
|
|
355
|
+
value: read.value,
|
|
356
|
+
revision: persistedStateRevision(read.value),
|
|
357
|
+
sha256: workflowEnvelopeContentSha256(read.value),
|
|
358
|
+
exists: true,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Both anchors must hold: revision (fast path) AND content sha (writers that skip revision stamping). */
|
|
363
|
+
function draftIsStale(draft: DeepInterviewStageDraft, current: CurrentState): boolean {
|
|
364
|
+
return draft.staged_against_revision !== current.revision || draft.staged_against_sha256 !== current.sha256;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Lossless keyed merge for `state.established_facts` under the staged surface.
|
|
369
|
+
* The generic envelope merge replaces the whole facts array; the staged contract
|
|
370
|
+
* is delta-only, so a one-fact patch must never erase prior confirmed/disputed
|
|
371
|
+
* facts (they carry the deterministic-floor evidence). Facts with an `id` merge
|
|
372
|
+
* field-wise by id; facts without an id are appended with exact-duplicate dedup.
|
|
373
|
+
* Staged deltas can never hard-delete a fact — dispute/supersede instead.
|
|
374
|
+
*/
|
|
375
|
+
function mergeEstablishedFacts(existing: readonly unknown[], incoming: readonly unknown[]): Record<string, unknown>[] {
|
|
376
|
+
const result: Record<string, unknown>[] = [];
|
|
377
|
+
const indexById = new Map<string, number>();
|
|
378
|
+
const add = (value: unknown): void => {
|
|
379
|
+
if (!isPlainObject(value)) return;
|
|
380
|
+
const id = typeof value.id === "string" && value.id.trim() !== "" ? value.id : undefined;
|
|
381
|
+
if (id !== undefined) {
|
|
382
|
+
const existingIndex = indexById.get(id);
|
|
383
|
+
if (existingIndex === undefined) {
|
|
384
|
+
indexById.set(id, result.length);
|
|
385
|
+
result.push({ ...value });
|
|
386
|
+
} else {
|
|
387
|
+
result[existingIndex] = { ...result[existingIndex], ...value };
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (result.some(item => JSON.stringify(item) === JSON.stringify(value))) return;
|
|
392
|
+
result.push({ ...value });
|
|
393
|
+
};
|
|
394
|
+
for (const fact of existing) add(fact);
|
|
395
|
+
for (const fact of incoming) add(fact);
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The single merge both `check` and `apply` execute. `check` reports its result;
|
|
401
|
+
* `apply` persists it. Divergence between the two is structurally impossible.
|
|
402
|
+
*
|
|
403
|
+
* Ambiguity is runtime-owned: after the merge, `current_ambiguity` is derived
|
|
404
|
+
* from the latest scored round, and the deterministic floor is recomputed and
|
|
405
|
+
* clamped via `applyAmbiguityFloorToEnvelope` — an agent-supplied
|
|
406
|
+
* `current_ambiguity` or under-reported round score is advisory input only and
|
|
407
|
+
* can never under-report below what persisted evidence supports.
|
|
408
|
+
*/
|
|
409
|
+
function computeMergedEnvelope(
|
|
410
|
+
current: Record<string, unknown>,
|
|
411
|
+
draft: DeepInterviewStageDraft,
|
|
412
|
+
nowIso: string,
|
|
413
|
+
): Record<string, unknown> {
|
|
414
|
+
// A poisoned (unverifiable) intent contract in the persisted base would make
|
|
415
|
+
// every merge throw forever; heal it instead of bricking the interview.
|
|
416
|
+
const { base: healedCurrent, healed } = healPoisonedIntentContract(current);
|
|
417
|
+
let merged: Record<string, unknown>;
|
|
418
|
+
try {
|
|
419
|
+
merged = mergeDeepInterviewEnvelope(healedCurrent, draft.payload) as Record<string, unknown>;
|
|
420
|
+
} catch (error) {
|
|
421
|
+
throw new DeepInterviewStageError(
|
|
422
|
+
"DI_STAGE_MERGE_REJECTED",
|
|
423
|
+
`staged payload violates a core invariant: ${error instanceof Error ? error.message : String(error)}`,
|
|
424
|
+
"fix the payload and re-stage (`skc deep-interview discard` then `stage`)",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
if (healed) merged.intent_contract_healed_at = nowIso;
|
|
428
|
+
merged.skill = "deep-interview";
|
|
429
|
+
merged.active = true;
|
|
430
|
+
merged.updated_at = nowIso;
|
|
431
|
+
merged.version = WORKFLOW_STATE_VERSION;
|
|
432
|
+
if (typeof merged.current_phase !== "string" || !merged.current_phase) merged.current_phase = "interviewing";
|
|
433
|
+
merged.session_id = draft.session_id;
|
|
434
|
+
// Staged facts are deltas: re-merge against the prior facts losslessly so a
|
|
435
|
+
// one-fact patch cannot erase confirmed/disputed history (#3387 finding 2).
|
|
436
|
+
const mergedState = isPlainObject(merged.state) ? (merged.state as Record<string, unknown>) : undefined;
|
|
437
|
+
const priorState = isPlainObject(current.state) ? (current.state as Record<string, unknown>) : undefined;
|
|
438
|
+
if (mergedState && priorState && Array.isArray(priorState.established_facts)) {
|
|
439
|
+
mergedState.established_facts = mergeEstablishedFacts(
|
|
440
|
+
priorState.established_facts,
|
|
441
|
+
Array.isArray(mergedState.established_facts) ? mergedState.established_facts : [],
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
merged = deriveRuntimeAmbiguity(merged, current);
|
|
445
|
+
try {
|
|
446
|
+
assertDeepInterviewEnvelopeInputLimits(merged);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
throw new DeepInterviewStageError(
|
|
449
|
+
"DI_STAGE_MERGE_REJECTED",
|
|
450
|
+
`merged state violates a bounded-input invariant: ${error instanceof Error ? error.message : String(error)}`,
|
|
451
|
+
"fix the payload and re-stage (`skc deep-interview discard` then `stage`)",
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
return merged;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Derive `state.current_ambiguity` — the CLI, not the agent, owns the effective
|
|
459
|
+
* ambiguity. A staged `state.current_ambiguity` is never trusted directly:
|
|
460
|
+
* - with a valid latest scored round (finite numeric `round` AND finite
|
|
461
|
+
* `ambiguity`), the value derives from that round;
|
|
462
|
+
* - with no valid scored evidence, the PRIOR persisted value is retained (a
|
|
463
|
+
* fresh interview keeps its seeded 1.0 — a staged 0.01 cannot survive);
|
|
464
|
+
* then the deterministic floor is recomputed and clamped.
|
|
465
|
+
*/
|
|
466
|
+
function deriveRuntimeAmbiguity(
|
|
467
|
+
merged: Record<string, unknown>,
|
|
468
|
+
previousState: Record<string, unknown>,
|
|
469
|
+
): Record<string, unknown> {
|
|
470
|
+
const state = isPlainObject(merged.state) ? (merged.state as Record<string, unknown>) : undefined;
|
|
471
|
+
if (state) {
|
|
472
|
+
const rounds = Array.isArray(state.rounds) ? state.rounds.filter(isPlainObject) : [];
|
|
473
|
+
let latestScored: Record<string, unknown> | undefined;
|
|
474
|
+
for (const round of rounds) {
|
|
475
|
+
if (round.lifecycle !== "scored") continue;
|
|
476
|
+
if (typeof round.ambiguity !== "number" || !Number.isFinite(round.ambiguity)) continue;
|
|
477
|
+
if (typeof round.round !== "number" || !Number.isFinite(round.round)) continue;
|
|
478
|
+
if (!latestScored || round.round >= (latestScored.round as number)) latestScored = round;
|
|
479
|
+
}
|
|
480
|
+
if (latestScored) {
|
|
481
|
+
state.current_ambiguity = latestScored.ambiguity;
|
|
482
|
+
} else {
|
|
483
|
+
// No valid scored evidence in the merged state: the staged value is
|
|
484
|
+
// discarded and the prior runtime-owned value (seed default 1.0) holds.
|
|
485
|
+
const prior = isPlainObject(previousState.state)
|
|
486
|
+
? (previousState.state as Record<string, unknown>).current_ambiguity
|
|
487
|
+
: undefined;
|
|
488
|
+
if (typeof prior === "number" && Number.isFinite(prior)) state.current_ambiguity = prior;
|
|
489
|
+
else delete state.current_ambiguity;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return applyAmbiguityFloorToEnvelope(merged).envelope as Record<string, unknown>;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// -----------------------------------------------------------------------------
|
|
496
|
+
// Verbs
|
|
497
|
+
// -----------------------------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* One explicit session boundary for every staged verb: `--session-id` flag,
|
|
501
|
+
* payload `session_id` (stage only), or `SKC_SESSION_ID`. Mutating verbs never
|
|
502
|
+
* fall back to latest-session auto-detect.
|
|
503
|
+
*/
|
|
504
|
+
function resolveStageSession(args: readonly string[], cwd: string, payloadSessionId?: unknown): string {
|
|
505
|
+
const session = resolveSkcSessionForWrite(cwd, {
|
|
506
|
+
flagValue: flagValue(args, "--session-id"),
|
|
507
|
+
payloadSessionId,
|
|
508
|
+
envSessionId: process.env.SKC_SESSION_ID,
|
|
509
|
+
});
|
|
510
|
+
return session.skcSessionId;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function handleStage(args: readonly string[], cwd: string): Promise<Record<string, unknown>> {
|
|
514
|
+
const rawInput = flagValue(args, "--input");
|
|
515
|
+
if (rawInput === undefined || rawInput === "") {
|
|
516
|
+
throw new DeepInterviewStageError("DI_STAGE_USAGE", "--input '<json>' (or @file) is required for stage");
|
|
517
|
+
}
|
|
518
|
+
const rawPayload = await parseJsonInput(rawInput, cwd);
|
|
519
|
+
const transition = parseTransition(flagValue(args, "--for"));
|
|
520
|
+
const sessionId = resolveStageSession(args, cwd, rawPayload.session_id);
|
|
521
|
+
const { payload, ignoredKeys } = sanitizeStagedPayload(rawPayload);
|
|
522
|
+
assertCorePayloadSchema(payload);
|
|
523
|
+
|
|
524
|
+
const statePath = statePathFor(cwd, sessionId);
|
|
525
|
+
return withWorkflowStateLock(
|
|
526
|
+
statePath,
|
|
527
|
+
async () => {
|
|
528
|
+
const existingDraft = await readDraft(cwd, sessionId);
|
|
529
|
+
if (existingDraft.kind === "valid") {
|
|
530
|
+
throw new DeepInterviewStageError(
|
|
531
|
+
"DI_STAGE_DRAFT_EXISTS",
|
|
532
|
+
`a staged draft already exists (draft_id=${existingDraft.draft.draft_id}, transition=${existingDraft.draft.transition}, created_at=${existingDraft.draft.created_at})`,
|
|
533
|
+
"apply it (`skc deep-interview apply`) or discard it (`skc deep-interview discard`) before staging again",
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
// A corrupt draft never blocks staging: it cannot be applied anyway, so
|
|
537
|
+
// staging over it is the self-healing path.
|
|
538
|
+
const current = await readCurrentState(cwd, sessionId);
|
|
539
|
+
if (!current.exists && transition !== "initialize-context") {
|
|
540
|
+
throw new DeepInterviewStageError(
|
|
541
|
+
"DI_STAGE_STATE_MISSING",
|
|
542
|
+
`no deep-interview state exists for session ${sessionId}; only --for initialize-context may stage against absent state`,
|
|
543
|
+
'seed the interview first with `skc deep-interview "<idea>"` or stage --for initialize-context',
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
const nowIso = new Date().toISOString();
|
|
547
|
+
const draft: DeepInterviewStageDraft = {
|
|
548
|
+
version: DRAFT_VERSION,
|
|
549
|
+
draft_id: randomUUID(),
|
|
550
|
+
session_id: sessionId,
|
|
551
|
+
transition,
|
|
552
|
+
staged_against_revision: current.revision,
|
|
553
|
+
staged_against_sha256: current.sha256,
|
|
554
|
+
payload,
|
|
555
|
+
created_at: nowIso,
|
|
556
|
+
};
|
|
557
|
+
// Fail-closed preview at stage time: reject payloads that could never apply.
|
|
558
|
+
computeMergedEnvelope(current.value, draft, nowIso);
|
|
559
|
+
await writeJsonAtomic(deepInterviewDraftPath(cwd, sessionId), draft, {
|
|
560
|
+
cwd,
|
|
561
|
+
audit: {
|
|
562
|
+
category: "state",
|
|
563
|
+
verb: "stage-draft",
|
|
564
|
+
owner: "skc-runtime",
|
|
565
|
+
skill: "deep-interview",
|
|
566
|
+
sessionId,
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
await writeSessionActivityMarker(cwd, sessionId, { writer: "deep-interview-stage" });
|
|
570
|
+
return {
|
|
571
|
+
ok: true,
|
|
572
|
+
verb: "stage",
|
|
573
|
+
draft_id: draft.draft_id,
|
|
574
|
+
transition,
|
|
575
|
+
session_id: sessionId,
|
|
576
|
+
staged_against_revision: draft.staged_against_revision,
|
|
577
|
+
draft_path: deepInterviewDraftPath(cwd, sessionId),
|
|
578
|
+
...(ignoredKeys.length > 0 ? { ignored_runtime_owned_keys: ignoredKeys } : {}),
|
|
579
|
+
};
|
|
580
|
+
},
|
|
581
|
+
{ cwd },
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function requireDraftRead(read: DraftReadResult, sessionId: string): DeepInterviewStageDraft {
|
|
586
|
+
if (read.kind === "absent") {
|
|
587
|
+
throw new DeepInterviewStageError(
|
|
588
|
+
"DI_STAGE_NO_DRAFT",
|
|
589
|
+
`no staged draft exists for session ${sessionId}`,
|
|
590
|
+
"stage one first: `skc deep-interview stage --for <transition> --input '<json>'`",
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
if (read.kind === "corrupt") {
|
|
594
|
+
throw new DeepInterviewStageError(
|
|
595
|
+
"DI_STAGE_DRAFT_CORRUPT",
|
|
596
|
+
`the staged draft is unreadable: ${read.error}`,
|
|
597
|
+
"discard it (`skc deep-interview discard`) and re-stage",
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
return read.draft;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function handleCheck(args: readonly string[], cwd: string): Promise<Record<string, unknown>> {
|
|
604
|
+
const sessionId = resolveStageSession(args, cwd);
|
|
605
|
+
const draft = requireDraftRead(await readDraft(cwd, sessionId), sessionId);
|
|
606
|
+
const current = await readCurrentState(cwd, sessionId);
|
|
607
|
+
const summaryBase = {
|
|
608
|
+
verb: "check",
|
|
609
|
+
draft_id: draft.draft_id,
|
|
610
|
+
transition: draft.transition,
|
|
611
|
+
session_id: sessionId,
|
|
612
|
+
staged_against_revision: draft.staged_against_revision,
|
|
613
|
+
current_revision: current.revision,
|
|
614
|
+
};
|
|
615
|
+
if (draftIsStale(draft, current)) {
|
|
616
|
+
return {
|
|
617
|
+
...summaryBase,
|
|
618
|
+
ok: false,
|
|
619
|
+
code: "DI_STAGE_REVISION_CONFLICT",
|
|
620
|
+
recovery: "state moved since staging; discard and re-stage against current state",
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
const merged = computeMergedEnvelope(current.value, draft, new Date().toISOString());
|
|
624
|
+
const state = isPlainObject(merged.state) ? merged.state : {};
|
|
625
|
+
return {
|
|
626
|
+
...summaryBase,
|
|
627
|
+
ok: true,
|
|
628
|
+
would_apply: true,
|
|
629
|
+
result_phase: merged.current_phase,
|
|
630
|
+
result_round_count: Array.isArray(state.rounds) ? state.rounds.length : 0,
|
|
631
|
+
result_fact_count: Array.isArray(state.established_facts) ? state.established_facts.length : 0,
|
|
632
|
+
...(typeof state.current_ambiguity === "number" ? { result_ambiguity: state.current_ambiguity } : {}),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function handleApply(args: readonly string[], cwd: string): Promise<Record<string, unknown>> {
|
|
637
|
+
const sessionId = resolveStageSession(args, cwd);
|
|
638
|
+
const statePath = statePathFor(cwd, sessionId);
|
|
639
|
+
return withWorkflowStateLock(
|
|
640
|
+
statePath,
|
|
641
|
+
async () => {
|
|
642
|
+
// Re-read the draft INSIDE the lock so a concurrent discard/stage cannot
|
|
643
|
+
// hand us a draft that no longer exists or was replaced.
|
|
644
|
+
const draft = requireDraftRead(await readDraft(cwd, sessionId), sessionId);
|
|
645
|
+
const current = await readCurrentState(cwd, sessionId);
|
|
646
|
+
// Replay safety: a prior apply that committed but crashed before draft
|
|
647
|
+
// removal leaves `last_applied_draft_id` in state. Recognize the commit,
|
|
648
|
+
// finish the cleanup, and settle as an idempotent no-op.
|
|
649
|
+
if (current.value.last_applied_draft_id === draft.draft_id) {
|
|
650
|
+
await removeDraftIfMatches(cwd, sessionId, draft.draft_id);
|
|
651
|
+
return {
|
|
652
|
+
ok: true,
|
|
653
|
+
verb: "apply",
|
|
654
|
+
draft_id: draft.draft_id,
|
|
655
|
+
transition: draft.transition,
|
|
656
|
+
session_id: sessionId,
|
|
657
|
+
applied_revision: current.revision,
|
|
658
|
+
state_path: statePath,
|
|
659
|
+
already_applied: true,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
if (draftIsStale(draft, current)) {
|
|
663
|
+
// CAS conflict: the draft can never legally apply, so auto-invalidate it.
|
|
664
|
+
await removeDraftIfMatches(cwd, sessionId, draft.draft_id);
|
|
665
|
+
throw new DeepInterviewStageError(
|
|
666
|
+
"DI_STAGE_REVISION_CONFLICT",
|
|
667
|
+
`state moved since staging (revision ${draft.staged_against_revision} -> ${current.revision} or content changed); the draft was invalidated`,
|
|
668
|
+
"re-stage against current state: `skc deep-interview stage --for <transition> --input '<json>'`",
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
const nowIso = new Date().toISOString();
|
|
672
|
+
const merged = computeMergedEnvelope(current.value, draft, nowIso);
|
|
673
|
+
merged.last_applied_draft_id = draft.draft_id;
|
|
674
|
+
let appliedRevision: number;
|
|
675
|
+
try {
|
|
676
|
+
const written = await writeGuardedWorkflowEnvelopeAtomic(statePath, merged, {
|
|
677
|
+
cwd,
|
|
678
|
+
policy: "source",
|
|
679
|
+
expectedRevision: current.revision,
|
|
680
|
+
lockHeld: true,
|
|
681
|
+
receipt: {
|
|
682
|
+
cwd,
|
|
683
|
+
skill: "deep-interview",
|
|
684
|
+
owner: "skc-runtime",
|
|
685
|
+
command: `skc deep-interview apply (${draft.transition})`,
|
|
686
|
+
sessionId,
|
|
687
|
+
nowIso,
|
|
688
|
+
mutationId: draft.draft_id,
|
|
689
|
+
},
|
|
690
|
+
audit: {
|
|
691
|
+
category: "state",
|
|
692
|
+
verb: "apply-staged-transition",
|
|
693
|
+
owner: "skc-runtime",
|
|
694
|
+
skill: "deep-interview",
|
|
695
|
+
sessionId,
|
|
696
|
+
mutationId: draft.draft_id,
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
appliedRevision = written.revision;
|
|
700
|
+
} catch (error) {
|
|
701
|
+
if (error instanceof StateWriteConflictError) {
|
|
702
|
+
await removeDraftIfMatches(cwd, sessionId, draft.draft_id);
|
|
703
|
+
throw new DeepInterviewStageError(
|
|
704
|
+
"DI_STAGE_REVISION_CONFLICT",
|
|
705
|
+
`state revision moved since staging; the draft was invalidated (${error.message})`,
|
|
706
|
+
"re-stage against current state: `skc deep-interview stage --for <transition> --input '<json>'`",
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
throw error;
|
|
710
|
+
}
|
|
711
|
+
// Post-commit cleanup is best-effort: the commit already happened, and a
|
|
712
|
+
// replay recognizes it via last_applied_draft_id instead of failing.
|
|
713
|
+
try {
|
|
714
|
+
await removeDraftIfMatches(cwd, sessionId, draft.draft_id);
|
|
715
|
+
await writeSessionActivityMarker(cwd, sessionId, { writer: "deep-interview-stage", path: statePath });
|
|
716
|
+
} catch {
|
|
717
|
+
// Swallow: state is committed; the next apply/discard settles the draft.
|
|
718
|
+
}
|
|
719
|
+
await syncStageHud(cwd, sessionId, merged);
|
|
720
|
+
const appliedState = isPlainObject(merged.state) ? (merged.state as Record<string, unknown>) : {};
|
|
721
|
+
return {
|
|
722
|
+
ok: true,
|
|
723
|
+
verb: "apply",
|
|
724
|
+
draft_id: draft.draft_id,
|
|
725
|
+
transition: draft.transition,
|
|
726
|
+
session_id: sessionId,
|
|
727
|
+
applied_revision: appliedRevision,
|
|
728
|
+
state_path: statePath,
|
|
729
|
+
...(typeof appliedState.current_ambiguity === "number"
|
|
730
|
+
? { current_ambiguity: appliedState.current_ambiguity }
|
|
731
|
+
: {}),
|
|
732
|
+
content_sha256: createHash("sha256").update(JSON.stringify(merged)).digest("hex").slice(0, 32),
|
|
733
|
+
};
|
|
734
|
+
},
|
|
735
|
+
{ cwd },
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function handleDiscard(args: readonly string[], cwd: string): Promise<Record<string, unknown>> {
|
|
740
|
+
const sessionId = resolveStageSession(args, cwd);
|
|
741
|
+
const statePath = statePathFor(cwd, sessionId);
|
|
742
|
+
// Same lock as stage/apply: a discard racing an in-flight apply must not
|
|
743
|
+
// delete the draft mid-consumption or return a torn read.
|
|
744
|
+
return withWorkflowStateLock(
|
|
745
|
+
statePath,
|
|
746
|
+
async () => {
|
|
747
|
+
const read = await readDraft(cwd, sessionId);
|
|
748
|
+
const removed = await removeDraftIfMatches(
|
|
749
|
+
cwd,
|
|
750
|
+
sessionId,
|
|
751
|
+
read.kind === "valid" ? read.draft.draft_id : undefined,
|
|
752
|
+
);
|
|
753
|
+
return {
|
|
754
|
+
ok: true,
|
|
755
|
+
verb: "discard",
|
|
756
|
+
session_id: sessionId,
|
|
757
|
+
removed,
|
|
758
|
+
...(read.kind === "valid" ? { draft_id: read.draft.draft_id, transition: read.draft.transition } : {}),
|
|
759
|
+
};
|
|
760
|
+
},
|
|
761
|
+
{ cwd },
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// -----------------------------------------------------------------------------
|
|
766
|
+
// Direct verbs: read / write / clear / handoff
|
|
767
|
+
// -----------------------------------------------------------------------------
|
|
768
|
+
|
|
769
|
+
async function handleRead(args: readonly string[], cwd: string): Promise<Record<string, unknown>> {
|
|
770
|
+
const sessionId = resolveStageSession(args, cwd);
|
|
771
|
+
const current = await readCurrentState(cwd, sessionId);
|
|
772
|
+
if (!current.exists) {
|
|
773
|
+
return {
|
|
774
|
+
ok: true,
|
|
775
|
+
verb: "read",
|
|
776
|
+
session_id: sessionId,
|
|
777
|
+
exists: false,
|
|
778
|
+
state_path: statePathFor(cwd, sessionId),
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
const draftRead = await readDraft(cwd, sessionId);
|
|
782
|
+
return {
|
|
783
|
+
ok: true,
|
|
784
|
+
verb: "read",
|
|
785
|
+
session_id: sessionId,
|
|
786
|
+
exists: true,
|
|
787
|
+
state_path: statePathFor(cwd, sessionId),
|
|
788
|
+
revision: current.revision,
|
|
789
|
+
content_sha256: current.sha256,
|
|
790
|
+
envelope: current.value,
|
|
791
|
+
...(draftRead.kind === "valid"
|
|
792
|
+
? {
|
|
793
|
+
pending_draft: {
|
|
794
|
+
draft_id: draftRead.draft.draft_id,
|
|
795
|
+
transition: draftRead.draft.transition,
|
|
796
|
+
created_at: draftRead.draft.created_at,
|
|
797
|
+
},
|
|
798
|
+
}
|
|
799
|
+
: {}),
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Direct write: an immediate stage+apply of one sanitized JSON payload —
|
|
805
|
+
* incremental merge by default, whole-state replacement with `--reset` (the
|
|
806
|
+
* locked intent contract survives a reset through the merge's immutability
|
|
807
|
+
* guard). Uses the same lock, sanitizer, merge, ambiguity derivation, and
|
|
808
|
+
* guarded revision-stamping writer as the staged path, so `write` cannot
|
|
809
|
+
* express anything `stage`+`apply` could not.
|
|
810
|
+
*/
|
|
811
|
+
async function handleWrite(args: readonly string[], cwd: string): Promise<Record<string, unknown>> {
|
|
812
|
+
const rawInput = flagValue(args, "--input");
|
|
813
|
+
if (rawInput === undefined || rawInput === "") {
|
|
814
|
+
throw new DeepInterviewStageError("DI_STAGE_USAGE", "--input '<json>' (or @file) is required for write");
|
|
815
|
+
}
|
|
816
|
+
const rawPayload = await parseJsonInput(rawInput, cwd);
|
|
817
|
+
const sessionId = resolveStageSession(args, cwd, rawPayload.session_id);
|
|
818
|
+
const reset = hasFlag(args, "--reset");
|
|
819
|
+
const { payload, ignoredKeys } = sanitizeStagedPayload(rawPayload);
|
|
820
|
+
assertCorePayloadSchema(payload);
|
|
821
|
+
|
|
822
|
+
const statePath = statePathFor(cwd, sessionId);
|
|
823
|
+
return withWorkflowStateLock(
|
|
824
|
+
statePath,
|
|
825
|
+
async () => {
|
|
826
|
+
const pendingDraft = await readDraft(cwd, sessionId);
|
|
827
|
+
if (pendingDraft.kind === "valid") {
|
|
828
|
+
throw new DeepInterviewStageError(
|
|
829
|
+
"DI_STAGE_DRAFT_EXISTS",
|
|
830
|
+
`a staged draft is pending (draft_id=${pendingDraft.draft.draft_id}); direct write would race it`,
|
|
831
|
+
"apply it (`skc deep-interview apply`) or discard it (`skc deep-interview discard`) first",
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
const current = await readCurrentState(cwd, sessionId);
|
|
835
|
+
const nowIso = new Date().toISOString();
|
|
836
|
+
const syntheticDraft: DeepInterviewStageDraft = {
|
|
837
|
+
version: DRAFT_VERSION,
|
|
838
|
+
draft_id: randomUUID(),
|
|
839
|
+
session_id: sessionId,
|
|
840
|
+
transition: "merge-state",
|
|
841
|
+
staged_against_revision: current.revision,
|
|
842
|
+
staged_against_sha256: current.sha256,
|
|
843
|
+
payload,
|
|
844
|
+
created_at: nowIso,
|
|
845
|
+
};
|
|
846
|
+
// --reset replaces: merge against an empty base but re-lock the intent
|
|
847
|
+
// contract from prior state through the merge's own immutability guard.
|
|
848
|
+
const base = reset
|
|
849
|
+
? (() => {
|
|
850
|
+
const priorState = isPlainObject(current.value.state)
|
|
851
|
+
? (current.value.state as Record<string, unknown>)
|
|
852
|
+
: {};
|
|
853
|
+
return priorState.intent_contract !== undefined
|
|
854
|
+
? { state: { intent_contract: priorState.intent_contract, intent_contract_required: true } }
|
|
855
|
+
: {};
|
|
856
|
+
})()
|
|
857
|
+
: current.value;
|
|
858
|
+
const merged = computeMergedEnvelope(base as Record<string, unknown>, syntheticDraft, nowIso);
|
|
859
|
+
merged.last_applied_draft_id = syntheticDraft.draft_id;
|
|
860
|
+
const written = await writeGuardedWorkflowEnvelopeAtomic(statePath, merged, {
|
|
861
|
+
cwd,
|
|
862
|
+
policy: "source",
|
|
863
|
+
expectedRevision: current.revision,
|
|
864
|
+
lockHeld: true,
|
|
865
|
+
receipt: {
|
|
866
|
+
cwd,
|
|
867
|
+
skill: "deep-interview",
|
|
868
|
+
owner: "skc-runtime",
|
|
869
|
+
command: `skc deep-interview write${reset ? " --reset" : ""}`,
|
|
870
|
+
sessionId,
|
|
871
|
+
nowIso,
|
|
872
|
+
mutationId: syntheticDraft.draft_id,
|
|
873
|
+
},
|
|
874
|
+
audit: {
|
|
875
|
+
category: "state",
|
|
876
|
+
verb: reset ? "write-reset" : "write-incremental",
|
|
877
|
+
owner: "skc-runtime",
|
|
878
|
+
skill: "deep-interview",
|
|
879
|
+
sessionId,
|
|
880
|
+
mutationId: syntheticDraft.draft_id,
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
await writeSessionActivityMarker(cwd, sessionId, { writer: "deep-interview-stage", path: statePath });
|
|
884
|
+
await syncStageHud(cwd, sessionId, merged);
|
|
885
|
+
const writtenState = isPlainObject(merged.state) ? (merged.state as Record<string, unknown>) : {};
|
|
886
|
+
return {
|
|
887
|
+
ok: true,
|
|
888
|
+
verb: "write",
|
|
889
|
+
mode: reset ? "reset" : "incremental",
|
|
890
|
+
session_id: sessionId,
|
|
891
|
+
applied_revision: written.revision,
|
|
892
|
+
state_path: statePath,
|
|
893
|
+
...(typeof writtenState.current_ambiguity === "number"
|
|
894
|
+
? { current_ambiguity: writtenState.current_ambiguity }
|
|
895
|
+
: {}),
|
|
896
|
+
...(ignoredKeys.length > 0 ? { ignored_runtime_owned_keys: ignoredKeys } : {}),
|
|
897
|
+
};
|
|
898
|
+
},
|
|
899
|
+
{ cwd },
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Publish a committed envelope to the active-state/HUD mirror. Best-effort:
|
|
905
|
+
* the durable commit already happened; a projection failure must never fail
|
|
906
|
+
* the command (matches syncDeepInterviewHud in deep-interview-runtime.ts).
|
|
907
|
+
*/
|
|
908
|
+
async function syncStageHud(cwd: string, sessionId: string, envelope: Record<string, unknown>): Promise<void> {
|
|
909
|
+
try {
|
|
910
|
+
const phase = typeof envelope.current_phase === "string" ? envelope.current_phase : "interviewing";
|
|
911
|
+
await syncSkillActiveState({
|
|
912
|
+
cwd,
|
|
913
|
+
skill: "deep-interview",
|
|
914
|
+
active: phase !== "complete",
|
|
915
|
+
phase,
|
|
916
|
+
sessionId,
|
|
917
|
+
source: "skc-deep-interview-native",
|
|
918
|
+
hud: deriveDeepInterviewHud(envelope, { phase }),
|
|
919
|
+
});
|
|
920
|
+
} catch {
|
|
921
|
+
// HUD sync is best-effort and must not change command semantics.
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/** Thin lifecycle passthroughs: same runtime plumbing, skc deep-interview surface. */
|
|
926
|
+
async function handleLifecyclePassthrough(
|
|
927
|
+
verb: "clear" | "handoff",
|
|
928
|
+
args: readonly string[],
|
|
929
|
+
cwd: string,
|
|
930
|
+
): Promise<DeepInterviewStageCommandResult> {
|
|
931
|
+
const forwarded =
|
|
932
|
+
verb === "clear"
|
|
933
|
+
? ["clear", "--mode", "deep-interview", ...args]
|
|
934
|
+
: ["handoff", "--mode", "deep-interview", ...args];
|
|
935
|
+
return runNativeStateCommand([...forwarded], cwd);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// -----------------------------------------------------------------------------
|
|
939
|
+
// Dispatch
|
|
940
|
+
// -----------------------------------------------------------------------------
|
|
941
|
+
|
|
942
|
+
export const DEEP_INTERVIEW_STAGE_VERBS = [
|
|
943
|
+
"stage",
|
|
944
|
+
"check",
|
|
945
|
+
"apply",
|
|
946
|
+
"discard",
|
|
947
|
+
"read",
|
|
948
|
+
"write",
|
|
949
|
+
"clear",
|
|
950
|
+
"handoff",
|
|
951
|
+
] as const;
|
|
952
|
+
export type DeepInterviewStageVerb = (typeof DEEP_INTERVIEW_STAGE_VERBS)[number];
|
|
953
|
+
|
|
954
|
+
export function isDeepInterviewStageVerb(value: string | undefined): value is DeepInterviewStageVerb {
|
|
955
|
+
return value !== undefined && (DEEP_INTERVIEW_STAGE_VERBS as readonly string[]).includes(value);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
export async function runDeepInterviewStageCommand(
|
|
959
|
+
verb: DeepInterviewStageVerb,
|
|
960
|
+
args: readonly string[],
|
|
961
|
+
cwd = process.cwd(),
|
|
962
|
+
): Promise<DeepInterviewStageCommandResult> {
|
|
963
|
+
const json = hasFlag(args, "--json");
|
|
964
|
+
try {
|
|
965
|
+
let summary: Record<string, unknown>;
|
|
966
|
+
switch (verb) {
|
|
967
|
+
case "stage":
|
|
968
|
+
summary = await handleStage(args, cwd);
|
|
969
|
+
break;
|
|
970
|
+
case "check":
|
|
971
|
+
summary = await handleCheck(args, cwd);
|
|
972
|
+
break;
|
|
973
|
+
case "apply":
|
|
974
|
+
summary = await handleApply(args, cwd);
|
|
975
|
+
break;
|
|
976
|
+
case "discard":
|
|
977
|
+
summary = await handleDiscard(args, cwd);
|
|
978
|
+
break;
|
|
979
|
+
case "read":
|
|
980
|
+
summary = await handleRead(args, cwd);
|
|
981
|
+
break;
|
|
982
|
+
case "write":
|
|
983
|
+
summary = await handleWrite(args, cwd);
|
|
984
|
+
break;
|
|
985
|
+
case "clear":
|
|
986
|
+
case "handoff":
|
|
987
|
+
return await handleLifecyclePassthrough(verb, args, cwd);
|
|
988
|
+
}
|
|
989
|
+
const status = summary.ok === false ? 3 : 0;
|
|
990
|
+
const stdout = json
|
|
991
|
+
? `${JSON.stringify(summary)}\n`
|
|
992
|
+
: `${Object.entries(summary)
|
|
993
|
+
.map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`)
|
|
994
|
+
.join(" ")}\n`;
|
|
995
|
+
return { status, stdout };
|
|
996
|
+
} catch (error) {
|
|
997
|
+
const staged =
|
|
998
|
+
error instanceof DeepInterviewStageError
|
|
999
|
+
? error
|
|
1000
|
+
: error instanceof SessionResolutionError
|
|
1001
|
+
? new DeepInterviewStageError(
|
|
1002
|
+
"DI_STAGE_SESSION_REQUIRED",
|
|
1003
|
+
error.message,
|
|
1004
|
+
"pass --session-id, set SKC_SESSION_ID, or include session_id in the staged payload, then retry",
|
|
1005
|
+
)
|
|
1006
|
+
: undefined;
|
|
1007
|
+
if (staged) {
|
|
1008
|
+
const body = json
|
|
1009
|
+
? `${JSON.stringify({ ok: false, code: staged.code, message: staged.message, ...(staged.recovery ? { recovery: staged.recovery } : {}) })}\n`
|
|
1010
|
+
: `${staged.code}: ${staged.message}${staged.recovery ? `\nrecovery: ${staged.recovery}` : ""}\n`;
|
|
1011
|
+
return { status: staged.exitStatus, stderr: body };
|
|
1012
|
+
}
|
|
1013
|
+
if (error instanceof CommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
|
|
1014
|
+
return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
|
|
1015
|
+
}
|
|
1016
|
+
}
|