@sayknow-cli/coding-agent 0.2.4 → 0.2.6

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 (150) hide show
  1. package/CHANGELOG.md +24 -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/lsp/startup-events.d.ts +1 -0
  12. package/dist/types/migrate/action-planner.d.ts +11 -0
  13. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  14. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  15. package/dist/types/migrate/adapters/index.d.ts +45 -0
  16. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  17. package/dist/types/migrate/executor.d.ts +2 -0
  18. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  19. package/dist/types/migrate/report.d.ts +18 -0
  20. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  21. package/dist/types/migrate/types.d.ts +126 -0
  22. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  23. package/dist/types/modes/components/welcome.d.ts +3 -1
  24. package/dist/types/modes/interactive-mode.d.ts +3 -0
  25. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  26. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  27. package/dist/types/research-plan/index.d.ts +1 -0
  28. package/dist/types/research-plan/ledger.d.ts +33 -0
  29. package/dist/types/rlm/artifacts.d.ts +1 -1
  30. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  31. package/dist/types/skc-runtime/deep-interview-recorder.d.ts +2 -0
  32. package/dist/types/skc-runtime/deep-interview-runtime.d.ts +2 -2
  33. package/dist/types/skc-runtime/goal-mode-request.d.ts +1 -1
  34. package/dist/types/skc-runtime/session-layout.d.ts +59 -0
  35. package/dist/types/skc-runtime/session-resolution.d.ts +47 -0
  36. package/dist/types/skc-runtime/state-graph.d.ts +1 -1
  37. package/dist/types/skc-runtime/state-runtime.d.ts +5 -4
  38. package/dist/types/skc-runtime/state-schema.d.ts +2 -0
  39. package/dist/types/skc-runtime/state-writer.d.ts +36 -7
  40. package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
  41. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +7 -4
  42. package/dist/types/skc-runtime/workflow-command-ref.d.ts +1 -1
  43. package/dist/types/skc-runtime/workflow-manifest.d.ts +1 -1
  44. package/dist/types/skill-state/active-state.d.ts +6 -11
  45. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  46. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +5 -0
  47. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  48. package/dist/types/task/spawn-gate.d.ts +1 -10
  49. package/package.json +7 -7
  50. package/scripts/build-binary.ts +0 -7
  51. package/src/cli/migrate-cli.ts +106 -0
  52. package/src/cli/setup-cli.ts +14 -1
  53. package/src/cli/update-cli.ts +53 -3
  54. package/src/cli.ts +1 -0
  55. package/src/commands/deep-interview.ts +2 -2
  56. package/src/commands/launch.ts +1 -1
  57. package/src/commands/migrate.ts +46 -0
  58. package/src/commands/state.ts +2 -1
  59. package/src/commands/team.ts +7 -3
  60. package/src/config/model-registry.ts +9 -2
  61. package/src/config/model-resolver.ts +13 -2
  62. package/src/config/models-config-schema.ts +1 -0
  63. package/src/config/settings-schema.ts +17 -0
  64. package/src/coordinator-mcp/policy.ts +10 -2
  65. package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +0 -1
  66. package/src/defaults/skc/skills/deep-interview/SKILL.md +30 -24
  67. package/src/defaults/skc/skills/ralplan/SKILL.md +10 -4
  68. package/src/defaults/skc/skills/team/SKILL.md +51 -47
  69. package/src/defaults/skc/skills/ultragoal/SKILL.md +17 -13
  70. package/src/exec/bash-executor.ts +3 -1
  71. package/src/extensibility/custom-commands/loader.ts +0 -7
  72. package/src/extensibility/skc-plugins/injection.ts +23 -4
  73. package/src/extensibility/skc-plugins/state.ts +16 -1
  74. package/src/harness-control-plane/storage.ts +14 -4
  75. package/src/hooks/native-skill-hook.ts +38 -12
  76. package/src/hooks/skill-state.ts +178 -83
  77. package/src/internal-urls/docs-index.generated.ts +11 -9
  78. package/src/lsp/startup-events.ts +24 -0
  79. package/src/migrate/action-planner.ts +318 -0
  80. package/src/migrate/adapters/claude-code.ts +39 -0
  81. package/src/migrate/adapters/codex.ts +70 -0
  82. package/src/migrate/adapters/index.ts +277 -0
  83. package/src/migrate/adapters/opencode.ts +52 -0
  84. package/src/migrate/executor.ts +81 -0
  85. package/src/migrate/mcp-mapper.ts +152 -0
  86. package/src/migrate/report.ts +104 -0
  87. package/src/migrate/skill-normalizer.ts +80 -0
  88. package/src/migrate/types.ts +163 -0
  89. package/src/modes/bridge/bridge-mode.ts +2 -2
  90. package/src/modes/components/custom-editor.ts +30 -20
  91. package/src/modes/components/welcome.ts +20 -9
  92. package/src/modes/controllers/input-controller.ts +21 -3
  93. package/src/modes/interactive-mode.ts +27 -19
  94. package/src/modes/prompt-action-autocomplete.ts +11 -1
  95. package/src/modes/rpc/rpc-mode.ts +2 -2
  96. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  97. package/src/prompts/agents/init.md +1 -1
  98. package/src/prompts/system/plan-mode-active.md +1 -1
  99. package/src/prompts/tools/ast-grep.md +1 -1
  100. package/src/prompts/tools/search.md +1 -1
  101. package/src/prompts/tools/task.md +1 -2
  102. package/src/research-plan/index.ts +1 -0
  103. package/src/research-plan/ledger.ts +177 -0
  104. package/src/rlm/artifacts.ts +12 -3
  105. package/src/rlm/index.ts +7 -0
  106. package/src/runtime-mcp/config-writer.ts +46 -0
  107. package/src/session/agent-session.ts +43 -41
  108. package/src/session/session-manager.ts +19 -2
  109. package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
  110. package/src/setup/hermes-setup.ts +1 -1
  111. package/src/skc-runtime/deep-interview-recorder.ts +51 -18
  112. package/src/skc-runtime/deep-interview-runtime.ts +49 -23
  113. package/src/skc-runtime/goal-mode-request.ts +26 -11
  114. package/src/skc-runtime/launch-tmux.ts +68 -15
  115. package/src/skc-runtime/ralplan-runtime.ts +79 -50
  116. package/src/skc-runtime/session-layout.ts +180 -0
  117. package/src/skc-runtime/session-resolution.ts +217 -0
  118. package/src/skc-runtime/state-graph.ts +1 -2
  119. package/src/skc-runtime/state-migrations.ts +1 -0
  120. package/src/skc-runtime/state-runtime.ts +237 -114
  121. package/src/skc-runtime/state-schema.ts +2 -0
  122. package/src/skc-runtime/state-writer.ts +310 -42
  123. package/src/skc-runtime/team-runtime.ts +43 -19
  124. package/src/skc-runtime/tmux-sessions.ts +43 -2
  125. package/src/skc-runtime/ultragoal-guard.ts +45 -2
  126. package/src/skc-runtime/ultragoal-runtime.ts +121 -41
  127. package/src/skc-runtime/workflow-command-ref.ts +1 -2
  128. package/src/skc-runtime/workflow-manifest.ts +1 -2
  129. package/src/skill-state/active-state.ts +116 -129
  130. package/src/skill-state/canonical-skills.ts +4 -0
  131. package/src/skill-state/deep-interview-mutation-guard.ts +238 -111
  132. package/src/skill-state/workflow-hud.ts +4 -2
  133. package/src/skill-state/workflow-state-contract.ts +3 -3
  134. package/src/slash-commands/builtin-registry.ts +8 -4
  135. package/src/system-prompt.ts +11 -9
  136. package/src/task/agents.ts +1 -22
  137. package/src/task/index.ts +1 -41
  138. package/src/task/spawn-gate.ts +1 -38
  139. package/src/task/types.ts +1 -1
  140. package/src/tools/ask.ts +34 -12
  141. package/src/tools/ast-edit.ts +2 -2
  142. package/src/tools/computer.ts +58 -4
  143. package/src/utils/edit-mode.ts +1 -1
  144. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  145. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  146. package/src/prompts/agents/explore.md +0 -58
  147. package/src/prompts/agents/plan.md +0 -49
  148. package/src/prompts/agents/reviewer.md +0 -141
  149. package/src/prompts/agents/task.md +0 -16
  150. package/src/prompts/review-request.md +0 -70
@@ -60,6 +60,7 @@ export const WorkflowStateEnvelopeSchema = z
60
60
  updated_at: z.string().optional(),
61
61
  session_id: z.string().optional(),
62
62
  receipt: WorkflowStateReceiptSchema.optional(),
63
+ state_revision: z.number().optional(),
63
64
  })
64
65
  .passthrough();
65
66
 
@@ -95,6 +96,7 @@ export const RequiredOnWriteEnvelopeSchema = z
95
96
  current_phase: z.string(),
96
97
  active: z.boolean(),
97
98
  receipt: RequiredWorkflowStateReceiptSchema,
99
+ state_revision: z.number().optional(),
98
100
  })
99
101
  .passthrough();
100
102
 
@@ -11,6 +11,13 @@ import {
11
11
  type WorkflowStateMutationOwner,
12
12
  type WorkflowStateReceipt,
13
13
  } from "../skill-state/workflow-state-contract";
14
+ import {
15
+ activeEntryPath as layoutActiveEntryPath,
16
+ activeSnapshotPath as layoutActiveSnapshotPath,
17
+ activeStateDir as layoutActiveStateDir,
18
+ auditPath as layoutAuditPath,
19
+ transactionJournalPath as layoutTransactionJournalPath,
20
+ } from "./session-layout";
14
21
  import { RequiredOnWriteEnvelopeSchema } from "./state-schema";
15
22
 
16
23
  /**
@@ -22,7 +29,7 @@ import { RequiredOnWriteEnvelopeSchema } from "./state-schema";
22
29
  * supplied mutation context. No lockfiles are used; isolation is by atomic rename,
23
30
  * append, O_EXCL creates, conditional deletes, per-entry active-state files,
24
31
  * and derived active-state snapshots.
25
- * Transaction journals are per mutation id under `.skc/state/transactions/`;
32
+ * Transaction journals are per mutation id under the session state transactions directory;
26
33
  * they are recovery evidence only, never global locks or waiters, so stale
27
34
  * journals do not block unrelated state reads or writes.
28
35
  */
@@ -46,10 +53,15 @@ export interface StateWriterReceiptContext {
46
53
  sessionId?: string;
47
54
  mutationId?: string;
48
55
  nowIso?: string;
56
+ verb?: string;
57
+ fromPhase?: string;
58
+ toPhase?: string;
59
+ forced?: boolean;
49
60
  }
50
61
 
51
62
  export interface StateWriterAuditContext {
52
63
  cwd?: string;
64
+ sessionId?: string;
53
65
  category: WriterCategory;
54
66
  verb: string;
55
67
  owner: WorkflowStateMutationOwner;
@@ -78,10 +90,23 @@ export interface WorkflowTransactionJournal {
78
90
  steps: string[];
79
91
  }
80
92
 
93
+ export type StateWritePolicy = "source" | "cache";
94
+
95
+ export interface GuardedStateWriterOptions extends StateWriterOptions {
96
+ policy: StateWritePolicy;
97
+ expectedRevision?: number;
98
+ sourceRevision?: number;
99
+ }
100
+
101
+ export type GuardedWriteResult =
102
+ | { path: string; written: true }
103
+ | { path: string; written: false; reason: "stale-skip" };
104
+
81
105
  export interface StateWriterOptions {
82
106
  cwd?: string;
83
107
  receipt?: StateWriterReceiptContext;
84
108
  audit?: StateWriterAuditContext;
109
+ sourceRevision?: number;
85
110
  /**
86
111
  * Cross-process lock tuning for read-modify-write paths that route through
87
112
  * `withWorkflowStateLock` / `updateJsonAtomic`. Omit for the hardened
@@ -90,6 +115,19 @@ export interface StateWriterOptions {
90
115
  lock?: FileLockOptions;
91
116
  }
92
117
 
118
+ export class StateWriteConflictError extends Error {
119
+ constructor(
120
+ public readonly path: string,
121
+ public readonly expectedRevision: number,
122
+ public readonly persistedRevision: number,
123
+ ) {
124
+ super(
125
+ `state write conflict at ${path}: expected revision ${expectedRevision}, persisted revision ${persistedRevision}`,
126
+ );
127
+ this.name = "StateWriteConflictError";
128
+ }
129
+ }
130
+
93
131
  export interface DeleteIfOwnedOptions extends StateWriterOptions {
94
132
  predicate?: (current: unknown) => boolean | Promise<boolean>;
95
133
  }
@@ -255,32 +293,23 @@ function safeString(value: unknown): string {
255
293
  return typeof value === "string" ? value : "";
256
294
  }
257
295
 
258
- function encodePathSegment(value: string): string {
259
- return encodeURIComponent(value).replaceAll(".", "%2E");
296
+ function requireSessionId(sessionScope: string | ActiveSessionScope | undefined, source: string): string {
297
+ const sessionId = typeof sessionScope === "string" ? sessionScope : sessionScope?.sessionId;
298
+ const normalizedSessionId = safeString(sessionId).trim();
299
+ if (!normalizedSessionId) throw new Error(`a non-empty SKC session id is required (${source})`);
300
+ return normalizedSessionId;
260
301
  }
261
302
 
262
303
  function activeStateDir(cwd: string, sessionScope?: string | ActiveSessionScope): string {
263
- const sessionId = typeof sessionScope === "string" ? sessionScope : sessionScope?.sessionId;
264
- const normalizedSessionId = safeString(sessionId).trim();
265
- const stateDir = path.join(cwd, ".skc", "state");
266
- return normalizedSessionId
267
- ? path.join(stateDir, "sessions", encodePathSegment(normalizedSessionId), "active")
268
- : path.join(stateDir, "active");
304
+ return layoutActiveStateDir(cwd, requireSessionId(sessionScope, "activeStateDir"));
269
305
  }
270
306
 
271
307
  function activeSnapshotPath(cwd: string, sessionScope?: string | ActiveSessionScope): string {
272
- const sessionId = typeof sessionScope === "string" ? sessionScope : sessionScope?.sessionId;
273
- const normalizedSessionId = safeString(sessionId).trim();
274
- const stateDir = path.join(cwd, ".skc", "state");
275
- return normalizedSessionId
276
- ? path.join(stateDir, "sessions", encodePathSegment(normalizedSessionId), "skill-active-state.json")
277
- : path.join(stateDir, "skill-active-state.json");
308
+ return layoutActiveSnapshotPath(cwd, requireSessionId(sessionScope, "activeSnapshotPath"));
278
309
  }
279
310
 
280
311
  function activeEntryPath(cwd: string, sessionScope: string | ActiveSessionScope | undefined, skill: string): string {
281
- const normalizedSkill = safeString(skill).trim();
282
- if (!normalizedSkill) throw new Error("skill is required");
283
- return path.join(activeStateDir(cwd, sessionScope), `${encodePathSegment(normalizedSkill)}.json`);
312
+ return layoutActiveEntryPath(cwd, requireSessionId(sessionScope, "activeEntryPath"), skill);
284
313
  }
285
314
 
286
315
  function activeSubskillKey(entry: ActiveSubskillEntry): string {
@@ -298,8 +327,28 @@ function flattenActiveSubskills(entries: SkillActiveEntry[]): ActiveSubskillEntr
298
327
  return [...deduped.values()];
299
328
  }
300
329
 
330
+ const CANONICAL_PIPELINE_RANK = new Map<string, number>([
331
+ ["deep-interview", 0],
332
+ ["ralplan", 1],
333
+ ["ultragoal", 2],
334
+ ]);
335
+
336
+ function canonicalPipelineRank(skill: string): number | undefined {
337
+ return CANONICAL_PIPELINE_RANK.get(skill);
338
+ }
339
+
340
+ function compareActiveEntryPrimary(a: SkillActiveEntry, b: SkillActiveEntry): number {
341
+ const aRank = canonicalPipelineRank(a.skill);
342
+ const bRank = canonicalPipelineRank(b.skill);
343
+ if (aRank !== undefined || bRank !== undefined) return (bRank ?? -1) - (aRank ?? -1);
344
+ const aTime = Date.parse(safeString(a.updated_at));
345
+ const bTime = Date.parse(safeString(b.updated_at));
346
+ if (Number.isFinite(aTime) || Number.isFinite(bTime)) return (bTime || 0) - (aTime || 0);
347
+ return 0;
348
+ }
349
+
301
350
  function buildActiveSnapshot(entries: SkillActiveEntry[]): SkillActiveState {
302
- const visible = entries.filter(entry => entry.active !== false);
351
+ const visible = entries.filter(entry => entry.active !== false).toSorted(compareActiveEntryPrimary);
303
352
  const primary = visible[0];
304
353
  return {
305
354
  version: 1,
@@ -336,14 +385,68 @@ async function readJsonIfPresent(filePath: string): Promise<unknown | undefined>
336
385
  }
337
386
  }
338
387
 
388
+ // Corrupt-tolerant variant for the guarded writers' revision computation: a prior
389
+ // file that is unparseable has no usable revision, so treat it as absent (revision 0)
390
+ // rather than throwing. This lets an authoritative/forced write overwrite corrupt
391
+ // state and a derived cache write overwrite (not stale-skip) corrupt cache.
392
+ async function readJsonIfPresentTolerant(filePath: string): Promise<unknown | undefined> {
393
+ try {
394
+ return await readJsonIfPresent(filePath);
395
+ } catch {
396
+ return undefined;
397
+ }
398
+ }
399
+
400
+ export function persistedStateRevision(value: unknown): number {
401
+ if (!isPlainObject(value)) return 0;
402
+ const revision = value.state_revision;
403
+ return typeof revision === "number" && Number.isFinite(revision) ? revision : 0;
404
+ }
405
+
406
+ function persistedSourceRevision(value: unknown): number {
407
+ if (!isPlainObject(value)) return 0;
408
+ const revision = value.source_state_revision;
409
+ return typeof revision === "number" && Number.isFinite(revision) ? revision : persistedStateRevision(value);
410
+ }
411
+
412
+ function withoutCandidateRevision(value: unknown): unknown {
413
+ if (!isPlainObject(value)) return value;
414
+ const next = { ...value };
415
+ delete next.state_revision;
416
+ return next;
417
+ }
418
+
419
+ function stampStateRevision(value: unknown, stateRevision: number, sourceRevision?: number): unknown {
420
+ if (!isPlainObject(value)) return value;
421
+ const next = withoutCandidateRevision(value) as Record<string, unknown>;
422
+ return {
423
+ ...next,
424
+ ...(sourceRevision === undefined ? {} : { source_state_revision: sourceRevision }),
425
+ state_revision: stateRevision,
426
+ };
427
+ }
428
+
339
429
  function withWorkflowReceipt(value: unknown, receipt: WorkflowStateReceipt | undefined): unknown {
340
430
  if (!receipt || !value || typeof value !== "object" || Array.isArray(value)) return value;
341
431
  return { ...(value as Record<string, unknown>), receipt };
342
432
  }
343
433
 
434
+ function stampWorkflowEnvelopeRevisionAndChecksum(
435
+ value: unknown,
436
+ filePath: string,
437
+ stateRevision: number,
438
+ sourceRevision: number | undefined,
439
+ options: StateWriterOptions | undefined,
440
+ ): unknown {
441
+ return stampWorkflowEnvelopeChecksum(
442
+ stampStateRevision(withWorkflowReceipt(value, buildReceipt(options)), stateRevision, sourceRevision),
443
+ filePath,
444
+ );
445
+ }
446
+
344
447
  function buildReceipt(options: StateWriterOptions | undefined): WorkflowStateReceipt | undefined {
345
448
  if (!options?.receipt) return undefined;
346
- return buildWorkflowStateReceipt({
449
+ const receipt = buildWorkflowStateReceipt({
347
450
  cwd: path.resolve(options.receipt.cwd ?? options.cwd ?? process.cwd()),
348
451
  skill: options.receipt.skill,
349
452
  owner: options.receipt.owner,
@@ -352,13 +455,18 @@ function buildReceipt(options: StateWriterOptions | undefined): WorkflowStateRec
352
455
  nowIso: options.receipt.nowIso,
353
456
  mutationId: options.receipt.mutationId,
354
457
  });
458
+ receipt.verb = options.receipt.verb;
459
+ receipt.from_phase = options.receipt.fromPhase;
460
+ receipt.to_phase = options.receipt.toPhase;
461
+ receipt.forced = options.receipt.forced;
462
+ return receipt;
355
463
  }
356
464
 
357
465
  async function maybeAudit(mutatedPath: string, options?: StateWriterOptions): Promise<void> {
358
466
  if (!options?.audit) return;
359
467
  const audit = options.audit;
360
468
  const cwd = path.resolve(audit.cwd ?? options.cwd ?? process.cwd());
361
- await appendAuditEntry(cwd, {
469
+ await appendAuditEntry(cwd, options?.audit?.sessionId ?? "", {
362
470
  ts: new Date().toISOString(),
363
471
  skill: audit.skill,
364
472
  category: audit.category,
@@ -385,6 +493,118 @@ async function atomicWrite(filePath: string, content: string): Promise<string> {
385
493
  return filePath;
386
494
  }
387
495
 
496
+ async function writeGuardedResolvedJsonAtomic(
497
+ filePath: string,
498
+ value: unknown,
499
+ options: GuardedStateWriterOptions,
500
+ ): Promise<GuardedWriteResult> {
501
+ return lockResolvedWorkflowTarget(
502
+ filePath,
503
+ async () => {
504
+ const current = await readJsonIfPresentTolerant(filePath);
505
+ const currentRevision = persistedStateRevision(current);
506
+
507
+ if (options.policy === "source") {
508
+ if (options.expectedRevision !== undefined && options.expectedRevision !== currentRevision) {
509
+ throw new StateWriteConflictError(filePath, options.expectedRevision, currentRevision);
510
+ }
511
+ const next = stampStateRevision(withWorkflowReceipt(value, buildReceipt(options)), currentRevision + 1);
512
+ await atomicWrite(filePath, jsonText(next));
513
+ await maybeAudit(filePath, options);
514
+ return { path: filePath, written: true };
515
+ }
516
+
517
+ const incomingSourceRevision =
518
+ options.sourceRevision ?? (isPlainObject(value) ? persistedStateRevision(value) : 0);
519
+ if (current !== undefined && incomingSourceRevision <= persistedSourceRevision(current)) {
520
+ return { path: filePath, written: false, reason: "stale-skip" };
521
+ }
522
+ const next = stampStateRevision(
523
+ withWorkflowReceipt(value, buildReceipt(options)),
524
+ currentRevision + 1,
525
+ incomingSourceRevision,
526
+ );
527
+ await atomicWrite(filePath, jsonText(next));
528
+ await maybeAudit(filePath, options);
529
+ return { path: filePath, written: true };
530
+ },
531
+ options.lock,
532
+ );
533
+ }
534
+
535
+ export async function writeGuardedJsonAtomic(
536
+ targetPath: string,
537
+ value: unknown,
538
+ options: GuardedStateWriterOptions,
539
+ ): Promise<GuardedWriteResult> {
540
+ const filePath = resolveSkcTarget(targetPath, cwdForOptions(options));
541
+ return writeGuardedResolvedJsonAtomic(filePath, value, options);
542
+ }
543
+
544
+ export async function writeGuardedWorkflowEnvelopeAtomic(
545
+ targetPath: string,
546
+ value: unknown,
547
+ options: GuardedStateWriterOptions,
548
+ ): Promise<GuardedWriteResult> {
549
+ const filePath = resolveSkcTarget(targetPath, cwdForOptions(options));
550
+ return lockResolvedWorkflowTarget(
551
+ filePath,
552
+ async () => {
553
+ const current = await readJsonIfPresentTolerant(filePath);
554
+ const currentRevision = persistedStateRevision(current);
555
+
556
+ if (options.policy === "source") {
557
+ if (options.expectedRevision !== undefined && options.expectedRevision !== currentRevision) {
558
+ throw new StateWriteConflictError(filePath, options.expectedRevision, currentRevision);
559
+ }
560
+ const next = stampWorkflowEnvelopeRevisionAndChecksum(
561
+ value,
562
+ filePath,
563
+ currentRevision + 1,
564
+ undefined,
565
+ options,
566
+ );
567
+ const parsed = RequiredOnWriteEnvelopeSchema.safeParse(next);
568
+ if (!parsed.success) {
569
+ throw new Error(
570
+ `Refusing to write invalid workflow state envelope to ${filePath}: ${parsed.error.issues
571
+ .map(issue => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
572
+ .join("; ")}`,
573
+ );
574
+ }
575
+ await atomicWrite(filePath, jsonText(next));
576
+ await maybeAudit(filePath, options);
577
+ return { path: filePath, written: true };
578
+ }
579
+
580
+ const incomingSourceRevision =
581
+ options.sourceRevision ?? (isPlainObject(value) ? persistedStateRevision(value) : 0);
582
+ if (current !== undefined && incomingSourceRevision <= persistedSourceRevision(current)) {
583
+ return { path: filePath, written: false, reason: "stale-skip" };
584
+ }
585
+ const next = stampWorkflowEnvelopeRevisionAndChecksum(
586
+ value,
587
+ filePath,
588
+ currentRevision + 1,
589
+ incomingSourceRevision,
590
+ options,
591
+ );
592
+ const parsed = RequiredOnWriteEnvelopeSchema.safeParse(next);
593
+ if (!parsed.success) {
594
+ throw new Error(
595
+ `Refusing to write invalid workflow state envelope to ${filePath}: ${parsed.error.issues
596
+ .map(issue => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
597
+ .join("; ")}`,
598
+ );
599
+ }
600
+ await atomicWrite(filePath, jsonText(next));
601
+ await maybeAudit(filePath, options);
602
+ return { path: filePath, written: true };
603
+ },
604
+ options.lock,
605
+ );
606
+ }
607
+
388
608
  export async function writeJsonAtomic(
389
609
  targetPath: string,
390
610
  value: unknown,
@@ -429,7 +649,7 @@ async function recordInvalidWorkflowTransition(args: {
429
649
  // internal write skipped a manifest edge.
430
650
  const cwd = path.resolve(options?.audit?.cwd ?? options?.cwd ?? process.cwd());
431
651
  try {
432
- await appendAuditEntry(cwd, {
652
+ await appendAuditEntry(cwd, options?.audit?.sessionId ?? "", {
433
653
  ts: new Date().toISOString(),
434
654
  skill,
435
655
  category: "state",
@@ -736,8 +956,7 @@ export async function removeFileAudited(targetPath: string, options?: StateWrite
736
956
  }
737
957
 
738
958
  /**
739
- * Active entry files under `.skc/state/active/<skill>.json` and
740
- * `.skc/state/sessions/<id>/active/<skill>.json` are authoritative. The
959
+ * Active entry files under `.skc/_session-{id}/state/active/<skill>.json` are authoritative. The
741
960
  * adjacent `skill-active-state.json` file is only a derived cache rebuilt from
742
961
  * those entries, so concurrent snapshot rebuilds can race without losing any
743
962
  * writer's per-skill state.
@@ -750,8 +969,16 @@ export async function writeActiveEntry(
750
969
  options?: StateWriterOptions,
751
970
  ): Promise<string> {
752
971
  const filePath = activeEntryPath(path.resolve(cwd), sessionScope, skill);
753
- await atomicWrite(filePath, jsonText({ ...entry, skill }));
754
- await maybeAudit(filePath, options);
972
+ await writeGuardedResolvedJsonAtomic(
973
+ filePath,
974
+ { ...entry, skill },
975
+ {
976
+ ...options,
977
+ policy: "cache",
978
+ sourceRevision:
979
+ persistedSourceRevision(entry) || persistedSourceRevision(await readJsonIfPresent(filePath)) + 1,
980
+ },
981
+ );
755
982
  return filePath;
756
983
  }
757
984
 
@@ -762,9 +989,24 @@ export async function removeActiveEntry(
762
989
  options?: StateWriterOptions,
763
990
  ): Promise<DeleteResult> {
764
991
  const filePath = activeEntryPath(path.resolve(cwd), sessionScope, skill);
765
- const deleted = await atomicRemove(filePath);
766
- if (deleted) await maybeAudit(filePath, options);
767
- return { path: filePath, deleted };
992
+ return lockResolvedWorkflowTarget(
993
+ filePath,
994
+ async () => {
995
+ const current = await readJsonIfPresent(filePath);
996
+ const incomingSourceRevision = options?.sourceRevision;
997
+ if (
998
+ current !== undefined &&
999
+ incomingSourceRevision !== undefined &&
1000
+ incomingSourceRevision < persistedSourceRevision(current)
1001
+ ) {
1002
+ return { path: filePath, deleted: false };
1003
+ }
1004
+ const deleted = await atomicRemove(filePath);
1005
+ if (deleted) await maybeAudit(filePath, options);
1006
+ return { path: filePath, deleted };
1007
+ },
1008
+ options?.lock,
1009
+ );
768
1010
  }
769
1011
 
770
1012
  export async function readActiveEntries(
@@ -799,8 +1041,14 @@ export async function rebuildActiveSnapshot(
799
1041
  const resolvedCwd = path.resolve(cwd);
800
1042
  const snapshotPath = activeSnapshotPath(resolvedCwd, sessionScope);
801
1043
  const entries = await readActiveEntries(resolvedCwd, sessionScope);
802
- await atomicWrite(snapshotPath, jsonText(buildActiveSnapshot(entries)));
803
- await maybeAudit(snapshotPath, options);
1044
+ await writeGuardedResolvedJsonAtomic(snapshotPath, buildActiveSnapshot(entries), {
1045
+ ...options,
1046
+ policy: "cache",
1047
+ sourceRevision: Math.max(
1048
+ persistedSourceRevision(await readJsonIfPresent(snapshotPath)) + 1,
1049
+ ...entries.map(entry => persistedSourceRevision(entry)),
1050
+ ),
1051
+ });
804
1052
  return snapshotPath;
805
1053
  }
806
1054
 
@@ -905,7 +1153,7 @@ export async function hardPrune(
905
1153
  }
906
1154
  if (options?.audit && removed.length > 0) {
907
1155
  const audit = options.audit;
908
- await appendAuditEntry(path.resolve(audit.cwd ?? options.cwd ?? process.cwd()), {
1156
+ await appendAuditEntry(path.resolve(audit.cwd ?? options.cwd ?? process.cwd()), audit.sessionId ?? "", {
909
1157
  ts: new Date().toISOString(),
910
1158
  skill: audit.skill,
911
1159
  category: audit.category,
@@ -947,26 +1195,41 @@ export async function forceOverwrite(
947
1195
  );
948
1196
  }
949
1197
 
950
- export async function appendAuditEntry(cwd: string, entry: AuditEntry): Promise<string> {
951
- const filePath = resolveSkcTarget(path.join(".skc", "state", "audit.jsonl"), cwd);
1198
+ export async function appendAuditEntry(
1199
+ cwd: string,
1200
+ sessionIdOrEntry: string | AuditEntry,
1201
+ maybeEntry?: AuditEntry,
1202
+ ): Promise<string> {
1203
+ const sessionId =
1204
+ typeof sessionIdOrEntry === "string"
1205
+ ? sessionIdOrEntry.trim()
1206
+ : safeString((sessionIdOrEntry as AuditEntry & { session_id?: unknown }).session_id).trim();
1207
+ if (!sessionId) throw new Error("a non-empty SKC session id is required (appendAuditEntry)");
1208
+ const entry = typeof sessionIdOrEntry === "string" ? maybeEntry : sessionIdOrEntry;
1209
+ if (!entry) throw new Error("audit entry is required");
1210
+ const filePath = resolveSkcTarget(layoutAuditPath(cwd, sessionId), cwd);
952
1211
  await fs.mkdir(path.dirname(filePath), { recursive: true });
953
1212
  await fs.appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf-8");
954
1213
  return filePath;
955
1214
  }
956
1215
 
957
- function transactionJournalPath(cwd: string, mutationId: string): string {
958
- return path.join(path.resolve(cwd), ".skc", "state", "transactions", `${encodePathSegment(mutationId)}.json`);
1216
+ function transactionJournalPath(cwd: string, sessionId: string, mutationId: string): string {
1217
+ return layoutTransactionJournalPath(path.resolve(cwd), sessionId, mutationId);
959
1218
  }
960
1219
 
961
1220
  export async function readWorkflowTransactionJournal(
962
1221
  cwd: string,
1222
+ sessionId: string,
963
1223
  mutationId: string,
964
1224
  ): Promise<WorkflowTransactionJournal | undefined> {
965
- return (await readJsonIfPresent(transactionJournalPath(cwd, mutationId))) as WorkflowTransactionJournal | undefined;
1225
+ return (await readJsonIfPresent(transactionJournalPath(cwd, sessionId, mutationId))) as
1226
+ | WorkflowTransactionJournal
1227
+ | undefined;
966
1228
  }
967
1229
 
968
1230
  export async function beginWorkflowTransactionJournal(input: {
969
1231
  cwd: string;
1232
+ sessionId: string;
970
1233
  mutationId: string;
971
1234
  caller?: CanonicalSkcWorkflowSkill;
972
1235
  callee?: CanonicalSkcWorkflowSkill;
@@ -985,7 +1248,7 @@ export async function beginWorkflowTransactionJournal(input: {
985
1248
  steps: [],
986
1249
  };
987
1250
  try {
988
- return await createJsonNoClobber(transactionJournalPath(input.cwd, input.mutationId), journal, {
1251
+ return await createJsonNoClobber(transactionJournalPath(input.cwd, input.sessionId, input.mutationId), journal, {
989
1252
  cwd: input.cwd,
990
1253
  });
991
1254
  } catch (error) {
@@ -996,17 +1259,22 @@ export async function beginWorkflowTransactionJournal(input: {
996
1259
 
997
1260
  export async function updateWorkflowTransactionJournal(
998
1261
  cwd: string,
1262
+ sessionId: string,
999
1263
  mutationId: string,
1000
1264
  patch: Partial<WorkflowTransactionJournal>,
1001
1265
  ): Promise<string> {
1002
- const filePath = transactionJournalPath(cwd, mutationId);
1266
+ const filePath = transactionJournalPath(cwd, sessionId, mutationId);
1003
1267
  const current = ((await readJsonIfPresent(filePath)) ?? {}) as WorkflowTransactionJournal;
1004
1268
  const next = { ...current, ...patch, updated_at: new Date().toISOString() } as WorkflowTransactionJournal;
1005
1269
  await atomicWrite(filePath, jsonText(next));
1006
1270
  return filePath;
1007
1271
  }
1008
1272
 
1009
- export async function completeWorkflowTransactionJournal(cwd: string, mutationId: string): Promise<void> {
1010
- await updateWorkflowTransactionJournal(cwd, mutationId, { status: "committed" });
1011
- await atomicRemove(transactionJournalPath(cwd, mutationId)).catch(() => false);
1273
+ export async function completeWorkflowTransactionJournal(
1274
+ cwd: string,
1275
+ sessionId: string,
1276
+ mutationId: string,
1277
+ ): Promise<void> {
1278
+ await updateWorkflowTransactionJournal(cwd, sessionId, mutationId, { status: "committed" });
1279
+ await atomicRemove(transactionJournalPath(cwd, sessionId, mutationId)).catch(() => false);
1012
1280
  }
@@ -5,8 +5,9 @@ import type { WorkflowHudSummary } from "../skill-state/active-state";
5
5
  import { buildTeamHudSummary as buildWorkflowTeamHudSummary } from "../skill-state/workflow-hud";
6
6
  import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
7
7
  import type { GcPidProbe, GcRecord } from "./gc-runtime";
8
-
9
8
  import { applySkcTmuxProfile, SKC_TMUX_LAUNCHED_ENV } from "./launch-tmux";
9
+ import { modeStatePath, sessionIdFromDirName, sessionReportsDir, teamStateRoot } from "./session-layout";
10
+ import { resolveSkcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
10
11
  import {
11
12
  AlreadyExistsError,
12
13
  appendJsonl as appendJsonlAudited,
@@ -554,7 +555,14 @@ function stateWriterOptions(filePath: string, category: "state" | "ledger" | "re
554
555
  const marker = `${path.sep}.skc${path.sep}`;
555
556
  const markerIndex = resolved.indexOf(marker);
556
557
  const cwd = markerIndex >= 0 ? resolved.slice(0, markerIndex) : process.cwd();
557
- return { cwd, audit: { category, verb, owner: "skc-runtime" as const } };
558
+ const parts = resolved.split(path.sep);
559
+ const sessionId =
560
+ parts.map(part => sessionIdFromDirName(part)).find((value): value is string => Boolean(value)) ??
561
+ process.env.SKC_SESSION_ID?.trim();
562
+ // Session-scoped audit requires a SKC session. When an explicit env-root override
563
+ // (e.g. SKC_TEAM_STATE_ROOT) is in effect with no resolvable session, omit the audit
564
+ // context entirely so the override write does not fail on a session-scoped audit.
565
+ return sessionId ? { cwd, audit: { category, verb, owner: "skc-runtime" as const, sessionId } } : { cwd };
558
566
  }
559
567
 
560
568
  function sanitizeName(value: string): string {
@@ -668,7 +676,8 @@ function workerIntegrationDedupePath(dir: string, worker: string): string {
668
676
  export function resolveSkcTeamStateRoot(cwd = process.cwd(), env: NodeJS.ProcessEnv = process.env): string {
669
677
  const explicit = env.SKC_TEAM_STATE_ROOT?.trim();
670
678
  if (explicit) return path.resolve(cwd, explicit);
671
- return path.join(cwd, ".skc", "state", "team");
679
+ const session = resolveSkcSessionForWrite(cwd, { envSessionId: env.SKC_SESSION_ID });
680
+ return teamStateRoot(cwd, session.skcSessionId);
672
681
  }
673
682
 
674
683
  async function readJsonFile<T>(filePath: string): Promise<T | null> {
@@ -1170,15 +1179,17 @@ async function writeWorkerLifecycleForConfig(
1170
1179
  return Object.fromEntries(entries.map(entry => [entry.worker, entry]));
1171
1180
  }
1172
1181
 
1173
- function teamModeStatePath(): string {
1174
- return path.join(".skc", "state", "team-state.json");
1182
+ function teamModeStatePath(cwd: string, sessionId: string): string {
1183
+ return modeStatePath(cwd, sessionId, "team");
1175
1184
  }
1176
1185
 
1177
1186
  export async function persistSkcTeamModeStateSummary(snapshot: SkcTeamSnapshot, cwd = process.cwd()): Promise<void> {
1178
1187
  const active = snapshot.phase !== "complete" && snapshot.phase !== "cancelled";
1179
1188
  const updatedAt = now();
1189
+ const sessionId = resolveSkcSessionForWrite(cwd, { envSessionId: process.env.SKC_SESSION_ID }).skcSessionId;
1190
+ const statePath = teamModeStatePath(cwd, sessionId);
1180
1191
  await writeWorkflowEnvelopeAtomic(
1181
- teamModeStatePath(),
1192
+ statePath,
1182
1193
  {
1183
1194
  skill: "team",
1184
1195
  version: WORKFLOW_STATE_VERSION,
@@ -1195,11 +1206,13 @@ export async function persistSkcTeamModeStateSummary(snapshot: SkcTeamSnapshot,
1195
1206
  skill: "team",
1196
1207
  owner: "skc-runtime",
1197
1208
  command: "skc team sync-team-summary",
1209
+ sessionId,
1198
1210
  nowIso: updatedAt,
1199
1211
  },
1200
- audit: { category: "state", verb: "sync-team-summary", owner: "skc-runtime", skill: "team" },
1212
+ audit: { category: "state", verb: "sync-team-summary", owner: "skc-runtime", skill: "team", sessionId },
1201
1213
  },
1202
1214
  );
1215
+ await writeSessionActivityMarker(cwd, sessionId, { writer: "team-runtime", path: statePath });
1203
1216
  }
1204
1217
 
1205
1218
  function appendLivenessRecoveryReason(
@@ -2120,7 +2133,14 @@ function integrationReportPath(dir: string): string {
2120
2133
  return path.join(dir, "integration-report.md");
2121
2134
  }
2122
2135
  function commitHygieneLedgerPath(config: SkcTeamConfig): string {
2123
- return path.join(config.leader_cwd, ".skc", "reports", "team-commit-hygiene", `${config.team_name}.ledger.json`);
2136
+ return path.join(
2137
+ sessionReportsDir(
2138
+ config.leader_cwd,
2139
+ resolveSkcSessionForWrite(config.leader_cwd, { envSessionId: process.env.SKC_SESSION_ID }).skcSessionId,
2140
+ ),
2141
+ "team-commit-hygiene",
2142
+ `${config.team_name}.ledger.json`,
2143
+ );
2124
2144
  }
2125
2145
  function integrationNowState(
2126
2146
  status: SkcTeamIntegrationStatus,
@@ -2186,11 +2206,15 @@ export type SkcWorkerCheckpointClassification =
2186
2206
 
2187
2207
  const UNMERGED_GIT_STATUS_CODES = new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
2188
2208
  const PROTECTED_WORKER_CHECKPOINT_PREFIXES = [
2189
- ".skc/state/",
2190
- ".skc/logs/",
2191
- ".skc/reports/",
2192
- ".skc/tmp/",
2193
- ".skc/ultragoal/",
2209
+ ".skc/_session-*/state/",
2210
+ ".skc/_session-*/logs/",
2211
+ ".skc/_session-*/reports/",
2212
+ ".skc/_session-*/runtime/",
2213
+ ".skc/_session-*/ultragoal/",
2214
+ ".skc/_session-*/plans/",
2215
+ ".skc/_session-*/specs/",
2216
+ ".skc/_session-*/rlm/",
2217
+ ".skc/_session-*/audit/",
2194
2218
  ];
2195
2219
 
2196
2220
  function parsePorcelainStatusFiles(stdout: string): string[] {
@@ -2211,12 +2235,12 @@ export function classifySkcTeamCheckpointFiles(files: string[]): { eligible: str
2211
2235
  const protectedFiles: string[] = [];
2212
2236
  for (const file of files) {
2213
2237
  const normalized = normalizeGitStatusPath(file);
2214
- if (
2215
- PROTECTED_WORKER_CHECKPOINT_PREFIXES.some(
2216
- prefix => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix),
2217
- )
2218
- )
2219
- protectedFiles.push(file);
2238
+ const isProtected = PROTECTED_WORKER_CHECKPOINT_PREFIXES.some(prefix => {
2239
+ if (!prefix.includes("*")) return normalized === prefix.slice(0, -1) || normalized.startsWith(prefix);
2240
+ const [head, tail] = prefix.split("*");
2241
+ return Boolean(head && tail) && normalized.startsWith(head) && normalized.slice(head.length).includes(tail);
2242
+ });
2243
+ if (isProtected) protectedFiles.push(file);
2220
2244
  else eligible.push(file);
2221
2245
  }
2222
2246
  return { eligible, protected: protectedFiles };