@gajae-code/coding-agent 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/types/async/job-manager.d.ts +3 -1
  3. package/dist/types/cli/daemon-cli.d.ts +25 -0
  4. package/dist/types/cli/migrate-cli.d.ts +20 -0
  5. package/dist/types/cli/notify-cli.d.ts +23 -0
  6. package/dist/types/cli/setup-cli.d.ts +20 -1
  7. package/dist/types/commands/daemon.d.ts +41 -0
  8. package/dist/types/commands/migrate.d.ts +33 -0
  9. package/dist/types/commands/notify.d.ts +41 -0
  10. package/dist/types/config/keybindings.d.ts +4 -0
  11. package/dist/types/config/model-profile-activation.d.ts +12 -0
  12. package/dist/types/config/model-profiles.d.ts +2 -1
  13. package/dist/types/config/model-registry.d.ts +3 -3
  14. package/dist/types/config/models-config-schema.d.ts +5 -0
  15. package/dist/types/config/settings-schema.d.ts +38 -0
  16. package/dist/types/coordinator/contract.d.ts +1 -1
  17. package/dist/types/daemon/builtin.d.ts +20 -0
  18. package/dist/types/daemon/control-types.d.ts +57 -0
  19. package/dist/types/daemon/runtime.d.ts +25 -0
  20. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  21. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
  22. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
  23. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  24. package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
  25. package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
  26. package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
  27. package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
  28. package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
  29. package/dist/types/gjc-runtime/state-writer.d.ts +38 -7
  30. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
  31. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
  32. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
  33. package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
  34. package/dist/types/harness-control-plane/storage.d.ts +2 -1
  35. package/dist/types/hooks/skill-state.d.ts +12 -4
  36. package/dist/types/migrate/action-planner.d.ts +11 -0
  37. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  38. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  39. package/dist/types/migrate/adapters/index.d.ts +45 -0
  40. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  41. package/dist/types/migrate/executor.d.ts +2 -0
  42. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  43. package/dist/types/migrate/report.d.ts +18 -0
  44. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  45. package/dist/types/migrate/types.d.ts +126 -0
  46. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  47. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  48. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  49. package/dist/types/modes/interactive-mode.d.ts +1 -1
  50. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  51. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  52. package/dist/types/modes/types.d.ts +7 -1
  53. package/dist/types/notifications/config-commands.d.ts +26 -0
  54. package/dist/types/notifications/config.d.ts +61 -0
  55. package/dist/types/notifications/helpers.d.ts +55 -0
  56. package/dist/types/notifications/html-format.d.ts +62 -0
  57. package/dist/types/notifications/index.d.ts +28 -0
  58. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  59. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  60. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  61. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  62. package/dist/types/notifications/telegram-daemon.d.ts +276 -0
  63. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  64. package/dist/types/notifications/threaded-inbound.d.ts +58 -0
  65. package/dist/types/notifications/threaded-render.d.ts +66 -0
  66. package/dist/types/notifications/topic-registry.d.ts +67 -0
  67. package/dist/types/research-plan/index.d.ts +1 -0
  68. package/dist/types/research-plan/ledger.d.ts +33 -0
  69. package/dist/types/rlm/artifacts.d.ts +1 -1
  70. package/dist/types/rlm/index.d.ts +12 -0
  71. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  72. package/dist/types/session/agent-session.d.ts +39 -2
  73. package/dist/types/session/auth-storage.d.ts +1 -1
  74. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  75. package/dist/types/setup/credential-import.d.ts +3 -0
  76. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  77. package/dist/types/skill-state/active-state.d.ts +6 -11
  78. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  79. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  80. package/dist/types/task/spawn-gate.d.ts +1 -10
  81. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  82. package/dist/types/tools/index.d.ts +18 -0
  83. package/dist/types/tools/subagent.d.ts +3 -0
  84. package/package.json +7 -7
  85. package/scripts/build-binary.ts +3 -0
  86. package/src/async/job-manager.ts +5 -1
  87. package/src/cli/daemon-cli.ts +122 -0
  88. package/src/cli/migrate-cli.ts +106 -0
  89. package/src/cli/notify-cli.ts +274 -0
  90. package/src/cli/setup-cli.ts +173 -84
  91. package/src/cli.ts +3 -0
  92. package/src/commands/daemon.ts +47 -0
  93. package/src/commands/deep-interview.ts +2 -2
  94. package/src/commands/migrate.ts +46 -0
  95. package/src/commands/notify.ts +61 -0
  96. package/src/commands/setup.ts +11 -1
  97. package/src/commands/state.ts +2 -1
  98. package/src/commands/team.ts +7 -3
  99. package/src/config/model-profile-activation.ts +74 -5
  100. package/src/config/model-profiles.ts +7 -4
  101. package/src/config/model-registry.ts +6 -3
  102. package/src/config/models-config-schema.ts +1 -1
  103. package/src/config/settings-schema.ts +29 -0
  104. package/src/coordinator/contract.ts +3 -0
  105. package/src/coordinator-mcp/policy.ts +10 -2
  106. package/src/coordinator-mcp/server.ts +270 -1
  107. package/src/daemon/builtin.ts +46 -0
  108. package/src/daemon/control-types.ts +65 -0
  109. package/src/daemon/runtime.ts +51 -0
  110. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
  111. package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
  112. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  113. package/src/defaults/gjc/skills/team/SKILL.md +51 -47
  114. package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -13
  115. package/src/extensibility/custom-commands/loader.ts +0 -7
  116. package/src/extensibility/extensions/runner.ts +4 -0
  117. package/src/extensibility/extensions/types.ts +8 -0
  118. package/src/extensibility/gjc-plugins/injection.ts +23 -4
  119. package/src/extensibility/gjc-plugins/state.ts +16 -1
  120. package/src/gjc-runtime/deep-interview-recorder.ts +51 -18
  121. package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
  122. package/src/gjc-runtime/goal-mode-request.ts +26 -11
  123. package/src/gjc-runtime/launch-tmux.ts +6 -1
  124. package/src/gjc-runtime/ralplan-runtime.ts +79 -50
  125. package/src/gjc-runtime/session-layout.ts +180 -0
  126. package/src/gjc-runtime/session-resolution.ts +217 -0
  127. package/src/gjc-runtime/state-graph.ts +1 -2
  128. package/src/gjc-runtime/state-migrations.ts +1 -0
  129. package/src/gjc-runtime/state-runtime.ts +247 -124
  130. package/src/gjc-runtime/state-schema.ts +2 -0
  131. package/src/gjc-runtime/state-writer.ts +289 -41
  132. package/src/gjc-runtime/team-runtime.ts +43 -19
  133. package/src/gjc-runtime/tmux-sessions.ts +7 -1
  134. package/src/gjc-runtime/ultragoal-guard.ts +102 -4
  135. package/src/gjc-runtime/ultragoal-runtime.ts +226 -60
  136. package/src/gjc-runtime/workflow-command-ref.ts +1 -2
  137. package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
  138. package/src/gjc-runtime/workflow-manifest.ts +12 -3
  139. package/src/goals/tools/goal-tool.ts +11 -2
  140. package/src/harness-control-plane/storage.ts +14 -4
  141. package/src/hooks/native-skill-hook.ts +38 -12
  142. package/src/hooks/skill-state.ts +178 -83
  143. package/src/internal-urls/docs-index.generated.ts +9 -6
  144. package/src/main.ts +30 -0
  145. package/src/migrate/action-planner.ts +318 -0
  146. package/src/migrate/adapters/claude-code.ts +39 -0
  147. package/src/migrate/adapters/codex.ts +70 -0
  148. package/src/migrate/adapters/index.ts +277 -0
  149. package/src/migrate/adapters/opencode.ts +52 -0
  150. package/src/migrate/executor.ts +81 -0
  151. package/src/migrate/mcp-mapper.ts +152 -0
  152. package/src/migrate/report.ts +104 -0
  153. package/src/migrate/skill-normalizer.ts +80 -0
  154. package/src/migrate/types.ts +163 -0
  155. package/src/modes/acp/acp-event-mapper.ts +1 -0
  156. package/src/modes/bridge/bridge-mode.ts +2 -2
  157. package/src/modes/components/custom-editor.ts +30 -20
  158. package/src/modes/components/hook-editor.ts +7 -2
  159. package/src/modes/components/oauth-selector.ts +19 -0
  160. package/src/modes/controllers/event-controller.ts +20 -0
  161. package/src/modes/controllers/selector-controller.ts +80 -17
  162. package/src/modes/interactive-mode.ts +6 -2
  163. package/src/modes/rpc/rpc-mode.ts +2 -2
  164. package/src/modes/runtime-init.ts +1 -0
  165. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  166. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  167. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  168. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  169. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  170. package/src/modes/types.ts +7 -1
  171. package/src/modes/utils/ui-helpers.ts +23 -0
  172. package/src/notifications/config-commands.ts +50 -0
  173. package/src/notifications/config.ts +107 -0
  174. package/src/notifications/helpers.ts +135 -0
  175. package/src/notifications/html-format.ts +389 -0
  176. package/src/notifications/index.ts +663 -0
  177. package/src/notifications/rate-limit-pool.ts +179 -0
  178. package/src/notifications/telegram-cli.ts +194 -0
  179. package/src/notifications/telegram-daemon-cli.ts +74 -0
  180. package/src/notifications/telegram-daemon-control.ts +370 -0
  181. package/src/notifications/telegram-daemon.ts +1370 -0
  182. package/src/notifications/telegram-reference.ts +335 -0
  183. package/src/notifications/threaded-inbound.ts +80 -0
  184. package/src/notifications/threaded-render.ts +155 -0
  185. package/src/notifications/topic-registry.ts +133 -0
  186. package/src/prompts/agents/init.md +1 -1
  187. package/src/prompts/system/plan-mode-active.md +1 -1
  188. package/src/prompts/tools/ast-grep.md +1 -1
  189. package/src/prompts/tools/search.md +1 -1
  190. package/src/prompts/tools/task.md +1 -2
  191. package/src/research-plan/index.ts +1 -0
  192. package/src/research-plan/ledger.ts +177 -0
  193. package/src/rlm/artifacts.ts +12 -3
  194. package/src/rlm/index.ts +26 -0
  195. package/src/runtime-mcp/config-writer.ts +46 -0
  196. package/src/sdk.ts +16 -0
  197. package/src/session/agent-session.ts +128 -24
  198. package/src/session/auth-storage.ts +3 -0
  199. package/src/session/session-dump-format.ts +43 -2
  200. package/src/session/session-manager.ts +39 -5
  201. package/src/setup/credential-auto-import.ts +258 -0
  202. package/src/setup/credential-import.ts +17 -0
  203. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  204. package/src/setup/hermes-setup.ts +1 -1
  205. package/src/setup/host-plugin-setup.ts +142 -0
  206. package/src/skill-state/active-state.ts +72 -108
  207. package/src/skill-state/canonical-skills.ts +4 -0
  208. package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
  209. package/src/skill-state/workflow-hud.ts +4 -2
  210. package/src/skill-state/workflow-state-contract.ts +3 -3
  211. package/src/slash-commands/builtin-registry.ts +4 -1
  212. package/src/task/agents.ts +1 -22
  213. package/src/task/executor.ts +5 -1
  214. package/src/task/index.ts +1 -41
  215. package/src/task/spawn-gate.ts +1 -38
  216. package/src/task/types.ts +1 -1
  217. package/src/tools/ask-answer-registry.ts +25 -0
  218. package/src/tools/ask.ts +108 -16
  219. package/src/tools/computer.ts +58 -4
  220. package/src/tools/image-gen.ts +5 -8
  221. package/src/tools/index.ts +19 -0
  222. package/src/tools/inspect-image.ts +16 -11
  223. package/src/tools/subagent-render.ts +7 -0
  224. package/src/tools/subagent.ts +38 -7
  225. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  226. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  227. package/src/prompts/agents/explore.md +0 -58
  228. package/src/prompts/agents/plan.md +0 -49
  229. package/src/prompts/agents/reviewer.md +0 -141
  230. package/src/prompts/agents/task.md +0 -16
  231. 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
- writeWorkflowEnvelopeAtomic,
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 `.gjc/state/[sessions/<id>/]<mode>-state.json` and
64
- * `.gjc/state/[sessions/<id>/]skill-active-state.json`. This is the sanctioned CLI mediator for
65
- * the mutation-guarded `.gjc/state` ACL — agents call it instead of editing those files directly.
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
- sessionId: string | undefined;
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 sessionId = resolveSessionIdFromArgs(args, payload);
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 { mode: mode as CanonicalGjcWorkflowSkill | undefined, sessionId, threadId, turnId, payload };
308
- }
309
-
310
- // Session-id resolution order: explicit --session-id flag, then payload
311
- // session_id, then GJC_SESSION_ID env var (set by AgentSession.sdk for
312
- // agent-initiated CLI invocations). The env-var default keeps shell
313
- // snippets in skill docs short while still routing state commands to the
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 | undefined,
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 encodeSessionSegment(value: string): string {
345
- return encodeURIComponent(value).replaceAll(".", "%2E");
349
+ function stateDirFor(cwd: string, sessionId: string): string {
350
+ return sessionStateDir(cwd, sessionId);
346
351
  }
347
352
 
348
- function stateDirFor(cwd: string, sessionId: string | undefined): string {
349
- const base = path.join(cwd, ".gjc", "state");
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 modeStateFile(cwd: string, mode: string, sessionId: string | undefined): string {
355
- return path.join(stateDirFor(cwd, sessionId), `${mode}-state.json`);
357
+ function activeStateFile(cwd: string, sessionId: string): string {
358
+ return activeSnapshotPath(cwd, sessionId);
356
359
  }
357
360
 
358
- function activeStateFile(cwd: string, sessionId: string | undefined): string {
359
- return path.join(stateDirFor(cwd, sessionId), SKILL_ACTIVE_STATE_FILE);
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 | undefined): 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 | undefined,
546
+ sessionId: string,
511
547
  ): Promise<DoctorSummary> {
512
- const root = path.join(cwd, ".gjc", "state");
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 | undefined): Promise<void> => {
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) ? 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 sessionId = resolveSessionIdFromArgs(args, payload);
731
- const summary = await collectDoctorSummary(cwd, rawSkill as CanonicalGjcWorkflowSkill | undefined, sessionId);
753
+ const session = await resolveGjcSessionForRead(cwd, {
754
+ flagValue: flagValue(args, "--session-id"),
755
+ payloadSessionId: payload?.session_id,
756
+ envSessionId: process.env.GJC_SESSION_ID,
757
+ });
758
+ const summary = await collectDoctorSummary(
759
+ cwd,
760
+ rawSkill as CanonicalGjcWorkflowSkill | undefined,
761
+ session.gjcSessionId,
762
+ );
732
763
  return {
733
764
  status: summary.ok ? 0 : 1,
734
765
  stdout: hasFlag(args, "--json") ? `${JSON.stringify(summary, null, 2)}\n` : renderDoctorText(summary),
@@ -737,6 +768,7 @@ async function handleDoctor(
737
768
 
738
769
  async function warnAndAuditOutOfBandIfNeeded(
739
770
  cwd: string,
771
+ sessionId: string,
740
772
  filePath: string,
741
773
  skill: CanonicalGjcWorkflowSkill,
742
774
  options?: { mutationId?: string; forced?: boolean },
@@ -751,7 +783,7 @@ async function warnAndAuditOutOfBandIfNeeded(
751
783
  }
752
784
  if (!mismatch) return undefined;
753
785
  const message = `WARNING: workflow mode-state out-of-band edit detected for ${skill}: ${filePath} expected sha256 ${mismatch.expected} but found ${mismatch.actual}`;
754
- await appendAuditEntry(cwd, {
786
+ await appendAuditEntry(cwd, sessionId, {
755
787
  ts: new Date().toISOString(),
756
788
  skill,
757
789
  category: "state",
@@ -766,12 +798,19 @@ async function warnAndAuditOutOfBandIfNeeded(
766
798
  return message;
767
799
  }
768
800
 
801
+ function existingStateRevision(value: unknown): number | undefined {
802
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
803
+ const revision = (value as Record<string, unknown>).state_revision;
804
+ return typeof revision === "number" && Number.isFinite(revision) ? revision : 0;
805
+ }
806
+
769
807
  async function writeJsonAtomic(
770
808
  cwd: string,
771
809
  filePath: string,
772
810
  value: unknown,
773
811
  verb: "write" | "clear" | "handoff" | "reconcile" = "write",
774
812
  options?: {
813
+ sessionId: string;
775
814
  skill?: CanonicalGjcWorkflowSkill;
776
815
  mutationId?: string;
777
816
  force?: boolean;
@@ -779,9 +818,9 @@ async function writeJsonAtomic(
779
818
  toPhase?: string;
780
819
  owner?: WorkflowStateMutationOwner;
781
820
  },
782
- ): Promise<{ warning?: string; stamped: Record<string, unknown> }> {
821
+ ): Promise<{ warning?: string; stamped: Record<string, unknown>; revision: number }> {
783
822
  const warning = options?.skill
784
- ? await warnAndAuditOutOfBandIfNeeded(cwd, filePath, options.skill, {
823
+ ? await warnAndAuditOutOfBandIfNeeded(cwd, options.sessionId, filePath, options.skill, {
785
824
  mutationId: options.mutationId,
786
825
  forced: options.force ?? false,
787
826
  })
@@ -789,9 +828,15 @@ async function writeJsonAtomic(
789
828
  if (warning && !options?.force) {
790
829
  throw new StateCommandError(2, `${warning}; use --force to overwrite tampered mode-state`);
791
830
  }
792
- await writeWorkflowEnvelopeAtomic(filePath, value, {
831
+ // Authoritative CLI/runtime write. Stamp the next state_revision under the
832
+ // writer lock; do not enforce an optimistic `expectedRevision` here (tamper
833
+ // detection is handled by warnAndAuditOutOfBandIfNeeded above, and a forced
834
+ // write must succeed over corrupt/missing prior state).
835
+ const writeResult = await writeGuardedWorkflowEnvelopeAtomic(filePath, value, {
793
836
  cwd,
837
+ policy: "source",
794
838
  audit: {
839
+ sessionId: options?.sessionId ?? "",
795
840
  category: "state",
796
841
  verb,
797
842
  owner: options?.owner ?? "gjc-state-cli",
@@ -802,7 +847,11 @@ async function writeJsonAtomic(
802
847
  forced: options?.force ?? false,
803
848
  },
804
849
  });
805
- return { warning, stamped: (await readJsonFile(filePath)) ?? {} };
850
+ // `writeResult.revision` is computed inside the writer lock, so it is the revision this
851
+ // write actually owns. Prefer it over a post-lock file re-read, which a concurrent writer
852
+ // could have advanced before the read — that race could otherwise let this payload be
853
+ // published with another writer's newer revision.
854
+ return { warning, stamped: (await readJsonFile(filePath)) ?? {}, revision: writeResult.revision };
806
855
  }
807
856
 
808
857
  function parseFieldsFlag(args: readonly string[]): StateProjectionField[] | undefined {
@@ -851,13 +900,14 @@ function parseSinceFlag(args: readonly string[]): string | undefined {
851
900
  async function readAuditWindow(
852
901
  cwd: string,
853
902
  args: readonly string[],
903
+ sessionId: string,
854
904
  ): Promise<{ entries: unknown[]; limit: number; since?: string; truncated: boolean }> {
855
905
  const limit = parseLimitFlag(args);
856
906
  const since = parseSinceFlag(args);
857
- const auditPath = path.join(cwd, ".gjc", "state", "audit.jsonl");
907
+ const auditFile = auditPath(cwd, sessionId);
858
908
  let raw = "";
859
909
  try {
860
- raw = await fs.readFile(auditPath, "utf-8");
910
+ raw = await fs.readFile(auditFile, "utf-8");
861
911
  } catch (error) {
862
912
  const err = error as NodeJS.ErrnoException;
863
913
  if (err.code !== "ENOENT") throw error;
@@ -965,6 +1015,14 @@ function buildHudForMode(
965
1015
  typeof (rawLedger as Record<string, unknown>).timestamp === "string"
966
1016
  ? ((rawLedger as Record<string, unknown>).timestamp as string)
967
1017
  : undefined,
1018
+ kind:
1019
+ typeof (rawLedger as Record<string, unknown>).kind === "string"
1020
+ ? ((rawLedger as Record<string, unknown>).kind as string)
1021
+ : undefined,
1022
+ evidence:
1023
+ typeof (rawLedger as Record<string, unknown>).evidence === "string"
1024
+ ? ((rawLedger as Record<string, unknown>).evidence as string)
1025
+ : undefined,
968
1026
  }
969
1027
  : undefined;
970
1028
  const status = typeof payload.status === "string" ? (payload.status as string) : (phase ?? "pending");
@@ -1014,7 +1072,7 @@ function buildHudForMode(
1014
1072
  async function syncWorkflowSkillState(options: {
1015
1073
  cwd: string;
1016
1074
  mode: CanonicalGjcWorkflowSkill;
1017
- sessionId: string | undefined;
1075
+ sessionId: string;
1018
1076
  threadId?: string;
1019
1077
  turnId?: string;
1020
1078
  active: boolean;
@@ -1034,6 +1092,7 @@ async function syncWorkflowSkillState(options: {
1034
1092
  source: "gjc-state-cli",
1035
1093
  hud: buildHudForMode(options.mode, options.payload),
1036
1094
  ...(options.receipt ? { receipt: options.receipt } : {}),
1095
+ sourceRevision: existingStateRevision(options.payload),
1037
1096
  });
1038
1097
  } catch {
1039
1098
  // HUD sync is best-effort and must not change command semantics.
@@ -1053,14 +1112,19 @@ async function syncWorkflowSkillState(options: {
1053
1112
  export async function reconcileWorkflowSkillState(options: {
1054
1113
  cwd: string;
1055
1114
  mode: CanonicalGjcWorkflowSkill;
1056
- sessionId: string | undefined;
1115
+ sessionId?: string;
1057
1116
  threadId?: string;
1058
1117
  turnId?: string;
1059
1118
  active: boolean;
1060
1119
  phase: string;
1061
1120
  payload: Record<string, unknown>;
1121
+ sourceRevision?: number;
1062
1122
  }): Promise<{ stateFile: string }> {
1063
- const { cwd, mode, sessionId, threadId, turnId, active, payload } = options;
1123
+ const { cwd, mode, threadId, turnId, active, payload } = options;
1124
+ const { gjcSessionId: sessionId } = resolveGjcSessionForWrite(cwd, {
1125
+ payloadSessionId: options.sessionId,
1126
+ envSessionId: process.env.GJC_SESSION_ID,
1127
+ });
1064
1128
  const filePath = modeStateFile(cwd, mode, sessionId);
1065
1129
  const existingRead = await readExistingStateForMutation(filePath);
1066
1130
  const existingPayload = existingRead.kind === "valid" ? existingRead.value : {};
@@ -1104,14 +1168,37 @@ export async function reconcileWorkflowSkillState(options: {
1104
1168
  const validation = validateWorkflowStateEnvelope(mode, merged);
1105
1169
  if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
1106
1170
 
1107
- await writeJsonAtomic(cwd, filePath, merged, "reconcile", {
1108
- skill: mode,
1109
- mutationId,
1110
- force: true,
1111
- fromPhase,
1112
- toPhase: trimmedPhase,
1113
- owner: "gjc-runtime",
1171
+ if (existingRead.kind === "corrupt") await fs.rm(filePath, { force: true });
1172
+ await writeGuardedWorkflowEnvelopeAtomic(filePath, merged, {
1173
+ cwd,
1174
+ policy: "source",
1175
+ receipt: {
1176
+ cwd,
1177
+ skill: mode,
1178
+ owner: "gjc-runtime",
1179
+ command: `gjc ${mode} (reconcile)`,
1180
+ sessionId,
1181
+ nowIso: nowIsoStr,
1182
+ mutationId,
1183
+ verb: "reconcile",
1184
+ forced: true,
1185
+ fromPhase,
1186
+ toPhase: trimmedPhase,
1187
+ },
1188
+ audit: {
1189
+ category: "state",
1190
+ verb: "reconcile",
1191
+ owner: "gjc-runtime",
1192
+ sessionId,
1193
+ skill: mode,
1194
+ mutationId,
1195
+ forced: true,
1196
+ fromPhase,
1197
+ toPhase: trimmedPhase,
1198
+ },
1114
1199
  });
1200
+ const persisted = (await readJsonFile(filePath)) ?? {};
1201
+ const sourceRevision = options.sourceRevision ?? existingStateRevision(persisted);
1115
1202
 
1116
1203
  // Reconciliation drives the active-state/HUD update directly (not via the
1117
1204
  // best-effort syncWorkflowSkillState wrapper) so a failed HUD/active-state write
@@ -1128,7 +1215,9 @@ export async function reconcileWorkflowSkillState(options: {
1128
1215
  source: "gjc-runtime-reconcile",
1129
1216
  hud: buildHudForMode(mode, merged),
1130
1217
  receipt,
1218
+ sourceRevision,
1131
1219
  });
1220
+ await touchStateActivityMarker(cwd, sessionId, filePath);
1132
1221
  return { stateFile: filePath };
1133
1222
  }
1134
1223
  export async function readWorkflowStateJson(
@@ -1136,7 +1225,11 @@ export async function readWorkflowStateJson(
1136
1225
  skill: CanonicalGjcWorkflowSkill,
1137
1226
  sessionId?: string,
1138
1227
  ): Promise<Record<string, unknown>> {
1139
- return (await readJsonFile(modeStateFile(cwd, skill, sessionId))) ?? {};
1228
+ const session = await resolveGjcSessionForRead(cwd, {
1229
+ payloadSessionId: sessionId,
1230
+ envSessionId: process.env.GJC_SESSION_ID,
1231
+ });
1232
+ return (await readJsonFile(modeStateFile(cwd, skill, session.gjcSessionId))) ?? {};
1140
1233
  }
1141
1234
 
1142
1235
  async function handleRead(
@@ -1144,12 +1237,12 @@ async function handleRead(
1144
1237
  cwd: string,
1145
1238
  positionalSkill: string | undefined,
1146
1239
  ): Promise<StateCommandResult> {
1147
- const selectors = await resolveSelectors(args, cwd, positionalSkill);
1148
- const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.sessionId));
1240
+ const selectors = await resolveSelectors(args, cwd, positionalSkill, "read");
1241
+ const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
1149
1242
  const fields = parseFieldsFlag(args);
1150
1243
  if (mode) {
1151
- const filePath = modeStateFile(cwd, mode, selectors.sessionId);
1152
- const existing = await readWorkflowStateJson(cwd, mode, selectors.sessionId);
1244
+ const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
1245
+ const existing = await readWorkflowStateJson(cwd, mode, selectors.gjcSessionId);
1153
1246
  const envelope = { skill: mode, state: existing, storage_path: filePath };
1154
1247
  const manifest = getSkillManifest(mode);
1155
1248
  if (fields) {
@@ -1177,7 +1270,7 @@ async function handleRead(
1177
1270
  : renderStateMarkdown(mode, envelope, manifest),
1178
1271
  };
1179
1272
  }
1180
- const filePath = activeStateFile(cwd, selectors.sessionId);
1273
+ const filePath = activeStateFile(cwd, selectors.gjcSessionId);
1181
1274
  const existingRaw = await readJsonValue(filePath);
1182
1275
  const existing = isPlainObject(existingRaw) ? existingRaw : null;
1183
1276
  return { status: 0, stdout: `${JSON.stringify(existing ?? {}, null, 2)}\n` };
@@ -1188,16 +1281,16 @@ async function handleStatus(
1188
1281
  cwd: string,
1189
1282
  positionalSkill: string | undefined,
1190
1283
  ): Promise<StateCommandResult> {
1191
- const selectors = await resolveSelectors(args, cwd, positionalSkill);
1192
- const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.sessionId));
1284
+ const selectors = await resolveSelectors(args, cwd, positionalSkill, "read");
1285
+ const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
1193
1286
  if (!mode) {
1194
1287
  throw new StateCommandError(
1195
1288
  2,
1196
- "gjc state status requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .gjc/state/skill-active-state.json",
1289
+ "gjc state status requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
1197
1290
  );
1198
1291
  }
1199
- const filePath = modeStateFile(cwd, mode, selectors.sessionId);
1200
- const existing = await readWorkflowStateJson(cwd, mode, selectors.sessionId);
1292
+ const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
1293
+ const existing = await readWorkflowStateJson(cwd, mode, selectors.gjcSessionId);
1201
1294
  const summary = buildStateStatusSummary(
1202
1295
  mode,
1203
1296
  { skill: mode, state: existing, storage_path: filePath },
@@ -1215,14 +1308,14 @@ async function handleWrite(
1215
1308
  cwd: string,
1216
1309
  positionalSkill: string | undefined,
1217
1310
  ): Promise<StateCommandResult> {
1218
- const selectors = await resolveSelectors(args, cwd, positionalSkill);
1219
- const { sessionId, threadId, turnId, payload } = selectors;
1311
+ const selectors = await resolveSelectors(args, cwd, positionalSkill, "write");
1312
+ const { gjcSessionId: sessionId, threadId, turnId, payload } = selectors;
1220
1313
  if (!payload) throw new StateCommandError(2, "gjc state write requires --input '<json>'");
1221
1314
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
1222
1315
  if (!mode)
1223
1316
  throw new StateCommandError(
1224
1317
  2,
1225
- "gjc state write requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .gjc/state/skill-active-state.json",
1318
+ "gjc state write requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
1226
1319
  );
1227
1320
 
1228
1321
  const filePath = modeStateFile(cwd, mode, sessionId);
@@ -1310,7 +1403,12 @@ async function handleWrite(
1310
1403
  const validation = validateWorkflowStateEnvelope(mode, merged);
1311
1404
  if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
1312
1405
 
1313
- const { warning: outOfBandWarning, stamped } = await writeJsonAtomic(cwd, filePath, merged, "write", {
1406
+ const {
1407
+ warning: outOfBandWarning,
1408
+ stamped,
1409
+ revision: stampedRevision,
1410
+ } = await writeJsonAtomic(cwd, filePath, merged, "write", {
1411
+ sessionId,
1314
1412
  skill: mode,
1315
1413
  mutationId,
1316
1414
  force: forced,
@@ -1321,7 +1419,14 @@ async function handleWrite(
1321
1419
 
1322
1420
  const phase = typeof merged.current_phase === "string" ? merged.current_phase : undefined;
1323
1421
  const active = merged.active !== false;
1422
+ // Reflect the lock-owned mode-state revision onto the in-memory payload so the active-state/HUD
1423
+ // sync derives a `sourceRevision` from the revision this write actually owns (computed inside the
1424
+ // writer lock), not the stale pre-write value or a post-lock re-read a concurrent writer could
1425
+ // have advanced; otherwise the active-state writer stale-skips the update and the mirror keeps the
1426
+ // prior phase (e.g. staying "interviewing" after a "handoff" write).
1427
+ merged.state_revision = stampedRevision;
1324
1428
  await syncWorkflowSkillState({ cwd, mode, sessionId, threadId, turnId, active, phase, payload: merged, receipt });
1429
+ await touchStateActivityMarker(cwd, sessionId, filePath);
1325
1430
 
1326
1431
  return {
1327
1432
  status: 0,
@@ -1344,13 +1449,13 @@ async function handleClear(
1344
1449
  cwd: string,
1345
1450
  positionalSkill: string | undefined,
1346
1451
  ): Promise<StateCommandResult> {
1347
- const selectors = await resolveSelectors(args, cwd, positionalSkill);
1348
- const { sessionId, threadId, turnId } = selectors;
1452
+ const selectors = await resolveSelectors(args, cwd, positionalSkill, "clear");
1453
+ const { gjcSessionId: sessionId, threadId, turnId } = selectors;
1349
1454
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
1350
1455
  if (!mode)
1351
1456
  throw new StateCommandError(
1352
1457
  2,
1353
- "gjc state clear requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .gjc/state/skill-active-state.json",
1458
+ "gjc state clear requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
1354
1459
  );
1355
1460
 
1356
1461
  const filePath = modeStateFile(cwd, mode, sessionId);
@@ -1363,6 +1468,10 @@ async function handleClear(
1363
1468
  );
1364
1469
  }
1365
1470
  const existing = existingRead.kind === "valid" ? existingRead.value : {};
1471
+ const staleReason = await describeStaleClearState(cwd, sessionId, mode, existing);
1472
+ if (staleReason && !forced) {
1473
+ throw new StateCommandError(2, `existing state for ${mode} is stale (${staleReason}); use --force to clear`);
1474
+ }
1366
1475
  const clearedAt = nowIso();
1367
1476
  const cleared: Record<string, unknown> = {
1368
1477
  skill: mode,
@@ -1385,6 +1494,7 @@ async function handleClear(
1385
1494
  });
1386
1495
  cleared.receipt = receipt;
1387
1496
  const { warning: outOfBandWarning, stamped } = await writeJsonAtomic(cwd, filePath, cleared, "clear", {
1497
+ sessionId,
1388
1498
  skill: mode,
1389
1499
  mutationId,
1390
1500
  force: forced,
@@ -1403,6 +1513,7 @@ async function handleClear(
1403
1513
  phase: "complete",
1404
1514
  payload: cleared,
1405
1515
  });
1516
+ await touchStateActivityMarker(cwd, sessionId, filePath);
1406
1517
  return {
1407
1518
  status: 0,
1408
1519
  stdout: renderCliWriteReceipt({
@@ -1443,13 +1554,13 @@ async function handleHandoff(
1443
1554
  cwd: string,
1444
1555
  positionalSkill: string | undefined,
1445
1556
  ): Promise<StateCommandResult> {
1446
- const selectors = await resolveSelectors(args, cwd, positionalSkill);
1447
- const { sessionId, threadId, turnId } = selectors;
1557
+ const selectors = await resolveSelectors(args, cwd, positionalSkill, "handoff");
1558
+ const { gjcSessionId: sessionId, threadId, turnId } = selectors;
1448
1559
  const caller = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
1449
1560
  if (!caller) {
1450
1561
  throw new StateCommandError(
1451
1562
  2,
1452
- "gjc state handoff requires --mode <caller>, positional <caller>, input.skill, or an active workflow in .gjc/state/skill-active-state.json",
1563
+ "gjc state handoff requires --mode <caller>, positional <caller>, input.skill, or an active workflow in the current session active state",
1453
1564
  );
1454
1565
  }
1455
1566
  const calleeRaw = flagValue(args, "--to")?.trim();
@@ -1552,6 +1663,7 @@ async function handleHandoff(
1552
1663
 
1553
1664
  await beginWorkflowTransactionJournal({
1554
1665
  cwd,
1666
+ sessionId,
1555
1667
  mutationId,
1556
1668
  caller,
1557
1669
  callee,
@@ -1567,21 +1679,23 @@ async function handleHandoff(
1567
1679
  // only; corrupt JSON / IO failures propagate as non-zero CLI status.
1568
1680
  const force = hasFlag(args, "--force");
1569
1681
  const calleeWrite = await writeJsonAtomic(cwd, calleePath, mergedCalleeState, "handoff", {
1682
+ sessionId,
1570
1683
  skill: callee,
1571
1684
  mutationId,
1572
1685
  force,
1573
1686
  fromPhase: typeof existingCallee.current_phase === "string" ? existingCallee.current_phase : undefined,
1574
1687
  toPhase: calleeInitial,
1575
1688
  });
1576
- await updateWorkflowTransactionJournal(cwd, mutationId, { steps: ["callee-mode-state"] });
1689
+ await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, { steps: ["callee-mode-state"] });
1577
1690
  const callerWrite = await writeJsonAtomic(cwd, callerPath, mergedCallerState, "handoff", {
1691
+ sessionId,
1578
1692
  skill: caller,
1579
1693
  mutationId,
1580
1694
  force,
1581
1695
  fromPhase: typeof existingCaller.current_phase === "string" ? existingCaller.current_phase : undefined,
1582
1696
  toPhase: "handoff",
1583
1697
  });
1584
- await updateWorkflowTransactionJournal(cwd, mutationId, {
1698
+ await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, {
1585
1699
  steps: ["callee-mode-state", "caller-mode-state"],
1586
1700
  });
1587
1701
  const warnings = [calleeWrite.warning, callerWrite.warning].filter(
@@ -1626,10 +1740,11 @@ async function handleHandoff(
1626
1740
  receipt: calleeReceipt,
1627
1741
  },
1628
1742
  });
1629
- await updateWorkflowTransactionJournal(cwd, mutationId, {
1743
+ await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, {
1630
1744
  steps: ["callee-mode-state", "caller-mode-state", "active-state"],
1631
1745
  });
1632
- await completeWorkflowTransactionJournal(cwd, mutationId);
1746
+ await completeWorkflowTransactionJournal(cwd, sessionId, mutationId);
1747
+ await touchStateActivityMarker(cwd, sessionId, callerPath);
1633
1748
 
1634
1749
  return {
1635
1750
  status: 0,
@@ -1668,7 +1783,7 @@ async function handleContract(
1668
1783
  cwd: string,
1669
1784
  positionalSkill: string | undefined,
1670
1785
  ): Promise<StateCommandResult> {
1671
- const { mode } = await resolveSelectors(args, cwd, positionalSkill);
1786
+ const { mode } = await resolveSelectors(args, cwd, positionalSkill, "read");
1672
1787
  if (!mode) {
1673
1788
  throw new StateCommandError(2, "gjc state contract requires --mode <skill>, positional <skill>, or input.skill");
1674
1789
  }
@@ -1723,11 +1838,7 @@ function categoryForStateRelativePath(relativePath: string): string | undefined
1723
1838
  if (normalized === "audit.jsonl") return undefined;
1724
1839
  if (normalized === SKILL_ACTIVE_STATE_FILE || normalized.endsWith(`/${SKILL_ACTIVE_STATE_FILE}`)) return undefined;
1725
1840
  if (normalized.startsWith("active/") || normalized.includes("/active/")) return undefined;
1726
- if (
1727
- /^[^/]+-state\.json$/.test(normalized) ||
1728
- (normalized.includes("/sessions/") && /\/[^/]+-state\.json$/.test(normalized))
1729
- )
1730
- return undefined;
1841
+ if (/^[^/]+-state\.json$/.test(normalized) || false) return undefined;
1731
1842
  if (normalized.startsWith("artifacts/") || normalized.includes("/artifacts/")) return "artifact";
1732
1843
  if (
1733
1844
  normalized.startsWith("logs/") ||
@@ -1753,9 +1864,10 @@ function categoryForStateRelativePath(relativePath: string): string | undefined
1753
1864
 
1754
1865
  async function collectRetentionCandidates(
1755
1866
  cwd: string,
1867
+ sessionId: string,
1756
1868
  skills: readonly CanonicalGjcWorkflowSkill[],
1757
1869
  ): Promise<RetentionCandidate[]> {
1758
- const stateRoot = path.join(cwd, ".gjc", "state");
1870
+ const stateRoot = sessionStateDir(cwd, sessionId);
1759
1871
  const policies = new Map<string, { keep?: number; maxAgeDays?: number }>();
1760
1872
  for (const skill of skills) {
1761
1873
  for (const policy of getSkillManifest(skill).retention) {
@@ -1836,7 +1948,11 @@ async function buildGcSummary(
1836
1948
  flagValue(args, "--skill")?.trim() || flagValue(args, "--mode")?.trim() || positionalSkill?.trim() || "all";
1837
1949
  if (rawSkill !== "all") assertKnownMode(rawSkill);
1838
1950
  const skills = rawSkill === "all" ? CANONICAL_GJC_WORKFLOW_SKILLS : [rawSkill as CanonicalGjcWorkflowSkill];
1839
- const eligible = selectRetentionEligible(await collectRetentionCandidates(cwd, skills));
1951
+ const session = await resolveGjcSessionForRead(cwd, {
1952
+ flagValue: flagValue(args, "--session-id"),
1953
+ envSessionId: process.env.GJC_SESSION_ID,
1954
+ });
1955
+ const eligible = selectRetentionEligible(await collectRetentionCandidates(cwd, session.gjcSessionId, skills));
1840
1956
  const counts: Record<string, number> = {};
1841
1957
  for (const candidate of eligible) counts[candidate.category] = (counts[candidate.category] ?? 0) + 1;
1842
1958
  const targets: GenericHardPruneTarget[] = eligible.map(candidate => ({
@@ -1850,6 +1966,7 @@ async function buildGcSummary(
1850
1966
  cwd,
1851
1967
  audit: {
1852
1968
  cwd,
1969
+ sessionId: session.gjcSessionId,
1853
1970
  skill: rawSkill,
1854
1971
  category: "prune",
1855
1972
  verb: "gc",
@@ -1861,7 +1978,7 @@ async function buildGcSummary(
1861
1978
  skill: rawSkill as CanonicalGjcWorkflowSkill | "all",
1862
1979
  dry_run: dryRun,
1863
1980
  eligible: eligible.map(candidate => candidate.relativePath),
1864
- pruned: pruned.map(filePath => path.relative(path.join(cwd, ".gjc", "state"), filePath)),
1981
+ pruned: pruned.map(filePath => path.relative(sessionStateDir(cwd, session.gjcSessionId), filePath)),
1865
1982
  counts,
1866
1983
  };
1867
1984
  }
@@ -1872,7 +1989,11 @@ async function handleGraph(
1872
1989
  positionalSkill: string | undefined,
1873
1990
  ): Promise<StateCommandResult> {
1874
1991
  if (hasFlag(args, "--history")) {
1875
- const history = await readAuditWindow(_cwd, args);
1992
+ const session = await resolveGjcSessionForRead(_cwd, {
1993
+ flagValue: flagValue(args, "--session-id"),
1994
+ envSessionId: process.env.GJC_SESSION_ID,
1995
+ });
1996
+ const history = await readAuditWindow(_cwd, args, session.gjcSessionId);
1876
1997
  return {
1877
1998
  status: 0,
1878
1999
  stdout: hasFlag(args, "--json") ? `${JSON.stringify(history, null, 2)}\n` : renderHistoryMarkdown(history),
@@ -1895,20 +2016,21 @@ async function handlePrune(
1895
2016
  cwd: string,
1896
2017
  positionalSkill: string | undefined,
1897
2018
  ): Promise<StateCommandResult> {
1898
- const selectors = await resolveSelectors(args, cwd, positionalSkill);
1899
- const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.sessionId));
2019
+ const selectors = await resolveSelectors(args, cwd, positionalSkill, "prune");
2020
+ const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
1900
2021
  if (!mode) {
1901
2022
  throw new StateCommandError(
1902
2023
  2,
1903
- "gjc state prune requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .gjc/state/skill-active-state.json",
2024
+ "gjc state prune requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
1904
2025
  );
1905
2026
  }
1906
- const filePath = modeStateFile(cwd, mode, selectors.sessionId);
2027
+ const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
1907
2028
  const olderThanDays = parseNonNegativeIntegerFlag(args, "--older-than");
1908
2029
  const status = flagValue(args, "--status")?.trim();
1909
2030
  const targets: GenericHardPruneTarget[] = [{ path: filePath, category: "prune" }];
1910
2031
  const audit: StateWriterAuditContext = {
1911
2032
  cwd,
2033
+ sessionId: selectors.gjcSessionId,
1912
2034
  skill: mode,
1913
2035
  category: "prune",
1914
2036
  verb: hasFlag(args, "--hard") ? "hard-prune" : "soft-delete",
@@ -1964,17 +2086,17 @@ async function handleMigrate(
1964
2086
  cwd: string,
1965
2087
  positionalSkill: string | undefined,
1966
2088
  ): Promise<StateCommandResult> {
1967
- const selectors = await resolveSelectors(args, cwd, positionalSkill);
1968
- const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.sessionId));
2089
+ const selectors = await resolveSelectors(args, cwd, positionalSkill, "migrate");
2090
+ const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.gjcSessionId));
1969
2091
  if (!mode) {
1970
2092
  throw new StateCommandError(
1971
2093
  2,
1972
- "gjc state migrate requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .gjc/state/skill-active-state.json",
2094
+ "gjc state migrate requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
1973
2095
  );
1974
2096
  }
1975
- const filePath = modeStateFile(cwd, mode, selectors.sessionId);
2097
+ const filePath = modeStateFile(cwd, mode, selectors.gjcSessionId);
1976
2098
  const forced = hasFlag(args, "--force");
1977
- const mismatchWarning = await warnAndAuditOutOfBandIfNeeded(cwd, filePath, mode, {
2099
+ const mismatchWarning = await warnAndAuditOutOfBandIfNeeded(cwd, selectors.gjcSessionId, filePath, mode, {
1978
2100
  forced,
1979
2101
  });
1980
2102
  if (mismatchWarning && !forced) {
@@ -1984,7 +2106,7 @@ async function handleMigrate(
1984
2106
  cwd,
1985
2107
  skill: mode,
1986
2108
  statePath: filePath,
1987
- sessionId: selectors.sessionId,
2109
+ sessionId: selectors.gjcSessionId,
1988
2110
  });
1989
2111
  return {
1990
2112
  status: 0,
@@ -2026,6 +2148,7 @@ export async function runNativeStateCommand(args: string[], cwd = process.cwd())
2026
2148
  }
2027
2149
  } catch (error) {
2028
2150
  if (error instanceof StateCommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
2151
+ if (error instanceof SessionResolutionError) return { status: 2, stderr: `${error.message}\n` };
2029
2152
  return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
2030
2153
  }
2031
2154
  }