@gajae-code/coding-agent 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -1
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/config/settings-schema.d.ts +54 -0
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +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-runtime.d.ts +18 -0
- 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 +31 -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/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/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 +4 -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 +11 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/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 +38 -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 +8 -1
- 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 +3 -1
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +7 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +4 -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/generate-sdk-operation-inventory.ts +3 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +285 -0
- package/src/async/job-manager.ts +78 -20
- 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 +51 -0
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +19 -16
- package/src/defaults/gjc/skills/team/SKILL.md +2 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -24
- 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/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/ralplan-runtime.ts +411 -17
- 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-runtime.ts +401 -611
- package/src/gjc-runtime/workflow-manifest.generated.json +8 -99
- package/src/gjc-runtime/workflow-manifest.ts +4 -68
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/acp-agent.ts +298 -186
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- 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 -14
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/extension-ui-controller.ts +7 -0
- package/src/modes/controllers/selector-controller.ts +45 -0
- package/src/modes/runtime-init.ts +3 -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/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +6 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/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 +11 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/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 +22 -0
- package/src/sdk/session.ts +88 -9
- package/src/session/agent-session.ts +822 -172
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +44 -6
- package/src/session/internal/managed-session-storage.ts +48 -6
- package/src/session/session-manager.ts +1614 -435
- package/src/session/session-storage.ts +9 -1
- 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 +23 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +23 -7
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +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 +4 -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";
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
RepositoryBindingError,
|
|
23
24
|
} from "./repository-binding";
|
|
24
25
|
import { GJC_RALPLAN_ARTIFACT_ENV, isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
|
|
25
|
-
import { gjcRoot, modeStatePath, sessionPlansDir } from "./session-layout";
|
|
26
|
+
import { gjcRoot, modeStatePath, sessionIdFromDirName, sessionPlansDir } from "./session-layout";
|
|
26
27
|
import { resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
|
|
27
28
|
import { migrateWorkflowState } from "./state-migrations";
|
|
28
29
|
import { runNativeStateCommand } from "./state-runtime";
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
writeArtifact,
|
|
34
35
|
writeWorkflowEnvelopeAtomic,
|
|
35
36
|
} from "./state-writer";
|
|
37
|
+
import { probeGjcTeamAvailability } from "./team-runtime";
|
|
36
38
|
import { assertSafePathComponent, CommandError, flagValue, hasFlag } from "./workflow-cli-common";
|
|
37
39
|
import { getSkillManifest } from "./workflow-manifest";
|
|
38
40
|
|
|
@@ -74,6 +76,16 @@ export const PLANNING_STUCK_MARKER = "PLANNING-STUCK";
|
|
|
74
76
|
export const RALPLAN_DEFAULT_MAX_REVIEW_PASSES_PER_LANE = 1;
|
|
75
77
|
/** Inclusive upper bound for `gjc.ralplan.maxReviewPassesPerLane` settings overrides. */
|
|
76
78
|
export const RALPLAN_MAX_REVIEW_PASSES_PER_LANE_LIMIT = 10;
|
|
79
|
+
export type RalplanAutoHandoffTarget = "off" | "ultragoal" | "team";
|
|
80
|
+
|
|
81
|
+
export interface RalplanAutoHandoffResolution {
|
|
82
|
+
configuredTarget: RalplanAutoHandoffTarget;
|
|
83
|
+
effectiveTarget: RalplanAutoHandoffTarget;
|
|
84
|
+
degradationReason: string | null;
|
|
85
|
+
source: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const RALPLAN_AUTO_HANDOFF_TARGETS = new Set<RalplanAutoHandoffTarget>(["off", "ultragoal", "team"]);
|
|
77
89
|
|
|
78
90
|
const RALPLAN_ITERATION_OPENER_STAGES = new Set<RalplanStage>(["planner", "revision"]);
|
|
79
91
|
|
|
@@ -391,6 +403,115 @@ export async function resolveRalplanMaxIterations(cwd: string): Promise<{ maxIte
|
|
|
391
403
|
if (user !== null) return { maxIterations: user, source: userPath };
|
|
392
404
|
return { maxIterations: RALPLAN_DEFAULT_MAX_ITERATIONS, source: "default" };
|
|
393
405
|
}
|
|
406
|
+
function parseRalplanAutoHandoffTarget(value: unknown): RalplanAutoHandoffTarget | undefined {
|
|
407
|
+
return typeof value === "string" && RALPLAN_AUTO_HANDOFF_TARGETS.has(value as RalplanAutoHandoffTarget)
|
|
408
|
+
? (value as RalplanAutoHandoffTarget)
|
|
409
|
+
: undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
type RalplanAutoHandoffOptions = {
|
|
413
|
+
planningStuck?: boolean;
|
|
414
|
+
teamAvailabilityProbe?: () => { available: true } | { available: false; reason: string };
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
type RalplanAutoHandoffSetting =
|
|
418
|
+
| { kind: "absent" }
|
|
419
|
+
| { kind: "valid"; value: RalplanAutoHandoffTarget }
|
|
420
|
+
| { kind: "invalid"; reason: string };
|
|
421
|
+
|
|
422
|
+
function parsePresentRalplanAutoHandoff(value: unknown): RalplanAutoHandoffSetting {
|
|
423
|
+
const target = parseRalplanAutoHandoffTarget(value);
|
|
424
|
+
return target === undefined
|
|
425
|
+
? {
|
|
426
|
+
kind: "invalid",
|
|
427
|
+
reason: "expected gjc.ralplan.autoHandoff to be one of off, ultragoal, team",
|
|
428
|
+
}
|
|
429
|
+
: { kind: "valid", value: target };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function parseRalplanAutoHandoffSettings(parsed: unknown): RalplanAutoHandoffSetting {
|
|
433
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { kind: "absent" };
|
|
434
|
+
const settings = parsed as Record<string, unknown>;
|
|
435
|
+
if (Object.hasOwn(settings, "gjc.ralplan.autoHandoff")) {
|
|
436
|
+
return parsePresentRalplanAutoHandoff(settings["gjc.ralplan.autoHandoff"]);
|
|
437
|
+
}
|
|
438
|
+
const gjc = settings.gjc;
|
|
439
|
+
if (!gjc || typeof gjc !== "object" || Array.isArray(gjc)) return { kind: "absent" };
|
|
440
|
+
const ralplan = (gjc as Record<string, unknown>).ralplan;
|
|
441
|
+
if (!ralplan || typeof ralplan !== "object" || Array.isArray(ralplan)) return { kind: "absent" };
|
|
442
|
+
const ralplanSettings = ralplan as Record<string, unknown>;
|
|
443
|
+
if (!Object.hasOwn(ralplanSettings, "autoHandoff")) return { kind: "absent" };
|
|
444
|
+
return parsePresentRalplanAutoHandoff(ralplanSettings.autoHandoff);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function readSettingsAutoHandoff(settingsPath: string): Promise<RalplanAutoHandoffSetting> {
|
|
448
|
+
let raw: string;
|
|
449
|
+
try {
|
|
450
|
+
raw = await Bun.file(settingsPath).text();
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (getErrorCode(error) === "ENOENT") return { kind: "absent" };
|
|
453
|
+
return {
|
|
454
|
+
kind: "invalid",
|
|
455
|
+
reason: `unable to read settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
let parsed: unknown;
|
|
459
|
+
try {
|
|
460
|
+
parsed = JSON.parse(raw);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
return {
|
|
463
|
+
kind: "invalid",
|
|
464
|
+
reason: `malformed JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return parseRalplanAutoHandoffSettings(parsed);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export async function resolveRalplanAutoHandoff(
|
|
471
|
+
cwd: string,
|
|
472
|
+
options: RalplanAutoHandoffOptions = {},
|
|
473
|
+
): Promise<RalplanAutoHandoffResolution> {
|
|
474
|
+
const projectPath = path.join(gjcRoot(cwd), "settings.json");
|
|
475
|
+
const project = await readSettingsAutoHandoff(projectPath);
|
|
476
|
+
if (project.kind === "invalid") {
|
|
477
|
+
throw new RalplanCommandError(2, `invalid ralplan settings at ${projectPath}: ${project.reason}`);
|
|
478
|
+
}
|
|
479
|
+
if (project.kind === "valid") {
|
|
480
|
+
return resolveRalplanAutoHandoffTarget(project.value, projectPath, options);
|
|
481
|
+
}
|
|
482
|
+
const userPath = path.join(getConfigRootDir(), "settings.json");
|
|
483
|
+
const user = await readSettingsAutoHandoff(userPath);
|
|
484
|
+
if (user.kind === "invalid") {
|
|
485
|
+
throw new RalplanCommandError(2, `invalid ralplan settings at ${userPath}: ${user.reason}`);
|
|
486
|
+
}
|
|
487
|
+
return resolveRalplanAutoHandoffTarget(
|
|
488
|
+
user.kind === "valid" ? user.value : "off",
|
|
489
|
+
user.kind === "valid" ? userPath : "default",
|
|
490
|
+
options,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function resolveRalplanAutoHandoffTarget(
|
|
495
|
+
configuredTarget: RalplanAutoHandoffTarget,
|
|
496
|
+
source: string,
|
|
497
|
+
options: RalplanAutoHandoffOptions,
|
|
498
|
+
): RalplanAutoHandoffResolution {
|
|
499
|
+
if (options.planningStuck) {
|
|
500
|
+
return { configuredTarget, effectiveTarget: "off", degradationReason: "planning_stuck", source };
|
|
501
|
+
}
|
|
502
|
+
if (configuredTarget !== "team")
|
|
503
|
+
return { configuredTarget, effectiveTarget: configuredTarget, degradationReason: null, source };
|
|
504
|
+
|
|
505
|
+
const availability = (options.teamAvailabilityProbe ?? probeGjcTeamAvailability)();
|
|
506
|
+
return availability.available
|
|
507
|
+
? { configuredTarget, effectiveTarget: "team", degradationReason: null, source }
|
|
508
|
+
: {
|
|
509
|
+
configuredTarget,
|
|
510
|
+
effectiveTarget: "off",
|
|
511
|
+
degradationReason: `team_unavailable:${availability.reason}`,
|
|
512
|
+
source,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
394
515
|
|
|
395
516
|
function parseMaxReviewPassesPerLaneValue(value: unknown): number | null {
|
|
396
517
|
return parseBoundedPositiveInteger(value, RALPLAN_MAX_REVIEW_PASSES_PER_LANE_LIMIT);
|
|
@@ -763,6 +884,8 @@ async function persistActiveRunId(cwd: string, sessionId: string, runId: string,
|
|
|
763
884
|
for (const key of Object.keys(existing)) {
|
|
764
885
|
if (key.startsWith("last_review_verdict")) delete existing[key];
|
|
765
886
|
}
|
|
887
|
+
delete existing.planning_stuck;
|
|
888
|
+
delete existing.auto_handoff;
|
|
766
889
|
}
|
|
767
890
|
if (
|
|
768
891
|
existing.run_id === runId &&
|
|
@@ -1094,6 +1217,194 @@ async function applyLaneVerdictUpdate(
|
|
|
1094
1217
|
{ cwd },
|
|
1095
1218
|
);
|
|
1096
1219
|
}
|
|
1220
|
+
function ralplanPlanningStuckIndexKey(entry: unknown): string | undefined {
|
|
1221
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return undefined;
|
|
1222
|
+
const record = entry as Record<string, unknown>;
|
|
1223
|
+
return record.planning_stuck === true ? "planning_stuck" : undefined;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
async function recordRalplanPlanningStuck(
|
|
1227
|
+
cwd: string,
|
|
1228
|
+
sessionId: string,
|
|
1229
|
+
runId: string,
|
|
1230
|
+
reason: string,
|
|
1231
|
+
): Promise<void> {
|
|
1232
|
+
const runDir = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId);
|
|
1233
|
+
await appendJsonlIdempotent(
|
|
1234
|
+
path.join(runDir, "index.jsonl"),
|
|
1235
|
+
{
|
|
1236
|
+
event: "planning_stuck",
|
|
1237
|
+
planning_stuck: true,
|
|
1238
|
+
marker: PLANNING_STUCK_MARKER,
|
|
1239
|
+
reason,
|
|
1240
|
+
created_at: new Date().toISOString(),
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
cwd,
|
|
1244
|
+
audit: {
|
|
1245
|
+
category: "ledger",
|
|
1246
|
+
verb: "append",
|
|
1247
|
+
owner: "gjc-runtime",
|
|
1248
|
+
skill: "ralplan",
|
|
1249
|
+
sessionId,
|
|
1250
|
+
},
|
|
1251
|
+
key: ralplanPlanningStuckIndexKey,
|
|
1252
|
+
},
|
|
1253
|
+
);
|
|
1254
|
+
const statePath = ralplanStatePath(cwd, sessionId);
|
|
1255
|
+
await withWorkflowStateLock(
|
|
1256
|
+
statePath,
|
|
1257
|
+
async () => {
|
|
1258
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
1259
|
+
if (existingRead.kind === "corrupt") return;
|
|
1260
|
+
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1261
|
+
if (existing.run_id !== runId) return;
|
|
1262
|
+
existing.planning_stuck = { marker: PLANNING_STUCK_MARKER, reason };
|
|
1263
|
+
existing = migrateWorkflowState(existing, "ralplan").state;
|
|
1264
|
+
existing.updated_at = new Date().toISOString();
|
|
1265
|
+
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
1266
|
+
cwd,
|
|
1267
|
+
lockHeld: true,
|
|
1268
|
+
receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan planning-stuck", sessionId },
|
|
1269
|
+
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
|
|
1270
|
+
});
|
|
1271
|
+
},
|
|
1272
|
+
{ cwd },
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
async function readRalplanFinalAdmission(
|
|
1277
|
+
cwd: string,
|
|
1278
|
+
sessionId: string,
|
|
1279
|
+
runId: string,
|
|
1280
|
+
): Promise<RalplanAutoHandoffResolution | undefined> {
|
|
1281
|
+
const index = await loadRalplanIndexForCap(cwd, sessionId, runId);
|
|
1282
|
+
if (index.rawText === undefined) return undefined;
|
|
1283
|
+
let finalAdmission: RalplanAutoHandoffResolution | undefined;
|
|
1284
|
+
for (const line of index.rawText.split(/\r?\n/)) {
|
|
1285
|
+
try {
|
|
1286
|
+
const row = JSON.parse(line) as Record<string, unknown>;
|
|
1287
|
+
if (row.stage === "final" && typeof row.path === "string" && typeof row.sha256 === "string") {
|
|
1288
|
+
finalAdmission = parseRalplanFinalAdmission(row.auto_handoff) ?? unavailableRalplanFinalAdmission();
|
|
1289
|
+
}
|
|
1290
|
+
} catch {
|
|
1291
|
+
// The artifact dedupe guard handles malformed ledger records separately.
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return finalAdmission;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
async function readRalplanPlanningStuck(cwd: string, sessionId: string, runId: string): Promise<boolean> {
|
|
1298
|
+
const index = await loadRalplanIndexForCap(cwd, sessionId, runId);
|
|
1299
|
+
if (index.rawText === undefined) return index.indexPresent;
|
|
1300
|
+
if (index.indexPresent && index.rawLineCount > 0 && index.parseableLines === 0) return true;
|
|
1301
|
+
for (const line of index.rawText.split(/\r?\n/)) {
|
|
1302
|
+
if (!line.trim()) continue;
|
|
1303
|
+
try {
|
|
1304
|
+
const row = JSON.parse(line) as Record<string, unknown>;
|
|
1305
|
+
if (row.planning_stuck === true) return true;
|
|
1306
|
+
} catch {
|
|
1307
|
+
return true;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
async function persistRalplanFinalAdmission(
|
|
1314
|
+
cwd: string,
|
|
1315
|
+
sessionId: string,
|
|
1316
|
+
runId: string,
|
|
1317
|
+
admission: RalplanAutoHandoffResolution,
|
|
1318
|
+
): Promise<void> {
|
|
1319
|
+
const statePath = ralplanStatePath(cwd, sessionId);
|
|
1320
|
+
await withWorkflowStateLock(
|
|
1321
|
+
statePath,
|
|
1322
|
+
async () => {
|
|
1323
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
1324
|
+
if (existingRead.kind === "corrupt") {
|
|
1325
|
+
throw new RalplanCommandError(
|
|
1326
|
+
2,
|
|
1327
|
+
`existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to record final admission`,
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1331
|
+
if (existing.run_id !== runId) return;
|
|
1332
|
+
existing.auto_handoff = admission;
|
|
1333
|
+
existing = migrateWorkflowState(existing, "ralplan").state;
|
|
1334
|
+
existing.updated_at = new Date().toISOString();
|
|
1335
|
+
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
1336
|
+
cwd,
|
|
1337
|
+
lockHeld: true,
|
|
1338
|
+
receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan final-admission", sessionId },
|
|
1339
|
+
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
|
|
1340
|
+
});
|
|
1341
|
+
},
|
|
1342
|
+
{ cwd },
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
async function findExistingRalplanRunOwners(cwd: string, runId: string): Promise<string[]> {
|
|
1347
|
+
let entries: Dirent<string>[];
|
|
1348
|
+
try {
|
|
1349
|
+
entries = await fs.readdir(gjcRoot(cwd), { withFileTypes: true });
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
const err = error as NodeJS.ErrnoException;
|
|
1352
|
+
if (err.code === "ENOENT" || err.code === "ENOTDIR") return [];
|
|
1353
|
+
throw error;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
const owners = await Promise.all(
|
|
1357
|
+
entries.map(async entry => {
|
|
1358
|
+
if (!entry.isDirectory()) return undefined;
|
|
1359
|
+
const sessionId = sessionIdFromDirName(entry.name);
|
|
1360
|
+
if (!sessionId) return undefined;
|
|
1361
|
+
|
|
1362
|
+
const stateRead = await readExistingStateForMutation(ralplanStatePath(cwd, sessionId));
|
|
1363
|
+
const stateOwnsRun =
|
|
1364
|
+
stateRead.kind === "valid" &&
|
|
1365
|
+
typeof stateRead.value.run_id === "string" &&
|
|
1366
|
+
stateRead.value.run_id.trim() === runId;
|
|
1367
|
+
if (stateOwnsRun) return sessionId;
|
|
1368
|
+
|
|
1369
|
+
try {
|
|
1370
|
+
const stat = await fs.stat(path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId));
|
|
1371
|
+
return stat.isDirectory() ? sessionId : undefined;
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
const err = error as NodeJS.ErrnoException;
|
|
1374
|
+
if (err.code === "ENOENT" || err.code === "ENOTDIR") return undefined;
|
|
1375
|
+
throw error;
|
|
1376
|
+
}
|
|
1377
|
+
}),
|
|
1378
|
+
);
|
|
1379
|
+
return owners.filter((owner): owner is string => owner !== undefined).sort();
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
async function resolveArtifactSessionId(args: readonly string[], cwd: string, explicitRunId: string | undefined) {
|
|
1383
|
+
const flagSessionId = flagValue(args, "--session-id");
|
|
1384
|
+
const currentSession = resolveGjcSessionForWrite(cwd, {
|
|
1385
|
+
flagValue: flagSessionId,
|
|
1386
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
1387
|
+
});
|
|
1388
|
+
if (!explicitRunId) return currentSession.gjcSessionId;
|
|
1389
|
+
|
|
1390
|
+
const owners = await findExistingRalplanRunOwners(cwd, explicitRunId);
|
|
1391
|
+
if (owners.length === 0) return currentSession.gjcSessionId;
|
|
1392
|
+
if (owners.length > 1) {
|
|
1393
|
+
throw new RalplanCommandError(
|
|
1394
|
+
2,
|
|
1395
|
+
`ralplan run ${explicitRunId} has multiple owner sessions (${owners.join(", ")}); repair the fragmented run before writing`,
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
const ownerSessionId = owners[0]!;
|
|
1400
|
+
if (flagSessionId !== undefined && currentSession.gjcSessionId !== ownerSessionId) {
|
|
1401
|
+
throw new RalplanCommandError(
|
|
1402
|
+
2,
|
|
1403
|
+
`ralplan run ${explicitRunId} is owned by session ${ownerSessionId}, not ${currentSession.gjcSessionId}`,
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
return ownerSessionId;
|
|
1407
|
+
}
|
|
1097
1408
|
|
|
1098
1409
|
async function resolveArtifactArgs(args: readonly string[], cwd: string): Promise<ResolvedArtifactArgs> {
|
|
1099
1410
|
const stage = flagValue(args, "--stage");
|
|
@@ -1115,20 +1426,18 @@ async function resolveArtifactArgs(args: readonly string[], cwd: string): Promis
|
|
|
1115
1426
|
throw new RalplanCommandError(2, `--artifact-env must be ${GJC_RALPLAN_ARTIFACT_ENV}`);
|
|
1116
1427
|
}
|
|
1117
1428
|
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
const sessionId = session.gjcSessionId;
|
|
1429
|
+
const explicitRunId = flagValue(args, "--run-id")?.trim();
|
|
1430
|
+
if (explicitRunId) assertSafePathComponent(explicitRunId, "run-id");
|
|
1431
|
+
|
|
1432
|
+
const sessionId = await resolveArtifactSessionId(args, cwd, explicitRunId);
|
|
1123
1433
|
assertSafePathComponent(sessionId, "session-id");
|
|
1124
1434
|
const sessionIdRaw = sessionId;
|
|
1125
1435
|
|
|
1126
1436
|
// Precedence for run_id:
|
|
1127
1437
|
// 1. explicit --run-id flag
|
|
1128
|
-
// 2. existing run_id field in
|
|
1129
|
-
// 3.
|
|
1438
|
+
// 2. existing run_id field in the resolved owner session's ralplan state
|
|
1439
|
+
// 3. resolved owner session id
|
|
1130
1440
|
// 4. freshly generated default run id
|
|
1131
|
-
const explicitRunId = flagValue(args, "--run-id")?.trim();
|
|
1132
1441
|
const runId = explicitRunId || (await readActiveRunId(cwd, sessionId)) || sessionIdRaw || defaultRunId();
|
|
1133
1442
|
assertSafePathComponent(runId, "run-id");
|
|
1134
1443
|
|
|
@@ -1170,6 +1479,7 @@ async function persistArtifact(
|
|
|
1170
1479
|
cwd: string,
|
|
1171
1480
|
content: string,
|
|
1172
1481
|
sha256: string,
|
|
1482
|
+
finalAdmission?: RalplanAutoHandoffResolution,
|
|
1173
1483
|
): Promise<PersistedArtifact> {
|
|
1174
1484
|
const runDir = path.join(sessionPlansDir(cwd, resolved.sessionId), "ralplan", resolved.runId);
|
|
1175
1485
|
|
|
@@ -1193,6 +1503,7 @@ async function persistArtifact(
|
|
|
1193
1503
|
path: filePath,
|
|
1194
1504
|
created_at: createdAt,
|
|
1195
1505
|
sha256,
|
|
1506
|
+
...(finalAdmission ? { auto_handoff: finalAdmission } : {}),
|
|
1196
1507
|
};
|
|
1197
1508
|
await appendJsonlIdempotent(path.join(runDir, "index.jsonl"), indexEntry, {
|
|
1198
1509
|
cwd,
|
|
@@ -1237,6 +1548,35 @@ interface ExistingStageArtifact {
|
|
|
1237
1548
|
path: string;
|
|
1238
1549
|
sha256: string;
|
|
1239
1550
|
createdAt: string;
|
|
1551
|
+
autoHandoff?: RalplanAutoHandoffResolution;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function parseRalplanFinalAdmission(value: unknown): RalplanAutoHandoffResolution | undefined {
|
|
1555
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1556
|
+
const admission = value as Record<string, unknown>;
|
|
1557
|
+
if (
|
|
1558
|
+
!RALPLAN_AUTO_HANDOFF_TARGETS.has(admission.configuredTarget as RalplanAutoHandoffTarget) ||
|
|
1559
|
+
!RALPLAN_AUTO_HANDOFF_TARGETS.has(admission.effectiveTarget as RalplanAutoHandoffTarget) ||
|
|
1560
|
+
(typeof admission.degradationReason !== "string" && admission.degradationReason !== null) ||
|
|
1561
|
+
typeof admission.source !== "string"
|
|
1562
|
+
) {
|
|
1563
|
+
return undefined;
|
|
1564
|
+
}
|
|
1565
|
+
return {
|
|
1566
|
+
configuredTarget: admission.configuredTarget as RalplanAutoHandoffTarget,
|
|
1567
|
+
effectiveTarget: admission.effectiveTarget as RalplanAutoHandoffTarget,
|
|
1568
|
+
degradationReason: typeof admission.degradationReason === "string" ? admission.degradationReason : null,
|
|
1569
|
+
source: admission.source,
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function unavailableRalplanFinalAdmission(): RalplanAutoHandoffResolution {
|
|
1574
|
+
return {
|
|
1575
|
+
configuredTarget: "off",
|
|
1576
|
+
effectiveTarget: "off",
|
|
1577
|
+
degradationReason: "admission_unavailable",
|
|
1578
|
+
source: "ledger",
|
|
1579
|
+
};
|
|
1240
1580
|
}
|
|
1241
1581
|
|
|
1242
1582
|
/**
|
|
@@ -1269,6 +1609,7 @@ function findExistingStageArtifact(
|
|
|
1269
1609
|
path: record.path,
|
|
1270
1610
|
sha256: record.sha256,
|
|
1271
1611
|
createdAt: typeof record.created_at === "string" ? record.created_at : "",
|
|
1612
|
+
...(stage === "final" ? { autoHandoff: parseRalplanFinalAdmission(record.auto_handoff) } : {}),
|
|
1272
1613
|
};
|
|
1273
1614
|
}
|
|
1274
1615
|
return match;
|
|
@@ -1354,6 +1695,7 @@ async function repairMissingStageArtifactLedger(
|
|
|
1354
1695
|
cwd: string,
|
|
1355
1696
|
resolved: Pick<ResolvedArtifactArgs, "sessionId" | "runId" | "stage" | "stageN">,
|
|
1356
1697
|
onDisk: OnDiskStageArtifact,
|
|
1698
|
+
finalAdmission?: RalplanAutoHandoffResolution,
|
|
1357
1699
|
): Promise<ExistingStageArtifact> {
|
|
1358
1700
|
const createdAt = new Date().toISOString();
|
|
1359
1701
|
const indexEntry = {
|
|
@@ -1362,6 +1704,7 @@ async function repairMissingStageArtifactLedger(
|
|
|
1362
1704
|
path: onDisk.path,
|
|
1363
1705
|
created_at: createdAt,
|
|
1364
1706
|
sha256: onDisk.sha256,
|
|
1707
|
+
...(finalAdmission ? { auto_handoff: finalAdmission } : {}),
|
|
1365
1708
|
};
|
|
1366
1709
|
const result = await appendJsonlIdempotent(
|
|
1367
1710
|
path.join(sessionPlansDir(cwd, resolved.sessionId), "ralplan", resolved.runId, "index.jsonl"),
|
|
@@ -1386,10 +1729,18 @@ async function repairMissingStageArtifactLedger(
|
|
|
1386
1729
|
path: record.path,
|
|
1387
1730
|
sha256: record.sha256,
|
|
1388
1731
|
createdAt: typeof record.created_at === "string" ? record.created_at : createdAt,
|
|
1732
|
+
...(resolved.stage === "final"
|
|
1733
|
+
? { autoHandoff: parseRalplanFinalAdmission(record.auto_handoff) ?? unavailableRalplanFinalAdmission() }
|
|
1734
|
+
: {}),
|
|
1389
1735
|
};
|
|
1390
1736
|
}
|
|
1391
1737
|
}
|
|
1392
|
-
return {
|
|
1738
|
+
return {
|
|
1739
|
+
path: onDisk.path,
|
|
1740
|
+
sha256: onDisk.sha256,
|
|
1741
|
+
createdAt,
|
|
1742
|
+
...(resolved.stage === "final" ? { autoHandoff: finalAdmission ?? unavailableRalplanFinalAdmission() } : {}),
|
|
1743
|
+
};
|
|
1393
1744
|
}
|
|
1394
1745
|
|
|
1395
1746
|
/**
|
|
@@ -1463,12 +1814,18 @@ async function buildRalplanHud(options: {
|
|
|
1463
1814
|
let architectPasses: number | undefined;
|
|
1464
1815
|
let criticPasses: number | undefined;
|
|
1465
1816
|
let verdict: string | undefined;
|
|
1817
|
+
let autoHandoff: RalplanAutoHandoffResolution | undefined;
|
|
1818
|
+
let planningStuck = false;
|
|
1466
1819
|
if (options.runId && options.sessionId) {
|
|
1467
|
-
const [rows, lastReviewVerdict] = await Promise.all([
|
|
1820
|
+
const [rows, lastReviewVerdict, persistedAutoHandoff, persistedPlanningStuck] = await Promise.all([
|
|
1468
1821
|
readRalplanIndexRows(options.cwd, options.sessionId, options.runId),
|
|
1469
1822
|
readRalplanLastReviewVerdict(options.cwd, options.sessionId),
|
|
1823
|
+
readRalplanFinalAdmission(options.cwd, options.sessionId, options.runId),
|
|
1824
|
+
readRalplanPlanningStuck(options.cwd, options.sessionId, options.runId),
|
|
1470
1825
|
]);
|
|
1471
1826
|
verdict = lastReviewVerdict;
|
|
1827
|
+
autoHandoff = persistedAutoHandoff;
|
|
1828
|
+
planningStuck = persistedPlanningStuck;
|
|
1472
1829
|
if (rows.length > 0) {
|
|
1473
1830
|
const summary = summarizeRalplanIndex(rows);
|
|
1474
1831
|
iterationFromIndex = summary.iteration;
|
|
@@ -1486,6 +1843,8 @@ async function buildRalplanHud(options: {
|
|
|
1486
1843
|
criticPasses,
|
|
1487
1844
|
reviewPassBudget: options.reviewPassBudget,
|
|
1488
1845
|
verdict,
|
|
1846
|
+
autoHandoff,
|
|
1847
|
+
planningStuck,
|
|
1489
1848
|
pendingApproval: options.pendingApproval,
|
|
1490
1849
|
latestSummary: options.latestSummary,
|
|
1491
1850
|
updatedAt: new Date().toISOString(),
|
|
@@ -1534,7 +1893,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1534
1893
|
);
|
|
1535
1894
|
}
|
|
1536
1895
|
if (resolved.stage === "final") await ensureFinalPendingApproval(cwd, resolved, existingArtifact);
|
|
1537
|
-
return buildDeduplicatedResult(resolved, existingArtifact, sha256, cwd, repositoryBinding);
|
|
1896
|
+
return await buildDeduplicatedResult(resolved, existingArtifact, sha256, cwd, repositoryBinding);
|
|
1538
1897
|
}
|
|
1539
1898
|
|
|
1540
1899
|
// `persistArtifact` writes the artifact before its ledger row. If a process crashed
|
|
@@ -1549,7 +1908,12 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1549
1908
|
);
|
|
1550
1909
|
}
|
|
1551
1910
|
if (resolved.stage === "final") await ensureFinalPendingApproval(cwd, resolved, onDiskArtifact);
|
|
1552
|
-
const repairedArtifact = await repairMissingStageArtifactLedger(
|
|
1911
|
+
const repairedArtifact = await repairMissingStageArtifactLedger(
|
|
1912
|
+
cwd,
|
|
1913
|
+
resolved,
|
|
1914
|
+
onDiskArtifact,
|
|
1915
|
+
resolved.stage === "final" ? unavailableRalplanFinalAdmission() : undefined,
|
|
1916
|
+
);
|
|
1553
1917
|
let appliedPersistedRoleState: PersistedRoleStateUpdate | undefined;
|
|
1554
1918
|
if (
|
|
1555
1919
|
persistedRoleState &&
|
|
@@ -1561,7 +1925,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1561
1925
|
if (laneVerdict && (await applyLaneVerdictUpdate(cwd, resolved.sessionId, laneVerdict, resolved.runId))) {
|
|
1562
1926
|
appliedLaneVerdict = laneVerdict;
|
|
1563
1927
|
}
|
|
1564
|
-
return buildDeduplicatedResult(
|
|
1928
|
+
return await buildDeduplicatedResult(
|
|
1565
1929
|
resolved,
|
|
1566
1930
|
repairedArtifact,
|
|
1567
1931
|
sha256,
|
|
@@ -1595,6 +1959,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1595
1959
|
iterationFloor: onDiskOpeners,
|
|
1596
1960
|
});
|
|
1597
1961
|
if (!capDecision.allowed) {
|
|
1962
|
+
await recordRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId, capDecision.reason);
|
|
1598
1963
|
return buildPlanningStuckResult({
|
|
1599
1964
|
json: resolved.json,
|
|
1600
1965
|
stage: resolved.stage,
|
|
@@ -1611,6 +1976,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1611
1976
|
onDiskLaneCounts: onDiskLaneArtifacts,
|
|
1612
1977
|
});
|
|
1613
1978
|
if (!laneBudgetDecision.allowed) {
|
|
1979
|
+
await recordRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId, laneBudgetDecision.reason);
|
|
1614
1980
|
return buildLaneBudgetStuckResult({
|
|
1615
1981
|
json: resolved.json,
|
|
1616
1982
|
stage: resolved.stage,
|
|
@@ -1621,15 +1987,27 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1621
1987
|
});
|
|
1622
1988
|
}
|
|
1623
1989
|
|
|
1990
|
+
let autoHandoff: RalplanAutoHandoffResolution | undefined;
|
|
1991
|
+
if (resolved.stage === "final") {
|
|
1992
|
+
// Resolve and validate the configured admission before any final artifact or
|
|
1993
|
+
// state write. The ledger row below is the durable receipt for deduplicated
|
|
1994
|
+
// retries; state is only a current-session projection.
|
|
1995
|
+
autoHandoff = await resolveRalplanAutoHandoff(cwd, {
|
|
1996
|
+
planningStuck: await readRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId),
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1624
1999
|
// Keep run-state `current_phase` coherent with the stage being persisted.
|
|
1625
2000
|
await persistActiveRunId(cwd, resolved.sessionId, resolved.runId, resolved.stage);
|
|
1626
|
-
const persisted = await persistArtifact(resolved, cwd, content, sha256);
|
|
2001
|
+
const persisted = await persistArtifact(resolved, cwd, content, sha256, autoHandoff);
|
|
1627
2002
|
if (persistedRoleState) {
|
|
1628
2003
|
await applyPersistedRoleStateUpdate(cwd, resolved.sessionId, persistedRoleState);
|
|
1629
2004
|
}
|
|
1630
2005
|
if (laneVerdict) {
|
|
1631
2006
|
await applyLaneVerdictUpdate(cwd, resolved.sessionId, laneVerdict);
|
|
1632
2007
|
}
|
|
2008
|
+
if (autoHandoff) {
|
|
2009
|
+
await persistRalplanFinalAdmission(cwd, resolved.sessionId, resolved.runId, autoHandoff);
|
|
2010
|
+
}
|
|
1633
2011
|
await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "ralplan-runtime", path: persisted.path });
|
|
1634
2012
|
await syncRalplanHud({
|
|
1635
2013
|
cwd,
|
|
@@ -1651,6 +2029,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1651
2029
|
: undefined;
|
|
1652
2030
|
|
|
1653
2031
|
const payload: Record<string, unknown> = {
|
|
2032
|
+
session_id: resolved.sessionId,
|
|
1654
2033
|
run_id: persisted.runId,
|
|
1655
2034
|
path: persisted.path,
|
|
1656
2035
|
stage: persisted.stage,
|
|
@@ -1663,6 +2042,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1663
2042
|
if (persistedRoleState) payload[`${persistedRoleState.role}_state`] = persistedRoleStatePayload(persistedRoleState);
|
|
1664
2043
|
if (reviewBudgetWarning) payload.review_budget_warning = reviewBudgetWarning;
|
|
1665
2044
|
if (laneVerdict) payload.lane_verdict = { lane: laneVerdict.lane, verdict: laneVerdict.verdict };
|
|
2045
|
+
if (autoHandoff) payload.auto_handoff = autoHandoff;
|
|
1666
2046
|
|
|
1667
2047
|
const stdout = resolved.json
|
|
1668
2048
|
? `${JSON.stringify(payload, null, 2)}\n`
|
|
@@ -1675,7 +2055,14 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
1675
2055
|
* do not rewrite artifacts, append rows, or churn run state; a crash-gap repair may
|
|
1676
2056
|
* complete riding persisted role/lane metadata before returning this receipt.
|
|
1677
2057
|
*/
|
|
1678
|
-
function
|
|
2058
|
+
function applyRalplanPlanningStuckOverride(
|
|
2059
|
+
admission: RalplanAutoHandoffResolution,
|
|
2060
|
+
planningStuck: boolean,
|
|
2061
|
+
): RalplanAutoHandoffResolution {
|
|
2062
|
+
return planningStuck ? { ...admission, effectiveTarget: "off", degradationReason: "planning_stuck" } : admission;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
async function buildDeduplicatedResult(
|
|
1679
2066
|
resolved: ResolvedArtifactArgs,
|
|
1680
2067
|
existing: ExistingStageArtifact,
|
|
1681
2068
|
sha256: string,
|
|
@@ -1683,8 +2070,9 @@ function buildDeduplicatedResult(
|
|
|
1683
2070
|
repositoryBinding: RepositoryBinding,
|
|
1684
2071
|
laneVerdict?: LaneVerdictUpdate,
|
|
1685
2072
|
persistedRoleState?: PersistedRoleStateUpdate,
|
|
1686
|
-
): RalplanCommandResult {
|
|
2073
|
+
): Promise<RalplanCommandResult> {
|
|
1687
2074
|
const payload: Record<string, unknown> = {
|
|
2075
|
+
session_id: resolved.sessionId,
|
|
1688
2076
|
run_id: resolved.runId,
|
|
1689
2077
|
path: existing.path,
|
|
1690
2078
|
stage: resolved.stage,
|
|
@@ -1705,6 +2093,11 @@ function buildDeduplicatedResult(
|
|
|
1705
2093
|
resolved.runId,
|
|
1706
2094
|
"pending-approval.md",
|
|
1707
2095
|
);
|
|
2096
|
+
const planningStuck = await readRalplanPlanningStuck(cwd, resolved.sessionId, resolved.runId);
|
|
2097
|
+
payload.auto_handoff = applyRalplanPlanningStuckOverride(
|
|
2098
|
+
existing.autoHandoff ?? unavailableRalplanFinalAdmission(),
|
|
2099
|
+
planningStuck,
|
|
2100
|
+
);
|
|
1708
2101
|
}
|
|
1709
2102
|
const stdout = resolved.json
|
|
1710
2103
|
? `${JSON.stringify(payload, null, 2)}\n`
|
|
@@ -1851,6 +2244,7 @@ async function handleConsensusHandoff(args: readonly string[], cwd: string): Pro
|
|
|
1851
2244
|
});
|
|
1852
2245
|
|
|
1853
2246
|
const summary = {
|
|
2247
|
+
session_id: resolved.sessionId,
|
|
1854
2248
|
skill: "ralplan",
|
|
1855
2249
|
mode,
|
|
1856
2250
|
state_path: statePath,
|
|
@@ -36,6 +36,7 @@ export function buildWorkerCommand(
|
|
|
36
36
|
worker: GjcTeamWorker,
|
|
37
37
|
platform: NodeJS.Platform = process.platform,
|
|
38
38
|
promptOverride?: string,
|
|
39
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
39
40
|
): string {
|
|
40
41
|
const quote = platform === "win32" ? powershellQuote : shellQuote;
|
|
41
42
|
const envAssignment = (key: string, value: string): string =>
|
|
@@ -74,6 +75,13 @@ export function buildWorkerCommand(
|
|
|
74
75
|
"GJC_TEAM_WORKER_MEMORY_GUARD_PATH",
|
|
75
76
|
workerMemoryGuardLedgerPath(path.join(config.state_root, config.team_name), worker.id),
|
|
76
77
|
),
|
|
78
|
+
// The worker derives its heartbeat cadence from the same window the leader
|
|
79
|
+
// enforces. tmux panes do not inherit the launching shell's environment, so
|
|
80
|
+
// without this a tightened window would leave workers publishing on the
|
|
81
|
+
// default cadence and reported stale while they are working.
|
|
82
|
+
...(env.GJC_TEAM_HEARTBEAT_STALE_MS?.trim()
|
|
83
|
+
? [envAssignment("GJC_TEAM_HEARTBEAT_STALE_MS", env.GJC_TEAM_HEARTBEAT_STALE_MS.trim())]
|
|
84
|
+
: []),
|
|
77
85
|
];
|
|
78
86
|
const joined = envLines.join(" ");
|
|
79
87
|
const clearInheritedSession = config.gjc_session_id
|