@gajae-code/coding-agent 0.11.10 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -2
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/cli.d.ts +8 -1
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-profile-contract.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +86 -3
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +43 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +22 -9
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/manager.d.ts +8 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +18 -2
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
- package/dist/types/sdk/host/control/operations.d.ts +2 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +51 -19
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +59 -13
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/skill-state/active-state.d.ts +0 -4
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/types.d.ts +36 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +23 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/package.json +9 -13
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +8 -0
- package/src/cli/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +49 -2
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/sdk.ts +91 -7
- package/src/commands/team.ts +1 -1
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +24 -0
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +84 -3
- package/src/config/settings.ts +254 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +42 -4
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -921
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +979 -71
- package/src/gjc-runtime/state-runtime.ts +50 -12
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +34 -5
- package/src/gjc-runtime/team-runtime.ts +970 -33
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
- package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
- package/src/gjc-runtime/workflow-manifest.ts +34 -179
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +10 -11
- package/src/internal-urls/local-protocol.ts +23 -4
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +47 -0
- package/src/modes/acp/acp-agent.ts +597 -67
- package/src/modes/acp/acp-event-mapper.ts +106 -12
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +76 -5
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +1 -4
- package/src/modes/components/welcome.ts +18 -3
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +23 -12
- package/src/modes/controllers/input-controller.ts +62 -6
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +127 -29
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +20 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/manager.ts +26 -6
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/lifecycle.ts +107 -7
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/config.ts +1 -1
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +503 -60
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
- package/src/sdk/bus/telegram-daemon.ts +531 -53
- package/src/sdk/host/control/dispatch.ts +7 -1
- package/src/sdk/host/control/operations.ts +2 -1
- package/src/sdk/host/host.ts +25 -3
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +76 -0
- package/src/sdk/protocol/operation-registry.ts +8 -2
- package/src/sdk/session.ts +25 -2
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +720 -190
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +40 -10
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +1372 -42
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +0 -10
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +65 -7
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +2 -0
- package/src/task/types.ts +22 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/ask.ts +89 -69
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1542 -310
- package/src/tools/resource-gc.ts +665 -125
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +4 -1
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
// Subpath import keeps this module native-free for the gjc-state-gates shards:
|
|
5
|
+
// the package barrel pulls procmgr/ptree → @gajae-code/natives.
|
|
6
|
+
import * as logger from "@gajae-code/utils/logger";
|
|
4
7
|
import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
5
8
|
import {
|
|
6
9
|
applyHandoffToActiveState,
|
|
@@ -265,7 +268,21 @@ async function describeStaleClearState(
|
|
|
265
268
|
return undefined;
|
|
266
269
|
}
|
|
267
270
|
|
|
268
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Route a workflow-state warning through the TUI-safe centralized file logger
|
|
273
|
+
* (console transport off by default) so interactive sessions never paint raw
|
|
274
|
+
* bytes into the alternate-screen stream (#3002). CLI command handlers may also
|
|
275
|
+
* collect the warning via an `onWarning` sink to surface it on the structured
|
|
276
|
+
* {@link StateCommandResult.stderr} channel, so `gjc state` automation still
|
|
277
|
+
* distinguishes corrupt state from absent state.
|
|
278
|
+
*/
|
|
279
|
+
function emitStateWarning(warning: string, context?: Record<string, unknown>): void {
|
|
280
|
+
logger.warn(warning, context);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
type StateWarningSink = (warning: string) => void;
|
|
284
|
+
|
|
285
|
+
async function readJsonFile(filePath: string, onWarning?: StateWarningSink): Promise<Record<string, unknown> | null> {
|
|
269
286
|
try {
|
|
270
287
|
const raw = await fs.readFile(filePath, "utf-8");
|
|
271
288
|
const parsed = JSON.parse(raw);
|
|
@@ -276,18 +293,22 @@ async function readJsonFile(filePath: string): Promise<Record<string, unknown> |
|
|
|
276
293
|
} catch (error) {
|
|
277
294
|
const err = error as NodeJS.ErrnoException;
|
|
278
295
|
if (err.code === "ENOENT") return null;
|
|
279
|
-
|
|
296
|
+
const warning = `WARNING: failed to read ${filePath}; ignoring corrupt state: ${err.message}`;
|
|
297
|
+
emitStateWarning(warning, { filePath, error: err.message });
|
|
298
|
+
onWarning?.(warning);
|
|
280
299
|
return null;
|
|
281
300
|
}
|
|
282
301
|
}
|
|
283
302
|
|
|
284
|
-
async function readJsonValue(filePath: string): Promise<unknown | null> {
|
|
303
|
+
async function readJsonValue(filePath: string, onWarning?: StateWarningSink): Promise<unknown | null> {
|
|
285
304
|
try {
|
|
286
305
|
return JSON.parse(await fs.readFile(filePath, "utf-8"));
|
|
287
306
|
} catch (error) {
|
|
288
307
|
const err = error as NodeJS.ErrnoException;
|
|
289
308
|
if (err.code === "ENOENT") return null;
|
|
290
|
-
|
|
309
|
+
const warning = `WARNING: failed to read ${filePath}; ignoring corrupt state: ${err.message}`;
|
|
310
|
+
emitStateWarning(warning, { filePath, error: err.message });
|
|
311
|
+
onWarning?.(warning);
|
|
291
312
|
return null;
|
|
292
313
|
}
|
|
293
314
|
}
|
|
@@ -836,7 +857,8 @@ function buildHudForMode(
|
|
|
836
857
|
: typeof payload.mode === "string"
|
|
837
858
|
? (payload.mode as string)
|
|
838
859
|
: undefined;
|
|
839
|
-
const
|
|
860
|
+
const rawVerdict = payload.last_review_verdict ?? payload.verdict;
|
|
861
|
+
const verdict = typeof rawVerdict === "string" ? rawVerdict : undefined;
|
|
840
862
|
const iteration = typeof payload.iteration === "number" ? (payload.iteration as number) : undefined;
|
|
841
863
|
const pendingApproval = payload.pending_approval === true || stage === "final";
|
|
842
864
|
return buildRalplanHudSummary({
|
|
@@ -1101,21 +1123,30 @@ export async function readWorkflowStateJson(
|
|
|
1101
1123
|
cwd: string,
|
|
1102
1124
|
skill: CanonicalGjcWorkflowSkill,
|
|
1103
1125
|
sessionId?: string,
|
|
1126
|
+
onWarning?: StateWarningSink,
|
|
1104
1127
|
): Promise<Record<string, unknown>> {
|
|
1105
1128
|
const session = await resolveGjcSessionForRead(cwd, {
|
|
1106
1129
|
payloadSessionId: sessionId,
|
|
1107
1130
|
envSessionId: process.env.GJC_SESSION_ID,
|
|
1108
1131
|
});
|
|
1109
|
-
return (await readJsonFile(modeStateFile(cwd, skill, session.gjcSessionId))) ?? {};
|
|
1132
|
+
return (await readJsonFile(modeStateFile(cwd, skill, session.gjcSessionId), onWarning)) ?? {};
|
|
1110
1133
|
}
|
|
1111
1134
|
|
|
1112
1135
|
async function handleRead(args: readonly string[], cwd: string): Promise<StateCommandResult> {
|
|
1113
1136
|
const selectors = await resolveSelectors(args, cwd, "read");
|
|
1114
1137
|
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
|
|
1115
1138
|
const fields = parseFieldsFlag(args);
|
|
1139
|
+
// Corrupt-state warnings are TUI-safe file-logged inside the readers; the CLI
|
|
1140
|
+
// path also surfaces them on the command result so `gjc state read`
|
|
1141
|
+
// automation can tell corrupt state from absent state (#3002).
|
|
1142
|
+
const warnings: string[] = [];
|
|
1143
|
+
const warningStderr = (): Pick<StateCommandResult, "stderr"> =>
|
|
1144
|
+
warnings.length ? { stderr: warnings.map(warning => `${warning}\n`).join("") } : {};
|
|
1116
1145
|
if (mode) {
|
|
1117
1146
|
const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
|
|
1118
|
-
const existing = await readWorkflowStateJson(cwd, mode, selectors.gjcSessionId
|
|
1147
|
+
const existing = await readWorkflowStateJson(cwd, mode, selectors.gjcSessionId, warning =>
|
|
1148
|
+
warnings.push(warning),
|
|
1149
|
+
);
|
|
1119
1150
|
const envelope = { skill: mode, state: existing, storage_path: filePath };
|
|
1120
1151
|
const manifest = getSkillManifest(mode);
|
|
1121
1152
|
if (fields) {
|
|
@@ -1125,6 +1156,7 @@ async function handleRead(args: readonly string[], cwd: string): Promise<StateCo
|
|
|
1125
1156
|
stdout: hasFlag(args, "--json")
|
|
1126
1157
|
? `${JSON.stringify(projected, null, 2)}\n`
|
|
1127
1158
|
: renderStateMarkdown(mode, projected, manifest),
|
|
1159
|
+
...warningStderr(),
|
|
1128
1160
|
};
|
|
1129
1161
|
}
|
|
1130
1162
|
if (hasFlag(args, "--compact")) {
|
|
@@ -1134,6 +1166,7 @@ async function handleRead(args: readonly string[], cwd: string): Promise<StateCo
|
|
|
1134
1166
|
stdout: hasFlag(args, "--json")
|
|
1135
1167
|
? `${JSON.stringify(compact, null, 2)}\n`
|
|
1136
1168
|
: renderStateMarkdown(mode, envelope, manifest),
|
|
1169
|
+
...warningStderr(),
|
|
1137
1170
|
};
|
|
1138
1171
|
}
|
|
1139
1172
|
return {
|
|
@@ -1141,12 +1174,13 @@ async function handleRead(args: readonly string[], cwd: string): Promise<StateCo
|
|
|
1141
1174
|
stdout: hasFlag(args, "--json")
|
|
1142
1175
|
? `${JSON.stringify(envelope, null, 2)}\n`
|
|
1143
1176
|
: renderStateMarkdown(mode, envelope, manifest),
|
|
1177
|
+
...warningStderr(),
|
|
1144
1178
|
};
|
|
1145
1179
|
}
|
|
1146
1180
|
const filePath = activeStateFile(cwd, selectors.gjcSessionId);
|
|
1147
|
-
const existingRaw = await readJsonValue(filePath);
|
|
1181
|
+
const existingRaw = await readJsonValue(filePath, warning => warnings.push(warning));
|
|
1148
1182
|
const existing = isPlainObject(existingRaw) ? existingRaw : null;
|
|
1149
|
-
return { status: 0, stdout: `${JSON.stringify(existing ?? {}, null, 2)}\n
|
|
1183
|
+
return { status: 0, stdout: `${JSON.stringify(existing ?? {}, null, 2)}\n`, ...warningStderr() };
|
|
1150
1184
|
}
|
|
1151
1185
|
|
|
1152
1186
|
async function handleStatus(args: readonly string[], cwd: string): Promise<StateCommandResult> {
|
|
@@ -1159,7 +1193,8 @@ async function handleStatus(args: readonly string[], cwd: string): Promise<State
|
|
|
1159
1193
|
);
|
|
1160
1194
|
}
|
|
1161
1195
|
const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
|
|
1162
|
-
const
|
|
1196
|
+
const warnings: string[] = [];
|
|
1197
|
+
const existing = await readWorkflowStateJson(cwd, mode, selectors.gjcSessionId, warning => warnings.push(warning));
|
|
1163
1198
|
const summary = buildStateStatusSummary(
|
|
1164
1199
|
mode,
|
|
1165
1200
|
{ skill: mode, state: existing, storage_path: filePath },
|
|
@@ -1169,6 +1204,7 @@ async function handleStatus(args: readonly string[], cwd: string): Promise<State
|
|
|
1169
1204
|
return {
|
|
1170
1205
|
status: 0,
|
|
1171
1206
|
stdout: hasFlag(args, "--json") ? `${JSON.stringify(summary, null, 2)}\n` : renderStateStatusLine(summary),
|
|
1207
|
+
...(warnings.length ? { stderr: warnings.map(warning => `${warning}\n`).join("") } : {}),
|
|
1172
1208
|
};
|
|
1173
1209
|
}
|
|
1174
1210
|
|
|
@@ -1603,7 +1639,7 @@ async function handleHandoffUnlocked(args: readonly string[], cwd: string): Prom
|
|
|
1603
1639
|
toPhase: "handoff",
|
|
1604
1640
|
});
|
|
1605
1641
|
await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, { steps: ["caller-mode-state"] });
|
|
1606
|
-
if (callerWrite.warning)
|
|
1642
|
+
if (callerWrite.warning) emitStateWarning(callerWrite.warning);
|
|
1607
1643
|
const stampedCallerReceipt = isPlainObject(callerWrite.stamped.receipt) ? callerWrite.stamped.receipt : {};
|
|
1608
1644
|
await syncSkillActiveState({
|
|
1609
1645
|
cwd,
|
|
@@ -1646,6 +1682,7 @@ async function handleHandoffUnlocked(args: readonly string[], cwd: string): Prom
|
|
|
1646
1682
|
active_state: activeStateFile(cwd, sessionId),
|
|
1647
1683
|
},
|
|
1648
1684
|
}),
|
|
1685
|
+
...(callerWrite.warning ? { stderr: `${callerWrite.warning}\n` } : {}),
|
|
1649
1686
|
};
|
|
1650
1687
|
}
|
|
1651
1688
|
|
|
@@ -1746,7 +1783,7 @@ async function handleHandoffUnlocked(args: readonly string[], cwd: string): Prom
|
|
|
1746
1783
|
);
|
|
1747
1784
|
const stampedCallerReceipt = isPlainObject(callerWrite.stamped.receipt) ? callerWrite.stamped.receipt : {};
|
|
1748
1785
|
const stampedCalleeReceipt = isPlainObject(calleeWrite.stamped.receipt) ? calleeWrite.stamped.receipt : {};
|
|
1749
|
-
for (const warning of warnings)
|
|
1786
|
+
for (const warning of warnings) emitStateWarning(warning);
|
|
1750
1787
|
if (process.env.GJC_STATE_HANDOFF_FAIL_AFTER_CALLER === mutationId) {
|
|
1751
1788
|
throw new StateCommandError(1, `injected handoff failure after caller write for ${mutationId}`);
|
|
1752
1789
|
}
|
|
@@ -1818,6 +1855,7 @@ async function handleHandoffUnlocked(args: readonly string[], cwd: string): Prom
|
|
|
1818
1855
|
active_state: activeStateFile(cwd, sessionId),
|
|
1819
1856
|
},
|
|
1820
1857
|
}),
|
|
1858
|
+
...(warnings.length ? { stderr: warnings.map(warning => `${warning}\n`).join("") } : {}),
|
|
1821
1859
|
};
|
|
1822
1860
|
}
|
|
1823
1861
|
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
type WorkflowStateMutationOwner,
|
|
12
12
|
type WorkflowStateReceipt,
|
|
13
13
|
} from "../skill-state/workflow-state-contract";
|
|
14
|
-
import { validateDeepInterviewV1Envelope } from "./deep-interview-state";
|
|
15
14
|
import {
|
|
16
15
|
activeEntryPath as layoutActiveEntryPath,
|
|
17
16
|
activeSnapshotPath as layoutActiveSnapshotPath,
|
|
@@ -78,47 +77,6 @@ export interface WorkflowEnvelopeIntegrityMismatch {
|
|
|
78
77
|
expected: string;
|
|
79
78
|
actual: string;
|
|
80
79
|
}
|
|
81
|
-
export type WorkflowReceiptVerification =
|
|
82
|
-
| "legacy"
|
|
83
|
-
| "native-valid"
|
|
84
|
-
| "receipt-malformed"
|
|
85
|
-
| "receipt-missing"
|
|
86
|
-
| "checksum-mismatch";
|
|
87
|
-
|
|
88
|
-
export interface GuardedWorkflowEnvelopeTransformOptions extends StateWriterOptions {
|
|
89
|
-
expectedRevision: number;
|
|
90
|
-
/** Additional native schema validation applied to both current and candidate envelopes. */
|
|
91
|
-
validate?: (value: Record<string, unknown>, legacy: boolean) => void;
|
|
92
|
-
transform: (
|
|
93
|
-
current: Record<string, unknown>,
|
|
94
|
-
context: { revision: number; legacy: boolean },
|
|
95
|
-
) =>
|
|
96
|
-
| { kind: "noop" }
|
|
97
|
-
| { kind: "write"; value: Record<string, unknown> }
|
|
98
|
-
| Promise<{ kind: "noop" } | { kind: "write"; value: Record<string, unknown> }>;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface GuardedWorkflowEnvelopeWarning {
|
|
102
|
-
code: "DI_POST_COMMIT_AUDIT_FAILED";
|
|
103
|
-
message: string;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export type GuardedWorkflowEnvelopeTransformResult =
|
|
107
|
-
| { path: string; written: false; reason: "noop"; revision: number; warnings: [] }
|
|
108
|
-
| {
|
|
109
|
-
path: string;
|
|
110
|
-
written: true;
|
|
111
|
-
revision: number;
|
|
112
|
-
stamped: Record<string, unknown>;
|
|
113
|
-
warnings: GuardedWorkflowEnvelopeWarning[];
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
export class GuardedWorkflowEnvelopeError extends Error {
|
|
117
|
-
constructor(public readonly code: string) {
|
|
118
|
-
super(code);
|
|
119
|
-
this.name = "GuardedWorkflowEnvelopeError";
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
80
|
|
|
123
81
|
export interface WorkflowTransactionJournal {
|
|
124
82
|
version: 1;
|
|
@@ -138,7 +96,6 @@ export interface GuardedStateWriterOptions extends StateWriterOptions {
|
|
|
138
96
|
policy: StateWritePolicy;
|
|
139
97
|
expectedRevision?: number;
|
|
140
98
|
sourceRevision?: number;
|
|
141
|
-
lockHeld?: boolean;
|
|
142
99
|
}
|
|
143
100
|
|
|
144
101
|
export type GuardedWriteResult =
|
|
@@ -150,13 +107,17 @@ export interface StateWriterOptions {
|
|
|
150
107
|
receipt?: StateWriterReceiptContext;
|
|
151
108
|
audit?: StateWriterAuditContext;
|
|
152
109
|
sourceRevision?: number;
|
|
153
|
-
orderingRevision?: number;
|
|
154
110
|
/**
|
|
155
111
|
* Cross-process lock tuning for read-modify-write paths that route through
|
|
156
112
|
* `withWorkflowStateLock` / `updateJsonAtomic`. Omit for the hardened
|
|
157
113
|
* `withFileLock` defaults.
|
|
158
114
|
*/
|
|
159
115
|
lock?: FileLockOptions;
|
|
116
|
+
/**
|
|
117
|
+
* Caller already holds the workflow state lock for this target path (via
|
|
118
|
+
* `withWorkflowStateLock`). Skip re-acquisition to avoid self-deadlock.
|
|
119
|
+
*/
|
|
120
|
+
lockHeld?: boolean;
|
|
160
121
|
}
|
|
161
122
|
|
|
162
123
|
export class StateWriteConflictError extends Error {
|
|
@@ -298,49 +259,6 @@ export function workflowEnvelopeContentSha256(value: unknown): string {
|
|
|
298
259
|
.update(JSON.stringify(canonicalizeJson(withoutReceiptChecksum(value))))
|
|
299
260
|
.digest("hex");
|
|
300
261
|
}
|
|
301
|
-
export function isNativeDeepInterviewV1(value: Record<string, unknown>): boolean {
|
|
302
|
-
if (value.skill !== "deep-interview") return false;
|
|
303
|
-
return value.schema_version === 1 || (isPlainObject(value.state) && value.state.schema_version === 1);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export function verifyWorkflowEnvelopeReceiptValue(value: unknown, filePath: string): WorkflowReceiptVerification {
|
|
307
|
-
if (!isPlainObject(value)) return "receipt-malformed";
|
|
308
|
-
const native = isNativeDeepInterviewV1(value);
|
|
309
|
-
if (!Object.hasOwn(value, "receipt")) return native ? "receipt-missing" : "legacy";
|
|
310
|
-
if (!isPlainObject(value.receipt) || !isPlainObject(value.receipt.content_sha256)) return "receipt-malformed";
|
|
311
|
-
|
|
312
|
-
const receipt = value.receipt as Record<string, unknown>;
|
|
313
|
-
const checksum = receipt.content_sha256 as Record<string, unknown>;
|
|
314
|
-
if (
|
|
315
|
-
typeof value.skill !== "string" ||
|
|
316
|
-
receipt.version !== 1 ||
|
|
317
|
-
receipt.skill !== value.skill ||
|
|
318
|
-
(receipt.skill !== "deep-interview" &&
|
|
319
|
-
receipt.skill !== "ralplan" &&
|
|
320
|
-
receipt.skill !== "ultragoal" &&
|
|
321
|
-
receipt.skill !== "team") ||
|
|
322
|
-
(receipt.owner !== "gjc-state-cli" && receipt.owner !== "gjc-runtime" && receipt.owner !== "gjc-hook") ||
|
|
323
|
-
typeof receipt.command !== "string" ||
|
|
324
|
-
typeof receipt.state_path !== "string" ||
|
|
325
|
-
typeof receipt.storage_path !== "string" ||
|
|
326
|
-
typeof receipt.mutated_at !== "string" ||
|
|
327
|
-
!Number.isFinite(Date.parse(receipt.mutated_at)) ||
|
|
328
|
-
typeof receipt.fresh_until !== "string" ||
|
|
329
|
-
!Number.isFinite(Date.parse(receipt.fresh_until)) ||
|
|
330
|
-
(receipt.status !== "fresh" && receipt.status !== "stale") ||
|
|
331
|
-
typeof receipt.mutation_id !== "string" ||
|
|
332
|
-
checksum.algorithm !== "sha256" ||
|
|
333
|
-
typeof checksum.value !== "string" ||
|
|
334
|
-
!/^[0-9a-f]{64}$/.test(checksum.value) ||
|
|
335
|
-
typeof checksum.covered_path !== "string" ||
|
|
336
|
-
path.resolve(checksum.covered_path) !== path.resolve(filePath) ||
|
|
337
|
-
typeof checksum.computed_at !== "string" ||
|
|
338
|
-
!Number.isFinite(Date.parse(checksum.computed_at))
|
|
339
|
-
) {
|
|
340
|
-
return "receipt-malformed";
|
|
341
|
-
}
|
|
342
|
-
return workflowEnvelopeContentSha256(value) === checksum.value ? "native-valid" : "checksum-mismatch";
|
|
343
|
-
}
|
|
344
262
|
|
|
345
263
|
export function stampWorkflowEnvelopeChecksum<T>(value: T, filePath: string, computedAt = new Date().toISOString()): T {
|
|
346
264
|
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
@@ -507,11 +425,6 @@ function persistedSourceRevision(value: unknown): number {
|
|
|
507
425
|
const revision = value.source_state_revision;
|
|
508
426
|
return typeof revision === "number" && Number.isFinite(revision) ? revision : persistedStateRevision(value);
|
|
509
427
|
}
|
|
510
|
-
function persistedCommittedModeRevision(value: unknown): number {
|
|
511
|
-
if (!isPlainObject(value)) return 0;
|
|
512
|
-
const revision = value.committed_mode_state_revision;
|
|
513
|
-
return typeof revision === "number" && Number.isFinite(revision) ? revision : 0;
|
|
514
|
-
}
|
|
515
428
|
|
|
516
429
|
function withoutCandidateRevision(value: unknown): unknown {
|
|
517
430
|
if (!isPlainObject(value)) return value;
|
|
@@ -620,12 +533,7 @@ async function writeGuardedResolvedJsonAtomic(
|
|
|
620
533
|
|
|
621
534
|
const incomingSourceRevision =
|
|
622
535
|
options.sourceRevision ?? (isPlainObject(value) ? persistedStateRevision(value) : 0);
|
|
623
|
-
if (
|
|
624
|
-
current !== undefined &&
|
|
625
|
-
(options.orderingRevision !== undefined
|
|
626
|
-
? options.orderingRevision <= persistedCommittedModeRevision(current)
|
|
627
|
-
: incomingSourceRevision <= persistedSourceRevision(current))
|
|
628
|
-
) {
|
|
536
|
+
if (current !== undefined && incomingSourceRevision <= persistedSourceRevision(current)) {
|
|
629
537
|
return { path: filePath, written: false, reason: "stale-skip", revision: currentRevision };
|
|
630
538
|
}
|
|
631
539
|
const next = stampStateRevision(
|
|
@@ -659,7 +567,6 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
|
|
|
659
567
|
const write = async (): Promise<GuardedWriteResult> => {
|
|
660
568
|
const current = await readJsonIfPresentTolerant(filePath);
|
|
661
569
|
const currentRevision = persistedStateRevision(current);
|
|
662
|
-
|
|
663
570
|
if (options.policy === "source") {
|
|
664
571
|
if (options.expectedRevision !== undefined && options.expectedRevision !== currentRevision) {
|
|
665
572
|
throw new StateWriteConflictError(filePath, options.expectedRevision, currentRevision);
|
|
@@ -683,15 +590,9 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
|
|
|
683
590
|
await maybeAudit(filePath, options);
|
|
684
591
|
return { path: filePath, written: true, revision: currentRevision + 1, stamped: next };
|
|
685
592
|
}
|
|
686
|
-
|
|
687
593
|
const incomingSourceRevision =
|
|
688
594
|
options.sourceRevision ?? (isPlainObject(value) ? persistedStateRevision(value) : 0);
|
|
689
|
-
if (
|
|
690
|
-
current !== undefined &&
|
|
691
|
-
(options.orderingRevision !== undefined
|
|
692
|
-
? options.orderingRevision <= persistedCommittedModeRevision(current)
|
|
693
|
-
: incomingSourceRevision <= persistedSourceRevision(current))
|
|
694
|
-
) {
|
|
595
|
+
if (current !== undefined && incomingSourceRevision <= persistedSourceRevision(current)) {
|
|
695
596
|
return { path: filePath, written: false, reason: "stale-skip", revision: currentRevision };
|
|
696
597
|
}
|
|
697
598
|
const next = stampWorkflowEnvelopeRevisionAndChecksum(
|
|
@@ -715,67 +616,6 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
|
|
|
715
616
|
};
|
|
716
617
|
return options.lockHeld ? write() : lockResolvedWorkflowTarget(filePath, write, options.lock);
|
|
717
618
|
}
|
|
718
|
-
export async function transformGuardedWorkflowEnvelopeAtomic(
|
|
719
|
-
targetPath: string,
|
|
720
|
-
options: GuardedWorkflowEnvelopeTransformOptions,
|
|
721
|
-
): Promise<GuardedWorkflowEnvelopeTransformResult> {
|
|
722
|
-
const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
|
|
723
|
-
return lockResolvedWorkflowTarget(
|
|
724
|
-
filePath,
|
|
725
|
-
async () => {
|
|
726
|
-
const read = await readExistingStateForMutation(filePath);
|
|
727
|
-
if (read.kind === "absent") throw new GuardedWorkflowEnvelopeError("DI_STATE_ABSENT");
|
|
728
|
-
if (read.kind === "corrupt") throw new GuardedWorkflowEnvelopeError("DI_STATE_CORRUPT");
|
|
729
|
-
const receipt = verifyWorkflowEnvelopeReceiptValue(read.value, filePath);
|
|
730
|
-
if (receipt === "receipt-malformed") throw new GuardedWorkflowEnvelopeError("DI_RECEIPT_MALFORMED");
|
|
731
|
-
if (receipt === "receipt-missing") throw new GuardedWorkflowEnvelopeError("DI_RECEIPT_MISSING");
|
|
732
|
-
if (receipt === "checksum-mismatch") throw new GuardedWorkflowEnvelopeError("DI_RECEIPT_CHECKSUM_MISMATCH");
|
|
733
|
-
const legacy = receipt === "legacy";
|
|
734
|
-
try {
|
|
735
|
-
if (isNativeDeepInterviewV1(read.value)) validateDeepInterviewV1Envelope(read.value);
|
|
736
|
-
options.validate?.(read.value, legacy);
|
|
737
|
-
} catch {
|
|
738
|
-
throw new GuardedWorkflowEnvelopeError("DI_STATE_SCHEMA_INVALID");
|
|
739
|
-
}
|
|
740
|
-
const phase = read.value.current_phase;
|
|
741
|
-
if (phase === "complete" || phase === "handoff" || read.value.active === false) {
|
|
742
|
-
throw new GuardedWorkflowEnvelopeError("DI_PHASE_NOT_REPAIRABLE");
|
|
743
|
-
}
|
|
744
|
-
const revision = persistedStateRevision(read.value);
|
|
745
|
-
if (revision !== options.expectedRevision) throw new GuardedWorkflowEnvelopeError("DI_REVISION_CONFLICT");
|
|
746
|
-
const transformed = await options.transform(read.value, { revision, legacy });
|
|
747
|
-
if (transformed.kind === "noop")
|
|
748
|
-
return { path: filePath, written: false, reason: "noop", revision, warnings: [] };
|
|
749
|
-
try {
|
|
750
|
-
if (isNativeDeepInterviewV1(transformed.value)) validateDeepInterviewV1Envelope(transformed.value);
|
|
751
|
-
options.validate?.(transformed.value, false);
|
|
752
|
-
} catch {
|
|
753
|
-
throw new GuardedWorkflowEnvelopeError("DI_STATE_SCHEMA_INVALID");
|
|
754
|
-
}
|
|
755
|
-
const stamped = stampWorkflowEnvelopeRevisionAndChecksum(
|
|
756
|
-
transformed.value,
|
|
757
|
-
filePath,
|
|
758
|
-
revision + 1,
|
|
759
|
-
undefined,
|
|
760
|
-
options,
|
|
761
|
-
) as Record<string, unknown>;
|
|
762
|
-
const parsed = RequiredOnWriteEnvelopeSchema.safeParse(stamped);
|
|
763
|
-
if (!parsed.success) throw new GuardedWorkflowEnvelopeError("DI_STATE_SCHEMA_INVALID");
|
|
764
|
-
await atomicWrite(filePath, jsonText(stamped));
|
|
765
|
-
const warnings: GuardedWorkflowEnvelopeWarning[] = [];
|
|
766
|
-
try {
|
|
767
|
-
await maybeAudit(filePath, options);
|
|
768
|
-
} catch (error) {
|
|
769
|
-
warnings.push({
|
|
770
|
-
code: "DI_POST_COMMIT_AUDIT_FAILED",
|
|
771
|
-
message: error instanceof Error ? error.message : String(error),
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
return { path: filePath, written: true, revision: revision + 1, stamped, warnings };
|
|
775
|
-
},
|
|
776
|
-
options.lock,
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
619
|
|
|
780
620
|
export async function writeJsonAtomic(
|
|
781
621
|
targetPath: string,
|
|
@@ -845,63 +685,71 @@ export async function writeWorkflowEnvelopeAtomic(
|
|
|
845
685
|
options?: StateWriterOptions,
|
|
846
686
|
): Promise<string> {
|
|
847
687
|
const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
688
|
+
const write = async (): Promise<string> => {
|
|
689
|
+
const withReceipt = withWorkflowReceipt(value, buildReceipt(options));
|
|
690
|
+
const stamped = stampWorkflowEnvelopeChecksum(withReceipt, filePath);
|
|
691
|
+
const parsed = RequiredOnWriteEnvelopeSchema.safeParse(stamped);
|
|
692
|
+
if (!parsed.success) {
|
|
693
|
+
throw new Error(
|
|
694
|
+
`Refusing to write invalid workflow state envelope to ${filePath}: ${parsed.error.issues
|
|
695
|
+
.map(issue => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
|
|
696
|
+
.join("; ")}`,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
// #658: internal runtime writers (ralplan/ultragoal/deep-interview/team) persist
|
|
700
|
+
// envelopes directly, bypassing the `gjc state` CLI transition gate (`isValidTransition`,
|
|
701
|
+
// historically the sole call site in state-runtime.ts). Re-assert that gate on every
|
|
702
|
+
// sanctioned envelope write so internal writes cannot persist invalid state-machine phase
|
|
703
|
+
// transitions silently. Forced writes (`gjc state ... --force`, reconcile repairs) carry
|
|
704
|
+
// `audit.forced` and bypass, mirroring the CLI's `use --force to bypass`.
|
|
705
|
+
//
|
|
706
|
+
// The gate governs ACTIVE workflow progression only. Deactivation/teardown writes
|
|
707
|
+
// (`active: false`, e.g. `gjc state clear`, which persists the universal `complete`
|
|
708
|
+
// sentinel that is not a per-skill manifest state) leave the transition graph and are
|
|
709
|
+
// intentionally exempt.
|
|
710
|
+
if (options?.audit?.forced !== true && parsed.data.active === true) {
|
|
711
|
+
const toPhase = parsed.data.current_phase.trim();
|
|
712
|
+
if (toPhase) {
|
|
713
|
+
// Lazy import: workflow-manifest dereferences CANONICAL_GJC_WORKFLOW_SKILLS at
|
|
714
|
+
// module load, and active-state -> state-writer -> workflow-manifest -> active-state
|
|
715
|
+
// is a load-time cycle. Importing at call time (after init) avoids the TDZ.
|
|
716
|
+
const { isKnownWorkflowState, isValidTransition } = await import("./workflow-manifest");
|
|
717
|
+
const skill = parsed.data.skill;
|
|
718
|
+
// Structural invariant (hard): a `current_phase` absent from the skill's manifest is
|
|
719
|
+
// never a legitimate internal write, matching the CLI/reconcile unknown-phase gate.
|
|
720
|
+
if (!isKnownWorkflowState(skill, toPhase)) {
|
|
721
|
+
throw new Error(
|
|
722
|
+
`Refusing to write unknown ${skill} phase "${toPhase}" to ${filePath}: not a known ${skill} manifest state (forced writes bypass via audit.forced)`,
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
// Transition invariant (#658, diagnostic-only safety net): resolve the prior phase
|
|
726
|
+
// (caller-supplied `audit.fromPhase`, else the active persisted envelope on disk) and
|
|
727
|
+
// flag edges the manifest does not define. Intentionally NON-blocking and audit-only
|
|
728
|
+
// — the CLI path already hard-fails invalid edges before reaching here, and legitimate
|
|
729
|
+
// internal repairs / ralplan short-mode stage skips move between valid states without a
|
|
730
|
+
// direct manifest edge. It records an `invalid_transition_detected` audit entry (no
|
|
731
|
+
// stderr) so such transitions are non-silent without breaking those flows.
|
|
732
|
+
const fromPhase = (options?.audit?.fromPhase ?? (await readPersistedPhase(filePath)))?.trim();
|
|
733
|
+
if (
|
|
734
|
+
fromPhase &&
|
|
735
|
+
fromPhase !== toPhase &&
|
|
736
|
+
isKnownWorkflowState(skill, fromPhase) &&
|
|
737
|
+
!isValidTransition(skill, fromPhase, toPhase)
|
|
738
|
+
) {
|
|
739
|
+
await recordInvalidWorkflowTransition({ filePath, skill, fromPhase, toPhase, options });
|
|
740
|
+
}
|
|
899
741
|
}
|
|
900
742
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
743
|
+
await atomicWrite(filePath, jsonText(stamped));
|
|
744
|
+
await maybeAudit(filePath, options);
|
|
745
|
+
return filePath;
|
|
746
|
+
};
|
|
747
|
+
// Serialize with every other writer of the same state path. Without this, a
|
|
748
|
+
// revision-preserving envelope write (seed/spec persistence) can interleave
|
|
749
|
+
// inside a staged apply's check-then-write window and be silently overwritten
|
|
750
|
+
// (#3387 architect finding 1). Callers already inside `withWorkflowStateLock`
|
|
751
|
+
// for this path pass `lockHeld: true`.
|
|
752
|
+
return options?.lockHeld ? write() : lockResolvedWorkflowTarget(filePath, write, options?.lock);
|
|
905
753
|
}
|
|
906
754
|
|
|
907
755
|
export async function writeTextAtomic(targetPath: string, text: string, options?: StateWriterOptions): Promise<string> {
|