@gajae-code/coding-agent 0.11.11 → 0.12.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 +131 -4
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/cli/read-cli.d.ts +1 -0
- 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/config/model-registry.d.ts +4 -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 +83 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +38 -4
- package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
- package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +10 -19
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
- package/dist/types/modes/components/settings-selector.d.ts +11 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +44 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/prompt-status.d.ts +23 -3
- package/dist/types/sdk/session.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +82 -12
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +48 -19
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +96 -12
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -4
- package/dist/types/skill-state/workflow-hud.d.ts +9 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +7 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/output-meta.d.ts +44 -3
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -7
- package/dist/types/tools/resource-gc.d.ts +4 -0
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/dist/types/utils/gh.d.ts +13 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/searxng.d.ts +7 -0
- package/dist/types/web/search/providers/xai.d.ts +2 -0
- package/package.json +11 -13
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +5 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +285 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/async/job-manager.ts +78 -20
- package/src/capability/index.ts +8 -0
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/team.ts +20 -2
- package/src/config/file-lock.ts +11 -2
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +74 -1
- package/src/config/settings.ts +246 -108
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
- package/src/defaults/gjc/skills/team/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/eval/py/executor.ts +7 -2
- package/src/exa/mcp-client.ts +12 -3
- package/src/exec/bash-executor.ts +144 -27
- package/src/extensibility/extensions/runner.ts +15 -0
- package/src/extensibility/extensions/types.ts +46 -5
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
- package/src/extensibility/gjc-plugins/index.ts +19 -2
- package/src/extensibility/gjc-plugins/installer.ts +189 -50
- package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
- package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
- package/src/extensibility/gjc-plugins/observability.ts +3 -3
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
- package/src/extensibility/gjc-plugins/schema.ts +84 -12
- package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
- package/src/extensibility/gjc-plugins/types.ts +141 -0
- package/src/extensibility/gjc-plugins/validation.ts +9 -0
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -943
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +42 -5
- package/src/gjc-runtime/team-runtime.ts +1085 -40
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
- package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
- package/src/gjc-runtime/workflow-manifest.ts +38 -247
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +21 -22
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +98 -0
- package/src/modes/acp/acp-agent.ts +339 -191
- package/src/modes/acp/acp-event-mapper.ts +14 -7
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- 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 +12 -1
- package/src/modes/components/oauth-selector.ts +96 -28
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +457 -83
- package/src/modes/components/settings-selector.ts +116 -23
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +3 -14
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +26 -2
- package/src/modes/controllers/input-controller.ts +18 -3
- package/src/modes/controllers/selector-controller.ts +127 -36
- package/src/modes/interactive-mode.ts +62 -3
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +22 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -5
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/read.md +4 -2
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/reminders/star-reminder.ts +4 -42
- package/src/runtime-mcp/manager.ts +19 -4
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/lifecycle.ts +25 -6
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +6 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +984 -146
- package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/reconciliation-store.ts +303 -0
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/control/dispatch.ts +5 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +23 -1
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +7 -2
- package/src/sdk/session.ts +101 -10
- package/src/session/agent-session.ts +1286 -220
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +44 -6
- package/src/session/internal/managed-session-storage.ts +48 -6
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +2364 -438
- package/src/session/session-storage.ts +9 -1
- package/src/session/streaming-output.ts +500 -111
- package/src/setup/credential-import.ts +75 -13
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +32 -10
- package/src/skill-state/workflow-hud.ts +42 -2
- package/src/skill-state/workflow-mutation-guard.ts +560 -8
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +23 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +23 -7
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +169 -118
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/bash.ts +503 -60
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/output-meta.ts +281 -20
- package/src/tools/path-utils.ts +8 -3
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1560 -310
- package/src/tools/resource-gc.ts +182 -18
- package/src/tools/review.ts +5 -7
- package/src/tools/search.ts +1 -0
- package/src/tools/subagent-render.ts +6 -2
- package/src/tools/subagent.ts +6 -0
- package/src/tools/telegram-send.ts +6 -3
- package/src/tools/tool-result.ts +14 -1
- package/src/utils/gh.ts +39 -0
- package/src/utils/image-resize.ts +3 -0
- package/src/web/search/providers/anthropic.ts +4 -4
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/kimi.ts +15 -5
- package/src/web/search/providers/searxng.ts +47 -4
- package/src/web/search/providers/xai.ts +7 -2
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import type { Dirent } from "node:fs";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
|
-
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { getConfigRootDir } from "@gajae-code/utils";
|
|
5
6
|
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
6
7
|
import { buildRalplanHudSummary } from "../skill-state/workflow-hud";
|
|
7
8
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
RepositoryBindingError,
|
|
23
24
|
} from "./repository-binding";
|
|
24
25
|
import { GJC_RALPLAN_ARTIFACT_ENV, isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
|
|
25
|
-
import { gjcRoot, modeStatePath, sessionPlansDir } from "./session-layout";
|
|
26
|
+
import { gjcRoot, modeStatePath, sessionIdFromDirName, sessionPlansDir } from "./session-layout";
|
|
26
27
|
import { resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
27
28
|
import { migrateWorkflowState } from "./state-migrations";
|
|
28
29
|
import { runNativeStateCommand } from "./state-runtime";
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
writeArtifact,
|
|
34
35
|
writeWorkflowEnvelopeAtomic,
|
|
35
36
|
} from "./state-writer";
|
|
37
|
+
import { probeGjcTeamAvailability } from "./team-runtime";
|
|
36
38
|
import { assertSafePathComponent, CommandError, flagValue, hasFlag } from "./workflow-cli-common";
|
|
37
39
|
import { getSkillManifest } from "./workflow-manifest";
|
|
38
40
|
|
|
@@ -50,7 +52,7 @@ import { getSkillManifest } from "./workflow-manifest";
|
|
|
50
52
|
*
|
|
51
53
|
* 2. **Artifact write**: `gjc ralplan --write --stage <type> --stage_n <N>
|
|
52
54
|
* (--artifact <path-or-string> | --artifact-env GJC_RALPLAN_ARTIFACT)
|
|
53
|
-
* [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
|
|
55
|
+
* [--run-id <id>] [--session-id <id>] [--lane-verdict <token>] [--json]` persists Planner / Architect
|
|
54
56
|
* / Critic / revision / post-interview / ADR / final markdown under `.gjc/plans/ralplan/<run-id>/`, maintains
|
|
55
57
|
* an `index.jsonl` audit log, copies `final` stages to `pending-approval.md`, and advances
|
|
56
58
|
* the HUD chip to reflect the latest persisted stage.
|
|
@@ -70,9 +72,35 @@ export const RALPLAN_DEFAULT_MAX_ITERATIONS = 5;
|
|
|
70
72
|
export const RALPLAN_MAX_ITERATIONS_LIMIT = 20;
|
|
71
73
|
/** Operator-visible stuck signal for headless/CI orchestration (#3165). */
|
|
72
74
|
export const PLANNING_STUCK_MARKER = "PLANNING-STUCK";
|
|
75
|
+
/** Default architect/critic review passes per consensus iteration. */
|
|
76
|
+
export const RALPLAN_DEFAULT_MAX_REVIEW_PASSES_PER_LANE = 1;
|
|
77
|
+
/** Inclusive upper bound for `gjc.ralplan.maxReviewPassesPerLane` settings overrides. */
|
|
78
|
+
export const RALPLAN_MAX_REVIEW_PASSES_PER_LANE_LIMIT = 10;
|
|
79
|
+
export type RalplanAutoHandoffTarget = "off" | "ultragoal" | "team";
|
|
80
|
+
|
|
81
|
+
export interface RalplanAutoHandoffResolution {
|
|
82
|
+
configuredTarget: RalplanAutoHandoffTarget;
|
|
83
|
+
effectiveTarget: RalplanAutoHandoffTarget;
|
|
84
|
+
degradationReason: string | null;
|
|
85
|
+
source: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const RALPLAN_AUTO_HANDOFF_TARGETS = new Set<RalplanAutoHandoffTarget>(["off", "ultragoal", "team"]);
|
|
73
89
|
|
|
74
90
|
const RALPLAN_ITERATION_OPENER_STAGES = new Set<RalplanStage>(["planner", "revision"]);
|
|
75
91
|
|
|
92
|
+
/** Collapse duplicate ledger rows for the same deterministic stage artifact before review-lane budget accounting. */
|
|
93
|
+
function deduplicateRalplanIndexRowsByStageIdentity(rows: readonly RalplanIndexRow[]): RalplanIndexRow[] {
|
|
94
|
+
const seen = new Set<string>();
|
|
95
|
+
return rows.filter(row => {
|
|
96
|
+
if (typeof row.stageN !== "number") return true;
|
|
97
|
+
const identity = `${row.stage}\u0000${row.stageN}`;
|
|
98
|
+
if (seen.has(identity)) return false;
|
|
99
|
+
seen.add(identity);
|
|
100
|
+
return true;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
76
104
|
export type RalplanIterationCapDecision =
|
|
77
105
|
| {
|
|
78
106
|
allowed: true;
|
|
@@ -147,8 +175,106 @@ export function evaluateRalplanIterationCap(input: {
|
|
|
147
175
|
};
|
|
148
176
|
}
|
|
149
177
|
|
|
178
|
+
export type RalplanReviewLane = "architect" | "critic";
|
|
179
|
+
|
|
180
|
+
export type RalplanReviewLaneBudgetDecision =
|
|
181
|
+
| {
|
|
182
|
+
allowed: true;
|
|
183
|
+
lane?: RalplanReviewLane;
|
|
184
|
+
currentPasses: number;
|
|
185
|
+
projectedPasses: number;
|
|
186
|
+
maxReviewPassesPerLane: number;
|
|
187
|
+
finalSlot: boolean;
|
|
188
|
+
ledgerNote?: string;
|
|
189
|
+
}
|
|
190
|
+
| {
|
|
191
|
+
allowed: false;
|
|
192
|
+
lane: RalplanReviewLane;
|
|
193
|
+
currentPasses: number;
|
|
194
|
+
projectedPasses: number;
|
|
195
|
+
maxReviewPassesPerLane: number;
|
|
196
|
+
finalSlot: false;
|
|
197
|
+
ledgerNote?: string;
|
|
198
|
+
reason: string;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Pure per-lane review-pass budget gate. Architect and critic passes are limited
|
|
203
|
+
* within the current consensus iteration; all other stages remain unconditionally
|
|
204
|
+
* available as escalation paths.
|
|
205
|
+
*/
|
|
206
|
+
export function evaluateRalplanReviewLaneBudget(input: {
|
|
207
|
+
rows: readonly RalplanIndexRow[];
|
|
208
|
+
stage: string;
|
|
209
|
+
maxReviewPassesPerLane?: unknown;
|
|
210
|
+
onDiskLaneCounts?: { architect: number; critic: number };
|
|
211
|
+
}): RalplanReviewLaneBudgetDecision {
|
|
212
|
+
const maxReviewPassesPerLane =
|
|
213
|
+
typeof input.maxReviewPassesPerLane === "number" &&
|
|
214
|
+
Number.isInteger(input.maxReviewPassesPerLane) &&
|
|
215
|
+
input.maxReviewPassesPerLane >= 1 &&
|
|
216
|
+
input.maxReviewPassesPerLane <= RALPLAN_MAX_REVIEW_PASSES_PER_LANE_LIMIT
|
|
217
|
+
? input.maxReviewPassesPerLane
|
|
218
|
+
: RALPLAN_DEFAULT_MAX_REVIEW_PASSES_PER_LANE;
|
|
219
|
+
if (input.stage !== "architect" && input.stage !== "critic") {
|
|
220
|
+
return {
|
|
221
|
+
allowed: true,
|
|
222
|
+
currentPasses: 0,
|
|
223
|
+
projectedPasses: 0,
|
|
224
|
+
maxReviewPassesPerLane,
|
|
225
|
+
finalSlot: false,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const lane = input.stage as RalplanReviewLane;
|
|
230
|
+
const rows = deduplicateRalplanIndexRowsByStageIdentity(input.rows);
|
|
231
|
+
const summary = summarizeRalplanIndex(rows);
|
|
232
|
+
const indexCurrent = summary.currentStages.filter(stage => stage === lane).length;
|
|
233
|
+
const parsedTotal = rows.filter(row => row.stage === lane).length;
|
|
234
|
+
const onDiskRaw = input.onDiskLaneCounts?.[lane];
|
|
235
|
+
const onDiskTotal = typeof onDiskRaw === "number" && Number.isInteger(onDiskRaw) && onDiskRaw > 0 ? onDiskRaw : 0;
|
|
236
|
+
const diskExcess = Math.max(0, onDiskTotal - parsedTotal);
|
|
237
|
+
const currentPasses = indexCurrent + diskExcess;
|
|
238
|
+
const projectedPasses = currentPasses + 1;
|
|
239
|
+
const ledgerNote =
|
|
240
|
+
diskExcess > 0
|
|
241
|
+
? ` (ledger under-count: parsed ${lane} rows=${parsedTotal}, on-disk ${lane} artifacts=${onDiskTotal})`
|
|
242
|
+
: undefined;
|
|
243
|
+
const finalSlot = projectedPasses === maxReviewPassesPerLane;
|
|
244
|
+
if (projectedPasses > maxReviewPassesPerLane) {
|
|
245
|
+
return {
|
|
246
|
+
allowed: false,
|
|
247
|
+
lane,
|
|
248
|
+
currentPasses,
|
|
249
|
+
projectedPasses,
|
|
250
|
+
maxReviewPassesPerLane,
|
|
251
|
+
finalSlot: false,
|
|
252
|
+
ledgerNote,
|
|
253
|
+
reason:
|
|
254
|
+
`ralplan review lane budget exceeded: ${lane} pass ${projectedPasses} of max ${maxReviewPassesPerLane} ` +
|
|
255
|
+
`in consensus iteration ${Math.max(1, summary.iteration)}${ledgerNote ?? ""}`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
allowed: true,
|
|
260
|
+
lane,
|
|
261
|
+
currentPasses,
|
|
262
|
+
projectedPasses,
|
|
263
|
+
maxReviewPassesPerLane,
|
|
264
|
+
finalSlot,
|
|
265
|
+
ledgerNote,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function getErrorCode(error: unknown): string | undefined {
|
|
270
|
+
if (!error || typeof error !== "object" || !("code" in error)) return undefined;
|
|
271
|
+
const code = (error as { code?: unknown }).code;
|
|
272
|
+
return typeof code === "string" ? code : undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
150
275
|
/** Filename pattern for persisted planner/revision stage artifacts. */
|
|
151
276
|
const OPENER_ARTIFACT_RE = /^stage-\d{2,}-(planner|revision)\.md$/;
|
|
277
|
+
const LANE_ARTIFACT_RE = /^stage-\d{2,}-(architect|critic)\.md$/;
|
|
152
278
|
|
|
153
279
|
/**
|
|
154
280
|
* Count on-disk planner/revision stage artifacts for a run. Used as a floor when
|
|
@@ -168,6 +294,30 @@ export async function countRalplanOnDiskOpeners(cwd: string, sessionId: string,
|
|
|
168
294
|
}
|
|
169
295
|
}
|
|
170
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Count on-disk Architect/Critic stage artifacts for a run. This is the
|
|
299
|
+
* fail-closed floor for a missing, truncated, or malformed `index.jsonl`.
|
|
300
|
+
*/
|
|
301
|
+
export async function countRalplanOnDiskLaneArtifacts(
|
|
302
|
+
cwd: string,
|
|
303
|
+
sessionId: string,
|
|
304
|
+
runId: string,
|
|
305
|
+
): Promise<{ architect: number; critic: number }> {
|
|
306
|
+
const runDir = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId);
|
|
307
|
+
try {
|
|
308
|
+
const entries = await fs.readdir(runDir);
|
|
309
|
+
const counts = { architect: 0, critic: 0 };
|
|
310
|
+
for (const name of entries) {
|
|
311
|
+
const match = LANE_ARTIFACT_RE.exec(name);
|
|
312
|
+
if (match) counts[match[1] as RalplanReviewLane] += 1;
|
|
313
|
+
}
|
|
314
|
+
return counts;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (getErrorCode(error) === "ENOENT") return { architect: 0, critic: 0 };
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
171
321
|
/**
|
|
172
322
|
* Load index rows for cap enforcement. Unlike HUD reads, returns structural
|
|
173
323
|
* signals so callers can fail closed when the ledger is empty/malformed while
|
|
@@ -177,7 +327,13 @@ export async function loadRalplanIndexForCap(
|
|
|
177
327
|
cwd: string,
|
|
178
328
|
sessionId: string,
|
|
179
329
|
runId: string,
|
|
180
|
-
): Promise<{
|
|
330
|
+
): Promise<{
|
|
331
|
+
rows: RalplanIndexRow[];
|
|
332
|
+
indexPresent: boolean;
|
|
333
|
+
parseableLines: number;
|
|
334
|
+
rawLineCount: number;
|
|
335
|
+
rawText?: string;
|
|
336
|
+
}> {
|
|
181
337
|
const indexPath = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId, "index.jsonl");
|
|
182
338
|
try {
|
|
183
339
|
const text = await fs.readFile(indexPath, "utf8");
|
|
@@ -192,6 +348,7 @@ export async function loadRalplanIndexForCap(
|
|
|
192
348
|
indexPresent: true,
|
|
193
349
|
parseableLines: rows.length,
|
|
194
350
|
rawLineCount: lines.length,
|
|
351
|
+
rawText: text,
|
|
195
352
|
};
|
|
196
353
|
} catch (error) {
|
|
197
354
|
const code =
|
|
@@ -204,16 +361,16 @@ export async function loadRalplanIndexForCap(
|
|
|
204
361
|
}
|
|
205
362
|
}
|
|
206
363
|
|
|
207
|
-
function
|
|
208
|
-
return typeof value === "number" &&
|
|
209
|
-
Number.isFinite(value) &&
|
|
210
|
-
Number.isInteger(value) &&
|
|
211
|
-
value >= 1 &&
|
|
212
|
-
value <= RALPLAN_MAX_ITERATIONS_LIMIT
|
|
364
|
+
function parseBoundedPositiveInteger(value: unknown, limit: number): number | null {
|
|
365
|
+
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 1 && value <= limit
|
|
213
366
|
? value
|
|
214
367
|
: null;
|
|
215
368
|
}
|
|
216
369
|
|
|
370
|
+
function parseMaxIterationsValue(value: unknown): number | null {
|
|
371
|
+
return parseBoundedPositiveInteger(value, RALPLAN_MAX_ITERATIONS_LIMIT);
|
|
372
|
+
}
|
|
373
|
+
|
|
217
374
|
async function readSettingsMaxIterations(settingsPath: string): Promise<number | null> {
|
|
218
375
|
try {
|
|
219
376
|
const raw = await Bun.file(settingsPath).text();
|
|
@@ -241,12 +398,198 @@ export async function resolveRalplanMaxIterations(cwd: string): Promise<{ maxIte
|
|
|
241
398
|
const projectPath = path.join(gjcRoot(cwd), "settings.json");
|
|
242
399
|
const project = await readSettingsMaxIterations(projectPath);
|
|
243
400
|
if (project !== null) return { maxIterations: project, source: projectPath };
|
|
244
|
-
const
|
|
245
|
-
const userPath = path.join(userDir, "settings.json");
|
|
401
|
+
const userPath = path.join(getConfigRootDir(), "settings.json");
|
|
246
402
|
const user = await readSettingsMaxIterations(userPath);
|
|
247
403
|
if (user !== null) return { maxIterations: user, source: userPath };
|
|
248
404
|
return { maxIterations: RALPLAN_DEFAULT_MAX_ITERATIONS, source: "default" };
|
|
249
405
|
}
|
|
406
|
+
function parseRalplanAutoHandoffTarget(value: unknown): RalplanAutoHandoffTarget | undefined {
|
|
407
|
+
return typeof value === "string" && RALPLAN_AUTO_HANDOFF_TARGETS.has(value as RalplanAutoHandoffTarget)
|
|
408
|
+
? (value as RalplanAutoHandoffTarget)
|
|
409
|
+
: undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
type RalplanAutoHandoffOptions = {
|
|
413
|
+
planningStuck?: boolean;
|
|
414
|
+
teamAvailabilityProbe?: () => { available: true } | { available: false; reason: string };
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
type RalplanAutoHandoffSetting =
|
|
418
|
+
| { kind: "absent" }
|
|
419
|
+
| { kind: "valid"; value: RalplanAutoHandoffTarget }
|
|
420
|
+
| { kind: "invalid"; reason: string };
|
|
421
|
+
|
|
422
|
+
function parsePresentRalplanAutoHandoff(value: unknown): RalplanAutoHandoffSetting {
|
|
423
|
+
const target = parseRalplanAutoHandoffTarget(value);
|
|
424
|
+
return target === undefined
|
|
425
|
+
? {
|
|
426
|
+
kind: "invalid",
|
|
427
|
+
reason: "expected gjc.ralplan.autoHandoff to be one of off, ultragoal, team",
|
|
428
|
+
}
|
|
429
|
+
: { kind: "valid", value: target };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function parseRalplanAutoHandoffSettings(parsed: unknown): RalplanAutoHandoffSetting {
|
|
433
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { kind: "absent" };
|
|
434
|
+
const settings = parsed as Record<string, unknown>;
|
|
435
|
+
if (Object.hasOwn(settings, "gjc.ralplan.autoHandoff")) {
|
|
436
|
+
return parsePresentRalplanAutoHandoff(settings["gjc.ralplan.autoHandoff"]);
|
|
437
|
+
}
|
|
438
|
+
const gjc = settings.gjc;
|
|
439
|
+
if (!gjc || typeof gjc !== "object" || Array.isArray(gjc)) return { kind: "absent" };
|
|
440
|
+
const ralplan = (gjc as Record<string, unknown>).ralplan;
|
|
441
|
+
if (!ralplan || typeof ralplan !== "object" || Array.isArray(ralplan)) return { kind: "absent" };
|
|
442
|
+
const ralplanSettings = ralplan as Record<string, unknown>;
|
|
443
|
+
if (!Object.hasOwn(ralplanSettings, "autoHandoff")) return { kind: "absent" };
|
|
444
|
+
return parsePresentRalplanAutoHandoff(ralplanSettings.autoHandoff);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function readSettingsAutoHandoff(settingsPath: string): Promise<RalplanAutoHandoffSetting> {
|
|
448
|
+
let raw: string;
|
|
449
|
+
try {
|
|
450
|
+
raw = await Bun.file(settingsPath).text();
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (getErrorCode(error) === "ENOENT") return { kind: "absent" };
|
|
453
|
+
return {
|
|
454
|
+
kind: "invalid",
|
|
455
|
+
reason: `unable to read settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
let parsed: unknown;
|
|
459
|
+
try {
|
|
460
|
+
parsed = JSON.parse(raw);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
return {
|
|
463
|
+
kind: "invalid",
|
|
464
|
+
reason: `malformed JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return parseRalplanAutoHandoffSettings(parsed);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export async function resolveRalplanAutoHandoff(
|
|
471
|
+
cwd: string,
|
|
472
|
+
options: RalplanAutoHandoffOptions = {},
|
|
473
|
+
): Promise<RalplanAutoHandoffResolution> {
|
|
474
|
+
const projectPath = path.join(gjcRoot(cwd), "settings.json");
|
|
475
|
+
const project = await readSettingsAutoHandoff(projectPath);
|
|
476
|
+
if (project.kind === "invalid") {
|
|
477
|
+
throw new RalplanCommandError(2, `invalid ralplan settings at ${projectPath}: ${project.reason}`);
|
|
478
|
+
}
|
|
479
|
+
if (project.kind === "valid") {
|
|
480
|
+
return resolveRalplanAutoHandoffTarget(project.value, projectPath, options);
|
|
481
|
+
}
|
|
482
|
+
const userPath = path.join(getConfigRootDir(), "settings.json");
|
|
483
|
+
const user = await readSettingsAutoHandoff(userPath);
|
|
484
|
+
if (user.kind === "invalid") {
|
|
485
|
+
throw new RalplanCommandError(2, `invalid ralplan settings at ${userPath}: ${user.reason}`);
|
|
486
|
+
}
|
|
487
|
+
return resolveRalplanAutoHandoffTarget(
|
|
488
|
+
user.kind === "valid" ? user.value : "off",
|
|
489
|
+
user.kind === "valid" ? userPath : "default",
|
|
490
|
+
options,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function resolveRalplanAutoHandoffTarget(
|
|
495
|
+
configuredTarget: RalplanAutoHandoffTarget,
|
|
496
|
+
source: string,
|
|
497
|
+
options: RalplanAutoHandoffOptions,
|
|
498
|
+
): RalplanAutoHandoffResolution {
|
|
499
|
+
if (options.planningStuck) {
|
|
500
|
+
return { configuredTarget, effectiveTarget: "off", degradationReason: "planning_stuck", source };
|
|
501
|
+
}
|
|
502
|
+
if (configuredTarget !== "team")
|
|
503
|
+
return { configuredTarget, effectiveTarget: configuredTarget, degradationReason: null, source };
|
|
504
|
+
|
|
505
|
+
const availability = (options.teamAvailabilityProbe ?? probeGjcTeamAvailability)();
|
|
506
|
+
return availability.available
|
|
507
|
+
? { configuredTarget, effectiveTarget: "team", degradationReason: null, source }
|
|
508
|
+
: {
|
|
509
|
+
configuredTarget,
|
|
510
|
+
effectiveTarget: "off",
|
|
511
|
+
degradationReason: `team_unavailable:${availability.reason}`,
|
|
512
|
+
source,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function parseMaxReviewPassesPerLaneValue(value: unknown): number | null {
|
|
517
|
+
return parseBoundedPositiveInteger(value, RALPLAN_MAX_REVIEW_PASSES_PER_LANE_LIMIT);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
type RalplanReviewPassesPerLaneSetting =
|
|
521
|
+
| { kind: "absent" }
|
|
522
|
+
| { kind: "valid"; value: number }
|
|
523
|
+
| { kind: "invalid"; reason: string };
|
|
524
|
+
|
|
525
|
+
function parsePresentMaxReviewPassesPerLane(value: unknown): RalplanReviewPassesPerLaneSetting {
|
|
526
|
+
const parsed = parseMaxReviewPassesPerLaneValue(value);
|
|
527
|
+
return parsed === null
|
|
528
|
+
? {
|
|
529
|
+
kind: "invalid",
|
|
530
|
+
reason:
|
|
531
|
+
"expected gjc.ralplan.maxReviewPassesPerLane to be an integer between 1 and " +
|
|
532
|
+
RALPLAN_MAX_REVIEW_PASSES_PER_LANE_LIMIT,
|
|
533
|
+
}
|
|
534
|
+
: { kind: "valid", value: parsed };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function parseMaxReviewPassesPerLaneSettings(parsed: unknown): RalplanReviewPassesPerLaneSetting {
|
|
538
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { kind: "absent" };
|
|
539
|
+
const settings = parsed as Record<string, unknown>;
|
|
540
|
+
if (Object.hasOwn(settings, "gjc.ralplan.maxReviewPassesPerLane")) {
|
|
541
|
+
return parsePresentMaxReviewPassesPerLane(settings["gjc.ralplan.maxReviewPassesPerLane"]);
|
|
542
|
+
}
|
|
543
|
+
const gjc = settings.gjc;
|
|
544
|
+
if (!gjc || typeof gjc !== "object" || Array.isArray(gjc)) return { kind: "absent" };
|
|
545
|
+
const ralplan = (gjc as Record<string, unknown>).ralplan;
|
|
546
|
+
if (!ralplan || typeof ralplan !== "object" || Array.isArray(ralplan)) return { kind: "absent" };
|
|
547
|
+
const ralplanSettings = ralplan as Record<string, unknown>;
|
|
548
|
+
if (!Object.hasOwn(ralplanSettings, "maxReviewPassesPerLane")) return { kind: "absent" };
|
|
549
|
+
return parsePresentMaxReviewPassesPerLane(ralplanSettings.maxReviewPassesPerLane);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function readSettingsMaxReviewPassesPerLane(settingsPath: string): Promise<RalplanReviewPassesPerLaneSetting> {
|
|
553
|
+
let raw: string;
|
|
554
|
+
try {
|
|
555
|
+
raw = await Bun.file(settingsPath).text();
|
|
556
|
+
} catch (error) {
|
|
557
|
+
if (getErrorCode(error) === "ENOENT") return { kind: "absent" };
|
|
558
|
+
return {
|
|
559
|
+
kind: "invalid",
|
|
560
|
+
reason: `unable to read settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
let parsed: unknown;
|
|
564
|
+
try {
|
|
565
|
+
parsed = JSON.parse(raw);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
return {
|
|
568
|
+
kind: "invalid",
|
|
569
|
+
reason: `malformed JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
return parseMaxReviewPassesPerLaneSettings(parsed);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Resolve the per-lane review-pass budget with project-over-user precedence. */
|
|
576
|
+
export async function resolveRalplanMaxReviewPassesPerLane(
|
|
577
|
+
cwd: string,
|
|
578
|
+
): Promise<{ maxReviewPassesPerLane: number; source: string }> {
|
|
579
|
+
const projectPath = path.join(gjcRoot(cwd), "settings.json");
|
|
580
|
+
const project = await readSettingsMaxReviewPassesPerLane(projectPath);
|
|
581
|
+
if (project.kind === "invalid") {
|
|
582
|
+
throw new RalplanCommandError(2, `invalid ralplan settings at ${projectPath}: ${project.reason}`);
|
|
583
|
+
}
|
|
584
|
+
if (project.kind === "valid") return { maxReviewPassesPerLane: project.value, source: projectPath };
|
|
585
|
+
const userPath = path.join(getConfigRootDir(), "settings.json");
|
|
586
|
+
const user = await readSettingsMaxReviewPassesPerLane(userPath);
|
|
587
|
+
if (user.kind === "invalid") {
|
|
588
|
+
throw new RalplanCommandError(2, `invalid ralplan settings at ${userPath}: ${user.reason}`);
|
|
589
|
+
}
|
|
590
|
+
if (user.kind === "valid") return { maxReviewPassesPerLane: user.value, source: userPath };
|
|
591
|
+
return { maxReviewPassesPerLane: RALPLAN_DEFAULT_MAX_REVIEW_PASSES_PER_LANE, source: "default" };
|
|
592
|
+
}
|
|
250
593
|
|
|
251
594
|
function buildPlanningStuckResult(input: {
|
|
252
595
|
json: boolean;
|
|
@@ -290,6 +633,51 @@ function buildPlanningStuckResult(input: {
|
|
|
290
633
|
};
|
|
291
634
|
}
|
|
292
635
|
|
|
636
|
+
function buildLaneBudgetStuckResult(input: {
|
|
637
|
+
json: boolean;
|
|
638
|
+
stage: RalplanStage;
|
|
639
|
+
stageN: number;
|
|
640
|
+
runId: string;
|
|
641
|
+
decision: Extract<RalplanReviewLaneBudgetDecision, { allowed: false }>;
|
|
642
|
+
source: string;
|
|
643
|
+
}): RalplanCommandResult {
|
|
644
|
+
const detail =
|
|
645
|
+
`${PLANNING_STUCK_MARKER}: ${input.decision.reason} ` +
|
|
646
|
+
`(run_id=${input.runId}, stage=${input.stage}, stage_n=${input.stageN}, source=${input.source}). ` +
|
|
647
|
+
`Stop re-invoking the ${input.decision.lane} review lane in this consensus iteration; ` +
|
|
648
|
+
"route a rule-2-justified blocker through a Planner revision opener (fresh lane budget) while opener budget remains, " +
|
|
649
|
+
"or escalate the best existing plan via post-interview/adr/final without auto-implementation.";
|
|
650
|
+
if (input.json) {
|
|
651
|
+
return {
|
|
652
|
+
status: 3,
|
|
653
|
+
stdout: `${JSON.stringify(
|
|
654
|
+
{
|
|
655
|
+
ok: false,
|
|
656
|
+
planning_stuck: true,
|
|
657
|
+
marker: PLANNING_STUCK_MARKER,
|
|
658
|
+
run_id: input.runId,
|
|
659
|
+
stage: input.stage,
|
|
660
|
+
stage_n: input.stageN,
|
|
661
|
+
lane: input.decision.lane,
|
|
662
|
+
passes: input.decision.currentPasses,
|
|
663
|
+
projected_passes: input.decision.projectedPasses,
|
|
664
|
+
max_review_passes_per_lane: input.decision.maxReviewPassesPerLane,
|
|
665
|
+
max_review_passes_source: input.source,
|
|
666
|
+
reason: input.decision.reason,
|
|
667
|
+
},
|
|
668
|
+
null,
|
|
669
|
+
2,
|
|
670
|
+
)}\n`,
|
|
671
|
+
stderr: `${detail}\n`,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
status: 3,
|
|
676
|
+
stdout: `${PLANNING_STUCK_MARKER}\n`,
|
|
677
|
+
stderr: `${detail}\n`,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
293
681
|
const KNOWN_ARCHITECT_KINDS = new Set(["openai-code"]);
|
|
294
682
|
const KNOWN_CRITIC_KINDS = new Set(["openai-code"]);
|
|
295
683
|
|
|
@@ -322,10 +710,15 @@ const VALUE_FLAGS = new Set([
|
|
|
322
710
|
"--critic",
|
|
323
711
|
"--planner-id",
|
|
324
712
|
"--planner-resumable",
|
|
713
|
+
"--architect-id",
|
|
714
|
+
"--architect-resumable",
|
|
715
|
+
"--critic-id",
|
|
716
|
+
"--critic-resumable",
|
|
325
717
|
"--fallback-reason",
|
|
326
718
|
"--fallback-attempted-id",
|
|
327
719
|
"--fallback-stage-n",
|
|
328
720
|
"--fallback-receipt-path",
|
|
721
|
+
"--lane-verdict",
|
|
329
722
|
]);
|
|
330
723
|
|
|
331
724
|
export function isRalplanArtifactWriteInvocation(args: readonly string[]): boolean {
|
|
@@ -485,6 +878,15 @@ async function persistActiveRunId(cwd: string, sessionId: string, runId: string,
|
|
|
485
878
|
// "complete"/"handoff" and disarm the Stop hook). PHASE_LOCK only guards same-run writes.
|
|
486
879
|
const isNewRun = existing.run_id !== runId;
|
|
487
880
|
const nextPhase = isNewRun ? stage : advanceCurrentPhase(existing.current_phase, stage);
|
|
881
|
+
if (isNewRun) {
|
|
882
|
+
// State writes shallow-merge, so clear both HUD verdict sources at the run boundary.
|
|
883
|
+
delete existing.verdict;
|
|
884
|
+
for (const key of Object.keys(existing)) {
|
|
885
|
+
if (key.startsWith("last_review_verdict")) delete existing[key];
|
|
886
|
+
}
|
|
887
|
+
delete existing.planning_stuck;
|
|
888
|
+
delete existing.auto_handoff;
|
|
889
|
+
}
|
|
488
890
|
if (
|
|
489
891
|
existing.run_id === runId &&
|
|
490
892
|
existing.version === WORKFLOW_STATE_VERSION &&
|
|
@@ -503,6 +905,7 @@ async function persistActiveRunId(cwd: string, sessionId: string, runId: string,
|
|
|
503
905
|
existing.updated_at = new Date().toISOString();
|
|
504
906
|
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
505
907
|
cwd,
|
|
908
|
+
lockHeld: true,
|
|
506
909
|
receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan persist-run-id", sessionId },
|
|
507
910
|
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
|
|
508
911
|
});
|
|
@@ -511,9 +914,12 @@ async function persistActiveRunId(cwd: string, sessionId: string, runId: string,
|
|
|
511
914
|
);
|
|
512
915
|
}
|
|
513
916
|
|
|
514
|
-
/*
|
|
917
|
+
/* ---------------------- persisted role-agent run-state --------------------- */
|
|
918
|
+
|
|
919
|
+
type PersistedRole = "planner" | RalplanReviewLane;
|
|
515
920
|
|
|
516
|
-
interface
|
|
921
|
+
interface PersistedRoleStateUpdate {
|
|
922
|
+
role: PersistedRole;
|
|
517
923
|
subagentId?: string;
|
|
518
924
|
resumable?: boolean;
|
|
519
925
|
fallbackReason?: string;
|
|
@@ -522,6 +928,25 @@ interface PlannerStateUpdate {
|
|
|
522
928
|
fallbackReceiptPath?: string;
|
|
523
929
|
}
|
|
524
930
|
|
|
931
|
+
interface LaneVerdictUpdate {
|
|
932
|
+
lane: RalplanReviewLane;
|
|
933
|
+
verdict: string;
|
|
934
|
+
stageN: number;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const PERSISTED_ROLES = ["planner", "architect", "critic"] as const;
|
|
938
|
+
|
|
939
|
+
const PERSISTED_ROLE_FLAGS: Record<PersistedRole, { id: string; resumable: string }> = {
|
|
940
|
+
planner: { id: "--planner-id", resumable: "--planner-resumable" },
|
|
941
|
+
architect: { id: "--architect-id", resumable: "--architect-resumable" },
|
|
942
|
+
critic: { id: "--critic-id", resumable: "--critic-resumable" },
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
const LANE_VERDICTS: Record<RalplanReviewLane, ReadonlySet<string>> = {
|
|
946
|
+
architect: new Set(["CLEAR", "WATCH", "BLOCK"]),
|
|
947
|
+
critic: new Set(["OKAY", "ITERATE", "REJECT"]),
|
|
948
|
+
};
|
|
949
|
+
|
|
525
950
|
function parseBooleanFlag(raw: string, flag: string): boolean {
|
|
526
951
|
if (raw === "true") return true;
|
|
527
952
|
if (raw === "false") return false;
|
|
@@ -534,7 +959,7 @@ function assertSubagentId(value: string, label: string): void {
|
|
|
534
959
|
}
|
|
535
960
|
}
|
|
536
961
|
|
|
537
|
-
function
|
|
962
|
+
function roleStateFlagValue(args: readonly string[], flag: string): string | undefined {
|
|
538
963
|
const value = flagValue(args, flag);
|
|
539
964
|
if (value === undefined && hasFlag(args, flag)) {
|
|
540
965
|
throw new RalplanCommandError(2, `missing value for ${flag}.`);
|
|
@@ -542,19 +967,64 @@ function plannerFlagValue(args: readonly string[], flag: string): string | undef
|
|
|
542
967
|
return value;
|
|
543
968
|
}
|
|
544
969
|
|
|
970
|
+
function persistedRoleForStage(stage: RalplanStage): PersistedRole | undefined {
|
|
971
|
+
if (stage === "planner" || stage === "revision") return "planner";
|
|
972
|
+
if (stage === "architect" || stage === "critic") return stage;
|
|
973
|
+
return undefined;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function suppliedPersistedRoleFlag(args: readonly string[], role: PersistedRole): string | undefined {
|
|
977
|
+
const flags = PERSISTED_ROLE_FLAGS[role];
|
|
978
|
+
if (hasFlag(args, flags.id)) return flags.id;
|
|
979
|
+
if (hasFlag(args, flags.resumable)) return flags.resumable;
|
|
980
|
+
return undefined;
|
|
981
|
+
}
|
|
982
|
+
|
|
545
983
|
/**
|
|
546
|
-
* Parse the optional persisted-
|
|
547
|
-
* `--write`.
|
|
548
|
-
*
|
|
549
|
-
*
|
|
984
|
+
* Parse the optional same-session persisted role-agent metadata that may ride
|
|
985
|
+
* alongside a `--write`. Planner metadata rides planner/revision stages;
|
|
986
|
+
* Architect and Critic metadata ride only their matching review lane. This is
|
|
987
|
+
* an audit/routing hint, not a durable subagent registry.
|
|
550
988
|
*/
|
|
551
|
-
function
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
const
|
|
557
|
-
|
|
989
|
+
function parsePersistedRoleStateArgs(
|
|
990
|
+
args: readonly string[],
|
|
991
|
+
stage: RalplanStage,
|
|
992
|
+
): PersistedRoleStateUpdate | undefined {
|
|
993
|
+
const role = persistedRoleForStage(stage);
|
|
994
|
+
for (const candidate of PERSISTED_ROLES) {
|
|
995
|
+
const suppliedFlag = suppliedPersistedRoleFlag(args, candidate);
|
|
996
|
+
if (suppliedFlag && candidate !== role) {
|
|
997
|
+
const expectedStages = candidate === "planner" ? "planner or revision" : candidate;
|
|
998
|
+
throw new RalplanCommandError(
|
|
999
|
+
2,
|
|
1000
|
+
`${suppliedFlag} is only valid with --stage ${expectedStages} (received ${stage}).`,
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const fallbackFlagsPresent = [
|
|
1006
|
+
"--fallback-reason",
|
|
1007
|
+
"--fallback-attempted-id",
|
|
1008
|
+
"--fallback-stage-n",
|
|
1009
|
+
"--fallback-receipt-path",
|
|
1010
|
+
].some(flag => hasFlag(args, flag));
|
|
1011
|
+
if (!role) {
|
|
1012
|
+
if (fallbackFlagsPresent) {
|
|
1013
|
+
throw new RalplanCommandError(
|
|
1014
|
+
2,
|
|
1015
|
+
`--fallback-reason is only valid with --stage planner, revision, architect, or critic (received ${stage}).`,
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
return undefined;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const flags = PERSISTED_ROLE_FLAGS[role];
|
|
1022
|
+
const subagentId = roleStateFlagValue(args, flags.id);
|
|
1023
|
+
const resumableRaw = roleStateFlagValue(args, flags.resumable);
|
|
1024
|
+
const fallbackReason = roleStateFlagValue(args, "--fallback-reason");
|
|
1025
|
+
const fallbackAttemptedId = roleStateFlagValue(args, "--fallback-attempted-id");
|
|
1026
|
+
const fallbackStageNRaw = roleStateFlagValue(args, "--fallback-stage-n");
|
|
1027
|
+
const fallbackReceiptPath = roleStateFlagValue(args, "--fallback-receipt-path");
|
|
558
1028
|
|
|
559
1029
|
const anyPresent = [
|
|
560
1030
|
subagentId,
|
|
@@ -566,14 +1036,13 @@ function parsePlannerStateArgs(args: readonly string[]): PlannerStateUpdate | un
|
|
|
566
1036
|
].some(value => value !== undefined);
|
|
567
1037
|
if (!anyPresent) return undefined;
|
|
568
1038
|
|
|
569
|
-
const update:
|
|
570
|
-
|
|
1039
|
+
const update: PersistedRoleStateUpdate = { role };
|
|
571
1040
|
if (subagentId !== undefined) {
|
|
572
|
-
assertSubagentId(subagentId,
|
|
1041
|
+
assertSubagentId(subagentId, flags.id);
|
|
573
1042
|
update.subagentId = subagentId;
|
|
574
1043
|
}
|
|
575
1044
|
if (resumableRaw !== undefined) {
|
|
576
|
-
update.resumable = parseBooleanFlag(resumableRaw,
|
|
1045
|
+
update.resumable = parseBooleanFlag(resumableRaw, flags.resumable);
|
|
577
1046
|
}
|
|
578
1047
|
|
|
579
1048
|
const anyFallback = [fallbackReason, fallbackAttemptedId, fallbackStageNRaw, fallbackReceiptPath].some(
|
|
@@ -581,7 +1050,7 @@ function parsePlannerStateArgs(args: readonly string[]): PlannerStateUpdate | un
|
|
|
581
1050
|
);
|
|
582
1051
|
if (anyFallback) {
|
|
583
1052
|
if (!fallbackReason) {
|
|
584
|
-
throw new RalplanCommandError(2,
|
|
1053
|
+
throw new RalplanCommandError(2, `--fallback-reason is required when recording ${role} fallback metadata.`);
|
|
585
1054
|
}
|
|
586
1055
|
if (!KNOWN_FALLBACK_REASONS.has(fallbackReason)) {
|
|
587
1056
|
throw new RalplanCommandError(
|
|
@@ -593,13 +1062,13 @@ function parsePlannerStateArgs(args: readonly string[]): PlannerStateUpdate | un
|
|
|
593
1062
|
if (fallbackAttemptedId === undefined) {
|
|
594
1063
|
throw new RalplanCommandError(
|
|
595
1064
|
2,
|
|
596
|
-
|
|
1065
|
+
`--fallback-attempted-id is required when recording ${role} fallback metadata.`,
|
|
597
1066
|
);
|
|
598
1067
|
}
|
|
599
1068
|
assertSubagentId(fallbackAttemptedId, "--fallback-attempted-id");
|
|
600
1069
|
update.fallbackAttemptedId = fallbackAttemptedId;
|
|
601
1070
|
if (fallbackStageNRaw === undefined) {
|
|
602
|
-
throw new RalplanCommandError(2,
|
|
1071
|
+
throw new RalplanCommandError(2, `--fallback-stage-n is required when recording ${role} fallback metadata.`);
|
|
603
1072
|
}
|
|
604
1073
|
update.fallbackStageN = parseStageN(fallbackStageNRaw);
|
|
605
1074
|
if (fallbackReceiptPath !== undefined) {
|
|
@@ -613,24 +1082,107 @@ function parsePlannerStateArgs(args: readonly string[]): PlannerStateUpdate | un
|
|
|
613
1082
|
return update;
|
|
614
1083
|
}
|
|
615
1084
|
|
|
616
|
-
/**
|
|
617
|
-
function
|
|
1085
|
+
/** Parse the self-reported review verdict carried by a lane artifact write. */
|
|
1086
|
+
function parseLaneVerdictArgs(
|
|
1087
|
+
args: readonly string[],
|
|
1088
|
+
stage: RalplanStage,
|
|
1089
|
+
stageN: number,
|
|
1090
|
+
): LaneVerdictUpdate | undefined {
|
|
1091
|
+
const rawVerdict = roleStateFlagValue(args, "--lane-verdict");
|
|
1092
|
+
if (rawVerdict === undefined) return undefined;
|
|
1093
|
+
if (stage !== "architect" && stage !== "critic") {
|
|
1094
|
+
throw new RalplanCommandError(
|
|
1095
|
+
2,
|
|
1096
|
+
`--lane-verdict is only valid with --stage architect or critic (received ${stage}).`,
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
const verdict = rawVerdict.trim().toUpperCase();
|
|
1100
|
+
if (!LANE_VERDICTS[stage].has(verdict)) {
|
|
1101
|
+
throw new RalplanCommandError(
|
|
1102
|
+
2,
|
|
1103
|
+
`invalid --lane-verdict for ${stage}: ${rawVerdict}. Expected one of: ${[...LANE_VERDICTS[stage]].join(", ")}.`,
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
return { lane: stage, verdict, stageN };
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Snake-case projection of persisted role metadata for state JSON + receipts.
|
|
1111
|
+
* Omitted fields stay absent — an unknown resumability value is never encoded
|
|
1112
|
+
* as literal null.
|
|
1113
|
+
*/
|
|
1114
|
+
function persistedRoleStatePayload(update: PersistedRoleStateUpdate): Record<string, unknown> {
|
|
618
1115
|
const payload: Record<string, unknown> = {};
|
|
619
|
-
|
|
620
|
-
if (update.
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if (update.
|
|
624
|
-
if (update.
|
|
1116
|
+
const prefix = update.role;
|
|
1117
|
+
if (update.subagentId !== undefined) {
|
|
1118
|
+
payload[prefix === "planner" ? "planner_subagent_id" : `${prefix}_id`] = update.subagentId;
|
|
1119
|
+
}
|
|
1120
|
+
if (update.resumable !== undefined) payload[`${prefix}_resumable`] = update.resumable;
|
|
1121
|
+
if (update.fallbackReason !== undefined) payload[`${prefix}_fallback_reason`] = update.fallbackReason;
|
|
1122
|
+
if (update.fallbackAttemptedId !== undefined)
|
|
1123
|
+
payload[`${prefix}_fallback_attempted_id`] = update.fallbackAttemptedId;
|
|
1124
|
+
if (update.fallbackStageN !== undefined) payload[`${prefix}_fallback_stage_n`] = update.fallbackStageN;
|
|
1125
|
+
if (update.fallbackReceiptPath !== undefined) {
|
|
1126
|
+
payload[`${prefix}_fallback_receipt_path`] = update.fallbackReceiptPath;
|
|
1127
|
+
}
|
|
625
1128
|
return payload;
|
|
626
1129
|
}
|
|
627
1130
|
|
|
628
1131
|
/**
|
|
629
|
-
* Merge persisted-
|
|
630
|
-
* audit/routing hint only
|
|
631
|
-
*
|
|
1132
|
+
* Merge persisted role-agent metadata into the ralplan run-state JSON, optionally only for its active run.
|
|
1133
|
+
* This is a same-session audit/routing hint only; it does not create a durable
|
|
1134
|
+
* cross-process subagent registry, ledger entry, or additional state file.
|
|
632
1135
|
*/
|
|
633
|
-
async function
|
|
1136
|
+
async function applyPersistedRoleStateUpdate(
|
|
1137
|
+
cwd: string,
|
|
1138
|
+
sessionId: string,
|
|
1139
|
+
update: PersistedRoleStateUpdate,
|
|
1140
|
+
expectedActiveRunId?: string,
|
|
1141
|
+
): Promise<boolean> {
|
|
1142
|
+
const statePath = ralplanStatePath(cwd, sessionId);
|
|
1143
|
+
return await withWorkflowStateLock(
|
|
1144
|
+
statePath,
|
|
1145
|
+
async () => {
|
|
1146
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
1147
|
+
if (existingRead.kind === "corrupt") {
|
|
1148
|
+
throw new RalplanCommandError(
|
|
1149
|
+
2,
|
|
1150
|
+
`existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to overwrite ${statePath}`,
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1154
|
+
if (expectedActiveRunId !== undefined && existing.run_id !== expectedActiveRunId) return false;
|
|
1155
|
+
Object.assign(existing, persistedRoleStatePayload(update));
|
|
1156
|
+
if (typeof existing.skill !== "string") existing.skill = "ralplan";
|
|
1157
|
+
if (typeof existing.active !== "boolean") existing.active = true;
|
|
1158
|
+
if (typeof existing.current_phase !== "string") existing.current_phase = "planner";
|
|
1159
|
+
existing = migrateWorkflowState(existing, "ralplan").state;
|
|
1160
|
+
existing.updated_at = new Date().toISOString();
|
|
1161
|
+
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
1162
|
+
cwd,
|
|
1163
|
+
lockHeld: true,
|
|
1164
|
+
receipt: {
|
|
1165
|
+
cwd,
|
|
1166
|
+
skill: "ralplan",
|
|
1167
|
+
owner: "gjc-runtime",
|
|
1168
|
+
command: `gjc ralplan ${update.role}-state`,
|
|
1169
|
+
sessionId,
|
|
1170
|
+
},
|
|
1171
|
+
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
|
|
1172
|
+
});
|
|
1173
|
+
return true;
|
|
1174
|
+
},
|
|
1175
|
+
{ cwd },
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/** Merge a lane's self-reported verdict into the same-session ralplan run state, optionally only for its active run. */
|
|
1180
|
+
async function applyLaneVerdictUpdate(
|
|
1181
|
+
cwd: string,
|
|
1182
|
+
sessionId: string,
|
|
1183
|
+
update: LaneVerdictUpdate,
|
|
1184
|
+
expectedActiveRunId?: string,
|
|
1185
|
+
): Promise<boolean> {
|
|
634
1186
|
const statePath = ralplanStatePath(cwd, sessionId);
|
|
635
1187
|
return await withWorkflowStateLock(
|
|
636
1188
|
statePath,
|
|
@@ -643,7 +1195,12 @@ async function applyPlannerStateUpdate(cwd: string, sessionId: string, update: P
|
|
|
643
1195
|
);
|
|
644
1196
|
}
|
|
645
1197
|
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
646
|
-
|
|
1198
|
+
if (expectedActiveRunId !== undefined && existing.run_id !== expectedActiveRunId) return false;
|
|
1199
|
+
Object.assign(existing, {
|
|
1200
|
+
last_review_verdict: update.verdict,
|
|
1201
|
+
last_review_verdict_lane: update.lane,
|
|
1202
|
+
last_review_verdict_stage_n: update.stageN,
|
|
1203
|
+
});
|
|
647
1204
|
if (typeof existing.skill !== "string") existing.skill = "ralplan";
|
|
648
1205
|
if (typeof existing.active !== "boolean") existing.active = true;
|
|
649
1206
|
if (typeof existing.current_phase !== "string") existing.current_phase = "planner";
|
|
@@ -651,13 +1208,203 @@ async function applyPlannerStateUpdate(cwd: string, sessionId: string, update: P
|
|
|
651
1208
|
existing.updated_at = new Date().toISOString();
|
|
652
1209
|
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
653
1210
|
cwd,
|
|
654
|
-
|
|
1211
|
+
lockHeld: true,
|
|
1212
|
+
receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan lane-verdict", sessionId },
|
|
655
1213
|
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
|
|
656
1214
|
});
|
|
1215
|
+
return true;
|
|
657
1216
|
},
|
|
658
1217
|
{ cwd },
|
|
659
1218
|
);
|
|
660
1219
|
}
|
|
1220
|
+
function ralplanPlanningStuckIndexKey(entry: unknown): string | undefined {
|
|
1221
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return undefined;
|
|
1222
|
+
const record = entry as Record<string, unknown>;
|
|
1223
|
+
return record.planning_stuck === true ? "planning_stuck" : undefined;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
async function recordRalplanPlanningStuck(
|
|
1227
|
+
cwd: string,
|
|
1228
|
+
sessionId: string,
|
|
1229
|
+
runId: string,
|
|
1230
|
+
reason: string,
|
|
1231
|
+
): Promise<void> {
|
|
1232
|
+
const runDir = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId);
|
|
1233
|
+
await appendJsonlIdempotent(
|
|
1234
|
+
path.join(runDir, "index.jsonl"),
|
|
1235
|
+
{
|
|
1236
|
+
event: "planning_stuck",
|
|
1237
|
+
planning_stuck: true,
|
|
1238
|
+
marker: PLANNING_STUCK_MARKER,
|
|
1239
|
+
reason,
|
|
1240
|
+
created_at: new Date().toISOString(),
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
cwd,
|
|
1244
|
+
audit: {
|
|
1245
|
+
category: "ledger",
|
|
1246
|
+
verb: "append",
|
|
1247
|
+
owner: "gjc-runtime",
|
|
1248
|
+
skill: "ralplan",
|
|
1249
|
+
sessionId,
|
|
1250
|
+
},
|
|
1251
|
+
key: ralplanPlanningStuckIndexKey,
|
|
1252
|
+
},
|
|
1253
|
+
);
|
|
1254
|
+
const statePath = ralplanStatePath(cwd, sessionId);
|
|
1255
|
+
await withWorkflowStateLock(
|
|
1256
|
+
statePath,
|
|
1257
|
+
async () => {
|
|
1258
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
1259
|
+
if (existingRead.kind === "corrupt") return;
|
|
1260
|
+
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1261
|
+
if (existing.run_id !== runId) return;
|
|
1262
|
+
existing.planning_stuck = { marker: PLANNING_STUCK_MARKER, reason };
|
|
1263
|
+
existing = migrateWorkflowState(existing, "ralplan").state;
|
|
1264
|
+
existing.updated_at = new Date().toISOString();
|
|
1265
|
+
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
1266
|
+
cwd,
|
|
1267
|
+
lockHeld: true,
|
|
1268
|
+
receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan planning-stuck", sessionId },
|
|
1269
|
+
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
|
|
1270
|
+
});
|
|
1271
|
+
},
|
|
1272
|
+
{ cwd },
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
async function readRalplanFinalAdmission(
|
|
1277
|
+
cwd: string,
|
|
1278
|
+
sessionId: string,
|
|
1279
|
+
runId: string,
|
|
1280
|
+
): Promise<RalplanAutoHandoffResolution | undefined> {
|
|
1281
|
+
const index = await loadRalplanIndexForCap(cwd, sessionId, runId);
|
|
1282
|
+
if (index.rawText === undefined) return undefined;
|
|
1283
|
+
let finalAdmission: RalplanAutoHandoffResolution | undefined;
|
|
1284
|
+
for (const line of index.rawText.split(/\r?\n/)) {
|
|
1285
|
+
try {
|
|
1286
|
+
const row = JSON.parse(line) as Record<string, unknown>;
|
|
1287
|
+
if (row.stage === "final" && typeof row.path === "string" && typeof row.sha256 === "string") {
|
|
1288
|
+
finalAdmission = parseRalplanFinalAdmission(row.auto_handoff) ?? unavailableRalplanFinalAdmission();
|
|
1289
|
+
}
|
|
1290
|
+
} catch {
|
|
1291
|
+
// The artifact dedupe guard handles malformed ledger records separately.
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return finalAdmission;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
async function readRalplanPlanningStuck(cwd: string, sessionId: string, runId: string): Promise<boolean> {
|
|
1298
|
+
const index = await loadRalplanIndexForCap(cwd, sessionId, runId);
|
|
1299
|
+
if (index.rawText === undefined) return index.indexPresent;
|
|
1300
|
+
if (index.indexPresent && index.rawLineCount > 0 && index.parseableLines === 0) return true;
|
|
1301
|
+
for (const line of index.rawText.split(/\r?\n/)) {
|
|
1302
|
+
if (!line.trim()) continue;
|
|
1303
|
+
try {
|
|
1304
|
+
const row = JSON.parse(line) as Record<string, unknown>;
|
|
1305
|
+
if (row.planning_stuck === true) return true;
|
|
1306
|
+
} catch {
|
|
1307
|
+
return true;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
async function persistRalplanFinalAdmission(
|
|
1314
|
+
cwd: string,
|
|
1315
|
+
sessionId: string,
|
|
1316
|
+
runId: string,
|
|
1317
|
+
admission: RalplanAutoHandoffResolution,
|
|
1318
|
+
): Promise<void> {
|
|
1319
|
+
const statePath = ralplanStatePath(cwd, sessionId);
|
|
1320
|
+
await withWorkflowStateLock(
|
|
1321
|
+
statePath,
|
|
1322
|
+
async () => {
|
|
1323
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
1324
|
+
if (existingRead.kind === "corrupt") {
|
|
1325
|
+
throw new RalplanCommandError(
|
|
1326
|
+
2,
|
|
1327
|
+
`existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to record final admission`,
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1331
|
+
if (existing.run_id !== runId) return;
|
|
1332
|
+
existing.auto_handoff = admission;
|
|
1333
|
+
existing = migrateWorkflowState(existing, "ralplan").state;
|
|
1334
|
+
existing.updated_at = new Date().toISOString();
|
|
1335
|
+
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
1336
|
+
cwd,
|
|
1337
|
+
lockHeld: true,
|
|
1338
|
+
receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan final-admission", sessionId },
|
|
1339
|
+
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
|
|
1340
|
+
});
|
|
1341
|
+
},
|
|
1342
|
+
{ cwd },
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
async function findExistingRalplanRunOwners(cwd: string, runId: string): Promise<string[]> {
|
|
1347
|
+
let entries: Dirent<string>[];
|
|
1348
|
+
try {
|
|
1349
|
+
entries = await fs.readdir(gjcRoot(cwd), { withFileTypes: true });
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
const err = error as NodeJS.ErrnoException;
|
|
1352
|
+
if (err.code === "ENOENT" || err.code === "ENOTDIR") return [];
|
|
1353
|
+
throw error;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
const owners = await Promise.all(
|
|
1357
|
+
entries.map(async entry => {
|
|
1358
|
+
if (!entry.isDirectory()) return undefined;
|
|
1359
|
+
const sessionId = sessionIdFromDirName(entry.name);
|
|
1360
|
+
if (!sessionId) return undefined;
|
|
1361
|
+
|
|
1362
|
+
const stateRead = await readExistingStateForMutation(ralplanStatePath(cwd, sessionId));
|
|
1363
|
+
const stateOwnsRun =
|
|
1364
|
+
stateRead.kind === "valid" &&
|
|
1365
|
+
typeof stateRead.value.run_id === "string" &&
|
|
1366
|
+
stateRead.value.run_id.trim() === runId;
|
|
1367
|
+
if (stateOwnsRun) return sessionId;
|
|
1368
|
+
|
|
1369
|
+
try {
|
|
1370
|
+
const stat = await fs.stat(path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId));
|
|
1371
|
+
return stat.isDirectory() ? sessionId : undefined;
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
const err = error as NodeJS.ErrnoException;
|
|
1374
|
+
if (err.code === "ENOENT" || err.code === "ENOTDIR") return undefined;
|
|
1375
|
+
throw error;
|
|
1376
|
+
}
|
|
1377
|
+
}),
|
|
1378
|
+
);
|
|
1379
|
+
return owners.filter((owner): owner is string => owner !== undefined).sort();
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
async function resolveArtifactSessionId(args: readonly string[], cwd: string, explicitRunId: string | undefined) {
|
|
1383
|
+
const flagSessionId = flagValue(args, "--session-id");
|
|
1384
|
+
const currentSession = resolveGjcSessionForWrite(cwd, {
|
|
1385
|
+
flagValue: flagSessionId,
|
|
1386
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
1387
|
+
});
|
|
1388
|
+
if (!explicitRunId) return currentSession.gjcSessionId;
|
|
1389
|
+
|
|
1390
|
+
const owners = await findExistingRalplanRunOwners(cwd, explicitRunId);
|
|
1391
|
+
if (owners.length === 0) return currentSession.gjcSessionId;
|
|
1392
|
+
if (owners.length > 1) {
|
|
1393
|
+
throw new RalplanCommandError(
|
|
1394
|
+
2,
|
|
1395
|
+
`ralplan run ${explicitRunId} has multiple owner sessions (${owners.join(", ")}); repair the fragmented run before writing`,
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
const ownerSessionId = owners[0]!;
|
|
1400
|
+
if (flagSessionId !== undefined && currentSession.gjcSessionId !== ownerSessionId) {
|
|
1401
|
+
throw new RalplanCommandError(
|
|
1402
|
+
2,
|
|
1403
|
+
`ralplan run ${explicitRunId} is owned by session ${ownerSessionId}, not ${currentSession.gjcSessionId}`,
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
return ownerSessionId;
|
|
1407
|
+
}
|
|
661
1408
|
|
|
662
1409
|
async function resolveArtifactArgs(args: readonly string[], cwd: string): Promise<ResolvedArtifactArgs> {
|
|
663
1410
|
const stage = flagValue(args, "--stage");
|
|
@@ -679,20 +1426,18 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
|
|
|
679
1426
|
throw new RalplanCommandError(2, `--artifact-env must be ${GJC_RALPLAN_ARTIFACT_ENV}`);
|
|
680
1427
|
}
|
|
681
1428
|
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const sessionId = session.gjcSessionId;
|
|
1429
|
+
const explicitRunId = flagValue(args, "--run-id")?.trim();
|
|
1430
|
+
if (explicitRunId) assertSafePathComponent(explicitRunId, "run-id");
|
|
1431
|
+
|
|
1432
|
+
const sessionId = await resolveArtifactSessionId(args, cwd, explicitRunId);
|
|
687
1433
|
assertSafePathComponent(sessionId, "session-id");
|
|
688
1434
|
const sessionIdRaw = sessionId;
|
|
689
1435
|
|
|
690
1436
|
// Precedence for run_id:
|
|
691
1437
|
// 1. explicit --run-id flag
|
|
692
|
-
// 2. existing run_id field in
|
|
693
|
-
// 3.
|
|
1438
|
+
// 2. existing run_id field in the resolved owner session's ralplan state
|
|
1439
|
+
// 3. resolved owner session id
|
|
694
1440
|
// 4. freshly generated default run id
|
|
695
|
-
const explicitRunId = flagValue(args, "--run-id")?.trim();
|
|
696
1441
|
const runId = explicitRunId || (await readActiveRunId(cwd, sessionId)) || sessionIdRaw || defaultRunId();
|
|
697
1442
|
assertSafePathComponent(runId, "run-id");
|
|
698
1443
|
|
|
@@ -734,6 +1479,7 @@ async function persistArtifact(
|
|
|
734
1479
|
cwd: string,
|
|
735
1480
|
content: string,
|
|
736
1481
|
sha256: string,
|
|
1482
|
+
finalAdmission?: RalplanAutoHandoffResolution,
|
|
737
1483
|
): Promise<PersistedArtifact> {
|
|
738
1484
|
const runDir = path.join(sessionPlansDir(cwd, resolved.sessionId), "ralplan", resolved.runId);
|
|
739
1485
|
|
|
@@ -757,6 +1503,7 @@ async function persistArtifact(
|
|
|
757
1503
|
path: filePath,
|
|
758
1504
|
created_at: createdAt,
|
|
759
1505
|
sha256,
|
|
1506
|
+
...(finalAdmission ? { auto_handoff: finalAdmission } : {}),
|
|
760
1507
|
};
|
|
761
1508
|
await appendJsonlIdempotent(path.join(runDir, "index.jsonl"), indexEntry, {
|
|
762
1509
|
cwd,
|
|
@@ -801,31 +1548,51 @@ interface ExistingStageArtifact {
|
|
|
801
1548
|
path: string;
|
|
802
1549
|
sha256: string;
|
|
803
1550
|
createdAt: string;
|
|
1551
|
+
autoHandoff?: RalplanAutoHandoffResolution;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function parseRalplanFinalAdmission(value: unknown): RalplanAutoHandoffResolution | undefined {
|
|
1555
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1556
|
+
const admission = value as Record<string, unknown>;
|
|
1557
|
+
if (
|
|
1558
|
+
!RALPLAN_AUTO_HANDOFF_TARGETS.has(admission.configuredTarget as RalplanAutoHandoffTarget) ||
|
|
1559
|
+
!RALPLAN_AUTO_HANDOFF_TARGETS.has(admission.effectiveTarget as RalplanAutoHandoffTarget) ||
|
|
1560
|
+
(typeof admission.degradationReason !== "string" && admission.degradationReason !== null) ||
|
|
1561
|
+
typeof admission.source !== "string"
|
|
1562
|
+
) {
|
|
1563
|
+
return undefined;
|
|
1564
|
+
}
|
|
1565
|
+
return {
|
|
1566
|
+
configuredTarget: admission.configuredTarget as RalplanAutoHandoffTarget,
|
|
1567
|
+
effectiveTarget: admission.effectiveTarget as RalplanAutoHandoffTarget,
|
|
1568
|
+
degradationReason: typeof admission.degradationReason === "string" ? admission.degradationReason : null,
|
|
1569
|
+
source: admission.source,
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function unavailableRalplanFinalAdmission(): RalplanAutoHandoffResolution {
|
|
1574
|
+
return {
|
|
1575
|
+
configuredTarget: "off",
|
|
1576
|
+
effectiveTarget: "off",
|
|
1577
|
+
degradationReason: "admission_unavailable",
|
|
1578
|
+
source: "ledger",
|
|
1579
|
+
};
|
|
804
1580
|
}
|
|
805
1581
|
|
|
806
1582
|
/**
|
|
807
|
-
* Find the most recent `index.jsonl` row for a `(stage, stage_n)` pair
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
* truth for dedup because it is exactly what a duplicate write would corrupt.
|
|
1583
|
+
* Find the most recent complete `index.jsonl` row for a `(stage, stage_n)` pair
|
|
1584
|
+
* in the single ledger snapshot used by the dedupe guard and both budget gates.
|
|
1585
|
+
* A parseable row missing `path` or `sha256` is intentionally treated as missing
|
|
1586
|
+
* so the deterministic on-disk probe can repair the crash gap.
|
|
812
1587
|
*/
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
sessionId: string,
|
|
816
|
-
runId: string,
|
|
1588
|
+
function findExistingStageArtifact(
|
|
1589
|
+
indexText: string | undefined,
|
|
817
1590
|
stage: RalplanStage,
|
|
818
1591
|
stageN: number,
|
|
819
|
-
):
|
|
820
|
-
|
|
821
|
-
let text: string;
|
|
822
|
-
try {
|
|
823
|
-
text = await fs.readFile(indexPath, "utf8");
|
|
824
|
-
} catch {
|
|
825
|
-
return undefined;
|
|
826
|
-
}
|
|
1592
|
+
): ExistingStageArtifact | undefined {
|
|
1593
|
+
if (indexText === undefined) return undefined;
|
|
827
1594
|
let match: ExistingStageArtifact | undefined;
|
|
828
|
-
for (const line of
|
|
1595
|
+
for (const line of indexText.split(/\r?\n/)) {
|
|
829
1596
|
const trimmed = line.trim();
|
|
830
1597
|
if (!trimmed) continue;
|
|
831
1598
|
let row: unknown;
|
|
@@ -842,11 +1609,140 @@ async function findExistingStageArtifact(
|
|
|
842
1609
|
path: record.path,
|
|
843
1610
|
sha256: record.sha256,
|
|
844
1611
|
createdAt: typeof record.created_at === "string" ? record.created_at : "",
|
|
1612
|
+
...(stage === "final" ? { autoHandoff: parseRalplanFinalAdmission(record.auto_handoff) } : {}),
|
|
845
1613
|
};
|
|
846
1614
|
}
|
|
847
1615
|
return match;
|
|
848
1616
|
}
|
|
849
1617
|
|
|
1618
|
+
interface OnDiskStageArtifact {
|
|
1619
|
+
path: string;
|
|
1620
|
+
sha256: string;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/** Probe the deterministic artifact path left by a crash before ledger append. */
|
|
1624
|
+
async function findOnDiskStageArtifact(
|
|
1625
|
+
cwd: string,
|
|
1626
|
+
resolved: Pick<ResolvedArtifactArgs, "sessionId" | "runId" | "stage" | "stageN">,
|
|
1627
|
+
): Promise<OnDiskStageArtifact | undefined> {
|
|
1628
|
+
const filePath = path.join(
|
|
1629
|
+
sessionPlansDir(cwd, resolved.sessionId),
|
|
1630
|
+
"ralplan",
|
|
1631
|
+
resolved.runId,
|
|
1632
|
+
`stage-${pad2(resolved.stageN)}-${resolved.stage}.md`,
|
|
1633
|
+
);
|
|
1634
|
+
try {
|
|
1635
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
1636
|
+
return { path: filePath, sha256: createHash("sha256").update(content).digest("hex") };
|
|
1637
|
+
} catch (error) {
|
|
1638
|
+
const code = getErrorCode(error);
|
|
1639
|
+
if (code === "ENOENT" || code === "ENOTDIR") return undefined;
|
|
1640
|
+
throw error;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/** Ensure a deduplicated final stage still has its byte-identical pending-approval copy. */
|
|
1645
|
+
async function ensureFinalPendingApproval(
|
|
1646
|
+
cwd: string,
|
|
1647
|
+
resolved: Pick<ResolvedArtifactArgs, "sessionId" | "runId" | "stageN">,
|
|
1648
|
+
stageArtifact: Pick<OnDiskStageArtifact, "path" | "sha256">,
|
|
1649
|
+
): Promise<string> {
|
|
1650
|
+
const pendingApprovalPath = path.join(
|
|
1651
|
+
sessionPlansDir(cwd, resolved.sessionId),
|
|
1652
|
+
"ralplan",
|
|
1653
|
+
resolved.runId,
|
|
1654
|
+
"pending-approval.md",
|
|
1655
|
+
);
|
|
1656
|
+
const stageContent = await fs.readFile(stageArtifact.path);
|
|
1657
|
+
const stageSha256 = createHash("sha256").update(stageContent).digest("hex");
|
|
1658
|
+
if (stageSha256 !== stageArtifact.sha256) {
|
|
1659
|
+
throw new RalplanCommandError(
|
|
1660
|
+
2,
|
|
1661
|
+
`refusing to deduplicate ralplan final stage ${resolved.stageN}: stage artifact sha256 mismatch at ${stageArtifact.path} (ledger sha256=${stageArtifact.sha256}, artifact sha256=${stageSha256}).`,
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
let pendingContent: Buffer;
|
|
1666
|
+
try {
|
|
1667
|
+
pendingContent = await fs.readFile(pendingApprovalPath);
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
if (getErrorCode(error) !== "ENOENT") throw error;
|
|
1670
|
+
await writeArtifact(pendingApprovalPath, stageContent.toString("utf8"), {
|
|
1671
|
+
cwd,
|
|
1672
|
+
audit: {
|
|
1673
|
+
category: "artifact",
|
|
1674
|
+
verb: "write",
|
|
1675
|
+
owner: "gjc-runtime",
|
|
1676
|
+
skill: "ralplan",
|
|
1677
|
+
sessionId: resolved.sessionId,
|
|
1678
|
+
},
|
|
1679
|
+
});
|
|
1680
|
+
return pendingApprovalPath;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const pendingSha256 = createHash("sha256").update(pendingContent).digest("hex");
|
|
1684
|
+
if (!pendingContent.equals(stageContent) || pendingSha256 !== stageSha256) {
|
|
1685
|
+
throw new RalplanCommandError(
|
|
1686
|
+
2,
|
|
1687
|
+
`refusing to deduplicate ralplan final stage ${resolved.stageN}: pending approval content mismatch at ${pendingApprovalPath} (stage sha256=${stageSha256}, pending sha256=${pendingSha256}).`,
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
return pendingApprovalPath;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/** Append the missing row for a deterministic artifact that survived a crash gap. */
|
|
1694
|
+
async function repairMissingStageArtifactLedger(
|
|
1695
|
+
cwd: string,
|
|
1696
|
+
resolved: Pick<ResolvedArtifactArgs, "sessionId" | "runId" | "stage" | "stageN">,
|
|
1697
|
+
onDisk: OnDiskStageArtifact,
|
|
1698
|
+
finalAdmission?: RalplanAutoHandoffResolution,
|
|
1699
|
+
): Promise<ExistingStageArtifact> {
|
|
1700
|
+
const createdAt = new Date().toISOString();
|
|
1701
|
+
const indexEntry = {
|
|
1702
|
+
stage: resolved.stage,
|
|
1703
|
+
stage_n: resolved.stageN,
|
|
1704
|
+
path: onDisk.path,
|
|
1705
|
+
created_at: createdAt,
|
|
1706
|
+
sha256: onDisk.sha256,
|
|
1707
|
+
...(finalAdmission ? { auto_handoff: finalAdmission } : {}),
|
|
1708
|
+
};
|
|
1709
|
+
const result = await appendJsonlIdempotent(
|
|
1710
|
+
path.join(sessionPlansDir(cwd, resolved.sessionId), "ralplan", resolved.runId, "index.jsonl"),
|
|
1711
|
+
indexEntry,
|
|
1712
|
+
{
|
|
1713
|
+
cwd,
|
|
1714
|
+
audit: {
|
|
1715
|
+
category: "ledger",
|
|
1716
|
+
verb: "append",
|
|
1717
|
+
owner: "gjc-runtime",
|
|
1718
|
+
skill: "ralplan",
|
|
1719
|
+
sessionId: resolved.sessionId,
|
|
1720
|
+
},
|
|
1721
|
+
key: ralplanIndexKey,
|
|
1722
|
+
},
|
|
1723
|
+
);
|
|
1724
|
+
const duplicate = result.duplicate;
|
|
1725
|
+
if (duplicate && typeof duplicate === "object" && !Array.isArray(duplicate)) {
|
|
1726
|
+
const record = duplicate as Record<string, unknown>;
|
|
1727
|
+
if (typeof record.path === "string" && typeof record.sha256 === "string") {
|
|
1728
|
+
return {
|
|
1729
|
+
path: record.path,
|
|
1730
|
+
sha256: record.sha256,
|
|
1731
|
+
createdAt: typeof record.created_at === "string" ? record.created_at : createdAt,
|
|
1732
|
+
...(resolved.stage === "final"
|
|
1733
|
+
? { autoHandoff: parseRalplanFinalAdmission(record.auto_handoff) ?? unavailableRalplanFinalAdmission() }
|
|
1734
|
+
: {}),
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return {
|
|
1739
|
+
path: onDisk.path,
|
|
1740
|
+
sha256: onDisk.sha256,
|
|
1741
|
+
createdAt,
|
|
1742
|
+
...(resolved.stage === "final" ? { autoHandoff: finalAdmission ?? unavailableRalplanFinalAdmission() } : {}),
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
|
|
850
1746
|
/**
|
|
851
1747
|
* Read and parse the run's `index.jsonl` rows. Best-effort: returns [] when the
|
|
852
1748
|
* file is absent or unreadable so HUD sync never fails on a missing index.
|
|
@@ -866,6 +1762,18 @@ async function readRalplanIndexRows(cwd: string, sessionId: string, runId: strin
|
|
|
866
1762
|
}
|
|
867
1763
|
}
|
|
868
1764
|
|
|
1765
|
+
/** Read the lane verdict from ralplan run state without making HUD sync fail on state read errors. */
|
|
1766
|
+
async function readRalplanLastReviewVerdict(cwd: string, sessionId: string): Promise<string | undefined> {
|
|
1767
|
+
try {
|
|
1768
|
+
const existingRead = await readExistingStateForMutation(ralplanStatePath(cwd, sessionId));
|
|
1769
|
+
return existingRead.kind === "valid" && typeof existingRead.value.last_review_verdict === "string"
|
|
1770
|
+
? existingRead.value.last_review_verdict
|
|
1771
|
+
: undefined;
|
|
1772
|
+
} catch {
|
|
1773
|
+
return undefined;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
869
1777
|
async function syncRalplanHud(options: {
|
|
870
1778
|
cwd: string;
|
|
871
1779
|
sessionId: string;
|
|
@@ -873,6 +1781,7 @@ async function syncRalplanHud(options: {
|
|
|
873
1781
|
pendingApproval: boolean;
|
|
874
1782
|
iteration?: number;
|
|
875
1783
|
runId?: string;
|
|
1784
|
+
reviewPassBudget?: number;
|
|
876
1785
|
latestSummary?: string;
|
|
877
1786
|
}): Promise<void> {
|
|
878
1787
|
try {
|
|
@@ -898,15 +1807,31 @@ async function buildRalplanHud(options: {
|
|
|
898
1807
|
latestSummary?: string;
|
|
899
1808
|
runId?: string;
|
|
900
1809
|
sessionId?: string;
|
|
1810
|
+
reviewPassBudget?: number;
|
|
901
1811
|
}) {
|
|
902
1812
|
let iterationFromIndex: number | undefined;
|
|
903
1813
|
let stages: string | undefined;
|
|
1814
|
+
let architectPasses: number | undefined;
|
|
1815
|
+
let criticPasses: number | undefined;
|
|
1816
|
+
let verdict: string | undefined;
|
|
1817
|
+
let autoHandoff: RalplanAutoHandoffResolution | undefined;
|
|
1818
|
+
let planningStuck = false;
|
|
904
1819
|
if (options.runId && options.sessionId) {
|
|
905
|
-
const rows = await
|
|
1820
|
+
const [rows, lastReviewVerdict, persistedAutoHandoff, persistedPlanningStuck] = await Promise.all([
|
|
1821
|
+
readRalplanIndexRows(options.cwd, options.sessionId, options.runId),
|
|
1822
|
+
readRalplanLastReviewVerdict(options.cwd, options.sessionId),
|
|
1823
|
+
readRalplanFinalAdmission(options.cwd, options.sessionId, options.runId),
|
|
1824
|
+
readRalplanPlanningStuck(options.cwd, options.sessionId, options.runId),
|
|
1825
|
+
]);
|
|
1826
|
+
verdict = lastReviewVerdict;
|
|
1827
|
+
autoHandoff = persistedAutoHandoff;
|
|
1828
|
+
planningStuck = persistedPlanningStuck;
|
|
906
1829
|
if (rows.length > 0) {
|
|
907
1830
|
const summary = summarizeRalplanIndex(rows);
|
|
908
1831
|
iterationFromIndex = summary.iteration;
|
|
909
1832
|
stages = formatRalplanStagePresence(summary.currentStages);
|
|
1833
|
+
architectPasses = summary.currentStages.filter(stage => stage === "architect").length;
|
|
1834
|
+
criticPasses = summary.currentStages.filter(stage => stage === "critic").length;
|
|
910
1835
|
}
|
|
911
1836
|
}
|
|
912
1837
|
return buildRalplanHudSummary({
|
|
@@ -914,6 +1839,12 @@ async function buildRalplanHud(options: {
|
|
|
914
1839
|
iteration: options.iteration,
|
|
915
1840
|
iterationFromIndex,
|
|
916
1841
|
stages,
|
|
1842
|
+
architectPasses,
|
|
1843
|
+
criticPasses,
|
|
1844
|
+
reviewPassBudget: options.reviewPassBudget,
|
|
1845
|
+
verdict,
|
|
1846
|
+
autoHandoff,
|
|
1847
|
+
planningStuck,
|
|
917
1848
|
pendingApproval: options.pendingApproval,
|
|
918
1849
|
latestSummary: options.latestSummary,
|
|
919
1850
|
updatedAt: new Date().toISOString(),
|
|
@@ -921,8 +1852,9 @@ async function buildRalplanHud(options: {
|
|
|
921
1852
|
}
|
|
922
1853
|
|
|
923
1854
|
async function handleArtifactWrite(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
|
|
924
|
-
const plannerState = parsePlannerStateArgs(args);
|
|
925
1855
|
const resolved = await resolveArtifactArgs(args, cwd);
|
|
1856
|
+
const persistedRoleState = parsePersistedRoleStateArgs(args, resolved.stage);
|
|
1857
|
+
const laneVerdict = parseLaneVerdictArgs(args, resolved.stage, resolved.stageN);
|
|
926
1858
|
// Fail closed before stage persistence / path writes when cwd drifted to a sibling repo.
|
|
927
1859
|
const repositoryBinding = await enforceRalplanRepositoryBinding(cwd, resolved.sessionId);
|
|
928
1860
|
// Artifact file paths (when --artifact points at a file) must stay under the bound root.
|
|
@@ -944,16 +1876,15 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
944
1876
|
const content = resolved.artifact.endsWith("\n") ? resolved.artifact : `${resolved.artifact}\n`;
|
|
945
1877
|
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
946
1878
|
|
|
1879
|
+
// Read the ledger once before persistence. The dedupe guard and both gates
|
|
1880
|
+
// consume this same snapshot so no additional ledger read can slip between gate
|
|
1881
|
+
// evaluation and persistence.
|
|
1882
|
+
const indexLoad = await loadRalplanIndexForCap(cwd, resolved.sessionId, resolved.runId);
|
|
1883
|
+
|
|
947
1884
|
// Duplicate-write guard: a second `--write` for the same (stage, stage_n) must not
|
|
948
1885
|
// silently clobber the artifact or append a duplicate ledger row. Classify before any
|
|
949
1886
|
// state mutation so a conflict never regresses run-state phase.
|
|
950
|
-
const existingArtifact =
|
|
951
|
-
cwd,
|
|
952
|
-
resolved.sessionId,
|
|
953
|
-
resolved.runId,
|
|
954
|
-
resolved.stage,
|
|
955
|
-
resolved.stageN,
|
|
956
|
-
);
|
|
1887
|
+
const existingArtifact = findExistingStageArtifact(indexLoad.rawText, resolved.stage, resolved.stageN);
|
|
957
1888
|
if (existingArtifact) {
|
|
958
1889
|
if (existingArtifact.sha256 !== sha256) {
|
|
959
1890
|
throw new RalplanCommandError(
|
|
@@ -961,7 +1892,48 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
961
1892
|
`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.`,
|
|
962
1893
|
);
|
|
963
1894
|
}
|
|
964
|
-
|
|
1895
|
+
if (resolved.stage === "final") await ensureFinalPendingApproval(cwd, resolved, existingArtifact);
|
|
1896
|
+
return await buildDeduplicatedResult(resolved, existingArtifact, sha256, cwd, repositoryBinding);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// `persistArtifact` writes the artifact before its ledger row. If a process crashed
|
|
1900
|
+
// in that gap, an identical retry repairs the row and remains a normal deduplicated
|
|
1901
|
+
// receipt; a differing retry retains the ordinary no-overwrite refusal.
|
|
1902
|
+
const onDiskArtifact = await findOnDiskStageArtifact(cwd, resolved);
|
|
1903
|
+
if (onDiskArtifact) {
|
|
1904
|
+
if (onDiskArtifact.sha256 !== sha256) {
|
|
1905
|
+
throw new RalplanCommandError(
|
|
1906
|
+
2,
|
|
1907
|
+
`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.`,
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
if (resolved.stage === "final") await ensureFinalPendingApproval(cwd, resolved, onDiskArtifact);
|
|
1911
|
+
const repairedArtifact = await repairMissingStageArtifactLedger(
|
|
1912
|
+
cwd,
|
|
1913
|
+
resolved,
|
|
1914
|
+
onDiskArtifact,
|
|
1915
|
+
resolved.stage === "final" ? unavailableRalplanFinalAdmission() : undefined,
|
|
1916
|
+
);
|
|
1917
|
+
let appliedPersistedRoleState: PersistedRoleStateUpdate | undefined;
|
|
1918
|
+
if (
|
|
1919
|
+
persistedRoleState &&
|
|
1920
|
+
(await applyPersistedRoleStateUpdate(cwd, resolved.sessionId, persistedRoleState, resolved.runId))
|
|
1921
|
+
) {
|
|
1922
|
+
appliedPersistedRoleState = persistedRoleState;
|
|
1923
|
+
}
|
|
1924
|
+
let appliedLaneVerdict: LaneVerdictUpdate | undefined;
|
|
1925
|
+
if (laneVerdict && (await applyLaneVerdictUpdate(cwd, resolved.sessionId, laneVerdict, resolved.runId))) {
|
|
1926
|
+
appliedLaneVerdict = laneVerdict;
|
|
1927
|
+
}
|
|
1928
|
+
return await buildDeduplicatedResult(
|
|
1929
|
+
resolved,
|
|
1930
|
+
repairedArtifact,
|
|
1931
|
+
sha256,
|
|
1932
|
+
cwd,
|
|
1933
|
+
repositoryBinding,
|
|
1934
|
+
appliedLaneVerdict,
|
|
1935
|
+
appliedPersistedRoleState,
|
|
1936
|
+
);
|
|
965
1937
|
}
|
|
966
1938
|
|
|
967
1939
|
// Consensus iteration budget (#3165): refuse new planner/revision openers past the cap.
|
|
@@ -969,31 +1941,72 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
969
1941
|
// critic, final, …) remain allowed so operators can escalate without auto-implementation.
|
|
970
1942
|
// On-disk opener artifacts floor the count so a wiped/truncated/malformed index.jsonl cannot
|
|
971
1943
|
// under-count and fail open after prior planner/revision writes.
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1944
|
+
//
|
|
1945
|
+
// Gate evaluation, artifact write, and ledger append are sequential within one
|
|
1946
|
+
// `gjc ralplan --write` invocation. This is NOT exclusive across processes: no
|
|
1947
|
+
// run-scoped lock or CAS admission exists, intentionally matching #3165's
|
|
1948
|
+
// check-then-persist exposure.
|
|
1949
|
+
const [onDiskOpeners, onDiskLaneArtifacts, iterationLimit, laneLimit] = await Promise.all([
|
|
1950
|
+
countRalplanOnDiskOpeners(cwd, resolved.sessionId, resolved.runId),
|
|
1951
|
+
countRalplanOnDiskLaneArtifacts(cwd, resolved.sessionId, resolved.runId),
|
|
1952
|
+
resolveRalplanMaxIterations(cwd),
|
|
1953
|
+
resolveRalplanMaxReviewPassesPerLane(cwd),
|
|
1954
|
+
]);
|
|
975
1955
|
const capDecision = evaluateRalplanIterationCap({
|
|
976
1956
|
rows: indexLoad.rows,
|
|
977
1957
|
stage: resolved.stage,
|
|
978
|
-
maxIterations,
|
|
1958
|
+
maxIterations: iterationLimit.maxIterations,
|
|
979
1959
|
iterationFloor: onDiskOpeners,
|
|
980
1960
|
});
|
|
981
1961
|
if (!capDecision.allowed) {
|
|
1962
|
+
await recordRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId, capDecision.reason);
|
|
982
1963
|
return buildPlanningStuckResult({
|
|
983
1964
|
json: resolved.json,
|
|
984
1965
|
stage: resolved.stage,
|
|
985
1966
|
stageN: resolved.stageN,
|
|
986
1967
|
runId: resolved.runId,
|
|
987
1968
|
decision: capDecision,
|
|
988
|
-
source:
|
|
1969
|
+
source: iterationLimit.source,
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
const laneBudgetDecision = evaluateRalplanReviewLaneBudget({
|
|
1973
|
+
rows: indexLoad.rows,
|
|
1974
|
+
stage: resolved.stage,
|
|
1975
|
+
maxReviewPassesPerLane: laneLimit.maxReviewPassesPerLane,
|
|
1976
|
+
onDiskLaneCounts: onDiskLaneArtifacts,
|
|
1977
|
+
});
|
|
1978
|
+
if (!laneBudgetDecision.allowed) {
|
|
1979
|
+
await recordRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId, laneBudgetDecision.reason);
|
|
1980
|
+
return buildLaneBudgetStuckResult({
|
|
1981
|
+
json: resolved.json,
|
|
1982
|
+
stage: resolved.stage,
|
|
1983
|
+
stageN: resolved.stageN,
|
|
1984
|
+
runId: resolved.runId,
|
|
1985
|
+
decision: laneBudgetDecision,
|
|
1986
|
+
source: laneLimit.source,
|
|
989
1987
|
});
|
|
990
1988
|
}
|
|
991
1989
|
|
|
1990
|
+
let autoHandoff: RalplanAutoHandoffResolution | undefined;
|
|
1991
|
+
if (resolved.stage === "final") {
|
|
1992
|
+
// Resolve and validate the configured admission before any final artifact or
|
|
1993
|
+
// state write. The ledger row below is the durable receipt for deduplicated
|
|
1994
|
+
// retries; state is only a current-session projection.
|
|
1995
|
+
autoHandoff = await resolveRalplanAutoHandoff(cwd, {
|
|
1996
|
+
planningStuck: await readRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId),
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
992
1999
|
// Keep run-state `current_phase` coherent with the stage being persisted.
|
|
993
2000
|
await persistActiveRunId(cwd, resolved.sessionId, resolved.runId, resolved.stage);
|
|
994
|
-
const persisted = await persistArtifact(resolved, cwd, content, sha256);
|
|
995
|
-
if (
|
|
996
|
-
await
|
|
2001
|
+
const persisted = await persistArtifact(resolved, cwd, content, sha256, autoHandoff);
|
|
2002
|
+
if (persistedRoleState) {
|
|
2003
|
+
await applyPersistedRoleStateUpdate(cwd, resolved.sessionId, persistedRoleState);
|
|
2004
|
+
}
|
|
2005
|
+
if (laneVerdict) {
|
|
2006
|
+
await applyLaneVerdictUpdate(cwd, resolved.sessionId, laneVerdict);
|
|
2007
|
+
}
|
|
2008
|
+
if (autoHandoff) {
|
|
2009
|
+
await persistRalplanFinalAdmission(cwd, resolved.sessionId, resolved.runId, autoHandoff);
|
|
997
2010
|
}
|
|
998
2011
|
await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "ralplan-runtime", path: persisted.path });
|
|
999
2012
|
await syncRalplanHud({
|
|
@@ -1003,9 +2016,20 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1003
2016
|
runId: persisted.runId,
|
|
1004
2017
|
pendingApproval: persisted.stage === "final",
|
|
1005
2018
|
iteration: persisted.stageN,
|
|
2019
|
+
reviewPassBudget: laneLimit.maxReviewPassesPerLane,
|
|
1006
2020
|
latestSummary: `persisted ${persisted.stage} stage ${persisted.stageN}`,
|
|
1007
2021
|
});
|
|
2022
|
+
const reviewBudgetWarning =
|
|
2023
|
+
laneBudgetDecision.lane && laneBudgetDecision.finalSlot && laneBudgetDecision.maxReviewPassesPerLane > 1
|
|
2024
|
+
? {
|
|
2025
|
+
lane: laneBudgetDecision.lane,
|
|
2026
|
+
passes: laneBudgetDecision.projectedPasses,
|
|
2027
|
+
max: laneBudgetDecision.maxReviewPassesPerLane,
|
|
2028
|
+
}
|
|
2029
|
+
: undefined;
|
|
2030
|
+
|
|
1008
2031
|
const payload: Record<string, unknown> = {
|
|
2032
|
+
session_id: resolved.sessionId,
|
|
1009
2033
|
run_id: persisted.runId,
|
|
1010
2034
|
path: persisted.path,
|
|
1011
2035
|
stage: persisted.stage,
|
|
@@ -1015,26 +2039,40 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1015
2039
|
created_at: persisted.createdAt,
|
|
1016
2040
|
};
|
|
1017
2041
|
if (persisted.pendingApprovalPath) payload.pending_approval_path = persisted.pendingApprovalPath;
|
|
1018
|
-
if (
|
|
2042
|
+
if (persistedRoleState) payload[`${persistedRoleState.role}_state`] = persistedRoleStatePayload(persistedRoleState);
|
|
2043
|
+
if (reviewBudgetWarning) payload.review_budget_warning = reviewBudgetWarning;
|
|
2044
|
+
if (laneVerdict) payload.lane_verdict = { lane: laneVerdict.lane, verdict: laneVerdict.verdict };
|
|
2045
|
+
if (autoHandoff) payload.auto_handoff = autoHandoff;
|
|
2046
|
+
|
|
1019
2047
|
const stdout = resolved.json
|
|
1020
2048
|
? `${JSON.stringify(payload, null, 2)}\n`
|
|
1021
|
-
: `Persisted ralplan ${persisted.stage} stage ${persisted.stageN} at ${persisted.path}.\n`;
|
|
2049
|
+
: `${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`;
|
|
1022
2050
|
return { status: 0, stdout };
|
|
1023
2051
|
}
|
|
1024
2052
|
|
|
1025
2053
|
/**
|
|
1026
|
-
* Deterministic
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
2054
|
+
* Deterministic receipt for an identical repeated `--write`. Ledger-backed duplicates
|
|
2055
|
+
* do not rewrite artifacts, append rows, or churn run state; a crash-gap repair may
|
|
2056
|
+
* complete riding persisted role/lane metadata before returning this receipt.
|
|
1029
2057
|
*/
|
|
1030
|
-
function
|
|
2058
|
+
function applyRalplanPlanningStuckOverride(
|
|
2059
|
+
admission: RalplanAutoHandoffResolution,
|
|
2060
|
+
planningStuck: boolean,
|
|
2061
|
+
): RalplanAutoHandoffResolution {
|
|
2062
|
+
return planningStuck ? { ...admission, effectiveTarget: "off", degradationReason: "planning_stuck" } : admission;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
async function buildDeduplicatedResult(
|
|
1031
2066
|
resolved: ResolvedArtifactArgs,
|
|
1032
2067
|
existing: ExistingStageArtifact,
|
|
1033
2068
|
sha256: string,
|
|
1034
2069
|
cwd: string,
|
|
1035
2070
|
repositoryBinding: RepositoryBinding,
|
|
1036
|
-
|
|
2071
|
+
laneVerdict?: LaneVerdictUpdate,
|
|
2072
|
+
persistedRoleState?: PersistedRoleStateUpdate,
|
|
2073
|
+
): Promise<RalplanCommandResult> {
|
|
1037
2074
|
const payload: Record<string, unknown> = {
|
|
2075
|
+
session_id: resolved.sessionId,
|
|
1038
2076
|
run_id: resolved.runId,
|
|
1039
2077
|
path: existing.path,
|
|
1040
2078
|
stage: resolved.stage,
|
|
@@ -1044,6 +2082,10 @@ function buildDeduplicatedResult(
|
|
|
1044
2082
|
created_at: existing.createdAt,
|
|
1045
2083
|
deduplicated: true,
|
|
1046
2084
|
};
|
|
2085
|
+
if (laneVerdict) payload.lane_verdict = { lane: laneVerdict.lane, verdict: laneVerdict.verdict };
|
|
2086
|
+
if (persistedRoleState) {
|
|
2087
|
+
payload[`${persistedRoleState.role}_state`] = persistedRoleStatePayload(persistedRoleState);
|
|
2088
|
+
}
|
|
1047
2089
|
if (resolved.stage === "final") {
|
|
1048
2090
|
payload.pending_approval_path = path.join(
|
|
1049
2091
|
sessionPlansDir(cwd, resolved.sessionId),
|
|
@@ -1051,6 +2093,11 @@ function buildDeduplicatedResult(
|
|
|
1051
2093
|
resolved.runId,
|
|
1052
2094
|
"pending-approval.md",
|
|
1053
2095
|
);
|
|
2096
|
+
const planningStuck = await readRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId);
|
|
2097
|
+
payload.auto_handoff = applyRalplanPlanningStuckOverride(
|
|
2098
|
+
existing.autoHandoff ?? unavailableRalplanFinalAdmission(),
|
|
2099
|
+
planningStuck,
|
|
2100
|
+
);
|
|
1054
2101
|
}
|
|
1055
2102
|
const stdout = resolved.json
|
|
1056
2103
|
? `${JSON.stringify(payload, null, 2)}\n`
|
|
@@ -1092,6 +2139,9 @@ function extractPositionalTask(args: readonly string[]): string {
|
|
|
1092
2139
|
}
|
|
1093
2140
|
|
|
1094
2141
|
function resolveConsensusArgs(args: readonly string[], cwd: string): ConsensusHandoffArgs {
|
|
2142
|
+
if (hasFlag(args, "--lane-verdict")) {
|
|
2143
|
+
throw new RalplanCommandError(2, "--lane-verdict is only supported with gjc ralplan --write.");
|
|
2144
|
+
}
|
|
1095
2145
|
const architectKind = flagValue(args, "--architect")?.trim() || undefined;
|
|
1096
2146
|
if (architectKind && !KNOWN_ARCHITECT_KINDS.has(architectKind)) {
|
|
1097
2147
|
throw new RalplanCommandError(
|
|
@@ -1194,6 +2244,7 @@ async function handleConsensusHandoff(args: readonly string[], cwd: string): Pro
|
|
|
1194
2244
|
});
|
|
1195
2245
|
|
|
1196
2246
|
const summary = {
|
|
2247
|
+
session_id: resolved.sessionId,
|
|
1197
2248
|
skill: "ralplan",
|
|
1198
2249
|
mode,
|
|
1199
2250
|
state_path: statePath,
|