@gajae-code/coding-agent 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +7 -0
- package/dist/types/cli/args.d.ts +3 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/model-profile-activation.d.ts +30 -0
- package/dist/types/config/model-profiles.d.ts +19 -0
- package/dist/types/config/model-registry.d.ts +8 -0
- package/dist/types/config/model-resolver.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +47 -0
- package/dist/types/config/settings-schema.d.ts +14 -4
- package/dist/types/debug/crash-diagnostics.d.ts +45 -0
- package/dist/types/debug/runtime-gauges.d.ts +6 -0
- package/dist/types/deep-interview/render-middleware.d.ts +1 -0
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/eval/py/kernel.d.ts +2 -0
- package/dist/types/exec/bash-executor.d.ts +10 -0
- package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +9 -0
- package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +3 -2
- package/dist/types/hooks/skill-state.d.ts +21 -0
- package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
- package/dist/types/internal-urls/types.d.ts +4 -0
- package/dist/types/lsp/index.d.ts +10 -10
- package/dist/types/main.d.ts +10 -1
- package/dist/types/modes/bridge/auth.d.ts +12 -0
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
- package/dist/types/modes/bridge/event-stream.d.ts +8 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +10 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
- package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
- package/dist/types/modes/components/model-selector.d.ts +6 -1
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +9 -0
- package/dist/types/modes/index.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/jobs-observer.d.ts +57 -0
- package/dist/types/modes/rpc/host-tools.d.ts +1 -16
- package/dist/types/modes/rpc/host-uris.d.ts +1 -38
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
- package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +11 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +1 -2
- package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/id.d.ts +7 -0
- package/dist/types/task/index.d.ts +5 -0
- package/dist/types/task/receipt.d.ts +85 -0
- package/dist/types/task/spawn-gate.d.ts +38 -0
- package/dist/types/task/types.d.ts +143 -11
- package/dist/types/tools/cron.d.ts +6 -0
- package/dist/types/tools/hindsight-recall.d.ts +0 -2
- package/dist/types/tools/hindsight-reflect.d.ts +0 -2
- package/dist/types/tools/hindsight-retain.d.ts +0 -2
- package/dist/types/tools/index.d.ts +6 -4
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +15 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +36 -0
- package/src/cli/args.ts +19 -2
- package/src/commands/deep-interview.ts +1 -0
- package/src/commands/harness.ts +289 -19
- package/src/commands/launch.ts +10 -2
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +22 -4
- package/src/config/keybindings.ts +6 -0
- package/src/config/model-profile-activation.ts +157 -0
- package/src/config/model-profiles.ts +155 -0
- package/src/config/model-registry.ts +19 -0
- package/src/config/model-resolver.ts +3 -2
- package/src/config/models-config-schema.ts +36 -0
- package/src/config/settings-schema.ts +16 -3
- package/src/dap/client.ts +17 -3
- package/src/debug/crash-diagnostics.ts +223 -0
- package/src/debug/runtime-gauges.ts +20 -0
- package/src/deep-interview/render-middleware.ts +6 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -3
- package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +61 -0
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/eval/py/executor.ts +21 -1
- package/src/eval/py/kernel.ts +15 -0
- package/src/exec/bash-executor.ts +41 -0
- package/src/gjc-runtime/cli-write-receipt.ts +31 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +69 -32
- package/src/gjc-runtime/ralplan-runtime.ts +213 -36
- package/src/gjc-runtime/state-migrations.ts +54 -7
- package/src/gjc-runtime/state-runtime.ts +461 -64
- package/src/gjc-runtime/state-schema.ts +192 -0
- package/src/gjc-runtime/state-writer.ts +32 -1
- package/src/gjc-runtime/team-runtime.ts +177 -105
- package/src/gjc-runtime/ultragoal-runtime.ts +231 -38
- package/src/gjc-runtime/workflow-command-ref.ts +239 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +108 -4
- package/src/gjc-runtime/workflow-manifest.ts +3 -1
- package/src/harness-control-plane/control-endpoint.ts +19 -8
- package/src/harness-control-plane/owner.ts +57 -10
- package/src/harness-control-plane/state-machine.ts +2 -1
- package/src/hooks/skill-state.ts +176 -26
- package/src/internal-urls/agent-protocol.ts +68 -21
- package/src/internal-urls/artifact-protocol.ts +12 -17
- package/src/internal-urls/docs-index.generated.ts +8 -10
- package/src/internal-urls/registry-helpers.ts +19 -16
- package/src/internal-urls/types.ts +4 -0
- package/src/lsp/client.ts +18 -2
- package/src/main.ts +88 -6
- package/src/modes/bridge/auth.ts +41 -0
- package/src/modes/bridge/bridge-client-bridge.ts +47 -0
- package/src/modes/bridge/bridge-mode.ts +520 -0
- package/src/modes/bridge/bridge-ui-context.ts +200 -0
- package/src/modes/bridge/event-stream.ts +70 -0
- package/src/modes/components/custom-editor.ts +101 -0
- package/src/modes/components/custom-provider-wizard.ts +318 -0
- package/src/modes/components/hook-selector.ts +61 -18
- package/src/modes/components/jobs-overlay-model.ts +109 -0
- package/src/modes/components/jobs-overlay.ts +172 -0
- package/src/modes/components/model-selector.ts +108 -18
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/status-line/presets.ts +7 -5
- package/src/modes/components/status-line/segments.ts +25 -0
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/status-line.ts +9 -1
- package/src/modes/controllers/extension-ui-controller.ts +39 -3
- package/src/modes/controllers/input-controller.ts +97 -9
- package/src/modes/controllers/selector-controller.ts +86 -1
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +27 -0
- package/src/modes/jobs-observer.ts +204 -0
- package/src/modes/rpc/host-tools.ts +1 -186
- package/src/modes/rpc/host-uris.ts +1 -235
- package/src/modes/rpc/rpc-client.ts +25 -10
- package/src/modes/rpc/rpc-mode.ts +12 -381
- package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
- package/src/modes/shared/agent-wire/command-validation.ts +131 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
- package/src/modes/shared/agent-wire/handshake.ts +117 -0
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
- package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
- package/src/modes/shared/agent-wire/protocol.ts +96 -0
- package/src/modes/shared/agent-wire/responses.ts +17 -0
- package/src/modes/shared/agent-wire/scopes.ts +89 -0
- package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
- package/src/modes/shared/agent-wire/ui-result.ts +48 -0
- package/src/modes/types.ts +2 -0
- package/src/prompts/memories/consolidation.md +1 -1
- package/src/prompts/memories/read-path.md +6 -7
- package/src/prompts/memories/unavailable.md +2 -2
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/recall.md +1 -0
- package/src/prompts/tools/reflect.md +1 -0
- package/src/prompts/tools/retain.md +1 -0
- package/src/prompts/tools/subagent.md +12 -7
- package/src/prompts/tools/task-summary.md +3 -9
- package/src/prompts/tools/task.md +5 -1
- package/src/sdk.ts +5 -1
- package/src/session/agent-session.ts +214 -38
- package/src/skill-state/deep-interview-mutation-guard.ts +23 -4
- package/src/skill-state/workflow-state-contract.ts +7 -4
- package/src/skill-state/workflow-state-version.ts +3 -0
- package/src/slash-commands/builtin-registry.ts +9 -1
- package/src/task/executor.ts +31 -5
- package/src/task/id.ts +33 -0
- package/src/task/index.ts +259 -67
- package/src/task/output-manager.ts +5 -4
- package/src/task/receipt.ts +297 -0
- package/src/task/render.ts +48 -131
- package/src/task/spawn-gate.ts +132 -0
- package/src/task/types.ts +48 -7
- package/src/tools/ask.ts +73 -33
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/cron.ts +48 -0
- package/src/tools/find.ts +4 -1
- package/src/tools/hindsight-recall.ts +0 -2
- package/src/tools/hindsight-reflect.ts +0 -2
- package/src/tools/hindsight-retain.ts +0 -2
- package/src/tools/index.ts +6 -18
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +4 -3
- package/src/tools/search.ts +1 -0
- package/src/tools/skill.ts +6 -1
- package/src/tools/subagent.ts +237 -84
|
@@ -2,12 +2,12 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import { Settings } from "../config/settings";
|
|
6
5
|
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
7
6
|
import { buildDeepInterviewHudSummary } from "../skill-state/workflow-hud";
|
|
7
|
+
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
8
8
|
import { runNativeRalplanCommand } from "./ralplan-runtime";
|
|
9
9
|
import { runNativeStateCommand } from "./state-runtime";
|
|
10
|
-
import { appendJsonl, writeArtifact,
|
|
10
|
+
import { appendJsonl, readExistingStateForMutation, writeArtifact, writeWorkflowEnvelopeAtomic } from "./state-writer";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Native implementation of `gjc deep-interview`.
|
|
@@ -96,16 +96,6 @@ function deepInterviewStatePath(cwd: string, sessionId: string | undefined): str
|
|
|
96
96
|
return path.join(stateDirFor(cwd, sessionId), "deep-interview-state.json");
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
async function readJsonObject(filePath: string): Promise<Record<string, unknown>> {
|
|
100
|
-
try {
|
|
101
|
-
const parsed = JSON.parse(await fs.readFile(filePath, "utf-8"));
|
|
102
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record<string, unknown>;
|
|
103
|
-
} catch {
|
|
104
|
-
// Missing/corrupt state should not prevent the sanctioned persistence CLI from writing a receipt.
|
|
105
|
-
}
|
|
106
|
-
return {};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
99
|
async function resolveSpecContent(rawSpec: string, cwd: string): Promise<string> {
|
|
110
100
|
const candidate = path.isAbsolute(rawSpec) ? rawSpec : path.resolve(cwd, rawSpec);
|
|
111
101
|
try {
|
|
@@ -145,6 +135,7 @@ export interface ResolvedDeepInterviewSpecWriteArgs {
|
|
|
145
135
|
json: boolean;
|
|
146
136
|
deliberate: boolean;
|
|
147
137
|
handoff?: "ralplan";
|
|
138
|
+
force: boolean;
|
|
148
139
|
}
|
|
149
140
|
|
|
150
141
|
export interface PersistedDeepInterviewSpec {
|
|
@@ -162,6 +153,8 @@ interface DeepInterviewSpecWriteSummary {
|
|
|
162
153
|
slug: string;
|
|
163
154
|
path: string;
|
|
164
155
|
sha256: string;
|
|
156
|
+
spec_path: string;
|
|
157
|
+
sha: string;
|
|
165
158
|
created_at: string;
|
|
166
159
|
state_path: string;
|
|
167
160
|
handoff?: {
|
|
@@ -197,11 +190,16 @@ async function readSettingsAmbiguityThreshold(
|
|
|
197
190
|
return { threshold: candidate, source: settingsPath };
|
|
198
191
|
}
|
|
199
192
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
203
|
-
const
|
|
204
|
-
|
|
193
|
+
function modernSettingsPath(): string {
|
|
194
|
+
const configDir = process.env.GJC_CODING_AGENT_DIR?.trim() || process.env.PI_CODING_AGENT_DIR?.trim();
|
|
195
|
+
if (configDir) return path.join(configDir, "config.yml");
|
|
196
|
+
const configRoot = process.env.GJC_CONFIG_DIR?.trim() || process.env.PI_CONFIG_DIR?.trim();
|
|
197
|
+
if (configRoot) return path.join(configRoot, "agent", "config.yml");
|
|
198
|
+
return path.join(os.homedir(), ".gjc", "agent", "config.yml");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function readModernSettingsAmbiguityThreshold(): Promise<{ threshold: number; source: string } | undefined> {
|
|
202
|
+
const modernConfigPath = modernSettingsPath();
|
|
205
203
|
let parsed: unknown;
|
|
206
204
|
try {
|
|
207
205
|
parsed = (await import("bun")).YAML.parse(await fs.readFile(modernConfigPath, "utf-8"));
|
|
@@ -218,7 +216,7 @@ async function readModernSettingsAmbiguityThreshold(
|
|
|
218
216
|
async function resolveConfiguredAmbiguityThreshold(
|
|
219
217
|
cwd: string,
|
|
220
218
|
): Promise<{ threshold: number; source: string } | undefined> {
|
|
221
|
-
const modernValue = await readModernSettingsAmbiguityThreshold(
|
|
219
|
+
const modernValue = await readModernSettingsAmbiguityThreshold();
|
|
222
220
|
if (modernValue) return modernValue;
|
|
223
221
|
const projectSettings = path.join(cwd, ".gjc", "settings.json");
|
|
224
222
|
const projectValue = await readSettingsAmbiguityThreshold(projectSettings);
|
|
@@ -292,6 +290,7 @@ async function resolveSpecWriteArgs(args: readonly string[], cwd: string): Promi
|
|
|
292
290
|
"--handoff",
|
|
293
291
|
"--deliberate",
|
|
294
292
|
"--json",
|
|
293
|
+
"--force",
|
|
295
294
|
]);
|
|
296
295
|
let skipNext = false;
|
|
297
296
|
for (const arg of args) {
|
|
@@ -315,6 +314,7 @@ async function resolveSpecWriteArgs(args: readonly string[], cwd: string): Promi
|
|
|
315
314
|
sessionId,
|
|
316
315
|
json: hasFlag(args, "--json"),
|
|
317
316
|
deliberate: hasFlag(args, "--deliberate"),
|
|
317
|
+
force: hasFlag(args, "--force"),
|
|
318
318
|
handoff: rawHandoff as "ralplan" | undefined,
|
|
319
319
|
};
|
|
320
320
|
}
|
|
@@ -388,6 +388,16 @@ export async function persistDeepInterviewSpec(
|
|
|
388
388
|
cwd: string,
|
|
389
389
|
resolved: ResolvedDeepInterviewSpecWriteArgs,
|
|
390
390
|
): Promise<PersistedDeepInterviewSpec> {
|
|
391
|
+
const statePath = deepInterviewStatePath(cwd, resolved.sessionId);
|
|
392
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
393
|
+
if (existingRead.kind === "corrupt" && !resolved.force) {
|
|
394
|
+
throw new DeepInterviewCommandError(
|
|
395
|
+
2,
|
|
396
|
+
`existing deep-interview state is corrupt or tampered (${existingRead.error}); use --force to overwrite ${statePath}`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const existing = existingRead.kind === "valid" ? existingRead.value : {};
|
|
400
|
+
|
|
391
401
|
const specPath = path.join(cwd, ".gjc", "specs", `deep-interview-${resolved.slug}.md`);
|
|
392
402
|
const content = resolved.spec.endsWith("\n") ? resolved.spec : `${resolved.spec}\n`;
|
|
393
403
|
await writeArtifact(specPath, content, {
|
|
@@ -403,14 +413,12 @@ export async function persistDeepInterviewSpec(
|
|
|
403
413
|
{ cwd, audit: { category: "ledger", verb: "append", owner: "gjc-runtime", skill: "deep-interview" } },
|
|
404
414
|
);
|
|
405
415
|
|
|
406
|
-
const statePath = deepInterviewStatePath(cwd, resolved.sessionId);
|
|
407
|
-
const existing = await readJsonObject(statePath);
|
|
408
416
|
const payload: Record<string, unknown> = {
|
|
409
417
|
...existing,
|
|
410
418
|
active: true,
|
|
411
419
|
current_phase: "handoff",
|
|
412
420
|
skill: "deep-interview",
|
|
413
|
-
version:
|
|
421
|
+
version: WORKFLOW_STATE_VERSION,
|
|
414
422
|
spec_slug: resolved.slug,
|
|
415
423
|
spec_path: specPath,
|
|
416
424
|
spec_sha256: sha256,
|
|
@@ -419,9 +427,23 @@ export async function persistDeepInterviewSpec(
|
|
|
419
427
|
updated_at: createdAt,
|
|
420
428
|
};
|
|
421
429
|
if (resolved.sessionId) payload.session_id = resolved.sessionId;
|
|
422
|
-
await
|
|
430
|
+
await writeWorkflowEnvelopeAtomic(statePath, payload, {
|
|
423
431
|
cwd,
|
|
424
|
-
|
|
432
|
+
receipt: {
|
|
433
|
+
cwd,
|
|
434
|
+
skill: "deep-interview",
|
|
435
|
+
owner: "gjc-runtime",
|
|
436
|
+
command: "gjc deep-interview persist-spec-state",
|
|
437
|
+
sessionId: resolved.sessionId,
|
|
438
|
+
nowIso: createdAt,
|
|
439
|
+
},
|
|
440
|
+
audit: {
|
|
441
|
+
category: "state",
|
|
442
|
+
verb: "write",
|
|
443
|
+
owner: "gjc-runtime",
|
|
444
|
+
skill: "deep-interview",
|
|
445
|
+
forced: resolved.force,
|
|
446
|
+
},
|
|
425
447
|
});
|
|
426
448
|
await syncDeepInterviewHud({
|
|
427
449
|
cwd,
|
|
@@ -447,6 +469,7 @@ async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepIntervi
|
|
|
447
469
|
active: true,
|
|
448
470
|
current_phase: "interviewing",
|
|
449
471
|
skill: "deep-interview",
|
|
472
|
+
version: WORKFLOW_STATE_VERSION,
|
|
450
473
|
resolution: resolved.resolution,
|
|
451
474
|
threshold: resolved.threshold,
|
|
452
475
|
threshold_source: resolved.thresholdSource,
|
|
@@ -464,8 +487,16 @@ async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepIntervi
|
|
|
464
487
|
(payload.state as Record<string, unknown>).language = resolved.language;
|
|
465
488
|
}
|
|
466
489
|
if (resolved.sessionId) payload.session_id = resolved.sessionId;
|
|
467
|
-
await
|
|
490
|
+
await writeWorkflowEnvelopeAtomic(statePath, payload, {
|
|
468
491
|
cwd,
|
|
492
|
+
receipt: {
|
|
493
|
+
cwd,
|
|
494
|
+
skill: "deep-interview",
|
|
495
|
+
owner: "gjc-runtime",
|
|
496
|
+
command: "gjc deep-interview seed",
|
|
497
|
+
sessionId: resolved.sessionId,
|
|
498
|
+
nowIso: now,
|
|
499
|
+
},
|
|
469
500
|
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "deep-interview" },
|
|
470
501
|
});
|
|
471
502
|
return statePath;
|
|
@@ -512,6 +543,8 @@ async function handleSpecWrite(args: readonly string[], cwd: string): Promise<De
|
|
|
512
543
|
slug: persisted.slug,
|
|
513
544
|
path: persisted.path,
|
|
514
545
|
sha256: persisted.sha256,
|
|
546
|
+
spec_path: persisted.path,
|
|
547
|
+
sha: persisted.sha256,
|
|
515
548
|
created_at: persisted.createdAt,
|
|
516
549
|
state_path: persisted.statePath,
|
|
517
550
|
};
|
|
@@ -549,10 +582,14 @@ async function handleSpecWrite(args: readonly string[], cwd: string): Promise<De
|
|
|
549
582
|
}
|
|
550
583
|
|
|
551
584
|
const stdout = resolved.json
|
|
552
|
-
? `${JSON.stringify(summary
|
|
585
|
+
? `${JSON.stringify(summary)}\n`
|
|
553
586
|
: [
|
|
554
|
-
`
|
|
555
|
-
|
|
587
|
+
`deep-interview spec_path=${persisted.path}`,
|
|
588
|
+
`sha=${persisted.sha256}`,
|
|
589
|
+
`state_path=${persisted.statePath}`,
|
|
590
|
+
shouldHandoff
|
|
591
|
+
? `handoff=ralplan run_id=${summary.handoff?.run_id ?? ""} state_path=${summary.handoff?.state_path ?? ""}`
|
|
592
|
+
: undefined,
|
|
556
593
|
"",
|
|
557
594
|
]
|
|
558
595
|
.filter((line): line is string => Boolean(line))
|
|
@@ -591,14 +628,14 @@ export async function runNativeDeepInterviewCommand(
|
|
|
591
628
|
idea: resolved.idea,
|
|
592
629
|
language: resolved.language,
|
|
593
630
|
state_path: statePath,
|
|
594
|
-
handoff: "
|
|
631
|
+
handoff: "/skill:deep-interview",
|
|
595
632
|
};
|
|
596
633
|
const stdout = resolved.json
|
|
597
|
-
? `${JSON.stringify(summary
|
|
634
|
+
? `${JSON.stringify(summary)}\n`
|
|
598
635
|
: [
|
|
599
|
-
`
|
|
600
|
-
`
|
|
601
|
-
"
|
|
636
|
+
`deep-interview seed state_path=${statePath}`,
|
|
637
|
+
`resolution=${resolved.resolution} threshold=${resolved.threshold} threshold_source=${resolved.thresholdSource}`,
|
|
638
|
+
"handoff=/skill:deep-interview",
|
|
602
639
|
"",
|
|
603
640
|
].join("\n");
|
|
604
641
|
return { status: 0, stdout };
|
|
@@ -3,8 +3,11 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { syncSkillActiveState } from "../skill-state/active-state";
|
|
5
5
|
import { buildRalplanHudSummary } from "../skill-state/workflow-hud";
|
|
6
|
+
import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
|
|
7
|
+
import { renderCliWriteReceipt } from "./cli-write-receipt";
|
|
6
8
|
import { isRestrictedRoleAgentBash } from "./restricted-role-agent-bash";
|
|
7
|
-
import {
|
|
9
|
+
import { migrateWorkflowState } from "./state-migrations";
|
|
10
|
+
import { appendJsonl, readExistingStateForMutation, writeArtifact, writeWorkflowEnvelopeAtomic } from "./state-writer";
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Native implementation of `gjc ralplan`.
|
|
@@ -39,6 +42,17 @@ const KNOWN_CRITIC_KINDS = new Set(["openai-code"]);
|
|
|
39
42
|
|
|
40
43
|
const PATH_COMPONENT_RE = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
|
|
41
44
|
|
|
45
|
+
const SUBAGENT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/;
|
|
46
|
+
|
|
47
|
+
const KNOWN_FALLBACK_REASONS = new Set([
|
|
48
|
+
"context_unavailable",
|
|
49
|
+
"not_found",
|
|
50
|
+
"no_runner",
|
|
51
|
+
"resume_failed",
|
|
52
|
+
"process_restart",
|
|
53
|
+
"missing_record",
|
|
54
|
+
]);
|
|
55
|
+
|
|
42
56
|
class RalplanCommandError extends Error {
|
|
43
57
|
constructor(
|
|
44
58
|
public readonly exitStatus: number,
|
|
@@ -57,6 +71,12 @@ const VALUE_FLAGS = new Set([
|
|
|
57
71
|
"--session-id",
|
|
58
72
|
"--architect",
|
|
59
73
|
"--critic",
|
|
74
|
+
"--planner-id",
|
|
75
|
+
"--planner-resumable",
|
|
76
|
+
"--fallback-reason",
|
|
77
|
+
"--fallback-attempted-id",
|
|
78
|
+
"--fallback-stage-n",
|
|
79
|
+
"--fallback-receipt-path",
|
|
60
80
|
]);
|
|
61
81
|
|
|
62
82
|
function flagValue(args: readonly string[], flag: string): string | undefined {
|
|
@@ -145,37 +165,188 @@ function ralplanStatePath(cwd: string, sessionId: string | undefined): string {
|
|
|
145
165
|
}
|
|
146
166
|
|
|
147
167
|
async function readActiveRunId(cwd: string, sessionId: string | undefined): Promise<string | undefined> {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return undefined;
|
|
168
|
+
const statePath = ralplanStatePath(cwd, sessionId);
|
|
169
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
170
|
+
if (existingRead.kind === "absent") return undefined;
|
|
171
|
+
if (existingRead.kind === "corrupt") {
|
|
172
|
+
throw new RalplanCommandError(
|
|
173
|
+
2,
|
|
174
|
+
`existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to overwrite ${statePath}`,
|
|
175
|
+
);
|
|
157
176
|
}
|
|
177
|
+
const candidate = typeof existingRead.value.run_id === "string" ? existingRead.value.run_id.trim() : "";
|
|
178
|
+
if (!candidate) return undefined;
|
|
179
|
+
assertSafePathComponent(candidate, "run-id");
|
|
180
|
+
return candidate;
|
|
158
181
|
}
|
|
159
182
|
|
|
160
183
|
async function persistActiveRunId(cwd: string, sessionId: string | undefined, runId: string): Promise<void> {
|
|
161
184
|
const statePath = ralplanStatePath(cwd, sessionId);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
} catch {
|
|
170
|
-
// fresh receipt; fall through to create
|
|
185
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
186
|
+
if (existingRead.kind === "corrupt") {
|
|
187
|
+
throw new RalplanCommandError(
|
|
188
|
+
2,
|
|
189
|
+
`existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to overwrite ${statePath}`,
|
|
190
|
+
);
|
|
171
191
|
}
|
|
172
|
-
|
|
192
|
+
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
193
|
+
|
|
194
|
+
if (existing.run_id === runId && existing.version === WORKFLOW_STATE_VERSION) return;
|
|
173
195
|
existing.run_id = runId;
|
|
174
196
|
if (typeof existing.skill !== "string") existing.skill = "ralplan";
|
|
175
197
|
if (typeof existing.active !== "boolean") existing.active = true;
|
|
198
|
+
if (typeof existing.current_phase !== "string") existing.current_phase = "planner";
|
|
199
|
+
existing = migrateWorkflowState(existing, "ralplan").state;
|
|
176
200
|
existing.updated_at = new Date().toISOString();
|
|
177
|
-
await
|
|
201
|
+
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
178
202
|
cwd,
|
|
203
|
+
receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan persist-run-id", sessionId },
|
|
204
|
+
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan" },
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* --------------------------- planner run-state --------------------------- */
|
|
209
|
+
|
|
210
|
+
interface PlannerStateUpdate {
|
|
211
|
+
subagentId?: string;
|
|
212
|
+
resumable?: boolean;
|
|
213
|
+
fallbackReason?: string;
|
|
214
|
+
fallbackAttemptedId?: string;
|
|
215
|
+
fallbackStageN?: number;
|
|
216
|
+
fallbackReceiptPath?: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parseBooleanFlag(raw: string, flag: string): boolean {
|
|
220
|
+
if (raw === "true") return true;
|
|
221
|
+
if (raw === "false") return false;
|
|
222
|
+
throw new RalplanCommandError(2, `invalid ${flag}: ${raw}. Expected "true" or "false".`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function assertSubagentId(value: string, label: string): void {
|
|
226
|
+
if (!SUBAGENT_ID_RE.test(value)) {
|
|
227
|
+
throw new RalplanCommandError(2, `invalid ${label}: ${value}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function plannerFlagValue(args: readonly string[], flag: string): string | undefined {
|
|
232
|
+
const value = flagValue(args, flag);
|
|
233
|
+
if (value === undefined && hasFlag(args, flag)) {
|
|
234
|
+
throw new RalplanCommandError(2, `missing value for ${flag}.`);
|
|
235
|
+
}
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Parse the optional persisted-Planner metadata flags that may ride alongside a
|
|
241
|
+
* `--write`. Returns `undefined` when none are present so existing writes are
|
|
242
|
+
* unaffected. Throws `RalplanCommandError` on any malformed value. This records
|
|
243
|
+
* a same-session audit/routing hint, not a durable subagent registry.
|
|
244
|
+
*/
|
|
245
|
+
function parsePlannerStateArgs(args: readonly string[]): PlannerStateUpdate | undefined {
|
|
246
|
+
const subagentId = plannerFlagValue(args, "--planner-id");
|
|
247
|
+
const resumableRaw = plannerFlagValue(args, "--planner-resumable");
|
|
248
|
+
const fallbackReason = plannerFlagValue(args, "--fallback-reason");
|
|
249
|
+
const fallbackAttemptedId = plannerFlagValue(args, "--fallback-attempted-id");
|
|
250
|
+
const fallbackStageNRaw = plannerFlagValue(args, "--fallback-stage-n");
|
|
251
|
+
const fallbackReceiptPath = plannerFlagValue(args, "--fallback-receipt-path");
|
|
252
|
+
|
|
253
|
+
const anyPresent = [
|
|
254
|
+
subagentId,
|
|
255
|
+
resumableRaw,
|
|
256
|
+
fallbackReason,
|
|
257
|
+
fallbackAttemptedId,
|
|
258
|
+
fallbackStageNRaw,
|
|
259
|
+
fallbackReceiptPath,
|
|
260
|
+
].some(value => value !== undefined);
|
|
261
|
+
if (!anyPresent) return undefined;
|
|
262
|
+
|
|
263
|
+
const update: PlannerStateUpdate = {};
|
|
264
|
+
|
|
265
|
+
if (subagentId !== undefined) {
|
|
266
|
+
assertSubagentId(subagentId, "--planner-id");
|
|
267
|
+
update.subagentId = subagentId;
|
|
268
|
+
}
|
|
269
|
+
if (resumableRaw !== undefined) {
|
|
270
|
+
update.resumable = parseBooleanFlag(resumableRaw, "--planner-resumable");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const anyFallback = [fallbackReason, fallbackAttemptedId, fallbackStageNRaw, fallbackReceiptPath].some(
|
|
274
|
+
value => value !== undefined,
|
|
275
|
+
);
|
|
276
|
+
if (anyFallback) {
|
|
277
|
+
if (!fallbackReason) {
|
|
278
|
+
throw new RalplanCommandError(2, "--fallback-reason is required when recording planner fallback metadata.");
|
|
279
|
+
}
|
|
280
|
+
if (!KNOWN_FALLBACK_REASONS.has(fallbackReason)) {
|
|
281
|
+
throw new RalplanCommandError(
|
|
282
|
+
2,
|
|
283
|
+
`invalid --fallback-reason: ${fallbackReason}. Expected one of: ${[...KNOWN_FALLBACK_REASONS].join(", ")}.`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
update.fallbackReason = fallbackReason;
|
|
287
|
+
if (fallbackAttemptedId === undefined) {
|
|
288
|
+
throw new RalplanCommandError(
|
|
289
|
+
2,
|
|
290
|
+
"--fallback-attempted-id is required when recording planner fallback metadata.",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
assertSubagentId(fallbackAttemptedId, "--fallback-attempted-id");
|
|
294
|
+
update.fallbackAttemptedId = fallbackAttemptedId;
|
|
295
|
+
if (fallbackStageNRaw === undefined) {
|
|
296
|
+
throw new RalplanCommandError(2, "--fallback-stage-n is required when recording planner fallback metadata.");
|
|
297
|
+
}
|
|
298
|
+
update.fallbackStageN = parseStageN(fallbackStageNRaw);
|
|
299
|
+
if (fallbackReceiptPath !== undefined) {
|
|
300
|
+
if (fallbackReceiptPath.trim() === "") {
|
|
301
|
+
throw new RalplanCommandError(2, "--fallback-receipt-path must not be empty.");
|
|
302
|
+
}
|
|
303
|
+
update.fallbackReceiptPath = fallbackReceiptPath;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return update;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Snake-case projection of a PlannerStateUpdate for state JSON + receipts. Omitted fields stay absent — an unknown `planner_resumable` is encoded by omission, never literal null. */
|
|
311
|
+
function plannerStatePayload(update: PlannerStateUpdate): Record<string, unknown> {
|
|
312
|
+
const payload: Record<string, unknown> = {};
|
|
313
|
+
if (update.subagentId !== undefined) payload.planner_subagent_id = update.subagentId;
|
|
314
|
+
if (update.resumable !== undefined) payload.planner_resumable = update.resumable;
|
|
315
|
+
if (update.fallbackReason !== undefined) payload.planner_fallback_reason = update.fallbackReason;
|
|
316
|
+
if (update.fallbackAttemptedId !== undefined) payload.planner_fallback_attempted_id = update.fallbackAttemptedId;
|
|
317
|
+
if (update.fallbackStageN !== undefined) payload.planner_fallback_stage_n = update.fallbackStageN;
|
|
318
|
+
if (update.fallbackReceiptPath !== undefined) payload.planner_fallback_receipt_path = update.fallbackReceiptPath;
|
|
319
|
+
return payload;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Merge persisted-Planner metadata into the ralplan run-state JSON. Same-session
|
|
324
|
+
* audit/routing hint only — it records what the caller has already proven and is
|
|
325
|
+
* NOT a durable cross-process subagent registry.
|
|
326
|
+
*/
|
|
327
|
+
async function applyPlannerStateUpdate(
|
|
328
|
+
cwd: string,
|
|
329
|
+
sessionId: string | undefined,
|
|
330
|
+
update: PlannerStateUpdate,
|
|
331
|
+
): Promise<void> {
|
|
332
|
+
const statePath = ralplanStatePath(cwd, sessionId);
|
|
333
|
+
const existingRead = await readExistingStateForMutation(statePath);
|
|
334
|
+
if (existingRead.kind === "corrupt") {
|
|
335
|
+
throw new RalplanCommandError(
|
|
336
|
+
2,
|
|
337
|
+
`existing ralplan state is corrupt or tampered (${existingRead.error}); refusing to overwrite ${statePath}`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
let existing: Record<string, unknown> = existingRead.kind === "valid" ? existingRead.value : {};
|
|
341
|
+
Object.assign(existing, plannerStatePayload(update));
|
|
342
|
+
if (typeof existing.skill !== "string") existing.skill = "ralplan";
|
|
343
|
+
if (typeof existing.active !== "boolean") existing.active = true;
|
|
344
|
+
if (typeof existing.current_phase !== "string") existing.current_phase = "planner";
|
|
345
|
+
existing = migrateWorkflowState(existing, "ralplan").state;
|
|
346
|
+
existing.updated_at = new Date().toISOString();
|
|
347
|
+
await writeWorkflowEnvelopeAtomic(statePath, existing, {
|
|
348
|
+
cwd,
|
|
349
|
+
receipt: { cwd, skill: "ralplan", owner: "gjc-runtime", command: "gjc ralplan planner-state", sessionId },
|
|
179
350
|
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan" },
|
|
180
351
|
});
|
|
181
352
|
}
|
|
@@ -296,8 +467,12 @@ async function syncRalplanHud(options: {
|
|
|
296
467
|
}
|
|
297
468
|
|
|
298
469
|
async function handleArtifactWrite(args: readonly string[], cwd: string): Promise<RalplanCommandResult> {
|
|
470
|
+
const plannerState = parsePlannerStateArgs(args);
|
|
299
471
|
const resolved = await resolveArtifactArgs(args, cwd);
|
|
300
472
|
const persisted = await persistArtifact(resolved, cwd);
|
|
473
|
+
if (plannerState) {
|
|
474
|
+
await applyPlannerStateUpdate(cwd, resolved.sessionId, plannerState);
|
|
475
|
+
}
|
|
301
476
|
await syncRalplanHud({
|
|
302
477
|
cwd,
|
|
303
478
|
sessionId: resolved.sessionId,
|
|
@@ -315,6 +490,7 @@ async function handleArtifactWrite(args: readonly string[], cwd: string): Promis
|
|
|
315
490
|
created_at: persisted.createdAt,
|
|
316
491
|
};
|
|
317
492
|
if (persisted.pendingApprovalPath) payload.pending_approval_path = persisted.pendingApprovalPath;
|
|
493
|
+
if (plannerState) payload.planner_state = plannerStatePayload(plannerState);
|
|
318
494
|
const stdout = resolved.json
|
|
319
495
|
? `${JSON.stringify(payload, null, 2)}\n`
|
|
320
496
|
: `Persisted ralplan ${persisted.stage} stage ${persisted.stageN} at ${persisted.path}.\n`;
|
|
@@ -406,6 +582,7 @@ async function seedRalplanState(
|
|
|
406
582
|
active: true,
|
|
407
583
|
current_phase: "planner",
|
|
408
584
|
skill: "ralplan",
|
|
585
|
+
version: WORKFLOW_STATE_VERSION,
|
|
409
586
|
mode: resolved.deliberate ? "deliberate" : "short",
|
|
410
587
|
interactive: resolved.interactive,
|
|
411
588
|
task: resolved.task,
|
|
@@ -415,8 +592,15 @@ async function seedRalplanState(
|
|
|
415
592
|
if (resolved.architectKind) payload.architect_kind = resolved.architectKind;
|
|
416
593
|
if (resolved.criticKind) payload.critic_kind = resolved.criticKind;
|
|
417
594
|
if (resolved.sessionId) payload.session_id = resolved.sessionId;
|
|
418
|
-
await
|
|
595
|
+
await writeWorkflowEnvelopeAtomic(statePath, payload, {
|
|
419
596
|
cwd,
|
|
597
|
+
receipt: {
|
|
598
|
+
cwd,
|
|
599
|
+
skill: "ralplan",
|
|
600
|
+
owner: "gjc-runtime",
|
|
601
|
+
command: "gjc ralplan seed",
|
|
602
|
+
sessionId: resolved.sessionId,
|
|
603
|
+
},
|
|
420
604
|
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "ralplan" },
|
|
421
605
|
});
|
|
422
606
|
return { statePath, runId };
|
|
@@ -441,26 +625,19 @@ async function handleConsensusHandoff(args: readonly string[], cwd: string): Pro
|
|
|
441
625
|
const summary = {
|
|
442
626
|
skill: "ralplan",
|
|
443
627
|
mode,
|
|
444
|
-
interactive: resolved.interactive,
|
|
445
|
-
architect: resolved.architectKind ?? "default",
|
|
446
|
-
critic: resolved.criticKind ?? "default",
|
|
447
|
-
task: resolved.task,
|
|
448
628
|
state_path: statePath,
|
|
449
629
|
run_id: runId,
|
|
450
|
-
handoff: "
|
|
630
|
+
handoff: "/skill:ralplan",
|
|
451
631
|
};
|
|
452
632
|
const stdout = resolved.json
|
|
453
|
-
?
|
|
633
|
+
? renderCliWriteReceipt({ ok: true, ...summary })
|
|
454
634
|
: [
|
|
455
|
-
`
|
|
456
|
-
`
|
|
457
|
-
resolved.architectKind
|
|
458
|
-
|
|
459
|
-
"Run `/skill:ralplan` inside the GJC agent to execute the consensus loop.",
|
|
635
|
+
`ralplan seed run_id=${runId}`,
|
|
636
|
+
`state_path=${statePath}`,
|
|
637
|
+
`mode=${mode} interactive=${resolved.interactive} architect=${resolved.architectKind ?? "default"} critic=${resolved.criticKind ?? "default"}`,
|
|
638
|
+
"handoff=/skill:ralplan",
|
|
460
639
|
"",
|
|
461
|
-
]
|
|
462
|
-
.filter((line): line is string => Boolean(line))
|
|
463
|
-
.join("\n");
|
|
640
|
+
].join("\n");
|
|
464
641
|
return { status: 0, stdout };
|
|
465
642
|
}
|
|
466
643
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import type { CanonicalGjcWorkflowSkill } from "../skill-state/active-state";
|
|
3
3
|
import { initialPhaseForSkill } from "../skill-state/initial-phase";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
canonicalWorkflowSkill,
|
|
6
|
+
WORKFLOW_STATE_RECEIPT_VERSION,
|
|
7
|
+
WORKFLOW_STATE_VERSION,
|
|
8
|
+
} from "../skill-state/workflow-state-contract";
|
|
5
9
|
import { writeWorkflowEnvelopeAtomic } from "./state-writer";
|
|
6
10
|
import { getSkillManifest } from "./workflow-manifest";
|
|
7
11
|
|
|
@@ -21,6 +25,17 @@ export interface MigrateAndPersistLegacyStateResult {
|
|
|
21
25
|
migrated: boolean;
|
|
22
26
|
path: string;
|
|
23
27
|
}
|
|
28
|
+
export interface MigrateWorkflowStateResult {
|
|
29
|
+
state: Record<string, unknown>;
|
|
30
|
+
fromVersion: number;
|
|
31
|
+
toVersion: number;
|
|
32
|
+
changed: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type WorkflowStateMigration = (
|
|
36
|
+
state: Record<string, unknown>,
|
|
37
|
+
skill: CanonicalGjcWorkflowSkill,
|
|
38
|
+
) => Record<string, unknown>;
|
|
24
39
|
|
|
25
40
|
const RECEIPT_STRING_FIELDS = [
|
|
26
41
|
"command",
|
|
@@ -75,6 +90,41 @@ function receiptWithRequiredFields(raw: unknown, skill: CanonicalGjcWorkflowSkil
|
|
|
75
90
|
function recordsEqual(left: Record<string, unknown>, right: Record<string, unknown>): boolean {
|
|
76
91
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
77
92
|
}
|
|
93
|
+
function migrateV1ToV2(state: Record<string, unknown>, skill: CanonicalGjcWorkflowSkill): Record<string, unknown> {
|
|
94
|
+
const migrated = cloneRecord(state);
|
|
95
|
+
migrated.version = WORKFLOW_STATE_VERSION;
|
|
96
|
+
migrated.skill = skill;
|
|
97
|
+
|
|
98
|
+
const sourcePhase = typeof migrated.current_phase === "string" ? migrated.current_phase : migrated.phase;
|
|
99
|
+
const normalizedPhase = normalizePhase(skill, sourcePhase);
|
|
100
|
+
migrated.current_phase = normalizedPhase;
|
|
101
|
+
if ("phase" in migrated && typeof migrated.phase === "string") migrated.phase = normalizedPhase;
|
|
102
|
+
|
|
103
|
+
return migrated;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const MIGRATIONS: Record<number, WorkflowStateMigration> = {
|
|
107
|
+
1: migrateV1ToV2,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export function migrateWorkflowState(raw: Record<string, unknown>, skill: string): MigrateWorkflowStateResult {
|
|
111
|
+
const canonicalSkill = canonicalSkillOrThrow(skill);
|
|
112
|
+
const fromVersion = typeof raw.version === "number" ? raw.version : 1;
|
|
113
|
+
if (fromVersion >= WORKFLOW_STATE_VERSION) {
|
|
114
|
+
return { state: raw, fromVersion, toVersion: fromVersion, changed: false };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let version = fromVersion;
|
|
118
|
+
let state = raw;
|
|
119
|
+
let changed = false;
|
|
120
|
+
while (version < WORKFLOW_STATE_VERSION && MIGRATIONS[version]) {
|
|
121
|
+
state = MIGRATIONS[version](state, canonicalSkill);
|
|
122
|
+
version += 1;
|
|
123
|
+
changed = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { state, fromVersion, toVersion: version, changed };
|
|
127
|
+
}
|
|
78
128
|
|
|
79
129
|
/**
|
|
80
130
|
* Pure legacy state normalizer for background/internal readers.
|
|
@@ -90,14 +140,11 @@ export function normalizeLegacyState(raw: Record<string, unknown>, skill: string
|
|
|
90
140
|
state.skill = canonicalSkill;
|
|
91
141
|
if (typeof state.version !== "number") state.version = 1;
|
|
92
142
|
if (typeof state.active !== "boolean") state.active = true;
|
|
93
|
-
|
|
94
|
-
const sourcePhase = typeof state.current_phase === "string" ? state.current_phase : state.phase;
|
|
95
|
-
const normalizedPhase = normalizePhase(canonicalSkill, sourcePhase);
|
|
96
|
-
state.current_phase = normalizedPhase;
|
|
97
|
-
if ("phase" in state && typeof state.phase === "string") state.phase = normalizedPhase;
|
|
143
|
+
if (typeof state.updated_at !== "string") state.updated_at = new Date().toISOString();
|
|
98
144
|
state.receipt = receiptWithRequiredFields(state.receipt, canonicalSkill);
|
|
99
145
|
|
|
100
|
-
|
|
146
|
+
const migrated = migrateWorkflowState(state, canonicalSkill).state;
|
|
147
|
+
return { state: migrated, changed: !recordsEqual(raw, migrated) };
|
|
101
148
|
}
|
|
102
149
|
|
|
103
150
|
export async function migrateAndPersistLegacyState(
|