@gajae-code/coding-agent 0.18.7 → 0.18.9
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 +377 -1
- package/dist/types/async/endpoint-id.d.ts +16 -0
- package/dist/types/async/job-manager.d.ts +1 -1
- package/dist/types/async/support.d.ts +0 -14
- package/dist/types/capability/mcp.d.ts +2 -0
- package/dist/types/capability/skill.d.ts +2 -0
- package/dist/types/capability/types.d.ts +2 -0
- package/dist/types/cli/public-command-errors.d.ts +3 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/commands/mcp-serve.d.ts +4 -0
- package/dist/types/config/autorouting-tier-map.d.ts +348 -9
- package/dist/types/config/file-lock.d.ts +1 -0
- package/dist/types/config/model-bindings-applier.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -3
- package/dist/types/config/model-registry.d.ts +4 -9
- package/dist/types/config/skill-settings-defaults.d.ts +2 -0
- package/dist/types/cursor-delete-path.d.ts +1 -0
- package/dist/types/cursor-mcp-name.d.ts +8 -0
- package/dist/types/customization/inventory.d.ts +2 -0
- package/dist/types/debug/report-bundle-parent.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
- package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
- package/dist/types/discovery/helpers.d.ts +6 -1
- package/dist/types/discovery/mcp-json.d.ts +1 -0
- package/dist/types/eval/py/kernel.d.ts +18 -0
- package/dist/types/export/html/css-color.d.ts +2 -0
- package/dist/types/export/html/html-export-destination.d.ts +2 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
- package/dist/types/extensibility/extensions/types.d.ts +7 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
- package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
- package/dist/types/extensibility/skill-management.d.ts +4 -1
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
- package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
- package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
- package/dist/types/internal-urls/skill-path.d.ts +1 -0
- package/dist/types/lsp/config.d.ts +8 -5
- package/dist/types/lsp/workspace-path.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +7 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
- package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +9 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
- package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
- package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
- package/dist/types/progress/collect-project-progress.d.ts +38 -0
- package/dist/types/progress/progress-contract.d.ts +154 -0
- package/dist/types/progress/progress-snapshot.d.ts +11 -0
- package/dist/types/progress/project-progress.d.ts +141 -0
- package/dist/types/progress/render-progress.d.ts +28 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -1
- package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
- package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -0
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
- package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
- package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
- package/dist/types/runtime-mcp/types.d.ts +6 -0
- package/dist/types/sdk/acp/adapter.d.ts +1 -1
- package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
- package/dist/types/sdk/broker/broker.d.ts +24 -2
- package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -1
- package/dist/types/sdk/broker/session-index.d.ts +12 -6
- package/dist/types/sdk/bus/index.d.ts +12 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +3 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/sdk/session.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +38 -9
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
- package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
- package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +31 -5
- package/dist/types/session/session-retirement.d.ts +31 -7
- package/dist/types/session/session-storage.d.ts +69 -2
- package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
- package/dist/types/setup/credential-auto-import.d.ts +1 -1
- package/dist/types/setup/credential-import.d.ts +6 -4
- package/dist/types/skill-state/active-state.d.ts +38 -0
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
- package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/isolation-dir.d.ts +26 -0
- package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -1
- package/dist/types/tools/browser/cdp-url.d.ts +2 -0
- package/dist/types/tools/github-side-effect.d.ts +22 -0
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/monitor-mutation.d.ts +9 -0
- package/dist/types/tools/python.d.ts +9 -1
- package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
- package/dist/types/tools/restricted-cwd.d.ts +1 -0
- package/dist/types/tools/todo-write.d.ts +6 -0
- package/dist/types/utils/tools-checksum.d.ts +7 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +21 -0
- package/scripts/verify-managed-empty-stop.ts +28 -0
- package/src/async/endpoint-id.ts +26 -0
- package/src/async/job-manager.ts +247 -132
- package/src/async/support.ts +0 -25
- package/src/capability/index.ts +15 -0
- package/src/capability/mcp.ts +2 -0
- package/src/capability/skill.ts +2 -0
- package/src/capability/types.ts +2 -0
- package/src/cli/auth-broker-cli.ts +1 -1
- package/src/cli/customize-doctor.ts +15 -2
- package/src/cli/doctor/repairs.ts +4 -1
- package/src/cli/doctor-supervisor.ts +3 -0
- package/src/cli/public-command-errors.ts +21 -1
- package/src/cli/public-command-registry.ts +28 -6
- package/src/cli/setup-cli.ts +4 -4
- package/src/cli/skills-cli.ts +5 -0
- package/src/commands/deep-interview.ts +47 -8
- package/src/commands/mcp-serve.ts +7 -0
- package/src/commands/sdk.ts +455 -397
- package/src/config/autorouting-tier-map.ts +117 -7
- package/src/config/file-lock.ts +316 -79
- package/src/config/mcp-schema.json +4 -0
- package/src/config/model-bindings-applier.ts +20 -0
- package/src/config/model-profile-activation.ts +240 -46
- package/src/config/model-registry.ts +47 -153
- package/src/config/skill-settings-defaults.ts +7 -0
- package/src/coordinator-mcp/policy.ts +3 -2
- package/src/coordinator-mcp/server.ts +2 -0
- package/src/cursor-delete-path.ts +10 -0
- package/src/cursor-mcp-name.ts +15 -0
- package/src/cursor.ts +13 -2
- package/src/customization/inventory.ts +4 -13
- package/src/dap/config.ts +2 -2
- package/src/debug/report-bundle-parent.ts +22 -0
- package/src/debug/report-bundle.ts +3 -0
- package/src/debug/system-info.ts +13 -1
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
- package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
- package/src/defaults/gjc-defaults.test.ts +18 -1
- package/src/defaults/gjc-defaults.ts +3 -0
- package/src/defaults/gjc-skills.generated.ts +58 -36
- package/src/discovery/agents.ts +6 -1
- package/src/discovery/builtin.ts +13 -1
- package/src/discovery/helpers.ts +171 -29
- package/src/discovery/mcp-json.ts +26 -12
- package/src/discovery/ssh.ts +1 -1
- package/src/discovery/substitute-plugin-root.ts +3 -1
- package/src/edit/file-read-cache.ts +1 -1
- package/src/eval/py/executor.ts +796 -154
- package/src/eval/py/kernel.ts +79 -13
- package/src/eval/py/tool-bridge.ts +4 -0
- package/src/export/html/css-color.ts +9 -0
- package/src/export/html/html-export-destination.ts +16 -0
- package/src/export/html/index.ts +7 -10
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
- package/src/extensibility/extensions/runner.ts +15 -1
- package/src/extensibility/extensions/types.ts +7 -0
- package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +85 -8
- package/src/extensibility/plugins/manager.ts +3 -2
- package/src/extensibility/plugins/marketplace/cache.ts +15 -0
- package/src/extensibility/plugins/marketplace/manager.ts +11 -3
- package/src/extensibility/plugins/plugin-link-path.ts +12 -0
- package/src/extensibility/runtime-skill-discovery.ts +20 -2
- package/src/extensibility/skill-management.ts +9 -2
- package/src/extensibility/skills.ts +6 -1
- package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
- package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
- package/src/gjc-runtime/deep-interview-stage.ts +98 -21
- package/src/gjc-runtime/gc-runtime.ts +206 -9
- package/src/gjc-runtime/launch-worktree.ts +70 -9
- package/src/gjc-runtime/psmux-detect.ts +28 -4
- package/src/gjc-runtime/ralplan-runtime.ts +447 -52
- package/src/gjc-runtime/session-state-sidecar.ts +3 -2
- package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
- package/src/gjc-runtime/state-argv.ts +2 -0
- package/src/gjc-runtime/state-migrations.ts +91 -29
- package/src/gjc-runtime/state-runtime.ts +3716 -135
- package/src/gjc-runtime/state-writer.ts +105 -16
- package/src/gjc-runtime/tmux-common.ts +8 -5
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
- package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
- package/src/gjc-runtime/workflow-manifest.ts +54 -21
- package/src/harness-control-plane/operate.ts +12 -4
- package/src/harness-control-plane/owner.ts +5 -1
- package/src/hashline/parser.ts +48 -9
- package/src/hashline/recovery.ts +13 -4
- package/src/internal-urls/docs-index.generated.ts +20 -19
- package/src/internal-urls/skill-path.ts +19 -0
- package/src/internal-urls/skill-protocol.ts +2 -0
- package/src/lsp/config.ts +12 -63
- package/src/lsp/edits.ts +6 -0
- package/src/lsp/workspace-path.ts +51 -0
- package/src/main.ts +195 -31
- package/src/memories/index.ts +88 -7
- package/src/modes/acp/acp-agent.ts +88 -4
- package/src/modes/components/custom-editor.ts +14 -0
- package/src/modes/components/customization/customization-dashboard.ts +12 -0
- package/src/modes/components/extensions/extension-dashboard.ts +2 -2
- package/src/modes/components/extensions/state-manager.ts +15 -5
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +1 -1
- package/src/modes/components/model-selector.ts +1 -4
- package/src/modes/components/provider-onboarding-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +4 -0
- package/src/modes/controllers/event-controller.ts +11 -4
- package/src/modes/controllers/input-controller.ts +76 -31
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -16
- package/src/modes/controllers/selector-controller.ts +24 -15
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +5 -1
- package/src/modes/runtime-init.ts +4 -0
- package/src/modes/utils/abort-message.ts +3 -1
- package/src/modes/utils/interrupt-ownership.ts +12 -0
- package/src/modes/utils/keybinding-matchers.ts +4 -0
- package/src/modes/utils/macos-option-forwarding.ts +80 -0
- package/src/progress/collect-project-progress.ts +153 -0
- package/src/progress/progress-contract.ts +172 -0
- package/src/progress/progress-snapshot.ts +140 -0
- package/src/progress/project-progress.ts +446 -0
- package/src/progress/render-progress.ts +209 -0
- package/src/prompts/system/master-mode.md +1 -0
- package/src/prompts/tools/skill-discovery.md +1 -1
- package/src/runtime-mcp/client.ts +4 -2
- package/src/runtime-mcp/config-writer.ts +10 -6
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/json-rpc.ts +2 -1
- package/src/runtime-mcp/manager.ts +29 -11
- package/src/runtime-mcp/mcp-redirect.ts +73 -0
- package/src/runtime-mcp/oauth-flow.ts +14 -2
- package/src/runtime-mcp/oauth-public-url.ts +14 -0
- package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
- package/src/runtime-mcp/pool.ts +6 -0
- package/src/runtime-mcp/project-host-values.ts +46 -0
- package/src/runtime-mcp/smithery-registry.ts +22 -3
- package/src/runtime-mcp/transports/http.ts +4 -3
- package/src/runtime-mcp/types.ts +6 -0
- package/src/sdk/acp/adapter.ts +95 -8
- package/src/sdk/broker/broker-exit.ts +52 -23
- package/src/sdk/broker/broker.ts +81 -6
- package/src/sdk/broker/doctor-restart.ts +4 -0
- package/src/sdk/broker/ensure.ts +31 -2
- package/src/sdk/broker/lifecycle.ts +711 -66
- package/src/sdk/broker/session-index.ts +191 -81
- package/src/sdk/bus/chat-daemon-cli.ts +38 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +238 -43
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
- package/src/sdk/cli/session-cli.ts +17 -3
- package/src/sdk/client/client.ts +28 -8
- package/src/sdk/host/query/handlers.ts +9 -1
- package/src/sdk/host/session-runtime.test.ts +2705 -303
- package/src/sdk/host/session-runtime.ts +490 -143
- package/src/sdk/host/surface-policy.ts +1 -0
- package/src/sdk/index.ts +8 -0
- package/src/sdk/lifecycle-session.ts +3 -1
- package/src/sdk/prompt-deadline-manager.ts +15 -0
- package/src/sdk/protocol/operation-inventory.generated.json +146 -0
- package/src/sdk/protocol/operation-registry.ts +9 -2
- package/src/sdk/router/session-router.ts +82 -18
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +69 -45
- package/src/session/agent-session.ts +679 -238
- package/src/session/artifacts.ts +176 -71
- package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
- package/src/session/internal/managed-session-scope.ts +3779 -462
- package/src/session/internal/managed-session-storage.ts +501 -78
- package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
- package/src/session/internal/task-artifact-owner-access.ts +19 -3
- package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
- package/src/session/session-manager.ts +951 -133
- package/src/session/session-retirement.ts +573 -14
- package/src/session/session-storage.ts +447 -15
- package/src/session/task-artifact-owner-codec.ts +71 -8
- package/src/session/task-artifact-owner-retirement.ts +21 -19
- package/src/setup/credential-auto-import.ts +12 -3
- package/src/setup/credential-import.ts +176 -5
- package/src/setup/paseo/announce.ts +6 -1
- package/src/skill-state/active-state.ts +145 -2
- package/src/skill-state/workflow-mutation-guard.ts +102 -10
- package/src/slash-commands/builtin-registry.ts +39 -9
- package/src/slash-commands/helpers/mcp.ts +8 -4
- package/src/slash-commands/helpers/progress-report.ts +23 -0
- package/src/stt/setup.ts +3 -5
- package/src/task/commands.ts +3 -1
- package/src/task/executor.ts +4 -0
- package/src/task/index.ts +6 -0
- package/src/task/isolation-dir.ts +143 -0
- package/src/task/worktree.ts +16 -7
- package/src/tools/ask.ts +205 -6
- package/src/tools/ast-edit-write-path.ts +68 -0
- package/src/tools/ast-edit.ts +10 -1
- package/src/tools/bash-allowed-prefixes.ts +15 -0
- package/src/tools/bash-skill-urls.ts +56 -3
- package/src/tools/bash.ts +48 -79
- package/src/tools/browser/cdp-url.ts +18 -0
- package/src/tools/browser.ts +8 -1
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/eval.ts +56 -36
- package/src/tools/gh.ts +10 -0
- package/src/tools/github-side-effect.ts +112 -0
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/monitor-mutation.ts +27 -0
- package/src/tools/monitor.ts +7 -0
- package/src/tools/python.ts +188 -66
- package/src/tools/recipe/index.ts +9 -0
- package/src/tools/recipe/recipe-guard.ts +19 -0
- package/src/tools/restricted-cwd.ts +10 -0
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +28 -1
- package/src/tools/todo-write.ts +10 -1
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/write.ts +1 -0
- package/src/utils/external-editor.ts +3 -2
- package/src/utils/tools-checksum.ts +34 -0
- package/src/utils/tools-manager.ts +24 -6
- package/src/web/insane/url-guard.ts +2 -1
- package/src/web/search/providers/codex.ts +3 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import type { Dirent } from "node:fs";
|
|
3
3
|
import * as fssync from "node:fs";
|
|
4
4
|
import * as fs from "node:fs/promises";
|
|
@@ -36,10 +36,14 @@ import { resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session
|
|
|
36
36
|
import { migrateWorkflowState } from "./state-migrations";
|
|
37
37
|
import { runNativeStateCommand } from "./state-runtime";
|
|
38
38
|
import {
|
|
39
|
-
|
|
39
|
+
type AppendJsonlIdempotentOptions,
|
|
40
|
+
type AppendJsonlIdempotentResult,
|
|
41
|
+
appendJsonl,
|
|
42
|
+
detectWorkflowEnvelopeIntegrityMismatch,
|
|
40
43
|
readExistingStateForMutation,
|
|
41
44
|
withWorkflowStateLock,
|
|
42
45
|
writeArtifact,
|
|
46
|
+
writeTextAtomic,
|
|
43
47
|
writeWorkflowEnvelopeAtomic,
|
|
44
48
|
} from "./state-writer";
|
|
45
49
|
import { assertSafePathComponent, CommandError, flagValue, hasFlag } from "./workflow-cli-common";
|
|
@@ -347,6 +351,53 @@ export async function countRalplanOnDiskLaneArtifacts(
|
|
|
347
351
|
* signals so callers can fail closed when the ledger is empty/malformed while
|
|
348
352
|
* opener artifacts already exist on disk.
|
|
349
353
|
*/
|
|
354
|
+
async function readRalplanIndexBounded(indexPath: string): Promise<string | undefined> {
|
|
355
|
+
let lexical: fssync.BigIntStats;
|
|
356
|
+
try {
|
|
357
|
+
lexical = await fs.lstat(indexPath, { bigint: true });
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (getErrorCode(error) === "ENOENT") return undefined;
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
if (lexical.isSymbolicLink() || !lexical.isFile() || lexical.size > BigInt(RALPLAN_MAX_INDEX_RECOVERY_BYTES))
|
|
363
|
+
throw new RalplanCommandError(2, "ralplan index exceeds its bounded recovery limit");
|
|
364
|
+
const flags = fssync.constants.O_RDONLY | (process.platform === "win32" ? 0 : (fssync.constants.O_NOFOLLOW ?? 0));
|
|
365
|
+
let handle: fs.FileHandle | undefined;
|
|
366
|
+
try {
|
|
367
|
+
handle = await fs.open(indexPath, flags);
|
|
368
|
+
const opened = await handle.stat({ bigint: true });
|
|
369
|
+
const same = (left: fssync.BigIntStats, right: fssync.BigIntStats) =>
|
|
370
|
+
left.dev === right.dev &&
|
|
371
|
+
left.ino === right.ino &&
|
|
372
|
+
left.mode === right.mode &&
|
|
373
|
+
left.size === right.size &&
|
|
374
|
+
left.mtimeNs === right.mtimeNs &&
|
|
375
|
+
left.ctimeNs === right.ctimeNs &&
|
|
376
|
+
left.nlink === right.nlink;
|
|
377
|
+
if (!opened.isFile() || !same(lexical, opened))
|
|
378
|
+
throw new RalplanCommandError(2, "ralplan index identity changed");
|
|
379
|
+
const buffer = Buffer.alloc(Number(opened.size));
|
|
380
|
+
let offset = 0;
|
|
381
|
+
while (offset < buffer.length) {
|
|
382
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
383
|
+
if (bytesRead === 0) throw new RalplanCommandError(2, "ralplan index changed during bounded read");
|
|
384
|
+
offset += bytesRead;
|
|
385
|
+
}
|
|
386
|
+
const afterDescriptor = await handle.stat({ bigint: true });
|
|
387
|
+
const afterPath = await fs.lstat(indexPath, { bigint: true });
|
|
388
|
+
if (
|
|
389
|
+
afterPath.isSymbolicLink() ||
|
|
390
|
+
!afterPath.isFile() ||
|
|
391
|
+
!same(opened, afterDescriptor) ||
|
|
392
|
+
!same(opened, afterPath)
|
|
393
|
+
)
|
|
394
|
+
throw new RalplanCommandError(2, "ralplan index changed during bounded read");
|
|
395
|
+
return buffer.toString("utf8");
|
|
396
|
+
} finally {
|
|
397
|
+
await handle?.close();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
350
401
|
export async function loadRalplanIndexForCap(
|
|
351
402
|
cwd: string,
|
|
352
403
|
sessionId: string,
|
|
@@ -360,7 +411,8 @@ export async function loadRalplanIndexForCap(
|
|
|
360
411
|
}> {
|
|
361
412
|
const indexPath = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId, "index.jsonl");
|
|
362
413
|
try {
|
|
363
|
-
const text = await
|
|
414
|
+
const text = await readRalplanIndexBounded(indexPath);
|
|
415
|
+
if (text === undefined) return { rows: [], indexPresent: false, parseableLines: 0, rawLineCount: 0 };
|
|
364
416
|
const lines = text.split(/\r?\n/).filter(line => line.trim().length > 0);
|
|
365
417
|
const rows: RalplanIndexRow[] = [];
|
|
366
418
|
for (const line of lines) {
|
|
@@ -1010,6 +1062,13 @@ async function persistActiveRunId(cwd: string, sessionId: string, runId: string,
|
|
|
1010
1062
|
}
|
|
1011
1063
|
delete existing.planning_stuck;
|
|
1012
1064
|
delete existing.auto_handoff;
|
|
1065
|
+
// Initial run establishment retains the incoming handoff; only a replacement run severs it.
|
|
1066
|
+
if (existing.run_id !== undefined) {
|
|
1067
|
+
delete existing.handoff_from;
|
|
1068
|
+
delete existing.handoff_at;
|
|
1069
|
+
delete existing.upstream_handoff_at;
|
|
1070
|
+
}
|
|
1071
|
+
delete existing.final_admission_phase_transition;
|
|
1013
1072
|
}
|
|
1014
1073
|
if (
|
|
1015
1074
|
existing.run_id === runId &&
|
|
@@ -1354,7 +1413,7 @@ async function recordRalplanPlanningStuck(
|
|
|
1354
1413
|
reason: string,
|
|
1355
1414
|
): Promise<void> {
|
|
1356
1415
|
const runDir = path.join(sessionPlansDir(cwd, sessionId), "ralplan", runId);
|
|
1357
|
-
await
|
|
1416
|
+
await appendRalplanIndexIdempotent(
|
|
1358
1417
|
path.join(runDir, "index.jsonl"),
|
|
1359
1418
|
{
|
|
1360
1419
|
event: "planning_stuck",
|
|
@@ -1439,6 +1498,7 @@ async function persistRalplanFinalAdmission(
|
|
|
1439
1498
|
sessionId: string,
|
|
1440
1499
|
runId: string,
|
|
1441
1500
|
admission: RalplanAutoHandoffResolution,
|
|
1501
|
+
publication: { id: string; sha256: string },
|
|
1442
1502
|
): Promise<void> {
|
|
1443
1503
|
const statePath = ralplanStatePath(cwd, sessionId);
|
|
1444
1504
|
await withWorkflowStateLock(
|
|
@@ -1453,7 +1513,64 @@ async function persistRalplanFinalAdmission(
|
|
|
1453
1513
|
}
|
|
1454
1514
|
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1455
1515
|
if (existing.run_id !== runId) return;
|
|
1516
|
+
const phase = typeof existing.current_phase === "string" ? existing.current_phase.trim() : "";
|
|
1517
|
+
if (getSkillManifest("ralplan").phaseLock.includes(phase) && (phase !== "final" || existing.active === false))
|
|
1518
|
+
return;
|
|
1519
|
+
const pending =
|
|
1520
|
+
existing.final_publication_pending &&
|
|
1521
|
+
typeof existing.final_publication_pending === "object" &&
|
|
1522
|
+
!Array.isArray(existing.final_publication_pending)
|
|
1523
|
+
? (existing.final_publication_pending as Record<string, unknown>)
|
|
1524
|
+
: undefined;
|
|
1525
|
+
if (
|
|
1526
|
+
pending?.publication_id !== publication.id ||
|
|
1527
|
+
pending.final_sha256 !== publication.sha256 ||
|
|
1528
|
+
pending.run_id !== runId
|
|
1529
|
+
)
|
|
1530
|
+
throw new RalplanCommandError(2, "final publication was superseded by a concurrent writer");
|
|
1531
|
+
const index = await loadRalplanIndexForCap(cwd, sessionId, runId);
|
|
1532
|
+
let lastFinalSha: string | undefined;
|
|
1533
|
+
for (const line of index.rawText?.split(/\r?\n/) ?? []) {
|
|
1534
|
+
if (!line.trim()) continue;
|
|
1535
|
+
try {
|
|
1536
|
+
const row = JSON.parse(line) as Record<string, unknown>;
|
|
1537
|
+
if (row.stage === "final" && typeof row.sha256 === "string") lastFinalSha = row.sha256;
|
|
1538
|
+
} catch {
|
|
1539
|
+
throw new RalplanCommandError(2, "final publication index is malformed");
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
if (lastFinalSha !== publication.sha256)
|
|
1543
|
+
throw new RalplanCommandError(2, "final publication index does not match the active writer");
|
|
1544
|
+
const currentAdmission = parseRalplanFinalAdmission(existing.auto_handoff);
|
|
1545
|
+
const receipt =
|
|
1546
|
+
existing.receipt && typeof existing.receipt === "object" && !Array.isArray(existing.receipt)
|
|
1547
|
+
? (existing.receipt as Record<string, unknown>)
|
|
1548
|
+
: undefined;
|
|
1549
|
+
const checksum =
|
|
1550
|
+
receipt?.content_sha256 &&
|
|
1551
|
+
typeof receipt.content_sha256 === "object" &&
|
|
1552
|
+
!Array.isArray(receipt.content_sha256)
|
|
1553
|
+
? (receipt.content_sha256 as Record<string, unknown>)
|
|
1554
|
+
: undefined;
|
|
1555
|
+
if (
|
|
1556
|
+
currentAdmission &&
|
|
1557
|
+
existing.final_publication_pending === undefined &&
|
|
1558
|
+
JSON.stringify(currentAdmission) === JSON.stringify(admission) &&
|
|
1559
|
+
receipt?.skill === "ralplan" &&
|
|
1560
|
+
receipt.owner === "gjc-runtime" &&
|
|
1561
|
+
receipt.command === "gjc ralplan final-admission" &&
|
|
1562
|
+
checksum?.algorithm === "sha256" &&
|
|
1563
|
+
typeof checksum.value === "string" &&
|
|
1564
|
+
/^[0-9a-f]{64}$/.test(checksum.value) &&
|
|
1565
|
+
checksum.covered_path === path.resolve(statePath) &&
|
|
1566
|
+
typeof checksum.computed_at === "string" &&
|
|
1567
|
+
checksum.computed_at.trim() !== "" &&
|
|
1568
|
+
!(await detectWorkflowEnvelopeIntegrityMismatch(statePath))
|
|
1569
|
+
) {
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1456
1572
|
existing.auto_handoff = admission;
|
|
1573
|
+
delete existing.final_publication_pending;
|
|
1457
1574
|
existing = migrateWorkflowState(existing, "ralplan").state;
|
|
1458
1575
|
existing.updated_at = new Date().toISOString();
|
|
1459
1576
|
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
@@ -1467,6 +1584,87 @@ async function persistRalplanFinalAdmission(
|
|
|
1467
1584
|
);
|
|
1468
1585
|
}
|
|
1469
1586
|
|
|
1587
|
+
async function markRalplanFinalPublicationPending(
|
|
1588
|
+
cwd: string,
|
|
1589
|
+
sessionId: string,
|
|
1590
|
+
runId: string,
|
|
1591
|
+
publication: { id: string; sha256: string },
|
|
1592
|
+
recovery = false,
|
|
1593
|
+
): Promise<boolean> {
|
|
1594
|
+
const statePath = ralplanStatePath(cwd, sessionId);
|
|
1595
|
+
return await withWorkflowStateLock(
|
|
1596
|
+
statePath,
|
|
1597
|
+
async () => {
|
|
1598
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
1599
|
+
if (existingRead.kind === "corrupt")
|
|
1600
|
+
throw new RalplanCommandError(2, `existing ralplan state is corrupt or tampered (${existingRead.error})`);
|
|
1601
|
+
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1602
|
+
const phase = typeof existing.current_phase === "string" ? existing.current_phase.trim() : "";
|
|
1603
|
+
// Active final is publication/approval, not a terminal lifecycle despite its stage phase lock.
|
|
1604
|
+
const phaseLocked =
|
|
1605
|
+
getSkillManifest("ralplan").phaseLock.includes(phase) && (phase !== "final" || existing.active === false);
|
|
1606
|
+
if (recovery) {
|
|
1607
|
+
// A receipt lookup may repair its current projection, never select a historical run.
|
|
1608
|
+
if (phaseLocked || (existingRead.kind === "valid" && existing.run_id !== runId)) return false;
|
|
1609
|
+
const pending = existing.final_publication_pending;
|
|
1610
|
+
if (
|
|
1611
|
+
pending !== undefined &&
|
|
1612
|
+
(!pending ||
|
|
1613
|
+
typeof pending !== "object" ||
|
|
1614
|
+
Array.isArray(pending) ||
|
|
1615
|
+
(pending as Record<string, unknown>).run_id !== runId ||
|
|
1616
|
+
(pending as Record<string, unknown>).final_sha256 !== publication.sha256)
|
|
1617
|
+
)
|
|
1618
|
+
return false;
|
|
1619
|
+
const index = await loadRalplanIndexForCap(cwd, sessionId, runId);
|
|
1620
|
+
let lastFinalSha: unknown;
|
|
1621
|
+
for (const line of index.rawText?.split(/\r?\n/) ?? []) {
|
|
1622
|
+
if (!line.trim()) continue;
|
|
1623
|
+
const row = JSON.parse(line) as Record<string, unknown>;
|
|
1624
|
+
if (row.stage === "final") lastFinalSha = row.sha256;
|
|
1625
|
+
}
|
|
1626
|
+
if (lastFinalSha !== publication.sha256) return false;
|
|
1627
|
+
} else if (existing.run_id === runId && phaseLocked) {
|
|
1628
|
+
throw new RalplanCommandError(2, "cannot publish a new final after the Ralplan run reached a locked phase");
|
|
1629
|
+
}
|
|
1630
|
+
if (existing.run_id !== runId) {
|
|
1631
|
+
delete existing.planning_stuck;
|
|
1632
|
+
delete existing.auto_handoff;
|
|
1633
|
+
// A first final may establish the run directly after a planning handoff.
|
|
1634
|
+
if (existing.run_id !== undefined) {
|
|
1635
|
+
delete existing.handoff_from;
|
|
1636
|
+
delete existing.handoff_at;
|
|
1637
|
+
delete existing.upstream_handoff_at;
|
|
1638
|
+
}
|
|
1639
|
+
delete existing.final_admission_phase_transition;
|
|
1640
|
+
}
|
|
1641
|
+
existing.skill = "ralplan";
|
|
1642
|
+
existing.version = WORKFLOW_STATE_VERSION;
|
|
1643
|
+
existing.session_id = sessionId;
|
|
1644
|
+
existing.run_id = runId;
|
|
1645
|
+
existing.active = true;
|
|
1646
|
+
existing.current_phase = "final";
|
|
1647
|
+
const startedAt = new Date().toISOString();
|
|
1648
|
+
existing.updated_at = startedAt;
|
|
1649
|
+
existing.final_publication_pending = {
|
|
1650
|
+
run_id: runId,
|
|
1651
|
+
publication_id: publication.id,
|
|
1652
|
+
final_sha256: publication.sha256,
|
|
1653
|
+
started_at: startedAt,
|
|
1654
|
+
};
|
|
1655
|
+
existing = migrateWorkflowState(existing, "ralplan").state;
|
|
1656
|
+
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
1657
|
+
cwd,
|
|
1658
|
+
lockHeld: true,
|
|
1659
|
+
receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan final-pending", sessionId },
|
|
1660
|
+
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan", sessionId },
|
|
1661
|
+
});
|
|
1662
|
+
return true;
|
|
1663
|
+
},
|
|
1664
|
+
{ cwd },
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1470
1668
|
async function findExistingRalplanRunOwners(cwd: string, runId: string): Promise<string[]> {
|
|
1471
1669
|
let entries: Dirent<string>[];
|
|
1472
1670
|
try {
|
|
@@ -1592,6 +1790,10 @@ interface PersistedArtifact {
|
|
|
1592
1790
|
pendingApprovalPath?: string;
|
|
1593
1791
|
}
|
|
1594
1792
|
|
|
1793
|
+
/** Keep the writer-side ralplan ledger within the bound enforced by state-runtime. */
|
|
1794
|
+
const RALPLAN_MAX_INDEX_BYTES = 1024 * 1024;
|
|
1795
|
+
const RALPLAN_MAX_INDEX_RECOVERY_BYTES = 8 * 1024 * 1024;
|
|
1796
|
+
|
|
1595
1797
|
/**
|
|
1596
1798
|
* Content-addressed identity for an `index.jsonl` row: a repeated `--write` of the
|
|
1597
1799
|
* same `(stage, stage_n)` at identical content (same sha256) is the #638 duplicate
|
|
@@ -1605,6 +1807,93 @@ function ralplanIndexKey(entry: unknown): string | undefined {
|
|
|
1605
1807
|
return `${stage}\u0000${stage_n}\u0000${sha256}`;
|
|
1606
1808
|
}
|
|
1607
1809
|
|
|
1810
|
+
function serializeRalplanIndexEntries(entries: readonly unknown[]): string {
|
|
1811
|
+
return entries.map(entry => `${JSON.stringify(entry)}\n`).join("");
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
/**
|
|
1815
|
+
* Compact the append-only ralplan ledger without dropping the current final receipt.
|
|
1816
|
+
* The newest row and newest planning-stuck marker are also retained so a write can
|
|
1817
|
+
* never report success while silently losing the receipt it just persisted.
|
|
1818
|
+
*/
|
|
1819
|
+
function compactRalplanIndexEntries(entries: readonly unknown[]): unknown[] {
|
|
1820
|
+
let latestFinal = -1;
|
|
1821
|
+
let latestPlanningStuck = -1;
|
|
1822
|
+
for (const [index, entry] of entries.entries()) {
|
|
1823
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
1824
|
+
const record = entry as Record<string, unknown>;
|
|
1825
|
+
if (record.stage === "final") latestFinal = index;
|
|
1826
|
+
if (record.planning_stuck === true) latestPlanningStuck = index;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
const retained = entries.map((entry, index) => ({
|
|
1830
|
+
entry,
|
|
1831
|
+
bytes: Buffer.byteLength(`${JSON.stringify(entry)}\n`, "utf8"),
|
|
1832
|
+
protected: index === entries.length - 1 || index === latestFinal || index === latestPlanningStuck,
|
|
1833
|
+
}));
|
|
1834
|
+
let totalBytes = retained.reduce((total, item) => total + item.bytes, 0);
|
|
1835
|
+
while (totalBytes > RALPLAN_MAX_INDEX_BYTES) {
|
|
1836
|
+
const removable = retained.findIndex(item => !item.protected);
|
|
1837
|
+
if (removable < 0) {
|
|
1838
|
+
throw new RalplanCommandError(2, "ralplan index live receipt set exceeds the 1 MiB bound");
|
|
1839
|
+
}
|
|
1840
|
+
totalBytes -= retained[removable]!.bytes;
|
|
1841
|
+
retained.splice(removable, 1);
|
|
1842
|
+
}
|
|
1843
|
+
return retained.map(item => item.entry);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/** Append a ralplan index row under the ledger lock, compacting before the write. */
|
|
1847
|
+
async function appendRalplanIndexIdempotent(
|
|
1848
|
+
targetPath: string,
|
|
1849
|
+
entry: unknown,
|
|
1850
|
+
options: AppendJsonlIdempotentOptions,
|
|
1851
|
+
): Promise<AppendJsonlIdempotentResult> {
|
|
1852
|
+
const { key, equals, ...writeOptions } = options;
|
|
1853
|
+
const resolvedTargetPath = path.resolve(options.cwd ?? process.cwd(), targetPath);
|
|
1854
|
+
return await withWorkflowStateLock(
|
|
1855
|
+
targetPath,
|
|
1856
|
+
async () => {
|
|
1857
|
+
const raw = (await readRalplanIndexBounded(resolvedTargetPath)) ?? "";
|
|
1858
|
+
const existing: unknown[] = [];
|
|
1859
|
+
let malformed = false;
|
|
1860
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1861
|
+
const trimmed = line.trim();
|
|
1862
|
+
if (!trimmed) continue;
|
|
1863
|
+
try {
|
|
1864
|
+
existing.push(JSON.parse(trimmed));
|
|
1865
|
+
} catch {
|
|
1866
|
+
malformed = true;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
const candidateKey = key ? key(entry) : undefined;
|
|
1870
|
+
const duplicate = equals
|
|
1871
|
+
? existing.find(item => equals(entry, item))
|
|
1872
|
+
: candidateKey === undefined
|
|
1873
|
+
? undefined
|
|
1874
|
+
: existing.find(item => key?.(item) === candidateKey);
|
|
1875
|
+
const candidateLine = `${JSON.stringify(entry)}\n`;
|
|
1876
|
+
if (Buffer.byteLength(raw, "utf8") + Buffer.byteLength(candidateLine, "utf8") <= RALPLAN_MAX_INDEX_BYTES) {
|
|
1877
|
+
if (duplicate !== undefined) return { path: resolvedTargetPath, appended: false, duplicate };
|
|
1878
|
+
await appendJsonl(targetPath, entry, writeOptions);
|
|
1879
|
+
return { path: resolvedTargetPath, appended: true };
|
|
1880
|
+
}
|
|
1881
|
+
if (malformed) throw new RalplanCommandError(2, "oversized ralplan index contains malformed JSON");
|
|
1882
|
+
const next = duplicate === undefined ? [...existing, entry] : existing;
|
|
1883
|
+
const compacted = compactRalplanIndexEntries(next);
|
|
1884
|
+
const serialized = serializeRalplanIndexEntries(compacted);
|
|
1885
|
+
const existingSerialized = serializeRalplanIndexEntries(existing);
|
|
1886
|
+
if (serialized !== existingSerialized) {
|
|
1887
|
+
await writeTextAtomic(targetPath, serialized, writeOptions);
|
|
1888
|
+
}
|
|
1889
|
+
return duplicate === undefined
|
|
1890
|
+
? { path: resolvedTargetPath, appended: true }
|
|
1891
|
+
: { path: resolvedTargetPath, appended: false, duplicate };
|
|
1892
|
+
},
|
|
1893
|
+
options,
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1608
1897
|
async function persistArtifact(
|
|
1609
1898
|
resolved: ResolvedArtifactArgs,
|
|
1610
1899
|
cwd: string,
|
|
@@ -1636,7 +1925,7 @@ async function persistArtifact(
|
|
|
1636
1925
|
sha256,
|
|
1637
1926
|
...(finalAdmission ? { auto_handoff: finalAdmission } : {}),
|
|
1638
1927
|
};
|
|
1639
|
-
await
|
|
1928
|
+
await appendRalplanIndexIdempotent(path.join(runDir, "index.jsonl"), indexEntry, {
|
|
1640
1929
|
cwd,
|
|
1641
1930
|
audit: {
|
|
1642
1931
|
category: "ledger",
|
|
@@ -1837,7 +2126,7 @@ async function repairMissingStageArtifactLedger(
|
|
|
1837
2126
|
sha256: onDisk.sha256,
|
|
1838
2127
|
...(finalAdmission ? { auto_handoff: finalAdmission } : {}),
|
|
1839
2128
|
};
|
|
1840
|
-
const result = await
|
|
2129
|
+
const result = await appendRalplanIndexIdempotent(
|
|
1841
2130
|
path.join(sessionPlansDir(cwd, resolved.sessionId), "ralplan", resolved.runId, "index.jsonl"),
|
|
1842
2131
|
indexEntry,
|
|
1843
2132
|
{
|
|
@@ -2034,6 +2323,7 @@ async function handleArtifactWrite(
|
|
|
2034
2323
|
args: readonly string[],
|
|
2035
2324
|
cwd: string,
|
|
2036
2325
|
agentDir?: string,
|
|
2326
|
+
finalPublicationLockHeld = false,
|
|
2037
2327
|
): Promise<RalplanCommandResult> {
|
|
2038
2328
|
// #4693: explicit --worktree-root binds every persistence root to the selected
|
|
2039
2329
|
// canonical worktree; the invocation cwd survives only for --artifact input
|
|
@@ -2044,6 +2334,17 @@ async function handleArtifactWrite(
|
|
|
2044
2334
|
const resolved = await resolveArtifactArgs(args, persistCwd, cwd, {
|
|
2045
2335
|
confineArtifactRoot: target.explicit ? cwd : undefined,
|
|
2046
2336
|
});
|
|
2337
|
+
if (resolved.stage === "final" && !finalPublicationLockHeld) {
|
|
2338
|
+
const publicationLockPath = path.join(
|
|
2339
|
+
sessionPlansDir(persistCwd, resolved.sessionId),
|
|
2340
|
+
"ralplan",
|
|
2341
|
+
resolved.runId,
|
|
2342
|
+
"final-publication",
|
|
2343
|
+
);
|
|
2344
|
+
return await withWorkflowStateLock(publicationLockPath, () => handleArtifactWrite(args, cwd, agentDir, true), {
|
|
2345
|
+
cwd: persistCwd,
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2047
2348
|
const persistedRoleState = parsePersistedRoleStateArgs(args, resolved.stage);
|
|
2048
2349
|
const laneVerdict = parseLaneVerdictArgs(args, resolved.stage, resolved.stageN);
|
|
2049
2350
|
// Fail closed before stage persistence / path writes when cwd drifted to a sibling repo.
|
|
@@ -2093,7 +2394,55 @@ async function handleArtifactWrite(
|
|
|
2093
2394
|
`refusing to overwrite ralplan ${resolved.stage} stage ${resolved.stageN} at ${existingArtifact.path}: an artifact with different content already exists (existing sha256=${existingArtifact.sha256}, new sha256=${sha256}). Use a new --stage_n to record another pass.`,
|
|
2094
2395
|
);
|
|
2095
2396
|
}
|
|
2096
|
-
if (resolved.stage === "final")
|
|
2397
|
+
if (resolved.stage === "final") {
|
|
2398
|
+
await ensureFinalPendingApproval(persistCwd, resolved, existingArtifact);
|
|
2399
|
+
await appendRalplanIndexIdempotent(
|
|
2400
|
+
path.join(sessionPlansDir(persistCwd, resolved.sessionId), "ralplan", resolved.runId, "index.jsonl"),
|
|
2401
|
+
{
|
|
2402
|
+
stage: "final",
|
|
2403
|
+
stage_n: resolved.stageN,
|
|
2404
|
+
path: existingArtifact.path,
|
|
2405
|
+
created_at: existingArtifact.createdAt,
|
|
2406
|
+
sha256: existingArtifact.sha256,
|
|
2407
|
+
...(existingArtifact.autoHandoff ? { auto_handoff: existingArtifact.autoHandoff } : {}),
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
cwd: persistCwd,
|
|
2411
|
+
key: ralplanIndexKey,
|
|
2412
|
+
audit: {
|
|
2413
|
+
category: "ledger",
|
|
2414
|
+
verb: "append",
|
|
2415
|
+
owner: "gjc-runtime",
|
|
2416
|
+
skill: "ralplan",
|
|
2417
|
+
sessionId: resolved.sessionId,
|
|
2418
|
+
},
|
|
2419
|
+
},
|
|
2420
|
+
);
|
|
2421
|
+
// The artifact and its authenticated ledger receipt are durable before the
|
|
2422
|
+
// current-session admission projection. Repair that projection after a
|
|
2423
|
+
// crash in the gap, before returning the deduplicated receipt.
|
|
2424
|
+
if (existingArtifact.autoHandoff) {
|
|
2425
|
+
const planningStuck = await readRalplanPlanningStuck(persistCwd, resolved.sessionId, resolved.runId);
|
|
2426
|
+
const publication = { id: randomUUID(), sha256: existingArtifact.sha256 };
|
|
2427
|
+
if (
|
|
2428
|
+
await markRalplanFinalPublicationPending(
|
|
2429
|
+
persistCwd,
|
|
2430
|
+
resolved.sessionId,
|
|
2431
|
+
resolved.runId,
|
|
2432
|
+
publication,
|
|
2433
|
+
true,
|
|
2434
|
+
)
|
|
2435
|
+
) {
|
|
2436
|
+
await persistRalplanFinalAdmission(
|
|
2437
|
+
persistCwd,
|
|
2438
|
+
resolved.sessionId,
|
|
2439
|
+
resolved.runId,
|
|
2440
|
+
applyRalplanPlanningStuckOverride(existingArtifact.autoHandoff, planningStuck),
|
|
2441
|
+
publication,
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2097
2446
|
return await buildDeduplicatedResult(resolved, existingArtifact, sha256, persistCwd, repositoryBinding);
|
|
2098
2447
|
}
|
|
2099
2448
|
|
|
@@ -2115,6 +2464,26 @@ async function handleArtifactWrite(
|
|
|
2115
2464
|
onDiskArtifact,
|
|
2116
2465
|
resolved.stage === "final" ? unavailableRalplanFinalAdmission() : undefined,
|
|
2117
2466
|
);
|
|
2467
|
+
if (resolved.stage === "final") {
|
|
2468
|
+
const recoveryPublication = { id: randomUUID(), sha256 };
|
|
2469
|
+
if (
|
|
2470
|
+
await markRalplanFinalPublicationPending(
|
|
2471
|
+
persistCwd,
|
|
2472
|
+
resolved.sessionId,
|
|
2473
|
+
resolved.runId,
|
|
2474
|
+
recoveryPublication,
|
|
2475
|
+
true,
|
|
2476
|
+
)
|
|
2477
|
+
) {
|
|
2478
|
+
await persistRalplanFinalAdmission(
|
|
2479
|
+
persistCwd,
|
|
2480
|
+
resolved.sessionId,
|
|
2481
|
+
resolved.runId,
|
|
2482
|
+
unavailableRalplanFinalAdmission(),
|
|
2483
|
+
recoveryPublication,
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2118
2487
|
let appliedPersistedRoleState: PersistedRoleStateUpdate | undefined;
|
|
2119
2488
|
if (
|
|
2120
2489
|
persistedRoleState &&
|
|
@@ -2189,6 +2558,7 @@ async function handleArtifactWrite(
|
|
|
2189
2558
|
}
|
|
2190
2559
|
|
|
2191
2560
|
let autoHandoff: RalplanAutoHandoffResolution | undefined;
|
|
2561
|
+
let finalPublication: { id: string; sha256: string } | undefined;
|
|
2192
2562
|
if (resolved.stage === "final") {
|
|
2193
2563
|
// Resolve and validate the configured admission before any final artifact or
|
|
2194
2564
|
// state write. The ledger row below is the durable receipt for deduplicated
|
|
@@ -2197,6 +2567,8 @@ async function handleArtifactWrite(
|
|
|
2197
2567
|
agentDir,
|
|
2198
2568
|
planningStuck: await readRalplanPlanningStuck(persistCwd, resolved.sessionId, resolved.runId),
|
|
2199
2569
|
});
|
|
2570
|
+
finalPublication = { id: randomUUID(), sha256 };
|
|
2571
|
+
await markRalplanFinalPublicationPending(persistCwd, resolved.sessionId, resolved.runId, finalPublication);
|
|
2200
2572
|
}
|
|
2201
2573
|
// Keep run-state `current_phase` coherent with the stage being persisted.
|
|
2202
2574
|
await persistActiveRunId(persistCwd, resolved.sessionId, resolved.runId, resolved.stage);
|
|
@@ -2208,7 +2580,13 @@ async function handleArtifactWrite(
|
|
|
2208
2580
|
await applyLaneVerdictUpdate(persistCwd, resolved.sessionId, laneVerdict);
|
|
2209
2581
|
}
|
|
2210
2582
|
if (autoHandoff) {
|
|
2211
|
-
await persistRalplanFinalAdmission(
|
|
2583
|
+
await persistRalplanFinalAdmission(
|
|
2584
|
+
persistCwd,
|
|
2585
|
+
resolved.sessionId,
|
|
2586
|
+
resolved.runId,
|
|
2587
|
+
autoHandoff,
|
|
2588
|
+
finalPublication!,
|
|
2589
|
+
);
|
|
2212
2590
|
}
|
|
2213
2591
|
await writeSessionActivityMarker(persistCwd, resolved.sessionId, {
|
|
2214
2592
|
writer: "ralplan-runtime",
|
|
@@ -2385,51 +2763,68 @@ async function seedRalplanState(
|
|
|
2385
2763
|
explicitTarget = false,
|
|
2386
2764
|
): Promise<{ statePath: string; runId: string; repositoryBinding: RepositoryBinding }> {
|
|
2387
2765
|
const statePath = ralplanStatePath(cwd, resolved.sessionId);
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2766
|
+
return withWorkflowStateLock(
|
|
2767
|
+
statePath,
|
|
2768
|
+
async () => {
|
|
2769
|
+
const existingStateRead = await readExistingStateForMutation(statePath);
|
|
2770
|
+
if (existingStateRead.kind === "corrupt")
|
|
2771
|
+
throw new RalplanCommandError(
|
|
2772
|
+
2,
|
|
2773
|
+
`existing ralplan state is corrupt or tampered (${existingStateRead.error}); refusing to overwrite ${statePath}`,
|
|
2774
|
+
);
|
|
2775
|
+
const existingRunId =
|
|
2776
|
+
existingStateRead.kind === "valid" && typeof existingStateRead.value.run_id === "string"
|
|
2777
|
+
? existingStateRead.value.run_id.trim() || undefined
|
|
2778
|
+
: undefined;
|
|
2779
|
+
const runId = existingRunId ?? resolved.sessionId ?? defaultRunId();
|
|
2780
|
+
assertSafePathComponent(runId, "run-id");
|
|
2781
|
+
const now = new Date().toISOString();
|
|
2782
|
+
const repositoryBinding = existingRunId
|
|
2783
|
+
? await enforceRalplanRepositoryBinding(cwd, resolved.sessionId, { exactWorktreeRoot: explicitTarget })
|
|
2784
|
+
: publicRepositoryBinding(await captureRepositoryBinding(cwd, { displayPath: cwd }));
|
|
2785
|
+
const payload: Record<string, unknown> = {
|
|
2786
|
+
active: true,
|
|
2787
|
+
current_phase: "planner",
|
|
2788
|
+
skill: "ralplan",
|
|
2789
|
+
version: WORKFLOW_STATE_VERSION,
|
|
2790
|
+
mode: resolved.deliberate ? "deliberate" : "short",
|
|
2791
|
+
interactive: resolved.interactive,
|
|
2792
|
+
task: resolved.task,
|
|
2793
|
+
run_id: runId,
|
|
2794
|
+
updated_at: now,
|
|
2795
|
+
repository_binding: repositoryBinding,
|
|
2796
|
+
};
|
|
2797
|
+
if (existingStateRead.kind === "valid" && existingStateRead.value.active === true) {
|
|
2798
|
+
for (const field of ["handoff_from", "handoff_at"] as const) {
|
|
2799
|
+
if (typeof existingStateRead.value[field] === "string") payload[field] = existingStateRead.value[field];
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
if (resolved.architectKind) payload.architect_kind = resolved.architectKind;
|
|
2803
|
+
if (resolved.criticKind) payload.critic_kind = resolved.criticKind;
|
|
2804
|
+
if (resolved.sessionId) payload.session_id = resolved.sessionId;
|
|
2805
|
+
await writeWorkflowEnvelopeAtomic(statePath, payload, {
|
|
2806
|
+
cwd,
|
|
2807
|
+
lockHeld: true,
|
|
2808
|
+
receipt: {
|
|
2809
|
+
cwd,
|
|
2810
|
+
skill: "ralplan",
|
|
2811
|
+
owner: "gjc-runtime",
|
|
2812
|
+
command: "gjc ralplan seed",
|
|
2813
|
+
sessionId: resolved.sessionId,
|
|
2814
|
+
},
|
|
2815
|
+
audit: {
|
|
2816
|
+
category: "state",
|
|
2817
|
+
verb: "write",
|
|
2818
|
+
owner: "gjc-runtime",
|
|
2819
|
+
skill: "ralplan",
|
|
2820
|
+
sessionId: resolved.sessionId,
|
|
2821
|
+
},
|
|
2822
|
+
});
|
|
2823
|
+
await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "ralplan-runtime", path: statePath });
|
|
2824
|
+
return { statePath, runId, repositoryBinding };
|
|
2429
2825
|
},
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
return { statePath, runId, repositoryBinding };
|
|
2826
|
+
{ cwd },
|
|
2827
|
+
);
|
|
2433
2828
|
}
|
|
2434
2829
|
|
|
2435
2830
|
async function handleConsensusHandoff(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
|
|
@@ -22,6 +22,7 @@ import { reduceTerminalReceiptState } from "../sdk/receipt-state";
|
|
|
22
22
|
import { TOOL_CATALOG } from "../tools/tool-catalog.generated";
|
|
23
23
|
import { sessionRoot, sessionRuntimeDir, sessionRuntimeStatePath } from "./session-layout";
|
|
24
24
|
import { SessionStateLockUnavailableError, withSessionStateFileLock } from "./session-state-lock";
|
|
25
|
+
import { trustedCoordinatorEnv } from "./sidecar-bootstrap-env";
|
|
25
26
|
import {
|
|
26
27
|
isValidOwnerIntent,
|
|
27
28
|
lifecyclePaths,
|
|
@@ -52,9 +53,9 @@ export const GJC_COORDINATOR_SIDECAR_SIGNATURE_REQUIRED_ENV = "GJC_COORDINATOR_S
|
|
|
52
53
|
* children. The process environment is deliberately scrubbed synchronously even when the
|
|
53
54
|
* key is malformed, so a rejected launch cannot leak it to descendants.
|
|
54
55
|
*/
|
|
55
|
-
const coordinatorSidecarSigningBootstrapUrl =
|
|
56
|
+
const coordinatorSidecarSigningBootstrapUrl = trustedCoordinatorEnv(GJC_COORDINATOR_SIDECAR_BOOTSTRAP_URL_ENV);
|
|
56
57
|
const coordinatorSidecarSigningKeyBootstrap = await (async (): Promise<string | undefined> => {
|
|
57
|
-
if (!coordinatorSidecarSigningBootstrapUrl) return
|
|
58
|
+
if (!coordinatorSidecarSigningBootstrapUrl) return trustedCoordinatorEnv(GJC_COORDINATOR_SIDECAR_SIGNING_KEY_ENV);
|
|
58
59
|
try {
|
|
59
60
|
const response = await fetch(coordinatorSidecarSigningBootstrapUrl);
|
|
60
61
|
if (!response.ok) return undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { canonicalEnvKey, type ProjectEnvSnapshot, projectEnvSnapshot } from "@gajae-code/utils/env-file";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Operator environment only. A value that matches the project dotenv snapshot
|
|
5
|
+
* was loaded by Bun from the repository and must not become signing material.
|
|
6
|
+
*/
|
|
7
|
+
export function trustedCoordinatorEnv(
|
|
8
|
+
name: string,
|
|
9
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
10
|
+
snapshot: ProjectEnvSnapshot = projectEnvSnapshot(),
|
|
11
|
+
): string | undefined {
|
|
12
|
+
const raw = env[name];
|
|
13
|
+
if (!raw) return undefined;
|
|
14
|
+
const key = canonicalEnvKey(name);
|
|
15
|
+
const declared = snapshot.values[key];
|
|
16
|
+
if (declared !== undefined && (snapshot.dynamic.has(key) || declared === raw)) return undefined;
|
|
17
|
+
const trimmed = raw.trim();
|
|
18
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
19
|
+
}
|