@sublang/playbook 9.0.0 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/docs/cli.md +109 -21
  3. package/docs/configuration.md +89 -40
  4. package/docs/embedding.md +126 -12
  5. package/package.json +14 -3
  6. package/reference/sdlc/captain.md +14 -10
  7. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
  8. package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
  9. package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
  10. package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
  11. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
  12. package/reference/sdlc/captain.playbook/captain.playbook.js +140 -10
  13. package/reference/sdlc/captain.playbook/captain.playbook.ts +188 -16
  14. package/reference/sdlc/code.md +35 -16
  15. package/reference/sdlc/code.playbook/bin/interactive-session.js +228 -23
  16. package/reference/sdlc/code.playbook/bin/launch-config.js +611 -221
  17. package/reference/sdlc/code.playbook/bin/playbook.js +304 -178
  18. package/reference/sdlc/code.playbook/bin/replay-observer.js +221 -0
  19. package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
  20. package/reference/sdlc/code.playbook/bin/run.js +669 -215
  21. package/reference/sdlc/code.playbook/bin/session-store.js +4546 -502
  22. package/reference/sdlc/code.playbook/code.fsm.d.ts +7 -0
  23. package/reference/sdlc/code.playbook/code.fsm.js +74 -25
  24. package/reference/sdlc/code.playbook/code.fsm.ts +83 -29
  25. package/reference/sdlc/code.playbook/code.gears.md +0 -2
  26. package/reference/sdlc/code.playbook/code.playbook.d.ts +5 -2
  27. package/reference/sdlc/code.playbook/code.playbook.js +54 -2
  28. package/reference/sdlc/code.playbook/code.playbook.ts +75 -6
  29. package/reference/sdlc/code.playbook/code.registry.d.ts +10 -3
  30. package/reference/sdlc/code.playbook/code.registry.js +10 -3
  31. package/reference/sdlc/code.playbook/code.registry.ts +23 -5
  32. package/reference/sdlc/code.playbook/playbook-captain.d.ts +103 -8
  33. package/reference/sdlc/code.playbook/playbook-captain.js +1871 -75
  34. package/reference/sdlc/code.playbook/playbook-captain.ts +2801 -102
  35. package/reference/sdlc/code.playbook/playbook.config.template.yaml +14 -10
  36. package/reference/sdlc/code.playbook/session-store.d.ts +82 -0
  37. package/reference/sdlc/code.playbook/session-store.js +113 -0
  38. package/reference/sdlc/decide.md +24 -16
  39. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +7 -0
  40. package/reference/sdlc/decide.playbook/decide.fsm.js +80 -29
  41. package/reference/sdlc/decide.playbook/decide.fsm.ts +89 -31
  42. package/reference/sdlc/decide.playbook/decide.gears.md +0 -1
  43. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +13 -5
  44. package/reference/sdlc/decide.playbook/decide.playbook.js +1712 -91
  45. package/reference/sdlc/decide.playbook/decide.playbook.ts +2677 -136
  46. package/reference/sdlc/decide.playbook/decide.registry.d.ts +7 -3
  47. package/reference/sdlc/decide.playbook/decide.registry.js +10 -3
  48. package/reference/sdlc/decide.playbook/decide.registry.ts +20 -5
  49. package/reference/sdlc/review.md +36 -18
  50. package/reference/sdlc/review.playbook/review.fsm.d.ts +7 -0
  51. package/reference/sdlc/review.playbook/review.fsm.js +133 -12
  52. package/reference/sdlc/review.playbook/review.fsm.ts +140 -12
  53. package/reference/sdlc/review.playbook/review.playbook.d.ts +5 -2
  54. package/reference/sdlc/review.playbook/review.playbook.js +65 -2
  55. package/reference/sdlc/review.playbook/review.playbook.ts +83 -6
  56. package/reference/sdlc/review.playbook/review.registry.d.ts +10 -3
  57. package/reference/sdlc/review.playbook/review.registry.js +10 -3
  58. package/reference/sdlc/review.playbook/review.registry.ts +23 -5
  59. package/slc/gears2fsm.md +6 -5
  60. package/slc/link.md +544 -41
  61. package/src/accepted-outcome.d.ts +18 -0
  62. package/src/accepted-outcome.js +94 -0
  63. package/src/accepted-outcome.ts +140 -0
  64. package/src/runtime.d.ts +164 -3
  65. package/src/runtime.ts +213 -2
  66. package/src/xstate-playbook-runtime.d.ts +149 -10
  67. package/src/xstate-playbook-runtime.js +2569 -270
  68. package/src/xstate-playbook-runtime.ts +4133 -490
  69. package/src/xstate-runtime.d.ts +59 -1
  70. package/src/xstate-runtime.js +866 -7
  71. package/src/xstate-runtime.ts +1397 -7
@@ -8,35 +8,44 @@ Roles:
8
8
 
9
9
  - Coder
10
10
 
11
- The caller supplies the coding intent including any specific context.
11
+ The caller supplies a coding request together with any relevant context.
12
12
 
13
- A coding intent follows either one direct implementation phase or an IR sequence consisting of a new-IR phase and one phase for each IR task.
13
+ A new coding intent follows either one direct implementation phase or an IR sequence consisting of a new-IR phase followed by one phase for each IR task.
14
+ An existing IR follows one IR-task phase for each remaining unfinished task, starting with its next unfinished task.
14
15
  Each phase ends with one new Coder commit (owned by `code`).
15
16
  After each `code`-owned commit, Captain shall call the `review` playbook and wait until it passes with no unsettled findings.
16
17
  Playbook `review` owns every review round and every review-fix commit.
17
- Do not amend any reviewed commit.
18
+ Do not rewrite any existing commit.
18
19
 
19
20
  When `review` passes a direct implementation phase, `code` is complete.
20
21
  When `review` passes a new IR or a nonfinal IR-task phase, Captain shall continue with the next unfinished IR-task phase.
21
22
  When `review` passes the final IR-task phase, `code` is complete.
22
- When `review` returns an authored abort or failure, or a terminal result that does not prove exact approval, `code` shall start no further phase and shall report the failure and the last `code`-owned commit to its caller.
23
+ When `review` returns an authored abort or failure, or a terminal result that does not establish that the supplied scope was evaluated with no unsettled findings, `code` shall start no further phase and shall report the failure and the last `code`-owned commit to its caller.
23
24
  When the nested `review` call fails outside that authored result contract, `code` shall park as failed and retain the control-plane error instead of reporting an authored review outcome.
24
25
 
25
26
  At the start of the first phase, Captain shall relay to Coder the complete caller input and any relevant run results in quotes (`>`), along with the following instruction:
26
27
 
27
28
  ```markdown
28
- Assess whether the coding intent can be completed well in one commit.
29
- If yes, implement and test it, update the affected specs, and ensure @specs/map.md remains accurate.
30
- Otherwise, decompose it into tasks sized to exactly one commit each, add a new IR under @specs/intents, and do not implement any IR task in this phase.
29
+ First determine whether the coding request starts a new coding intent or continues an existing IR with unfinished work.
30
+ If the request may continue an existing IR but does not identify it unambiguously, ask Boss before changing files.
31
+
32
+ For a new coding intent, assess whether it can be completed well in one commit.
33
+ If it can, implement and test it, update the affected specs, and ensure @specs/map.md remains accurate.
34
+ If it cannot, decompose it into tasks sized to exactly one commit each, add a new IR under @specs/intents, and do not implement any IR task in this phase.
31
35
  Plan affected spec updates before, with, or after their corresponding code changes, either as standalone IR tasks or as explicit work within related tasks.
32
36
 
37
+ For an existing IR, read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.
38
+ Do not implement a later task in this phase.
39
+ Mark the IR's progress and deliverables when relevant.
40
+ If the IR will be finished after this phase, double-check that all acceptance criteria are met.
41
+
33
42
  Consult @specs/map.md for relevant context and @specs/meta.md for spec requirements, if needed.
34
43
  ```
35
44
 
36
- At the start of every IR-task phase, Captain shall relay to Coder the exact next task and any relevant run results in quotes (`>`), along with the following instruction:
45
+ At the start of every later IR-task phase, Captain shall relay to Coder the original caller input, the IR identity, and any relevant run results in quotes (`>`), along with the following instruction:
37
46
 
38
47
  ```markdown
39
- Read IR-<#> and implement exactly the next unfinished task, including corresponding tests or specs if any.
48
+ Read the identified IR and implement exactly its next unfinished task, including corresponding tests or specs if any.
40
49
  Do not implement a later task in this phase.
41
50
  Mark the IR's progress and deliverables when relevant.
42
51
  If the IR will be finished after this phase, double-check that all acceptance criteria are met.
@@ -45,19 +54,29 @@ If the IR will be finished after this phase, double-check that all acceptance cr
45
54
  At the start of *every* phase, Captain shall append the following instruction:
46
55
 
47
56
  ```markdown
57
+ Keep to the original intent and follow what it asks.
48
58
  Do not re-run tests or builds whose inputs have not changed since any previous reported run.
49
59
  Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.
50
60
  Make the commit message explain concisely what changed and why, including relevant verification.
51
- Report it as exactly one final-response line beginning `Commit: `, followed only by the exact commit identity.
52
- Coder is <coder-llm>; format the model token in conventional human form.
61
+ Identify every new commit you make.
62
+ Coder is <coder-llm>.
53
63
  ```
54
64
 
55
- After the first phase (either direct implementation or a new-IR phase), Captain shall call playbook `review` and input the following in quotes (`>`):
65
+ Every new-intent phase has two semantic outcomes: direct implementation and new IR; the new-IR outcome identifies the created IR.
66
+ Every IR-task phase, including the first phase for an existing IR, identifies the IR and the implemented task, and has two semantic outcomes: more tasks and final task.
67
+ The first phase therefore has four semantic outcomes — direct implementation, new IR, more tasks, and final task — plus the Boss question when the continued IR is ambiguous.
68
+ Each semantic outcome requires affirmative support in Coder's result, but no phase transition shall depend on a fixed presentation format of Coder's reply.
69
+ Captain shall use the repository-effect receipt as the authoritative identity of the phase's new commit.
70
+
71
+ At the end of every phase, Captain shall call playbook `review` and input the following in quotes (`>`):
56
72
 
57
- > Initial intent: \<caller-input\>
73
+ > Original intent: \<caller-input\>
74
+ > Review scope: the commit \<code-commit\> from this coding phase and its resulting repository state.
58
75
  > Coder output: \<coder-output\>
59
76
 
60
- After every IR-task phase, Captain shall call playbook `review` and input the following in quotes (`>`):
77
+ For an IR-task phase, Captain shall additionally input the following in quotes (`>`):
61
78
 
62
- > IR task: \<ir-task\>
63
- > Coder output: \<coder-output\>
79
+ > Current IR task: \<ir-task\>
80
+
81
+ A nested `review` passes the phase only when its result applies to that supplied review scope, returns the exact evaluated repository revision, and affirmatively establishes that no unsettled findings remain.
82
+ On successful completion, `code` returns the exact last `code`-owned commit, the exact final evaluated repository revision, and the fact that every phase's review passed with no unsettled findings.
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { randomUUID } from 'node:crypto';
10
10
  import { constants, realpathSync } from 'node:fs';
11
- import { lstat, open, readFile, realpath, unlink } from 'node:fs/promises';
11
+ import { lstat, open, realpath, unlink } from 'node:fs/promises';
12
12
  import {
13
13
  basename,
14
14
  dirname,
@@ -30,9 +30,15 @@ import {
30
30
  CaptainSessionHostCleanupError,
31
31
  captainOptionsFromConfig,
32
32
  createCaptainSessionHost,
33
+ installRetainedGenerationsForLaunch,
34
+ reportSkippedCaptainSession,
33
35
  validateFrozenExecutionConfig,
34
36
  } from './run.js';
35
37
  import { prepareConfiguredRegistries } from './provision.js';
38
+ import {
39
+ createReplayRecordObserver,
40
+ replayIncompleteMessage,
41
+ } from './replay-observer.js';
36
42
  import {
37
43
  assertCaptainSessionExecutionCompatible,
38
44
  createCaptainSessionStore,
@@ -47,6 +53,8 @@ export const MANAGED_INTERACTIVE_PAYLOAD_FILE =
47
53
  export const MANAGED_INTERACTIVE_PAYLOAD_SCHEMA_VERSION = 1;
48
54
  export const MANAGED_INTERACTIVE_PAYLOAD_KIND =
49
55
  'playbook-managed-interactive-launch';
56
+ export const MANAGED_INTERACTIVE_READINESS_WITNESS_FILE =
57
+ 'playbook-managed-readiness';
50
58
 
51
59
  const PAYLOAD_KEYS = [
52
60
  'schemaVersion',
@@ -82,10 +90,7 @@ export function validateManagedInteractivePayload(value) {
82
90
  }
83
91
  assertUuid(value.sessionId, 'managed interactive session id');
84
92
  assertCanonicalAbsolutePath(value.cwd, 'managed interactive working directory');
85
- assertCanonicalAbsolutePath(
86
- value.sessionsDir,
87
- 'managed interactive sessions directory',
88
- );
93
+ assertAbsolutePath(value.sessionsDir, 'managed interactive sessions directory');
89
94
  if (typeof value.noProvision !== 'boolean') {
90
95
  throw new Error('managed interactive noProvision must be a boolean');
91
96
  }
@@ -156,6 +161,18 @@ export async function writeManagedInteractivePayload(workDir, value) {
156
161
  return path;
157
162
  }
158
163
 
164
+ export async function publishManagedInteractiveReadinessWitness(
165
+ workDir,
166
+ sessionId,
167
+ ) {
168
+ return writeManagedInteractiveReadinessClaim({
169
+ workDir,
170
+ sessionId,
171
+ claim: 'ready',
172
+ rejectExisting: true,
173
+ });
174
+ }
175
+
159
176
  export async function readManagedInteractivePayload(path) {
160
177
  assertCanonicalAbsolutePath(path, 'managed interactive launch descriptor');
161
178
  assertManagedDescriptorPath(path);
@@ -300,15 +317,48 @@ export function createManagedInteractiveLifecycle(payloadValue, options = {}) {
300
317
  let leaseQuarantined = false;
301
318
  let activeTurn;
302
319
  let executionProjection;
320
+ let freshLaunchRecord;
321
+ let replayChannel;
303
322
 
304
323
  const release = async () => {
305
324
  if (released || lease === undefined) return;
306
325
  const owned = lease;
307
- await owned.release();
326
+ let status;
327
+ try {
328
+ status = await owned.release();
329
+ } catch (error) {
330
+ await replayChannel?.reportIfIncomplete();
331
+ throw error;
332
+ }
333
+ await replayChannel?.reportIfIncomplete(status);
308
334
  if (lease === owned) lease = undefined;
309
335
  released = true;
310
336
  };
311
337
 
338
+ const abandonFreshLaunchRecord = async ({ claim = false } = {}) => {
339
+ const record = freshLaunchRecord;
340
+ if (
341
+ record === undefined ||
342
+ lease === undefined ||
343
+ Object.keys(record.retainedGenerations ?? {}).length !== 0
344
+ ) {
345
+ return;
346
+ }
347
+ if (
348
+ claim &&
349
+ !(await writeManagedInteractiveReadinessClaim({
350
+ workDir: payload.workDir,
351
+ sessionId: payload.sessionId,
352
+ claim: 'abandon',
353
+ rejectExisting: false,
354
+ }))
355
+ ) {
356
+ return;
357
+ }
358
+ freshLaunchRecord = undefined;
359
+ await lease.abandonFreshSettled({ expected: record });
360
+ };
361
+
312
362
  return Object.freeze({
313
363
  async initializeRuntime(context) {
314
364
  if (initialized || lease !== undefined || released) {
@@ -328,7 +378,16 @@ export function createManagedInteractiveLifecycle(payloadValue, options = {}) {
328
378
  let host;
329
379
  try {
330
380
  lease = await store.acquire(payload.sessionId);
331
- const authoritative = await lease.read();
381
+ replayChannel = createManagedReplayChannel({
382
+ lease,
383
+ sessionId: payload.sessionId,
384
+ presentationGate: context.observers[0],
385
+ });
386
+ const authoritative =
387
+ typeof lease.recoverUnresolvedEffectAbandonment === 'function'
388
+ ? await lease.recoverUnresolvedEffectAbandonment()
389
+ : await lease.read();
390
+ let retainedGenerations = {};
332
391
  let restoreSnapshot;
333
392
  const prepareRegistryModule =
334
393
  options.prepareRegistryModule ??
@@ -381,14 +440,21 @@ export function createManagedInteractiveLifecycle(payloadValue, options = {}) {
381
440
  { loadModule, prepareRegistryModule },
382
441
  );
383
442
  restoreSnapshot = record.snapshot;
443
+ retainedGenerations = record.retainedGenerations ?? {};
384
444
  }
385
445
 
386
446
  const created = await createSessionHost({
387
447
  config: executionProjection,
388
448
  sessionId: payload.sessionId,
389
449
  cwd: payload.cwd,
450
+ sessionLease: lease,
390
451
  loadModule,
391
- observers: context.observers,
452
+ observers: [
453
+ ...context.observers,
454
+ ...(replayChannel === undefined
455
+ ? []
456
+ : [replayChannel.observer]),
457
+ ],
392
458
  ...(options.adapterImports
393
459
  ? { adapterImports: options.adapterImports }
394
460
  : {}),
@@ -401,18 +467,50 @@ export function createManagedInteractiveLifecycle(payloadValue, options = {}) {
401
467
  ...(options.createHostRuntime
402
468
  ? { createHostRuntime: options.createHostRuntime }
403
469
  : {}),
470
+ ...(options.createEffectLedgerWriteAhead
471
+ ? {
472
+ createEffectLedgerWriteAhead:
473
+ options.createEffectLedgerWriteAhead,
474
+ }
475
+ : {}),
404
476
  ...(restoreSnapshot !== undefined ? { restoreSnapshot } : {}),
405
477
  });
406
478
  ({ host, shell } = created);
407
- if (payload.mode === 'fresh') {
408
- await lease.initializeSettled({
409
- cwd: payload.cwd,
410
- structuralProjection:
411
- projectCaptainSessionStructure(executionProjection),
412
- executionProjection,
413
- snapshot: created.snapshot,
414
- });
415
- }
479
+ await installRetainedGenerationsForLaunch({
480
+ lease,
481
+ shell,
482
+ ...(payload.mode === 'fresh'
483
+ ? {
484
+ freshBoundary: {
485
+ cwd: payload.cwd,
486
+ structuralProjection:
487
+ projectCaptainSessionStructure(executionProjection),
488
+ executionProjection,
489
+ snapshot: created.snapshot,
490
+ },
491
+ onFreshRecord(record) {
492
+ freshLaunchRecord = record;
493
+ },
494
+ onLegacyRecord: (record) =>
495
+ reportSkippedCaptainSession(
496
+ options.stderr ?? process.stderr,
497
+ 'playbook',
498
+ 'legacy',
499
+ record,
500
+ ),
501
+ onInvalidRecord: (record) =>
502
+ reportSkippedCaptainSession(
503
+ options.stderr ?? process.stderr,
504
+ 'playbook',
505
+ 'invalid',
506
+ record,
507
+ ),
508
+ }
509
+ : {}),
510
+ retainedGenerations,
511
+ reconcileRepositoryEffects: created.reconcileRepositoryEffects,
512
+ });
513
+ await replayChannel?.reportIfIncomplete();
416
514
  initialized = true;
417
515
  return {
418
516
  abortActiveTurn: (...args) => host.abortActiveTurn(...args),
@@ -445,6 +543,13 @@ export function createManagedInteractiveLifecycle(payloadValue, options = {}) {
445
543
  failures.push(disposeError);
446
544
  }
447
545
  }
546
+ if (!leaseQuarantined) {
547
+ try {
548
+ await abandonFreshLaunchRecord();
549
+ } catch (abandonmentError) {
550
+ failures.push(abandonmentError);
551
+ }
552
+ }
448
553
  shell = undefined;
449
554
  if (!leaseQuarantined) {
450
555
  try {
@@ -503,25 +608,119 @@ export function createManagedInteractiveLifecycle(payloadValue, options = {}) {
503
608
  `managed interactive Captain turn produced ${replies.length} usable Boss-visible replies; expected exactly one`,
504
609
  );
505
610
  }
506
- const snapshot = shell?.exportSnapshot();
507
- if (snapshot === undefined) {
611
+ const settlement = shell?.exportSettlement();
612
+ if (settlement === undefined) {
508
613
  throw new Error(
509
- 'managed interactive Captain turn settled without an exportable session snapshot',
614
+ 'managed interactive Captain turn settled without an exportable session settlement',
510
615
  );
511
616
  }
512
- await lease.settle({ attemptId: activeTurn.attemptId, snapshot });
617
+ await lease.settle({
618
+ attemptId: activeTurn.attemptId,
619
+ snapshot: settlement.snapshot,
620
+ unresolvedEffects: settlement.unresolvedEffects,
621
+ retentionUpdates: settlement.retentionUpdates,
622
+ });
623
+ await replayChannel?.reportIfIncomplete();
513
624
  activeTurn = undefined;
514
625
  },
515
626
 
516
- async shutdown() {
627
+ async shutdown(context) {
517
628
  // Cligent invokes lifecycle shutdown only after runtime disposal and the
518
629
  // complete turn transaction, so ownership retirement cannot race either.
519
630
  if (leaseQuarantined) return;
520
- await release();
631
+ const failures = [];
632
+ if (context !== undefined) {
633
+ try {
634
+ await abandonFreshLaunchRecord({ claim: true });
635
+ } catch (error) {
636
+ failures.push(error);
637
+ }
638
+ }
639
+ try {
640
+ await release();
641
+ } catch (error) {
642
+ failures.push(error);
643
+ }
644
+ if (failures.length === 1) throw failures[0];
645
+ if (failures.length > 1) {
646
+ throw new AggregateError(
647
+ failures,
648
+ 'managed interactive shutdown could not retract its unused fresh session and retire its lease',
649
+ );
650
+ }
521
651
  },
522
652
  });
523
653
  }
524
654
 
655
+ function createManagedReplayChannel({
656
+ lease,
657
+ sessionId,
658
+ presentationGate,
659
+ }) {
660
+ if (
661
+ typeof lease?.append !== 'function' ||
662
+ typeof lease?.streamStatus !== 'function'
663
+ ) {
664
+ return undefined;
665
+ }
666
+ return createReplayRecordObserver({
667
+ lease,
668
+ async onIncomplete() {
669
+ if (typeof presentationGate?.onRecord !== 'function') return;
670
+ await presentationGate.onRecord({
671
+ type: 'captain_status',
672
+ turnId: null,
673
+ timestamp: Date.now(),
674
+ message: replayIncompleteMessage(sessionId),
675
+ });
676
+ },
677
+ });
678
+ }
679
+
680
+ async function writeManagedInteractiveReadinessClaim({
681
+ workDir,
682
+ sessionId,
683
+ claim,
684
+ rejectExisting,
685
+ }) {
686
+ assertCanonicalAbsolutePath(
687
+ workDir,
688
+ 'managed interactive readiness work directory',
689
+ );
690
+ assertUuid(sessionId, 'managed interactive readiness session id');
691
+ const path = join(workDir, MANAGED_INTERACTIVE_READINESS_WITNESS_FILE);
692
+ let handle;
693
+ try {
694
+ handle = await open(path, 'wx', 0o600);
695
+ } catch (cause) {
696
+ if (cause?.code === 'EEXIST' && !rejectExisting) return false;
697
+ throw new Error(
698
+ `managed interactive readiness claim could not be created: ${message(cause)}`,
699
+ { cause },
700
+ );
701
+ }
702
+ try {
703
+ await handle.chmod(0o600);
704
+ const stat = await handle.stat();
705
+ if (!stat.isFile() || (stat.mode & 0o7777) !== 0o600) {
706
+ throw new Error(
707
+ 'managed interactive readiness claim is not a private regular file',
708
+ );
709
+ }
710
+ if (
711
+ typeof process.getuid === 'function' &&
712
+ stat.uid !== process.getuid()
713
+ ) {
714
+ throw new Error('managed interactive readiness claim has a foreign owner');
715
+ }
716
+ await handle.writeFile(`${claim} ${sessionId}\n`, 'utf8');
717
+ await handle.sync();
718
+ } finally {
719
+ await handle.close();
720
+ }
721
+ return true;
722
+ }
723
+
525
724
  export function parseManagedInteractiveChildArgs(argv) {
526
725
  if (argv.length !== 1) {
527
726
  throw new Error(
@@ -759,6 +958,12 @@ function assertCanonicalAbsolutePath(value, path) {
759
958
  }
760
959
  }
761
960
 
961
+ function assertAbsolutePath(value, path) {
962
+ if (typeof value !== 'string' || !isAbsolute(value)) {
963
+ throw new Error(`${path} must be an absolute path`);
964
+ }
965
+ }
966
+
762
967
  function assertManagedDescriptorPath(path) {
763
968
  if (basename(path) !== MANAGED_INTERACTIVE_PAYLOAD_FILE) {
764
969
  throw new Error(