@gajae-code/coding-agent 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +73 -1
  3. package/dist/types/cli/migrate-cli.d.ts +20 -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/settings-schema.d.ts +27 -0
  7. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
  8. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
  9. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  10. package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
  11. package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
  12. package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
  13. package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
  14. package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +36 -7
  16. package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
  17. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +7 -4
  18. package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
  19. package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
  20. package/dist/types/harness-control-plane/storage.d.ts +2 -1
  21. package/dist/types/hooks/skill-state.d.ts +12 -4
  22. package/dist/types/migrate/action-planner.d.ts +11 -0
  23. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  24. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  25. package/dist/types/migrate/adapters/index.d.ts +45 -0
  26. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  27. package/dist/types/migrate/executor.d.ts +2 -0
  28. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  29. package/dist/types/migrate/report.d.ts +18 -0
  30. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  31. package/dist/types/migrate/types.d.ts +126 -0
  32. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  33. package/dist/types/modes/components/welcome.d.ts +3 -1
  34. package/dist/types/modes/interactive-mode.d.ts +3 -0
  35. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  36. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  37. package/dist/types/research-plan/index.d.ts +1 -0
  38. package/dist/types/research-plan/ledger.d.ts +33 -0
  39. package/dist/types/rlm/artifacts.d.ts +1 -1
  40. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  41. package/dist/types/skill-state/active-state.d.ts +6 -11
  42. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  43. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  44. package/dist/types/task/spawn-gate.d.ts +1 -10
  45. package/package.json +7 -7
  46. package/src/cli/migrate-cli.ts +106 -0
  47. package/src/cli/setup-cli.ts +14 -1
  48. package/src/cli.ts +1 -0
  49. package/src/commands/deep-interview.ts +2 -2
  50. package/src/commands/launch.ts +1 -1
  51. package/src/commands/migrate.ts +46 -0
  52. package/src/commands/state.ts +2 -1
  53. package/src/commands/team.ts +7 -3
  54. package/src/config/model-registry.ts +9 -2
  55. package/src/config/model-resolver.ts +13 -2
  56. package/src/config/settings-schema.ts +17 -0
  57. package/src/coordinator-mcp/policy.ts +10 -2
  58. package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
  59. package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
  60. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  61. package/src/defaults/gjc/skills/team/SKILL.md +51 -47
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +17 -13
  63. package/src/exec/bash-executor.ts +3 -1
  64. package/src/extensibility/custom-commands/loader.ts +0 -7
  65. package/src/extensibility/gjc-plugins/injection.ts +23 -4
  66. package/src/extensibility/gjc-plugins/state.ts +16 -1
  67. package/src/gjc-runtime/deep-interview-recorder.ts +43 -18
  68. package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
  69. package/src/gjc-runtime/goal-mode-request.ts +26 -11
  70. package/src/gjc-runtime/launch-tmux.ts +68 -15
  71. package/src/gjc-runtime/ralplan-runtime.ts +79 -50
  72. package/src/gjc-runtime/session-layout.ts +180 -0
  73. package/src/gjc-runtime/session-resolution.ts +217 -0
  74. package/src/gjc-runtime/state-graph.ts +1 -2
  75. package/src/gjc-runtime/state-migrations.ts +1 -0
  76. package/src/gjc-runtime/state-runtime.ts +230 -121
  77. package/src/gjc-runtime/state-schema.ts +2 -0
  78. package/src/gjc-runtime/state-writer.ts +289 -41
  79. package/src/gjc-runtime/team-runtime.ts +43 -19
  80. package/src/gjc-runtime/tmux-sessions.ts +43 -2
  81. package/src/gjc-runtime/ultragoal-guard.ts +45 -2
  82. package/src/gjc-runtime/ultragoal-runtime.ts +121 -41
  83. package/src/gjc-runtime/workflow-command-ref.ts +1 -2
  84. package/src/gjc-runtime/workflow-manifest.ts +1 -2
  85. package/src/harness-control-plane/storage.ts +14 -4
  86. package/src/hooks/native-skill-hook.ts +38 -12
  87. package/src/hooks/skill-state.ts +178 -83
  88. package/src/internal-urls/docs-index.generated.ts +9 -6
  89. package/src/migrate/action-planner.ts +318 -0
  90. package/src/migrate/adapters/claude-code.ts +39 -0
  91. package/src/migrate/adapters/codex.ts +70 -0
  92. package/src/migrate/adapters/index.ts +277 -0
  93. package/src/migrate/adapters/opencode.ts +52 -0
  94. package/src/migrate/executor.ts +81 -0
  95. package/src/migrate/mcp-mapper.ts +152 -0
  96. package/src/migrate/report.ts +104 -0
  97. package/src/migrate/skill-normalizer.ts +80 -0
  98. package/src/migrate/types.ts +163 -0
  99. package/src/modes/bridge/bridge-mode.ts +2 -2
  100. package/src/modes/components/custom-editor.ts +30 -20
  101. package/src/modes/components/welcome.ts +42 -9
  102. package/src/modes/controllers/input-controller.ts +21 -3
  103. package/src/modes/interactive-mode.ts +22 -1
  104. package/src/modes/prompt-action-autocomplete.ts +11 -1
  105. package/src/modes/rpc/rpc-mode.ts +2 -2
  106. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  107. package/src/prompts/agents/init.md +1 -1
  108. package/src/prompts/system/plan-mode-active.md +1 -1
  109. package/src/prompts/tools/ast-grep.md +1 -1
  110. package/src/prompts/tools/search.md +1 -1
  111. package/src/prompts/tools/task.md +1 -2
  112. package/src/research-plan/index.ts +1 -0
  113. package/src/research-plan/ledger.ts +177 -0
  114. package/src/rlm/artifacts.ts +12 -3
  115. package/src/rlm/index.ts +7 -0
  116. package/src/runtime-mcp/config-writer.ts +46 -0
  117. package/src/session/agent-session.ts +15 -21
  118. package/src/session/session-manager.ts +19 -2
  119. package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
  120. package/src/setup/hermes-setup.ts +1 -1
  121. package/src/skill-state/active-state.ts +72 -108
  122. package/src/skill-state/canonical-skills.ts +4 -0
  123. package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
  124. package/src/skill-state/workflow-hud.ts +4 -2
  125. package/src/skill-state/workflow-state-contract.ts +3 -3
  126. package/src/slash-commands/builtin-registry.ts +8 -4
  127. package/src/system-prompt.ts +11 -9
  128. package/src/task/agents.ts +1 -22
  129. package/src/task/index.ts +1 -41
  130. package/src/task/spawn-gate.ts +1 -38
  131. package/src/task/types.ts +1 -1
  132. package/src/tools/ask.ts +34 -12
  133. package/src/tools/computer.ts +58 -4
  134. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  135. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  136. package/src/prompts/agents/explore.md +0 -58
  137. package/src/prompts/agents/plan.md +0 -49
  138. package/src/prompts/agents/reviewer.md +0 -141
  139. package/src/prompts/agents/task.md +0 -16
  140. 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 `.gjc/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 GJC 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, ".gjc", "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, ".gjc", "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 {
@@ -356,14 +385,68 @@ async function readJsonIfPresent(filePath: string): Promise<unknown | undefined>
356
385
  }
357
386
  }
358
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
+
359
429
  function withWorkflowReceipt(value: unknown, receipt: WorkflowStateReceipt | undefined): unknown {
360
430
  if (!receipt || !value || typeof value !== "object" || Array.isArray(value)) return value;
361
431
  return { ...(value as Record<string, unknown>), receipt };
362
432
  }
363
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
+
364
447
  function buildReceipt(options: StateWriterOptions | undefined): WorkflowStateReceipt | undefined {
365
448
  if (!options?.receipt) return undefined;
366
- return buildWorkflowStateReceipt({
449
+ const receipt = buildWorkflowStateReceipt({
367
450
  cwd: path.resolve(options.receipt.cwd ?? options.cwd ?? process.cwd()),
368
451
  skill: options.receipt.skill,
369
452
  owner: options.receipt.owner,
@@ -372,13 +455,18 @@ function buildReceipt(options: StateWriterOptions | undefined): WorkflowStateRec
372
455
  nowIso: options.receipt.nowIso,
373
456
  mutationId: options.receipt.mutationId,
374
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;
375
463
  }
376
464
 
377
465
  async function maybeAudit(mutatedPath: string, options?: StateWriterOptions): Promise<void> {
378
466
  if (!options?.audit) return;
379
467
  const audit = options.audit;
380
468
  const cwd = path.resolve(audit.cwd ?? options.cwd ?? process.cwd());
381
- await appendAuditEntry(cwd, {
469
+ await appendAuditEntry(cwd, options?.audit?.sessionId ?? "", {
382
470
  ts: new Date().toISOString(),
383
471
  skill: audit.skill,
384
472
  category: audit.category,
@@ -405,6 +493,118 @@ async function atomicWrite(filePath: string, content: string): Promise<string> {
405
493
  return filePath;
406
494
  }
407
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 = resolveGjcTarget(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 = resolveGjcTarget(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
+
408
608
  export async function writeJsonAtomic(
409
609
  targetPath: string,
410
610
  value: unknown,
@@ -449,7 +649,7 @@ async function recordInvalidWorkflowTransition(args: {
449
649
  // internal write skipped a manifest edge.
450
650
  const cwd = path.resolve(options?.audit?.cwd ?? options?.cwd ?? process.cwd());
451
651
  try {
452
- await appendAuditEntry(cwd, {
652
+ await appendAuditEntry(cwd, options?.audit?.sessionId ?? "", {
453
653
  ts: new Date().toISOString(),
454
654
  skill,
455
655
  category: "state",
@@ -756,8 +956,7 @@ export async function removeFileAudited(targetPath: string, options?: StateWrite
756
956
  }
757
957
 
758
958
  /**
759
- * Active entry files under `.gjc/state/active/<skill>.json` and
760
- * `.gjc/state/sessions/<id>/active/<skill>.json` are authoritative. The
959
+ * Active entry files under `.gjc/_session-{id}/state/active/<skill>.json` are authoritative. The
761
960
  * adjacent `skill-active-state.json` file is only a derived cache rebuilt from
762
961
  * those entries, so concurrent snapshot rebuilds can race without losing any
763
962
  * writer's per-skill state.
@@ -770,8 +969,16 @@ export async function writeActiveEntry(
770
969
  options?: StateWriterOptions,
771
970
  ): Promise<string> {
772
971
  const filePath = activeEntryPath(path.resolve(cwd), sessionScope, skill);
773
- await atomicWrite(filePath, jsonText({ ...entry, skill }));
774
- 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
+ );
775
982
  return filePath;
776
983
  }
777
984
 
@@ -782,9 +989,24 @@ export async function removeActiveEntry(
782
989
  options?: StateWriterOptions,
783
990
  ): Promise<DeleteResult> {
784
991
  const filePath = activeEntryPath(path.resolve(cwd), sessionScope, skill);
785
- const deleted = await atomicRemove(filePath);
786
- if (deleted) await maybeAudit(filePath, options);
787
- 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
+ );
788
1010
  }
789
1011
 
790
1012
  export async function readActiveEntries(
@@ -819,8 +1041,14 @@ export async function rebuildActiveSnapshot(
819
1041
  const resolvedCwd = path.resolve(cwd);
820
1042
  const snapshotPath = activeSnapshotPath(resolvedCwd, sessionScope);
821
1043
  const entries = await readActiveEntries(resolvedCwd, sessionScope);
822
- await atomicWrite(snapshotPath, jsonText(buildActiveSnapshot(entries)));
823
- 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
+ });
824
1052
  return snapshotPath;
825
1053
  }
826
1054
 
@@ -925,7 +1153,7 @@ export async function hardPrune(
925
1153
  }
926
1154
  if (options?.audit && removed.length > 0) {
927
1155
  const audit = options.audit;
928
- await appendAuditEntry(path.resolve(audit.cwd ?? options.cwd ?? process.cwd()), {
1156
+ await appendAuditEntry(path.resolve(audit.cwd ?? options.cwd ?? process.cwd()), audit.sessionId ?? "", {
929
1157
  ts: new Date().toISOString(),
930
1158
  skill: audit.skill,
931
1159
  category: audit.category,
@@ -967,26 +1195,41 @@ export async function forceOverwrite(
967
1195
  );
968
1196
  }
969
1197
 
970
- export async function appendAuditEntry(cwd: string, entry: AuditEntry): Promise<string> {
971
- const filePath = resolveGjcTarget(path.join(".gjc", "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 GJC 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 = resolveGjcTarget(layoutAuditPath(cwd, sessionId), cwd);
972
1211
  await fs.mkdir(path.dirname(filePath), { recursive: true });
973
1212
  await fs.appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf-8");
974
1213
  return filePath;
975
1214
  }
976
1215
 
977
- function transactionJournalPath(cwd: string, mutationId: string): string {
978
- return path.join(path.resolve(cwd), ".gjc", "state", "transactions", `${encodePathSegment(mutationId)}.json`);
1216
+ function transactionJournalPath(cwd: string, sessionId: string, mutationId: string): string {
1217
+ return layoutTransactionJournalPath(path.resolve(cwd), sessionId, mutationId);
979
1218
  }
980
1219
 
981
1220
  export async function readWorkflowTransactionJournal(
982
1221
  cwd: string,
1222
+ sessionId: string,
983
1223
  mutationId: string,
984
1224
  ): Promise<WorkflowTransactionJournal | undefined> {
985
- return (await readJsonIfPresent(transactionJournalPath(cwd, mutationId))) as WorkflowTransactionJournal | undefined;
1225
+ return (await readJsonIfPresent(transactionJournalPath(cwd, sessionId, mutationId))) as
1226
+ | WorkflowTransactionJournal
1227
+ | undefined;
986
1228
  }
987
1229
 
988
1230
  export async function beginWorkflowTransactionJournal(input: {
989
1231
  cwd: string;
1232
+ sessionId: string;
990
1233
  mutationId: string;
991
1234
  caller?: CanonicalGjcWorkflowSkill;
992
1235
  callee?: CanonicalGjcWorkflowSkill;
@@ -1005,7 +1248,7 @@ export async function beginWorkflowTransactionJournal(input: {
1005
1248
  steps: [],
1006
1249
  };
1007
1250
  try {
1008
- return await createJsonNoClobber(transactionJournalPath(input.cwd, input.mutationId), journal, {
1251
+ return await createJsonNoClobber(transactionJournalPath(input.cwd, input.sessionId, input.mutationId), journal, {
1009
1252
  cwd: input.cwd,
1010
1253
  });
1011
1254
  } catch (error) {
@@ -1016,17 +1259,22 @@ export async function beginWorkflowTransactionJournal(input: {
1016
1259
 
1017
1260
  export async function updateWorkflowTransactionJournal(
1018
1261
  cwd: string,
1262
+ sessionId: string,
1019
1263
  mutationId: string,
1020
1264
  patch: Partial<WorkflowTransactionJournal>,
1021
1265
  ): Promise<string> {
1022
- const filePath = transactionJournalPath(cwd, mutationId);
1266
+ const filePath = transactionJournalPath(cwd, sessionId, mutationId);
1023
1267
  const current = ((await readJsonIfPresent(filePath)) ?? {}) as WorkflowTransactionJournal;
1024
1268
  const next = { ...current, ...patch, updated_at: new Date().toISOString() } as WorkflowTransactionJournal;
1025
1269
  await atomicWrite(filePath, jsonText(next));
1026
1270
  return filePath;
1027
1271
  }
1028
1272
 
1029
- export async function completeWorkflowTransactionJournal(cwd: string, mutationId: string): Promise<void> {
1030
- await updateWorkflowTransactionJournal(cwd, mutationId, { status: "committed" });
1031
- 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);
1032
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 { applyGjcTmuxProfile, GJC_TMUX_LAUNCHED_ENV } from "./launch-tmux";
9
+ import { modeStatePath, sessionIdFromDirName, sessionReportsDir, teamStateRoot } from "./session-layout";
10
+ import { resolveGjcSessionForWrite, 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}.gjc${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: "gjc-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.GJC_SESSION_ID?.trim();
562
+ // Session-scoped audit requires a GJC session. When an explicit env-root override
563
+ // (e.g. GJC_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: "gjc-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 resolveGjcTeamStateRoot(cwd = process.cwd(), env: NodeJS.ProcessEnv = process.env): string {
669
677
  const explicit = env.GJC_TEAM_STATE_ROOT?.trim();
670
678
  if (explicit) return path.resolve(cwd, explicit);
671
- return path.join(cwd, ".gjc", "state", "team");
679
+ const session = resolveGjcSessionForWrite(cwd, { envSessionId: env.GJC_SESSION_ID });
680
+ return teamStateRoot(cwd, session.gjcSessionId);
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(".gjc", "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 persistGjcTeamModeStateSummary(snapshot: GjcTeamSnapshot, cwd = process.cwd()): Promise<void> {
1178
1187
  const active = snapshot.phase !== "complete" && snapshot.phase !== "cancelled";
1179
1188
  const updatedAt = now();
1189
+ const sessionId = resolveGjcSessionForWrite(cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId;
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 persistGjcTeamModeStateSummary(snapshot: GjcTeamSnapshot,
1195
1206
  skill: "team",
1196
1207
  owner: "gjc-runtime",
1197
1208
  command: "gjc team sync-team-summary",
1209
+ sessionId,
1198
1210
  nowIso: updatedAt,
1199
1211
  },
1200
- audit: { category: "state", verb: "sync-team-summary", owner: "gjc-runtime", skill: "team" },
1212
+ audit: { category: "state", verb: "sync-team-summary", owner: "gjc-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: GjcTeamConfig): string {
2123
- return path.join(config.leader_cwd, ".gjc", "reports", "team-commit-hygiene", `${config.team_name}.ledger.json`);
2136
+ return path.join(
2137
+ sessionReportsDir(
2138
+ config.leader_cwd,
2139
+ resolveGjcSessionForWrite(config.leader_cwd, { envSessionId: process.env.GJC_SESSION_ID }).gjcSessionId,
2140
+ ),
2141
+ "team-commit-hygiene",
2142
+ `${config.team_name}.ledger.json`,
2143
+ );
2124
2144
  }
2125
2145
  function integrationNowState(
2126
2146
  status: GjcTeamIntegrationStatus,
@@ -2186,11 +2206,15 @@ export type GjcWorkerCheckpointClassification =
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
- ".gjc/state/",
2190
- ".gjc/logs/",
2191
- ".gjc/reports/",
2192
- ".gjc/tmp/",
2193
- ".gjc/ultragoal/",
2209
+ ".gjc/_session-*/state/",
2210
+ ".gjc/_session-*/logs/",
2211
+ ".gjc/_session-*/reports/",
2212
+ ".gjc/_session-*/runtime/",
2213
+ ".gjc/_session-*/ultragoal/",
2214
+ ".gjc/_session-*/plans/",
2215
+ ".gjc/_session-*/specs/",
2216
+ ".gjc/_session-*/rlm/",
2217
+ ".gjc/_session-*/audit/",
2194
2218
  ];
2195
2219
 
2196
2220
  function parsePorcelainStatusFiles(stdout: string): string[] {
@@ -2211,12 +2235,12 @@ export function classifyGjcTeamCheckpointFiles(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 };