@sayknow-cli/coding-agent 0.4.7 → 0.5.1
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 +122 -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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { getConfigRootDir } from "@sayknow-cli/utils";
|
|
4
5
|
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
5
6
|
import { buildRalplanHudSummary } from "../skill-state/workflow-hud";
|
|
6
7
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
@@ -11,8 +12,17 @@ import {
|
|
|
11
12
|
type RalplanIndexRow,
|
|
12
13
|
summarizeRalplanIndex,
|
|
13
14
|
} from "./ledger-event-renderer";
|
|
15
|
+
import {
|
|
16
|
+
assertCwdMatchesRepositoryBinding,
|
|
17
|
+
assertPathUnderRepositoryBinding,
|
|
18
|
+
captureRepositoryBinding,
|
|
19
|
+
parseRepositoryBinding,
|
|
20
|
+
publicRepositoryBinding,
|
|
21
|
+
type RepositoryBinding,
|
|
22
|
+
RepositoryBindingError,
|
|
23
|
+
} from "./repository-binding";
|
|
14
24
|
import { isRestrictedRoleAgentBash, SKC_RALPLAN_ARTIFACT_ENV } from "./restricted-role-agent-bash";
|
|
15
|
-
import { modeStatePath, sessionPlansDir } from "./session-layout";
|
|
25
|
+
import { modeStatePath, sessionPlansDir, skcRoot } from "./session-layout";
|
|
16
26
|
import { resolveSkcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
17
27
|
import { migrateWorkflowState } from "./state-migrations";
|
|
18
28
|
import { runNativeStateCommand } from "./state-runtime";
|
|
@@ -40,7 +50,7 @@ import { getSkillManifest } from "./workflow-manifest";
|
|
|
40
50
|
*
|
|
41
51
|
* 2. **Artifact write**: `skc ralplan --write --stage <type> --stage_n <N>
|
|
42
52
|
* (--artifact <path-or-string> | --artifact-env SKC_RALPLAN_ARTIFACT)
|
|
43
|
-
* [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
|
|
53
|
+
* [--run-id <id>] [--session-id <id>] [--lane-verdict <token>] [--json]` persists Planner / Architect
|
|
44
54
|
* / Critic / revision / post-interview / ADR / final markdown under `.skc/plans/ralplan/<run-id>/`, maintains
|
|
45
55
|
* an `index.jsonl` audit log, copies `final` stages to `pending-approval.md`, and advances
|
|
46
56
|
* the HUD chip to reflect the latest persisted stage.
|
|
@@ -54,6 +64,498 @@ export interface RalplanCommandResult {
|
|
|
54
64
|
|
|
55
65
|
const KNOWN_STAGES = ["planner", "architect", "critic", "revision", "post-interview", "adr", "final"] as const;
|
|
56
66
|
type RalplanStage = (typeof KNOWN_STAGES)[number];
|
|
67
|
+
/** Default consensus iterations (planner + revision openers) per run. Matches SKILL.md re-review cap. */
|
|
68
|
+
export const RALPLAN_DEFAULT_MAX_ITERATIONS = 5;
|
|
69
|
+
/** Inclusive upper bound for `skc.ralplan.maxIterations` settings overrides. */
|
|
70
|
+
export const RALPLAN_MAX_ITERATIONS_LIMIT = 20;
|
|
71
|
+
/** Operator-visible stuck signal for headless/CI orchestration (#3165). */
|
|
72
|
+
export const PLANNING_STUCK_MARKER = "PLANNING-STUCK";
|
|
73
|
+
/** Default architect/critic review passes per consensus iteration. */
|
|
74
|
+
export const RALPLAN_DEFAULT_MAX_REVIEW_PASSES_PER_LANE = 1;
|
|
75
|
+
/** Inclusive upper bound for `skc.ralplan.maxReviewPassesPerLane` settings overrides. */
|
|
76
|
+
export const RALPLAN_MAX_REVIEW_PASSES_PER_LANE_LIMIT = 10;
|
|
77
|
+
|
|
78
|
+
const RALPLAN_ITERATION_OPENER_STAGES = new Set<RalplanStage>(["planner", "revision"]);
|
|
79
|
+
|
|
80
|
+
/** Collapse duplicate ledger rows for the same deterministic stage artifact before review-lane budget accounting. */
|
|
81
|
+
function deduplicateRalplanIndexRowsByStageIdentity(rows: readonly RalplanIndexRow[]): RalplanIndexRow[] {
|
|
82
|
+
const seen = new Set<string>();
|
|
83
|
+
return rows.filter(row => {
|
|
84
|
+
if (typeof row.stageN !== "number") return true;
|
|
85
|
+
const identity = `${row.stage}\u0000${row.stageN}`;
|
|
86
|
+
if (seen.has(identity)) return false;
|
|
87
|
+
seen.add(identity);
|
|
88
|
+
return true;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type RalplanIterationCapDecision =
|
|
93
|
+
| {
|
|
94
|
+
allowed: true;
|
|
95
|
+
currentIterations: number;
|
|
96
|
+
projectedIterations: number;
|
|
97
|
+
maxIterations: number;
|
|
98
|
+
}
|
|
99
|
+
| {
|
|
100
|
+
allowed: false;
|
|
101
|
+
currentIterations: number;
|
|
102
|
+
projectedIterations: number;
|
|
103
|
+
maxIterations: number;
|
|
104
|
+
reason: string;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Pure consensus-iteration budget gate (#3165).
|
|
109
|
+
*
|
|
110
|
+
* A `planner` or `revision` write opens a new iteration (same definition as
|
|
111
|
+
* `summarizeRalplanIndex`). Other stages never open iterations and are always
|
|
112
|
+
* allowed by this gate — including `final` after the cap is already reached.
|
|
113
|
+
*
|
|
114
|
+
* `iterationFloor` raises the observed opener count when on-disk evidence or a
|
|
115
|
+
* recovered ledger is higher than the parsed index (fail-closed vs wipe/truncate).
|
|
116
|
+
*/
|
|
117
|
+
export function evaluateRalplanIterationCap(input: {
|
|
118
|
+
rows: readonly RalplanIndexRow[];
|
|
119
|
+
stage: string;
|
|
120
|
+
maxIterations?: number;
|
|
121
|
+
/** Minimum opener count (e.g. on-disk stage-*-{planner,revision}.md). */
|
|
122
|
+
iterationFloor?: number;
|
|
123
|
+
}): RalplanIterationCapDecision {
|
|
124
|
+
const maxIterations =
|
|
125
|
+
typeof input.maxIterations === "number" &&
|
|
126
|
+
Number.isInteger(input.maxIterations) &&
|
|
127
|
+
input.maxIterations >= 1 &&
|
|
128
|
+
input.maxIterations <= RALPLAN_MAX_ITERATIONS_LIMIT
|
|
129
|
+
? input.maxIterations
|
|
130
|
+
: RALPLAN_DEFAULT_MAX_ITERATIONS;
|
|
131
|
+
const fromIndex = summarizeRalplanIndex(input.rows).iteration;
|
|
132
|
+
const floor =
|
|
133
|
+
typeof input.iterationFloor === "number" && Number.isInteger(input.iterationFloor) && input.iterationFloor > 0
|
|
134
|
+
? input.iterationFloor
|
|
135
|
+
: 0;
|
|
136
|
+
const currentIterations = Math.max(fromIndex, floor);
|
|
137
|
+
if (!RALPLAN_ITERATION_OPENER_STAGES.has(input.stage as RalplanStage)) {
|
|
138
|
+
return {
|
|
139
|
+
allowed: true,
|
|
140
|
+
currentIterations,
|
|
141
|
+
projectedIterations: currentIterations,
|
|
142
|
+
maxIterations,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const projectedIterations = currentIterations + 1;
|
|
146
|
+
if (projectedIterations > maxIterations) {
|
|
147
|
+
const ledgerNote = floor > fromIndex ? ` (ledger under-count: index=${fromIndex}, on-disk openers=${floor})` : "";
|
|
148
|
+
return {
|
|
149
|
+
allowed: false,
|
|
150
|
+
currentIterations,
|
|
151
|
+
projectedIterations,
|
|
152
|
+
maxIterations,
|
|
153
|
+
reason:
|
|
154
|
+
`ralplan consensus iteration cap exceeded: opening ${input.stage} would start ` +
|
|
155
|
+
`iteration ${projectedIterations} (max ${maxIterations})${ledgerNote}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
allowed: true,
|
|
160
|
+
currentIterations,
|
|
161
|
+
projectedIterations,
|
|
162
|
+
maxIterations,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export type RalplanReviewLane = "architect" | "critic";
|
|
167
|
+
|
|
168
|
+
export type RalplanReviewLaneBudgetDecision =
|
|
169
|
+
| {
|
|
170
|
+
allowed: true;
|
|
171
|
+
lane?: RalplanReviewLane;
|
|
172
|
+
currentPasses: number;
|
|
173
|
+
projectedPasses: number;
|
|
174
|
+
maxReviewPassesPerLane: number;
|
|
175
|
+
finalSlot: boolean;
|
|
176
|
+
ledgerNote?: string;
|
|
177
|
+
}
|
|
178
|
+
| {
|
|
179
|
+
allowed: false;
|
|
180
|
+
lane: RalplanReviewLane;
|
|
181
|
+
currentPasses: number;
|
|
182
|
+
projectedPasses: number;
|
|
183
|
+
maxReviewPassesPerLane: number;
|
|
184
|
+
finalSlot: false;
|
|
185
|
+
ledgerNote?: string;
|
|
186
|
+
reason: string;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Pure per-lane review-pass budget gate. Architect and critic passes are limited
|
|
191
|
+
* within the current consensus iteration; all other stages remain unconditionally
|
|
192
|
+
* available as escalation paths.
|
|
193
|
+
*/
|
|
194
|
+
export function evaluateRalplanReviewLaneBudget(input: {
|
|
195
|
+
rows: readonly RalplanIndexRow[];
|
|
196
|
+
stage: string;
|
|
197
|
+
maxReviewPassesPerLane?: unknown;
|
|
198
|
+
onDiskLaneCounts?: { architect: number; critic: number };
|
|
199
|
+
}): RalplanReviewLaneBudgetDecision {
|
|
200
|
+
const maxReviewPassesPerLane =
|
|
201
|
+
typeof input.maxReviewPassesPerLane === "number" &&
|
|
202
|
+
Number.isInteger(input.maxReviewPassesPerLane) &&
|
|
203
|
+
input.maxReviewPassesPerLane >= 1 &&
|
|
204
|
+
input.maxReviewPassesPerLane <= RALPLAN_MAX_REVIEW_PASSES_PER_LANE_LIMIT
|
|
205
|
+
? input.maxReviewPassesPerLane
|
|
206
|
+
: RALPLAN_DEFAULT_MAX_REVIEW_PASSES_PER_LANE;
|
|
207
|
+
if (input.stage !== "architect" && input.stage !== "critic") {
|
|
208
|
+
return {
|
|
209
|
+
allowed: true,
|
|
210
|
+
currentPasses: 0,
|
|
211
|
+
projectedPasses: 0,
|
|
212
|
+
maxReviewPassesPerLane,
|
|
213
|
+
finalSlot: false,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const lane = input.stage as RalplanReviewLane;
|
|
218
|
+
const rows = deduplicateRalplanIndexRowsByStageIdentity(input.rows);
|
|
219
|
+
const summary = summarizeRalplanIndex(rows);
|
|
220
|
+
const indexCurrent = summary.currentStages.filter(stage => stage === lane).length;
|
|
221
|
+
const parsedTotal = rows.filter(row => row.stage === lane).length;
|
|
222
|
+
const onDiskRaw = input.onDiskLaneCounts?.[lane];
|
|
223
|
+
const onDiskTotal = typeof onDiskRaw === "number" && Number.isInteger(onDiskRaw) && onDiskRaw > 0 ? onDiskRaw : 0;
|
|
224
|
+
const diskExcess = Math.max(0, onDiskTotal - parsedTotal);
|
|
225
|
+
const currentPasses = indexCurrent + diskExcess;
|
|
226
|
+
const projectedPasses = currentPasses + 1;
|
|
227
|
+
const ledgerNote =
|
|
228
|
+
diskExcess > 0
|
|
229
|
+
? ` (ledger under-count: parsed ${lane} rows=${parsedTotal}, on-disk ${lane} artifacts=${onDiskTotal})`
|
|
230
|
+
: undefined;
|
|
231
|
+
const finalSlot = projectedPasses === maxReviewPassesPerLane;
|
|
232
|
+
if (projectedPasses > maxReviewPassesPerLane) {
|
|
233
|
+
return {
|
|
234
|
+
allowed: false,
|
|
235
|
+
lane,
|
|
236
|
+
currentPasses,
|
|
237
|
+
projectedPasses,
|
|
238
|
+
maxReviewPassesPerLane,
|
|
239
|
+
finalSlot: false,
|
|
240
|
+
ledgerNote,
|
|
241
|
+
reason:
|
|
242
|
+
`ralplan review lane budget exceeded: ${lane} pass ${projectedPasses} of max ${maxReviewPassesPerLane} ` +
|
|
243
|
+
`in consensus iteration ${Math.max(1, summary.iteration)}${ledgerNote ?? ""}`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
allowed: true,
|
|
248
|
+
lane,
|
|
249
|
+
currentPasses,
|
|
250
|
+
projectedPasses,
|
|
251
|
+
maxReviewPassesPerLane,
|
|
252
|
+
finalSlot,
|
|
253
|
+
ledgerNote,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getErrorCode(error: unknown): string | undefined {
|
|
258
|
+
if (!error || typeof error !== "object" || !("code" in error)) return undefined;
|
|
259
|
+
const code = (error as { code?: unknown }).code;
|
|
260
|
+
return typeof code === "string" ? code : undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Filename pattern for persisted planner/revision stage artifacts. */
|
|
264
|
+
const OPENER_ARTIFACT_RE = /^stage-\d{2,}-(planner|revision)\.md$/;
|
|
265
|
+
const LANE_ARTIFACT_RE = /^stage-\d{2,}-(architect|critic)\.md$/;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Count on-disk planner/revision stage artifacts for a run. Used as a floor when
|
|
269
|
+
* `index.jsonl` is missing, empty, truncated, or otherwise under-counts openers.
|
|
270
|
+
*/
|
|
271
|
+
export async function countRalplanOnDiskOpeners(cwd: string, sessionId: string, runId: string): Promise<number> {
|
|
272
|
+
const runDir = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId);
|
|
273
|
+
try {
|
|
274
|
+
const entries = await fs.readdir(runDir);
|
|
275
|
+
let count = 0;
|
|
276
|
+
for (const name of entries) {
|
|
277
|
+
if (OPENER_ARTIFACT_RE.test(name)) count += 1;
|
|
278
|
+
}
|
|
279
|
+
return count;
|
|
280
|
+
} catch {
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Count on-disk Architect/Critic stage artifacts for a run. This is the
|
|
287
|
+
* fail-closed floor for a missing, truncated, or malformed `index.jsonl`.
|
|
288
|
+
*/
|
|
289
|
+
export async function countRalplanOnDiskLaneArtifacts(
|
|
290
|
+
cwd: string,
|
|
291
|
+
sessionId: string,
|
|
292
|
+
runId: string,
|
|
293
|
+
): Promise<{ architect: number; critic: number }> {
|
|
294
|
+
const runDir = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId);
|
|
295
|
+
try {
|
|
296
|
+
const entries = await fs.readdir(runDir);
|
|
297
|
+
const counts = { architect: 0, critic: 0 };
|
|
298
|
+
for (const name of entries) {
|
|
299
|
+
const match = LANE_ARTIFACT_RE.exec(name);
|
|
300
|
+
if (match) counts[match[1] as RalplanReviewLane] += 1;
|
|
301
|
+
}
|
|
302
|
+
return counts;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (getErrorCode(error) === "ENOENT") return { architect: 0, critic: 0 };
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Load index rows for cap enforcement. Unlike HUD reads, returns structural
|
|
311
|
+
* signals so callers can fail closed when the ledger is empty/malformed while
|
|
312
|
+
* opener artifacts already exist on disk.
|
|
313
|
+
*/
|
|
314
|
+
export async function loadRalplanIndexForCap(
|
|
315
|
+
cwd: string,
|
|
316
|
+
sessionId: string,
|
|
317
|
+
runId: string,
|
|
318
|
+
): Promise<{
|
|
319
|
+
rows: RalplanIndexRow[];
|
|
320
|
+
indexPresent: boolean;
|
|
321
|
+
parseableLines: number;
|
|
322
|
+
rawLineCount: number;
|
|
323
|
+
rawText?: string;
|
|
324
|
+
}> {
|
|
325
|
+
const indexPath = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId, "index.jsonl");
|
|
326
|
+
try {
|
|
327
|
+
const text = await fs.readFile(indexPath, "utf8");
|
|
328
|
+
const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
|
|
329
|
+
const rows: RalplanIndexRow[] = [];
|
|
330
|
+
for (const line of lines) {
|
|
331
|
+
const row = parseRalplanIndexLine(line);
|
|
332
|
+
if (row) rows.push(row);
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
rows,
|
|
336
|
+
indexPresent: true,
|
|
337
|
+
parseableLines: rows.length,
|
|
338
|
+
rawLineCount: lines.length,
|
|
339
|
+
rawText: text,
|
|
340
|
+
};
|
|
341
|
+
} catch (error) {
|
|
342
|
+
const code =
|
|
343
|
+
error && typeof error === "object" && "code" in error ? (error as { code?: string }).code : undefined;
|
|
344
|
+
if (code === "ENOENT") {
|
|
345
|
+
return { rows: [], indexPresent: false, parseableLines: 0, rawLineCount: 0 };
|
|
346
|
+
}
|
|
347
|
+
// Unreadable index: treat as present-but-untrusted empty parse.
|
|
348
|
+
return { rows: [], indexPresent: true, parseableLines: 0, rawLineCount: 0 };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function parseBoundedPositiveInteger(value: unknown, limit: number): number | null {
|
|
353
|
+
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 1 && value <= limit
|
|
354
|
+
? value
|
|
355
|
+
: null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function parseMaxIterationsValue(value: unknown): number | null {
|
|
359
|
+
return parseBoundedPositiveInteger(value, RALPLAN_MAX_ITERATIONS_LIMIT);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function readSettingsMaxIterations(settingsPath: string): Promise<number | null> {
|
|
363
|
+
try {
|
|
364
|
+
const raw = await Bun.file(settingsPath).text();
|
|
365
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
366
|
+
const flat = parseMaxIterationsValue(parsed["skc.ralplan.maxIterations"]);
|
|
367
|
+
if (flat !== null) return flat;
|
|
368
|
+
const skc = parsed.skc;
|
|
369
|
+
if (skc && typeof skc === "object") {
|
|
370
|
+
const ralplan = (skc as Record<string, unknown>).ralplan;
|
|
371
|
+
if (ralplan && typeof ralplan === "object") {
|
|
372
|
+
return parseMaxIterationsValue((ralplan as Record<string, unknown>).maxIterations);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
} catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Resolve ralplan consensus iteration cap. Project `./.skc/settings.json` overrides
|
|
383
|
+
* user settings, else default 5.
|
|
384
|
+
*/
|
|
385
|
+
export async function resolveRalplanMaxIterations(cwd: string): Promise<{ maxIterations: number; source: string }> {
|
|
386
|
+
const projectPath = path.join(skcRoot(cwd), "settings.json");
|
|
387
|
+
const project = await readSettingsMaxIterations(projectPath);
|
|
388
|
+
if (project !== null) return { maxIterations: project, source: projectPath };
|
|
389
|
+
const userPath = path.join(getConfigRootDir(), "settings.json");
|
|
390
|
+
const user = await readSettingsMaxIterations(userPath);
|
|
391
|
+
if (user !== null) return { maxIterations: user, source: userPath };
|
|
392
|
+
return { maxIterations: RALPLAN_DEFAULT_MAX_ITERATIONS, source: "default" };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function parseMaxReviewPassesPerLaneValue(value: unknown): number | null {
|
|
396
|
+
return parseBoundedPositiveInteger(value, RALPLAN_MAX_REVIEW_PASSES_PER_LANE_LIMIT);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
type RalplanReviewPassesPerLaneSetting =
|
|
400
|
+
| { kind: "absent" }
|
|
401
|
+
| { kind: "valid"; value: number }
|
|
402
|
+
| { kind: "invalid"; reason: string };
|
|
403
|
+
|
|
404
|
+
function parsePresentMaxReviewPassesPerLane(value: unknown): RalplanReviewPassesPerLaneSetting {
|
|
405
|
+
const parsed = parseMaxReviewPassesPerLaneValue(value);
|
|
406
|
+
return parsed === null
|
|
407
|
+
? {
|
|
408
|
+
kind: "invalid",
|
|
409
|
+
reason:
|
|
410
|
+
"expected skc.ralplan.maxReviewPassesPerLane to be an integer between 1 and " +
|
|
411
|
+
RALPLAN_MAX_REVIEW_PASSES_PER_LANE_LIMIT,
|
|
412
|
+
}
|
|
413
|
+
: { kind: "valid", value: parsed };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function parseMaxReviewPassesPerLaneSettings(parsed: unknown): RalplanReviewPassesPerLaneSetting {
|
|
417
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { kind: "absent" };
|
|
418
|
+
const settings = parsed as Record<string, unknown>;
|
|
419
|
+
if (Object.hasOwn(settings, "skc.ralplan.maxReviewPassesPerLane")) {
|
|
420
|
+
return parsePresentMaxReviewPassesPerLane(settings["skc.ralplan.maxReviewPassesPerLane"]);
|
|
421
|
+
}
|
|
422
|
+
const skc = settings.skc;
|
|
423
|
+
if (!skc || typeof skc !== "object" || Array.isArray(skc)) return { kind: "absent" };
|
|
424
|
+
const ralplan = (skc as Record<string, unknown>).ralplan;
|
|
425
|
+
if (!ralplan || typeof ralplan !== "object" || Array.isArray(ralplan)) return { kind: "absent" };
|
|
426
|
+
const ralplanSettings = ralplan as Record<string, unknown>;
|
|
427
|
+
if (!Object.hasOwn(ralplanSettings, "maxReviewPassesPerLane")) return { kind: "absent" };
|
|
428
|
+
return parsePresentMaxReviewPassesPerLane(ralplanSettings.maxReviewPassesPerLane);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function readSettingsMaxReviewPassesPerLane(settingsPath: string): Promise<RalplanReviewPassesPerLaneSetting> {
|
|
432
|
+
let raw: string;
|
|
433
|
+
try {
|
|
434
|
+
raw = await Bun.file(settingsPath).text();
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (getErrorCode(error) === "ENOENT") return { kind: "absent" };
|
|
437
|
+
return {
|
|
438
|
+
kind: "invalid",
|
|
439
|
+
reason: `unable to read settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
let parsed: unknown;
|
|
443
|
+
try {
|
|
444
|
+
parsed = JSON.parse(raw);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
return {
|
|
447
|
+
kind: "invalid",
|
|
448
|
+
reason: `malformed JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
return parseMaxReviewPassesPerLaneSettings(parsed);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Resolve the per-lane review-pass budget with project-over-user precedence. */
|
|
455
|
+
export async function resolveRalplanMaxReviewPassesPerLane(
|
|
456
|
+
cwd: string,
|
|
457
|
+
): Promise<{ maxReviewPassesPerLane: number; source: string }> {
|
|
458
|
+
const projectPath = path.join(skcRoot(cwd), "settings.json");
|
|
459
|
+
const project = await readSettingsMaxReviewPassesPerLane(projectPath);
|
|
460
|
+
if (project.kind === "invalid") {
|
|
461
|
+
throw new RalplanCommandError(2, `invalid ralplan settings at ${projectPath}: ${project.reason}`);
|
|
462
|
+
}
|
|
463
|
+
if (project.kind === "valid") return { maxReviewPassesPerLane: project.value, source: projectPath };
|
|
464
|
+
const userPath = path.join(getConfigRootDir(), "settings.json");
|
|
465
|
+
const user = await readSettingsMaxReviewPassesPerLane(userPath);
|
|
466
|
+
if (user.kind === "invalid") {
|
|
467
|
+
throw new RalplanCommandError(2, `invalid ralplan settings at ${userPath}: ${user.reason}`);
|
|
468
|
+
}
|
|
469
|
+
if (user.kind === "valid") return { maxReviewPassesPerLane: user.value, source: userPath };
|
|
470
|
+
return { maxReviewPassesPerLane: RALPLAN_DEFAULT_MAX_REVIEW_PASSES_PER_LANE, source: "default" };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function buildPlanningStuckResult(input: {
|
|
474
|
+
json: boolean;
|
|
475
|
+
stage: RalplanStage;
|
|
476
|
+
stageN: number;
|
|
477
|
+
runId: string;
|
|
478
|
+
decision: Extract<RalplanIterationCapDecision, { allowed: false }>;
|
|
479
|
+
source: string;
|
|
480
|
+
}): RalplanCommandResult {
|
|
481
|
+
const detail =
|
|
482
|
+
`${PLANNING_STUCK_MARKER}: ${input.decision.reason} ` +
|
|
483
|
+
`(run_id=${input.runId}, stage=${input.stage}, stage_n=${input.stageN}, source=${input.source}). ` +
|
|
484
|
+
`Stop opening planner/revision passes; escalate the best existing plan via final/pending-approval without auto-implementation.`;
|
|
485
|
+
if (input.json) {
|
|
486
|
+
return {
|
|
487
|
+
status: 3,
|
|
488
|
+
stdout: `${JSON.stringify(
|
|
489
|
+
{
|
|
490
|
+
ok: false,
|
|
491
|
+
planning_stuck: true,
|
|
492
|
+
marker: PLANNING_STUCK_MARKER,
|
|
493
|
+
run_id: input.runId,
|
|
494
|
+
stage: input.stage,
|
|
495
|
+
stage_n: input.stageN,
|
|
496
|
+
iteration: input.decision.currentIterations,
|
|
497
|
+
projected_iteration: input.decision.projectedIterations,
|
|
498
|
+
max_iterations: input.decision.maxIterations,
|
|
499
|
+
max_iterations_source: input.source,
|
|
500
|
+
reason: input.decision.reason,
|
|
501
|
+
},
|
|
502
|
+
null,
|
|
503
|
+
2,
|
|
504
|
+
)}\n`,
|
|
505
|
+
stderr: `${detail}\n`,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
status: 3,
|
|
510
|
+
stdout: `${PLANNING_STUCK_MARKER}\n`,
|
|
511
|
+
stderr: `${detail}\n`,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function buildLaneBudgetStuckResult(input: {
|
|
516
|
+
json: boolean;
|
|
517
|
+
stage: RalplanStage;
|
|
518
|
+
stageN: number;
|
|
519
|
+
runId: string;
|
|
520
|
+
decision: Extract<RalplanReviewLaneBudgetDecision, { allowed: false }>;
|
|
521
|
+
source: string;
|
|
522
|
+
}): RalplanCommandResult {
|
|
523
|
+
const detail =
|
|
524
|
+
`${PLANNING_STUCK_MARKER}: ${input.decision.reason} ` +
|
|
525
|
+
`(run_id=${input.runId}, stage=${input.stage}, stage_n=${input.stageN}, source=${input.source}). ` +
|
|
526
|
+
`Stop re-invoking the ${input.decision.lane} review lane in this consensus iteration; ` +
|
|
527
|
+
"route a rule-2-justified blocker through a Planner revision opener (fresh lane budget) while opener budget remains, " +
|
|
528
|
+
"or escalate the best existing plan via post-interview/adr/final without auto-implementation.";
|
|
529
|
+
if (input.json) {
|
|
530
|
+
return {
|
|
531
|
+
status: 3,
|
|
532
|
+
stdout: `${JSON.stringify(
|
|
533
|
+
{
|
|
534
|
+
ok: false,
|
|
535
|
+
planning_stuck: true,
|
|
536
|
+
marker: PLANNING_STUCK_MARKER,
|
|
537
|
+
run_id: input.runId,
|
|
538
|
+
stage: input.stage,
|
|
539
|
+
stage_n: input.stageN,
|
|
540
|
+
lane: input.decision.lane,
|
|
541
|
+
passes: input.decision.currentPasses,
|
|
542
|
+
projected_passes: input.decision.projectedPasses,
|
|
543
|
+
max_review_passes_per_lane: input.decision.maxReviewPassesPerLane,
|
|
544
|
+
max_review_passes_source: input.source,
|
|
545
|
+
reason: input.decision.reason,
|
|
546
|
+
},
|
|
547
|
+
null,
|
|
548
|
+
2,
|
|
549
|
+
)}\n`,
|
|
550
|
+
stderr: `${detail}\n`,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
status: 3,
|
|
555
|
+
stdout: `${PLANNING_STUCK_MARKER}\n`,
|
|
556
|
+
stderr: `${detail}\n`,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
57
559
|
|
|
58
560
|
const KNOWN_ARCHITECT_KINDS = new Set(["openai-code"]);
|
|
59
561
|
const KNOWN_CRITIC_KINDS = new Set(["openai-code"]);
|
|
@@ -87,10 +589,15 @@ const VALUE_FLAGS = new Set([
|
|
|
87
589
|
"--critic",
|
|
88
590
|
"--planner-id",
|
|
89
591
|
"--planner-resumable",
|
|
592
|
+
"--architect-id",
|
|
593
|
+
"--architect-resumable",
|
|
594
|
+
"--critic-id",
|
|
595
|
+
"--critic-resumable",
|
|
90
596
|
"--fallback-reason",
|
|
91
597
|
"--fallback-attempted-id",
|
|
92
598
|
"--fallback-stage-n",
|
|
93
599
|
"--fallback-receipt-path",
|
|
600
|
+
"--lane-verdict",
|
|
94
601
|
]);
|
|
95
602
|
|
|
96
603
|
export function isRalplanArtifactWriteInvocation(args: readonly string[]): boolean {
|
|
@@ -179,6 +686,43 @@ async function readActiveRunId(cwd: string, sessionId: string): Promise<string |
|
|
|
179
686
|
return candidate;
|
|
180
687
|
}
|
|
181
688
|
|
|
689
|
+
/**
|
|
690
|
+
* Read the authoritative repository binding from ralplan run state and fail closed
|
|
691
|
+
* when the active cwd no longer matches (handoff / QA / stage write) (#2901).
|
|
692
|
+
*/
|
|
693
|
+
async function enforceRalplanRepositoryBinding(cwd: string, sessionId: string): Promise<RepositoryBinding> {
|
|
694
|
+
const statePath = ralplanStatePath(cwd, sessionId);
|
|
695
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
696
|
+
if (existingRead.kind === "corrupt") {
|
|
697
|
+
throw new RalplanCommandError(
|
|
698
|
+
2,
|
|
699
|
+
`existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to proceed at ${statePath}`,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
if (existingRead.kind === "absent") {
|
|
703
|
+
// No prior seed — capture live authority so subsequent handoffs still have a stamp.
|
|
704
|
+
return publicRepositoryBinding(await captureRepositoryBinding(cwd, { displayPath: cwd }));
|
|
705
|
+
}
|
|
706
|
+
const raw = existingRead.value.repository_binding ?? existingRead.value.repositoryBinding;
|
|
707
|
+
try {
|
|
708
|
+
if (raw === undefined) {
|
|
709
|
+
// Legacy seeds without a field: stamp current cwd and require future writes match it.
|
|
710
|
+
return publicRepositoryBinding(await captureRepositoryBinding(cwd, { displayPath: cwd }));
|
|
711
|
+
}
|
|
712
|
+
const binding = parseRepositoryBinding(raw);
|
|
713
|
+
await assertCwdMatchesRepositoryBinding(cwd, binding);
|
|
714
|
+
return publicRepositoryBinding(binding);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
if (error instanceof RepositoryBindingError) {
|
|
717
|
+
throw new RalplanCommandError(2, `ralplan repository binding rejected: ${error.message}`);
|
|
718
|
+
}
|
|
719
|
+
throw new RalplanCommandError(
|
|
720
|
+
2,
|
|
721
|
+
`ralplan repository binding rejected: ${error instanceof Error ? error.message : String(error)}`,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
182
726
|
/**
|
|
183
727
|
* Run-state phases that an artifact write must never reopen. Once ralplan has
|
|
184
728
|
* reached a terminal/handed-off phase, a stray `--write` must not regress
|
|
@@ -213,6 +757,13 @@ async function persistActiveRunId(cwd: string, sessionId: string, runId: string,
|
|
|
213
757
|
// "complete"/"handoff" and disarm the Stop hook). PHASE_LOCK only guards same-run writes.
|
|
214
758
|
const isNewRun = existing.run_id !== runId;
|
|
215
759
|
const nextPhase = isNewRun ? stage : advanceCurrentPhase(existing.current_phase, stage);
|
|
760
|
+
if (isNewRun) {
|
|
761
|
+
// State writes shallow-merge, so clear both HUD verdict sources at the run boundary.
|
|
762
|
+
delete existing.verdict;
|
|
763
|
+
for (const key of Object.keys(existing)) {
|
|
764
|
+
if (key.startsWith("last_review_verdict")) delete existing[key];
|
|
765
|
+
}
|
|
766
|
+
}
|
|
216
767
|
if (
|
|
217
768
|
existing.run_id === runId &&
|
|
218
769
|
existing.version === WORKFLOW_STATE_VERSION &&
|
|
@@ -231,6 +782,7 @@ async function persistActiveRunId(cwd: string, sessionId: string, runId: string,
|
|
|
231
782
|
existing.updated_at = new Date().toISOString();
|
|
232
783
|
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
233
784
|
cwd,
|
|
785
|
+
lockHeld: true,
|
|
234
786
|
receipt: { cwd, skill: "ralplan", owner: "skc-runtime", command: "skc ralplan persist-run-id", sessionId },
|
|
235
787
|
audit: { category: "state", verb: "write", owner: "skc-runtime", skill: "ralplan", sessionId },
|
|
236
788
|
});
|
|
@@ -239,9 +791,12 @@ async function persistActiveRunId(cwd: string, sessionId: string, runId: string,
|
|
|
239
791
|
);
|
|
240
792
|
}
|
|
241
793
|
|
|
242
|
-
/*
|
|
794
|
+
/* ---------------------- persisted role-agent run-state --------------------- */
|
|
795
|
+
|
|
796
|
+
type PersistedRole = "planner" | RalplanReviewLane;
|
|
243
797
|
|
|
244
|
-
interface
|
|
798
|
+
interface PersistedRoleStateUpdate {
|
|
799
|
+
role: PersistedRole;
|
|
245
800
|
subagentId?: string;
|
|
246
801
|
resumable?: boolean;
|
|
247
802
|
fallbackReason?: string;
|
|
@@ -250,6 +805,25 @@ interface PlannerStateUpdate {
|
|
|
250
805
|
fallbackReceiptPath?: string;
|
|
251
806
|
}
|
|
252
807
|
|
|
808
|
+
interface LaneVerdictUpdate {
|
|
809
|
+
lane: RalplanReviewLane;
|
|
810
|
+
verdict: string;
|
|
811
|
+
stageN: number;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const PERSISTED_ROLES = ["planner", "architect", "critic"] as const;
|
|
815
|
+
|
|
816
|
+
const PERSISTED_ROLE_FLAGS: Record<PersistedRole, { id: string; resumable: string }> = {
|
|
817
|
+
planner: { id: "--planner-id", resumable: "--planner-resumable" },
|
|
818
|
+
architect: { id: "--architect-id", resumable: "--architect-resumable" },
|
|
819
|
+
critic: { id: "--critic-id", resumable: "--critic-resumable" },
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const LANE_VERDICTS: Record<RalplanReviewLane, ReadonlySet<string>> = {
|
|
823
|
+
architect: new Set(["CLEAR", "WATCH", "BLOCK"]),
|
|
824
|
+
critic: new Set(["OKAY", "ITERATE", "REJECT"]),
|
|
825
|
+
};
|
|
826
|
+
|
|
253
827
|
function parseBooleanFlag(raw: string, flag: string): boolean {
|
|
254
828
|
if (raw === "true") return true;
|
|
255
829
|
if (raw === "false") return false;
|
|
@@ -262,7 +836,7 @@ function assertSubagentId(value: string, label: string): void {
|
|
|
262
836
|
}
|
|
263
837
|
}
|
|
264
838
|
|
|
265
|
-
function
|
|
839
|
+
function roleStateFlagValue(args: readonly string[], flag: string): string | undefined {
|
|
266
840
|
const value = flagValue(args, flag);
|
|
267
841
|
if (value === undefined && hasFlag(args, flag)) {
|
|
268
842
|
throw new RalplanCommandError(2, `missing value for ${flag}.`);
|
|
@@ -270,19 +844,64 @@ function plannerFlagValue(args: readonly string[], flag: string): string | undef
|
|
|
270
844
|
return value;
|
|
271
845
|
}
|
|
272
846
|
|
|
847
|
+
function persistedRoleForStage(stage: RalplanStage): PersistedRole | undefined {
|
|
848
|
+
if (stage === "planner" || stage === "revision") return "planner";
|
|
849
|
+
if (stage === "architect" || stage === "critic") return stage;
|
|
850
|
+
return undefined;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function suppliedPersistedRoleFlag(args: readonly string[], role: PersistedRole): string | undefined {
|
|
854
|
+
const flags = PERSISTED_ROLE_FLAGS[role];
|
|
855
|
+
if (hasFlag(args, flags.id)) return flags.id;
|
|
856
|
+
if (hasFlag(args, flags.resumable)) return flags.resumable;
|
|
857
|
+
return undefined;
|
|
858
|
+
}
|
|
859
|
+
|
|
273
860
|
/**
|
|
274
|
-
* Parse the optional persisted-
|
|
275
|
-
* `--write`.
|
|
276
|
-
*
|
|
277
|
-
*
|
|
861
|
+
* Parse the optional same-session persisted role-agent metadata that may ride
|
|
862
|
+
* alongside a `--write`. Planner metadata rides planner/revision stages;
|
|
863
|
+
* Architect and Critic metadata ride only their matching review lane. This is
|
|
864
|
+
* an audit/routing hint, not a durable subagent registry.
|
|
278
865
|
*/
|
|
279
|
-
function
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
|
|
866
|
+
function parsePersistedRoleStateArgs(
|
|
867
|
+
args: readonly string[],
|
|
868
|
+
stage: RalplanStage,
|
|
869
|
+
): PersistedRoleStateUpdate | undefined {
|
|
870
|
+
const role = persistedRoleForStage(stage);
|
|
871
|
+
for (const candidate of PERSISTED_ROLES) {
|
|
872
|
+
const suppliedFlag = suppliedPersistedRoleFlag(args, candidate);
|
|
873
|
+
if (suppliedFlag && candidate !== role) {
|
|
874
|
+
const expectedStages = candidate === "planner" ? "planner or revision" : candidate;
|
|
875
|
+
throw new RalplanCommandError(
|
|
876
|
+
2,
|
|
877
|
+
`${suppliedFlag} is only valid with --stage ${expectedStages} (received ${stage}).`,
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const fallbackFlagsPresent = [
|
|
883
|
+
"--fallback-reason",
|
|
884
|
+
"--fallback-attempted-id",
|
|
885
|
+
"--fallback-stage-n",
|
|
886
|
+
"--fallback-receipt-path",
|
|
887
|
+
].some(flag => hasFlag(args, flag));
|
|
888
|
+
if (!role) {
|
|
889
|
+
if (fallbackFlagsPresent) {
|
|
890
|
+
throw new RalplanCommandError(
|
|
891
|
+
2,
|
|
892
|
+
`--fallback-reason is only valid with --stage planner, revision, architect, or critic (received ${stage}).`,
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
return undefined;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const flags = PERSISTED_ROLE_FLAGS[role];
|
|
899
|
+
const subagentId = roleStateFlagValue(args, flags.id);
|
|
900
|
+
const resumableRaw = roleStateFlagValue(args, flags.resumable);
|
|
901
|
+
const fallbackReason = roleStateFlagValue(args, "--fallback-reason");
|
|
902
|
+
const fallbackAttemptedId = roleStateFlagValue(args, "--fallback-attempted-id");
|
|
903
|
+
const fallbackStageNRaw = roleStateFlagValue(args, "--fallback-stage-n");
|
|
904
|
+
const fallbackReceiptPath = roleStateFlagValue(args, "--fallback-receipt-path");
|
|
286
905
|
|
|
287
906
|
const anyPresent = [
|
|
288
907
|
subagentId,
|
|
@@ -294,14 +913,13 @@ function parsePlannerStateArgs(args: readonly string[]): PlannerStateUpdate | un
|
|
|
294
913
|
].some(value => value !== undefined);
|
|
295
914
|
if (!anyPresent) return undefined;
|
|
296
915
|
|
|
297
|
-
const update:
|
|
298
|
-
|
|
916
|
+
const update: PersistedRoleStateUpdate = { role };
|
|
299
917
|
if (subagentId !== undefined) {
|
|
300
|
-
assertSubagentId(subagentId,
|
|
918
|
+
assertSubagentId(subagentId, flags.id);
|
|
301
919
|
update.subagentId = subagentId;
|
|
302
920
|
}
|
|
303
921
|
if (resumableRaw !== undefined) {
|
|
304
|
-
update.resumable = parseBooleanFlag(resumableRaw,
|
|
922
|
+
update.resumable = parseBooleanFlag(resumableRaw, flags.resumable);
|
|
305
923
|
}
|
|
306
924
|
|
|
307
925
|
const anyFallback = [fallbackReason, fallbackAttemptedId, fallbackStageNRaw, fallbackReceiptPath].some(
|
|
@@ -309,7 +927,7 @@ function parsePlannerStateArgs(args: readonly string[]): PlannerStateUpdate | un
|
|
|
309
927
|
);
|
|
310
928
|
if (anyFallback) {
|
|
311
929
|
if (!fallbackReason) {
|
|
312
|
-
throw new RalplanCommandError(2,
|
|
930
|
+
throw new RalplanCommandError(2, `--fallback-reason is required when recording ${role} fallback metadata.`);
|
|
313
931
|
}
|
|
314
932
|
if (!KNOWN_FALLBACK_REASONS.has(fallbackReason)) {
|
|
315
933
|
throw new RalplanCommandError(
|
|
@@ -321,13 +939,13 @@ function parsePlannerStateArgs(args: readonly string[]): PlannerStateUpdate | un
|
|
|
321
939
|
if (fallbackAttemptedId === undefined) {
|
|
322
940
|
throw new RalplanCommandError(
|
|
323
941
|
2,
|
|
324
|
-
|
|
942
|
+
`--fallback-attempted-id is required when recording ${role} fallback metadata.`,
|
|
325
943
|
);
|
|
326
944
|
}
|
|
327
945
|
assertSubagentId(fallbackAttemptedId, "--fallback-attempted-id");
|
|
328
946
|
update.fallbackAttemptedId = fallbackAttemptedId;
|
|
329
947
|
if (fallbackStageNRaw === undefined) {
|
|
330
|
-
throw new RalplanCommandError(2,
|
|
948
|
+
throw new RalplanCommandError(2, `--fallback-stage-n is required when recording ${role} fallback metadata.`);
|
|
331
949
|
}
|
|
332
950
|
update.fallbackStageN = parseStageN(fallbackStageNRaw);
|
|
333
951
|
if (fallbackReceiptPath !== undefined) {
|
|
@@ -341,24 +959,63 @@ function parsePlannerStateArgs(args: readonly string[]): PlannerStateUpdate | un
|
|
|
341
959
|
return update;
|
|
342
960
|
}
|
|
343
961
|
|
|
344
|
-
/**
|
|
345
|
-
function
|
|
962
|
+
/** Parse the self-reported review verdict carried by a lane artifact write. */
|
|
963
|
+
function parseLaneVerdictArgs(
|
|
964
|
+
args: readonly string[],
|
|
965
|
+
stage: RalplanStage,
|
|
966
|
+
stageN: number,
|
|
967
|
+
): LaneVerdictUpdate | undefined {
|
|
968
|
+
const rawVerdict = roleStateFlagValue(args, "--lane-verdict");
|
|
969
|
+
if (rawVerdict === undefined) return undefined;
|
|
970
|
+
if (stage !== "architect" && stage !== "critic") {
|
|
971
|
+
throw new RalplanCommandError(
|
|
972
|
+
2,
|
|
973
|
+
`--lane-verdict is only valid with --stage architect or critic (received ${stage}).`,
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
const verdict = rawVerdict.trim().toUpperCase();
|
|
977
|
+
if (!LANE_VERDICTS[stage].has(verdict)) {
|
|
978
|
+
throw new RalplanCommandError(
|
|
979
|
+
2,
|
|
980
|
+
`invalid --lane-verdict for ${stage}: ${rawVerdict}. Expected one of: ${[...LANE_VERDICTS[stage]].join(", ")}.`,
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
return { lane: stage, verdict, stageN };
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Snake-case projection of persisted role metadata for state JSON + receipts.
|
|
988
|
+
* Omitted fields stay absent — an unknown resumability value is never encoded
|
|
989
|
+
* as literal null.
|
|
990
|
+
*/
|
|
991
|
+
function persistedRoleStatePayload(update: PersistedRoleStateUpdate): Record<string, unknown> {
|
|
346
992
|
const payload: Record<string, unknown> = {};
|
|
347
|
-
|
|
348
|
-
if (update.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (update.
|
|
352
|
-
if (update.
|
|
993
|
+
const prefix = update.role;
|
|
994
|
+
if (update.subagentId !== undefined) {
|
|
995
|
+
payload[prefix === "planner" ? "planner_subagent_id" : `${prefix}_id`] = update.subagentId;
|
|
996
|
+
}
|
|
997
|
+
if (update.resumable !== undefined) payload[`${prefix}_resumable`] = update.resumable;
|
|
998
|
+
if (update.fallbackReason !== undefined) payload[`${prefix}_fallback_reason`] = update.fallbackReason;
|
|
999
|
+
if (update.fallbackAttemptedId !== undefined)
|
|
1000
|
+
payload[`${prefix}_fallback_attempted_id`] = update.fallbackAttemptedId;
|
|
1001
|
+
if (update.fallbackStageN !== undefined) payload[`${prefix}_fallback_stage_n`] = update.fallbackStageN;
|
|
1002
|
+
if (update.fallbackReceiptPath !== undefined) {
|
|
1003
|
+
payload[`${prefix}_fallback_receipt_path`] = update.fallbackReceiptPath;
|
|
1004
|
+
}
|
|
353
1005
|
return payload;
|
|
354
1006
|
}
|
|
355
1007
|
|
|
356
1008
|
/**
|
|
357
|
-
* Merge persisted-
|
|
358
|
-
* audit/routing hint only
|
|
359
|
-
*
|
|
1009
|
+
* Merge persisted role-agent metadata into the ralplan run-state JSON, optionally only for its active run.
|
|
1010
|
+
* This is a same-session audit/routing hint only; it does not create a durable
|
|
1011
|
+
* cross-process subagent registry, ledger entry, or additional state file.
|
|
360
1012
|
*/
|
|
361
|
-
async function
|
|
1013
|
+
async function applyPersistedRoleStateUpdate(
|
|
1014
|
+
cwd: string,
|
|
1015
|
+
sessionId: string,
|
|
1016
|
+
update: PersistedRoleStateUpdate,
|
|
1017
|
+
expectedActiveRunId?: string,
|
|
1018
|
+
): Promise<boolean> {
|
|
362
1019
|
const statePath = ralplanStatePath(cwd, sessionId);
|
|
363
1020
|
return await withWorkflowStateLock(
|
|
364
1021
|
statePath,
|
|
@@ -371,7 +1028,8 @@ async function applyPlannerStateUpdate(cwd: string, sessionId: string, update: P
|
|
|
371
1028
|
);
|
|
372
1029
|
}
|
|
373
1030
|
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
374
|
-
|
|
1031
|
+
if (expectedActiveRunId !== undefined && existing.run_id !== expectedActiveRunId) return false;
|
|
1032
|
+
Object.assign(existing, persistedRoleStatePayload(update));
|
|
375
1033
|
if (typeof existing.skill !== "string") existing.skill = "ralplan";
|
|
376
1034
|
if (typeof existing.active !== "boolean") existing.active = true;
|
|
377
1035
|
if (typeof existing.current_phase !== "string") existing.current_phase = "planner";
|
|
@@ -379,9 +1037,59 @@ async function applyPlannerStateUpdate(cwd: string, sessionId: string, update: P
|
|
|
379
1037
|
existing.updated_at = new Date().toISOString();
|
|
380
1038
|
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
381
1039
|
cwd,
|
|
382
|
-
|
|
1040
|
+
lockHeld: true,
|
|
1041
|
+
receipt: {
|
|
1042
|
+
cwd,
|
|
1043
|
+
skill: "ralplan",
|
|
1044
|
+
owner: "skc-runtime",
|
|
1045
|
+
command: `skc ralplan ${update.role}-state`,
|
|
1046
|
+
sessionId,
|
|
1047
|
+
},
|
|
383
1048
|
audit: { category: "state", verb: "write", owner: "skc-runtime", skill: "ralplan", sessionId },
|
|
384
1049
|
});
|
|
1050
|
+
return true;
|
|
1051
|
+
},
|
|
1052
|
+
{ cwd },
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/** Merge a lane's self-reported verdict into the same-session ralplan run state, optionally only for its active run. */
|
|
1057
|
+
async function applyLaneVerdictUpdate(
|
|
1058
|
+
cwd: string,
|
|
1059
|
+
sessionId: string,
|
|
1060
|
+
update: LaneVerdictUpdate,
|
|
1061
|
+
expectedActiveRunId?: string,
|
|
1062
|
+
): Promise<boolean> {
|
|
1063
|
+
const statePath = ralplanStatePath(cwd, sessionId);
|
|
1064
|
+
return await withWorkflowStateLock(
|
|
1065
|
+
statePath,
|
|
1066
|
+
async () => {
|
|
1067
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
1068
|
+
if (existingRead.kind === "corrupt") {
|
|
1069
|
+
throw new RalplanCommandError(
|
|
1070
|
+
2,
|
|
1071
|
+
`existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to overwrite ${statePath}`,
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1075
|
+
if (expectedActiveRunId !== undefined && existing.run_id !== expectedActiveRunId) return false;
|
|
1076
|
+
Object.assign(existing, {
|
|
1077
|
+
last_review_verdict: update.verdict,
|
|
1078
|
+
last_review_verdict_lane: update.lane,
|
|
1079
|
+
last_review_verdict_stage_n: update.stageN,
|
|
1080
|
+
});
|
|
1081
|
+
if (typeof existing.skill !== "string") existing.skill = "ralplan";
|
|
1082
|
+
if (typeof existing.active !== "boolean") existing.active = true;
|
|
1083
|
+
if (typeof existing.current_phase !== "string") existing.current_phase = "planner";
|
|
1084
|
+
existing = migrateWorkflowState(existing, "ralplan").state;
|
|
1085
|
+
existing.updated_at = new Date().toISOString();
|
|
1086
|
+
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
1087
|
+
cwd,
|
|
1088
|
+
lockHeld: true,
|
|
1089
|
+
receipt: { cwd, skill: "ralplan", owner: "skc-runtime", command: "skc ralplan lane-verdict", sessionId },
|
|
1090
|
+
audit: { category: "state", verb: "write", owner: "skc-runtime", skill: "ralplan", sessionId },
|
|
1091
|
+
});
|
|
1092
|
+
return true;
|
|
385
1093
|
},
|
|
386
1094
|
{ cwd },
|
|
387
1095
|
);
|
|
@@ -532,28 +1240,19 @@ interface ExistingStageArtifact {
|
|
|
532
1240
|
}
|
|
533
1241
|
|
|
534
1242
|
/**
|
|
535
|
-
* Find the most recent `index.jsonl` row for a `(stage, stage_n)` pair
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
* truth for dedup because it is exactly what a duplicate write would corrupt.
|
|
1243
|
+
* Find the most recent complete `index.jsonl` row for a `(stage, stage_n)` pair
|
|
1244
|
+
* in the single ledger snapshot used by the dedupe guard and both budget gates.
|
|
1245
|
+
* A parseable row missing `path` or `sha256` is intentionally treated as missing
|
|
1246
|
+
* so the deterministic on-disk probe can repair the crash gap.
|
|
540
1247
|
*/
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
sessionId: string,
|
|
544
|
-
runId: string,
|
|
1248
|
+
function findExistingStageArtifact(
|
|
1249
|
+
indexText: string | undefined,
|
|
545
1250
|
stage: RalplanStage,
|
|
546
1251
|
stageN: number,
|
|
547
|
-
):
|
|
548
|
-
|
|
549
|
-
let text: string;
|
|
550
|
-
try {
|
|
551
|
-
text = await fs.readFile(indexPath, "utf8");
|
|
552
|
-
} catch {
|
|
553
|
-
return undefined;
|
|
554
|
-
}
|
|
1252
|
+
): ExistingStageArtifact | undefined {
|
|
1253
|
+
if (indexText === undefined) return undefined;
|
|
555
1254
|
let match: ExistingStageArtifact | undefined;
|
|
556
|
-
for (const line of
|
|
1255
|
+
for (const line of indexText.split(/\r?\n/)) {
|
|
557
1256
|
const trimmed = line.trim();
|
|
558
1257
|
if (!trimmed) continue;
|
|
559
1258
|
let row: unknown;
|
|
@@ -575,6 +1274,124 @@ async function findExistingStageArtifact(
|
|
|
575
1274
|
return match;
|
|
576
1275
|
}
|
|
577
1276
|
|
|
1277
|
+
interface OnDiskStageArtifact {
|
|
1278
|
+
path: string;
|
|
1279
|
+
sha256: string;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/** Probe the deterministic artifact path left by a crash before ledger append. */
|
|
1283
|
+
async function findOnDiskStageArtifact(
|
|
1284
|
+
cwd: string,
|
|
1285
|
+
resolved: Pick<ResolvedArtifactArgs, "sessionId" | "runId" | "stage" | "stageN">,
|
|
1286
|
+
): Promise<OnDiskStageArtifact | undefined> {
|
|
1287
|
+
const filePath = path.join(
|
|
1288
|
+
sessionPlansDir(cwd, resolved.sessionId),
|
|
1289
|
+
"ralplan",
|
|
1290
|
+
resolved.runId,
|
|
1291
|
+
`stage-${pad2(resolved.stageN)}-${resolved.stage}.md`,
|
|
1292
|
+
);
|
|
1293
|
+
try {
|
|
1294
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
1295
|
+
return { path: filePath, sha256: createHash("sha256").update(content).digest("hex") };
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
const code = getErrorCode(error);
|
|
1298
|
+
if (code === "ENOENT" || code === "ENOTDIR") return undefined;
|
|
1299
|
+
throw error;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/** Ensure a deduplicated final stage still has its byte-identical pending-approval copy. */
|
|
1304
|
+
async function ensureFinalPendingApproval(
|
|
1305
|
+
cwd: string,
|
|
1306
|
+
resolved: Pick<ResolvedArtifactArgs, "sessionId" | "runId" | "stageN">,
|
|
1307
|
+
stageArtifact: Pick<OnDiskStageArtifact, "path" | "sha256">,
|
|
1308
|
+
): Promise<string> {
|
|
1309
|
+
const pendingApprovalPath = path.join(
|
|
1310
|
+
sessionPlansDir(cwd, resolved.sessionId),
|
|
1311
|
+
"ralplan",
|
|
1312
|
+
resolved.runId,
|
|
1313
|
+
"pending-approval.md",
|
|
1314
|
+
);
|
|
1315
|
+
const stageContent = await fs.readFile(stageArtifact.path);
|
|
1316
|
+
const stageSha256 = createHash("sha256").update(stageContent).digest("hex");
|
|
1317
|
+
if (stageSha256 !== stageArtifact.sha256) {
|
|
1318
|
+
throw new RalplanCommandError(
|
|
1319
|
+
2,
|
|
1320
|
+
`refusing to deduplicate ralplan final stage ${resolved.stageN}: stage artifact sha256 mismatch at ${stageArtifact.path} (ledger sha256=${stageArtifact.sha256}, artifact sha256=${stageSha256}).`,
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
let pendingContent: Buffer;
|
|
1325
|
+
try {
|
|
1326
|
+
pendingContent = await fs.readFile(pendingApprovalPath);
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
if (getErrorCode(error) !== "ENOENT") throw error;
|
|
1329
|
+
await writeArtifact(pendingApprovalPath, stageContent.toString("utf8"), {
|
|
1330
|
+
cwd,
|
|
1331
|
+
audit: {
|
|
1332
|
+
category: "artifact",
|
|
1333
|
+
verb: "write",
|
|
1334
|
+
owner: "skc-runtime",
|
|
1335
|
+
skill: "ralplan",
|
|
1336
|
+
sessionId: resolved.sessionId,
|
|
1337
|
+
},
|
|
1338
|
+
});
|
|
1339
|
+
return pendingApprovalPath;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const pendingSha256 = createHash("sha256").update(pendingContent).digest("hex");
|
|
1343
|
+
if (!pendingContent.equals(stageContent) || pendingSha256 !== stageSha256) {
|
|
1344
|
+
throw new RalplanCommandError(
|
|
1345
|
+
2,
|
|
1346
|
+
`refusing to deduplicate ralplan final stage ${resolved.stageN}: pending approval content mismatch at ${pendingApprovalPath} (stage sha256=${stageSha256}, pending sha256=${pendingSha256}).`,
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
return pendingApprovalPath;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/** Append the missing row for a deterministic artifact that survived a crash gap. */
|
|
1353
|
+
async function repairMissingStageArtifactLedger(
|
|
1354
|
+
cwd: string,
|
|
1355
|
+
resolved: Pick<ResolvedArtifactArgs, "sessionId" | "runId" | "stage" | "stageN">,
|
|
1356
|
+
onDisk: OnDiskStageArtifact,
|
|
1357
|
+
): Promise<ExistingStageArtifact> {
|
|
1358
|
+
const createdAt = new Date().toISOString();
|
|
1359
|
+
const indexEntry = {
|
|
1360
|
+
stage: resolved.stage,
|
|
1361
|
+
stage_n: resolved.stageN,
|
|
1362
|
+
path: onDisk.path,
|
|
1363
|
+
created_at: createdAt,
|
|
1364
|
+
sha256: onDisk.sha256,
|
|
1365
|
+
};
|
|
1366
|
+
const result = await appendJsonlIdempotent(
|
|
1367
|
+
path.join(sessionPlansDir(cwd, resolved.sessionId), "ralplan", resolved.runId, "index.jsonl"),
|
|
1368
|
+
indexEntry,
|
|
1369
|
+
{
|
|
1370
|
+
cwd,
|
|
1371
|
+
audit: {
|
|
1372
|
+
category: "ledger",
|
|
1373
|
+
verb: "append",
|
|
1374
|
+
owner: "skc-runtime",
|
|
1375
|
+
skill: "ralplan",
|
|
1376
|
+
sessionId: resolved.sessionId,
|
|
1377
|
+
},
|
|
1378
|
+
key: ralplanIndexKey,
|
|
1379
|
+
},
|
|
1380
|
+
);
|
|
1381
|
+
const duplicate = result.duplicate;
|
|
1382
|
+
if (duplicate && typeof duplicate === "object" && !Array.isArray(duplicate)) {
|
|
1383
|
+
const record = duplicate as Record<string, unknown>;
|
|
1384
|
+
if (typeof record.path === "string" && typeof record.sha256 === "string") {
|
|
1385
|
+
return {
|
|
1386
|
+
path: record.path,
|
|
1387
|
+
sha256: record.sha256,
|
|
1388
|
+
createdAt: typeof record.created_at === "string" ? record.created_at : createdAt,
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
return { path: onDisk.path, sha256: onDisk.sha256, createdAt };
|
|
1393
|
+
}
|
|
1394
|
+
|
|
578
1395
|
/**
|
|
579
1396
|
* Read and parse the run's `index.jsonl` rows. Best-effort: returns [] when the
|
|
580
1397
|
* file is absent or unreadable so HUD sync never fails on a missing index.
|
|
@@ -594,6 +1411,18 @@ async function readRalplanIndexRows(cwd: string, sessionId: string, runId: strin
|
|
|
594
1411
|
}
|
|
595
1412
|
}
|
|
596
1413
|
|
|
1414
|
+
/** Read the lane verdict from ralplan run state without making HUD sync fail on state read errors. */
|
|
1415
|
+
async function readRalplanLastReviewVerdict(cwd: string, sessionId: string): Promise<string | undefined> {
|
|
1416
|
+
try {
|
|
1417
|
+
const existingRead = await readExistingStateForMutation(ralplanStatePath(cwd, sessionId));
|
|
1418
|
+
return existingRead.kind === "valid" && typeof existingRead.value.last_review_verdict === "string"
|
|
1419
|
+
? existingRead.value.last_review_verdict
|
|
1420
|
+
: undefined;
|
|
1421
|
+
} catch {
|
|
1422
|
+
return undefined;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
597
1426
|
async function syncRalplanHud(options: {
|
|
598
1427
|
cwd: string;
|
|
599
1428
|
sessionId: string;
|
|
@@ -601,6 +1430,7 @@ async function syncRalplanHud(options: {
|
|
|
601
1430
|
pendingApproval: boolean;
|
|
602
1431
|
iteration?: number;
|
|
603
1432
|
runId?: string;
|
|
1433
|
+
reviewPassBudget?: number;
|
|
604
1434
|
latestSummary?: string;
|
|
605
1435
|
}): Promise<void> {
|
|
606
1436
|
try {
|
|
@@ -626,15 +1456,25 @@ async function buildRalplanHud(options: {
|
|
|
626
1456
|
latestSummary?: string;
|
|
627
1457
|
runId?: string;
|
|
628
1458
|
sessionId?: string;
|
|
1459
|
+
reviewPassBudget?: number;
|
|
629
1460
|
}) {
|
|
630
1461
|
let iterationFromIndex: number | undefined;
|
|
631
1462
|
let stages: string | undefined;
|
|
1463
|
+
let architectPasses: number | undefined;
|
|
1464
|
+
let criticPasses: number | undefined;
|
|
1465
|
+
let verdict: string | undefined;
|
|
632
1466
|
if (options.runId && options.sessionId) {
|
|
633
|
-
const rows = await
|
|
1467
|
+
const [rows, lastReviewVerdict] = await Promise.all([
|
|
1468
|
+
readRalplanIndexRows(options.cwd, options.sessionId, options.runId),
|
|
1469
|
+
readRalplanLastReviewVerdict(options.cwd, options.sessionId),
|
|
1470
|
+
]);
|
|
1471
|
+
verdict = lastReviewVerdict;
|
|
634
1472
|
if (rows.length > 0) {
|
|
635
1473
|
const summary = summarizeRalplanIndex(rows);
|
|
636
1474
|
iterationFromIndex = summary.iteration;
|
|
637
1475
|
stages = formatRalplanStagePresence(summary.currentStages);
|
|
1476
|
+
architectPasses = summary.currentStages.filter(stage => stage === "architect").length;
|
|
1477
|
+
criticPasses = summary.currentStages.filter(stage => stage === "critic").length;
|
|
638
1478
|
}
|
|
639
1479
|
}
|
|
640
1480
|
return buildRalplanHudSummary({
|
|
@@ -642,6 +1482,10 @@ async function buildRalplanHud(options: {
|
|
|
642
1482
|
iteration: options.iteration,
|
|
643
1483
|
iterationFromIndex,
|
|
644
1484
|
stages,
|
|
1485
|
+
architectPasses,
|
|
1486
|
+
criticPasses,
|
|
1487
|
+
reviewPassBudget: options.reviewPassBudget,
|
|
1488
|
+
verdict,
|
|
645
1489
|
pendingApproval: options.pendingApproval,
|
|
646
1490
|
latestSummary: options.latestSummary,
|
|
647
1491
|
updatedAt: new Date().toISOString(),
|
|
@@ -649,21 +1493,39 @@ async function buildRalplanHud(options: {
|
|
|
649
1493
|
}
|
|
650
1494
|
|
|
651
1495
|
async function handleArtifactWrite(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
|
|
652
|
-
const plannerState = parsePlannerStateArgs(args);
|
|
653
1496
|
const resolved = await resolveArtifactArgs(args, cwd);
|
|
1497
|
+
const persistedRoleState = parsePersistedRoleStateArgs(args, resolved.stage);
|
|
1498
|
+
const laneVerdict = parseLaneVerdictArgs(args, resolved.stage, resolved.stageN);
|
|
1499
|
+
// Fail closed before stage persistence / path writes when cwd drifted to a sibling repo.
|
|
1500
|
+
const repositoryBinding = await enforceRalplanRepositoryBinding(cwd, resolved.sessionId);
|
|
1501
|
+
// Artifact file paths (when --artifact points at a file) must stay under the bound root.
|
|
1502
|
+
const rawArtifact = flagValue(args, "--artifact");
|
|
1503
|
+
if (rawArtifact && !isRestrictedRoleAgentBash()) {
|
|
1504
|
+
const candidate = path.isAbsolute(rawArtifact) ? rawArtifact : path.resolve(cwd, rawArtifact);
|
|
1505
|
+
try {
|
|
1506
|
+
const stat = await fs.stat(candidate);
|
|
1507
|
+
if (stat.isFile()) {
|
|
1508
|
+
assertPathUnderRepositoryBinding(repositoryBinding, candidate);
|
|
1509
|
+
}
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
if (error instanceof RepositoryBindingError) {
|
|
1512
|
+
throw new RalplanCommandError(2, `ralplan repository binding rejected: ${error.message}`);
|
|
1513
|
+
}
|
|
1514
|
+
// Non-file / missing path is inline content; resolveArtifactContent already handled it.
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
654
1517
|
const content = resolved.artifact.endsWith("\n") ? resolved.artifact : `${resolved.artifact}\n`;
|
|
655
1518
|
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
656
1519
|
|
|
1520
|
+
// Read the ledger once before persistence. The dedupe guard and both gates
|
|
1521
|
+
// consume this same snapshot so no additional ledger read can slip between gate
|
|
1522
|
+
// evaluation and persistence.
|
|
1523
|
+
const indexLoad = await loadRalplanIndexForCap(cwd, resolved.sessionId, resolved.runId);
|
|
1524
|
+
|
|
657
1525
|
// Duplicate-write guard: a second `--write` for the same (stage, stage_n) must not
|
|
658
1526
|
// silently clobber the artifact or append a duplicate ledger row. Classify before any
|
|
659
1527
|
// state mutation so a conflict never regresses run-state phase.
|
|
660
|
-
const existingArtifact =
|
|
661
|
-
cwd,
|
|
662
|
-
resolved.sessionId,
|
|
663
|
-
resolved.runId,
|
|
664
|
-
resolved.stage,
|
|
665
|
-
resolved.stageN,
|
|
666
|
-
);
|
|
1528
|
+
const existingArtifact = findExistingStageArtifact(indexLoad.rawText, resolved.stage, resolved.stageN);
|
|
667
1529
|
if (existingArtifact) {
|
|
668
1530
|
if (existingArtifact.sha256 !== sha256) {
|
|
669
1531
|
throw new RalplanCommandError(
|
|
@@ -671,14 +1533,102 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
671
1533
|
`refusing to overwrite ralplan ${resolved.stage} stage ${resolved.stageN} at ${existingArtifact.path}: an artifact with different content already exists (existing sha256=${existingArtifact.sha256}, new sha256=${sha256}). Use a new --stage_n to record another pass.`,
|
|
672
1534
|
);
|
|
673
1535
|
}
|
|
674
|
-
|
|
1536
|
+
if (resolved.stage === "final") await ensureFinalPendingApproval(cwd, resolved, existingArtifact);
|
|
1537
|
+
return buildDeduplicatedResult(resolved, existingArtifact, sha256, cwd, repositoryBinding);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// `persistArtifact` writes the artifact before its ledger row. If a process crashed
|
|
1541
|
+
// in that gap, an identical retry repairs the row and remains a normal deduplicated
|
|
1542
|
+
// receipt; a differing retry retains the ordinary no-overwrite refusal.
|
|
1543
|
+
const onDiskArtifact = await findOnDiskStageArtifact(cwd, resolved);
|
|
1544
|
+
if (onDiskArtifact) {
|
|
1545
|
+
if (onDiskArtifact.sha256 !== sha256) {
|
|
1546
|
+
throw new RalplanCommandError(
|
|
1547
|
+
2,
|
|
1548
|
+
`refusing to overwrite ralplan ${resolved.stage} stage ${resolved.stageN} at ${onDiskArtifact.path}: an artifact with different content already exists (existing sha256=${onDiskArtifact.sha256}, new sha256=${sha256}). Use a new --stage_n to record another pass.`,
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
if (resolved.stage === "final") await ensureFinalPendingApproval(cwd, resolved, onDiskArtifact);
|
|
1552
|
+
const repairedArtifact = await repairMissingStageArtifactLedger(cwd, resolved, onDiskArtifact);
|
|
1553
|
+
let appliedPersistedRoleState: PersistedRoleStateUpdate | undefined;
|
|
1554
|
+
if (
|
|
1555
|
+
persistedRoleState &&
|
|
1556
|
+
(await applyPersistedRoleStateUpdate(cwd, resolved.sessionId, persistedRoleState, resolved.runId))
|
|
1557
|
+
) {
|
|
1558
|
+
appliedPersistedRoleState = persistedRoleState;
|
|
1559
|
+
}
|
|
1560
|
+
let appliedLaneVerdict: LaneVerdictUpdate | undefined;
|
|
1561
|
+
if (laneVerdict && (await applyLaneVerdictUpdate(cwd, resolved.sessionId, laneVerdict, resolved.runId))) {
|
|
1562
|
+
appliedLaneVerdict = laneVerdict;
|
|
1563
|
+
}
|
|
1564
|
+
return buildDeduplicatedResult(
|
|
1565
|
+
resolved,
|
|
1566
|
+
repairedArtifact,
|
|
1567
|
+
sha256,
|
|
1568
|
+
cwd,
|
|
1569
|
+
repositoryBinding,
|
|
1570
|
+
appliedLaneVerdict,
|
|
1571
|
+
appliedPersistedRoleState,
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Consensus iteration budget (#3165): refuse new planner/revision openers past the cap.
|
|
1576
|
+
// Dedupe returns above so identical re-writes never stuck-signal. Non-openers (architect,
|
|
1577
|
+
// critic, final, …) remain allowed so operators can escalate without auto-implementation.
|
|
1578
|
+
// On-disk opener artifacts floor the count so a wiped/truncated/malformed index.jsonl cannot
|
|
1579
|
+
// under-count and fail open after prior planner/revision writes.
|
|
1580
|
+
//
|
|
1581
|
+
// Gate evaluation, artifact write, and ledger append are sequential within one
|
|
1582
|
+
// `skc ralplan --write` invocation. This is NOT exclusive across processes: no
|
|
1583
|
+
// run-scoped lock or CAS admission exists, intentionally matching #3165's
|
|
1584
|
+
// check-then-persist exposure.
|
|
1585
|
+
const [onDiskOpeners, onDiskLaneArtifacts, iterationLimit, laneLimit] = await Promise.all([
|
|
1586
|
+
countRalplanOnDiskOpeners(cwd, resolved.sessionId, resolved.runId),
|
|
1587
|
+
countRalplanOnDiskLaneArtifacts(cwd, resolved.sessionId, resolved.runId),
|
|
1588
|
+
resolveRalplanMaxIterations(cwd),
|
|
1589
|
+
resolveRalplanMaxReviewPassesPerLane(cwd),
|
|
1590
|
+
]);
|
|
1591
|
+
const capDecision = evaluateRalplanIterationCap({
|
|
1592
|
+
rows: indexLoad.rows,
|
|
1593
|
+
stage: resolved.stage,
|
|
1594
|
+
maxIterations: iterationLimit.maxIterations,
|
|
1595
|
+
iterationFloor: onDiskOpeners,
|
|
1596
|
+
});
|
|
1597
|
+
if (!capDecision.allowed) {
|
|
1598
|
+
return buildPlanningStuckResult({
|
|
1599
|
+
json: resolved.json,
|
|
1600
|
+
stage: resolved.stage,
|
|
1601
|
+
stageN: resolved.stageN,
|
|
1602
|
+
runId: resolved.runId,
|
|
1603
|
+
decision: capDecision,
|
|
1604
|
+
source: iterationLimit.source,
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
const laneBudgetDecision = evaluateRalplanReviewLaneBudget({
|
|
1608
|
+
rows: indexLoad.rows,
|
|
1609
|
+
stage: resolved.stage,
|
|
1610
|
+
maxReviewPassesPerLane: laneLimit.maxReviewPassesPerLane,
|
|
1611
|
+
onDiskLaneCounts: onDiskLaneArtifacts,
|
|
1612
|
+
});
|
|
1613
|
+
if (!laneBudgetDecision.allowed) {
|
|
1614
|
+
return buildLaneBudgetStuckResult({
|
|
1615
|
+
json: resolved.json,
|
|
1616
|
+
stage: resolved.stage,
|
|
1617
|
+
stageN: resolved.stageN,
|
|
1618
|
+
runId: resolved.runId,
|
|
1619
|
+
decision: laneBudgetDecision,
|
|
1620
|
+
source: laneLimit.source,
|
|
1621
|
+
});
|
|
675
1622
|
}
|
|
676
1623
|
|
|
677
1624
|
// Keep run-state `current_phase` coherent with the stage being persisted.
|
|
678
1625
|
await persistActiveRunId(cwd, resolved.sessionId, resolved.runId, resolved.stage);
|
|
679
1626
|
const persisted = await persistArtifact(resolved, cwd, content, sha256);
|
|
680
|
-
if (
|
|
681
|
-
await
|
|
1627
|
+
if (persistedRoleState) {
|
|
1628
|
+
await applyPersistedRoleStateUpdate(cwd, resolved.sessionId, persistedRoleState);
|
|
1629
|
+
}
|
|
1630
|
+
if (laneVerdict) {
|
|
1631
|
+
await applyLaneVerdictUpdate(cwd, resolved.sessionId, laneVerdict);
|
|
682
1632
|
}
|
|
683
1633
|
await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "ralplan-runtime", path: persisted.path });
|
|
684
1634
|
await syncRalplanHud({
|
|
@@ -688,34 +1638,51 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
688
1638
|
runId: persisted.runId,
|
|
689
1639
|
pendingApproval: persisted.stage === "final",
|
|
690
1640
|
iteration: persisted.stageN,
|
|
1641
|
+
reviewPassBudget: laneLimit.maxReviewPassesPerLane,
|
|
691
1642
|
latestSummary: `persisted ${persisted.stage} stage ${persisted.stageN}`,
|
|
692
1643
|
});
|
|
1644
|
+
const reviewBudgetWarning =
|
|
1645
|
+
laneBudgetDecision.lane && laneBudgetDecision.finalSlot && laneBudgetDecision.maxReviewPassesPerLane > 1
|
|
1646
|
+
? {
|
|
1647
|
+
lane: laneBudgetDecision.lane,
|
|
1648
|
+
passes: laneBudgetDecision.projectedPasses,
|
|
1649
|
+
max: laneBudgetDecision.maxReviewPassesPerLane,
|
|
1650
|
+
}
|
|
1651
|
+
: undefined;
|
|
1652
|
+
|
|
693
1653
|
const payload: Record<string, unknown> = {
|
|
694
1654
|
run_id: persisted.runId,
|
|
695
1655
|
path: persisted.path,
|
|
696
1656
|
stage: persisted.stage,
|
|
697
1657
|
stage_n: persisted.stageN,
|
|
698
1658
|
sha256: persisted.sha256,
|
|
1659
|
+
repository_binding: repositoryBinding,
|
|
699
1660
|
created_at: persisted.createdAt,
|
|
700
1661
|
};
|
|
701
1662
|
if (persisted.pendingApprovalPath) payload.pending_approval_path = persisted.pendingApprovalPath;
|
|
702
|
-
if (
|
|
1663
|
+
if (persistedRoleState) payload[`${persistedRoleState.role}_state`] = persistedRoleStatePayload(persistedRoleState);
|
|
1664
|
+
if (reviewBudgetWarning) payload.review_budget_warning = reviewBudgetWarning;
|
|
1665
|
+
if (laneVerdict) payload.lane_verdict = { lane: laneVerdict.lane, verdict: laneVerdict.verdict };
|
|
1666
|
+
|
|
703
1667
|
const stdout = resolved.json
|
|
704
1668
|
? `${JSON.stringify(payload, null, 2)}\n`
|
|
705
|
-
: `Persisted ralplan ${persisted.stage} stage ${persisted.stageN} at ${persisted.path}.\n`;
|
|
1669
|
+
: `${reviewBudgetWarning ? `Warning: ralplan ${reviewBudgetWarning.lane} review budget final slot used (${reviewBudgetWarning.passes}/${reviewBudgetWarning.max}).\n` : ""}Persisted ralplan ${persisted.stage} stage ${persisted.stageN} at ${persisted.path}.\n`;
|
|
706
1670
|
return { status: 0, stdout };
|
|
707
1671
|
}
|
|
708
1672
|
|
|
709
1673
|
/**
|
|
710
|
-
* Deterministic
|
|
711
|
-
*
|
|
712
|
-
*
|
|
1674
|
+
* Deterministic receipt for an identical repeated `--write`. Ledger-backed duplicates
|
|
1675
|
+
* do not rewrite artifacts, append rows, or churn run state; a crash-gap repair may
|
|
1676
|
+
* complete riding persisted role/lane metadata before returning this receipt.
|
|
713
1677
|
*/
|
|
714
1678
|
function buildDeduplicatedResult(
|
|
715
1679
|
resolved: ResolvedArtifactArgs,
|
|
716
1680
|
existing: ExistingStageArtifact,
|
|
717
1681
|
sha256: string,
|
|
718
1682
|
cwd: string,
|
|
1683
|
+
repositoryBinding: RepositoryBinding,
|
|
1684
|
+
laneVerdict?: LaneVerdictUpdate,
|
|
1685
|
+
persistedRoleState?: PersistedRoleStateUpdate,
|
|
719
1686
|
): RalplanCommandResult {
|
|
720
1687
|
const payload: Record<string, unknown> = {
|
|
721
1688
|
run_id: resolved.runId,
|
|
@@ -723,9 +1690,14 @@ function buildDeduplicatedResult(
|
|
|
723
1690
|
stage: resolved.stage,
|
|
724
1691
|
stage_n: resolved.stageN,
|
|
725
1692
|
sha256,
|
|
1693
|
+
repository_binding: repositoryBinding,
|
|
726
1694
|
created_at: existing.createdAt,
|
|
727
1695
|
deduplicated: true,
|
|
728
1696
|
};
|
|
1697
|
+
if (laneVerdict) payload.lane_verdict = { lane: laneVerdict.lane, verdict: laneVerdict.verdict };
|
|
1698
|
+
if (persistedRoleState) {
|
|
1699
|
+
payload[`${persistedRoleState.role}_state`] = persistedRoleStatePayload(persistedRoleState);
|
|
1700
|
+
}
|
|
729
1701
|
if (resolved.stage === "final") {
|
|
730
1702
|
payload.pending_approval_path = path.join(
|
|
731
1703
|
sessionPlansDir(cwd, resolved.sessionId),
|
|
@@ -774,6 +1746,9 @@ function extractPositionalTask(args: readonly string[]): string {
|
|
|
774
1746
|
}
|
|
775
1747
|
|
|
776
1748
|
function resolveConsensusArgs(args: readonly string[], cwd: string): ConsensusHandoffArgs {
|
|
1749
|
+
if (hasFlag(args, "--lane-verdict")) {
|
|
1750
|
+
throw new RalplanCommandError(2, "--lane-verdict is only supported with skc ralplan --write.");
|
|
1751
|
+
}
|
|
777
1752
|
const architectKind = flagValue(args, "--architect")?.trim() || undefined;
|
|
778
1753
|
if (architectKind && !KNOWN_ARCHITECT_KINDS.has(architectKind)) {
|
|
779
1754
|
throw new RalplanCommandError(
|
|
@@ -809,7 +1784,7 @@ function resolveConsensusArgs(args: readonly string[], cwd: string): ConsensusHa
|
|
|
809
1784
|
async function seedRalplanState(
|
|
810
1785
|
cwd: string,
|
|
811
1786
|
resolved: ConsensusHandoffArgs,
|
|
812
|
-
): Promise<{ statePath: string; runId: string }> {
|
|
1787
|
+
): Promise<{ statePath: string; runId: string; repositoryBinding: RepositoryBinding }> {
|
|
813
1788
|
const statePath = ralplanStatePath(cwd, resolved.sessionId);
|
|
814
1789
|
// Reuse an existing run id when present so a re-invocation of `skc ralplan "task"` doesn't
|
|
815
1790
|
// orphan in-progress artifacts under a fresh run id.
|
|
@@ -817,6 +1792,11 @@ async function seedRalplanState(
|
|
|
817
1792
|
const runId = existingRunId ?? resolved.sessionId ?? defaultRunId();
|
|
818
1793
|
assertSafePathComponent(runId, "run-id");
|
|
819
1794
|
const now = new Date().toISOString();
|
|
1795
|
+
// When an active seed already carries authority, re-entry must match it (fail closed).
|
|
1796
|
+
// Otherwise stamp the current cwd as the durable binding for this run.
|
|
1797
|
+
const repositoryBinding = existingRunId
|
|
1798
|
+
? await enforceRalplanRepositoryBinding(cwd, resolved.sessionId)
|
|
1799
|
+
: publicRepositoryBinding(await captureRepositoryBinding(cwd, { displayPath: cwd }));
|
|
820
1800
|
const payload: Record<string, unknown> = {
|
|
821
1801
|
active: true,
|
|
822
1802
|
current_phase: "planner",
|
|
@@ -827,6 +1807,7 @@ async function seedRalplanState(
|
|
|
827
1807
|
task: resolved.task,
|
|
828
1808
|
run_id: runId,
|
|
829
1809
|
updated_at: now,
|
|
1810
|
+
repository_binding: repositoryBinding,
|
|
830
1811
|
};
|
|
831
1812
|
if (resolved.architectKind) payload.architect_kind = resolved.architectKind;
|
|
832
1813
|
if (resolved.criticKind) payload.critic_kind = resolved.criticKind;
|
|
@@ -849,7 +1830,7 @@ async function seedRalplanState(
|
|
|
849
1830
|
},
|
|
850
1831
|
});
|
|
851
1832
|
await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "ralplan-runtime", path: statePath });
|
|
852
|
-
return { statePath, runId };
|
|
1833
|
+
return { statePath, runId, repositoryBinding };
|
|
853
1834
|
}
|
|
854
1835
|
|
|
855
1836
|
async function handleConsensusHandoff(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
|
|
@@ -857,7 +1838,7 @@ async function handleConsensusHandoff(args: readonly string[], cwd: string): Pro
|
|
|
857
1838
|
if (!resolved.task) {
|
|
858
1839
|
throw new RalplanCommandError(2, 'skc ralplan requires a task description, e.g. `skc ralplan "<task>"`.');
|
|
859
1840
|
}
|
|
860
|
-
const { statePath, runId } = await seedRalplanState(cwd, resolved);
|
|
1841
|
+
const { statePath, runId, repositoryBinding } = await seedRalplanState(cwd, resolved);
|
|
861
1842
|
const mode = resolved.deliberate ? "deliberate" : "short";
|
|
862
1843
|
await syncRalplanHud({
|
|
863
1844
|
cwd,
|
|
@@ -875,6 +1856,7 @@ async function handleConsensusHandoff(args: readonly string[], cwd: string): Pro
|
|
|
875
1856
|
state_path: statePath,
|
|
876
1857
|
run_id: runId,
|
|
877
1858
|
handoff: "/skill:ralplan",
|
|
1859
|
+
repository_binding: repositoryBinding,
|
|
878
1860
|
};
|
|
879
1861
|
const stdout = resolved.json
|
|
880
1862
|
? renderCliWriteReceipt({ ok: true, ...summary })
|