@gajae-code/coding-agent 0.12.0 → 0.12.2
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 +111 -0
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/config/settings-schema.d.ts +56 -2
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +16 -2
- package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
- package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +23 -3
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +37 -80
- package/dist/types/modes/acp/acp-agent.d.ts +2 -19
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
- package/dist/types/modes/components/settings-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +1 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +32 -0
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/index.d.ts +29 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/prompt-status.d.ts +23 -3
- package/dist/types/sdk/session.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +40 -2
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +30 -4
- package/dist/types/session/session-manager.d.ts +23 -6
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +36 -7
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -0
- package/dist/types/skill-state/workflow-hud.d.ts +6 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +7 -1
- package/dist/types/task/output-manager.d.ts +1 -0
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +9 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +6 -0
- package/dist/types/tools/output-meta.d.ts +23 -2
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read.d.ts +0 -6
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/utils/gh.d.ts +13 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/searxng.d.ts +7 -0
- package/dist/types/web/search/providers/xai.d.ts +2 -0
- package/package.json +10 -8
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
- package/scripts/generate-sdk-operation-inventory.ts +5 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +290 -0
- package/src/async/job-manager.ts +99 -39
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/commands/team.ts +19 -1
- package/src/config/file-lock.ts +11 -2
- package/src/config/settings-schema.ts +53 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/debug/index.ts +42 -17
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +25 -20
- package/src/defaults/gjc/skills/team/SKILL.md +2 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -26
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/exa/mcp-client.ts +12 -3
- package/src/exec/bash-executor.ts +144 -27
- package/src/extensibility/extensions/runner.ts +15 -0
- package/src/extensibility/extensions/types.ts +18 -1
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
- package/src/extensibility/gjc-plugins/index.ts +19 -2
- package/src/extensibility/gjc-plugins/installer.ts +189 -50
- package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
- package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
- package/src/extensibility/gjc-plugins/observability.ts +3 -3
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
- package/src/extensibility/gjc-plugins/schema.ts +84 -12
- package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
- package/src/extensibility/gjc-plugins/types.ts +141 -0
- package/src/extensibility/gjc-plugins/validation.ts +9 -0
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/launch-worktree.ts +103 -1
- package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
- package/src/gjc-runtime/ralplan-runtime.ts +491 -27
- package/src/gjc-runtime/team-launch.ts +8 -0
- package/src/gjc-runtime/team-runtime.ts +140 -32
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
- package/src/gjc-runtime/ultragoal-runtime.ts +635 -612
- package/src/gjc-runtime/workflow-manifest.generated.json +37 -99
- package/src/gjc-runtime/workflow-manifest.ts +21 -70
- package/src/internal-urls/docs-index.generated.ts +16 -16
- package/src/modes/DESIGN.md +104 -0
- package/src/modes/acp/acp-agent.ts +298 -186
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/irc-sidebar.ts +30 -1
- package/src/modes/components/notifications-settings-editor.ts +12 -1
- package/src/modes/components/oauth-selector.ts +68 -30
- package/src/modes/components/session-observer-overlay.ts +305 -86
- package/src/modes/components/settings-selector.ts +56 -5
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-status-header.ts +3 -15
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +42 -34
- package/src/modes/controllers/event-controller.ts +27 -16
- package/src/modes/controllers/extension-ui-controller.ts +193 -34
- package/src/modes/controllers/selector-controller.ts +160 -22
- package/src/modes/interactive-mode.ts +189 -62
- package/src/modes/jobs-observer.ts +1 -1
- package/src/modes/runtime-init.ts +3 -0
- package/src/modes/theme/theme.ts +11 -9
- package/src/modes/types.ts +88 -0
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -4
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/reminders/star-reminder.ts +4 -42
- package/src/runtime-mcp/manager.ts +19 -4
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/lifecycle.ts +18 -13
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +7 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/index.ts +661 -126
- package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/reconciliation-store.ts +95 -16
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +12 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/session.ts +89 -9
- package/src/session/agent-session.ts +844 -173
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +200 -111
- package/src/session/internal/managed-session-storage.ts +492 -62
- package/src/session/session-manager.ts +1614 -435
- package/src/session/session-storage.ts +38 -45
- package/src/session/streaming-output.ts +238 -68
- package/src/setup/credential-import.ts +75 -13
- package/src/skill-state/active-state.ts +32 -0
- package/src/skill-state/workflow-hud.ts +17 -0
- package/src/skill-state/workflow-mutation-guard.ts +496 -2
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +280 -45
- package/src/task/output-manager.ts +24 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +34 -8
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +86 -55
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/bash.ts +503 -60
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +6 -0
- package/src/tools/output-meta.ts +108 -14
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +18 -0
- package/src/tools/review.ts +5 -7
- package/src/tools/search.ts +1 -0
- package/src/tools/subagent-render.ts +6 -2
- package/src/tools/subagent.ts +6 -0
- package/src/tools/telegram-send.ts +6 -3
- package/src/utils/gh.ts +39 -0
- package/src/utils/image-resize.ts +3 -0
- package/src/web/search/providers/anthropic.ts +4 -4
- package/src/web/search/providers/kimi.ts +15 -5
- package/src/web/search/providers/searxng.ts +47 -4
- package/src/web/search/providers/xai.ts +7 -2
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import type { Dirent } from "node:fs";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import { getConfigRootDir } from "@gajae-code/utils";
|
|
@@ -12,6 +13,12 @@ import {
|
|
|
12
13
|
type RalplanIndexRow,
|
|
13
14
|
summarizeRalplanIndex,
|
|
14
15
|
} from "./ledger-event-renderer";
|
|
16
|
+
import {
|
|
17
|
+
type IndexedReviewArtifact,
|
|
18
|
+
parseReviewConflictDocument,
|
|
19
|
+
reviewArtifactIndexKey,
|
|
20
|
+
serializeReviewConflictDocument,
|
|
21
|
+
} from "./ralplan-review-conflicts";
|
|
15
22
|
import {
|
|
16
23
|
assertCwdMatchesRepositoryBinding,
|
|
17
24
|
assertPathUnderRepositoryBinding,
|
|
@@ -22,7 +29,7 @@ import {
|
|
|
22
29
|
RepositoryBindingError,
|
|
23
30
|
} from "./repository-binding";
|
|
24
31
|
import { GJC_RALPLAN_ARTIFACT_ENV, isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
|
|
25
|
-
import { gjcRoot, modeStatePath, sessionPlansDir } from "./session-layout";
|
|
32
|
+
import { gjcRoot, modeStatePath, sessionIdFromDirName, sessionPlansDir } from "./session-layout";
|
|
26
33
|
import { resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
27
34
|
import { migrateWorkflowState } from "./state-migrations";
|
|
28
35
|
import { runNativeStateCommand } from "./state-runtime";
|
|
@@ -33,9 +40,9 @@ import {
|
|
|
33
40
|
writeArtifact,
|
|
34
41
|
writeWorkflowEnvelopeAtomic,
|
|
35
42
|
} from "./state-writer";
|
|
43
|
+
import { probeGjcTeamAvailability } from "./team-runtime";
|
|
36
44
|
import { assertSafePathComponent, CommandError, flagValue, hasFlag } from "./workflow-cli-common";
|
|
37
45
|
import { getSkillManifest } from "./workflow-manifest";
|
|
38
|
-
|
|
39
46
|
/**
|
|
40
47
|
* Native implementation of `gjc ralplan`.
|
|
41
48
|
*
|
|
@@ -51,9 +58,11 @@ import { getSkillManifest } from "./workflow-manifest";
|
|
|
51
58
|
* 2. **Artifact write**: `gjc ralplan --write --stage <type> --stage_n <N>
|
|
52
59
|
* (--artifact <path-or-string> | --artifact-env GJC_RALPLAN_ARTIFACT)
|
|
53
60
|
* [--run-id <id>] [--session-id <id>] [--lane-verdict <token>] [--json]` persists Planner / Architect
|
|
54
|
-
* / Critic / revision / post-interview / ADR / final
|
|
55
|
-
* an `index.jsonl` audit log, copies `final`
|
|
56
|
-
* the HUD chip to reflect the latest
|
|
61
|
+
* / Critic / disposition / revision / post-interview / ADR / final artifacts under
|
|
62
|
+
* `.gjc/plans/ralplan/<run-id>/`, maintains an `index.jsonl` audit log, copies `final`
|
|
63
|
+
* stages to `pending-approval.md`, and advances the HUD chip to reflect the latest
|
|
64
|
+
* persisted stage. Disposition stage artifacts are fail-closed JSON documents that
|
|
65
|
+
* record typed review conflicts with authoritative same-pass source receipts (#2902).
|
|
57
66
|
*/
|
|
58
67
|
|
|
59
68
|
export interface RalplanCommandResult {
|
|
@@ -62,7 +71,16 @@ export interface RalplanCommandResult {
|
|
|
62
71
|
stderr?: string;
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
const KNOWN_STAGES = [
|
|
74
|
+
const KNOWN_STAGES = [
|
|
75
|
+
"planner",
|
|
76
|
+
"architect",
|
|
77
|
+
"critic",
|
|
78
|
+
"disposition",
|
|
79
|
+
"revision",
|
|
80
|
+
"post-interview",
|
|
81
|
+
"adr",
|
|
82
|
+
"final",
|
|
83
|
+
] as const;
|
|
66
84
|
type RalplanStage = (typeof KNOWN_STAGES)[number];
|
|
67
85
|
/** Default consensus iterations (planner + revision openers) per run. Matches SKILL.md re-review cap. */
|
|
68
86
|
export const RALPLAN_DEFAULT_MAX_ITERATIONS = 5;
|
|
@@ -74,6 +92,16 @@ export const PLANNING_STUCK_MARKER = "PLANNING-STUCK";
|
|
|
74
92
|
export const RALPLAN_DEFAULT_MAX_REVIEW_PASSES_PER_LANE = 1;
|
|
75
93
|
/** Inclusive upper bound for `gjc.ralplan.maxReviewPassesPerLane` settings overrides. */
|
|
76
94
|
export const RALPLAN_MAX_REVIEW_PASSES_PER_LANE_LIMIT = 10;
|
|
95
|
+
export type RalplanAutoHandoffTarget = "off" | "ultragoal" | "team";
|
|
96
|
+
|
|
97
|
+
export interface RalplanAutoHandoffResolution {
|
|
98
|
+
configuredTarget: RalplanAutoHandoffTarget;
|
|
99
|
+
effectiveTarget: RalplanAutoHandoffTarget;
|
|
100
|
+
degradationReason: string | null;
|
|
101
|
+
source: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const RALPLAN_AUTO_HANDOFF_TARGETS = new Set<RalplanAutoHandoffTarget>(["off", "ultragoal", "team"]);
|
|
77
105
|
|
|
78
106
|
const RALPLAN_ITERATION_OPENER_STAGES = new Set<RalplanStage>(["planner", "revision"]);
|
|
79
107
|
|
|
@@ -391,6 +419,115 @@ export async function resolveRalplanMaxIterations(cwd: string): Promise<{ maxIte
|
|
|
391
419
|
if (user !== null) return { maxIterations: user, source: userPath };
|
|
392
420
|
return { maxIterations: RALPLAN_DEFAULT_MAX_ITERATIONS, source: "default" };
|
|
393
421
|
}
|
|
422
|
+
function parseRalplanAutoHandoffTarget(value: unknown): RalplanAutoHandoffTarget | undefined {
|
|
423
|
+
return typeof value === "string" && RALPLAN_AUTO_HANDOFF_TARGETS.has(value as RalplanAutoHandoffTarget)
|
|
424
|
+
? (value as RalplanAutoHandoffTarget)
|
|
425
|
+
: undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
type RalplanAutoHandoffOptions = {
|
|
429
|
+
planningStuck?: boolean;
|
|
430
|
+
teamAvailabilityProbe?: () => { available: true } | { available: false; reason: string };
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
type RalplanAutoHandoffSetting =
|
|
434
|
+
| { kind: "absent" }
|
|
435
|
+
| { kind: "valid"; value: RalplanAutoHandoffTarget }
|
|
436
|
+
| { kind: "invalid"; reason: string };
|
|
437
|
+
|
|
438
|
+
function parsePresentRalplanAutoHandoff(value: unknown): RalplanAutoHandoffSetting {
|
|
439
|
+
const target = parseRalplanAutoHandoffTarget(value);
|
|
440
|
+
return target === undefined
|
|
441
|
+
? {
|
|
442
|
+
kind: "invalid",
|
|
443
|
+
reason: "expected gjc.ralplan.autoHandoff to be one of off, ultragoal, team",
|
|
444
|
+
}
|
|
445
|
+
: { kind: "valid", value: target };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function parseRalplanAutoHandoffSettings(parsed: unknown): RalplanAutoHandoffSetting {
|
|
449
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { kind: "absent" };
|
|
450
|
+
const settings = parsed as Record<string, unknown>;
|
|
451
|
+
if (Object.hasOwn(settings, "gjc.ralplan.autoHandoff")) {
|
|
452
|
+
return parsePresentRalplanAutoHandoff(settings["gjc.ralplan.autoHandoff"]);
|
|
453
|
+
}
|
|
454
|
+
const gjc = settings.gjc;
|
|
455
|
+
if (!gjc || typeof gjc !== "object" || Array.isArray(gjc)) return { kind: "absent" };
|
|
456
|
+
const ralplan = (gjc as Record<string, unknown>).ralplan;
|
|
457
|
+
if (!ralplan || typeof ralplan !== "object" || Array.isArray(ralplan)) return { kind: "absent" };
|
|
458
|
+
const ralplanSettings = ralplan as Record<string, unknown>;
|
|
459
|
+
if (!Object.hasOwn(ralplanSettings, "autoHandoff")) return { kind: "absent" };
|
|
460
|
+
return parsePresentRalplanAutoHandoff(ralplanSettings.autoHandoff);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function readSettingsAutoHandoff(settingsPath: string): Promise<RalplanAutoHandoffSetting> {
|
|
464
|
+
let raw: string;
|
|
465
|
+
try {
|
|
466
|
+
raw = await Bun.file(settingsPath).text();
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (getErrorCode(error) === "ENOENT") return { kind: "absent" };
|
|
469
|
+
return {
|
|
470
|
+
kind: "invalid",
|
|
471
|
+
reason: `unable to read settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
let parsed: unknown;
|
|
475
|
+
try {
|
|
476
|
+
parsed = JSON.parse(raw);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
return {
|
|
479
|
+
kind: "invalid",
|
|
480
|
+
reason: `malformed JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
return parseRalplanAutoHandoffSettings(parsed);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export async function resolveRalplanAutoHandoff(
|
|
487
|
+
cwd: string,
|
|
488
|
+
options: RalplanAutoHandoffOptions = {},
|
|
489
|
+
): Promise<RalplanAutoHandoffResolution> {
|
|
490
|
+
const projectPath = path.join(gjcRoot(cwd), "settings.json");
|
|
491
|
+
const project = await readSettingsAutoHandoff(projectPath);
|
|
492
|
+
if (project.kind === "invalid") {
|
|
493
|
+
throw new RalplanCommandError(2, `invalid ralplan settings at ${projectPath}: ${project.reason}`);
|
|
494
|
+
}
|
|
495
|
+
if (project.kind === "valid") {
|
|
496
|
+
return resolveRalplanAutoHandoffTarget(project.value, projectPath, options);
|
|
497
|
+
}
|
|
498
|
+
const userPath = path.join(getConfigRootDir(), "settings.json");
|
|
499
|
+
const user = await readSettingsAutoHandoff(userPath);
|
|
500
|
+
if (user.kind === "invalid") {
|
|
501
|
+
throw new RalplanCommandError(2, `invalid ralplan settings at ${userPath}: ${user.reason}`);
|
|
502
|
+
}
|
|
503
|
+
return resolveRalplanAutoHandoffTarget(
|
|
504
|
+
user.kind === "valid" ? user.value : "off",
|
|
505
|
+
user.kind === "valid" ? userPath : "default",
|
|
506
|
+
options,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function resolveRalplanAutoHandoffTarget(
|
|
511
|
+
configuredTarget: RalplanAutoHandoffTarget,
|
|
512
|
+
source: string,
|
|
513
|
+
options: RalplanAutoHandoffOptions,
|
|
514
|
+
): RalplanAutoHandoffResolution {
|
|
515
|
+
if (options.planningStuck) {
|
|
516
|
+
return { configuredTarget, effectiveTarget: "off", degradationReason: "planning_stuck", source };
|
|
517
|
+
}
|
|
518
|
+
if (configuredTarget !== "team")
|
|
519
|
+
return { configuredTarget, effectiveTarget: configuredTarget, degradationReason: null, source };
|
|
520
|
+
|
|
521
|
+
const availability = (options.teamAvailabilityProbe ?? probeGjcTeamAvailability)();
|
|
522
|
+
return availability.available
|
|
523
|
+
? { configuredTarget, effectiveTarget: "team", degradationReason: null, source }
|
|
524
|
+
: {
|
|
525
|
+
configuredTarget,
|
|
526
|
+
effectiveTarget: "off",
|
|
527
|
+
degradationReason: `team_unavailable:${availability.reason}`,
|
|
528
|
+
source,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
394
531
|
|
|
395
532
|
function parseMaxReviewPassesPerLaneValue(value: unknown): number | null {
|
|
396
533
|
return parseBoundedPositiveInteger(value, RALPLAN_MAX_REVIEW_PASSES_PER_LANE_LIMIT);
|
|
@@ -763,6 +900,8 @@ async function persistActiveRunId(cwd: string, sessionId: string, runId: string,
|
|
|
763
900
|
for (const key of Object.keys(existing)) {
|
|
764
901
|
if (key.startsWith("last_review_verdict")) delete existing[key];
|
|
765
902
|
}
|
|
903
|
+
delete existing.planning_stuck;
|
|
904
|
+
delete existing.auto_handoff;
|
|
766
905
|
}
|
|
767
906
|
if (
|
|
768
907
|
existing.run_id === runId &&
|
|
@@ -1094,6 +1233,194 @@ async function applyLaneVerdictUpdate(
|
|
|
1094
1233
|
{ cwd },
|
|
1095
1234
|
);
|
|
1096
1235
|
}
|
|
1236
|
+
function ralplanPlanningStuckIndexKey(entry: unknown): string | undefined {
|
|
1237
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return undefined;
|
|
1238
|
+
const record = entry as Record<string, unknown>;
|
|
1239
|
+
return record.planning_stuck === true ? "planning_stuck" : undefined;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
async function recordRalplanPlanningStuck(
|
|
1243
|
+
cwd: string,
|
|
1244
|
+
sessionId: string,
|
|
1245
|
+
runId: string,
|
|
1246
|
+
reason: string,
|
|
1247
|
+
): Promise<void> {
|
|
1248
|
+
const runDir = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId);
|
|
1249
|
+
await appendJsonlIdempotent(
|
|
1250
|
+
path.join(runDir, "index.jsonl"),
|
|
1251
|
+
{
|
|
1252
|
+
event: "planning_stuck",
|
|
1253
|
+
planning_stuck: true,
|
|
1254
|
+
marker: PLANNING_STUCK_MARKER,
|
|
1255
|
+
reason,
|
|
1256
|
+
created_at: new Date().toISOString(),
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
cwd,
|
|
1260
|
+
audit: {
|
|
1261
|
+
category: "ledger",
|
|
1262
|
+
verb: "append",
|
|
1263
|
+
owner: "gjc-runtime",
|
|
1264
|
+
skill: "ralplan",
|
|
1265
|
+
sessionId,
|
|
1266
|
+
},
|
|
1267
|
+
key: ralplanPlanningStuckIndexKey,
|
|
1268
|
+
},
|
|
1269
|
+
);
|
|
1270
|
+
const statePath = ralplanStatePath(cwd, sessionId);
|
|
1271
|
+
await withWorkflowStateLock(
|
|
1272
|
+
statePath,
|
|
1273
|
+
async () => {
|
|
1274
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
1275
|
+
if (existingRead.kind === "corrupt") return;
|
|
1276
|
+
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1277
|
+
if (existing.run_id !== runId) return;
|
|
1278
|
+
existing.planning_stuck = { marker: PLANNING_STUCK_MARKER, reason };
|
|
1279
|
+
existing = migrateWorkflowState(existing, "ralplan").state;
|
|
1280
|
+
existing.updated_at = new Date().toISOString();
|
|
1281
|
+
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
1282
|
+
cwd,
|
|
1283
|
+
lockHeld: true,
|
|
1284
|
+
receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan planning-stuck", sessionId },
|
|
1285
|
+
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
|
|
1286
|
+
});
|
|
1287
|
+
},
|
|
1288
|
+
{ cwd },
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
async function readRalplanFinalAdmission(
|
|
1293
|
+
cwd: string,
|
|
1294
|
+
sessionId: string,
|
|
1295
|
+
runId: string,
|
|
1296
|
+
): Promise<RalplanAutoHandoffResolution | undefined> {
|
|
1297
|
+
const index = await loadRalplanIndexForCap(cwd, sessionId, runId);
|
|
1298
|
+
if (index.rawText === undefined) return undefined;
|
|
1299
|
+
let finalAdmission: RalplanAutoHandoffResolution | undefined;
|
|
1300
|
+
for (const line of index.rawText.split(/\r?\n/)) {
|
|
1301
|
+
try {
|
|
1302
|
+
const row = JSON.parse(line) as Record<string, unknown>;
|
|
1303
|
+
if (row.stage === "final" && typeof row.path === "string" && typeof row.sha256 === "string") {
|
|
1304
|
+
finalAdmission = parseRalplanFinalAdmission(row.auto_handoff) ?? unavailableRalplanFinalAdmission();
|
|
1305
|
+
}
|
|
1306
|
+
} catch {
|
|
1307
|
+
// The artifact dedupe guard handles malformed ledger records separately.
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return finalAdmission;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
async function readRalplanPlanningStuck(cwd: string, sessionId: string, runId: string): Promise<boolean> {
|
|
1314
|
+
const index = await loadRalplanIndexForCap(cwd, sessionId, runId);
|
|
1315
|
+
if (index.rawText === undefined) return index.indexPresent;
|
|
1316
|
+
if (index.indexPresent && index.rawLineCount > 0 && index.parseableLines === 0) return true;
|
|
1317
|
+
for (const line of index.rawText.split(/\r?\n/)) {
|
|
1318
|
+
if (!line.trim()) continue;
|
|
1319
|
+
try {
|
|
1320
|
+
const row = JSON.parse(line) as Record<string, unknown>;
|
|
1321
|
+
if (row.planning_stuck === true) return true;
|
|
1322
|
+
} catch {
|
|
1323
|
+
return true;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
return false;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
async function persistRalplanFinalAdmission(
|
|
1330
|
+
cwd: string,
|
|
1331
|
+
sessionId: string,
|
|
1332
|
+
runId: string,
|
|
1333
|
+
admission: RalplanAutoHandoffResolution,
|
|
1334
|
+
): Promise<void> {
|
|
1335
|
+
const statePath = ralplanStatePath(cwd, sessionId);
|
|
1336
|
+
await withWorkflowStateLock(
|
|
1337
|
+
statePath,
|
|
1338
|
+
async () => {
|
|
1339
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
1340
|
+
if (existingRead.kind === "corrupt") {
|
|
1341
|
+
throw new RalplanCommandError(
|
|
1342
|
+
2,
|
|
1343
|
+
`existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to record final admission`,
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1347
|
+
if (existing.run_id !== runId) return;
|
|
1348
|
+
existing.auto_handoff = admission;
|
|
1349
|
+
existing = migrateWorkflowState(existing, "ralplan").state;
|
|
1350
|
+
existing.updated_at = new Date().toISOString();
|
|
1351
|
+
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
1352
|
+
cwd,
|
|
1353
|
+
lockHeld: true,
|
|
1354
|
+
receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan final-admission", sessionId },
|
|
1355
|
+
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
|
|
1356
|
+
});
|
|
1357
|
+
},
|
|
1358
|
+
{ cwd },
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
async function findExistingRalplanRunOwners(cwd: string, runId: string): Promise<string[]> {
|
|
1363
|
+
let entries: Dirent<string>[];
|
|
1364
|
+
try {
|
|
1365
|
+
entries = await fs.readdir(gjcRoot(cwd), { withFileTypes: true });
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
const err = error as NodeJS.ErrnoException;
|
|
1368
|
+
if (err.code === "ENOENT" || err.code === "ENOTDIR") return [];
|
|
1369
|
+
throw error;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const owners = await Promise.all(
|
|
1373
|
+
entries.map(async entry => {
|
|
1374
|
+
if (!entry.isDirectory()) return undefined;
|
|
1375
|
+
const sessionId = sessionIdFromDirName(entry.name);
|
|
1376
|
+
if (!sessionId) return undefined;
|
|
1377
|
+
|
|
1378
|
+
const stateRead = await readExistingStateForMutation(ralplanStatePath(cwd, sessionId));
|
|
1379
|
+
const stateOwnsRun =
|
|
1380
|
+
stateRead.kind === "valid" &&
|
|
1381
|
+
typeof stateRead.value.run_id === "string" &&
|
|
1382
|
+
stateRead.value.run_id.trim() === runId;
|
|
1383
|
+
if (stateOwnsRun) return sessionId;
|
|
1384
|
+
|
|
1385
|
+
try {
|
|
1386
|
+
const stat = await fs.stat(path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId));
|
|
1387
|
+
return stat.isDirectory() ? sessionId : undefined;
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
const err = error as NodeJS.ErrnoException;
|
|
1390
|
+
if (err.code === "ENOENT" || err.code === "ENOTDIR") return undefined;
|
|
1391
|
+
throw error;
|
|
1392
|
+
}
|
|
1393
|
+
}),
|
|
1394
|
+
);
|
|
1395
|
+
return owners.filter((owner): owner is string => owner !== undefined).sort();
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
async function resolveArtifactSessionId(args: readonly string[], cwd: string, explicitRunId: string | undefined) {
|
|
1399
|
+
const flagSessionId = flagValue(args, "--session-id");
|
|
1400
|
+
const currentSession = resolveGjcSessionForWrite(cwd, {
|
|
1401
|
+
flagValue: flagSessionId,
|
|
1402
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
1403
|
+
});
|
|
1404
|
+
if (!explicitRunId) return currentSession.gjcSessionId;
|
|
1405
|
+
|
|
1406
|
+
const owners = await findExistingRalplanRunOwners(cwd, explicitRunId);
|
|
1407
|
+
if (owners.length === 0) return currentSession.gjcSessionId;
|
|
1408
|
+
if (owners.length > 1) {
|
|
1409
|
+
throw new RalplanCommandError(
|
|
1410
|
+
2,
|
|
1411
|
+
`ralplan run ${explicitRunId} has multiple owner sessions (${owners.join(", ")}); repair the fragmented run before writing`,
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
const ownerSessionId = owners[0]!;
|
|
1416
|
+
if (flagSessionId !== undefined && currentSession.gjcSessionId !== ownerSessionId) {
|
|
1417
|
+
throw new RalplanCommandError(
|
|
1418
|
+
2,
|
|
1419
|
+
`ralplan run ${explicitRunId} is owned by session ${ownerSessionId}, not ${currentSession.gjcSessionId}`,
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
return ownerSessionId;
|
|
1423
|
+
}
|
|
1097
1424
|
|
|
1098
1425
|
async function resolveArtifactArgs(args: readonly string[], cwd: string): Promise<ResolvedArtifactArgs> {
|
|
1099
1426
|
const stage = flagValue(args, "--stage");
|
|
@@ -1115,20 +1442,18 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
|
|
|
1115
1442
|
throw new RalplanCommandError(2, `--artifact-env must be ${GJC_RALPLAN_ARTIFACT_ENV}`);
|
|
1116
1443
|
}
|
|
1117
1444
|
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
const sessionId = session.gjcSessionId;
|
|
1445
|
+
const explicitRunId = flagValue(args, "--run-id")?.trim();
|
|
1446
|
+
if (explicitRunId) assertSafePathComponent(explicitRunId, "run-id");
|
|
1447
|
+
|
|
1448
|
+
const sessionId = await resolveArtifactSessionId(args, cwd, explicitRunId);
|
|
1123
1449
|
assertSafePathComponent(sessionId, "session-id");
|
|
1124
1450
|
const sessionIdRaw = sessionId;
|
|
1125
1451
|
|
|
1126
1452
|
// Precedence for run_id:
|
|
1127
1453
|
// 1. explicit --run-id flag
|
|
1128
|
-
// 2. existing run_id field in
|
|
1129
|
-
// 3.
|
|
1454
|
+
// 2. existing run_id field in the resolved owner session's ralplan state
|
|
1455
|
+
// 3. resolved owner session id
|
|
1130
1456
|
// 4. freshly generated default run id
|
|
1131
|
-
const explicitRunId = flagValue(args, "--run-id")?.trim();
|
|
1132
1457
|
const runId = explicitRunId || (await readActiveRunId(cwd, sessionId)) || sessionIdRaw || defaultRunId();
|
|
1133
1458
|
assertSafePathComponent(runId, "run-id");
|
|
1134
1459
|
|
|
@@ -1170,6 +1495,7 @@ async function persistArtifact(
|
|
|
1170
1495
|
cwd: string,
|
|
1171
1496
|
content: string,
|
|
1172
1497
|
sha256: string,
|
|
1498
|
+
finalAdmission?: RalplanAutoHandoffResolution,
|
|
1173
1499
|
): Promise<PersistedArtifact> {
|
|
1174
1500
|
const runDir = path.join(sessionPlansDir(cwd, resolved.sessionId), "ralplan", resolved.runId);
|
|
1175
1501
|
|
|
@@ -1193,6 +1519,7 @@ async function persistArtifact(
|
|
|
1193
1519
|
path: filePath,
|
|
1194
1520
|
created_at: createdAt,
|
|
1195
1521
|
sha256,
|
|
1522
|
+
...(finalAdmission ? { auto_handoff: finalAdmission } : {}),
|
|
1196
1523
|
};
|
|
1197
1524
|
await appendJsonlIdempotent(path.join(runDir, "index.jsonl"), indexEntry, {
|
|
1198
1525
|
cwd,
|
|
@@ -1237,6 +1564,35 @@ interface ExistingStageArtifact {
|
|
|
1237
1564
|
path: string;
|
|
1238
1565
|
sha256: string;
|
|
1239
1566
|
createdAt: string;
|
|
1567
|
+
autoHandoff?: RalplanAutoHandoffResolution;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function parseRalplanFinalAdmission(value: unknown): RalplanAutoHandoffResolution | undefined {
|
|
1571
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1572
|
+
const admission = value as Record<string, unknown>;
|
|
1573
|
+
if (
|
|
1574
|
+
!RALPLAN_AUTO_HANDOFF_TARGETS.has(admission.configuredTarget as RalplanAutoHandoffTarget) ||
|
|
1575
|
+
!RALPLAN_AUTO_HANDOFF_TARGETS.has(admission.effectiveTarget as RalplanAutoHandoffTarget) ||
|
|
1576
|
+
(typeof admission.degradationReason !== "string" && admission.degradationReason !== null) ||
|
|
1577
|
+
typeof admission.source !== "string"
|
|
1578
|
+
) {
|
|
1579
|
+
return undefined;
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
configuredTarget: admission.configuredTarget as RalplanAutoHandoffTarget,
|
|
1583
|
+
effectiveTarget: admission.effectiveTarget as RalplanAutoHandoffTarget,
|
|
1584
|
+
degradationReason: typeof admission.degradationReason === "string" ? admission.degradationReason : null,
|
|
1585
|
+
source: admission.source,
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function unavailableRalplanFinalAdmission(): RalplanAutoHandoffResolution {
|
|
1590
|
+
return {
|
|
1591
|
+
configuredTarget: "off",
|
|
1592
|
+
effectiveTarget: "off",
|
|
1593
|
+
degradationReason: "admission_unavailable",
|
|
1594
|
+
source: "ledger",
|
|
1595
|
+
};
|
|
1240
1596
|
}
|
|
1241
1597
|
|
|
1242
1598
|
/**
|
|
@@ -1269,6 +1625,7 @@ function findExistingStageArtifact(
|
|
|
1269
1625
|
path: record.path,
|
|
1270
1626
|
sha256: record.sha256,
|
|
1271
1627
|
createdAt: typeof record.created_at === "string" ? record.created_at : "",
|
|
1628
|
+
...(stage === "final" ? { autoHandoff: parseRalplanFinalAdmission(record.auto_handoff) } : {}),
|
|
1272
1629
|
};
|
|
1273
1630
|
}
|
|
1274
1631
|
return match;
|
|
@@ -1354,6 +1711,7 @@ async function repairMissingStageArtifactLedger(
|
|
|
1354
1711
|
cwd: string,
|
|
1355
1712
|
resolved: Pick<ResolvedArtifactArgs, "sessionId" | "runId" | "stage" | "stageN">,
|
|
1356
1713
|
onDisk: OnDiskStageArtifact,
|
|
1714
|
+
finalAdmission?: RalplanAutoHandoffResolution,
|
|
1357
1715
|
): Promise<ExistingStageArtifact> {
|
|
1358
1716
|
const createdAt = new Date().toISOString();
|
|
1359
1717
|
const indexEntry = {
|
|
@@ -1362,6 +1720,7 @@ async function repairMissingStageArtifactLedger(
|
|
|
1362
1720
|
path: onDisk.path,
|
|
1363
1721
|
created_at: createdAt,
|
|
1364
1722
|
sha256: onDisk.sha256,
|
|
1723
|
+
...(finalAdmission ? { auto_handoff: finalAdmission } : {}),
|
|
1365
1724
|
};
|
|
1366
1725
|
const result = await appendJsonlIdempotent(
|
|
1367
1726
|
path.join(sessionPlansDir(cwd, resolved.sessionId), "ralplan", resolved.runId, "index.jsonl"),
|
|
@@ -1386,10 +1745,18 @@ async function repairMissingStageArtifactLedger(
|
|
|
1386
1745
|
path: record.path,
|
|
1387
1746
|
sha256: record.sha256,
|
|
1388
1747
|
createdAt: typeof record.created_at === "string" ? record.created_at : createdAt,
|
|
1748
|
+
...(resolved.stage === "final"
|
|
1749
|
+
? { autoHandoff: parseRalplanFinalAdmission(record.auto_handoff) ?? unavailableRalplanFinalAdmission() }
|
|
1750
|
+
: {}),
|
|
1389
1751
|
};
|
|
1390
1752
|
}
|
|
1391
1753
|
}
|
|
1392
|
-
return {
|
|
1754
|
+
return {
|
|
1755
|
+
path: onDisk.path,
|
|
1756
|
+
sha256: onDisk.sha256,
|
|
1757
|
+
createdAt,
|
|
1758
|
+
...(resolved.stage === "final" ? { autoHandoff: finalAdmission ?? unavailableRalplanFinalAdmission() } : {}),
|
|
1759
|
+
};
|
|
1393
1760
|
}
|
|
1394
1761
|
|
|
1395
1762
|
/**
|
|
@@ -1463,12 +1830,18 @@ async function buildRalplanHud(options: {
|
|
|
1463
1830
|
let architectPasses: number | undefined;
|
|
1464
1831
|
let criticPasses: number | undefined;
|
|
1465
1832
|
let verdict: string | undefined;
|
|
1833
|
+
let autoHandoff: RalplanAutoHandoffResolution | undefined;
|
|
1834
|
+
let planningStuck = false;
|
|
1466
1835
|
if (options.runId && options.sessionId) {
|
|
1467
|
-
const [rows, lastReviewVerdict] = await Promise.all([
|
|
1836
|
+
const [rows, lastReviewVerdict, persistedAutoHandoff, persistedPlanningStuck] = await Promise.all([
|
|
1468
1837
|
readRalplanIndexRows(options.cwd, options.sessionId, options.runId),
|
|
1469
1838
|
readRalplanLastReviewVerdict(options.cwd, options.sessionId),
|
|
1839
|
+
readRalplanFinalAdmission(options.cwd, options.sessionId, options.runId),
|
|
1840
|
+
readRalplanPlanningStuck(options.cwd, options.sessionId, options.runId),
|
|
1470
1841
|
]);
|
|
1471
1842
|
verdict = lastReviewVerdict;
|
|
1843
|
+
autoHandoff = persistedAutoHandoff;
|
|
1844
|
+
planningStuck = persistedPlanningStuck;
|
|
1472
1845
|
if (rows.length > 0) {
|
|
1473
1846
|
const summary = summarizeRalplanIndex(rows);
|
|
1474
1847
|
iterationFromIndex = summary.iteration;
|
|
@@ -1486,12 +1859,62 @@ async function buildRalplanHud(options: {
|
|
|
1486
1859
|
criticPasses,
|
|
1487
1860
|
reviewPassBudget: options.reviewPassBudget,
|
|
1488
1861
|
verdict,
|
|
1862
|
+
autoHandoff,
|
|
1863
|
+
planningStuck,
|
|
1489
1864
|
pendingApproval: options.pendingApproval,
|
|
1490
1865
|
latestSummary: options.latestSummary,
|
|
1491
1866
|
updatedAt: new Date().toISOString(),
|
|
1492
1867
|
});
|
|
1493
1868
|
}
|
|
1494
1869
|
|
|
1870
|
+
/**
|
|
1871
|
+
* Disposition-stage artifacts are machine-checkable JSON. Validate, enforce
|
|
1872
|
+
* authoritative same-pass provenance against the run index, and re-serialize to
|
|
1873
|
+
* canonical form so join gates and re-review share one shape (#2902 / #3013).
|
|
1874
|
+
*/
|
|
1875
|
+
function normalizeDispositionArtifact(raw: string, expectedStageN: number, indexText: string | undefined): string {
|
|
1876
|
+
try {
|
|
1877
|
+
const indexedArtifacts = buildIndexedReviewArtifacts(indexText);
|
|
1878
|
+
const doc = parseReviewConflictDocument(raw, {
|
|
1879
|
+
expectedStageN,
|
|
1880
|
+
indexedArtifacts,
|
|
1881
|
+
});
|
|
1882
|
+
return serializeReviewConflictDocument(doc);
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
throw new RalplanCommandError(
|
|
1885
|
+
2,
|
|
1886
|
+
`invalid ralplan disposition artifact: ${error instanceof Error ? error.message : String(error)}`,
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/** Parse complete path/sha256 rows from a run `index.jsonl` snapshot for provenance. */
|
|
1892
|
+
function buildIndexedReviewArtifacts(indexText: string | undefined): Map<string, IndexedReviewArtifact> {
|
|
1893
|
+
const map = new Map<string, IndexedReviewArtifact>();
|
|
1894
|
+
if (indexText === undefined) return map;
|
|
1895
|
+
for (const line of indexText.split(/\r?\n/)) {
|
|
1896
|
+
const trimmed = line.trim();
|
|
1897
|
+
if (!trimmed) continue;
|
|
1898
|
+
let row: unknown;
|
|
1899
|
+
try {
|
|
1900
|
+
row = JSON.parse(trimmed);
|
|
1901
|
+
} catch {
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
|
1905
|
+
const record = row as Record<string, unknown>;
|
|
1906
|
+
if (typeof record.stage !== "string") continue;
|
|
1907
|
+
if (typeof record.stage_n !== "number" || !Number.isInteger(record.stage_n)) continue;
|
|
1908
|
+
if (typeof record.path !== "string" || typeof record.sha256 !== "string") continue;
|
|
1909
|
+
// Last complete row for an identity wins (matches findExistingStageArtifact).
|
|
1910
|
+
map.set(reviewArtifactIndexKey(record.stage, record.stage_n), {
|
|
1911
|
+
path: record.path,
|
|
1912
|
+
sha256: record.sha256,
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
return map;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1495
1918
|
async function handleArtifactWrite(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
|
|
1496
1919
|
const resolved = await resolveArtifactArgs(args, cwd);
|
|
1497
1920
|
const persistedRoleState = parsePersistedRoleStateArgs(args, resolved.stage);
|
|
@@ -1514,14 +1937,20 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1514
1937
|
// Non-file / missing path is inline content; resolveArtifactContent already handled it.
|
|
1515
1938
|
}
|
|
1516
1939
|
}
|
|
1517
|
-
const content = resolved.artifact.endsWith("\n") ? resolved.artifact : `${resolved.artifact}\n`;
|
|
1518
|
-
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
1519
1940
|
|
|
1520
|
-
// Read the ledger once before persistence. The dedupe guard
|
|
1521
|
-
// consume this same snapshot so no additional ledger
|
|
1522
|
-
// evaluation and persistence.
|
|
1941
|
+
// Read the ledger once before persistence. The dedupe guard, both gates, and
|
|
1942
|
+
// disposition provenance consume this same snapshot so no additional ledger
|
|
1943
|
+
// read can slip between gate evaluation and persistence.
|
|
1523
1944
|
const indexLoad = await loadRalplanIndexForCap(cwd, resolved.sessionId, resolved.runId);
|
|
1524
1945
|
|
|
1946
|
+
// Disposition stage: fail-closed parse/normalize with authoritative receipts (#2902).
|
|
1947
|
+
const normalizedArtifact =
|
|
1948
|
+
resolved.stage === "disposition"
|
|
1949
|
+
? normalizeDispositionArtifact(resolved.artifact, resolved.stageN, indexLoad.rawText)
|
|
1950
|
+
: resolved.artifact;
|
|
1951
|
+
const content = normalizedArtifact.endsWith("\n") ? normalizedArtifact : `${normalizedArtifact}\n`;
|
|
1952
|
+
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
1953
|
+
|
|
1525
1954
|
// Duplicate-write guard: a second `--write` for the same (stage, stage_n) must not
|
|
1526
1955
|
// silently clobber the artifact or append a duplicate ledger row. Classify before any
|
|
1527
1956
|
// state mutation so a conflict never regresses run-state phase.
|
|
@@ -1534,7 +1963,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1534
1963
|
);
|
|
1535
1964
|
}
|
|
1536
1965
|
if (resolved.stage === "final") await ensureFinalPendingApproval(cwd, resolved, existingArtifact);
|
|
1537
|
-
return buildDeduplicatedResult(resolved, existingArtifact, sha256, cwd, repositoryBinding);
|
|
1966
|
+
return await buildDeduplicatedResult(resolved, existingArtifact, sha256, cwd, repositoryBinding);
|
|
1538
1967
|
}
|
|
1539
1968
|
|
|
1540
1969
|
// `persistArtifact` writes the artifact before its ledger row. If a process crashed
|
|
@@ -1549,7 +1978,12 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1549
1978
|
);
|
|
1550
1979
|
}
|
|
1551
1980
|
if (resolved.stage === "final") await ensureFinalPendingApproval(cwd, resolved, onDiskArtifact);
|
|
1552
|
-
const repairedArtifact = await repairMissingStageArtifactLedger(
|
|
1981
|
+
const repairedArtifact = await repairMissingStageArtifactLedger(
|
|
1982
|
+
cwd,
|
|
1983
|
+
resolved,
|
|
1984
|
+
onDiskArtifact,
|
|
1985
|
+
resolved.stage === "final" ? unavailableRalplanFinalAdmission() : undefined,
|
|
1986
|
+
);
|
|
1553
1987
|
let appliedPersistedRoleState: PersistedRoleStateUpdate | undefined;
|
|
1554
1988
|
if (
|
|
1555
1989
|
persistedRoleState &&
|
|
@@ -1561,7 +1995,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1561
1995
|
if (laneVerdict && (await applyLaneVerdictUpdate(cwd, resolved.sessionId, laneVerdict, resolved.runId))) {
|
|
1562
1996
|
appliedLaneVerdict = laneVerdict;
|
|
1563
1997
|
}
|
|
1564
|
-
return buildDeduplicatedResult(
|
|
1998
|
+
return await buildDeduplicatedResult(
|
|
1565
1999
|
resolved,
|
|
1566
2000
|
repairedArtifact,
|
|
1567
2001
|
sha256,
|
|
@@ -1595,6 +2029,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1595
2029
|
iterationFloor: onDiskOpeners,
|
|
1596
2030
|
});
|
|
1597
2031
|
if (!capDecision.allowed) {
|
|
2032
|
+
await recordRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId, capDecision.reason);
|
|
1598
2033
|
return buildPlanningStuckResult({
|
|
1599
2034
|
json: resolved.json,
|
|
1600
2035
|
stage: resolved.stage,
|
|
@@ -1611,6 +2046,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1611
2046
|
onDiskLaneCounts: onDiskLaneArtifacts,
|
|
1612
2047
|
});
|
|
1613
2048
|
if (!laneBudgetDecision.allowed) {
|
|
2049
|
+
await recordRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId, laneBudgetDecision.reason);
|
|
1614
2050
|
return buildLaneBudgetStuckResult({
|
|
1615
2051
|
json: resolved.json,
|
|
1616
2052
|
stage: resolved.stage,
|
|
@@ -1621,15 +2057,27 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1621
2057
|
});
|
|
1622
2058
|
}
|
|
1623
2059
|
|
|
2060
|
+
let autoHandoff: RalplanAutoHandoffResolution | undefined;
|
|
2061
|
+
if (resolved.stage === "final") {
|
|
2062
|
+
// Resolve and validate the configured admission before any final artifact or
|
|
2063
|
+
// state write. The ledger row below is the durable receipt for deduplicated
|
|
2064
|
+
// retries; state is only a current-session projection.
|
|
2065
|
+
autoHandoff = await resolveRalplanAutoHandoff(cwd, {
|
|
2066
|
+
planningStuck: await readRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId),
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
1624
2069
|
// Keep run-state `current_phase` coherent with the stage being persisted.
|
|
1625
2070
|
await persistActiveRunId(cwd, resolved.sessionId, resolved.runId, resolved.stage);
|
|
1626
|
-
const persisted = await persistArtifact(resolved, cwd, content, sha256);
|
|
2071
|
+
const persisted = await persistArtifact(resolved, cwd, content, sha256, autoHandoff);
|
|
1627
2072
|
if (persistedRoleState) {
|
|
1628
2073
|
await applyPersistedRoleStateUpdate(cwd, resolved.sessionId, persistedRoleState);
|
|
1629
2074
|
}
|
|
1630
2075
|
if (laneVerdict) {
|
|
1631
2076
|
await applyLaneVerdictUpdate(cwd, resolved.sessionId, laneVerdict);
|
|
1632
2077
|
}
|
|
2078
|
+
if (autoHandoff) {
|
|
2079
|
+
await persistRalplanFinalAdmission(cwd, resolved.sessionId, resolved.runId, autoHandoff);
|
|
2080
|
+
}
|
|
1633
2081
|
await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "ralplan-runtime", path: persisted.path });
|
|
1634
2082
|
await syncRalplanHud({
|
|
1635
2083
|
cwd,
|
|
@@ -1651,6 +2099,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1651
2099
|
: undefined;
|
|
1652
2100
|
|
|
1653
2101
|
const payload: Record<string, unknown> = {
|
|
2102
|
+
session_id: resolved.sessionId,
|
|
1654
2103
|
run_id: persisted.runId,
|
|
1655
2104
|
path: persisted.path,
|
|
1656
2105
|
stage: persisted.stage,
|
|
@@ -1663,6 +2112,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1663
2112
|
if (persistedRoleState) payload[`${persistedRoleState.role}_state`] = persistedRoleStatePayload(persistedRoleState);
|
|
1664
2113
|
if (reviewBudgetWarning) payload.review_budget_warning = reviewBudgetWarning;
|
|
1665
2114
|
if (laneVerdict) payload.lane_verdict = { lane: laneVerdict.lane, verdict: laneVerdict.verdict };
|
|
2115
|
+
if (autoHandoff) payload.auto_handoff = autoHandoff;
|
|
1666
2116
|
|
|
1667
2117
|
const stdout = resolved.json
|
|
1668
2118
|
? `${JSON.stringify(payload, null, 2)}\n`
|
|
@@ -1675,7 +2125,14 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1675
2125
|
* do not rewrite artifacts, append rows, or churn run state; a crash-gap repair may
|
|
1676
2126
|
* complete riding persisted role/lane metadata before returning this receipt.
|
|
1677
2127
|
*/
|
|
1678
|
-
function
|
|
2128
|
+
function applyRalplanPlanningStuckOverride(
|
|
2129
|
+
admission: RalplanAutoHandoffResolution,
|
|
2130
|
+
planningStuck: boolean,
|
|
2131
|
+
): RalplanAutoHandoffResolution {
|
|
2132
|
+
return planningStuck ? { ...admission, effectiveTarget: "off", degradationReason: "planning_stuck" } : admission;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
async function buildDeduplicatedResult(
|
|
1679
2136
|
resolved: ResolvedArtifactArgs,
|
|
1680
2137
|
existing: ExistingStageArtifact,
|
|
1681
2138
|
sha256: string,
|
|
@@ -1683,8 +2140,9 @@ function buildDeduplicatedResult(
|
|
|
1683
2140
|
repositoryBinding: RepositoryBinding,
|
|
1684
2141
|
laneVerdict?: LaneVerdictUpdate,
|
|
1685
2142
|
persistedRoleState?: PersistedRoleStateUpdate,
|
|
1686
|
-
): RalplanCommandResult {
|
|
2143
|
+
): Promise<RalplanCommandResult> {
|
|
1687
2144
|
const payload: Record<string, unknown> = {
|
|
2145
|
+
session_id: resolved.sessionId,
|
|
1688
2146
|
run_id: resolved.runId,
|
|
1689
2147
|
path: existing.path,
|
|
1690
2148
|
stage: resolved.stage,
|
|
@@ -1705,6 +2163,11 @@ function buildDeduplicatedResult(
|
|
|
1705
2163
|
resolved.runId,
|
|
1706
2164
|
"pending-approval.md",
|
|
1707
2165
|
);
|
|
2166
|
+
const planningStuck = await readRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId);
|
|
2167
|
+
payload.auto_handoff = applyRalplanPlanningStuckOverride(
|
|
2168
|
+
existing.autoHandoff ?? unavailableRalplanFinalAdmission(),
|
|
2169
|
+
planningStuck,
|
|
2170
|
+
);
|
|
1708
2171
|
}
|
|
1709
2172
|
const stdout = resolved.json
|
|
1710
2173
|
? `${JSON.stringify(payload, null, 2)}\n`
|
|
@@ -1851,6 +2314,7 @@ async function handleConsensusHandoff(args: readonly string[], cwd: string): Pro
|
|
|
1851
2314
|
});
|
|
1852
2315
|
|
|
1853
2316
|
const summary = {
|
|
2317
|
+
session_id: resolved.sessionId,
|
|
1854
2318
|
skill: "ralplan",
|
|
1855
2319
|
mode,
|
|
1856
2320
|
state_path: statePath,
|