@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.
- package/README.md +1 -1
- package/docs/cli.md +109 -21
- package/docs/configuration.md +89 -40
- package/docs/embedding.md +126 -12
- package/package.json +14 -3
- package/reference/sdlc/captain.md +14 -10
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
- package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
- package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
- package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
- package/reference/sdlc/captain.playbook/captain.playbook.js +140 -10
- package/reference/sdlc/captain.playbook/captain.playbook.ts +188 -16
- package/reference/sdlc/code.md +35 -16
- package/reference/sdlc/code.playbook/bin/interactive-session.js +228 -23
- package/reference/sdlc/code.playbook/bin/launch-config.js +611 -221
- package/reference/sdlc/code.playbook/bin/playbook.js +304 -178
- package/reference/sdlc/code.playbook/bin/replay-observer.js +221 -0
- package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
- package/reference/sdlc/code.playbook/bin/run.js +669 -215
- package/reference/sdlc/code.playbook/bin/session-store.js +4546 -502
- package/reference/sdlc/code.playbook/code.fsm.d.ts +7 -0
- package/reference/sdlc/code.playbook/code.fsm.js +74 -25
- package/reference/sdlc/code.playbook/code.fsm.ts +83 -29
- package/reference/sdlc/code.playbook/code.gears.md +0 -2
- package/reference/sdlc/code.playbook/code.playbook.d.ts +5 -2
- package/reference/sdlc/code.playbook/code.playbook.js +54 -2
- package/reference/sdlc/code.playbook/code.playbook.ts +75 -6
- package/reference/sdlc/code.playbook/code.registry.d.ts +10 -3
- package/reference/sdlc/code.playbook/code.registry.js +10 -3
- package/reference/sdlc/code.playbook/code.registry.ts +23 -5
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +103 -8
- package/reference/sdlc/code.playbook/playbook-captain.js +1871 -75
- package/reference/sdlc/code.playbook/playbook-captain.ts +2801 -102
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +14 -10
- package/reference/sdlc/code.playbook/session-store.d.ts +82 -0
- package/reference/sdlc/code.playbook/session-store.js +113 -0
- package/reference/sdlc/decide.md +24 -16
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +7 -0
- package/reference/sdlc/decide.playbook/decide.fsm.js +80 -29
- package/reference/sdlc/decide.playbook/decide.fsm.ts +89 -31
- package/reference/sdlc/decide.playbook/decide.gears.md +0 -1
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +13 -5
- package/reference/sdlc/decide.playbook/decide.playbook.js +1712 -91
- package/reference/sdlc/decide.playbook/decide.playbook.ts +2677 -136
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +7 -3
- package/reference/sdlc/decide.playbook/decide.registry.js +10 -3
- package/reference/sdlc/decide.playbook/decide.registry.ts +20 -5
- package/reference/sdlc/review.md +36 -18
- package/reference/sdlc/review.playbook/review.fsm.d.ts +7 -0
- package/reference/sdlc/review.playbook/review.fsm.js +133 -12
- package/reference/sdlc/review.playbook/review.fsm.ts +140 -12
- package/reference/sdlc/review.playbook/review.playbook.d.ts +5 -2
- package/reference/sdlc/review.playbook/review.playbook.js +65 -2
- package/reference/sdlc/review.playbook/review.playbook.ts +83 -6
- package/reference/sdlc/review.playbook/review.registry.d.ts +10 -3
- package/reference/sdlc/review.playbook/review.registry.js +10 -3
- package/reference/sdlc/review.playbook/review.registry.ts +23 -5
- package/slc/gears2fsm.md +6 -5
- package/slc/link.md +544 -41
- package/src/accepted-outcome.d.ts +18 -0
- package/src/accepted-outcome.js +94 -0
- package/src/accepted-outcome.ts +140 -0
- package/src/runtime.d.ts +164 -3
- package/src/runtime.ts +213 -2
- package/src/xstate-playbook-runtime.d.ts +149 -10
- package/src/xstate-playbook-runtime.js +2569 -270
- package/src/xstate-playbook-runtime.ts +4133 -490
- package/src/xstate-runtime.d.ts +59 -1
- package/src/xstate-runtime.js +866 -7
- package/src/xstate-runtime.ts +1397 -7
package/reference/sdlc/code.md
CHANGED
|
@@ -8,35 +8,44 @@ Roles:
|
|
|
8
8
|
|
|
9
9
|
- Coder
|
|
10
10
|
|
|
11
|
-
The caller supplies
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
29
|
-
If
|
|
30
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
52
|
-
Coder is <coder-llm
|
|
61
|
+
Identify every new commit you make.
|
|
62
|
+
Coder is <coder-llm>.
|
|
53
63
|
```
|
|
54
64
|
|
|
55
|
-
|
|
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
|
-
>
|
|
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
|
-
|
|
77
|
+
For an IR-task phase, Captain shall additionally input the following in quotes (`>`):
|
|
61
78
|
|
|
62
|
-
> IR task: \<ir-task\>
|
|
63
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
507
|
-
if (
|
|
611
|
+
const settlement = shell?.exportSettlement();
|
|
612
|
+
if (settlement === undefined) {
|
|
508
613
|
throw new Error(
|
|
509
|
-
'managed interactive Captain turn settled without an exportable session
|
|
614
|
+
'managed interactive Captain turn settled without an exportable session settlement',
|
|
510
615
|
);
|
|
511
616
|
}
|
|
512
|
-
await lease.settle({
|
|
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
|
-
|
|
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(
|