@gajae-code/coding-agent 0.11.11 → 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 +49 -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 +29 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +22 -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/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -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 +8 -0
- 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/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/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/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +15 -1
- 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/reconciliation-store.d.ts +65 -0
- 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/session/agent-session.d.ts +44 -10
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +25 -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/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 +4 -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 +2 -0
- 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/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 +1 -1
- 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 +23 -1
- package/src/config/settings.ts +246 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/types.ts +28 -4
- 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/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +742 -85
- 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 +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 +8 -9
- 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 +43 -7
- 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/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/oauth-selector.ts +42 -12
- 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/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +19 -2
- package/src/modes/controllers/input-controller.ts +18 -3
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +62 -3
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +19 -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/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/broker/lifecycle.ts +25 -6
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +341 -38
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/reconciliation-store.ts +224 -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/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +29 -0
- package/src/sdk/protocol/operation-registry.ts +7 -2
- package/src/sdk/session.ts +13 -1
- package/src/session/agent-session.ts +466 -50
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +764 -17
- 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/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 +182 -18
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -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,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
|
-
import * as os from "node:os";
|
|
4
3
|
import * as path from "node:path";
|
|
4
|
+
import { getConfigRootDir } from "@gajae-code/utils";
|
|
5
5
|
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
6
6
|
import { buildRalplanHudSummary } from "../skill-state/workflow-hud";
|
|
7
7
|
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
@@ -50,7 +50,7 @@ import { getSkillManifest } from "./workflow-manifest";
|
|
|
50
50
|
*
|
|
51
51
|
* 2. **Artifact write**: `gjc ralplan --write --stage <type> --stage_n <N>
|
|
52
52
|
* (--artifact <path-or-string> | --artifact-env GJC_RALPLAN_ARTIFACT)
|
|
53
|
-
* [--run-id <id>] [--session-id <id>] [--json]` persists Planner / Architect
|
|
53
|
+
* [--run-id <id>] [--session-id <id>] [--lane-verdict <token>] [--json]` persists Planner / Architect
|
|
54
54
|
* / Critic / revision / post-interview / ADR / final markdown under `.gjc/plans/ralplan/<run-id>/`, maintains
|
|
55
55
|
* an `index.jsonl` audit log, copies `final` stages to `pending-approval.md`, and advances
|
|
56
56
|
* the HUD chip to reflect the latest persisted stage.
|
|
@@ -70,9 +70,25 @@ export const RALPLAN_DEFAULT_MAX_ITERATIONS = 5;
|
|
|
70
70
|
export const RALPLAN_MAX_ITERATIONS_LIMIT = 20;
|
|
71
71
|
/** Operator-visible stuck signal for headless/CI orchestration (#3165). */
|
|
72
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;
|
|
73
77
|
|
|
74
78
|
const RALPLAN_ITERATION_OPENER_STAGES = new Set<RalplanStage>(["planner", "revision"]);
|
|
75
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
|
+
|
|
76
92
|
export type RalplanIterationCapDecision =
|
|
77
93
|
| {
|
|
78
94
|
allowed: true;
|
|
@@ -147,8 +163,106 @@ export function evaluateRalplanIterationCap(input: {
|
|
|
147
163
|
};
|
|
148
164
|
}
|
|
149
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
|
+
|
|
150
263
|
/** Filename pattern for persisted planner/revision stage artifacts. */
|
|
151
264
|
const OPENER_ARTIFACT_RE = /^stage-\d{2,}-(planner|revision)\.md$/;
|
|
265
|
+
const LANE_ARTIFACT_RE = /^stage-\d{2,}-(architect|critic)\.md$/;
|
|
152
266
|
|
|
153
267
|
/**
|
|
154
268
|
* Count on-disk planner/revision stage artifacts for a run. Used as a floor when
|
|
@@ -168,6 +282,30 @@ export async function countRalplanOnDiskOpeners(cwd: string, sessionId: string,
|
|
|
168
282
|
}
|
|
169
283
|
}
|
|
170
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
|
+
|
|
171
309
|
/**
|
|
172
310
|
* Load index rows for cap enforcement. Unlike HUD reads, returns structural
|
|
173
311
|
* signals so callers can fail closed when the ledger is empty/malformed while
|
|
@@ -177,7 +315,13 @@ export async function loadRalplanIndexForCap(
|
|
|
177
315
|
cwd: string,
|
|
178
316
|
sessionId: string,
|
|
179
317
|
runId: string,
|
|
180
|
-
): Promise<{
|
|
318
|
+
): Promise<{
|
|
319
|
+
rows: RalplanIndexRow[];
|
|
320
|
+
indexPresent: boolean;
|
|
321
|
+
parseableLines: number;
|
|
322
|
+
rawLineCount: number;
|
|
323
|
+
rawText?: string;
|
|
324
|
+
}> {
|
|
181
325
|
const indexPath = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId, "index.jsonl");
|
|
182
326
|
try {
|
|
183
327
|
const text = await fs.readFile(indexPath, "utf8");
|
|
@@ -192,6 +336,7 @@ export async function loadRalplanIndexForCap(
|
|
|
192
336
|
indexPresent: true,
|
|
193
337
|
parseableLines: rows.length,
|
|
194
338
|
rawLineCount: lines.length,
|
|
339
|
+
rawText: text,
|
|
195
340
|
};
|
|
196
341
|
} catch (error) {
|
|
197
342
|
const code =
|
|
@@ -204,16 +349,16 @@ export async function loadRalplanIndexForCap(
|
|
|
204
349
|
}
|
|
205
350
|
}
|
|
206
351
|
|
|
207
|
-
function
|
|
208
|
-
return typeof value === "number" &&
|
|
209
|
-
Number.isFinite(value) &&
|
|
210
|
-
Number.isInteger(value) &&
|
|
211
|
-
value >= 1 &&
|
|
212
|
-
value <= RALPLAN_MAX_ITERATIONS_LIMIT
|
|
352
|
+
function parseBoundedPositiveInteger(value: unknown, limit: number): number | null {
|
|
353
|
+
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 1 && value <= limit
|
|
213
354
|
? value
|
|
214
355
|
: null;
|
|
215
356
|
}
|
|
216
357
|
|
|
358
|
+
function parseMaxIterationsValue(value: unknown): number | null {
|
|
359
|
+
return parseBoundedPositiveInteger(value, RALPLAN_MAX_ITERATIONS_LIMIT);
|
|
360
|
+
}
|
|
361
|
+
|
|
217
362
|
async function readSettingsMaxIterations(settingsPath: string): Promise<number | null> {
|
|
218
363
|
try {
|
|
219
364
|
const raw = await Bun.file(settingsPath).text();
|
|
@@ -241,13 +386,90 @@ export async function resolveRalplanMaxIterations(cwd: string): Promise<{ maxIte
|
|
|
241
386
|
const projectPath = path.join(gjcRoot(cwd), "settings.json");
|
|
242
387
|
const project = await readSettingsMaxIterations(projectPath);
|
|
243
388
|
if (project !== null) return { maxIterations: project, source: projectPath };
|
|
244
|
-
const
|
|
245
|
-
const userPath = path.join(userDir, "settings.json");
|
|
389
|
+
const userPath = path.join(getConfigRootDir(), "settings.json");
|
|
246
390
|
const user = await readSettingsMaxIterations(userPath);
|
|
247
391
|
if (user !== null) return { maxIterations: user, source: userPath };
|
|
248
392
|
return { maxIterations: RALPLAN_DEFAULT_MAX_ITERATIONS, source: "default" };
|
|
249
393
|
}
|
|
250
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
|
+
|
|
251
473
|
function buildPlanningStuckResult(input: {
|
|
252
474
|
json: boolean;
|
|
253
475
|
stage: RalplanStage;
|
|
@@ -290,6 +512,51 @@ function buildPlanningStuckResult(input: {
|
|
|
290
512
|
};
|
|
291
513
|
}
|
|
292
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
|
+
}
|
|
559
|
+
|
|
293
560
|
const KNOWN_ARCHITECT_KINDS = new Set(["openai-code"]);
|
|
294
561
|
const KNOWN_CRITIC_KINDS = new Set(["openai-code"]);
|
|
295
562
|
|
|
@@ -322,10 +589,15 @@ const VALUE_FLAGS = new Set([
|
|
|
322
589
|
"--critic",
|
|
323
590
|
"--planner-id",
|
|
324
591
|
"--planner-resumable",
|
|
592
|
+
"--architect-id",
|
|
593
|
+
"--architect-resumable",
|
|
594
|
+
"--critic-id",
|
|
595
|
+
"--critic-resumable",
|
|
325
596
|
"--fallback-reason",
|
|
326
597
|
"--fallback-attempted-id",
|
|
327
598
|
"--fallback-stage-n",
|
|
328
599
|
"--fallback-receipt-path",
|
|
600
|
+
"--lane-verdict",
|
|
329
601
|
]);
|
|
330
602
|
|
|
331
603
|
export function isRalplanArtifactWriteInvocation(args: readonly string[]): boolean {
|
|
@@ -485,6 +757,13 @@ async function persistActiveRunId(cwd: string, sessionId: string, runId: string,
|
|
|
485
757
|
// "complete"/"handoff" and disarm the Stop hook). PHASE_LOCK only guards same-run writes.
|
|
486
758
|
const isNewRun = existing.run_id !== runId;
|
|
487
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
|
+
}
|
|
488
767
|
if (
|
|
489
768
|
existing.run_id === runId &&
|
|
490
769
|
existing.version === WORKFLOW_STATE_VERSION &&
|
|
@@ -503,6 +782,7 @@ async function persistActiveRunId(cwd: string, sessionId: string, runId: string,
|
|
|
503
782
|
existing.updated_at = new Date().toISOString();
|
|
504
783
|
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
505
784
|
cwd,
|
|
785
|
+
lockHeld: true,
|
|
506
786
|
receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan persist-run-id", sessionId },
|
|
507
787
|
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
|
|
508
788
|
});
|
|
@@ -511,9 +791,12 @@ async function persistActiveRunId(cwd: string, sessionId: string, runId: string,
|
|
|
511
791
|
);
|
|
512
792
|
}
|
|
513
793
|
|
|
514
|
-
/*
|
|
794
|
+
/* ---------------------- persisted role-agent run-state --------------------- */
|
|
795
|
+
|
|
796
|
+
type PersistedRole = "planner" | RalplanReviewLane;
|
|
515
797
|
|
|
516
|
-
interface
|
|
798
|
+
interface PersistedRoleStateUpdate {
|
|
799
|
+
role: PersistedRole;
|
|
517
800
|
subagentId?: string;
|
|
518
801
|
resumable?: boolean;
|
|
519
802
|
fallbackReason?: string;
|
|
@@ -522,6 +805,25 @@ interface PlannerStateUpdate {
|
|
|
522
805
|
fallbackReceiptPath?: string;
|
|
523
806
|
}
|
|
524
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
|
+
|
|
525
827
|
function parseBooleanFlag(raw: string, flag: string): boolean {
|
|
526
828
|
if (raw === "true") return true;
|
|
527
829
|
if (raw === "false") return false;
|
|
@@ -534,7 +836,7 @@ function assertSubagentId(value: string, label: string): void {
|
|
|
534
836
|
}
|
|
535
837
|
}
|
|
536
838
|
|
|
537
|
-
function
|
|
839
|
+
function roleStateFlagValue(args: readonly string[], flag: string): string | undefined {
|
|
538
840
|
const value = flagValue(args, flag);
|
|
539
841
|
if (value === undefined && hasFlag(args, flag)) {
|
|
540
842
|
throw new RalplanCommandError(2, `missing value for ${flag}.`);
|
|
@@ -542,19 +844,64 @@ function plannerFlagValue(args: readonly string[], flag: string): string | undef
|
|
|
542
844
|
return value;
|
|
543
845
|
}
|
|
544
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
|
+
|
|
545
860
|
/**
|
|
546
|
-
* Parse the optional persisted-
|
|
547
|
-
* `--write`.
|
|
548
|
-
*
|
|
549
|
-
*
|
|
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.
|
|
550
865
|
*/
|
|
551
|
-
function
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
const
|
|
557
|
-
|
|
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");
|
|
558
905
|
|
|
559
906
|
const anyPresent = [
|
|
560
907
|
subagentId,
|
|
@@ -566,14 +913,13 @@ function parsePlannerStateArgs(args: readonly string[]): PlannerStateUpdate | un
|
|
|
566
913
|
].some(value => value !== undefined);
|
|
567
914
|
if (!anyPresent) return undefined;
|
|
568
915
|
|
|
569
|
-
const update:
|
|
570
|
-
|
|
916
|
+
const update: PersistedRoleStateUpdate = { role };
|
|
571
917
|
if (subagentId !== undefined) {
|
|
572
|
-
assertSubagentId(subagentId,
|
|
918
|
+
assertSubagentId(subagentId, flags.id);
|
|
573
919
|
update.subagentId = subagentId;
|
|
574
920
|
}
|
|
575
921
|
if (resumableRaw !== undefined) {
|
|
576
|
-
update.resumable = parseBooleanFlag(resumableRaw,
|
|
922
|
+
update.resumable = parseBooleanFlag(resumableRaw, flags.resumable);
|
|
577
923
|
}
|
|
578
924
|
|
|
579
925
|
const anyFallback = [fallbackReason, fallbackAttemptedId, fallbackStageNRaw, fallbackReceiptPath].some(
|
|
@@ -581,7 +927,7 @@ function parsePlannerStateArgs(args: readonly string[]): PlannerStateUpdate | un
|
|
|
581
927
|
);
|
|
582
928
|
if (anyFallback) {
|
|
583
929
|
if (!fallbackReason) {
|
|
584
|
-
throw new RalplanCommandError(2,
|
|
930
|
+
throw new RalplanCommandError(2, `--fallback-reason is required when recording ${role} fallback metadata.`);
|
|
585
931
|
}
|
|
586
932
|
if (!KNOWN_FALLBACK_REASONS.has(fallbackReason)) {
|
|
587
933
|
throw new RalplanCommandError(
|
|
@@ -593,13 +939,13 @@ function parsePlannerStateArgs(args: readonly string[]): PlannerStateUpdate | un
|
|
|
593
939
|
if (fallbackAttemptedId === undefined) {
|
|
594
940
|
throw new RalplanCommandError(
|
|
595
941
|
2,
|
|
596
|
-
|
|
942
|
+
`--fallback-attempted-id is required when recording ${role} fallback metadata.`,
|
|
597
943
|
);
|
|
598
944
|
}
|
|
599
945
|
assertSubagentId(fallbackAttemptedId, "--fallback-attempted-id");
|
|
600
946
|
update.fallbackAttemptedId = fallbackAttemptedId;
|
|
601
947
|
if (fallbackStageNRaw === undefined) {
|
|
602
|
-
throw new RalplanCommandError(2,
|
|
948
|
+
throw new RalplanCommandError(2, `--fallback-stage-n is required when recording ${role} fallback metadata.`);
|
|
603
949
|
}
|
|
604
950
|
update.fallbackStageN = parseStageN(fallbackStageNRaw);
|
|
605
951
|
if (fallbackReceiptPath !== undefined) {
|
|
@@ -613,24 +959,63 @@ function parsePlannerStateArgs(args: readonly string[]): PlannerStateUpdate | un
|
|
|
613
959
|
return update;
|
|
614
960
|
}
|
|
615
961
|
|
|
616
|
-
/**
|
|
617
|
-
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> {
|
|
618
992
|
const payload: Record<string, unknown> = {};
|
|
619
|
-
|
|
620
|
-
if (update.
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if (update.
|
|
624
|
-
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
|
+
}
|
|
625
1005
|
return payload;
|
|
626
1006
|
}
|
|
627
1007
|
|
|
628
1008
|
/**
|
|
629
|
-
* Merge persisted-
|
|
630
|
-
* audit/routing hint only
|
|
631
|
-
*
|
|
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.
|
|
632
1012
|
*/
|
|
633
|
-
async function
|
|
1013
|
+
async function applyPersistedRoleStateUpdate(
|
|
1014
|
+
cwd: string,
|
|
1015
|
+
sessionId: string,
|
|
1016
|
+
update: PersistedRoleStateUpdate,
|
|
1017
|
+
expectedActiveRunId?: string,
|
|
1018
|
+
): Promise<boolean> {
|
|
634
1019
|
const statePath = ralplanStatePath(cwd, sessionId);
|
|
635
1020
|
return await withWorkflowStateLock(
|
|
636
1021
|
statePath,
|
|
@@ -643,7 +1028,8 @@ async function applyPlannerStateUpdate(cwd: string, sessionId: string, update: P
|
|
|
643
1028
|
);
|
|
644
1029
|
}
|
|
645
1030
|
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
646
|
-
|
|
1031
|
+
if (expectedActiveRunId !== undefined && existing.run_id !== expectedActiveRunId) return false;
|
|
1032
|
+
Object.assign(existing, persistedRoleStatePayload(update));
|
|
647
1033
|
if (typeof existing.skill !== "string") existing.skill = "ralplan";
|
|
648
1034
|
if (typeof existing.active !== "boolean") existing.active = true;
|
|
649
1035
|
if (typeof existing.current_phase !== "string") existing.current_phase = "planner";
|
|
@@ -651,9 +1037,59 @@ async function applyPlannerStateUpdate(cwd: string, sessionId: string, update: P
|
|
|
651
1037
|
existing.updated_at = new Date().toISOString();
|
|
652
1038
|
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
653
1039
|
cwd,
|
|
654
|
-
|
|
1040
|
+
lockHeld: true,
|
|
1041
|
+
receipt: {
|
|
1042
|
+
cwd,
|
|
1043
|
+
skill: "ralplan",
|
|
1044
|
+
owner: "gjc-runtime",
|
|
1045
|
+
command: `gjc ralplan ${update.role}-state`,
|
|
1046
|
+
sessionId,
|
|
1047
|
+
},
|
|
655
1048
|
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
|
|
656
1049
|
});
|
|
1050
|
+
return true;
|
|
1051
|
+
},
|
|
1052
|
+
{ cwd },
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/** Merge a lane's self-reported verdict into the same-session ralplan run state, optionally only for its active run. */
|
|
1057
|
+
async function applyLaneVerdictUpdate(
|
|
1058
|
+
cwd: string,
|
|
1059
|
+
sessionId: string,
|
|
1060
|
+
update: LaneVerdictUpdate,
|
|
1061
|
+
expectedActiveRunId?: string,
|
|
1062
|
+
): Promise<boolean> {
|
|
1063
|
+
const statePath = ralplanStatePath(cwd, sessionId);
|
|
1064
|
+
return await withWorkflowStateLock(
|
|
1065
|
+
statePath,
|
|
1066
|
+
async () => {
|
|
1067
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
1068
|
+
if (existingRead.kind === "corrupt") {
|
|
1069
|
+
throw new RalplanCommandError(
|
|
1070
|
+
2,
|
|
1071
|
+
`existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to overwrite ${statePath}`,
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1075
|
+
if (expectedActiveRunId !== undefined && existing.run_id !== expectedActiveRunId) return false;
|
|
1076
|
+
Object.assign(existing, {
|
|
1077
|
+
last_review_verdict: update.verdict,
|
|
1078
|
+
last_review_verdict_lane: update.lane,
|
|
1079
|
+
last_review_verdict_stage_n: update.stageN,
|
|
1080
|
+
});
|
|
1081
|
+
if (typeof existing.skill !== "string") existing.skill = "ralplan";
|
|
1082
|
+
if (typeof existing.active !== "boolean") existing.active = true;
|
|
1083
|
+
if (typeof existing.current_phase !== "string") existing.current_phase = "planner";
|
|
1084
|
+
existing = migrateWorkflowState(existing, "ralplan").state;
|
|
1085
|
+
existing.updated_at = new Date().toISOString();
|
|
1086
|
+
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
1087
|
+
cwd,
|
|
1088
|
+
lockHeld: true,
|
|
1089
|
+
receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan lane-verdict", sessionId },
|
|
1090
|
+
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
|
|
1091
|
+
});
|
|
1092
|
+
return true;
|
|
657
1093
|
},
|
|
658
1094
|
{ cwd },
|
|
659
1095
|
);
|
|
@@ -804,28 +1240,19 @@ interface ExistingStageArtifact {
|
|
|
804
1240
|
}
|
|
805
1241
|
|
|
806
1242
|
/**
|
|
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.
|
|
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.
|
|
812
1247
|
*/
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
sessionId: string,
|
|
816
|
-
runId: string,
|
|
1248
|
+
function findExistingStageArtifact(
|
|
1249
|
+
indexText: string | undefined,
|
|
817
1250
|
stage: RalplanStage,
|
|
818
1251
|
stageN: number,
|
|
819
|
-
):
|
|
820
|
-
|
|
821
|
-
let text: string;
|
|
822
|
-
try {
|
|
823
|
-
text = await fs.readFile(indexPath, "utf8");
|
|
824
|
-
} catch {
|
|
825
|
-
return undefined;
|
|
826
|
-
}
|
|
1252
|
+
): ExistingStageArtifact | undefined {
|
|
1253
|
+
if (indexText === undefined) return undefined;
|
|
827
1254
|
let match: ExistingStageArtifact | undefined;
|
|
828
|
-
for (const line of
|
|
1255
|
+
for (const line of indexText.split(/\r?\n/)) {
|
|
829
1256
|
const trimmed = line.trim();
|
|
830
1257
|
if (!trimmed) continue;
|
|
831
1258
|
let row: unknown;
|
|
@@ -847,6 +1274,124 @@ async function findExistingStageArtifact(
|
|
|
847
1274
|
return match;
|
|
848
1275
|
}
|
|
849
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
|
+
|
|
850
1395
|
/**
|
|
851
1396
|
* Read and parse the run's `index.jsonl` rows. Best-effort: returns [] when the
|
|
852
1397
|
* file is absent or unreadable so HUD sync never fails on a missing index.
|
|
@@ -866,6 +1411,18 @@ async function readRalplanIndexRows(cwd: string, sessionId: string, runId: strin
|
|
|
866
1411
|
}
|
|
867
1412
|
}
|
|
868
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
|
+
|
|
869
1426
|
async function syncRalplanHud(options: {
|
|
870
1427
|
cwd: string;
|
|
871
1428
|
sessionId: string;
|
|
@@ -873,6 +1430,7 @@ async function syncRalplanHud(options: {
|
|
|
873
1430
|
pendingApproval: boolean;
|
|
874
1431
|
iteration?: number;
|
|
875
1432
|
runId?: string;
|
|
1433
|
+
reviewPassBudget?: number;
|
|
876
1434
|
latestSummary?: string;
|
|
877
1435
|
}): Promise<void> {
|
|
878
1436
|
try {
|
|
@@ -898,15 +1456,25 @@ async function buildRalplanHud(options: {
|
|
|
898
1456
|
latestSummary?: string;
|
|
899
1457
|
runId?: string;
|
|
900
1458
|
sessionId?: string;
|
|
1459
|
+
reviewPassBudget?: number;
|
|
901
1460
|
}) {
|
|
902
1461
|
let iterationFromIndex: number | undefined;
|
|
903
1462
|
let stages: string | undefined;
|
|
1463
|
+
let architectPasses: number | undefined;
|
|
1464
|
+
let criticPasses: number | undefined;
|
|
1465
|
+
let verdict: string | undefined;
|
|
904
1466
|
if (options.runId && options.sessionId) {
|
|
905
|
-
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;
|
|
906
1472
|
if (rows.length > 0) {
|
|
907
1473
|
const summary = summarizeRalplanIndex(rows);
|
|
908
1474
|
iterationFromIndex = summary.iteration;
|
|
909
1475
|
stages = formatRalplanStagePresence(summary.currentStages);
|
|
1476
|
+
architectPasses = summary.currentStages.filter(stage => stage === "architect").length;
|
|
1477
|
+
criticPasses = summary.currentStages.filter(stage => stage === "critic").length;
|
|
910
1478
|
}
|
|
911
1479
|
}
|
|
912
1480
|
return buildRalplanHudSummary({
|
|
@@ -914,6 +1482,10 @@ async function buildRalplanHud(options: {
|
|
|
914
1482
|
iteration: options.iteration,
|
|
915
1483
|
iterationFromIndex,
|
|
916
1484
|
stages,
|
|
1485
|
+
architectPasses,
|
|
1486
|
+
criticPasses,
|
|
1487
|
+
reviewPassBudget: options.reviewPassBudget,
|
|
1488
|
+
verdict,
|
|
917
1489
|
pendingApproval: options.pendingApproval,
|
|
918
1490
|
latestSummary: options.latestSummary,
|
|
919
1491
|
updatedAt: new Date().toISOString(),
|
|
@@ -921,8 +1493,9 @@ async function buildRalplanHud(options: {
|
|
|
921
1493
|
}
|
|
922
1494
|
|
|
923
1495
|
async function handleArtifactWrite(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
|
|
924
|
-
const plannerState = parsePlannerStateArgs(args);
|
|
925
1496
|
const resolved = await resolveArtifactArgs(args, cwd);
|
|
1497
|
+
const persistedRoleState = parsePersistedRoleStateArgs(args, resolved.stage);
|
|
1498
|
+
const laneVerdict = parseLaneVerdictArgs(args, resolved.stage, resolved.stageN);
|
|
926
1499
|
// Fail closed before stage persistence / path writes when cwd drifted to a sibling repo.
|
|
927
1500
|
const repositoryBinding = await enforceRalplanRepositoryBinding(cwd, resolved.sessionId);
|
|
928
1501
|
// Artifact file paths (when --artifact points at a file) must stay under the bound root.
|
|
@@ -944,16 +1517,15 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
944
1517
|
const content = resolved.artifact.endsWith("\n") ? resolved.artifact : `${resolved.artifact}\n`;
|
|
945
1518
|
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
946
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
|
+
|
|
947
1525
|
// Duplicate-write guard: a second `--write` for the same (stage, stage_n) must not
|
|
948
1526
|
// silently clobber the artifact or append a duplicate ledger row. Classify before any
|
|
949
1527
|
// 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
|
-
);
|
|
1528
|
+
const existingArtifact = findExistingStageArtifact(indexLoad.rawText, resolved.stage, resolved.stageN);
|
|
957
1529
|
if (existingArtifact) {
|
|
958
1530
|
if (existingArtifact.sha256 !== sha256) {
|
|
959
1531
|
throw new RalplanCommandError(
|
|
@@ -961,21 +1533,65 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
961
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.`,
|
|
962
1534
|
);
|
|
963
1535
|
}
|
|
1536
|
+
if (resolved.stage === "final") await ensureFinalPendingApproval(cwd, resolved, existingArtifact);
|
|
964
1537
|
return buildDeduplicatedResult(resolved, existingArtifact, sha256, cwd, repositoryBinding);
|
|
965
1538
|
}
|
|
966
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
|
+
|
|
967
1575
|
// Consensus iteration budget (#3165): refuse new planner/revision openers past the cap.
|
|
968
1576
|
// Dedupe returns above so identical re-writes never stuck-signal. Non-openers (architect,
|
|
969
1577
|
// critic, final, …) remain allowed so operators can escalate without auto-implementation.
|
|
970
1578
|
// On-disk opener artifacts floor the count so a wiped/truncated/malformed index.jsonl cannot
|
|
971
1579
|
// under-count and fail open after prior planner/revision writes.
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
+
]);
|
|
975
1591
|
const capDecision = evaluateRalplanIterationCap({
|
|
976
1592
|
rows: indexLoad.rows,
|
|
977
1593
|
stage: resolved.stage,
|
|
978
|
-
maxIterations,
|
|
1594
|
+
maxIterations: iterationLimit.maxIterations,
|
|
979
1595
|
iterationFloor: onDiskOpeners,
|
|
980
1596
|
});
|
|
981
1597
|
if (!capDecision.allowed) {
|
|
@@ -985,15 +1601,34 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
985
1601
|
stageN: resolved.stageN,
|
|
986
1602
|
runId: resolved.runId,
|
|
987
1603
|
decision: capDecision,
|
|
988
|
-
source:
|
|
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,
|
|
989
1621
|
});
|
|
990
1622
|
}
|
|
991
1623
|
|
|
992
1624
|
// Keep run-state `current_phase` coherent with the stage being persisted.
|
|
993
1625
|
await persistActiveRunId(cwd, resolved.sessionId, resolved.runId, resolved.stage);
|
|
994
1626
|
const persisted = await persistArtifact(resolved, cwd, content, sha256);
|
|
995
|
-
if (
|
|
996
|
-
await
|
|
1627
|
+
if (persistedRoleState) {
|
|
1628
|
+
await applyPersistedRoleStateUpdate(cwd, resolved.sessionId, persistedRoleState);
|
|
1629
|
+
}
|
|
1630
|
+
if (laneVerdict) {
|
|
1631
|
+
await applyLaneVerdictUpdate(cwd, resolved.sessionId, laneVerdict);
|
|
997
1632
|
}
|
|
998
1633
|
await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "ralplan-runtime", path: persisted.path });
|
|
999
1634
|
await syncRalplanHud({
|
|
@@ -1003,8 +1638,18 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1003
1638
|
runId: persisted.runId,
|
|
1004
1639
|
pendingApproval: persisted.stage === "final",
|
|
1005
1640
|
iteration: persisted.stageN,
|
|
1641
|
+
reviewPassBudget: laneLimit.maxReviewPassesPerLane,
|
|
1006
1642
|
latestSummary: `persisted ${persisted.stage} stage ${persisted.stageN}`,
|
|
1007
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
|
+
|
|
1008
1653
|
const payload: Record<string, unknown> = {
|
|
1009
1654
|
run_id: persisted.runId,
|
|
1010
1655
|
path: persisted.path,
|
|
@@ -1015,17 +1660,20 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1015
1660
|
created_at: persisted.createdAt,
|
|
1016
1661
|
};
|
|
1017
1662
|
if (persisted.pendingApprovalPath) payload.pending_approval_path = persisted.pendingApprovalPath;
|
|
1018
|
-
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
|
+
|
|
1019
1667
|
const stdout = resolved.json
|
|
1020
1668
|
? `${JSON.stringify(payload, null, 2)}\n`
|
|
1021
|
-
: `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`;
|
|
1022
1670
|
return { status: 0, stdout };
|
|
1023
1671
|
}
|
|
1024
1672
|
|
|
1025
1673
|
/**
|
|
1026
|
-
* Deterministic
|
|
1027
|
-
*
|
|
1028
|
-
*
|
|
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.
|
|
1029
1677
|
*/
|
|
1030
1678
|
function buildDeduplicatedResult(
|
|
1031
1679
|
resolved: ResolvedArtifactArgs,
|
|
@@ -1033,6 +1681,8 @@ function buildDeduplicatedResult(
|
|
|
1033
1681
|
sha256: string,
|
|
1034
1682
|
cwd: string,
|
|
1035
1683
|
repositoryBinding: RepositoryBinding,
|
|
1684
|
+
laneVerdict?: LaneVerdictUpdate,
|
|
1685
|
+
persistedRoleState?: PersistedRoleStateUpdate,
|
|
1036
1686
|
): RalplanCommandResult {
|
|
1037
1687
|
const payload: Record<string, unknown> = {
|
|
1038
1688
|
run_id: resolved.runId,
|
|
@@ -1044,6 +1694,10 @@ function buildDeduplicatedResult(
|
|
|
1044
1694
|
created_at: existing.createdAt,
|
|
1045
1695
|
deduplicated: true,
|
|
1046
1696
|
};
|
|
1697
|
+
if (laneVerdict) payload.lane_verdict = { lane: laneVerdict.lane, verdict: laneVerdict.verdict };
|
|
1698
|
+
if (persistedRoleState) {
|
|
1699
|
+
payload[`${persistedRoleState.role}_state`] = persistedRoleStatePayload(persistedRoleState);
|
|
1700
|
+
}
|
|
1047
1701
|
if (resolved.stage === "final") {
|
|
1048
1702
|
payload.pending_approval_path = path.join(
|
|
1049
1703
|
sessionPlansDir(cwd, resolved.sessionId),
|
|
@@ -1092,6 +1746,9 @@ function extractPositionalTask(args: readonly string[]): string {
|
|
|
1092
1746
|
}
|
|
1093
1747
|
|
|
1094
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
|
+
}
|
|
1095
1752
|
const architectKind = flagValue(args, "--architect")?.trim() || undefined;
|
|
1096
1753
|
if (architectKind && !KNOWN_ARCHITECT_KINDS.has(architectKind)) {
|
|
1097
1754
|
throw new RalplanCommandError(
|