@sayknow-cli/coding-agent 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/types/cli/migrate-cli.d.ts +20 -0
  3. package/dist/types/cli/update-cli.d.ts +3 -0
  4. package/dist/types/commands/migrate.d.ts +33 -0
  5. package/dist/types/config/keybindings.d.ts +4 -0
  6. package/dist/types/config/model-registry.d.ts +3 -0
  7. package/dist/types/config/models-config-schema.d.ts +5 -0
  8. package/dist/types/config/settings-schema.d.ts +27 -0
  9. package/dist/types/harness-control-plane/storage.d.ts +2 -1
  10. package/dist/types/hooks/skill-state.d.ts +12 -4
  11. package/dist/types/i18n/messages/en.d.ts +14 -0
  12. package/dist/types/lsp/startup-events.d.ts +1 -0
  13. package/dist/types/migrate/action-planner.d.ts +11 -0
  14. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  15. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  16. package/dist/types/migrate/adapters/index.d.ts +45 -0
  17. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  18. package/dist/types/migrate/executor.d.ts +2 -0
  19. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  20. package/dist/types/migrate/report.d.ts +18 -0
  21. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  22. package/dist/types/migrate/types.d.ts +126 -0
  23. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  24. package/dist/types/modes/components/welcome.d.ts +3 -1
  25. package/dist/types/modes/interactive-mode.d.ts +3 -0
  26. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  27. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  28. package/dist/types/research-plan/index.d.ts +1 -0
  29. package/dist/types/research-plan/ledger.d.ts +33 -0
  30. package/dist/types/rlm/artifacts.d.ts +1 -1
  31. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  32. package/dist/types/skc-runtime/deep-interview-recorder.d.ts +2 -0
  33. package/dist/types/skc-runtime/deep-interview-runtime.d.ts +2 -2
  34. package/dist/types/skc-runtime/goal-mode-request.d.ts +1 -1
  35. package/dist/types/skc-runtime/session-layout.d.ts +59 -0
  36. package/dist/types/skc-runtime/session-resolution.d.ts +47 -0
  37. package/dist/types/skc-runtime/state-graph.d.ts +1 -1
  38. package/dist/types/skc-runtime/state-runtime.d.ts +5 -4
  39. package/dist/types/skc-runtime/state-schema.d.ts +2 -0
  40. package/dist/types/skc-runtime/state-writer.d.ts +36 -7
  41. package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
  42. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +7 -4
  43. package/dist/types/skc-runtime/workflow-command-ref.d.ts +1 -1
  44. package/dist/types/skc-runtime/workflow-manifest.d.ts +1 -1
  45. package/dist/types/skill-state/active-state.d.ts +6 -11
  46. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  47. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +5 -0
  48. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  49. package/dist/types/task/spawn-gate.d.ts +1 -10
  50. package/package.json +7 -7
  51. package/scripts/build-binary.ts +0 -7
  52. package/src/cli/migrate-cli.ts +106 -0
  53. package/src/cli/setup-cli.ts +14 -1
  54. package/src/cli/update-cli.ts +53 -3
  55. package/src/cli.ts +1 -0
  56. package/src/commands/deep-interview.ts +2 -2
  57. package/src/commands/launch.ts +1 -1
  58. package/src/commands/migrate.ts +46 -0
  59. package/src/commands/state.ts +2 -1
  60. package/src/commands/team.ts +7 -3
  61. package/src/config/model-registry.ts +9 -2
  62. package/src/config/model-resolver.ts +13 -2
  63. package/src/config/models-config-schema.ts +1 -0
  64. package/src/config/settings-schema.ts +17 -0
  65. package/src/coordinator-mcp/policy.ts +10 -2
  66. package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +0 -1
  67. package/src/defaults/skc/skills/deep-interview/SKILL.md +30 -24
  68. package/src/defaults/skc/skills/ralplan/SKILL.md +10 -4
  69. package/src/defaults/skc/skills/team/SKILL.md +51 -47
  70. package/src/defaults/skc/skills/ultragoal/SKILL.md +17 -13
  71. package/src/exec/bash-executor.ts +3 -1
  72. package/src/extensibility/custom-commands/loader.ts +0 -7
  73. package/src/extensibility/skc-plugins/injection.ts +23 -4
  74. package/src/extensibility/skc-plugins/state.ts +16 -1
  75. package/src/harness-control-plane/storage.ts +14 -4
  76. package/src/hooks/native-skill-hook.ts +38 -12
  77. package/src/hooks/skill-state.ts +178 -83
  78. package/src/i18n/messages/de.ts +14 -0
  79. package/src/i18n/messages/en.ts +14 -0
  80. package/src/i18n/messages/es.ts +14 -0
  81. package/src/i18n/messages/fr.ts +14 -0
  82. package/src/i18n/messages/ja.ts +14 -0
  83. package/src/i18n/messages/ko.ts +14 -0
  84. package/src/i18n/messages/zh.ts +14 -0
  85. package/src/internal-urls/docs-index.generated.ts +11 -9
  86. package/src/lsp/startup-events.ts +24 -0
  87. package/src/migrate/action-planner.ts +318 -0
  88. package/src/migrate/adapters/claude-code.ts +39 -0
  89. package/src/migrate/adapters/codex.ts +70 -0
  90. package/src/migrate/adapters/index.ts +277 -0
  91. package/src/migrate/adapters/opencode.ts +52 -0
  92. package/src/migrate/executor.ts +81 -0
  93. package/src/migrate/mcp-mapper.ts +152 -0
  94. package/src/migrate/report.ts +104 -0
  95. package/src/migrate/skill-normalizer.ts +80 -0
  96. package/src/migrate/types.ts +163 -0
  97. package/src/modes/bridge/bridge-mode.ts +2 -2
  98. package/src/modes/components/custom-editor.ts +30 -20
  99. package/src/modes/components/model-selector.ts +42 -18
  100. package/src/modes/components/welcome.ts +20 -9
  101. package/src/modes/controllers/input-controller.ts +21 -3
  102. package/src/modes/interactive-mode.ts +27 -19
  103. package/src/modes/prompt-action-autocomplete.ts +11 -1
  104. package/src/modes/rpc/rpc-mode.ts +2 -2
  105. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  106. package/src/prompts/agents/init.md +1 -1
  107. package/src/prompts/system/plan-mode-active.md +1 -1
  108. package/src/prompts/tools/ast-grep.md +1 -1
  109. package/src/prompts/tools/search.md +1 -1
  110. package/src/prompts/tools/task.md +1 -2
  111. package/src/research-plan/index.ts +1 -0
  112. package/src/research-plan/ledger.ts +177 -0
  113. package/src/rlm/artifacts.ts +12 -3
  114. package/src/rlm/index.ts +7 -0
  115. package/src/runtime-mcp/config-writer.ts +46 -0
  116. package/src/session/agent-session.ts +43 -41
  117. package/src/session/session-manager.ts +19 -2
  118. package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
  119. package/src/setup/hermes-setup.ts +1 -1
  120. package/src/skc-runtime/deep-interview-recorder.ts +51 -18
  121. package/src/skc-runtime/deep-interview-runtime.ts +49 -23
  122. package/src/skc-runtime/goal-mode-request.ts +26 -11
  123. package/src/skc-runtime/launch-tmux.ts +68 -15
  124. package/src/skc-runtime/ralplan-runtime.ts +79 -50
  125. package/src/skc-runtime/session-layout.ts +180 -0
  126. package/src/skc-runtime/session-resolution.ts +217 -0
  127. package/src/skc-runtime/state-graph.ts +1 -2
  128. package/src/skc-runtime/state-migrations.ts +1 -0
  129. package/src/skc-runtime/state-runtime.ts +237 -114
  130. package/src/skc-runtime/state-schema.ts +2 -0
  131. package/src/skc-runtime/state-writer.ts +310 -42
  132. package/src/skc-runtime/team-runtime.ts +43 -19
  133. package/src/skc-runtime/tmux-sessions.ts +43 -2
  134. package/src/skc-runtime/ultragoal-guard.ts +45 -2
  135. package/src/skc-runtime/ultragoal-runtime.ts +121 -41
  136. package/src/skc-runtime/workflow-command-ref.ts +1 -2
  137. package/src/skc-runtime/workflow-manifest.ts +1 -2
  138. package/src/skill-state/active-state.ts +116 -129
  139. package/src/skill-state/canonical-skills.ts +4 -0
  140. package/src/skill-state/deep-interview-mutation-guard.ts +238 -111
  141. package/src/skill-state/workflow-hud.ts +4 -2
  142. package/src/skill-state/workflow-state-contract.ts +3 -3
  143. package/src/slash-commands/builtin-registry.ts +8 -4
  144. package/src/system-prompt.ts +11 -9
  145. package/src/task/agents.ts +1 -22
  146. package/src/task/index.ts +1 -41
  147. package/src/task/spawn-gate.ts +1 -38
  148. package/src/task/types.ts +1 -1
  149. package/src/tools/ask.ts +34 -12
  150. package/src/tools/ast-edit.ts +2 -2
  151. package/src/tools/computer.ts +58 -4
  152. package/src/utils/edit-mode.ts +1 -1
  153. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  154. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  155. package/src/prompts/agents/explore.md +0 -58
  156. package/src/prompts/agents/plan.md +0 -49
  157. package/src/prompts/agents/reviewer.md +0 -141
  158. package/src/prompts/agents/task.md +0 -16
  159. 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
+ resolveSkcSessionForRead,
33
+ resolveSkcSessionForWrite,
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 `skc state read|write|clear` command surface.
62
69
  *
63
- * Simple file-receipt operations against `.skc/state/[sessions/<id>/]<mode>-state.json` and
64
- * `.skc/state/[sessions/<id>/]skill-active-state.json`. This is the sanctioned CLI mediator for
65
- * the mutation-guarded `.skc/state` ACL — agents call it instead of editing those files directly.
70
+ * Simple file-receipt operations against session-scoped state under
71
+ * `.skc/_session-{id}/state/`. This is the sanctioned CLI mediator for
72
+ * mutation-guarded SKC 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_SKC_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: CanonicalSkcWorkflowSkill | undefined;
272
- sessionId: string | undefined;
280
+ skcSessionId: 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,33 +311,32 @@ async function resolveSelectors(
297
311
  }
298
312
  if (mode) assertKnownMode(mode);
299
313
 
300
- const explicitSessionId = flagValue(args, "--session-id");
301
- // Session-id resolution order: explicit --session-id flag, then payload
302
- // session_id, then SKC_SESSION_ID env var (set by AgentSession.sdk for
303
- // agent-initiated CLI invocations). The env-var default keeps shell
304
- // snippets in skill docs short while still routing writes/reads to the
305
- // caller's session-scoped state files.
306
- let sessionId = explicitSessionId !== undefined ? explicitSessionId.trim() || undefined : undefined;
307
- if (!sessionId && payload && typeof payload.session_id === "string") {
308
- sessionId = payload.session_id.trim() || undefined;
309
- }
310
- if (!sessionId && explicitSessionId === undefined) {
311
- const envSessionId = process.env.SKC_SESSION_ID?.trim();
312
- if (envSessionId) sessionId = envSessionId;
313
- }
314
- if (sessionId) assertSafePathComponent(sessionId, "session-id");
314
+ const sessionSources = {
315
+ flagValue: flagValue(args, "--session-id"),
316
+ payloadSessionId: payload?.session_id,
317
+ envSessionId: process.env.SKC_SESSION_ID,
318
+ };
319
+ const session = WRITE_SESSION_ACTIONS.has(action)
320
+ ? resolveSkcSessionForWrite(cwd, sessionSources)
321
+ : await resolveSkcSessionForRead(cwd, sessionSources);
315
322
 
316
323
  const threadId = flagValue(args, "--thread-id")?.trim() || undefined;
317
324
  if (threadId) assertSafePathComponent(threadId, "thread-id");
318
325
  const turnId = flagValue(args, "--turn-id")?.trim() || undefined;
319
326
  if (turnId) assertSafePathComponent(turnId, "turn-id");
320
327
 
321
- return { mode: mode as CanonicalSkcWorkflowSkill | undefined, sessionId, threadId, turnId, payload };
328
+ return {
329
+ mode: mode as CanonicalSkcWorkflowSkill | undefined,
330
+ skcSessionId: session.skcSessionId,
331
+ threadId,
332
+ turnId,
333
+ payload,
334
+ };
322
335
  }
323
336
 
324
337
  async function inferModeFromActiveState(
325
338
  cwd: string,
326
- sessionId: string | undefined,
339
+ sessionId: string,
327
340
  ): Promise<CanonicalSkcWorkflowSkill | undefined> {
328
341
  const state = await readVisibleSkillActiveState(cwd, sessionId);
329
342
  const entries = listActiveSkills(state);
@@ -333,22 +346,53 @@ async function inferModeFromActiveState(
333
346
  return canonical ?? undefined;
334
347
  }
335
348
 
336
- function encodeSessionSegment(value: string): string {
337
- return encodeURIComponent(value).replaceAll(".", "%2E");
349
+ function stateDirFor(cwd: string, sessionId: string): string {
350
+ return sessionStateDir(cwd, sessionId);
338
351
  }
339
352
 
340
- function stateDirFor(cwd: string, sessionId: string | undefined): string {
341
- const base = path.join(cwd, ".skc", "state");
342
- if (!sessionId) return base;
343
- return path.join(base, "sessions", encodeSessionSegment(sessionId));
353
+ function modeStateFile(cwd: string, mode: string, sessionId: string): string {
354
+ return modeStatePath(cwd, sessionId, mode);
344
355
  }
345
356
 
346
- function modeStateFile(cwd: string, mode: string, sessionId: string | undefined): string {
347
- return path.join(stateDirFor(cwd, sessionId), `${mode}-state.json`);
357
+ function activeStateFile(cwd: string, sessionId: string): string {
358
+ return activeSnapshotPath(cwd, sessionId);
359
+ }
360
+
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: CanonicalSkcWorkflowSkill,
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;
348
381
  }
349
382
 
350
- function activeStateFile(cwd: string, sessionId: string | undefined): string {
351
- return path.join(stateDirFor(cwd, sessionId), SKILL_ACTIVE_STATE_FILE);
383
+ async function describeStaleClearState(
384
+ cwd: string,
385
+ sessionId: string,
386
+ mode: CanonicalSkcWorkflowSkill,
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;
352
396
  }
353
397
 
354
398
  async function readJsonFile(filePath: string): Promise<Record<string, unknown> | null> {
@@ -438,7 +482,7 @@ function doctorProblem(
438
482
  : { type, path: pathValue, message, fixCommand };
439
483
  }
440
484
 
441
- function activeEntryDir(cwd: string, sessionId: string | undefined): string {
485
+ function activeEntryDir(cwd: string, sessionId: string): string {
442
486
  return path.join(stateDirFor(cwd, sessionId), "active");
443
487
  }
444
488
 
@@ -499,9 +543,9 @@ function pushPhaseDriftProblem(options: {
499
543
  async function collectDoctorSummary(
500
544
  cwd: string,
501
545
  skill: CanonicalSkcWorkflowSkill | undefined,
502
- sessionId: string | undefined,
546
+ sessionId: string,
503
547
  ): Promise<DoctorSummary> {
504
- const root = path.join(cwd, ".skc", "state");
548
+ const root = sessionStateDir(cwd, sessionId);
505
549
  const skills = skill ? [skill] : [...CANONICAL_SKC_WORKFLOW_SKILLS];
506
550
  const problems: DoctorProblem[] = [];
507
551
  let filesScanned = 0;
@@ -575,7 +619,7 @@ async function collectDoctorSummary(
575
619
  }
576
620
  }
577
621
 
578
- const inspectActiveScope = async (scopeSessionId: string | undefined): Promise<void> => {
622
+ const inspectActiveScope = async (scopeSessionId: string): Promise<void> => {
579
623
  const snapshotPath = activeStateFile(cwd, scopeSessionId);
580
624
  const snapshot = await readRawJson(snapshotPath);
581
625
  if (snapshot.exists) filesScanned += 1;
@@ -616,7 +660,9 @@ async function collectDoctorSummary(
616
660
  }
617
661
  }
618
662
  if (isPlainObject(snapshot.value)) {
619
- 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
+ : [];
620
666
  for (const entry of activeSkills) {
621
667
  const entrySkill = skillFromActiveValue(entry);
622
668
  if (!entrySkill) continue;
@@ -650,21 +696,6 @@ async function collectDoctorSummary(
650
696
  };
651
697
 
652
698
  await inspectActiveScope(sessionId);
653
- if (!sessionId) {
654
- const sessionsDir = path.join(root, "sessions");
655
- let sessions: string[] = [];
656
- try {
657
- const entries = await fs.readdir(sessionsDir, { withFileTypes: true });
658
- sessions = entries
659
- .filter(entry => entry.isDirectory())
660
- .map(entry => entry.name)
661
- .sort();
662
- } catch (error) {
663
- const err = error as NodeJS.ErrnoException;
664
- if (err.code !== "ENOENT") throw error;
665
- }
666
- for (const rawSession of sessions) await inspectActiveScope(decodeURIComponent(rawSession));
667
- }
668
699
 
669
700
  problems.sort(
670
701
  (a, b) =>
@@ -718,9 +749,17 @@ async function handleDoctor(
718
749
  ): Promise<StateCommandResult> {
719
750
  const rawSkill = flagValue(args, "--skill")?.trim() || flagValue(args, "--mode")?.trim() || positionalSkill?.trim();
720
751
  if (rawSkill) assertKnownMode(rawSkill);
721
- const sessionId = flagValue(args, "--session-id")?.trim() || undefined;
722
- if (sessionId) assertSafePathComponent(sessionId, "session-id");
723
- const summary = await collectDoctorSummary(cwd, rawSkill as CanonicalSkcWorkflowSkill | undefined, sessionId);
752
+ const payload = await readInputJson(flagValue(args, "--input"), cwd);
753
+ const session = await resolveSkcSessionForRead(cwd, {
754
+ flagValue: flagValue(args, "--session-id"),
755
+ payloadSessionId: payload?.session_id,
756
+ envSessionId: process.env.SKC_SESSION_ID,
757
+ });
758
+ const summary = await collectDoctorSummary(
759
+ cwd,
760
+ rawSkill as CanonicalSkcWorkflowSkill | undefined,
761
+ session.skcSessionId,
762
+ );
724
763
  return {
725
764
  status: summary.ok ? 0 : 1,
726
765
  stdout: hasFlag(args, "--json") ? `${JSON.stringify(summary, null, 2)}\n` : renderDoctorText(summary),
@@ -729,6 +768,7 @@ async function handleDoctor(
729
768
 
730
769
  async function warnAndAuditOutOfBandIfNeeded(
731
770
  cwd: string,
771
+ sessionId: string,
732
772
  filePath: string,
733
773
  skill: CanonicalSkcWorkflowSkill,
734
774
  options?: { mutationId?: string; forced?: boolean },
@@ -743,7 +783,7 @@ async function warnAndAuditOutOfBandIfNeeded(
743
783
  }
744
784
  if (!mismatch) return undefined;
745
785
  const message = `WARNING: workflow mode-state out-of-band edit detected for ${skill}: ${filePath} expected sha256 ${mismatch.expected} but found ${mismatch.actual}`;
746
- await appendAuditEntry(cwd, {
786
+ await appendAuditEntry(cwd, sessionId, {
747
787
  ts: new Date().toISOString(),
748
788
  skill,
749
789
  category: "state",
@@ -758,12 +798,19 @@ async function warnAndAuditOutOfBandIfNeeded(
758
798
  return message;
759
799
  }
760
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
+
761
807
  async function writeJsonAtomic(
762
808
  cwd: string,
763
809
  filePath: string,
764
810
  value: unknown,
765
811
  verb: "write" | "clear" | "handoff" | "reconcile" = "write",
766
812
  options?: {
813
+ sessionId: string;
767
814
  skill?: CanonicalSkcWorkflowSkill;
768
815
  mutationId?: string;
769
816
  force?: boolean;
@@ -773,7 +820,7 @@ async function writeJsonAtomic(
773
820
  },
774
821
  ): Promise<{ warning?: string; stamped: Record<string, unknown> }> {
775
822
  const warning = options?.skill
776
- ? await warnAndAuditOutOfBandIfNeeded(cwd, filePath, options.skill, {
823
+ ? await warnAndAuditOutOfBandIfNeeded(cwd, options.sessionId, filePath, options.skill, {
777
824
  mutationId: options.mutationId,
778
825
  forced: options.force ?? false,
779
826
  })
@@ -781,9 +828,15 @@ async function writeJsonAtomic(
781
828
  if (warning && !options?.force) {
782
829
  throw new StateCommandError(2, `${warning}; use --force to overwrite tampered mode-state`);
783
830
  }
784
- 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
+ await writeGuardedWorkflowEnvelopeAtomic(filePath, value, {
785
836
  cwd,
837
+ policy: "source",
786
838
  audit: {
839
+ sessionId: options?.sessionId ?? "",
787
840
  category: "state",
788
841
  verb,
789
842
  owner: options?.owner ?? "skc-state-cli",
@@ -843,13 +896,14 @@ function parseSinceFlag(args: readonly string[]): string | undefined {
843
896
  async function readAuditWindow(
844
897
  cwd: string,
845
898
  args: readonly string[],
899
+ sessionId: string,
846
900
  ): Promise<{ entries: unknown[]; limit: number; since?: string; truncated: boolean }> {
847
901
  const limit = parseLimitFlag(args);
848
902
  const since = parseSinceFlag(args);
849
- const auditPath = path.join(cwd, ".skc", "state", "audit.jsonl");
903
+ const auditFile = auditPath(cwd, sessionId);
850
904
  let raw = "";
851
905
  try {
852
- raw = await fs.readFile(auditPath, "utf-8");
906
+ raw = await fs.readFile(auditFile, "utf-8");
853
907
  } catch (error) {
854
908
  const err = error as NodeJS.ErrnoException;
855
909
  if (err.code !== "ENOENT") throw error;
@@ -957,6 +1011,14 @@ function buildHudForMode(
957
1011
  typeof (rawLedger as Record<string, unknown>).timestamp === "string"
958
1012
  ? ((rawLedger as Record<string, unknown>).timestamp as string)
959
1013
  : undefined,
1014
+ kind:
1015
+ typeof (rawLedger as Record<string, unknown>).kind === "string"
1016
+ ? ((rawLedger as Record<string, unknown>).kind as string)
1017
+ : undefined,
1018
+ evidence:
1019
+ typeof (rawLedger as Record<string, unknown>).evidence === "string"
1020
+ ? ((rawLedger as Record<string, unknown>).evidence as string)
1021
+ : undefined,
960
1022
  }
961
1023
  : undefined;
962
1024
  const status = typeof payload.status === "string" ? (payload.status as string) : (phase ?? "pending");
@@ -1006,7 +1068,7 @@ function buildHudForMode(
1006
1068
  async function syncWorkflowSkillState(options: {
1007
1069
  cwd: string;
1008
1070
  mode: CanonicalSkcWorkflowSkill;
1009
- sessionId: string | undefined;
1071
+ sessionId: string;
1010
1072
  threadId?: string;
1011
1073
  turnId?: string;
1012
1074
  active: boolean;
@@ -1026,6 +1088,7 @@ async function syncWorkflowSkillState(options: {
1026
1088
  source: "skc-state-cli",
1027
1089
  hud: buildHudForMode(options.mode, options.payload),
1028
1090
  ...(options.receipt ? { receipt: options.receipt } : {}),
1091
+ sourceRevision: existingStateRevision(options.payload),
1029
1092
  });
1030
1093
  } catch {
1031
1094
  // HUD sync is best-effort and must not change command semantics.
@@ -1045,14 +1108,19 @@ async function syncWorkflowSkillState(options: {
1045
1108
  export async function reconcileWorkflowSkillState(options: {
1046
1109
  cwd: string;
1047
1110
  mode: CanonicalSkcWorkflowSkill;
1048
- sessionId: string | undefined;
1111
+ sessionId?: string;
1049
1112
  threadId?: string;
1050
1113
  turnId?: string;
1051
1114
  active: boolean;
1052
1115
  phase: string;
1053
1116
  payload: Record<string, unknown>;
1117
+ sourceRevision?: number;
1054
1118
  }): Promise<{ stateFile: string }> {
1055
- const { cwd, mode, sessionId, threadId, turnId, active, payload } = options;
1119
+ const { cwd, mode, threadId, turnId, active, payload } = options;
1120
+ const { skcSessionId: sessionId } = resolveSkcSessionForWrite(cwd, {
1121
+ payloadSessionId: options.sessionId,
1122
+ envSessionId: process.env.SKC_SESSION_ID,
1123
+ });
1056
1124
  const filePath = modeStateFile(cwd, mode, sessionId);
1057
1125
  const existingRead = await readExistingStateForMutation(filePath);
1058
1126
  const existingPayload = existingRead.kind === "valid" ? existingRead.value : {};
@@ -1096,14 +1164,37 @@ export async function reconcileWorkflowSkillState(options: {
1096
1164
  const validation = validateWorkflowStateEnvelope(mode, merged);
1097
1165
  if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
1098
1166
 
1099
- await writeJsonAtomic(cwd, filePath, merged, "reconcile", {
1100
- skill: mode,
1101
- mutationId,
1102
- force: true,
1103
- fromPhase,
1104
- toPhase: trimmedPhase,
1105
- owner: "skc-runtime",
1167
+ if (existingRead.kind === "corrupt") await fs.rm(filePath, { force: true });
1168
+ await writeGuardedWorkflowEnvelopeAtomic(filePath, merged, {
1169
+ cwd,
1170
+ policy: "source",
1171
+ receipt: {
1172
+ cwd,
1173
+ skill: mode,
1174
+ owner: "skc-runtime",
1175
+ command: `skc ${mode} (reconcile)`,
1176
+ sessionId,
1177
+ nowIso: nowIsoStr,
1178
+ mutationId,
1179
+ verb: "reconcile",
1180
+ forced: true,
1181
+ fromPhase,
1182
+ toPhase: trimmedPhase,
1183
+ },
1184
+ audit: {
1185
+ category: "state",
1186
+ verb: "reconcile",
1187
+ owner: "skc-runtime",
1188
+ sessionId,
1189
+ skill: mode,
1190
+ mutationId,
1191
+ forced: true,
1192
+ fromPhase,
1193
+ toPhase: trimmedPhase,
1194
+ },
1106
1195
  });
1196
+ const persisted = (await readJsonFile(filePath)) ?? {};
1197
+ const sourceRevision = options.sourceRevision ?? existingStateRevision(persisted);
1107
1198
 
1108
1199
  // Reconciliation drives the active-state/HUD update directly (not via the
1109
1200
  // best-effort syncWorkflowSkillState wrapper) so a failed HUD/active-state write
@@ -1120,7 +1211,9 @@ export async function reconcileWorkflowSkillState(options: {
1120
1211
  source: "skc-runtime-reconcile",
1121
1212
  hud: buildHudForMode(mode, merged),
1122
1213
  receipt,
1214
+ sourceRevision,
1123
1215
  });
1216
+ await touchStateActivityMarker(cwd, sessionId, filePath);
1124
1217
  return { stateFile: filePath };
1125
1218
  }
1126
1219
  export async function readWorkflowStateJson(
@@ -1128,7 +1221,11 @@ export async function readWorkflowStateJson(
1128
1221
  skill: CanonicalSkcWorkflowSkill,
1129
1222
  sessionId?: string,
1130
1223
  ): Promise<Record<string, unknown>> {
1131
- return (await readJsonFile(modeStateFile(cwd, skill, sessionId))) ?? {};
1224
+ const session = await resolveSkcSessionForRead(cwd, {
1225
+ payloadSessionId: sessionId,
1226
+ envSessionId: process.env.SKC_SESSION_ID,
1227
+ });
1228
+ return (await readJsonFile(modeStateFile(cwd, skill, session.skcSessionId))) ?? {};
1132
1229
  }
1133
1230
 
1134
1231
  async function handleRead(
@@ -1136,12 +1233,12 @@ async function handleRead(
1136
1233
  cwd: string,
1137
1234
  positionalSkill: string | undefined,
1138
1235
  ): Promise<StateCommandResult> {
1139
- const selectors = await resolveSelectors(args, cwd, positionalSkill);
1140
- const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.sessionId));
1236
+ const selectors = await resolveSelectors(args, cwd, positionalSkill, "read");
1237
+ const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.skcSessionId));
1141
1238
  const fields = parseFieldsFlag(args);
1142
1239
  if (mode) {
1143
- const filePath = modeStateFile(cwd, mode, selectors.sessionId);
1144
- const existing = await readWorkflowStateJson(cwd, mode, selectors.sessionId);
1240
+ const filePath = modeStateFile(cwd, mode, selectors.skcSessionId);
1241
+ const existing = await readWorkflowStateJson(cwd, mode, selectors.skcSessionId);
1145
1242
  const envelope = { skill: mode, state: existing, storage_path: filePath };
1146
1243
  const manifest = getSkillManifest(mode);
1147
1244
  if (fields) {
@@ -1169,7 +1266,7 @@ async function handleRead(
1169
1266
  : renderStateMarkdown(mode, envelope, manifest),
1170
1267
  };
1171
1268
  }
1172
- const filePath = activeStateFile(cwd, selectors.sessionId);
1269
+ const filePath = activeStateFile(cwd, selectors.skcSessionId);
1173
1270
  const existingRaw = await readJsonValue(filePath);
1174
1271
  const existing = isPlainObject(existingRaw) ? existingRaw : null;
1175
1272
  return { status: 0, stdout: `${JSON.stringify(existing ?? {}, null, 2)}\n` };
@@ -1180,16 +1277,16 @@ async function handleStatus(
1180
1277
  cwd: string,
1181
1278
  positionalSkill: string | undefined,
1182
1279
  ): Promise<StateCommandResult> {
1183
- const selectors = await resolveSelectors(args, cwd, positionalSkill);
1184
- const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.sessionId));
1280
+ const selectors = await resolveSelectors(args, cwd, positionalSkill, "read");
1281
+ const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.skcSessionId));
1185
1282
  if (!mode) {
1186
1283
  throw new StateCommandError(
1187
1284
  2,
1188
- "skc state status requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .skc/state/skill-active-state.json",
1285
+ "skc state status requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
1189
1286
  );
1190
1287
  }
1191
- const filePath = modeStateFile(cwd, mode, selectors.sessionId);
1192
- const existing = await readWorkflowStateJson(cwd, mode, selectors.sessionId);
1288
+ const filePath = modeStateFile(cwd, mode, selectors.skcSessionId);
1289
+ const existing = await readWorkflowStateJson(cwd, mode, selectors.skcSessionId);
1193
1290
  const summary = buildStateStatusSummary(
1194
1291
  mode,
1195
1292
  { skill: mode, state: existing, storage_path: filePath },
@@ -1207,14 +1304,14 @@ async function handleWrite(
1207
1304
  cwd: string,
1208
1305
  positionalSkill: string | undefined,
1209
1306
  ): Promise<StateCommandResult> {
1210
- const selectors = await resolveSelectors(args, cwd, positionalSkill);
1211
- const { sessionId, threadId, turnId, payload } = selectors;
1307
+ const selectors = await resolveSelectors(args, cwd, positionalSkill, "write");
1308
+ const { skcSessionId: sessionId, threadId, turnId, payload } = selectors;
1212
1309
  if (!payload) throw new StateCommandError(2, "skc state write requires --input '<json>'");
1213
1310
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
1214
1311
  if (!mode)
1215
1312
  throw new StateCommandError(
1216
1313
  2,
1217
- "skc state write requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .skc/state/skill-active-state.json",
1314
+ "skc state write requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
1218
1315
  );
1219
1316
 
1220
1317
  const filePath = modeStateFile(cwd, mode, sessionId);
@@ -1303,6 +1400,7 @@ async function handleWrite(
1303
1400
  if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
1304
1401
 
1305
1402
  const { warning: outOfBandWarning, stamped } = await writeJsonAtomic(cwd, filePath, merged, "write", {
1403
+ sessionId,
1306
1404
  skill: mode,
1307
1405
  mutationId,
1308
1406
  force: forced,
@@ -1313,7 +1411,14 @@ async function handleWrite(
1313
1411
 
1314
1412
  const phase = typeof merged.current_phase === "string" ? merged.current_phase : undefined;
1315
1413
  const active = merged.active !== false;
1414
+ // Reflect the freshly stamped mode-state revision onto the in-memory payload so the
1415
+ // active-state/HUD sync derives a `sourceRevision` from the persisted revision, not the
1416
+ // stale pre-write value; otherwise the active-state writer stale-skips the update and the
1417
+ // mirror keeps the prior phase (e.g. staying "interviewing" after a "handoff" write).
1418
+ const stampedRevision = existingStateRevision(stamped);
1419
+ if (typeof stampedRevision === "number") merged.state_revision = stampedRevision;
1316
1420
  await syncWorkflowSkillState({ cwd, mode, sessionId, threadId, turnId, active, phase, payload: merged, receipt });
1421
+ await touchStateActivityMarker(cwd, sessionId, filePath);
1317
1422
 
1318
1423
  return {
1319
1424
  status: 0,
@@ -1336,13 +1441,13 @@ async function handleClear(
1336
1441
  cwd: string,
1337
1442
  positionalSkill: string | undefined,
1338
1443
  ): Promise<StateCommandResult> {
1339
- const selectors = await resolveSelectors(args, cwd, positionalSkill);
1340
- const { sessionId, threadId, turnId } = selectors;
1444
+ const selectors = await resolveSelectors(args, cwd, positionalSkill, "clear");
1445
+ const { skcSessionId: sessionId, threadId, turnId } = selectors;
1341
1446
  const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
1342
1447
  if (!mode)
1343
1448
  throw new StateCommandError(
1344
1449
  2,
1345
- "skc state clear requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .skc/state/skill-active-state.json",
1450
+ "skc state clear requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
1346
1451
  );
1347
1452
 
1348
1453
  const filePath = modeStateFile(cwd, mode, sessionId);
@@ -1355,6 +1460,10 @@ async function handleClear(
1355
1460
  );
1356
1461
  }
1357
1462
  const existing = existingRead.kind === "valid" ? existingRead.value : {};
1463
+ const staleReason = await describeStaleClearState(cwd, sessionId, mode, existing);
1464
+ if (staleReason && !forced) {
1465
+ throw new StateCommandError(2, `existing state for ${mode} is stale (${staleReason}); use --force to clear`);
1466
+ }
1358
1467
  const clearedAt = nowIso();
1359
1468
  const cleared: Record<string, unknown> = {
1360
1469
  skill: mode,
@@ -1377,6 +1486,7 @@ async function handleClear(
1377
1486
  });
1378
1487
  cleared.receipt = receipt;
1379
1488
  const { warning: outOfBandWarning, stamped } = await writeJsonAtomic(cwd, filePath, cleared, "clear", {
1489
+ sessionId,
1380
1490
  skill: mode,
1381
1491
  mutationId,
1382
1492
  force: forced,
@@ -1395,6 +1505,7 @@ async function handleClear(
1395
1505
  phase: "complete",
1396
1506
  payload: cleared,
1397
1507
  });
1508
+ await touchStateActivityMarker(cwd, sessionId, filePath);
1398
1509
  return {
1399
1510
  status: 0,
1400
1511
  stdout: renderCliWriteReceipt({
@@ -1435,13 +1546,13 @@ async function handleHandoff(
1435
1546
  cwd: string,
1436
1547
  positionalSkill: string | undefined,
1437
1548
  ): Promise<StateCommandResult> {
1438
- const selectors = await resolveSelectors(args, cwd, positionalSkill);
1439
- const { sessionId, threadId, turnId } = selectors;
1549
+ const selectors = await resolveSelectors(args, cwd, positionalSkill, "handoff");
1550
+ const { skcSessionId: sessionId, threadId, turnId } = selectors;
1440
1551
  const caller = selectors.mode ?? (await inferModeFromActiveState(cwd, sessionId));
1441
1552
  if (!caller) {
1442
1553
  throw new StateCommandError(
1443
1554
  2,
1444
- "skc state handoff requires --mode <caller>, positional <caller>, input.skill, or an active workflow in .skc/state/skill-active-state.json",
1555
+ "skc state handoff requires --mode <caller>, positional <caller>, input.skill, or an active workflow in the current session active state",
1445
1556
  );
1446
1557
  }
1447
1558
  const calleeRaw = flagValue(args, "--to")?.trim();
@@ -1544,6 +1655,7 @@ async function handleHandoff(
1544
1655
 
1545
1656
  await beginWorkflowTransactionJournal({
1546
1657
  cwd,
1658
+ sessionId,
1547
1659
  mutationId,
1548
1660
  caller,
1549
1661
  callee,
@@ -1559,21 +1671,23 @@ async function handleHandoff(
1559
1671
  // only; corrupt JSON / IO failures propagate as non-zero CLI status.
1560
1672
  const force = hasFlag(args, "--force");
1561
1673
  const calleeWrite = await writeJsonAtomic(cwd, calleePath, mergedCalleeState, "handoff", {
1674
+ sessionId,
1562
1675
  skill: callee,
1563
1676
  mutationId,
1564
1677
  force,
1565
1678
  fromPhase: typeof existingCallee.current_phase === "string" ? existingCallee.current_phase : undefined,
1566
1679
  toPhase: calleeInitial,
1567
1680
  });
1568
- await updateWorkflowTransactionJournal(cwd, mutationId, { steps: ["callee-mode-state"] });
1681
+ await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, { steps: ["callee-mode-state"] });
1569
1682
  const callerWrite = await writeJsonAtomic(cwd, callerPath, mergedCallerState, "handoff", {
1683
+ sessionId,
1570
1684
  skill: caller,
1571
1685
  mutationId,
1572
1686
  force,
1573
1687
  fromPhase: typeof existingCaller.current_phase === "string" ? existingCaller.current_phase : undefined,
1574
1688
  toPhase: "handoff",
1575
1689
  });
1576
- await updateWorkflowTransactionJournal(cwd, mutationId, {
1690
+ await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, {
1577
1691
  steps: ["callee-mode-state", "caller-mode-state"],
1578
1692
  });
1579
1693
  const warnings = [calleeWrite.warning, callerWrite.warning].filter(
@@ -1618,10 +1732,11 @@ async function handleHandoff(
1618
1732
  receipt: calleeReceipt,
1619
1733
  },
1620
1734
  });
1621
- await updateWorkflowTransactionJournal(cwd, mutationId, {
1735
+ await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, {
1622
1736
  steps: ["callee-mode-state", "caller-mode-state", "active-state"],
1623
1737
  });
1624
- await completeWorkflowTransactionJournal(cwd, mutationId);
1738
+ await completeWorkflowTransactionJournal(cwd, sessionId, mutationId);
1739
+ await touchStateActivityMarker(cwd, sessionId, callerPath);
1625
1740
 
1626
1741
  return {
1627
1742
  status: 0,
@@ -1660,7 +1775,7 @@ async function handleContract(
1660
1775
  cwd: string,
1661
1776
  positionalSkill: string | undefined,
1662
1777
  ): Promise<StateCommandResult> {
1663
- const { mode } = await resolveSelectors(args, cwd, positionalSkill);
1778
+ const { mode } = await resolveSelectors(args, cwd, positionalSkill, "read");
1664
1779
  if (!mode) {
1665
1780
  throw new StateCommandError(2, "skc state contract requires --mode <skill>, positional <skill>, or input.skill");
1666
1781
  }
@@ -1715,11 +1830,7 @@ function categoryForStateRelativePath(relativePath: string): string | undefined
1715
1830
  if (normalized === "audit.jsonl") return undefined;
1716
1831
  if (normalized === SKILL_ACTIVE_STATE_FILE || normalized.endsWith(`/${SKILL_ACTIVE_STATE_FILE}`)) return undefined;
1717
1832
  if (normalized.startsWith("active/") || normalized.includes("/active/")) return undefined;
1718
- if (
1719
- /^[^/]+-state\.json$/.test(normalized) ||
1720
- (normalized.includes("/sessions/") && /\/[^/]+-state\.json$/.test(normalized))
1721
- )
1722
- return undefined;
1833
+ if (/^[^/]+-state\.json$/.test(normalized) || false) return undefined;
1723
1834
  if (normalized.startsWith("artifacts/") || normalized.includes("/artifacts/")) return "artifact";
1724
1835
  if (
1725
1836
  normalized.startsWith("logs/") ||
@@ -1745,9 +1856,10 @@ function categoryForStateRelativePath(relativePath: string): string | undefined
1745
1856
 
1746
1857
  async function collectRetentionCandidates(
1747
1858
  cwd: string,
1859
+ sessionId: string,
1748
1860
  skills: readonly CanonicalSkcWorkflowSkill[],
1749
1861
  ): Promise<RetentionCandidate[]> {
1750
- const stateRoot = path.join(cwd, ".skc", "state");
1862
+ const stateRoot = sessionStateDir(cwd, sessionId);
1751
1863
  const policies = new Map<string, { keep?: number; maxAgeDays?: number }>();
1752
1864
  for (const skill of skills) {
1753
1865
  for (const policy of getSkillManifest(skill).retention) {
@@ -1828,7 +1940,11 @@ async function buildGcSummary(
1828
1940
  flagValue(args, "--skill")?.trim() || flagValue(args, "--mode")?.trim() || positionalSkill?.trim() || "all";
1829
1941
  if (rawSkill !== "all") assertKnownMode(rawSkill);
1830
1942
  const skills = rawSkill === "all" ? CANONICAL_SKC_WORKFLOW_SKILLS : [rawSkill as CanonicalSkcWorkflowSkill];
1831
- const eligible = selectRetentionEligible(await collectRetentionCandidates(cwd, skills));
1943
+ const session = await resolveSkcSessionForRead(cwd, {
1944
+ flagValue: flagValue(args, "--session-id"),
1945
+ envSessionId: process.env.SKC_SESSION_ID,
1946
+ });
1947
+ const eligible = selectRetentionEligible(await collectRetentionCandidates(cwd, session.skcSessionId, skills));
1832
1948
  const counts: Record<string, number> = {};
1833
1949
  for (const candidate of eligible) counts[candidate.category] = (counts[candidate.category] ?? 0) + 1;
1834
1950
  const targets: GenericHardPruneTarget[] = eligible.map(candidate => ({
@@ -1842,6 +1958,7 @@ async function buildGcSummary(
1842
1958
  cwd,
1843
1959
  audit: {
1844
1960
  cwd,
1961
+ sessionId: session.skcSessionId,
1845
1962
  skill: rawSkill,
1846
1963
  category: "prune",
1847
1964
  verb: "gc",
@@ -1853,7 +1970,7 @@ async function buildGcSummary(
1853
1970
  skill: rawSkill as CanonicalSkcWorkflowSkill | "all",
1854
1971
  dry_run: dryRun,
1855
1972
  eligible: eligible.map(candidate => candidate.relativePath),
1856
- pruned: pruned.map(filePath => path.relative(path.join(cwd, ".skc", "state"), filePath)),
1973
+ pruned: pruned.map(filePath => path.relative(sessionStateDir(cwd, session.skcSessionId), filePath)),
1857
1974
  counts,
1858
1975
  };
1859
1976
  }
@@ -1864,7 +1981,11 @@ async function handleGraph(
1864
1981
  positionalSkill: string | undefined,
1865
1982
  ): Promise<StateCommandResult> {
1866
1983
  if (hasFlag(args, "--history")) {
1867
- const history = await readAuditWindow(_cwd, args);
1984
+ const session = await resolveSkcSessionForRead(_cwd, {
1985
+ flagValue: flagValue(args, "--session-id"),
1986
+ envSessionId: process.env.SKC_SESSION_ID,
1987
+ });
1988
+ const history = await readAuditWindow(_cwd, args, session.skcSessionId);
1868
1989
  return {
1869
1990
  status: 0,
1870
1991
  stdout: hasFlag(args, "--json") ? `${JSON.stringify(history, null, 2)}\n` : renderHistoryMarkdown(history),
@@ -1887,20 +2008,21 @@ async function handlePrune(
1887
2008
  cwd: string,
1888
2009
  positionalSkill: string | undefined,
1889
2010
  ): Promise<StateCommandResult> {
1890
- const selectors = await resolveSelectors(args, cwd, positionalSkill);
1891
- const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.sessionId));
2011
+ const selectors = await resolveSelectors(args, cwd, positionalSkill, "prune");
2012
+ const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.skcSessionId));
1892
2013
  if (!mode) {
1893
2014
  throw new StateCommandError(
1894
2015
  2,
1895
- "skc state prune requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .skc/state/skill-active-state.json",
2016
+ "skc state prune requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
1896
2017
  );
1897
2018
  }
1898
- const filePath = modeStateFile(cwd, mode, selectors.sessionId);
2019
+ const filePath = modeStateFile(cwd, mode, selectors.skcSessionId);
1899
2020
  const olderThanDays = parseNonNegativeIntegerFlag(args, "--older-than");
1900
2021
  const status = flagValue(args, "--status")?.trim();
1901
2022
  const targets: GenericHardPruneTarget[] = [{ path: filePath, category: "prune" }];
1902
2023
  const audit: StateWriterAuditContext = {
1903
2024
  cwd,
2025
+ sessionId: selectors.skcSessionId,
1904
2026
  skill: mode,
1905
2027
  category: "prune",
1906
2028
  verb: hasFlag(args, "--hard") ? "hard-prune" : "soft-delete",
@@ -1956,17 +2078,17 @@ async function handleMigrate(
1956
2078
  cwd: string,
1957
2079
  positionalSkill: string | undefined,
1958
2080
  ): Promise<StateCommandResult> {
1959
- const selectors = await resolveSelectors(args, cwd, positionalSkill);
1960
- const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.sessionId));
2081
+ const selectors = await resolveSelectors(args, cwd, positionalSkill, "migrate");
2082
+ const mode = selectors.mode ?? (await inferModeFromActiveState(cwd, selectors.skcSessionId));
1961
2083
  if (!mode) {
1962
2084
  throw new StateCommandError(
1963
2085
  2,
1964
- "skc state migrate requires --mode <skill>, positional <skill>, input.skill, or an active workflow in .skc/state/skill-active-state.json",
2086
+ "skc state migrate requires --mode <skill>, positional <skill>, input.skill, or an active workflow in the current session active state",
1965
2087
  );
1966
2088
  }
1967
- const filePath = modeStateFile(cwd, mode, selectors.sessionId);
2089
+ const filePath = modeStateFile(cwd, mode, selectors.skcSessionId);
1968
2090
  const forced = hasFlag(args, "--force");
1969
- const mismatchWarning = await warnAndAuditOutOfBandIfNeeded(cwd, filePath, mode, {
2091
+ const mismatchWarning = await warnAndAuditOutOfBandIfNeeded(cwd, selectors.skcSessionId, filePath, mode, {
1970
2092
  forced,
1971
2093
  });
1972
2094
  if (mismatchWarning && !forced) {
@@ -1976,7 +2098,7 @@ async function handleMigrate(
1976
2098
  cwd,
1977
2099
  skill: mode,
1978
2100
  statePath: filePath,
1979
- sessionId: selectors.sessionId,
2101
+ sessionId: selectors.skcSessionId,
1980
2102
  });
1981
2103
  return {
1982
2104
  status: 0,
@@ -2018,6 +2140,7 @@ export async function runNativeStateCommand(args: string[], cwd = process.cwd())
2018
2140
  }
2019
2141
  } catch (error) {
2020
2142
  if (error instanceof StateCommandError) return { status: error.exitStatus, stderr: `${error.message}\n` };
2143
+ if (error instanceof SessionResolutionError) return { status: 2, stderr: `${error.message}\n` };
2021
2144
  return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
2022
2145
  }
2023
2146
  }