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