@gajae-code/coding-agent 0.6.4 → 0.7.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 +51 -0
- package/dist/types/async/job-manager.d.ts +3 -1
- package/dist/types/cli/daemon-cli.d.ts +25 -0
- package/dist/types/cli/migrate-cli.d.ts +20 -0
- package/dist/types/cli/notify-cli.d.ts +23 -0
- package/dist/types/cli/setup-cli.d.ts +20 -1
- package/dist/types/commands/daemon.d.ts +41 -0
- package/dist/types/commands/migrate.d.ts +33 -0
- package/dist/types/commands/notify.d.ts +41 -0
- package/dist/types/config/keybindings.d.ts +4 -0
- package/dist/types/config/model-profile-activation.d.ts +12 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +38 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/daemon/builtin.d.ts +20 -0
- package/dist/types/daemon/control-types.d.ts +57 -0
- package/dist/types/daemon/runtime.d.ts +25 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
- package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +38 -7
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
- package/dist/types/harness-control-plane/storage.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +12 -4
- package/dist/types/migrate/action-planner.d.ts +11 -0
- package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
- package/dist/types/migrate/adapters/codex.d.ts +5 -0
- package/dist/types/migrate/adapters/index.d.ts +45 -0
- package/dist/types/migrate/adapters/opencode.d.ts +2 -0
- package/dist/types/migrate/executor.d.ts +2 -0
- package/dist/types/migrate/mcp-mapper.d.ts +20 -0
- package/dist/types/migrate/report.d.ts +18 -0
- package/dist/types/migrate/skill-normalizer.d.ts +27 -0
- package/dist/types/migrate/types.d.ts +126 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/oauth-selector.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/notifications/config-commands.d.ts +26 -0
- package/dist/types/notifications/config.d.ts +61 -0
- package/dist/types/notifications/helpers.d.ts +55 -0
- package/dist/types/notifications/html-format.d.ts +62 -0
- package/dist/types/notifications/index.d.ts +28 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
- package/dist/types/notifications/telegram-cli.d.ts +19 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
- package/dist/types/notifications/telegram-daemon.d.ts +276 -0
- package/dist/types/notifications/telegram-reference.d.ts +111 -0
- package/dist/types/notifications/threaded-inbound.d.ts +58 -0
- package/dist/types/notifications/threaded-render.d.ts +66 -0
- package/dist/types/notifications/topic-registry.d.ts +67 -0
- package/dist/types/research-plan/index.d.ts +1 -0
- package/dist/types/research-plan/ledger.d.ts +33 -0
- package/dist/types/rlm/artifacts.d.ts +1 -1
- package/dist/types/rlm/index.d.ts +12 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
- package/dist/types/session/agent-session.d.ts +39 -2
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/setup/credential-auto-import.d.ts +63 -0
- package/dist/types/setup/credential-import.d.ts +3 -0
- package/dist/types/setup/host-plugin-setup.d.ts +39 -0
- package/dist/types/skill-state/active-state.d.ts +6 -11
- package/dist/types/skill-state/canonical-skills.d.ts +3 -0
- package/dist/types/skill-state/workflow-hud.d.ts +2 -0
- package/dist/types/task/spawn-gate.d.ts +1 -10
- package/dist/types/tools/ask-answer-registry.d.ts +13 -0
- package/dist/types/tools/index.d.ts +18 -0
- package/dist/types/tools/subagent.d.ts +3 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +3 -0
- package/src/async/job-manager.ts +5 -1
- package/src/cli/daemon-cli.ts +122 -0
- package/src/cli/migrate-cli.ts +106 -0
- package/src/cli/notify-cli.ts +274 -0
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +3 -0
- package/src/commands/daemon.ts +47 -0
- package/src/commands/deep-interview.ts +2 -2
- package/src/commands/migrate.ts +46 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +7 -3
- package/src/config/model-profile-activation.ts +74 -5
- package/src/config/model-profiles.ts +7 -4
- package/src/config/model-registry.ts +6 -3
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +29 -0
- package/src/coordinator/contract.ts +3 -0
- package/src/coordinator-mcp/policy.ts +10 -2
- package/src/coordinator-mcp/server.ts +270 -1
- package/src/daemon/builtin.ts +46 -0
- package/src/daemon/control-types.ts +65 -0
- package/src/daemon/runtime.ts +51 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
- package/src/defaults/gjc/skills/team/SKILL.md +51 -47
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -13
- package/src/extensibility/custom-commands/loader.ts +0 -7
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/extensibility/gjc-plugins/injection.ts +23 -4
- package/src/extensibility/gjc-plugins/state.ts +16 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +51 -18
- package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
- package/src/gjc-runtime/goal-mode-request.ts +26 -11
- package/src/gjc-runtime/launch-tmux.ts +6 -1
- package/src/gjc-runtime/ralplan-runtime.ts +79 -50
- package/src/gjc-runtime/session-layout.ts +180 -0
- package/src/gjc-runtime/session-resolution.ts +217 -0
- package/src/gjc-runtime/state-graph.ts +1 -2
- package/src/gjc-runtime/state-migrations.ts +1 -0
- package/src/gjc-runtime/state-runtime.ts +247 -124
- package/src/gjc-runtime/state-schema.ts +2 -0
- package/src/gjc-runtime/state-writer.ts +289 -41
- package/src/gjc-runtime/team-runtime.ts +43 -19
- package/src/gjc-runtime/tmux-sessions.ts +7 -1
- package/src/gjc-runtime/ultragoal-guard.ts +102 -4
- package/src/gjc-runtime/ultragoal-runtime.ts +226 -60
- package/src/gjc-runtime/workflow-command-ref.ts +1 -2
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
- package/src/gjc-runtime/workflow-manifest.ts +12 -3
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/harness-control-plane/storage.ts +14 -4
- package/src/hooks/native-skill-hook.ts +38 -12
- package/src/hooks/skill-state.ts +178 -83
- package/src/internal-urls/docs-index.generated.ts +9 -6
- package/src/main.ts +30 -0
- package/src/migrate/action-planner.ts +318 -0
- package/src/migrate/adapters/claude-code.ts +39 -0
- package/src/migrate/adapters/codex.ts +70 -0
- package/src/migrate/adapters/index.ts +277 -0
- package/src/migrate/adapters/opencode.ts +52 -0
- package/src/migrate/executor.ts +81 -0
- package/src/migrate/mcp-mapper.ts +152 -0
- package/src/migrate/report.ts +104 -0
- package/src/migrate/skill-normalizer.ts +80 -0
- package/src/migrate/types.ts +163 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- package/src/modes/bridge/bridge-mode.ts +2 -2
- package/src/modes/components/custom-editor.ts +30 -20
- package/src/modes/components/hook-editor.ts +7 -2
- package/src/modes/components/oauth-selector.ts +19 -0
- package/src/modes/controllers/event-controller.ts +20 -0
- package/src/modes/controllers/selector-controller.ts +80 -17
- package/src/modes/interactive-mode.ts +6 -2
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/event-contract.ts +1 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
- package/src/modes/shared/agent-wire/event-observation.ts +16 -0
- package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
- package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/utils/ui-helpers.ts +23 -0
- package/src/notifications/config-commands.ts +50 -0
- package/src/notifications/config.ts +107 -0
- package/src/notifications/helpers.ts +135 -0
- package/src/notifications/html-format.ts +389 -0
- package/src/notifications/index.ts +663 -0
- package/src/notifications/rate-limit-pool.ts +179 -0
- package/src/notifications/telegram-cli.ts +194 -0
- package/src/notifications/telegram-daemon-cli.ts +74 -0
- package/src/notifications/telegram-daemon-control.ts +370 -0
- package/src/notifications/telegram-daemon.ts +1370 -0
- package/src/notifications/telegram-reference.ts +335 -0
- package/src/notifications/threaded-inbound.ts +80 -0
- package/src/notifications/threaded-render.ts +155 -0
- package/src/notifications/topic-registry.ts +133 -0
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -2
- package/src/research-plan/index.ts +1 -0
- package/src/research-plan/ledger.ts +177 -0
- package/src/rlm/artifacts.ts +12 -3
- package/src/rlm/index.ts +26 -0
- package/src/runtime-mcp/config-writer.ts +46 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +128 -24
- package/src/session/auth-storage.ts +3 -0
- package/src/session/session-dump-format.ts +43 -2
- package/src/session/session-manager.ts +39 -5
- package/src/setup/credential-auto-import.ts +258 -0
- package/src/setup/credential-import.ts +17 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
- package/src/setup/hermes-setup.ts +1 -1
- package/src/setup/host-plugin-setup.ts +142 -0
- package/src/skill-state/active-state.ts +72 -108
- package/src/skill-state/canonical-skills.ts +4 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
- package/src/skill-state/workflow-hud.ts +4 -2
- package/src/skill-state/workflow-state-contract.ts +3 -3
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/agents.ts +1 -22
- package/src/task/executor.ts +5 -1
- package/src/task/index.ts +1 -41
- package/src/task/spawn-gate.ts +1 -38
- package/src/task/types.ts +1 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +108 -16
- package/src/tools/computer.ts +58 -4
- package/src/tools/image-gen.ts +5 -8
- package/src/tools/index.ts +19 -0
- package/src/tools/inspect-image.ts +16 -11
- package/src/tools/subagent-render.ts +7 -0
- package/src/tools/subagent.ts +38 -7
- package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
- package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
- package/src/prompts/agents/explore.md +0 -58
- package/src/prompts/agents/plan.md +0 -49
- package/src/prompts/agents/reviewer.md +0 -141
- package/src/prompts/agents/task.md +0 -16
- package/src/prompts/review-request.md +0 -70
|
@@ -27,6 +27,13 @@ import {
|
|
|
27
27
|
} from "../skill-state/workflow-state-contract";
|
|
28
28
|
import { renderCliWriteReceipt } from "./cli-write-receipt";
|
|
29
29
|
import { mergeDeepInterviewEnvelope, normalizeDeepInterviewEnvelope } from "./deep-interview-state";
|
|
30
|
+
import { activeSnapshotPath, auditPath, modeStatePath, sessionStateDir } from "./session-layout";
|
|
31
|
+
import {
|
|
32
|
+
resolveGjcSessionForRead,
|
|
33
|
+
resolveGjcSessionForWrite,
|
|
34
|
+
SessionResolutionError,
|
|
35
|
+
writeSessionActivityMarker,
|
|
36
|
+
} from "./session-resolution";
|
|
30
37
|
import { renderStateGraph, type StateGraphFormat } from "./state-graph";
|
|
31
38
|
import { migrateAndPersistLegacyState, migrateWorkflowState } from "./state-migrations";
|
|
32
39
|
import {
|
|
@@ -53,16 +60,16 @@ import {
|
|
|
53
60
|
softDelete,
|
|
54
61
|
updateWorkflowTransactionJournal,
|
|
55
62
|
type WorkflowEnvelopeIntegrityMismatch,
|
|
56
|
-
|
|
63
|
+
writeGuardedWorkflowEnvelopeAtomic,
|
|
57
64
|
} from "./state-writer";
|
|
58
65
|
import { getSkillManifest, isKnownWorkflowState, isValidTransition, typedArgsFor } from "./workflow-manifest";
|
|
59
66
|
|
|
60
67
|
/**
|
|
61
68
|
* Native implementation of the `gjc state read|write|clear` command surface.
|
|
62
69
|
*
|
|
63
|
-
* Simple file-receipt operations against
|
|
64
|
-
* `.gjc/
|
|
65
|
-
*
|
|
70
|
+
* Simple file-receipt operations against session-scoped state under
|
|
71
|
+
* `.gjc/_session-{id}/state/`. This is the sanctioned CLI mediator for
|
|
72
|
+
* mutation-guarded GJC state — agents call it instead of editing those files directly.
|
|
66
73
|
*/
|
|
67
74
|
|
|
68
75
|
export interface StateCommandResult {
|
|
@@ -72,6 +79,7 @@ export interface StateCommandResult {
|
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
const SKILL_ACTIVE_STATE_FILE = "skill-active-state.json";
|
|
82
|
+
const TERMINAL_CLEAR_PHASES = new Set(["complete", "completed", "cancelled", "canceled", "failed"]);
|
|
75
83
|
const PATH_COMPONENT_RE = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
|
|
76
84
|
const KNOWN_MODES: readonly string[] = CANONICAL_GJC_WORKFLOW_SKILLS;
|
|
77
85
|
|
|
@@ -269,16 +277,22 @@ async function readInputJson(value: string | undefined, cwd: string): Promise<Re
|
|
|
269
277
|
|
|
270
278
|
interface ResolvedSelectors {
|
|
271
279
|
mode: CanonicalGjcWorkflowSkill | undefined;
|
|
272
|
-
|
|
280
|
+
gjcSessionId: string;
|
|
273
281
|
threadId: string | undefined;
|
|
274
282
|
turnId: string | undefined;
|
|
275
283
|
payload: Record<string, unknown> | undefined;
|
|
276
284
|
}
|
|
277
285
|
|
|
286
|
+
// `clear` resolves like a read (explicit -> payload -> env -> latest-activity marker)
|
|
287
|
+
// per the spec: read/status/clear may fall back to the most-recent session. Commands
|
|
288
|
+
// that create or mutate new state roots still require an explicit/env session id.
|
|
289
|
+
const WRITE_SESSION_ACTIONS = new Set<ParsedInvocation["action"]>(["write", "handoff", "prune", "migrate"]);
|
|
290
|
+
|
|
278
291
|
async function resolveSelectors(
|
|
279
292
|
args: readonly string[],
|
|
280
293
|
cwd: string,
|
|
281
294
|
positionalSkill: string | undefined,
|
|
295
|
+
action: ParsedInvocation["action"],
|
|
282
296
|
): Promise<ResolvedSelectors> {
|
|
283
297
|
const payload = await readInputJson(flagValue(args, "--input"), cwd);
|
|
284
298
|
|
|
@@ -297,41 +311,32 @@ async function resolveSelectors(
|
|
|
297
311
|
}
|
|
298
312
|
if (mode) assertKnownMode(mode);
|
|
299
313
|
|
|
300
|
-
const
|
|
314
|
+
const sessionSources = {
|
|
315
|
+
flagValue: flagValue(args, "--session-id"),
|
|
316
|
+
payloadSessionId: payload?.session_id,
|
|
317
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
318
|
+
};
|
|
319
|
+
const session = WRITE_SESSION_ACTIONS.has(action)
|
|
320
|
+
? resolveGjcSessionForWrite(cwd, sessionSources)
|
|
321
|
+
: await resolveGjcSessionForRead(cwd, sessionSources);
|
|
301
322
|
|
|
302
323
|
const threadId = flagValue(args, "--thread-id")?.trim() || undefined;
|
|
303
324
|
if (threadId) assertSafePathComponent(threadId, "thread-id");
|
|
304
325
|
const turnId = flagValue(args, "--turn-id")?.trim() || undefined;
|
|
305
326
|
if (turnId) assertSafePathComponent(turnId, "turn-id");
|
|
306
327
|
|
|
307
|
-
return {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
// caller's session-scoped state files.
|
|
315
|
-
function resolveSessionIdFromArgs(
|
|
316
|
-
args: readonly string[],
|
|
317
|
-
payload: Record<string, unknown> | undefined,
|
|
318
|
-
): string | undefined {
|
|
319
|
-
const explicitSessionId = flagValue(args, "--session-id");
|
|
320
|
-
let sessionId = explicitSessionId !== undefined ? explicitSessionId.trim() || undefined : undefined;
|
|
321
|
-
if (!sessionId && payload && typeof payload.session_id === "string") {
|
|
322
|
-
sessionId = payload.session_id.trim() || undefined;
|
|
323
|
-
}
|
|
324
|
-
if (!sessionId && explicitSessionId === undefined) {
|
|
325
|
-
const envSessionId = process.env.GJC_SESSION_ID?.trim();
|
|
326
|
-
if (envSessionId) sessionId = envSessionId;
|
|
327
|
-
}
|
|
328
|
-
if (sessionId) assertSafePathComponent(sessionId, "session-id");
|
|
329
|
-
return sessionId;
|
|
328
|
+
return {
|
|
329
|
+
mode: mode as CanonicalGjcWorkflowSkill | undefined,
|
|
330
|
+
gjcSessionId: session.gjcSessionId,
|
|
331
|
+
threadId,
|
|
332
|
+
turnId,
|
|
333
|
+
payload,
|
|
334
|
+
};
|
|
330
335
|
}
|
|
331
336
|
|
|
332
337
|
async function inferModeFromActiveState(
|
|
333
338
|
cwd: string,
|
|
334
|
-
sessionId: string
|
|
339
|
+
sessionId: string,
|
|
335
340
|
): Promise<CanonicalGjcWorkflowSkill | undefined> {
|
|
336
341
|
const state = await readVisibleSkillActiveState(cwd, sessionId);
|
|
337
342
|
const entries = listActiveSkills(state);
|
|
@@ -341,22 +346,53 @@ async function inferModeFromActiveState(
|
|
|
341
346
|
return canonical ?? undefined;
|
|
342
347
|
}
|
|
343
348
|
|
|
344
|
-
function
|
|
345
|
-
return
|
|
349
|
+
function stateDirFor(cwd: string, sessionId: string): string {
|
|
350
|
+
return sessionStateDir(cwd, sessionId);
|
|
346
351
|
}
|
|
347
352
|
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
if (!sessionId) return base;
|
|
351
|
-
return path.join(base, "sessions", encodeSessionSegment(sessionId));
|
|
353
|
+
function modeStateFile(cwd: string, mode: string, sessionId: string): string {
|
|
354
|
+
return modeStatePath(cwd, sessionId, mode);
|
|
352
355
|
}
|
|
353
356
|
|
|
354
|
-
function
|
|
355
|
-
return
|
|
357
|
+
function activeStateFile(cwd: string, sessionId: string): string {
|
|
358
|
+
return activeSnapshotPath(cwd, sessionId);
|
|
356
359
|
}
|
|
357
360
|
|
|
358
|
-
function
|
|
359
|
-
return path.
|
|
361
|
+
function stateRelativePath(cwd: string, filePath: string): string {
|
|
362
|
+
return path.relative(cwd, filePath).split(path.sep).join(path.posix.sep);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function touchStateActivityMarker(cwd: string, sessionId: string, filePath: string): Promise<void> {
|
|
366
|
+
await writeSessionActivityMarker(cwd, sessionId, {
|
|
367
|
+
writer: "state-runtime",
|
|
368
|
+
path: stateRelativePath(cwd, filePath),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function readActivePhaseForSkill(
|
|
373
|
+
cwd: string,
|
|
374
|
+
sessionId: string,
|
|
375
|
+
mode: CanonicalGjcWorkflowSkill,
|
|
376
|
+
): Promise<string | undefined> {
|
|
377
|
+
const state = await readVisibleSkillActiveState(cwd, sessionId);
|
|
378
|
+
const entries = listActiveSkills(state);
|
|
379
|
+
const entry = entries.find(item => item.skill === mode) ?? (state?.skill === mode ? state : undefined);
|
|
380
|
+
return isPlainObject(entry) && typeof entry.phase === "string" ? entry.phase.trim() || undefined : undefined;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function describeStaleClearState(
|
|
384
|
+
cwd: string,
|
|
385
|
+
sessionId: string,
|
|
386
|
+
mode: CanonicalGjcWorkflowSkill,
|
|
387
|
+
existing: Record<string, unknown>,
|
|
388
|
+
): Promise<string | undefined> {
|
|
389
|
+
const phase = typeof existing.current_phase === "string" ? existing.current_phase.trim() : undefined;
|
|
390
|
+
if (phase && TERMINAL_CLEAR_PHASES.has(phase)) return `mode-state is already terminal (${phase})`;
|
|
391
|
+
const activePhase = await readActivePhaseForSkill(cwd, sessionId, mode);
|
|
392
|
+
if (activePhase && phase && activePhase !== phase) {
|
|
393
|
+
return `active-state phase ${activePhase} differs from mode-state phase ${phase}`;
|
|
394
|
+
}
|
|
395
|
+
return undefined;
|
|
360
396
|
}
|
|
361
397
|
|
|
362
398
|
async function readJsonFile(filePath: string): Promise<Record<string, unknown> | null> {
|
|
@@ -446,7 +482,7 @@ function doctorProblem(
|
|
|
446
482
|
: { type, path: pathValue, message, fixCommand };
|
|
447
483
|
}
|
|
448
484
|
|
|
449
|
-
function activeEntryDir(cwd: string, sessionId: string
|
|
485
|
+
function activeEntryDir(cwd: string, sessionId: string): string {
|
|
450
486
|
return path.join(stateDirFor(cwd, sessionId), "active");
|
|
451
487
|
}
|
|
452
488
|
|
|
@@ -507,9 +543,9 @@ function pushPhaseDriftProblem(options: {
|
|
|
507
543
|
async function collectDoctorSummary(
|
|
508
544
|
cwd: string,
|
|
509
545
|
skill: CanonicalGjcWorkflowSkill | undefined,
|
|
510
|
-
sessionId: string
|
|
546
|
+
sessionId: string,
|
|
511
547
|
): Promise<DoctorSummary> {
|
|
512
|
-
const root =
|
|
548
|
+
const root = sessionStateDir(cwd, sessionId);
|
|
513
549
|
const skills = skill ? [skill] : [...CANONICAL_GJC_WORKFLOW_SKILLS];
|
|
514
550
|
const problems: DoctorProblem[] = [];
|
|
515
551
|
let filesScanned = 0;
|
|
@@ -583,7 +619,7 @@ async function collectDoctorSummary(
|
|
|
583
619
|
}
|
|
584
620
|
}
|
|
585
621
|
|
|
586
|
-
const inspectActiveScope = async (scopeSessionId: string
|
|
622
|
+
const inspectActiveScope = async (scopeSessionId: string): Promise<void> => {
|
|
587
623
|
const snapshotPath = activeStateFile(cwd, scopeSessionId);
|
|
588
624
|
const snapshot = await readRawJson(snapshotPath);
|
|
589
625
|
if (snapshot.exists) filesScanned += 1;
|
|
@@ -624,7 +660,9 @@ async function collectDoctorSummary(
|
|
|
624
660
|
}
|
|
625
661
|
}
|
|
626
662
|
if (isPlainObject(snapshot.value)) {
|
|
627
|
-
const activeSkills = Array.isArray(snapshot.value.active_skills)
|
|
663
|
+
const activeSkills: unknown[] = Array.isArray(snapshot.value.active_skills)
|
|
664
|
+
? snapshot.value.active_skills
|
|
665
|
+
: [];
|
|
628
666
|
for (const entry of activeSkills) {
|
|
629
667
|
const entrySkill = skillFromActiveValue(entry);
|
|
630
668
|
if (!entrySkill) continue;
|
|
@@ -658,21 +696,6 @@ async function collectDoctorSummary(
|
|
|
658
696
|
};
|
|
659
697
|
|
|
660
698
|
await inspectActiveScope(sessionId);
|
|
661
|
-
if (!sessionId) {
|
|
662
|
-
const sessionsDir = path.join(root, "sessions");
|
|
663
|
-
let sessions: string[] = [];
|
|
664
|
-
try {
|
|
665
|
-
const entries = await fs.readdir(sessionsDir, { withFileTypes: true });
|
|
666
|
-
sessions = entries
|
|
667
|
-
.filter(entry => entry.isDirectory())
|
|
668
|
-
.map(entry => entry.name)
|
|
669
|
-
.sort();
|
|
670
|
-
} catch (error) {
|
|
671
|
-
const err = error as NodeJS.ErrnoException;
|
|
672
|
-
if (err.code !== "ENOENT") throw error;
|
|
673
|
-
}
|
|
674
|
-
for (const rawSession of sessions) await inspectActiveScope(decodeURIComponent(rawSession));
|
|
675
|
-
}
|
|
676
699
|
|
|
677
700
|
problems.sort(
|
|
678
701
|
(a, b) =>
|
|
@@ -727,8 +750,16 @@ async function handleDoctor(
|
|
|
727
750
|
const rawSkill = flagValue(args, "--skill")?.trim() || flagValue(args, "--mode")?.trim() || positionalSkill?.trim();
|
|
728
751
|
if (rawSkill) assertKnownMode(rawSkill);
|
|
729
752
|
const payload = await readInputJson(flagValue(args, "--input"), cwd);
|
|
730
|
-
const
|
|
731
|
-
|
|
753
|
+
const session = await resolveGjcSessionForRead(cwd, {
|
|
754
|
+
flagValue: flagValue(args, "--session-id"),
|
|
755
|
+
payloadSessionId: payload?.session_id,
|
|
756
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
757
|
+
});
|
|
758
|
+
const summary = await collectDoctorSummary(
|
|
759
|
+
cwd,
|
|
760
|
+
rawSkill as CanonicalGjcWorkflowSkill | undefined,
|
|
761
|
+
session.gjcSessionId,
|
|
762
|
+
);
|
|
732
763
|
return {
|
|
733
764
|
status: summary.ok ? 0 : 1,
|
|
734
765
|
stdout: hasFlag(args, "--json") ? `${JSON.stringify(summary, null, 2)}\n` : renderDoctorText(summary),
|
|
@@ -737,6 +768,7 @@ async function handleDoctor(
|
|
|
737
768
|
|
|
738
769
|
async function warnAndAuditOutOfBandIfNeeded(
|
|
739
770
|
cwd: string,
|
|
771
|
+
sessionId: string,
|
|
740
772
|
filePath: string,
|
|
741
773
|
skill: CanonicalGjcWorkflowSkill,
|
|
742
774
|
options?: { mutationId?: string; forced?: boolean },
|
|
@@ -751,7 +783,7 @@ async function warnAndAuditOutOfBandIfNeeded(
|
|
|
751
783
|
}
|
|
752
784
|
if (!mismatch) return undefined;
|
|
753
785
|
const message = `WARNING: workflow mode-state out-of-band edit detected for ${skill}: ${filePath} expected sha256 ${mismatch.expected} but found ${mismatch.actual}`;
|
|
754
|
-
await appendAuditEntry(cwd, {
|
|
786
|
+
await appendAuditEntry(cwd, sessionId, {
|
|
755
787
|
ts: new Date().toISOString(),
|
|
756
788
|
skill,
|
|
757
789
|
category: "state",
|
|
@@ -766,12 +798,19 @@ async function warnAndAuditOutOfBandIfNeeded(
|
|
|
766
798
|
return message;
|
|
767
799
|
}
|
|
768
800
|
|
|
801
|
+
function existingStateRevision(value: unknown): number | undefined {
|
|
802
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
803
|
+
const revision = (value as Record<string, unknown>).state_revision;
|
|
804
|
+
return typeof revision === "number" && Number.isFinite(revision) ? revision : 0;
|
|
805
|
+
}
|
|
806
|
+
|
|
769
807
|
async function writeJsonAtomic(
|
|
770
808
|
cwd: string,
|
|
771
809
|
filePath: string,
|
|
772
810
|
value: unknown,
|
|
773
811
|
verb: "write" | "clear" | "handoff" | "reconcile" = "write",
|
|
774
812
|
options?: {
|
|
813
|
+
sessionId: string;
|
|
775
814
|
skill?: CanonicalGjcWorkflowSkill;
|
|
776
815
|
mutationId?: string;
|
|
777
816
|
force?: boolean;
|
|
@@ -779,9 +818,9 @@ async function writeJsonAtomic(
|
|
|
779
818
|
toPhase?: string;
|
|
780
819
|
owner?: WorkflowStateMutationOwner;
|
|
781
820
|
},
|
|
782
|
-
): Promise<{ warning?: string; stamped: Record<string, unknown
|
|
821
|
+
): Promise<{ warning?: string; stamped: Record<string, unknown>; revision: number }> {
|
|
783
822
|
const warning = options?.skill
|
|
784
|
-
? await warnAndAuditOutOfBandIfNeeded(cwd, filePath, options.skill, {
|
|
823
|
+
? await warnAndAuditOutOfBandIfNeeded(cwd, options.sessionId, filePath, options.skill, {
|
|
785
824
|
mutationId: options.mutationId,
|
|
786
825
|
forced: options.force ?? false,
|
|
787
826
|
})
|
|
@@ -789,9 +828,15 @@ async function writeJsonAtomic(
|
|
|
789
828
|
if (warning && !options?.force) {
|
|
790
829
|
throw new StateCommandError(2, `${warning}; use --force to overwrite tampered mode-state`);
|
|
791
830
|
}
|
|
792
|
-
|
|
831
|
+
// Authoritative CLI/runtime write. Stamp the next state_revision under the
|
|
832
|
+
// writer lock; do not enforce an optimistic `expectedRevision` here (tamper
|
|
833
|
+
// detection is handled by warnAndAuditOutOfBandIfNeeded above, and a forced
|
|
834
|
+
// write must succeed over corrupt/missing prior state).
|
|
835
|
+
const writeResult = await writeGuardedWorkflowEnvelopeAtomic(filePath, value, {
|
|
793
836
|
cwd,
|
|
837
|
+
policy: "source",
|
|
794
838
|
audit: {
|
|
839
|
+
sessionId: options?.sessionId ?? "",
|
|
795
840
|
category: "state",
|
|
796
841
|
verb,
|
|
797
842
|
owner: options?.owner ?? "gjc-state-cli",
|
|
@@ -802,7 +847,11 @@ async function writeJsonAtomic(
|
|
|
802
847
|
forced: options?.force ?? false,
|
|
803
848
|
},
|
|
804
849
|
});
|
|
805
|
-
|
|
850
|
+
// `writeResult.revision` is computed inside the writer lock, so it is the revision this
|
|
851
|
+
// write actually owns. Prefer it over a post-lock file re-read, which a concurrent writer
|
|
852
|
+
// could have advanced before the read — that race could otherwise let this payload be
|
|
853
|
+
// published with another writer's newer revision.
|
|
854
|
+
return { warning, stamped: (await readJsonFile(filePath)) ?? {}, revision: writeResult.revision };
|
|
806
855
|
}
|
|
807
856
|
|
|
808
857
|
function parseFieldsFlag(args: readonly string[]): StateProjectionField[] | undefined {
|
|
@@ -851,13 +900,14 @@ function parseSinceFlag(args: readonly string[]): string | undefined {
|
|
|
851
900
|
async function readAuditWindow(
|
|
852
901
|
cwd: string,
|
|
853
902
|
args: readonly string[],
|
|
903
|
+
sessionId: string,
|
|
854
904
|
): Promise<{ entries: unknown[]; limit: number; since?: string; truncated: boolean }> {
|
|
855
905
|
const limit = parseLimitFlag(args);
|
|
856
906
|
const since = parseSinceFlag(args);
|
|
857
|
-
const
|
|
907
|
+
const auditFile = auditPath(cwd, sessionId);
|
|
858
908
|
let raw = "";
|
|
859
909
|
try {
|
|
860
|
-
raw = await fs.readFile(
|
|
910
|
+
raw = await fs.readFile(auditFile, "utf-8");
|
|
861
911
|
} catch (error) {
|
|
862
912
|
const err = error as NodeJS.ErrnoException;
|
|
863
913
|
if (err.code !== "ENOENT") throw error;
|
|
@@ -965,6 +1015,14 @@ function buildHudForMode(
|
|
|
965
1015
|
typeof (rawLedger as Record<string, unknown>).timestamp === "string"
|
|
966
1016
|
? ((rawLedger as Record<string, unknown>).timestamp as string)
|
|
967
1017
|
: undefined,
|
|
1018
|
+
kind:
|
|
1019
|
+
typeof (rawLedger as Record<string, unknown>).kind === "string"
|
|
1020
|
+
? ((rawLedger as Record<string, unknown>).kind as string)
|
|
1021
|
+
: undefined,
|
|
1022
|
+
evidence:
|
|
1023
|
+
typeof (rawLedger as Record<string, unknown>).evidence === "string"
|
|
1024
|
+
? ((rawLedger as Record<string, unknown>).evidence as string)
|
|
1025
|
+
: undefined,
|
|
968
1026
|
}
|
|
969
1027
|
: undefined;
|
|
970
1028
|
const status = typeof payload.status === "string" ? (payload.status as string) : (phase ?? "pending");
|
|
@@ -1014,7 +1072,7 @@ function buildHudForMode(
|
|
|
1014
1072
|
async function syncWorkflowSkillState(options: {
|
|
1015
1073
|
cwd: string;
|
|
1016
1074
|
mode: CanonicalGjcWorkflowSkill;
|
|
1017
|
-
sessionId: string
|
|
1075
|
+
sessionId: string;
|
|
1018
1076
|
threadId?: string;
|
|
1019
1077
|
turnId?: string;
|
|
1020
1078
|
active: boolean;
|
|
@@ -1034,6 +1092,7 @@ async function syncWorkflowSkillState(options: {
|
|
|
1034
1092
|
source: "gjc-state-cli",
|
|
1035
1093
|
hud: buildHudForMode(options.mode, options.payload),
|
|
1036
1094
|
...(options.receipt ? { receipt: options.receipt } : {}),
|
|
1095
|
+
sourceRevision: existingStateRevision(options.payload),
|
|
1037
1096
|
});
|
|
1038
1097
|
} catch {
|
|
1039
1098
|
// HUD sync is best-effort and must not change command semantics.
|
|
@@ -1053,14 +1112,19 @@ async function syncWorkflowSkillState(options: {
|
|
|
1053
1112
|
export async function reconcileWorkflowSkillState(options: {
|
|
1054
1113
|
cwd: string;
|
|
1055
1114
|
mode: CanonicalGjcWorkflowSkill;
|
|
1056
|
-
sessionId
|
|
1115
|
+
sessionId?: string;
|
|
1057
1116
|
threadId?: string;
|
|
1058
1117
|
turnId?: string;
|
|
1059
1118
|
active: boolean;
|
|
1060
1119
|
phase: string;
|
|
1061
1120
|
payload: Record<string, unknown>;
|
|
1121
|
+
sourceRevision?: number;
|
|
1062
1122
|
}): Promise<{ stateFile: string }> {
|
|
1063
|
-
const { cwd, mode,
|
|
1123
|
+
const { cwd, mode, threadId, turnId, active, payload } = options;
|
|
1124
|
+
const { gjcSessionId: sessionId } = resolveGjcSessionForWrite(cwd, {
|
|
1125
|
+
payloadSessionId: options.sessionId,
|
|
1126
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
1127
|
+
});
|
|
1064
1128
|
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
1065
1129
|
const existingRead = await readExistingStateForMutation(filePath);
|
|
1066
1130
|
const existingPayload = existingRead.kind === "valid" ? existingRead.value : {};
|
|
@@ -1104,14 +1168,37 @@ export async function reconcileWorkflowSkillState(options: {
|
|
|
1104
1168
|
const validation = validateWorkflowStateEnvelope(mode, merged);
|
|
1105
1169
|
if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
|
|
1106
1170
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1171
|
+
if (existingRead.kind === "corrupt") await fs.rm(filePath, { force: true });
|
|
1172
|
+
await writeGuardedWorkflowEnvelopeAtomic(filePath, merged, {
|
|
1173
|
+
cwd,
|
|
1174
|
+
policy: "source",
|
|
1175
|
+
receipt: {
|
|
1176
|
+
cwd,
|
|
1177
|
+
skill: mode,
|
|
1178
|
+
owner: "gjc-runtime",
|
|
1179
|
+
command: `gjc ${mode} (reconcile)`,
|
|
1180
|
+
sessionId,
|
|
1181
|
+
nowIso: nowIsoStr,
|
|
1182
|
+
mutationId,
|
|
1183
|
+
verb: "reconcile",
|
|
1184
|
+
forced: true,
|
|
1185
|
+
fromPhase,
|
|
1186
|
+
toPhase: trimmedPhase,
|
|
1187
|
+
},
|
|
1188
|
+
audit: {
|
|
1189
|
+
category: "state",
|
|
1190
|
+
verb: "reconcile",
|
|
1191
|
+
owner: "gjc-runtime",
|
|
1192
|
+
sessionId,
|
|
1193
|
+
skill: mode,
|
|
1194
|
+
mutationId,
|
|
1195
|
+
forced: true,
|
|
1196
|
+
fromPhase,
|
|
1197
|
+
toPhase: trimmedPhase,
|
|
1198
|
+
},
|
|
1114
1199
|
});
|
|
1200
|
+
const persisted = (await readJsonFile(filePath)) ?? {};
|
|
1201
|
+
const sourceRevision = options.sourceRevision ?? existingStateRevision(persisted);
|
|
1115
1202
|
|
|
1116
1203
|
// Reconciliation drives the active-state/HUD update directly (not via the
|
|
1117
1204
|
// best-effort syncWorkflowSkillState wrapper) so a failed HUD/active-state write
|
|
@@ -1128,7 +1215,9 @@ export async function reconcileWorkflowSkillState(options: {
|
|
|
1128
1215
|
source: "gjc-runtime-reconcile",
|
|
1129
1216
|
hud: buildHudForMode(mode, merged),
|
|
1130
1217
|
receipt,
|
|
1218
|
+
sourceRevision,
|
|
1131
1219
|
});
|
|
1220
|
+
await touchStateActivityMarker(cwd, sessionId, filePath);
|
|
1132
1221
|
return { stateFile: filePath };
|
|
1133
1222
|
}
|
|
1134
1223
|
export async function readWorkflowStateJson(
|
|
@@ -1136,7 +1225,11 @@ export async function readWorkflowStateJson(
|
|
|
1136
1225
|
skill: CanonicalGjcWorkflowSkill,
|
|
1137
1226
|
sessionId?: string,
|
|
1138
1227
|
): Promise<Record<string, unknown>> {
|
|
1139
|
-
|
|
1228
|
+
const session = await resolveGjcSessionForRead(cwd, {
|
|
1229
|
+
payloadSessionId: sessionId,
|
|
1230
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
1231
|
+
});
|
|
1232
|
+
return (await readJsonFile(modeStateFile(cwd, skill, session.gjcSessionId))) ?? {};
|
|
1140
1233
|
}
|
|
1141
1234
|
|
|
1142
1235
|
async function handleRead(
|
|
@@ -1144,12 +1237,12 @@ async function handleRead(
|
|
|
1144
1237
|
cwd: string,
|
|
1145
1238
|
positionalSkill: string | undefined,
|
|
1146
1239
|
): Promise<StateCommandResult> {
|
|
1147
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1148
|
-
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.
|
|
1240
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "read");
|
|
1241
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
|
|
1149
1242
|
const fields = parseFieldsFlag(args);
|
|
1150
1243
|
if (mode) {
|
|
1151
|
-
const filePath = modeStateFile(cwd, mode, selectors.
|
|
1152
|
-
const existing = await readWorkflowStateJson(cwd, mode, selectors.
|
|
1244
|
+
const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
|
|
1245
|
+
const existing = await readWorkflowStateJson(cwd, mode, selectors.gjcSessionId);
|
|
1153
1246
|
const envelope = { skill: mode, state: existing, storage_path: filePath };
|
|
1154
1247
|
const manifest = getSkillManifest(mode);
|
|
1155
1248
|
if (fields) {
|
|
@@ -1177,7 +1270,7 @@ async function handleRead(
|
|
|
1177
1270
|
: renderStateMarkdown(mode, envelope, manifest),
|
|
1178
1271
|
};
|
|
1179
1272
|
}
|
|
1180
|
-
const filePath = activeStateFile(cwd, selectors.
|
|
1273
|
+
const filePath = activeStateFile(cwd, selectors.gjcSessionId);
|
|
1181
1274
|
const existingRaw = await readJsonValue(filePath);
|
|
1182
1275
|
const existing = isPlainObject(existingRaw) ? existingRaw : null;
|
|
1183
1276
|
return { status: 0, stdout: `${JSON.stringify(existing ?? {}, null, 2)}\n` };
|
|
@@ -1188,16 +1281,16 @@ async function handleStatus(
|
|
|
1188
1281
|
cwd: string,
|
|
1189
1282
|
positionalSkill: string | undefined,
|
|
1190
1283
|
): Promise<StateCommandResult> {
|
|
1191
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1192
|
-
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.
|
|
1284
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "read");
|
|
1285
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
|
|
1193
1286
|
if (!mode) {
|
|
1194
1287
|
throw new StateCommandError(
|
|
1195
1288
|
2,
|
|
1196
|
-
"gjc state status requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
1289
|
+
"gjc state status requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1197
1290
|
);
|
|
1198
1291
|
}
|
|
1199
|
-
const filePath = modeStateFile(cwd, mode, selectors.
|
|
1200
|
-
const existing = await readWorkflowStateJson(cwd, mode, selectors.
|
|
1292
|
+
const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
|
|
1293
|
+
const existing = await readWorkflowStateJson(cwd, mode, selectors.gjcSessionId);
|
|
1201
1294
|
const summary = buildStateStatusSummary(
|
|
1202
1295
|
mode,
|
|
1203
1296
|
{ skill: mode, state: existing, storage_path: filePath },
|
|
@@ -1215,14 +1308,14 @@ async function handleWrite(
|
|
|
1215
1308
|
cwd: string,
|
|
1216
1309
|
positionalSkill: string | undefined,
|
|
1217
1310
|
): Promise<StateCommandResult> {
|
|
1218
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1219
|
-
const { sessionId, threadId, turnId, payload } = selectors;
|
|
1311
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "write");
|
|
1312
|
+
const { gjcSessionId: sessionId, threadId, turnId, payload } = selectors;
|
|
1220
1313
|
if (!payload) throw new StateCommandError(2, "gjc state write requires --input '<json>'");
|
|
1221
1314
|
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
|
|
1222
1315
|
if (!mode)
|
|
1223
1316
|
throw new StateCommandError(
|
|
1224
1317
|
2,
|
|
1225
|
-
"gjc state write requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
1318
|
+
"gjc state write requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1226
1319
|
);
|
|
1227
1320
|
|
|
1228
1321
|
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
@@ -1310,7 +1403,12 @@ async function handleWrite(
|
|
|
1310
1403
|
const validation = validateWorkflowStateEnvelope(mode, merged);
|
|
1311
1404
|
if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
|
|
1312
1405
|
|
|
1313
|
-
const {
|
|
1406
|
+
const {
|
|
1407
|
+
warning: outOfBandWarning,
|
|
1408
|
+
stamped,
|
|
1409
|
+
revision: stampedRevision,
|
|
1410
|
+
} = await writeJsonAtomic(cwd, filePath, merged, "write", {
|
|
1411
|
+
sessionId,
|
|
1314
1412
|
skill: mode,
|
|
1315
1413
|
mutationId,
|
|
1316
1414
|
force: forced,
|
|
@@ -1321,7 +1419,14 @@ async function handleWrite(
|
|
|
1321
1419
|
|
|
1322
1420
|
const phase = typeof merged.current_phase === "string" ? merged.current_phase : undefined;
|
|
1323
1421
|
const active = merged.active !== false;
|
|
1422
|
+
// Reflect the lock-owned mode-state revision onto the in-memory payload so the active-state/HUD
|
|
1423
|
+
// sync derives a `sourceRevision` from the revision this write actually owns (computed inside the
|
|
1424
|
+
// writer lock), not the stale pre-write value or a post-lock re-read a concurrent writer could
|
|
1425
|
+
// have advanced; otherwise the active-state writer stale-skips the update and the mirror keeps the
|
|
1426
|
+
// prior phase (e.g. staying "interviewing" after a "handoff" write).
|
|
1427
|
+
merged.state_revision = stampedRevision;
|
|
1324
1428
|
await syncWorkflowSkillState({ cwd, mode, sessionId, threadId, turnId, active, phase, payload: merged, receipt });
|
|
1429
|
+
await touchStateActivityMarker(cwd, sessionId, filePath);
|
|
1325
1430
|
|
|
1326
1431
|
return {
|
|
1327
1432
|
status: 0,
|
|
@@ -1344,13 +1449,13 @@ async function handleClear(
|
|
|
1344
1449
|
cwd: string,
|
|
1345
1450
|
positionalSkill: string | undefined,
|
|
1346
1451
|
): Promise<StateCommandResult> {
|
|
1347
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1348
|
-
const { sessionId, threadId, turnId } = selectors;
|
|
1452
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "clear");
|
|
1453
|
+
const { gjcSessionId: sessionId, threadId, turnId } = selectors;
|
|
1349
1454
|
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
|
|
1350
1455
|
if (!mode)
|
|
1351
1456
|
throw new StateCommandError(
|
|
1352
1457
|
2,
|
|
1353
|
-
"gjc state clear requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
1458
|
+
"gjc state clear requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1354
1459
|
);
|
|
1355
1460
|
|
|
1356
1461
|
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
@@ -1363,6 +1468,10 @@ async function handleClear(
|
|
|
1363
1468
|
);
|
|
1364
1469
|
}
|
|
1365
1470
|
const existing = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1471
|
+
const staleReason = await describeStaleClearState(cwd, sessionId, mode, existing);
|
|
1472
|
+
if (staleReason && !forced) {
|
|
1473
|
+
throw new StateCommandError(2, `existing state for ${mode} is stale (${staleReason}); use --force to clear`);
|
|
1474
|
+
}
|
|
1366
1475
|
const clearedAt = nowIso();
|
|
1367
1476
|
const cleared: Record<string, unknown> = {
|
|
1368
1477
|
skill: mode,
|
|
@@ -1385,6 +1494,7 @@ async function handleClear(
|
|
|
1385
1494
|
});
|
|
1386
1495
|
cleared.receipt = receipt;
|
|
1387
1496
|
const { warning: outOfBandWarning, stamped } = await writeJsonAtomic(cwd, filePath, cleared, "clear", {
|
|
1497
|
+
sessionId,
|
|
1388
1498
|
skill: mode,
|
|
1389
1499
|
mutationId,
|
|
1390
1500
|
force: forced,
|
|
@@ -1403,6 +1513,7 @@ async function handleClear(
|
|
|
1403
1513
|
phase: "complete",
|
|
1404
1514
|
payload: cleared,
|
|
1405
1515
|
});
|
|
1516
|
+
await touchStateActivityMarker(cwd, sessionId, filePath);
|
|
1406
1517
|
return {
|
|
1407
1518
|
status: 0,
|
|
1408
1519
|
stdout: renderCliWriteReceipt({
|
|
@@ -1443,13 +1554,13 @@ async function handleHandoff(
|
|
|
1443
1554
|
cwd: string,
|
|
1444
1555
|
positionalSkill: string | undefined,
|
|
1445
1556
|
): Promise<StateCommandResult> {
|
|
1446
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1447
|
-
const { sessionId, threadId, turnId } = selectors;
|
|
1557
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "handoff");
|
|
1558
|
+
const { gjcSessionId: sessionId, threadId, turnId } = selectors;
|
|
1448
1559
|
const caller = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
|
|
1449
1560
|
if (!caller) {
|
|
1450
1561
|
throw new StateCommandError(
|
|
1451
1562
|
2,
|
|
1452
|
-
"gjc state handoff requires --mode <caller>, positional <caller>, input.skill, or an active workflow in
|
|
1563
|
+
"gjc state handoff requires --mode <caller>, positional <caller>, input.skill, or an active workflow in the current session active state",
|
|
1453
1564
|
);
|
|
1454
1565
|
}
|
|
1455
1566
|
const calleeRaw = flagValue(args, "--to")?.trim();
|
|
@@ -1552,6 +1663,7 @@ async function handleHandoff(
|
|
|
1552
1663
|
|
|
1553
1664
|
await beginWorkflowTransactionJournal({
|
|
1554
1665
|
cwd,
|
|
1666
|
+
sessionId,
|
|
1555
1667
|
mutationId,
|
|
1556
1668
|
caller,
|
|
1557
1669
|
callee,
|
|
@@ -1567,21 +1679,23 @@ async function handleHandoff(
|
|
|
1567
1679
|
// only; corrupt JSON / IO failures propagate as non-zero CLI status.
|
|
1568
1680
|
const force = hasFlag(args, "--force");
|
|
1569
1681
|
const calleeWrite = await writeJsonAtomic(cwd, calleePath, mergedCalleeState, "handoff", {
|
|
1682
|
+
sessionId,
|
|
1570
1683
|
skill: callee,
|
|
1571
1684
|
mutationId,
|
|
1572
1685
|
force,
|
|
1573
1686
|
fromPhase: typeof existingCallee.current_phase === "string" ? existingCallee.current_phase : undefined,
|
|
1574
1687
|
toPhase: calleeInitial,
|
|
1575
1688
|
});
|
|
1576
|
-
await updateWorkflowTransactionJournal(cwd, mutationId, { steps: ["callee-mode-state"] });
|
|
1689
|
+
await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, { steps: ["callee-mode-state"] });
|
|
1577
1690
|
const callerWrite = await writeJsonAtomic(cwd, callerPath, mergedCallerState, "handoff", {
|
|
1691
|
+
sessionId,
|
|
1578
1692
|
skill: caller,
|
|
1579
1693
|
mutationId,
|
|
1580
1694
|
force,
|
|
1581
1695
|
fromPhase: typeof existingCaller.current_phase === "string" ? existingCaller.current_phase : undefined,
|
|
1582
1696
|
toPhase: "handoff",
|
|
1583
1697
|
});
|
|
1584
|
-
await updateWorkflowTransactionJournal(cwd, mutationId, {
|
|
1698
|
+
await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, {
|
|
1585
1699
|
steps: ["callee-mode-state", "caller-mode-state"],
|
|
1586
1700
|
});
|
|
1587
1701
|
const warnings = [calleeWrite.warning, callerWrite.warning].filter(
|
|
@@ -1626,10 +1740,11 @@ async function handleHandoff(
|
|
|
1626
1740
|
receipt: calleeReceipt,
|
|
1627
1741
|
},
|
|
1628
1742
|
});
|
|
1629
|
-
await updateWorkflowTransactionJournal(cwd, mutationId, {
|
|
1743
|
+
await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, {
|
|
1630
1744
|
steps: ["callee-mode-state", "caller-mode-state", "active-state"],
|
|
1631
1745
|
});
|
|
1632
|
-
await completeWorkflowTransactionJournal(cwd, mutationId);
|
|
1746
|
+
await completeWorkflowTransactionJournal(cwd, sessionId, mutationId);
|
|
1747
|
+
await touchStateActivityMarker(cwd, sessionId, callerPath);
|
|
1633
1748
|
|
|
1634
1749
|
return {
|
|
1635
1750
|
status: 0,
|
|
@@ -1668,7 +1783,7 @@ async function handleContract(
|
|
|
1668
1783
|
cwd: string,
|
|
1669
1784
|
positionalSkill: string | undefined,
|
|
1670
1785
|
): Promise<StateCommandResult> {
|
|
1671
|
-
const { mode } = await resolveSelectors(args, cwd, positionalSkill);
|
|
1786
|
+
const { mode } = await resolveSelectors(args, cwd, positionalSkill, "read");
|
|
1672
1787
|
if (!mode) {
|
|
1673
1788
|
throw new StateCommandError(2, "gjc state contract requires --mode <skill>, positional <skill>, or input.skill");
|
|
1674
1789
|
}
|
|
@@ -1723,11 +1838,7 @@ function categoryForStateRelativePath(relativePath: string): string | undefined
|
|
|
1723
1838
|
if (normalized === "audit.jsonl") return undefined;
|
|
1724
1839
|
if (normalized === SKILL_ACTIVE_STATE_FILE || normalized.endsWith(`/${SKILL_ACTIVE_STATE_FILE}`)) return undefined;
|
|
1725
1840
|
if (normalized.startsWith("active/") || normalized.includes("/active/")) return undefined;
|
|
1726
|
-
if (
|
|
1727
|
-
/^[^/]+-state\.json$/.test(normalized) ||
|
|
1728
|
-
(normalized.includes("/sessions/") && /\/[^/]+-state\.json$/.test(normalized))
|
|
1729
|
-
)
|
|
1730
|
-
return undefined;
|
|
1841
|
+
if (/^[^/]+-state\.json$/.test(normalized) || false) return undefined;
|
|
1731
1842
|
if (normalized.startsWith("artifacts/") || normalized.includes("/artifacts/")) return "artifact";
|
|
1732
1843
|
if (
|
|
1733
1844
|
normalized.startsWith("logs/") ||
|
|
@@ -1753,9 +1864,10 @@ function categoryForStateRelativePath(relativePath: string): string | undefined
|
|
|
1753
1864
|
|
|
1754
1865
|
async function collectRetentionCandidates(
|
|
1755
1866
|
cwd: string,
|
|
1867
|
+
sessionId: string,
|
|
1756
1868
|
skills: readonly CanonicalGjcWorkflowSkill[],
|
|
1757
1869
|
): Promise<RetentionCandidate[]> {
|
|
1758
|
-
const stateRoot =
|
|
1870
|
+
const stateRoot = sessionStateDir(cwd, sessionId);
|
|
1759
1871
|
const policies = new Map<string, { keep?: number; maxAgeDays?: number }>();
|
|
1760
1872
|
for (const skill of skills) {
|
|
1761
1873
|
for (const policy of getSkillManifest(skill).retention) {
|
|
@@ -1836,7 +1948,11 @@ async function buildGcSummary(
|
|
|
1836
1948
|
flagValue(args, "--skill")?.trim() || flagValue(args, "--mode")?.trim() || positionalSkill?.trim() || "all";
|
|
1837
1949
|
if (rawSkill !== "all") assertKnownMode(rawSkill);
|
|
1838
1950
|
const skills = rawSkill === "all" ? CANONICAL_GJC_WORKFLOW_SKILLS : [rawSkill as CanonicalGjcWorkflowSkill];
|
|
1839
|
-
const
|
|
1951
|
+
const session = await resolveGjcSessionForRead(cwd, {
|
|
1952
|
+
flagValue: flagValue(args, "--session-id"),
|
|
1953
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
1954
|
+
});
|
|
1955
|
+
const eligible = selectRetentionEligible(await collectRetentionCandidates(cwd, session.gjcSessionId, skills));
|
|
1840
1956
|
const counts: Record<string, number> = {};
|
|
1841
1957
|
for (const candidate of eligible) counts[candidate.category] = (counts[candidate.category] ?? 0) + 1;
|
|
1842
1958
|
const targets: GenericHardPruneTarget[] = eligible.map(candidate => ({
|
|
@@ -1850,6 +1966,7 @@ async function buildGcSummary(
|
|
|
1850
1966
|
cwd,
|
|
1851
1967
|
audit: {
|
|
1852
1968
|
cwd,
|
|
1969
|
+
sessionId: session.gjcSessionId,
|
|
1853
1970
|
skill: rawSkill,
|
|
1854
1971
|
category: "prune",
|
|
1855
1972
|
verb: "gc",
|
|
@@ -1861,7 +1978,7 @@ async function buildGcSummary(
|
|
|
1861
1978
|
skill: rawSkill as CanonicalGjcWorkflowSkill | "all",
|
|
1862
1979
|
dry_run: dryRun,
|
|
1863
1980
|
eligible: eligible.map(candidate => candidate.relativePath),
|
|
1864
|
-
pruned: pruned.map(filePath => path.relative(
|
|
1981
|
+
pruned: pruned.map(filePath => path.relative(sessionStateDir(cwd, session.gjcSessionId), filePath)),
|
|
1865
1982
|
counts,
|
|
1866
1983
|
};
|
|
1867
1984
|
}
|
|
@@ -1872,7 +1989,11 @@ async function handleGraph(
|
|
|
1872
1989
|
positionalSkill: string | undefined,
|
|
1873
1990
|
): Promise<StateCommandResult> {
|
|
1874
1991
|
if (hasFlag(args, "--history")) {
|
|
1875
|
-
const
|
|
1992
|
+
const session = await resolveGjcSessionForRead(_cwd, {
|
|
1993
|
+
flagValue: flagValue(args, "--session-id"),
|
|
1994
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
1995
|
+
});
|
|
1996
|
+
const history = await readAuditWindow(_cwd, args, session.gjcSessionId);
|
|
1876
1997
|
return {
|
|
1877
1998
|
status: 0,
|
|
1878
1999
|
stdout: hasFlag(args, "--json") ? `${JSON.stringify(history, null, 2)}\n` : renderHistoryMarkdown(history),
|
|
@@ -1895,20 +2016,21 @@ async function handlePrune(
|
|
|
1895
2016
|
cwd: string,
|
|
1896
2017
|
positionalSkill: string | undefined,
|
|
1897
2018
|
): Promise<StateCommandResult> {
|
|
1898
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1899
|
-
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.
|
|
2019
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "prune");
|
|
2020
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
|
|
1900
2021
|
if (!mode) {
|
|
1901
2022
|
throw new StateCommandError(
|
|
1902
2023
|
2,
|
|
1903
|
-
"gjc state prune requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
2024
|
+
"gjc state prune requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1904
2025
|
);
|
|
1905
2026
|
}
|
|
1906
|
-
const filePath = modeStateFile(cwd, mode, selectors.
|
|
2027
|
+
const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
|
|
1907
2028
|
const olderThanDays = parseNonNegativeIntegerFlag(args, "--older-than");
|
|
1908
2029
|
const status = flagValue(args, "--status")?.trim();
|
|
1909
2030
|
const targets: GenericHardPruneTarget[] = [{ path: filePath, category: "prune" }];
|
|
1910
2031
|
const audit: StateWriterAuditContext = {
|
|
1911
2032
|
cwd,
|
|
2033
|
+
sessionId: selectors.gjcSessionId,
|
|
1912
2034
|
skill: mode,
|
|
1913
2035
|
category: "prune",
|
|
1914
2036
|
verb: hasFlag(args, "--hard") ? "hard-prune" : "soft-delete",
|
|
@@ -1964,17 +2086,17 @@ async function handleMigrate(
|
|
|
1964
2086
|
cwd: string,
|
|
1965
2087
|
positionalSkill: string | undefined,
|
|
1966
2088
|
): Promise<StateCommandResult> {
|
|
1967
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1968
|
-
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.
|
|
2089
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "migrate");
|
|
2090
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
|
|
1969
2091
|
if (!mode) {
|
|
1970
2092
|
throw new StateCommandError(
|
|
1971
2093
|
2,
|
|
1972
|
-
"gjc state migrate requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
2094
|
+
"gjc state migrate requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1973
2095
|
);
|
|
1974
2096
|
}
|
|
1975
|
-
const filePath = modeStateFile(cwd, mode, selectors.
|
|
2097
|
+
const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
|
|
1976
2098
|
const forced = hasFlag(args, "--force");
|
|
1977
|
-
const mismatchWarning = await warnAndAuditOutOfBandIfNeeded(cwd, filePath, mode, {
|
|
2099
|
+
const mismatchWarning = await warnAndAuditOutOfBandIfNeeded(cwd, selectors.gjcSessionId, filePath, mode, {
|
|
1978
2100
|
forced,
|
|
1979
2101
|
});
|
|
1980
2102
|
if (mismatchWarning && !forced) {
|
|
@@ -1984,7 +2106,7 @@ async function handleMigrate(
|
|
|
1984
2106
|
cwd,
|
|
1985
2107
|
skill: mode,
|
|
1986
2108
|
statePath: filePath,
|
|
1987
|
-
sessionId: selectors.
|
|
2109
|
+
sessionId: selectors.gjcSessionId,
|
|
1988
2110
|
});
|
|
1989
2111
|
return {
|
|
1990
2112
|
status: 0,
|
|
@@ -2026,6 +2148,7 @@ export async function runNativeStateCommand(args: string[], cwd = process.cwd())
|
|
|
2026
2148
|
}
|
|
2027
2149
|
} catch (error) {
|
|
2028
2150
|
if (error instanceof StateCommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
|
|
2151
|
+
if (error instanceof SessionResolutionError) return { status: 2, stderr: `${error.message}\n` };
|
|
2029
2152
|
return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
|
|
2030
2153
|
}
|
|
2031
2154
|
}
|