@gajae-code/coding-agent 0.11.4 → 0.11.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 (146) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/cli/notify-cli.d.ts +5 -1
  3. package/dist/types/config/settings-schema.d.ts +20 -0
  4. package/dist/types/export/html/template.generated.d.ts +1 -1
  5. package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
  6. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  7. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
  8. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
  9. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
  10. package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
  11. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  12. package/dist/types/harness-control-plane/owner.d.ts +3 -1
  13. package/dist/types/internal-urls/local-protocol.d.ts +18 -0
  14. package/dist/types/main.d.ts +2 -2
  15. package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
  16. package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
  17. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
  18. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  19. package/dist/types/modes/components/tool-transcript-format.d.ts +58 -0
  20. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
  21. package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
  22. package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
  23. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
  24. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
  25. package/dist/types/plan-mode/approved-plan.d.ts +1 -0
  26. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  27. package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
  28. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
  29. package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
  30. package/dist/types/sdk/bus/config-commands.d.ts +8 -0
  31. package/dist/types/sdk/bus/config.d.ts +22 -0
  32. package/dist/types/sdk/bus/index.d.ts +162 -2
  33. package/dist/types/sdk/bus/notification-service.d.ts +71 -8
  34. package/dist/types/sdk/bus/session-control.d.ts +11 -8
  35. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
  36. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
  37. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
  38. package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
  39. package/dist/types/sdk/session.d.ts +16 -1
  40. package/dist/types/secrets/index.d.ts +4 -2
  41. package/dist/types/secrets/obfuscator.d.ts +1 -1
  42. package/dist/types/session/agent-session.d.ts +31 -2
  43. package/dist/types/session/artifacts.d.ts +30 -8
  44. package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
  45. package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
  46. package/dist/types/session/session-manager.d.ts +88 -18
  47. package/dist/types/session/session-storage.d.ts +27 -0
  48. package/dist/types/session/streaming-output.d.ts +4 -0
  49. package/dist/types/task/executor.d.ts +9 -0
  50. package/dist/types/tool-discovery/tool-index.d.ts +2 -0
  51. package/dist/types/tools/ask.d.ts +48 -0
  52. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  53. package/dist/types/tools/index.d.ts +4 -0
  54. package/package.json +9 -9
  55. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  56. package/src/cli/notify-cli.ts +40 -18
  57. package/src/commands/sdk.ts +7 -2
  58. package/src/config/settings-schema.ts +17 -0
  59. package/src/daemon/runtime.ts +2 -2
  60. package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
  61. package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
  62. package/src/defaults/gjc/skills/team/SKILL.md +7 -9
  63. package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
  64. package/src/eval/py/index.ts +2 -2
  65. package/src/export/html/template.generated.ts +1 -1
  66. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  67. package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
  68. package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
  69. package/src/gjc-runtime/deep-interview-state.ts +127 -3
  70. package/src/gjc-runtime/launch-tmux.ts +61 -6
  71. package/src/gjc-runtime/managed-owner-admission.ts +352 -0
  72. package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
  73. package/src/gjc-runtime/state-runtime.ts +38 -12
  74. package/src/gjc-runtime/team-runtime.ts +1 -1
  75. package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
  76. package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  77. package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
  78. package/src/harness-control-plane/owner.ts +59 -3
  79. package/src/hindsight/client.ts +65 -2
  80. package/src/internal-urls/agent-protocol.ts +76 -5
  81. package/src/internal-urls/docs-index.generated.ts +7 -6
  82. package/src/internal-urls/local-protocol.ts +416 -18
  83. package/src/lsp/index.ts +4 -88
  84. package/src/main.ts +37 -13
  85. package/src/modes/acp/acp-agent.ts +8 -0
  86. package/src/modes/components/ansi-display-validator.ts +54 -0
  87. package/src/modes/components/notifications-settings-editor.ts +26 -2
  88. package/src/modes/components/session-observer-overlay.ts +40 -24
  89. package/src/modes/components/settings-selector.ts +3 -1
  90. package/src/modes/components/tool-transcript-format.ts +321 -0
  91. package/src/modes/components/transcript-viewer-overlay.ts +39 -17
  92. package/src/modes/controllers/goal-mode-controller.ts +20 -1
  93. package/src/modes/controllers/plan-mode-controller.ts +2 -0
  94. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  95. package/src/modes/controllers/selector-controller.ts +70 -8
  96. package/src/modes/interactive-mode.ts +1 -0
  97. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
  98. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
  99. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
  100. package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
  101. package/src/plan-mode/approved-plan.ts +3 -1
  102. package/src/prompts/agents/architect.md +1 -0
  103. package/src/prompts/agents/critic.md +1 -0
  104. package/src/prompts/system/custom-system-prompt.md +1 -1
  105. package/src/prompts/system/system-prompt.md +2 -1
  106. package/src/prompts/tools/lsp.md +3 -3
  107. package/src/runtime-mcp/manager.ts +16 -1
  108. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  109. package/src/sdk/broker/session-index.ts +122 -64
  110. package/src/sdk/bus/chat-daemon-cli.ts +72 -10
  111. package/src/sdk/bus/chat-daemon-control.ts +526 -130
  112. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  113. package/src/sdk/bus/config-commands.ts +22 -0
  114. package/src/sdk/bus/config.ts +37 -0
  115. package/src/sdk/bus/index.ts +384 -92
  116. package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
  117. package/src/sdk/bus/notification-service.ts +284 -62
  118. package/src/sdk/bus/recent-activity.ts +14 -5
  119. package/src/sdk/bus/session-control.ts +150 -44
  120. package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
  121. package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
  122. package/src/sdk/bus/telegram-daemon-control.ts +294 -76
  123. package/src/sdk/bus/telegram-daemon.ts +1788 -267
  124. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  125. package/src/sdk/session.ts +155 -27
  126. package/src/secrets/index.ts +53 -10
  127. package/src/secrets/obfuscator.ts +21 -18
  128. package/src/secrets/regex.ts +2 -0
  129. package/src/session/agent-session.ts +495 -253
  130. package/src/session/artifacts.ts +188 -23
  131. package/src/session/context-estimation.ts +7 -2
  132. package/src/session/internal/managed-session-scope.ts +200 -38
  133. package/src/session/internal/managed-session-storage.ts +1152 -46
  134. package/src/session/session-manager.ts +1055 -148
  135. package/src/session/session-storage.ts +109 -11
  136. package/src/session/streaming-output.ts +6 -0
  137. package/src/task/executor.ts +71 -26
  138. package/src/task/index.ts +63 -20
  139. package/src/tool-discovery/tool-index.ts +18 -0
  140. package/src/tools/ask.ts +129 -32
  141. package/src/tools/bash-skill-urls.ts +14 -3
  142. package/src/tools/bash.ts +2 -2
  143. package/src/tools/index.ts +4 -0
  144. package/src/tools/plan-mode-guard.ts +1 -0
  145. package/src/tools/sqlite-reader.ts +3 -2
  146. package/src/tools/subagent.ts +2 -0
@@ -14,13 +14,16 @@ import {
14
14
  import {
15
15
  acquireManagedLock,
16
16
  captureManagedFileNoFollow,
17
+ captureManagedFilePrefixNoFollow,
17
18
  copyManagedFileNoReplace,
18
19
  ensureManagedDirectory,
19
20
  fsyncManagedArtifactTree,
21
+ ManagedSessionDescendantStore,
20
22
  type ManagedStorageLock,
23
+ managedDirectoryRoot,
21
24
  publishManagedFileNoReplace,
22
- publishManagedFileNoReplaceSync,
23
25
  publishManagedTombstone,
26
+ retainManagedDirectoryAuthority,
24
27
  validateManagedArtifactTree,
25
28
  } from "./managed-session-storage";
26
29
 
@@ -41,6 +44,49 @@ export interface ManagedScope {
41
44
  platform: "posix" | "win32";
42
45
  }
43
46
 
47
+ const managedRoots = new WeakMap<ManagedScope, ReturnType<typeof managedDirectoryRoot>>();
48
+ const managedDirectoryIdentities = new WeakMap<ManagedScope, { dev: bigint; ino: bigint }>();
49
+ const managedDirectoryAuthorities = new WeakMap<ManagedScope, native.RecoveryFsRoot | undefined>();
50
+
51
+ export function managedDirectoryAuthorityForScope(scope: ManagedScope): native.RecoveryFsRoot | undefined {
52
+ if (!managedDirectoryAuthorities.has(scope)) throw new Error("Managed session directory authority was not prepared");
53
+ return managedDirectoryAuthorities.get(scope);
54
+ }
55
+
56
+ export function managedDirectoryIdentityForScope(scope: ManagedScope): { dev: bigint; ino: bigint } {
57
+ const identity = managedDirectoryIdentities.get(scope);
58
+ if (!identity) throw new Error("Managed session directory identity was not prepared");
59
+ return identity;
60
+ }
61
+
62
+ function configuredRootPath(scope: ManagedScope): string {
63
+ let candidate = pathIsWithin(scope.agentDir, scope.sessionsRoot) ? scope.agentDir : path.dirname(scope.sessionsRoot);
64
+ for (;;) {
65
+ try {
66
+ const stat = fs.lstatSync(candidate);
67
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`Unsafe configured root: ${candidate}`);
68
+ return fs.realpathSync.native(candidate);
69
+ } catch (error) {
70
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
71
+ const parent = path.dirname(candidate);
72
+ if (parent === candidate) throw new Error("Configured managed root is unavailable.");
73
+ candidate = parent;
74
+ }
75
+ }
76
+ }
77
+
78
+ function scopeRoot(scope: ManagedScope) {
79
+ const retained = managedRoots.get(scope);
80
+ if (retained) return retained;
81
+ const root = managedDirectoryRoot(configuredRootPath(scope));
82
+ managedRoots.set(scope, root);
83
+ return root;
84
+ }
85
+
86
+ export function managedRootForScope(scope: ManagedScope) {
87
+ return scopeRoot(scope);
88
+ }
89
+
44
90
  export type ManagedMigrationPolicy = "copy-retain" | "disabled";
45
91
 
46
92
  export type ManagedScopeErrorCode =
@@ -228,15 +274,11 @@ function isBinding(value: unknown): value is Binding {
228
274
  );
229
275
  }
230
276
 
231
- function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution | undefined {
232
- const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
233
- let raw: string;
277
+ function validateBindingRaw(scope: ManagedScope, raw: string): ManagedScopeResolution | undefined {
234
278
  let parsed: unknown;
235
279
  try {
236
- raw = captureManagedFileNoFollow(bindingPath).bytes.toString("utf8");
237
280
  parsed = JSON.parse(raw);
238
- } catch (error) {
239
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
281
+ } catch {
240
282
  return { kind: "error", code: "binding_invalid", message: "The managed scope binding is invalid JSON." };
241
283
  }
242
284
  if (!isBinding(parsed))
@@ -263,6 +305,18 @@ function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution |
263
305
  return undefined;
264
306
  }
265
307
 
308
+ function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution | undefined {
309
+ const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
310
+ let raw: string;
311
+ try {
312
+ raw = captureManagedFileNoFollow(bindingPath).bytes.toString("utf8");
313
+ } catch (error) {
314
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
315
+ return { kind: "error", code: "binding_invalid", message: "The managed scope binding is invalid JSON." };
316
+ }
317
+ return validateBindingRaw(scope, raw);
318
+ }
319
+
266
320
  export function resolveManagedScope(input: {
267
321
  cwd: string;
268
322
  agentDir: string;
@@ -270,6 +324,23 @@ export function resolveManagedScope(input: {
270
324
  }): ManagedScopeResolution {
271
325
  const identity = identityFor(input.cwd);
272
326
  if (!identity.ok) return nativeFailure(identity.code);
327
+ try {
328
+ if (fs.lstatSync(input.sessionsRoot).isSymbolicLink()) {
329
+ return {
330
+ kind: "error",
331
+ code: "sessions_root_unavailable",
332
+ message: "The sessions root is not a safe directory.",
333
+ };
334
+ }
335
+ } catch (error) {
336
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
337
+ return {
338
+ kind: "error",
339
+ code: "sessions_root_unavailable",
340
+ message: "The sessions root could not be inspected.",
341
+ };
342
+ }
343
+ }
273
344
  const sessionsRoot = canonicalizeTrustedPath(input.sessionsRoot);
274
345
  const agentDir = canonicalizeTrustedPath(input.agentDir);
275
346
  const digest = scopeDigest(identity.platform, identity.canonicalPath);
@@ -390,6 +461,49 @@ function fsyncManagedParent(pathname: string): void {
390
461
  }
391
462
  }
392
463
 
464
+ type CandidatePreflight =
465
+ | { kind: "capture"; identity: { dev: bigint; ino: bigint; size: number; mtimeNs: bigint } }
466
+ | {
467
+ kind: "foreign";
468
+ }
469
+ | {
470
+ kind: "invalid";
471
+ code: string;
472
+ };
473
+
474
+ function preflightCandidate(filePath: string, scope: ManagedScope): CandidatePreflight {
475
+ try {
476
+ const snapshot = captureManagedFilePrefixNoFollow(filePath, HEADER_MAX_BYTES);
477
+ const lineEnd = snapshot.bytes.indexOf(0x0a);
478
+ if (lineEnd < 0) return { kind: "invalid", code: "invalid_header" };
479
+ const value: unknown = JSON.parse(snapshot.bytes.subarray(0, lineEnd).toString("utf8"));
480
+ if (!value || typeof value !== "object" || Array.isArray(value))
481
+ return { kind: "invalid", code: "invalid_header" };
482
+ const header = value as Record<string, unknown>;
483
+ if (header.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string")
484
+ return { kind: "invalid", code: "invalid_header" };
485
+ const candidateIdentity = identityFor(header.cwd);
486
+ if (
487
+ candidateIdentity.ok &&
488
+ (candidateIdentity.platform !== scope.platform || candidateIdentity.canonicalPath !== scope.canonicalCwd)
489
+ )
490
+ return { kind: "foreign" };
491
+ return { kind: "capture", identity: snapshot.identity };
492
+ } catch {
493
+ return { kind: "invalid", code: "unreadable_candidate" };
494
+ }
495
+ }
496
+
497
+ function matchesPreflightIdentity(candidate: ManagedCandidate, preflight: CandidatePreflight): boolean {
498
+ return (
499
+ preflight.kind === "capture" &&
500
+ candidate.identity.dev === preflight.identity.dev &&
501
+ candidate.identity.ino === preflight.identity.ino &&
502
+ candidate.identity.size === preflight.identity.size &&
503
+ candidate.identity.mtimeNs === preflight.identity.mtimeNs
504
+ );
505
+ }
506
+
393
507
  function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): ManagedCandidate | { code: string } {
394
508
  try {
395
509
  const snapshot = captureManagedFileNoFollow(filePath);
@@ -400,7 +514,8 @@ function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): Manage
400
514
  const header = value as Record<string, unknown>;
401
515
  if (header.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string")
402
516
  return { code: "invalid_header" };
403
- if (!identityFor(header.cwd).ok) return { code: "invalid_cwd" };
517
+ const cwdIdentity = identityFor(header.cwd);
518
+ if (!cwdIdentity.ok) return { code: `cwd_${cwdIdentity.code}` };
404
519
  const named = fs.lstatSync(filePath, { bigint: true });
405
520
  if (
406
521
  !named.isFile() ||
@@ -450,10 +565,13 @@ function discoveredLegacyDirectoryNames(scope: ManagedScope): readonly string[]
450
565
  return [...known, ...discovered];
451
566
  }
452
567
 
568
+ type CandidateInspection = ManagedCandidate | { code: string } | { foreign: true };
569
+
453
570
  function listDirectoryCandidates(
454
571
  directory: string,
455
572
  provenance: "v2" | "legacy",
456
- ): readonly (ManagedCandidate | { code: string })[] {
573
+ scope: ManagedScope,
574
+ ): readonly CandidateInspection[] {
457
575
  let directoryStat: fs.Stats;
458
576
  try {
459
577
  directoryStat = fs.lstatSync(directory);
@@ -465,17 +583,26 @@ function listDirectoryCandidates(
465
583
  return fs
466
584
  .readdirSync(directory, { withFileTypes: true })
467
585
  .filter(entry => entry.name.endsWith(".jsonl"))
468
- .map(entry => inspectCandidate(path.join(directory, entry.name), provenance));
586
+ .map(entry => {
587
+ const filePath = path.join(directory, entry.name);
588
+ const preflight = preflightCandidate(filePath, scope);
589
+ if (preflight.kind === "invalid") return { code: preflight.code };
590
+ if (preflight.kind === "foreign") return { foreign: true };
591
+ const candidate = inspectCandidate(filePath, provenance);
592
+ if ("code" in candidate) return candidate;
593
+ return matchesPreflightIdentity(candidate, preflight) ? candidate : { code: "source_changed" };
594
+ });
469
595
  }
470
596
 
471
597
  export async function ensureManagedScope(scope: ManagedScope): Promise<ManagedScopeResolution> {
472
598
  try {
473
- ensureManagedDirectory(scope.sessionsRoot);
474
- ensureManagedDirectory(scope.directoryPath);
599
+ const root = scopeRoot(scope);
600
+ ensureManagedDirectory(scope.sessionsRoot, root);
601
+ ensureManagedDirectory(scope.directoryPath, root);
475
602
  const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
476
603
  const binding = `${JSON.stringify(bindingFor(scope))}\n`;
477
604
  try {
478
- await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding));
605
+ await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root);
479
606
  } catch (error) {
480
607
  if ((error as Error).message !== "destination_conflict") throw error;
481
608
  }
@@ -492,24 +619,46 @@ export async function ensureManagedScope(scope: ManagedScope): Promise<ManagedSc
492
619
  /** Synchronously create and validate the v2 binding before a default session writer exists. */
493
620
  export function prepareManagedSessionScopeForWriteSync(scope: ManagedScope): ManagedScopeResolution {
494
621
  try {
495
- ensureManagedDirectory(scope.sessionsRoot);
496
- ensureManagedDirectory(scope.directoryPath);
497
- const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
622
+ const root = scopeRoot(scope);
623
+ ensureManagedDirectory(scope.sessionsRoot, root);
624
+ ensureManagedDirectory(scope.directoryPath, root);
625
+ const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
626
+ if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
627
+ throw new Error("Managed session directory changed");
628
+ const retainedAuthority = retainManagedDirectoryAuthority(root, scope.directoryPath, {
629
+ dev: preparedDirectory.dev,
630
+ ino: preparedDirectory.ino,
631
+ });
632
+ const store = new ManagedSessionDescendantStore(
633
+ root,
634
+ scope.directoryPath,
635
+ retainedAuthority ? { authority: retainedAuthority, authorityBaseDir: scope.directoryPath } : undefined,
636
+ );
637
+ const binding = new TextEncoder().encode(`${JSON.stringify(bindingFor(scope))}\n`);
498
638
  try {
499
- publishManagedFileNoReplaceSync(
500
- bindingPath,
501
- new TextEncoder().encode(`${JSON.stringify(bindingFor(scope))}\n`),
502
- );
639
+ store.publishNoReplaceSync(MANAGED_SESSION_BINDING_FILE, binding);
503
640
  } catch (error) {
504
641
  if ((error as Error).message !== "destination_conflict") throw error;
505
642
  }
506
- const validated = validateExistingBinding(scope);
643
+ const capturedBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
644
+ if (!capturedBinding) throw new Error("Managed scope binding is unavailable");
645
+ const validated = validateBindingRaw(scope, capturedBinding.bytes.toString("utf8"));
646
+ managedDirectoryAuthorities.set(scope, retainedAuthority);
647
+ const directoryStat = fs.lstatSync(scope.directoryPath, { bigint: true });
648
+ if (
649
+ !directoryStat.isDirectory() ||
650
+ directoryStat.isSymbolicLink() ||
651
+ directoryStat.dev !== preparedDirectory.dev ||
652
+ directoryStat.ino !== preparedDirectory.ino
653
+ )
654
+ throw new Error("Managed session directory changed");
655
+ managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
507
656
  if (validated) return validated;
508
657
  const internal = managedInternalDirectory(scope);
509
- ensureManagedDirectory(internal);
510
- ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY));
511
- ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY));
512
- ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY));
658
+ ensureManagedDirectory(internal, root);
659
+ ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root);
660
+ ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root);
661
+ ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root);
513
662
  return { kind: "resolved", scope };
514
663
  } catch (error) {
515
664
  return {
@@ -544,14 +693,18 @@ export function listManagedCandidates(scope: ManagedScope): ManagedCandidateList
544
693
  ];
545
694
  const seen = new Set<string>();
546
695
  for (const directory of directories) {
547
- for (const candidate of listDirectoryCandidates(directory.path, directory.provenance)) {
696
+ for (const candidate of listDirectoryCandidates(directory.path, directory.provenance, scope)) {
548
697
  if ("code" in candidate) {
549
698
  invalid.push({ code: candidate.code });
550
699
  continue;
551
700
  }
701
+ if ("foreign" in candidate) {
702
+ foreignCount++;
703
+ continue;
704
+ }
552
705
  const candidateIdentity = identityFor(candidate.cwd);
553
706
  if (!candidateIdentity.ok) {
554
- invalid.push({ code: "invalid_cwd" });
707
+ invalid.push({ code: `cwd_${candidateIdentity.code}` });
555
708
  continue;
556
709
  }
557
710
  if (
@@ -1088,6 +1241,7 @@ function artifactTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot | und
1088
1241
  typeof item.ino === "string" &&
1089
1242
  typeof item.size === "string" &&
1090
1243
  typeof item.mtimeNs === "string" &&
1244
+ typeof item.ctimeNs === "string" &&
1091
1245
  (item.sha256 === undefined || typeof item.sha256 === "string")
1092
1246
  );
1093
1247
  })
@@ -1626,12 +1780,14 @@ function restoreDetachedArtifactRoot(detached: DetachedArtifactRoot): void {
1626
1780
  }
1627
1781
 
1628
1782
  async function copyArtifacts(
1783
+ scope: ManagedScope,
1629
1784
  sourceTranscript: string,
1630
1785
  destinationTranscript: string,
1631
1786
  manifest: readonly ArtifactManifestEntry[],
1632
1787
  lock: ManagedStorageLock,
1633
1788
  sourceRootOverride?: string,
1634
1789
  ): Promise<void> {
1790
+ const root = scopeRoot(scope);
1635
1791
  if (manifest.length === 0) return;
1636
1792
  const sourceRoot = sourceRootOverride ?? sourceTranscript.slice(0, -6);
1637
1793
  if (!manifestMatches(sourceTranscript, manifest, sourceRoot)) throw new Error("source_changed");
@@ -1641,8 +1797,8 @@ async function copyArtifacts(
1641
1797
  const source = path.join(sourceRoot, entry.path);
1642
1798
  const destination = path.join(destinationRoot, entry.path);
1643
1799
  if (entry.kind === "directory") {
1644
- if (entry.path === "") ensureManagedDirectory(destinationRoot);
1645
- else ensureManagedDirectory(destination);
1800
+ if (entry.path === "") ensureManagedDirectory(destinationRoot, root);
1801
+ else ensureManagedDirectory(destination, root);
1646
1802
  continue;
1647
1803
  }
1648
1804
  const snapshot = captureManagedFileNoFollow(source);
@@ -1652,7 +1808,7 @@ async function copyArtifacts(
1652
1808
  )
1653
1809
  throw new Error("source_changed");
1654
1810
  try {
1655
- await copyManagedFileNoReplace(source, destination, snapshot);
1811
+ await copyManagedFileNoReplace(source, destination, snapshot, root);
1656
1812
  } catch (error) {
1657
1813
  if ((error as Error).message !== "destination_conflict") throw error;
1658
1814
  }
@@ -1769,6 +1925,7 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
1769
1925
  lock = await acquireManagedLock(
1770
1926
  path.join(managedInternalDirectory(scope), MANAGED_LOCKS_DIRECTORY),
1771
1927
  path.basename(tombstone, ".json"),
1928
+ scopeRoot(scope),
1772
1929
  );
1773
1930
  const lockedTargets = retiredTargets(scope, tombstone);
1774
1931
  if (!lockedTargets) continue;
@@ -1880,10 +2037,11 @@ export async function prepareManagedSessionScopeForWrite(scope: ManagedScope): P
1880
2037
  if (prepared.kind === "error") return prepared;
1881
2038
  try {
1882
2039
  const internal = managedInternalDirectory(scope);
1883
- ensureManagedDirectory(internal);
1884
- ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY));
1885
- ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY));
1886
- ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY));
2040
+ const root = scopeRoot(scope);
2041
+ ensureManagedDirectory(internal, root);
2042
+ ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root);
2043
+ ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root);
2044
+ ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root);
1887
2045
  await reconcileManagedTombstones(scope);
1888
2046
  return { kind: "resolved", scope };
1889
2047
  } catch (error) {
@@ -1934,7 +2092,7 @@ export async function openManagedCandidateForWrite(
1934
2092
  let lock: ManagedStorageLock | undefined;
1935
2093
  let detachedArtifacts: DetachedArtifactRoot | undefined;
1936
2094
  try {
1937
- lock = await acquireManagedLock(path.join(internal, MANAGED_LOCKS_DIRECTORY), operation);
2095
+ lock = await acquireManagedLock(path.join(internal, MANAGED_LOCKS_DIRECTORY), operation, scopeRoot(scope));
1938
2096
  const afterLock = validateCandidateForScope(scope, current);
1939
2097
  if (!afterLock)
1940
2098
  return { kind: "error", code: "source_changed", message: "The legacy candidate changed during migration." };
@@ -2000,11 +2158,11 @@ export async function openManagedCandidateForWrite(
2000
2158
  if (artifactPlan && fs.existsSync(artifactPlan.detachedPath)) throw new Error("destination_conflict");
2001
2159
  detachedArtifacts = artifactPlan ? detachArtifactRootForMigration(artifactPlan) : undefined;
2002
2160
  manifest = detachedArtifacts ? artifactManifestFromSnapshot(detachedArtifacts.tree) : [];
2003
- await copyArtifacts(afterLock.path, destination, manifest, lock, detachedArtifacts?.detachedPath);
2161
+ await copyArtifacts(scope, afterLock.path, destination, manifest, lock, detachedArtifacts?.detachedPath);
2004
2162
  if (!existing) {
2005
2163
  try {
2006
2164
  lock.assertOwned();
2007
- await copyManagedFileNoReplace(afterLock.path, destination, sourceSnapshot);
2165
+ await copyManagedFileNoReplace(afterLock.path, destination, sourceSnapshot, scopeRoot(scope));
2008
2166
  } catch (error) {
2009
2167
  if ((error as Error).message !== "destination_conflict") throw error;
2010
2168
  }
@@ -2101,7 +2259,11 @@ export async function deleteManagedSessionCandidate(
2101
2259
  const operation = path.basename(tombstone, ".json");
2102
2260
  let lock: ManagedStorageLock | undefined;
2103
2261
  try {
2104
- lock = await acquireManagedLock(path.join(managedInternalDirectory(scope), MANAGED_LOCKS_DIRECTORY), operation);
2262
+ lock = await acquireManagedLock(
2263
+ path.join(managedInternalDirectory(scope), MANAGED_LOCKS_DIRECTORY),
2264
+ operation,
2265
+ scopeRoot(scope),
2266
+ );
2105
2267
  let targets = retiredTargets(scope, tombstone);
2106
2268
  if (!targets) {
2107
2269
  if (!current) throw new Error("source_changed");