@gajae-code/coding-agent 0.6.4 → 0.6.5
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 +22 -0
- package/dist/types/cli/migrate-cli.d.ts +20 -0
- package/dist/types/commands/migrate.d.ts +33 -0
- package/dist/types/config/keybindings.d.ts +4 -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 +36 -7
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +7 -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/shared/agent-wire/unattended-audit.d.ts +1 -1
- 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/runtime-mcp/config-writer.d.ts +26 -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/package.json +7 -7
- package/src/cli/migrate-cli.ts +106 -0
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +2 -2
- package/src/commands/migrate.ts +46 -0
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +7 -3
- package/src/coordinator-mcp/policy.ts +10 -2
- 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 +17 -13
- package/src/extensibility/custom-commands/loader.ts +0 -7
- 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 +43 -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 +230 -121
- 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 +45 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +121 -41
- package/src/gjc-runtime/workflow-command-ref.ts +1 -2
- package/src/gjc-runtime/workflow-manifest.ts +1 -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 +6 -4
- 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/bridge/bridge-mode.ts +2 -2
- package/src/modes/components/custom-editor.ts +30 -20
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
- 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 +7 -0
- package/src/runtime-mcp/config-writer.ts +46 -0
- package/src/session/agent-session.ts +15 -21
- package/src/setup/hermes-setup.ts +1 -1
- 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/task/agents.ts +1 -22
- 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.ts +34 -12
- package/src/tools/computer.ts +58 -4
- 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;
|
|
@@ -781,7 +820,7 @@ async function writeJsonAtomic(
|
|
|
781
820
|
},
|
|
782
821
|
): Promise<{ warning?: string; stamped: Record<string, unknown> }> {
|
|
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
|
+
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",
|
|
@@ -851,13 +896,14 @@ function parseSinceFlag(args: readonly string[]): string | undefined {
|
|
|
851
896
|
async function readAuditWindow(
|
|
852
897
|
cwd: string,
|
|
853
898
|
args: readonly string[],
|
|
899
|
+
sessionId: string,
|
|
854
900
|
): Promise<{ entries: unknown[]; limit: number; since?: string; truncated: boolean }> {
|
|
855
901
|
const limit = parseLimitFlag(args);
|
|
856
902
|
const since = parseSinceFlag(args);
|
|
857
|
-
const
|
|
903
|
+
const auditFile = auditPath(cwd, sessionId);
|
|
858
904
|
let raw = "";
|
|
859
905
|
try {
|
|
860
|
-
raw = await fs.readFile(
|
|
906
|
+
raw = await fs.readFile(auditFile, "utf-8");
|
|
861
907
|
} catch (error) {
|
|
862
908
|
const err = error as NodeJS.ErrnoException;
|
|
863
909
|
if (err.code !== "ENOENT") throw error;
|
|
@@ -965,6 +1011,14 @@ function buildHudForMode(
|
|
|
965
1011
|
typeof (rawLedger as Record<string, unknown>).timestamp === "string"
|
|
966
1012
|
? ((rawLedger as Record<string, unknown>).timestamp as string)
|
|
967
1013
|
: undefined,
|
|
1014
|
+
kind:
|
|
1015
|
+
typeof (rawLedger as Record<string, unknown>).kind === "string"
|
|
1016
|
+
? ((rawLedger as Record<string, unknown>).kind as string)
|
|
1017
|
+
: undefined,
|
|
1018
|
+
evidence:
|
|
1019
|
+
typeof (rawLedger as Record<string, unknown>).evidence === "string"
|
|
1020
|
+
? ((rawLedger as Record<string, unknown>).evidence as string)
|
|
1021
|
+
: undefined,
|
|
968
1022
|
}
|
|
969
1023
|
: undefined;
|
|
970
1024
|
const status = typeof payload.status === "string" ? (payload.status as string) : (phase ?? "pending");
|
|
@@ -1014,7 +1068,7 @@ function buildHudForMode(
|
|
|
1014
1068
|
async function syncWorkflowSkillState(options: {
|
|
1015
1069
|
cwd: string;
|
|
1016
1070
|
mode: CanonicalGjcWorkflowSkill;
|
|
1017
|
-
sessionId: string
|
|
1071
|
+
sessionId: string;
|
|
1018
1072
|
threadId?: string;
|
|
1019
1073
|
turnId?: string;
|
|
1020
1074
|
active: boolean;
|
|
@@ -1034,6 +1088,7 @@ async function syncWorkflowSkillState(options: {
|
|
|
1034
1088
|
source: "gjc-state-cli",
|
|
1035
1089
|
hud: buildHudForMode(options.mode, options.payload),
|
|
1036
1090
|
...(options.receipt ? { receipt: options.receipt } : {}),
|
|
1091
|
+
sourceRevision: existingStateRevision(options.payload),
|
|
1037
1092
|
});
|
|
1038
1093
|
} catch {
|
|
1039
1094
|
// HUD sync is best-effort and must not change command semantics.
|
|
@@ -1053,14 +1108,19 @@ async function syncWorkflowSkillState(options: {
|
|
|
1053
1108
|
export async function reconcileWorkflowSkillState(options: {
|
|
1054
1109
|
cwd: string;
|
|
1055
1110
|
mode: CanonicalGjcWorkflowSkill;
|
|
1056
|
-
sessionId
|
|
1111
|
+
sessionId?: string;
|
|
1057
1112
|
threadId?: string;
|
|
1058
1113
|
turnId?: string;
|
|
1059
1114
|
active: boolean;
|
|
1060
1115
|
phase: string;
|
|
1061
1116
|
payload: Record<string, unknown>;
|
|
1117
|
+
sourceRevision?: number;
|
|
1062
1118
|
}): Promise<{ stateFile: string }> {
|
|
1063
|
-
const { cwd, mode,
|
|
1119
|
+
const { cwd, mode, threadId, turnId, active, payload } = options;
|
|
1120
|
+
const { gjcSessionId: sessionId } = resolveGjcSessionForWrite(cwd, {
|
|
1121
|
+
payloadSessionId: options.sessionId,
|
|
1122
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
1123
|
+
});
|
|
1064
1124
|
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
1065
1125
|
const existingRead = await readExistingStateForMutation(filePath);
|
|
1066
1126
|
const existingPayload = existingRead.kind === "valid" ? existingRead.value : {};
|
|
@@ -1104,14 +1164,37 @@ export async function reconcileWorkflowSkillState(options: {
|
|
|
1104
1164
|
const validation = validateWorkflowStateEnvelope(mode, merged);
|
|
1105
1165
|
if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
|
|
1106
1166
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1167
|
+
if (existingRead.kind === "corrupt") await fs.rm(filePath, { force: true });
|
|
1168
|
+
await writeGuardedWorkflowEnvelopeAtomic(filePath, merged, {
|
|
1169
|
+
cwd,
|
|
1170
|
+
policy: "source",
|
|
1171
|
+
receipt: {
|
|
1172
|
+
cwd,
|
|
1173
|
+
skill: mode,
|
|
1174
|
+
owner: "gjc-runtime",
|
|
1175
|
+
command: `gjc ${mode} (reconcile)`,
|
|
1176
|
+
sessionId,
|
|
1177
|
+
nowIso: nowIsoStr,
|
|
1178
|
+
mutationId,
|
|
1179
|
+
verb: "reconcile",
|
|
1180
|
+
forced: true,
|
|
1181
|
+
fromPhase,
|
|
1182
|
+
toPhase: trimmedPhase,
|
|
1183
|
+
},
|
|
1184
|
+
audit: {
|
|
1185
|
+
category: "state",
|
|
1186
|
+
verb: "reconcile",
|
|
1187
|
+
owner: "gjc-runtime",
|
|
1188
|
+
sessionId,
|
|
1189
|
+
skill: mode,
|
|
1190
|
+
mutationId,
|
|
1191
|
+
forced: true,
|
|
1192
|
+
fromPhase,
|
|
1193
|
+
toPhase: trimmedPhase,
|
|
1194
|
+
},
|
|
1114
1195
|
});
|
|
1196
|
+
const persisted = (await readJsonFile(filePath)) ?? {};
|
|
1197
|
+
const sourceRevision = options.sourceRevision ?? existingStateRevision(persisted);
|
|
1115
1198
|
|
|
1116
1199
|
// Reconciliation drives the active-state/HUD update directly (not via the
|
|
1117
1200
|
// best-effort syncWorkflowSkillState wrapper) so a failed HUD/active-state write
|
|
@@ -1128,7 +1211,9 @@ export async function reconcileWorkflowSkillState(options: {
|
|
|
1128
1211
|
source: "gjc-runtime-reconcile",
|
|
1129
1212
|
hud: buildHudForMode(mode, merged),
|
|
1130
1213
|
receipt,
|
|
1214
|
+
sourceRevision,
|
|
1131
1215
|
});
|
|
1216
|
+
await touchStateActivityMarker(cwd, sessionId, filePath);
|
|
1132
1217
|
return { stateFile: filePath };
|
|
1133
1218
|
}
|
|
1134
1219
|
export async function readWorkflowStateJson(
|
|
@@ -1136,7 +1221,11 @@ export async function readWorkflowStateJson(
|
|
|
1136
1221
|
skill: CanonicalGjcWorkflowSkill,
|
|
1137
1222
|
sessionId?: string,
|
|
1138
1223
|
): Promise<Record<string, unknown>> {
|
|
1139
|
-
|
|
1224
|
+
const session = await resolveGjcSessionForRead(cwd, {
|
|
1225
|
+
payloadSessionId: sessionId,
|
|
1226
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
1227
|
+
});
|
|
1228
|
+
return (await readJsonFile(modeStateFile(cwd, skill, session.gjcSessionId))) ?? {};
|
|
1140
1229
|
}
|
|
1141
1230
|
|
|
1142
1231
|
async function handleRead(
|
|
@@ -1144,12 +1233,12 @@ async function handleRead(
|
|
|
1144
1233
|
cwd: string,
|
|
1145
1234
|
positionalSkill: string | undefined,
|
|
1146
1235
|
): Promise<StateCommandResult> {
|
|
1147
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1148
|
-
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.
|
|
1236
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "read");
|
|
1237
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
|
|
1149
1238
|
const fields = parseFieldsFlag(args);
|
|
1150
1239
|
if (mode) {
|
|
1151
|
-
const filePath = modeStateFile(cwd, mode, selectors.
|
|
1152
|
-
const existing = await readWorkflowStateJson(cwd, mode, selectors.
|
|
1240
|
+
const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
|
|
1241
|
+
const existing = await readWorkflowStateJson(cwd, mode, selectors.gjcSessionId);
|
|
1153
1242
|
const envelope = { skill: mode, state: existing, storage_path: filePath };
|
|
1154
1243
|
const manifest = getSkillManifest(mode);
|
|
1155
1244
|
if (fields) {
|
|
@@ -1177,7 +1266,7 @@ async function handleRead(
|
|
|
1177
1266
|
: renderStateMarkdown(mode, envelope, manifest),
|
|
1178
1267
|
};
|
|
1179
1268
|
}
|
|
1180
|
-
const filePath = activeStateFile(cwd, selectors.
|
|
1269
|
+
const filePath = activeStateFile(cwd, selectors.gjcSessionId);
|
|
1181
1270
|
const existingRaw = await readJsonValue(filePath);
|
|
1182
1271
|
const existing = isPlainObject(existingRaw) ? existingRaw : null;
|
|
1183
1272
|
return { status: 0, stdout: `${JSON.stringify(existing ?? {}, null, 2)}\n` };
|
|
@@ -1188,16 +1277,16 @@ async function handleStatus(
|
|
|
1188
1277
|
cwd: string,
|
|
1189
1278
|
positionalSkill: string | undefined,
|
|
1190
1279
|
): Promise<StateCommandResult> {
|
|
1191
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1192
|
-
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.
|
|
1280
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "read");
|
|
1281
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
|
|
1193
1282
|
if (!mode) {
|
|
1194
1283
|
throw new StateCommandError(
|
|
1195
1284
|
2,
|
|
1196
|
-
"gjc state status requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
1285
|
+
"gjc state status requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1197
1286
|
);
|
|
1198
1287
|
}
|
|
1199
|
-
const filePath = modeStateFile(cwd, mode, selectors.
|
|
1200
|
-
const existing = await readWorkflowStateJson(cwd, mode, selectors.
|
|
1288
|
+
const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
|
|
1289
|
+
const existing = await readWorkflowStateJson(cwd, mode, selectors.gjcSessionId);
|
|
1201
1290
|
const summary = buildStateStatusSummary(
|
|
1202
1291
|
mode,
|
|
1203
1292
|
{ skill: mode, state: existing, storage_path: filePath },
|
|
@@ -1215,14 +1304,14 @@ async function handleWrite(
|
|
|
1215
1304
|
cwd: string,
|
|
1216
1305
|
positionalSkill: string | undefined,
|
|
1217
1306
|
): Promise<StateCommandResult> {
|
|
1218
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1219
|
-
const { sessionId, threadId, turnId, payload } = selectors;
|
|
1307
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "write");
|
|
1308
|
+
const { gjcSessionId: sessionId, threadId, turnId, payload } = selectors;
|
|
1220
1309
|
if (!payload) throw new StateCommandError(2, "gjc state write requires --input '<json>'");
|
|
1221
1310
|
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
|
|
1222
1311
|
if (!mode)
|
|
1223
1312
|
throw new StateCommandError(
|
|
1224
1313
|
2,
|
|
1225
|
-
"gjc state write requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
1314
|
+
"gjc state write requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1226
1315
|
);
|
|
1227
1316
|
|
|
1228
1317
|
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
@@ -1311,6 +1400,7 @@ async function handleWrite(
|
|
|
1311
1400
|
if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
|
|
1312
1401
|
|
|
1313
1402
|
const { warning: outOfBandWarning, stamped } = await writeJsonAtomic(cwd, filePath, merged, "write", {
|
|
1403
|
+
sessionId,
|
|
1314
1404
|
skill: mode,
|
|
1315
1405
|
mutationId,
|
|
1316
1406
|
force: forced,
|
|
@@ -1322,6 +1412,7 @@ async function handleWrite(
|
|
|
1322
1412
|
const phase = typeof merged.current_phase === "string" ? merged.current_phase : undefined;
|
|
1323
1413
|
const active = merged.active !== false;
|
|
1324
1414
|
await syncWorkflowSkillState({ cwd, mode, sessionId, threadId, turnId, active, phase, payload: merged, receipt });
|
|
1415
|
+
await touchStateActivityMarker(cwd, sessionId, filePath);
|
|
1325
1416
|
|
|
1326
1417
|
return {
|
|
1327
1418
|
status: 0,
|
|
@@ -1344,13 +1435,13 @@ async function handleClear(
|
|
|
1344
1435
|
cwd: string,
|
|
1345
1436
|
positionalSkill: string | undefined,
|
|
1346
1437
|
): Promise<StateCommandResult> {
|
|
1347
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1348
|
-
const { sessionId, threadId, turnId } = selectors;
|
|
1438
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "clear");
|
|
1439
|
+
const { gjcSessionId: sessionId, threadId, turnId } = selectors;
|
|
1349
1440
|
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
|
|
1350
1441
|
if (!mode)
|
|
1351
1442
|
throw new StateCommandError(
|
|
1352
1443
|
2,
|
|
1353
|
-
"gjc state clear requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
1444
|
+
"gjc state clear requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1354
1445
|
);
|
|
1355
1446
|
|
|
1356
1447
|
const filePath = modeStateFile(cwd, mode, sessionId);
|
|
@@ -1363,6 +1454,10 @@ async function handleClear(
|
|
|
1363
1454
|
);
|
|
1364
1455
|
}
|
|
1365
1456
|
const existing = existingRead.kind === "valid" ? existingRead.value : {};
|
|
1457
|
+
const staleReason = await describeStaleClearState(cwd, sessionId, mode, existing);
|
|
1458
|
+
if (staleReason && !forced) {
|
|
1459
|
+
throw new StateCommandError(2, `existing state for ${mode} is stale (${staleReason}); use --force to clear`);
|
|
1460
|
+
}
|
|
1366
1461
|
const clearedAt = nowIso();
|
|
1367
1462
|
const cleared: Record<string, unknown> = {
|
|
1368
1463
|
skill: mode,
|
|
@@ -1385,6 +1480,7 @@ async function handleClear(
|
|
|
1385
1480
|
});
|
|
1386
1481
|
cleared.receipt = receipt;
|
|
1387
1482
|
const { warning: outOfBandWarning, stamped } = await writeJsonAtomic(cwd, filePath, cleared, "clear", {
|
|
1483
|
+
sessionId,
|
|
1388
1484
|
skill: mode,
|
|
1389
1485
|
mutationId,
|
|
1390
1486
|
force: forced,
|
|
@@ -1403,6 +1499,7 @@ async function handleClear(
|
|
|
1403
1499
|
phase: "complete",
|
|
1404
1500
|
payload: cleared,
|
|
1405
1501
|
});
|
|
1502
|
+
await touchStateActivityMarker(cwd, sessionId, filePath);
|
|
1406
1503
|
return {
|
|
1407
1504
|
status: 0,
|
|
1408
1505
|
stdout: renderCliWriteReceipt({
|
|
@@ -1443,13 +1540,13 @@ async function handleHandoff(
|
|
|
1443
1540
|
cwd: string,
|
|
1444
1541
|
positionalSkill: string | undefined,
|
|
1445
1542
|
): Promise<StateCommandResult> {
|
|
1446
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1447
|
-
const { sessionId, threadId, turnId } = selectors;
|
|
1543
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "handoff");
|
|
1544
|
+
const { gjcSessionId: sessionId, threadId, turnId } = selectors;
|
|
1448
1545
|
const caller = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
|
|
1449
1546
|
if (!caller) {
|
|
1450
1547
|
throw new StateCommandError(
|
|
1451
1548
|
2,
|
|
1452
|
-
"gjc state handoff requires --mode <caller>, positional <caller>, input.skill, or an active workflow in
|
|
1549
|
+
"gjc state handoff requires --mode <caller>, positional <caller>, input.skill, or an active workflow in the current session active state",
|
|
1453
1550
|
);
|
|
1454
1551
|
}
|
|
1455
1552
|
const calleeRaw = flagValue(args, "--to")?.trim();
|
|
@@ -1552,6 +1649,7 @@ async function handleHandoff(
|
|
|
1552
1649
|
|
|
1553
1650
|
await beginWorkflowTransactionJournal({
|
|
1554
1651
|
cwd,
|
|
1652
|
+
sessionId,
|
|
1555
1653
|
mutationId,
|
|
1556
1654
|
caller,
|
|
1557
1655
|
callee,
|
|
@@ -1567,21 +1665,23 @@ async function handleHandoff(
|
|
|
1567
1665
|
// only; corrupt JSON / IO failures propagate as non-zero CLI status.
|
|
1568
1666
|
const force = hasFlag(args, "--force");
|
|
1569
1667
|
const calleeWrite = await writeJsonAtomic(cwd, calleePath, mergedCalleeState, "handoff", {
|
|
1668
|
+
sessionId,
|
|
1570
1669
|
skill: callee,
|
|
1571
1670
|
mutationId,
|
|
1572
1671
|
force,
|
|
1573
1672
|
fromPhase: typeof existingCallee.current_phase === "string" ? existingCallee.current_phase : undefined,
|
|
1574
1673
|
toPhase: calleeInitial,
|
|
1575
1674
|
});
|
|
1576
|
-
await updateWorkflowTransactionJournal(cwd, mutationId, { steps: ["callee-mode-state"] });
|
|
1675
|
+
await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, { steps: ["callee-mode-state"] });
|
|
1577
1676
|
const callerWrite = await writeJsonAtomic(cwd, callerPath, mergedCallerState, "handoff", {
|
|
1677
|
+
sessionId,
|
|
1578
1678
|
skill: caller,
|
|
1579
1679
|
mutationId,
|
|
1580
1680
|
force,
|
|
1581
1681
|
fromPhase: typeof existingCaller.current_phase === "string" ? existingCaller.current_phase : undefined,
|
|
1582
1682
|
toPhase: "handoff",
|
|
1583
1683
|
});
|
|
1584
|
-
await updateWorkflowTransactionJournal(cwd, mutationId, {
|
|
1684
|
+
await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, {
|
|
1585
1685
|
steps: ["callee-mode-state", "caller-mode-state"],
|
|
1586
1686
|
});
|
|
1587
1687
|
const warnings = [calleeWrite.warning, callerWrite.warning].filter(
|
|
@@ -1626,10 +1726,11 @@ async function handleHandoff(
|
|
|
1626
1726
|
receipt: calleeReceipt,
|
|
1627
1727
|
},
|
|
1628
1728
|
});
|
|
1629
|
-
await updateWorkflowTransactionJournal(cwd, mutationId, {
|
|
1729
|
+
await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, {
|
|
1630
1730
|
steps: ["callee-mode-state", "caller-mode-state", "active-state"],
|
|
1631
1731
|
});
|
|
1632
|
-
await completeWorkflowTransactionJournal(cwd, mutationId);
|
|
1732
|
+
await completeWorkflowTransactionJournal(cwd, sessionId, mutationId);
|
|
1733
|
+
await touchStateActivityMarker(cwd, sessionId, callerPath);
|
|
1633
1734
|
|
|
1634
1735
|
return {
|
|
1635
1736
|
status: 0,
|
|
@@ -1668,7 +1769,7 @@ async function handleContract(
|
|
|
1668
1769
|
cwd: string,
|
|
1669
1770
|
positionalSkill: string | undefined,
|
|
1670
1771
|
): Promise<StateCommandResult> {
|
|
1671
|
-
const { mode } = await resolveSelectors(args, cwd, positionalSkill);
|
|
1772
|
+
const { mode } = await resolveSelectors(args, cwd, positionalSkill, "read");
|
|
1672
1773
|
if (!mode) {
|
|
1673
1774
|
throw new StateCommandError(2, "gjc state contract requires --mode <skill>, positional <skill>, or input.skill");
|
|
1674
1775
|
}
|
|
@@ -1723,11 +1824,7 @@ function categoryForStateRelativePath(relativePath: string): string | undefined
|
|
|
1723
1824
|
if (normalized === "audit.jsonl") return undefined;
|
|
1724
1825
|
if (normalized === SKILL_ACTIVE_STATE_FILE || normalized.endsWith(`/${SKILL_ACTIVE_STATE_FILE}`)) return undefined;
|
|
1725
1826
|
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;
|
|
1827
|
+
if (/^[^/]+-state\.json$/.test(normalized) || false) return undefined;
|
|
1731
1828
|
if (normalized.startsWith("artifacts/") || normalized.includes("/artifacts/")) return "artifact";
|
|
1732
1829
|
if (
|
|
1733
1830
|
normalized.startsWith("logs/") ||
|
|
@@ -1753,9 +1850,10 @@ function categoryForStateRelativePath(relativePath: string): string | undefined
|
|
|
1753
1850
|
|
|
1754
1851
|
async function collectRetentionCandidates(
|
|
1755
1852
|
cwd: string,
|
|
1853
|
+
sessionId: string,
|
|
1756
1854
|
skills: readonly CanonicalGjcWorkflowSkill[],
|
|
1757
1855
|
): Promise<RetentionCandidate[]> {
|
|
1758
|
-
const stateRoot =
|
|
1856
|
+
const stateRoot = sessionStateDir(cwd, sessionId);
|
|
1759
1857
|
const policies = new Map<string, { keep?: number; maxAgeDays?: number }>();
|
|
1760
1858
|
for (const skill of skills) {
|
|
1761
1859
|
for (const policy of getSkillManifest(skill).retention) {
|
|
@@ -1836,7 +1934,11 @@ async function buildGcSummary(
|
|
|
1836
1934
|
flagValue(args, "--skill")?.trim() || flagValue(args, "--mode")?.trim() || positionalSkill?.trim() || "all";
|
|
1837
1935
|
if (rawSkill !== "all") assertKnownMode(rawSkill);
|
|
1838
1936
|
const skills = rawSkill === "all" ? CANONICAL_GJC_WORKFLOW_SKILLS : [rawSkill as CanonicalGjcWorkflowSkill];
|
|
1839
|
-
const
|
|
1937
|
+
const session = await resolveGjcSessionForRead(cwd, {
|
|
1938
|
+
flagValue: flagValue(args, "--session-id"),
|
|
1939
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
1940
|
+
});
|
|
1941
|
+
const eligible = selectRetentionEligible(await collectRetentionCandidates(cwd, session.gjcSessionId, skills));
|
|
1840
1942
|
const counts: Record<string, number> = {};
|
|
1841
1943
|
for (const candidate of eligible) counts[candidate.category] = (counts[candidate.category] ?? 0) + 1;
|
|
1842
1944
|
const targets: GenericHardPruneTarget[] = eligible.map(candidate => ({
|
|
@@ -1850,6 +1952,7 @@ async function buildGcSummary(
|
|
|
1850
1952
|
cwd,
|
|
1851
1953
|
audit: {
|
|
1852
1954
|
cwd,
|
|
1955
|
+
sessionId: session.gjcSessionId,
|
|
1853
1956
|
skill: rawSkill,
|
|
1854
1957
|
category: "prune",
|
|
1855
1958
|
verb: "gc",
|
|
@@ -1861,7 +1964,7 @@ async function buildGcSummary(
|
|
|
1861
1964
|
skill: rawSkill as CanonicalGjcWorkflowSkill | "all",
|
|
1862
1965
|
dry_run: dryRun,
|
|
1863
1966
|
eligible: eligible.map(candidate => candidate.relativePath),
|
|
1864
|
-
pruned: pruned.map(filePath => path.relative(
|
|
1967
|
+
pruned: pruned.map(filePath => path.relative(sessionStateDir(cwd, session.gjcSessionId), filePath)),
|
|
1865
1968
|
counts,
|
|
1866
1969
|
};
|
|
1867
1970
|
}
|
|
@@ -1872,7 +1975,11 @@ async function handleGraph(
|
|
|
1872
1975
|
positionalSkill: string | undefined,
|
|
1873
1976
|
): Promise<StateCommandResult> {
|
|
1874
1977
|
if (hasFlag(args, "--history")) {
|
|
1875
|
-
const
|
|
1978
|
+
const session = await resolveGjcSessionForRead(_cwd, {
|
|
1979
|
+
flagValue: flagValue(args, "--session-id"),
|
|
1980
|
+
envSessionId: process.env.GJC_SESSION_ID,
|
|
1981
|
+
});
|
|
1982
|
+
const history = await readAuditWindow(_cwd, args, session.gjcSessionId);
|
|
1876
1983
|
return {
|
|
1877
1984
|
status: 0,
|
|
1878
1985
|
stdout: hasFlag(args, "--json") ? `${JSON.stringify(history, null, 2)}\n` : renderHistoryMarkdown(history),
|
|
@@ -1895,20 +2002,21 @@ async function handlePrune(
|
|
|
1895
2002
|
cwd: string,
|
|
1896
2003
|
positionalSkill: string | undefined,
|
|
1897
2004
|
): Promise<StateCommandResult> {
|
|
1898
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1899
|
-
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.
|
|
2005
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "prune");
|
|
2006
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
|
|
1900
2007
|
if (!mode) {
|
|
1901
2008
|
throw new StateCommandError(
|
|
1902
2009
|
2,
|
|
1903
|
-
"gjc state prune requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
2010
|
+
"gjc state prune requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1904
2011
|
);
|
|
1905
2012
|
}
|
|
1906
|
-
const filePath = modeStateFile(cwd, mode, selectors.
|
|
2013
|
+
const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
|
|
1907
2014
|
const olderThanDays = parseNonNegativeIntegerFlag(args, "--older-than");
|
|
1908
2015
|
const status = flagValue(args, "--status")?.trim();
|
|
1909
2016
|
const targets: GenericHardPruneTarget[] = [{ path: filePath, category: "prune" }];
|
|
1910
2017
|
const audit: StateWriterAuditContext = {
|
|
1911
2018
|
cwd,
|
|
2019
|
+
sessionId: selectors.gjcSessionId,
|
|
1912
2020
|
skill: mode,
|
|
1913
2021
|
category: "prune",
|
|
1914
2022
|
verb: hasFlag(args, "--hard") ? "hard-prune" : "soft-delete",
|
|
@@ -1964,17 +2072,17 @@ async function handleMigrate(
|
|
|
1964
2072
|
cwd: string,
|
|
1965
2073
|
positionalSkill: string | undefined,
|
|
1966
2074
|
): Promise<StateCommandResult> {
|
|
1967
|
-
const selectors = await resolveSelectors(args, cwd, positionalSkill);
|
|
1968
|
-
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.
|
|
2075
|
+
const selectors = await resolveSelectors(args, cwd, positionalSkill, "migrate");
|
|
2076
|
+
const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
|
|
1969
2077
|
if (!mode) {
|
|
1970
2078
|
throw new StateCommandError(
|
|
1971
2079
|
2,
|
|
1972
|
-
"gjc state migrate requires --mode <skill>, positional <skill>, input.skill, or an active workflow in
|
|
2080
|
+
"gjc state migrate requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
|
|
1973
2081
|
);
|
|
1974
2082
|
}
|
|
1975
|
-
const filePath = modeStateFile(cwd, mode, selectors.
|
|
2083
|
+
const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
|
|
1976
2084
|
const forced = hasFlag(args, "--force");
|
|
1977
|
-
const mismatchWarning = await warnAndAuditOutOfBandIfNeeded(cwd, filePath, mode, {
|
|
2085
|
+
const mismatchWarning = await warnAndAuditOutOfBandIfNeeded(cwd, selectors.gjcSessionId, filePath, mode, {
|
|
1978
2086
|
forced,
|
|
1979
2087
|
});
|
|
1980
2088
|
if (mismatchWarning && !forced) {
|
|
@@ -1984,7 +2092,7 @@ async function handleMigrate(
|
|
|
1984
2092
|
cwd,
|
|
1985
2093
|
skill: mode,
|
|
1986
2094
|
statePath: filePath,
|
|
1987
|
-
sessionId: selectors.
|
|
2095
|
+
sessionId: selectors.gjcSessionId,
|
|
1988
2096
|
});
|
|
1989
2097
|
return {
|
|
1990
2098
|
status: 0,
|
|
@@ -2026,6 +2134,7 @@ export async function runNativeStateCommand(args: string[], cwd = process.cwd())
|
|
|
2026
2134
|
}
|
|
2027
2135
|
} catch (error) {
|
|
2028
2136
|
if (error instanceof StateCommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
|
|
2137
|
+
if (error instanceof SessionResolutionError) return { status: 2, stderr: `${error.message}\n` };
|
|
2029
2138
|
return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
|
|
2030
2139
|
}
|
|
2031
2140
|
}
|