@gajae-code/coding-agent 0.2.5 → 0.3.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.
Files changed (112) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/types/async/job-manager.d.ts +84 -2
  3. package/dist/types/commands/harness.d.ts +37 -0
  4. package/dist/types/config/settings-schema.d.ts +6 -0
  5. package/dist/types/config/settings.d.ts +2 -0
  6. package/dist/types/deep-interview/render-middleware.d.ts +5 -0
  7. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  9. package/dist/types/extensibility/shared-events.d.ts +1 -0
  10. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  11. package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
  12. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  13. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  14. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
  16. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  17. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  18. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  19. package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
  20. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  21. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  22. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  23. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  24. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  25. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  26. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  27. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  28. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  29. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  30. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  31. package/dist/types/harness-control-plane/types.d.ts +162 -0
  32. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  33. package/dist/types/hooks/skill-state.d.ts +2 -29
  34. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  35. package/dist/types/modes/interactive-mode.d.ts +1 -0
  36. package/dist/types/modes/types.d.ts +1 -0
  37. package/dist/types/sdk.d.ts +2 -0
  38. package/dist/types/session/agent-session.d.ts +8 -0
  39. package/dist/types/skill-state/active-state.d.ts +2 -0
  40. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  41. package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
  42. package/dist/types/task/executor.d.ts +3 -0
  43. package/dist/types/task/types.d.ts +55 -3
  44. package/dist/types/tools/subagent.d.ts +11 -1
  45. package/package.json +7 -7
  46. package/src/async/job-manager.ts +298 -6
  47. package/src/cli/auth-broker-cli.ts +1 -0
  48. package/src/cli/config-cli.ts +10 -2
  49. package/src/cli.ts +2 -0
  50. package/src/commands/harness.ts +592 -0
  51. package/src/commands/team.ts +36 -39
  52. package/src/config/settings-schema.ts +7 -0
  53. package/src/config/settings.ts +5 -0
  54. package/src/deep-interview/render-middleware.ts +366 -0
  55. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  56. package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
  57. package/src/extensibility/custom-tools/types.ts +1 -0
  58. package/src/extensibility/extensions/types.ts +6 -0
  59. package/src/extensibility/shared-events.ts +1 -0
  60. package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
  61. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  62. package/src/gjc-runtime/ralplan-runtime.ts +25 -10
  63. package/src/gjc-runtime/state-graph.ts +86 -0
  64. package/src/gjc-runtime/state-migrations.ts +132 -0
  65. package/src/gjc-runtime/state-renderer.ts +345 -0
  66. package/src/gjc-runtime/state-runtime.ts +733 -21
  67. package/src/gjc-runtime/state-validation.ts +49 -0
  68. package/src/gjc-runtime/state-writer.ts +718 -0
  69. package/src/gjc-runtime/team-runtime.ts +1083 -89
  70. package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
  71. package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
  72. package/src/gjc-runtime/workflow-manifest.ts +425 -0
  73. package/src/harness-control-plane/classifier.ts +128 -0
  74. package/src/harness-control-plane/control-endpoint.ts +137 -0
  75. package/src/harness-control-plane/finalize.ts +222 -0
  76. package/src/harness-control-plane/frame-mapper.ts +286 -0
  77. package/src/harness-control-plane/operate.ts +225 -0
  78. package/src/harness-control-plane/owner.ts +553 -0
  79. package/src/harness-control-plane/preserve.ts +102 -0
  80. package/src/harness-control-plane/receipts.ts +216 -0
  81. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  82. package/src/harness-control-plane/seams.ts +39 -0
  83. package/src/harness-control-plane/session-lease.ts +388 -0
  84. package/src/harness-control-plane/state-machine.ts +97 -0
  85. package/src/harness-control-plane/storage.ts +257 -0
  86. package/src/harness-control-plane/types.ts +214 -0
  87. package/src/hooks/skill-keywords.ts +4 -2
  88. package/src/hooks/skill-state.ts +24 -41
  89. package/src/internal-urls/docs-index.generated.ts +1 -1
  90. package/src/modes/components/assistant-message.ts +5 -1
  91. package/src/modes/components/hook-selector.ts +72 -2
  92. package/src/modes/controllers/event-controller.ts +71 -6
  93. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  94. package/src/modes/controllers/input-controller.ts +9 -1
  95. package/src/modes/controllers/selector-controller.ts +2 -1
  96. package/src/modes/interactive-mode.ts +1 -0
  97. package/src/modes/types.ts +1 -0
  98. package/src/prompts/agents/executor.md +13 -0
  99. package/src/prompts/tools/subagent.md +33 -3
  100. package/src/sdk.ts +4 -0
  101. package/src/session/agent-session.ts +231 -33
  102. package/src/session/session-manager.ts +13 -1
  103. package/src/skill-state/active-state.ts +58 -65
  104. package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
  105. package/src/skill-state/initial-phase.ts +2 -0
  106. package/src/skill-state/workflow-state-contract.ts +26 -0
  107. package/src/task/executor.ts +50 -8
  108. package/src/task/index.ts +120 -8
  109. package/src/task/render.ts +6 -3
  110. package/src/task/types.ts +56 -3
  111. package/src/tools/ask.ts +28 -7
  112. package/src/tools/subagent.ts +255 -64
@@ -1,4 +1,3 @@
1
- import { randomBytes } from "node:crypto";
2
1
  import * as fs from "node:fs/promises";
3
2
  import * as path from "node:path";
4
3
  import type { WorkflowHudSummary } from "../skill-state/active-state";
@@ -18,11 +17,39 @@ import {
18
17
  buildUltragoalHudSummary,
19
18
  } from "../skill-state/workflow-hud";
20
19
  import {
20
+ type AuditEntry,
21
21
  buildWorkflowStateReceipt,
22
22
  canonicalWorkflowSkill,
23
23
  describeWorkflowStateContract,
24
24
  type WorkflowStateReceipt,
25
25
  } from "../skill-state/workflow-state-contract";
26
+ import { renderStateGraph, type StateGraphFormat } from "./state-graph";
27
+ import { migrateAndPersistLegacyState } from "./state-migrations";
28
+ import {
29
+ buildStateStatusSummary,
30
+ compactProjectStateJson,
31
+ projectStateFields,
32
+ renderContractMarkdown,
33
+ renderHistoryMarkdown,
34
+ renderStateMarkdown,
35
+ renderStateStatusLine,
36
+ STATE_FIELD_ALLOWLIST,
37
+ type StateProjectionField,
38
+ } from "./state-renderer";
39
+ import { validateWorkflowStateEnvelope } from "./state-validation";
40
+ import {
41
+ appendAuditEntry,
42
+ beginWorkflowTransactionJournal,
43
+ completeWorkflowTransactionJournal,
44
+ detectWorkflowEnvelopeIntegrityMismatch,
45
+ type GenericHardPruneTarget,
46
+ hardPrune,
47
+ type StateWriterAuditContext,
48
+ softDelete,
49
+ updateWorkflowTransactionJournal,
50
+ writeWorkflowEnvelopeAtomic,
51
+ } from "./state-writer";
52
+ import { getSkillManifest, isKnownWorkflowState, isValidTransition, typedArgsFor } from "./workflow-manifest";
26
53
 
27
54
  /**
28
55
  * Native implementation of the `gjc state read|write|clear` command surface.
@@ -62,11 +89,92 @@ function hasFlag(args: readonly string[], flag: string): boolean {
62
89
  return args.includes(flag);
63
90
  }
64
91
 
65
- const FLAGS_WITH_VALUES = new Set(["--input", "--mode", "--session-id", "--thread-id", "--turn-id", "--to"]);
66
- const ACTION_NAMES = new Set(["read", "write", "clear", "contract", "handoff"]);
92
+ const GRAPH_FORMATS = new Set(["ascii", "mermaid", "dot"]);
93
+ const FLAGS_WITH_VALUES = new Set([
94
+ "--input",
95
+ "--mode",
96
+ "--session-id",
97
+ "--thread-id",
98
+ "--turn-id",
99
+ "--to",
100
+ "--skill",
101
+ "--format",
102
+ "--older-than",
103
+ "--status",
104
+ "--fields",
105
+ "--since",
106
+ "--limit",
107
+ ]);
108
+ const ACTION_NAMES = new Set([
109
+ "read",
110
+ "write",
111
+ "clear",
112
+ "contract",
113
+ "handoff",
114
+ "graph",
115
+ "prune",
116
+ "gc",
117
+ "migrate",
118
+ "status",
119
+ ]);
120
+ const BOOLEAN_FLAGS = new Set([
121
+ "--json",
122
+ "--replace",
123
+ "--hard",
124
+ "--dry-run",
125
+ "--migrate",
126
+ "--compact",
127
+ "--history",
128
+ "--force",
129
+ ]);
130
+ const VERB_SPECIFIC_FLAGS = new Set([
131
+ "--skill",
132
+ "--format",
133
+ "--older-than",
134
+ "--status",
135
+ "--fields",
136
+ "--since",
137
+ "--limit",
138
+ "--history",
139
+ ]);
140
+
141
+ function flagName(arg: string): string | undefined {
142
+ if (!arg.startsWith("--")) return undefined;
143
+ const equalsIndex = arg.indexOf("=");
144
+ return equalsIndex >= 0 ? arg.slice(0, equalsIndex) : arg;
145
+ }
146
+
147
+ function manifestFlagNames(action: ParsedInvocation["action"], positionalSkill: string | undefined): Set<string> {
148
+ const names = new Set<string>();
149
+ const skills =
150
+ positionalSkill && KNOWN_MODES.includes(positionalSkill)
151
+ ? [positionalSkill as CanonicalGjcWorkflowSkill]
152
+ : CANONICAL_GJC_WORKFLOW_SKILLS;
153
+ for (const skill of skills) {
154
+ for (const arg of typedArgsFor(skill, action)) names.add(`--${arg.name}`);
155
+ }
156
+ return names;
157
+ }
158
+
159
+ function assertKnownFlags(args: readonly string[], parsed: ParsedInvocation): void {
160
+ const manifestFlags = manifestFlagNames(parsed.action, parsed.positionalSkill);
161
+ for (const arg of args) {
162
+ const flag = flagName(arg);
163
+ if (!flag) continue;
164
+ if (
165
+ FLAGS_WITH_VALUES.has(flag) ||
166
+ BOOLEAN_FLAGS.has(flag) ||
167
+ VERB_SPECIFIC_FLAGS.has(flag) ||
168
+ manifestFlags.has(flag)
169
+ ) {
170
+ continue;
171
+ }
172
+ throw new StateCommandError(2, `unknown gjc state flag: ${flag}`);
173
+ }
174
+ }
67
175
 
68
176
  interface ParsedInvocation {
69
- action: "read" | "write" | "clear" | "contract" | "handoff";
177
+ action: "read" | "write" | "clear" | "contract" | "handoff" | "graph" | "prune" | "gc" | "migrate" | "status";
70
178
  positionalSkill?: string;
71
179
  }
72
180
 
@@ -90,7 +198,7 @@ function parsePositionalArgs(args: readonly string[]): ParsedInvocation {
90
198
  const first = positional[0];
91
199
  const second = positional[1];
92
200
  if (first && ACTION_NAMES.has(first)) {
93
- return { action: first as ParsedInvocation["action"] };
201
+ return { action: first as ParsedInvocation["action"], positionalSkill: second };
94
202
  }
95
203
  if (first && second && ACTION_NAMES.has(second)) {
96
204
  return { action: second as ParsedInvocation["action"], positionalSkill: first };
@@ -240,11 +348,152 @@ async function readJsonFile(filePath: string): Promise<Record<string, unknown> |
240
348
  }
241
349
  }
242
350
 
243
- async function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {
244
- await fs.mkdir(path.dirname(filePath), { recursive: true });
245
- const tmp = `${filePath}.tmp-${randomBytes(6).toString("hex")}`;
246
- await fs.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`);
247
- await fs.rename(tmp, filePath);
351
+ async function readJsonValue(filePath: string): Promise<unknown | null> {
352
+ try {
353
+ return JSON.parse(await fs.readFile(filePath, "utf-8"));
354
+ } catch (error) {
355
+ const err = error as NodeJS.ErrnoException;
356
+ if (err.code === "ENOENT") return null;
357
+ throw new StateCommandError(1, `failed to read ${filePath}: ${err.message}`);
358
+ }
359
+ }
360
+
361
+ async function warnAndAuditOutOfBandIfNeeded(
362
+ cwd: string,
363
+ filePath: string,
364
+ skill: CanonicalGjcWorkflowSkill,
365
+ options?: { mutationId?: string; forced?: boolean },
366
+ ): Promise<string | undefined> {
367
+ const mismatch = await detectWorkflowEnvelopeIntegrityMismatch(filePath);
368
+ if (!mismatch) return undefined;
369
+ const message = `WARNING: workflow mode-state out-of-band edit detected for ${skill}: ${filePath} expected sha256 ${mismatch.expected} but found ${mismatch.actual}`;
370
+ await appendAuditEntry(cwd, {
371
+ ts: new Date().toISOString(),
372
+ skill,
373
+ category: "state",
374
+ verb: "out_of_band_detected",
375
+ owner: "gjc-state-cli",
376
+ mutation_id: options?.mutationId ?? `${skill}:out-of-band:${new Date().toISOString()}`,
377
+ forced: options?.forced ?? false,
378
+ paths: [filePath],
379
+ expected_sha256: mismatch.expected,
380
+ actual_sha256: mismatch.actual,
381
+ } as AuditEntry);
382
+ return message;
383
+ }
384
+
385
+ async function writeJsonAtomic(
386
+ cwd: string,
387
+ filePath: string,
388
+ value: unknown,
389
+ verb: "write" | "clear" | "handoff" = "write",
390
+ options?: {
391
+ skill?: CanonicalGjcWorkflowSkill;
392
+ mutationId?: string;
393
+ force?: boolean;
394
+ fromPhase?: string;
395
+ toPhase?: string;
396
+ },
397
+ ): Promise<string | undefined> {
398
+ const warning = options?.skill
399
+ ? await warnAndAuditOutOfBandIfNeeded(cwd, filePath, options.skill, {
400
+ mutationId: options.mutationId,
401
+ forced: options.force ?? false,
402
+ })
403
+ : undefined;
404
+ if (warning && !options?.force) {
405
+ throw new StateCommandError(2, `${warning}; use --force to overwrite tampered mode-state`);
406
+ }
407
+ await writeWorkflowEnvelopeAtomic(filePath, value, {
408
+ cwd,
409
+ audit: {
410
+ category: "state",
411
+ verb,
412
+ owner: "gjc-state-cli",
413
+ skill: options?.skill,
414
+ mutationId: options?.mutationId,
415
+ fromPhase: options?.fromPhase,
416
+ toPhase: options?.toPhase,
417
+ forced: options?.force ?? false,
418
+ },
419
+ });
420
+ return warning;
421
+ }
422
+
423
+ function parseFieldsFlag(args: readonly string[]): StateProjectionField[] | undefined {
424
+ const raw = flagValue(args, "--fields");
425
+ if (raw === undefined) return undefined;
426
+ const allowed = new Set<string>(STATE_FIELD_ALLOWLIST);
427
+ const fields = raw
428
+ .split(",")
429
+ .map(field => field.trim())
430
+ .filter(Boolean);
431
+ const unknown = fields.filter(field => !allowed.has(field));
432
+ if (unknown.length) {
433
+ throw new StateCommandError(
434
+ 2,
435
+ `unknown --fields value(s): ${unknown.join(", ")}. Allowed fields: ${STATE_FIELD_ALLOWLIST.join(", ")}`,
436
+ );
437
+ }
438
+ return fields as StateProjectionField[];
439
+ }
440
+
441
+ function parseLimitFlag(args: readonly string[], defaultLimit = 50): number {
442
+ const raw = flagValue(args, "--limit");
443
+ if (raw === undefined) return defaultLimit;
444
+ const parsed = Number(raw);
445
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 500) {
446
+ throw new StateCommandError(2, "gjc state --limit requires an integer from 1 to 500");
447
+ }
448
+ return parsed;
449
+ }
450
+
451
+ function parseSinceFlag(args: readonly string[]): string | undefined {
452
+ const raw = flagValue(args, "--since")?.trim();
453
+ if (!raw) return undefined;
454
+ const duration = raw.match(/^(\d+)(m|h|d)$/);
455
+ if (duration) {
456
+ const amount = Number(duration[1]);
457
+ const unit = duration[2];
458
+ const multiplier = unit === "m" ? 60_000 : unit === "h" ? 3_600_000 : 86_400_000;
459
+ return new Date(Date.now() - amount * multiplier).toISOString();
460
+ }
461
+ if (Number.isNaN(Date.parse(raw)))
462
+ throw new StateCommandError(2, "gjc state --since requires an ISO timestamp or duration like 30m, 6h, 7d");
463
+ return new Date(raw).toISOString();
464
+ }
465
+
466
+ async function readAuditWindow(
467
+ cwd: string,
468
+ args: readonly string[],
469
+ ): Promise<{ entries: unknown[]; limit: number; since?: string; truncated: boolean }> {
470
+ const limit = parseLimitFlag(args);
471
+ const since = parseSinceFlag(args);
472
+ const auditPath = path.join(cwd, ".gjc", "state", "audit.jsonl");
473
+ let raw = "";
474
+ try {
475
+ raw = await fs.readFile(auditPath, "utf-8");
476
+ } catch (error) {
477
+ const err = error as NodeJS.ErrnoException;
478
+ if (err.code !== "ENOENT") throw error;
479
+ }
480
+ const selected: unknown[] = [];
481
+ let matched = 0;
482
+ const lines = raw.split(/\r?\n/).filter(line => line.trim().length > 0);
483
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
484
+ const line = lines[index];
485
+ let entry: unknown;
486
+ try {
487
+ entry = JSON.parse(line);
488
+ } catch {
489
+ continue;
490
+ }
491
+ if (since && isPlainObject(entry) && typeof entry.ts === "string" && Date.parse(entry.ts) < Date.parse(since))
492
+ break;
493
+ matched += 1;
494
+ if (selected.length < limit) selected.push(entry);
495
+ }
496
+ return { entries: selected.reverse(), limit, ...(since ? { since } : {}), truncated: matched > limit };
248
497
  }
249
498
 
250
499
  function isPlainObject(value: unknown): value is Record<string, unknown> {
@@ -408,6 +657,14 @@ async function syncWorkflowSkillState(options: {
408
657
  // HUD sync is best-effort and must not change command semantics.
409
658
  }
410
659
  }
660
+ export async function readWorkflowStateJson(
661
+ cwd: string,
662
+ skill: CanonicalGjcWorkflowSkill,
663
+ sessionId?: string,
664
+ ): Promise<Record<string, unknown>> {
665
+ return (await readJsonFile(modeStateFile(cwd, skill, sessionId))) ?? {};
666
+ }
667
+
411
668
  async function handleRead(
412
669
  args: readonly string[],
413
670
  cwd: string,
@@ -415,19 +672,70 @@ async function handleRead(
415
672
  ): Promise<StateCommandResult> {
416
673
  const selectors = await resolveSelectors(args, cwd, positionalSkill);
417
674
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.sessionId));
675
+ const fields = parseFieldsFlag(args);
418
676
  if (mode) {
419
677
  const filePath = modeStateFile(cwd, mode, selectors.sessionId);
420
- const existing = await readJsonFile(filePath);
678
+ const existing = await readWorkflowStateJson(cwd, mode, selectors.sessionId);
679
+ const envelope = { skill: mode, state: existing, storage_path: filePath };
680
+ const manifest = getSkillManifest(mode);
681
+ if (fields) {
682
+ const projected = projectStateFields(mode, envelope, manifest, fields);
683
+ return {
684
+ status: 0,
685
+ stdout: hasFlag(args, "--json")
686
+ ? `${JSON.stringify(projected, null, 2)}\n`
687
+ : renderStateMarkdown(mode, projected, manifest),
688
+ };
689
+ }
690
+ if (hasFlag(args, "--compact")) {
691
+ const compact = compactProjectStateJson(mode, envelope, manifest);
692
+ return {
693
+ status: 0,
694
+ stdout: hasFlag(args, "--json")
695
+ ? `${JSON.stringify(compact, null, 2)}\n`
696
+ : renderStateMarkdown(mode, envelope, manifest),
697
+ };
698
+ }
421
699
  return {
422
700
  status: 0,
423
- stdout: `${JSON.stringify({ skill: mode, state: existing, storage_path: filePath }, null, 2)}\n`,
701
+ stdout: hasFlag(args, "--json")
702
+ ? `${JSON.stringify(envelope, null, 2)}\n`
703
+ : renderStateMarkdown(mode, envelope, manifest),
424
704
  };
425
705
  }
426
706
  const filePath = activeStateFile(cwd, selectors.sessionId);
427
- const existing = await readJsonFile(filePath);
707
+ const existingRaw = await readJsonValue(filePath);
708
+ const existing = isPlainObject(existingRaw) ? existingRaw : null;
428
709
  return { status: 0, stdout: `${JSON.stringify(existing ?? {}, null, 2)}\n` };
429
710
  }
430
711
 
712
+ async function handleStatus(
713
+ args: readonly string[],
714
+ cwd: string,
715
+ positionalSkill: string | undefined,
716
+ ): Promise<StateCommandResult> {
717
+ const selectors = await resolveSelectors(args, cwd, positionalSkill);
718
+ const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.sessionId));
719
+ if (!mode) {
720
+ throw new StateCommandError(
721
+ 2,
722
+ "gjc state status requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .gjc/state/skill-active-state.json",
723
+ );
724
+ }
725
+ const filePath = modeStateFile(cwd, mode, selectors.sessionId);
726
+ const existing = await readWorkflowStateJson(cwd, mode, selectors.sessionId);
727
+ const summary = buildStateStatusSummary(
728
+ mode,
729
+ { skill: mode, state: existing, storage_path: filePath },
730
+ getSkillManifest(mode),
731
+ filePath,
732
+ );
733
+ return {
734
+ status: 0,
735
+ stdout: hasFlag(args, "--json") ? `${JSON.stringify(summary, null, 2)}\n` : renderStateStatusLine(summary),
736
+ };
737
+ }
738
+
431
739
  async function handleWrite(
432
740
  args: readonly string[],
433
741
  cwd: string,
@@ -444,8 +752,10 @@ async function handleWrite(
444
752
  );
445
753
 
446
754
  const filePath = modeStateFile(cwd, mode, sessionId);
447
- const existing = await readJsonFile(filePath);
755
+ const existingRaw = await readJsonValue(filePath);
756
+ const existing = isPlainObject(existingRaw) ? existingRaw : null;
448
757
  const nowIsoStr = nowIso();
758
+ const mutationId = `${mode}:${nowIsoStr}`;
449
759
  const receipt = buildWorkflowStateReceipt({
450
760
  cwd,
451
761
  skill: mode,
@@ -453,7 +763,11 @@ async function handleWrite(
453
763
  command: `gjc state ${mode} write`,
454
764
  sessionId,
455
765
  nowIso: nowIsoStr,
766
+ mutationId,
456
767
  });
768
+ if (existingRaw !== null && !isPlainObject(existingRaw)) {
769
+ throw new StateCommandError(2, `existing state for ${mode} must be a JSON object before write`);
770
+ }
457
771
  const existingPayload = existing ?? {};
458
772
  const innerState = (payload.state as Record<string, unknown> | undefined) ?? {};
459
773
  const incomingPhase =
@@ -476,6 +790,10 @@ async function handleWrite(
476
790
  delete merged.state;
477
791
  }
478
792
  }
793
+ const preDefaultValidation = validateWorkflowStateEnvelope(mode, merged);
794
+ if (!preDefaultValidation.valid) {
795
+ throw new StateCommandError(2, preDefaultValidation.error ?? `invalid ${mode} state envelope`);
796
+ }
479
797
  merged.skill = mode;
480
798
  if (incomingPhase) {
481
799
  merged.current_phase = incomingPhase;
@@ -488,7 +806,29 @@ async function handleWrite(
488
806
  merged.updated_at = nowIsoStr;
489
807
  merged.receipt = receipt;
490
808
  if (sessionId && typeof merged.session_id !== "string") merged.session_id = sessionId;
491
- await writeJsonAtomic(filePath, merged);
809
+
810
+ const fromPhase = typeof existingPayload.current_phase === "string" ? existingPayload.current_phase : undefined;
811
+ const toPhase = typeof merged.current_phase === "string" ? merged.current_phase : undefined;
812
+ const forced = hasFlag(args, "--force");
813
+ if (fromPhase && toPhase && isKnownWorkflowState(mode, fromPhase) && isKnownWorkflowState(mode, toPhase)) {
814
+ if (!isValidTransition(mode, fromPhase, toPhase) && !forced) {
815
+ throw new StateCommandError(
816
+ 2,
817
+ `invalid ${mode} phase transition from ${fromPhase} to ${toPhase}; use --force to bypass`,
818
+ );
819
+ }
820
+ }
821
+
822
+ const validation = validateWorkflowStateEnvelope(mode, merged);
823
+ if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
824
+
825
+ const outOfBandWarning = await writeJsonAtomic(cwd, filePath, merged, "write", {
826
+ skill: mode,
827
+ mutationId,
828
+ force: forced,
829
+ fromPhase,
830
+ toPhase,
831
+ });
492
832
 
493
833
  const phase = typeof merged.current_phase === "string" ? merged.current_phase : undefined;
494
834
  const active = merged.active !== false;
@@ -497,6 +837,7 @@ async function handleWrite(
497
837
  return {
498
838
  status: 0,
499
839
  stdout: `${JSON.stringify({ skill: mode, state: merged, receipt }, null, 2)}\n`,
840
+ ...(outOfBandWarning ? { stderr: `${outOfBandWarning}\n` } : {}),
500
841
  };
501
842
  }
502
843
 
@@ -522,7 +863,12 @@ async function handleClear(
522
863
  current_phase: "complete",
523
864
  updated_at: nowIso(),
524
865
  };
525
- await writeJsonAtomic(filePath, cleared);
866
+ const outOfBandWarning = await writeJsonAtomic(cwd, filePath, cleared, "clear", {
867
+ skill: mode,
868
+ force: hasFlag(args, "--force"),
869
+ fromPhase: typeof existing.current_phase === "string" ? existing.current_phase : undefined,
870
+ toPhase: "complete",
871
+ });
526
872
 
527
873
  await syncWorkflowSkillState({
528
874
  cwd,
@@ -534,8 +880,11 @@ async function handleClear(
534
880
  phase: "complete",
535
881
  payload: cleared,
536
882
  });
537
-
538
- return { status: 0, stdout: `${JSON.stringify(cleared, null, 2)}\n` };
883
+ return {
884
+ status: 0,
885
+ stdout: `${JSON.stringify(cleared, null, 2)}\n`,
886
+ ...(outOfBandWarning ? { stderr: `${outOfBandWarning}\n` } : {}),
887
+ };
539
888
  }
540
889
 
541
890
  /**
@@ -593,6 +942,7 @@ async function handleHandoff(
593
942
  const existingCallee = (await readJsonFile(calleePath)) ?? {};
594
943
 
595
944
  const handoffAt = nowIso();
945
+ const mutationId = `${caller}:handoff:${callee}:${handoffAt}`;
596
946
  const callerReceipt = buildWorkflowStateReceipt({
597
947
  cwd,
598
948
  skill: caller,
@@ -600,6 +950,7 @@ async function handleHandoff(
600
950
  command: `gjc state ${caller} handoff --to ${callee}`,
601
951
  sessionId,
602
952
  nowIso: handoffAt,
953
+ mutationId,
603
954
  });
604
955
  const calleeReceipt = buildWorkflowStateReceipt({
605
956
  cwd,
@@ -608,6 +959,7 @@ async function handleHandoff(
608
959
  command: `gjc state ${caller} handoff --to ${callee}`,
609
960
  sessionId,
610
961
  nowIso: handoffAt,
962
+ mutationId,
611
963
  });
612
964
 
613
965
  const calleeInitial = initialPhaseForSkill(callee);
@@ -636,6 +988,14 @@ async function handleHandoff(
636
988
  receipt: callerReceipt,
637
989
  };
638
990
 
991
+ await beginWorkflowTransactionJournal({
992
+ cwd,
993
+ mutationId,
994
+ caller,
995
+ callee,
996
+ paths: [calleePath, callerPath, activeStateFile(cwd, sessionId)],
997
+ });
998
+
639
999
  // Atomic write order (architecture blocker AR-3): mode-state files first,
640
1000
  // then a single atomic active-state mutation per file (session before root)
641
1001
  // via applyHandoffToActiveState. The single-write transaction prevents the
@@ -643,8 +1003,31 @@ async function handleHandoff(
643
1003
  // and write order keeps the session-scoped source of truth ahead of the
644
1004
  // root aggregate. strict:true on the active-state read tolerates ENOENT
645
1005
  // only; corrupt JSON / IO failures propagate as non-zero CLI status.
646
- await writeJsonAtomic(calleePath, mergedCalleeState);
647
- await writeJsonAtomic(callerPath, mergedCallerState);
1006
+ const force = hasFlag(args, "--force");
1007
+ const warnings = [
1008
+ await writeJsonAtomic(cwd, calleePath, mergedCalleeState, "handoff", {
1009
+ skill: callee,
1010
+ mutationId,
1011
+ force,
1012
+ fromPhase: typeof existingCallee.current_phase === "string" ? existingCallee.current_phase : undefined,
1013
+ toPhase: calleeInitial,
1014
+ }),
1015
+ await updateWorkflowTransactionJournal(cwd, mutationId, { steps: ["callee-mode-state"] }).then(() => undefined),
1016
+ await writeJsonAtomic(cwd, callerPath, mergedCallerState, "handoff", {
1017
+ skill: caller,
1018
+ mutationId,
1019
+ force,
1020
+ fromPhase: typeof existingCaller.current_phase === "string" ? existingCaller.current_phase : undefined,
1021
+ toPhase: "handoff",
1022
+ }),
1023
+ await updateWorkflowTransactionJournal(cwd, mutationId, {
1024
+ steps: ["callee-mode-state", "caller-mode-state"],
1025
+ }).then(() => undefined),
1026
+ ].filter((warning): warning is string => typeof warning === "string");
1027
+ for (const warning of warnings) process.stderr.write(`${warning}\n`);
1028
+ if (process.env.GJC_STATE_HANDOFF_FAIL_AFTER_CALLER === mutationId) {
1029
+ throw new StateCommandError(1, `injected handoff failure after caller write for ${mutationId}`);
1030
+ }
648
1031
  await applyHandoffToActiveState({
649
1032
  cwd,
650
1033
  nowIso: handoffAt,
@@ -678,6 +1061,10 @@ async function handleHandoff(
678
1061
  receipt: calleeReceipt,
679
1062
  },
680
1063
  });
1064
+ await updateWorkflowTransactionJournal(cwd, mutationId, {
1065
+ steps: ["callee-mode-state", "caller-mode-state", "active-state"],
1066
+ });
1067
+ await completeWorkflowTransactionJournal(cwd, mutationId);
681
1068
 
682
1069
  return {
683
1070
  status: 0,
@@ -705,14 +1092,329 @@ async function handleContract(
705
1092
  throw new StateCommandError(2, "gjc state contract requires --mode <skill>, positional <skill>, or input.skill");
706
1093
  }
707
1094
  const payload = { skill: mode, contract: describeWorkflowStateContract(mode) };
708
- return { status: 0, stdout: `${JSON.stringify(payload, null, 2)}\n` };
1095
+ return {
1096
+ status: 0,
1097
+ stdout: hasFlag(args, "--json")
1098
+ ? `${JSON.stringify(payload, null, 2)}\n`
1099
+ : renderContractMarkdown(mode, payload.contract),
1100
+ };
1101
+ }
1102
+
1103
+ function parseNonNegativeIntegerFlag(args: readonly string[], flag: string): number | undefined {
1104
+ const value = flagValue(args, flag);
1105
+ if (value === undefined) return undefined;
1106
+ const parsed = Number(value);
1107
+ if (!Number.isInteger(parsed) || parsed < 0) {
1108
+ throw new StateCommandError(2, `gjc state ${flag} requires a non-negative integer value`);
1109
+ }
1110
+ return parsed;
1111
+ }
1112
+
1113
+ function statusFromFile(value: unknown): string | undefined {
1114
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
1115
+ const record = value as Record<string, unknown>;
1116
+ if (typeof record.status === "string") return record.status;
1117
+ if (record.receipt && typeof record.receipt === "object" && !Array.isArray(record.receipt)) {
1118
+ const receiptStatus = (record.receipt as Record<string, unknown>).status;
1119
+ if (typeof receiptStatus === "string") return receiptStatus;
1120
+ }
1121
+ return undefined;
1122
+ }
1123
+
1124
+ interface RetentionCandidate {
1125
+ path: string;
1126
+ relativePath: string;
1127
+ category: string;
1128
+ mtimeMs: number;
1129
+ policy: { keep?: number; maxAgeDays?: number };
1130
+ }
1131
+
1132
+ interface GcSummary {
1133
+ skill: CanonicalGjcWorkflowSkill | "all";
1134
+ dry_run: boolean;
1135
+ eligible: string[];
1136
+ pruned: string[];
1137
+ counts: Record<string, number>;
1138
+ }
1139
+
1140
+ function categoryForStateRelativePath(relativePath: string): string | undefined {
1141
+ const normalized = relativePath.split(path.sep).join("/");
1142
+ if (normalized === "audit.jsonl") return undefined;
1143
+ if (normalized === SKILL_ACTIVE_STATE_FILE || normalized.endsWith(`/${SKILL_ACTIVE_STATE_FILE}`)) return undefined;
1144
+ if (normalized.startsWith("active/") || normalized.includes("/active/")) return undefined;
1145
+ if (
1146
+ /^[^/]+-state\.json$/.test(normalized) ||
1147
+ (normalized.includes("/sessions/") && /\/[^/]+-state\.json$/.test(normalized))
1148
+ )
1149
+ return undefined;
1150
+ if (normalized.startsWith("artifacts/") || normalized.includes("/artifacts/")) return "artifact";
1151
+ if (
1152
+ normalized.startsWith("logs/") ||
1153
+ normalized.includes("/logs/") ||
1154
+ normalized.endsWith(".log") ||
1155
+ normalized.endsWith(".jsonl")
1156
+ )
1157
+ return "log";
1158
+ if (normalized.startsWith("reports/") || normalized.includes("/reports/")) return "report";
1159
+ if (normalized.startsWith("ledgers/") || normalized.includes("/ledgers/")) return "ledger";
1160
+ if (normalized.startsWith("agents/") || normalized.includes("/agents/")) return "agents";
1161
+ if (normalized.startsWith("force/") || normalized.includes("/force/")) return "force";
1162
+ if (
1163
+ normalized.startsWith("prune/") ||
1164
+ normalized.includes("/prune/") ||
1165
+ normalized.startsWith("delete/") ||
1166
+ normalized.includes("/delete/")
1167
+ )
1168
+ return "prune/delete";
1169
+ if (normalized.startsWith("transactions/") || normalized.includes("/transactions/")) return "prune/delete";
1170
+ return undefined;
1171
+ }
1172
+
1173
+ async function collectRetentionCandidates(
1174
+ cwd: string,
1175
+ skills: readonly CanonicalGjcWorkflowSkill[],
1176
+ ): Promise<RetentionCandidate[]> {
1177
+ const stateRoot = path.join(cwd, ".gjc", "state");
1178
+ const policies = new Map<string, { keep?: number; maxAgeDays?: number }>();
1179
+ for (const skill of skills) {
1180
+ for (const policy of getSkillManifest(skill).retention) {
1181
+ const existing = policies.get(policy.category);
1182
+ policies.set(policy.category, {
1183
+ keep: Math.max(existing?.keep ?? 0, policy.keep ?? 0) || undefined,
1184
+ maxAgeDays:
1185
+ existing?.maxAgeDays === undefined
1186
+ ? policy.maxAgeDays
1187
+ : policy.maxAgeDays === undefined
1188
+ ? existing.maxAgeDays
1189
+ : Math.max(existing.maxAgeDays, policy.maxAgeDays),
1190
+ });
1191
+ }
1192
+ }
1193
+ const candidates: RetentionCandidate[] = [];
1194
+ async function visit(dir: string): Promise<void> {
1195
+ let entries: string[];
1196
+ try {
1197
+ entries = await fs.readdir(dir);
1198
+ } catch (error) {
1199
+ const err = error as NodeJS.ErrnoException;
1200
+ if (err.code === "ENOENT") return;
1201
+ throw error;
1202
+ }
1203
+ for (const entry of entries) {
1204
+ const filePath = path.join(dir, entry);
1205
+ const stat = await fs.stat(filePath);
1206
+ if (stat.isDirectory()) {
1207
+ await visit(filePath);
1208
+ continue;
1209
+ }
1210
+ if (!stat.isFile()) continue;
1211
+ const relativePath = path.relative(stateRoot, filePath);
1212
+ const category = categoryForStateRelativePath(relativePath);
1213
+ if (!category) continue;
1214
+ const policy = policies.get(category);
1215
+ if (!policy) continue;
1216
+ candidates.push({ path: filePath, relativePath, category, mtimeMs: stat.mtimeMs, policy });
1217
+ }
1218
+ }
1219
+ await visit(stateRoot);
1220
+ return candidates;
1221
+ }
1222
+
1223
+ function selectRetentionEligible(candidates: readonly RetentionCandidate[]): RetentionCandidate[] {
1224
+ const now = Date.now();
1225
+ const byCategory = new Map<string, RetentionCandidate[]>();
1226
+ for (const candidate of candidates) {
1227
+ const list = byCategory.get(candidate.category) ?? [];
1228
+ list.push(candidate);
1229
+ byCategory.set(candidate.category, list);
1230
+ }
1231
+ const eligible = new Set<RetentionCandidate>();
1232
+ for (const list of byCategory.values()) {
1233
+ list.sort((a, b) => b.mtimeMs - a.mtimeMs || a.relativePath.localeCompare(b.relativePath));
1234
+ for (let index = 0; index < list.length; index += 1) {
1235
+ const candidate = list[index];
1236
+ const keep = candidate.policy.keep ?? 0;
1237
+ if (keep > 0 && index < keep) continue;
1238
+ if (candidate.policy.maxAgeDays !== undefined) {
1239
+ const maxAgeMs = candidate.policy.maxAgeDays * 24 * 60 * 60 * 1000;
1240
+ if (now - candidate.mtimeMs < maxAgeMs) continue;
1241
+ }
1242
+ if (candidate.policy.keep !== undefined || candidate.policy.maxAgeDays !== undefined) eligible.add(candidate);
1243
+ }
1244
+ }
1245
+ return [...eligible].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
1246
+ }
1247
+
1248
+ async function buildGcSummary(
1249
+ args: readonly string[],
1250
+ cwd: string,
1251
+ positionalSkill: string | undefined,
1252
+ dryRun: boolean,
1253
+ ): Promise<GcSummary> {
1254
+ const rawSkill =
1255
+ flagValue(args, "--skill")?.trim() || flagValue(args, "--mode")?.trim() || positionalSkill?.trim() || "all";
1256
+ if (rawSkill !== "all") assertKnownMode(rawSkill);
1257
+ const skills = rawSkill === "all" ? CANONICAL_GJC_WORKFLOW_SKILLS : [rawSkill as CanonicalGjcWorkflowSkill];
1258
+ const eligible = selectRetentionEligible(await collectRetentionCandidates(cwd, skills));
1259
+ const counts: Record<string, number> = {};
1260
+ for (const candidate of eligible) counts[candidate.category] = (counts[candidate.category] ?? 0) + 1;
1261
+ const targets: GenericHardPruneTarget[] = eligible.map(candidate => ({
1262
+ path: candidate.path,
1263
+ category: candidate.category,
1264
+ }));
1265
+ let pruned: string[] = [];
1266
+ if (!dryRun && targets.length > 0) {
1267
+ const eligiblePaths = new Set(eligible.map(candidate => path.resolve(candidate.path)));
1268
+ pruned = await hardPrune(targets, context => eligiblePaths.has(path.resolve(context.path)), {
1269
+ cwd,
1270
+ audit: {
1271
+ cwd,
1272
+ skill: rawSkill,
1273
+ category: "prune",
1274
+ verb: "gc",
1275
+ owner: "gjc-state-cli",
1276
+ },
1277
+ });
1278
+ }
1279
+ return {
1280
+ skill: rawSkill as CanonicalGjcWorkflowSkill | "all",
1281
+ dry_run: dryRun,
1282
+ eligible: eligible.map(candidate => candidate.relativePath),
1283
+ pruned: pruned.map(filePath => path.relative(path.join(cwd, ".gjc", "state"), filePath)),
1284
+ counts,
1285
+ };
1286
+ }
1287
+
1288
+ async function handleGraph(
1289
+ args: readonly string[],
1290
+ _cwd: string,
1291
+ positionalSkill: string | undefined,
1292
+ ): Promise<StateCommandResult> {
1293
+ if (hasFlag(args, "--history")) {
1294
+ const history = await readAuditWindow(_cwd, args);
1295
+ return {
1296
+ status: 0,
1297
+ stdout: hasFlag(args, "--json") ? `${JSON.stringify(history, null, 2)}\n` : renderHistoryMarkdown(history),
1298
+ };
1299
+ }
1300
+ const rawSkill = flagValue(args, "--skill")?.trim() || positionalSkill?.trim() || "all";
1301
+ if (rawSkill !== "all") assertKnownMode(rawSkill);
1302
+ const format = flagValue(args, "--format")?.trim() || "ascii";
1303
+ if (!GRAPH_FORMATS.has(format)) {
1304
+ throw new StateCommandError(2, `Invalid graph format: ${format}. Expected one of: ascii, mermaid, dot.`);
1305
+ }
1306
+ return {
1307
+ status: 0,
1308
+ stdout: renderStateGraph(rawSkill as CanonicalGjcWorkflowSkill | "all", format as StateGraphFormat),
1309
+ };
1310
+ }
1311
+
1312
+ async function handlePrune(
1313
+ args: readonly string[],
1314
+ cwd: string,
1315
+ positionalSkill: string | undefined,
1316
+ ): Promise<StateCommandResult> {
1317
+ const selectors = await resolveSelectors(args, cwd, positionalSkill);
1318
+ const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.sessionId));
1319
+ if (!mode) {
1320
+ throw new StateCommandError(
1321
+ 2,
1322
+ "gjc state prune requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .gjc/state/skill-active-state.json",
1323
+ );
1324
+ }
1325
+ const filePath = modeStateFile(cwd, mode, selectors.sessionId);
1326
+ const olderThanDays = parseNonNegativeIntegerFlag(args, "--older-than");
1327
+ const status = flagValue(args, "--status")?.trim();
1328
+ const targets: GenericHardPruneTarget[] = [{ path: filePath, category: "prune" }];
1329
+ const audit: StateWriterAuditContext = {
1330
+ cwd,
1331
+ skill: mode,
1332
+ category: "prune",
1333
+ verb: hasFlag(args, "--hard") ? "hard-prune" : "soft-delete",
1334
+ owner: "gjc-state-cli",
1335
+ };
1336
+ const olderThanMs = olderThanDays === undefined ? undefined : olderThanDays * 24 * 60 * 60 * 1000;
1337
+ const matchesSelector = async (
1338
+ stat: { mtimeMs: number | bigint },
1339
+ readJson: () => Promise<unknown>,
1340
+ ): Promise<boolean> => {
1341
+ const mtimeMs = typeof stat.mtimeMs === "bigint" ? Number(stat.mtimeMs) : stat.mtimeMs;
1342
+ if (olderThanMs !== undefined && Date.now() - mtimeMs < olderThanMs) return false;
1343
+ if (status) return statusFromFile(await readJson()) === status;
1344
+ return true;
1345
+ };
1346
+ if (hasFlag(args, "--hard")) {
1347
+ const pruned = await hardPrune(
1348
+ targets,
1349
+ context => (context.stat ? matchesSelector(context.stat, context.readJson) : false),
1350
+ { cwd, audit },
1351
+ );
1352
+ return { status: 0, stdout: `${JSON.stringify({ skill: mode, hard: true, pruned }, null, 2)}\n` };
1353
+ }
1354
+ let deleted: string[] = [];
1355
+ try {
1356
+ const stat = await fs.stat(filePath);
1357
+ if (await matchesSelector(stat, async () => JSON.parse(await fs.readFile(filePath, "utf-8")))) {
1358
+ const archivedPath = await softDelete(
1359
+ filePath,
1360
+ { skill: mode, reason: "gjc state prune", status: status ?? null, older_than_days: olderThanDays ?? null },
1361
+ { cwd, audit },
1362
+ );
1363
+ deleted = [archivedPath];
1364
+ }
1365
+ } catch (error) {
1366
+ const err = error as NodeJS.ErrnoException;
1367
+ if (err.code !== "ENOENT") throw error;
1368
+ }
1369
+ return { status: 0, stdout: `${JSON.stringify({ skill: mode, hard: false, soft_deleted: deleted }, null, 2)}\n` };
1370
+ }
1371
+
1372
+ async function handleGc(
1373
+ args: readonly string[],
1374
+ cwd: string,
1375
+ positionalSkill: string | undefined,
1376
+ ): Promise<StateCommandResult> {
1377
+ const summary = await buildGcSummary(args, cwd, positionalSkill, hasFlag(args, "--dry-run"));
1378
+ return { status: 0, stdout: `${JSON.stringify(summary, null, 2)}\n` };
1379
+ }
1380
+
1381
+ async function handleMigrate(
1382
+ args: readonly string[],
1383
+ cwd: string,
1384
+ positionalSkill: string | undefined,
1385
+ ): Promise<StateCommandResult> {
1386
+ const selectors = await resolveSelectors(args, cwd, positionalSkill);
1387
+ const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.sessionId));
1388
+ if (!mode) {
1389
+ throw new StateCommandError(
1390
+ 2,
1391
+ "gjc state migrate requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .gjc/state/skill-active-state.json",
1392
+ );
1393
+ }
1394
+ const filePath = modeStateFile(cwd, mode, selectors.sessionId);
1395
+ const mismatchWarning = await warnAndAuditOutOfBandIfNeeded(cwd, filePath, mode, {
1396
+ forced: hasFlag(args, "--force"),
1397
+ });
1398
+ const result = await migrateAndPersistLegacyState({
1399
+ cwd,
1400
+ skill: mode,
1401
+ statePath: filePath,
1402
+ sessionId: selectors.sessionId,
1403
+ });
1404
+ return {
1405
+ status: 0,
1406
+ stdout: `${JSON.stringify({ skill: mode, ...result, integrity_mismatch: Boolean(mismatchWarning) }, null, 2)}\n`,
1407
+ ...(mismatchWarning ? { stderr: `${mismatchWarning}\n` } : {}),
1408
+ };
709
1409
  }
710
1410
 
711
1411
  export async function runNativeStateCommand(args: string[], cwd = process.cwd()): Promise<StateCommandResult> {
712
1412
  try {
713
1413
  const parsed = parsePositionalArgs(args);
1414
+ assertKnownFlags(args, parsed);
714
1415
  switch (parsed.action) {
715
1416
  case "read":
1417
+ if (hasFlag(args, "--migrate")) return await handleMigrate(args, cwd, parsed.positionalSkill);
716
1418
  return await handleRead(args, cwd, parsed.positionalSkill);
717
1419
  case "write":
718
1420
  return await handleWrite(args, cwd, parsed.positionalSkill);
@@ -720,8 +1422,18 @@ export async function runNativeStateCommand(args: string[], cwd = process.cwd())
720
1422
  return await handleClear(args, cwd, parsed.positionalSkill);
721
1423
  case "contract":
722
1424
  return await handleContract(args, cwd, parsed.positionalSkill);
1425
+ case "status":
1426
+ return await handleStatus(args, cwd, parsed.positionalSkill);
723
1427
  case "handoff":
724
1428
  return await handleHandoff(args, cwd, parsed.positionalSkill);
1429
+ case "graph":
1430
+ return await handleGraph(args, cwd, parsed.positionalSkill);
1431
+ case "prune":
1432
+ return await handlePrune(args, cwd, parsed.positionalSkill);
1433
+ case "gc":
1434
+ return await handleGc(args, cwd, parsed.positionalSkill);
1435
+ case "migrate":
1436
+ return await handleMigrate(args, cwd, parsed.positionalSkill);
725
1437
  default:
726
1438
  return { status: 2, stderr: `Unknown gjc state command: ${parsed.action}\n` };
727
1439
  }