@sublang/playbook 8.0.0 → 10.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 +3 -3
- package/docs/cli.md +66 -23
- package/docs/configuration.md +13 -8
- package/docs/embedding.md +45 -14
- package/package.json +7 -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 +151 -10
- package/reference/sdlc/captain.playbook/captain.playbook.ts +200 -14
- package/reference/sdlc/code.md +0 -1
- package/reference/sdlc/code.playbook/bin/interactive-session.js +170 -17
- package/reference/sdlc/code.playbook/bin/launch-config.js +136 -4
- package/reference/sdlc/code.playbook/bin/playbook.js +81 -4
- package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
- package/reference/sdlc/code.playbook/bin/run.js +365 -63
- package/reference/sdlc/code.playbook/bin/session-store.js +2877 -209
- package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -1
- package/reference/sdlc/code.playbook/code.fsm.js +85 -29
- package/reference/sdlc/code.playbook/code.fsm.ts +95 -33
- 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 +67 -4
- package/reference/sdlc/code.playbook/code.playbook.ts +87 -8
- 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 +99 -7
- package/reference/sdlc/code.playbook/playbook-captain.js +1894 -82
- package/reference/sdlc/code.playbook/playbook-captain.ts +2809 -109
- package/reference/sdlc/decide.md +0 -1
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +8 -1
- 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 +15 -5
- package/reference/sdlc/decide.playbook/decide.playbook.js +1994 -191
- package/reference/sdlc/decide.playbook/decide.playbook.ts +3209 -404
- 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.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 +78 -4
- package/reference/sdlc/review.playbook/review.playbook.ts +95 -8
- 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 +25 -7
- package/slc/link.md +727 -82
- 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 +165 -3
- package/src/runtime.ts +214 -2
- package/src/xstate-playbook-runtime.d.ts +162 -13
- package/src/xstate-playbook-runtime.js +3344 -564
- package/src/xstate-playbook-runtime.ts +4873 -637
- package/src/xstate-runtime.d.ts +76 -8
- package/src/xstate-runtime.js +1001 -64
- package/src/xstate-runtime.ts +1640 -91
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
// deterministic entry mapping, the controller captain-call
|
|
32
32
|
// strategy with its single corrective re-ask (CAPPLAY-18),
|
|
33
33
|
// controller-port submission, and status formatting.
|
|
34
|
-
// Compat: spec.compat = { artifactSchema:
|
|
34
|
+
// Compat: spec.compat = { artifactSchema: 3, runtimeAbi: 1 }
|
|
35
35
|
// (DR-022; checked at construction by the loading engine).
|
|
36
|
-
import { createXStatePlaybookRuntime, defaultComposeCaptainPrompt, normalizeError, normalizeErrorCompact, parseJudgeJson, snapshotJsonValue,
|
|
36
|
+
import { createXStatePlaybookRuntime, defaultComposeCaptainPrompt, emptyPlaybookEffectLedger, normalizeError, normalizeErrorCompact, parseJudgeJson, snapshotJsonValue, } from '../../../src/xstate-runtime.js';
|
|
37
37
|
import { captainMachine, } from './captain.fsm.js';
|
|
38
38
|
function assertNonEmptyString(value, label) {
|
|
39
39
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
@@ -119,6 +119,18 @@ function validateParsedActingDecision(value) {
|
|
|
119
119
|
}
|
|
120
120
|
return { action: 'deliver' };
|
|
121
121
|
}
|
|
122
|
+
if (value.action === 'resume') {
|
|
123
|
+
const allowed = new Set(['action', 'playbookId']);
|
|
124
|
+
for (const key of Object.keys(value)) {
|
|
125
|
+
if (!allowed.has(key)) {
|
|
126
|
+
throw new TypeError(`parse-resolved decision carries undeclared ${key}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
action: 'resume',
|
|
131
|
+
playbookId: assertNonEmptyString(value.playbookId, 'parse-resolved decision playbookId'),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
122
134
|
if (value.action !== 'start' && value.action !== 'switch') {
|
|
123
135
|
throw new TypeError(`parse-resolved decision names unknown action ${String(value.action)}`);
|
|
124
136
|
}
|
|
@@ -168,8 +180,8 @@ function classifyControllerTurn(text, _ports, _signal, _snapshotOrState, _bounda
|
|
|
168
180
|
// the turn as a recoverable hub return with no action executed.
|
|
169
181
|
// ---------------------------------------------------------------------------
|
|
170
182
|
const RESTATED_REPLY_CONTRACT = [
|
|
171
|
-
'Reply again with exactly one JSON object `{ "action": …, … }` and no other text, selecting exactly one action from the closed set `respond` | `start` | `switch` | `dismiss` | `deliver` | `runtime`:',
|
|
172
|
-
'`{ "action": "respond", "text": … }`, `{ "action": "start", "playbookId": …, "input": … }`, `{ "action": "switch", "playbookId": …, "input": … }`, `{ "action": "dismiss" }`, `{ "action": "deliver" }`, or `{ "action": "runtime", "actionId": … }`; every `input` is one nonempty complete standalone request.',
|
|
183
|
+
'Reply again with exactly one JSON object `{ "action": …, … }` and no other text, selecting exactly one action from the closed set `respond` | `resume` | `start` | `switch` | `dismiss` | `deliver` | `runtime`:',
|
|
184
|
+
'`{ "action": "respond", "text": … }`, `{ "action": "resume", "playbookId": … }`, `{ "action": "start", "playbookId": …, "input": … }`, `{ "action": "switch", "playbookId": …, "input": … }`, `{ "action": "dismiss" }`, `{ "action": "deliver" }`, or `{ "action": "runtime", "actionId": … }`; every `input` is one nonempty complete standalone request.',
|
|
173
185
|
].join('\n');
|
|
174
186
|
function correctiveDecisionPrompt(prompt, reason) {
|
|
175
187
|
return [
|
|
@@ -221,6 +233,23 @@ function readDecisionReply(reply, options, selfPlaybookId, declaredActions) {
|
|
|
221
233
|
return { reason: shape };
|
|
222
234
|
return { selection: { action, text: parsed.text } };
|
|
223
235
|
}
|
|
236
|
+
case 'resume': {
|
|
237
|
+
const shape = requireKeys(['playbookId']) ?? nonEmpty('playbookId');
|
|
238
|
+
if (shape !== undefined)
|
|
239
|
+
return { reason: shape };
|
|
240
|
+
const playbookId = parsed.playbookId;
|
|
241
|
+
if (playbookId === selfPlaybookId) {
|
|
242
|
+
return {
|
|
243
|
+
reason: `the ${action} target may never be this Captain playbook itself`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (!options.enabledPlaybooks.some((entry) => entry.id === playbookId)) {
|
|
247
|
+
return {
|
|
248
|
+
reason: `the ${action} target ${JSON.stringify(playbookId)} is not an enabled catalog id`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return { selection: { action, playbookId } };
|
|
252
|
+
}
|
|
224
253
|
case 'start':
|
|
225
254
|
case 'switch': {
|
|
226
255
|
const shape = requireKeys(['playbookId', 'input']) ??
|
|
@@ -276,6 +305,9 @@ function selectionFromParsedDecision(decision) {
|
|
|
276
305
|
if (decision.action === 'deliver') {
|
|
277
306
|
return { action: 'deliver' };
|
|
278
307
|
}
|
|
308
|
+
if (decision.action === 'resume') {
|
|
309
|
+
return { action: 'resume', playbookId: decision.playbookId };
|
|
310
|
+
}
|
|
279
311
|
// A parse-resolved `start` / `switch` is always the exact command remainder
|
|
280
312
|
// supplied by the host (CAPTAIN-7 command table).
|
|
281
313
|
return {
|
|
@@ -297,10 +329,71 @@ function decisionOutputOf(selection) {
|
|
|
297
329
|
}
|
|
298
330
|
// ---------------------------------------------------------------------------
|
|
299
331
|
// Settlement validation: the returned settlement is the only evidence of
|
|
300
|
-
// effects, and the machine retains only its status, facts,
|
|
301
|
-
// disposition, and leaf-state summary (CAPPLAY-10). A
|
|
302
|
-
// is a host control-plane failure.
|
|
332
|
+
// effects, and the machine retains only its status, facts, bounded unresolved
|
|
333
|
+
// effects, receipt disposition, and leaf-state summary (CAPPLAY-10). A
|
|
334
|
+
// malformed settlement is a host control-plane failure.
|
|
303
335
|
// ---------------------------------------------------------------------------
|
|
336
|
+
const SETTLEMENT_GIT_OID_PATTERN = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
337
|
+
const SETTLEMENT_UNRESOLVED_CLASSIFICATIONS = new Set([
|
|
338
|
+
'one-descendant-commit',
|
|
339
|
+
'multiple-commits',
|
|
340
|
+
'rewritten-or-non-descendant',
|
|
341
|
+
'worktree-only-change',
|
|
342
|
+
'concurrent-or-foreign-change',
|
|
343
|
+
'observation-ambiguous',
|
|
344
|
+
'incomplete',
|
|
345
|
+
]);
|
|
346
|
+
function validateSettlementUnresolvedEffects(value) {
|
|
347
|
+
const detached = snapshotJsonValue(value, 'controller settlement unresolvedEffects');
|
|
348
|
+
if (!Array.isArray(detached)) {
|
|
349
|
+
throw new TypeError('controller settlement unresolvedEffects must be an array');
|
|
350
|
+
}
|
|
351
|
+
for (const [index, raw] of detached.entries()) {
|
|
352
|
+
const path = `controller settlement unresolvedEffects[${index}]`;
|
|
353
|
+
if (!isRecord(raw)) {
|
|
354
|
+
throw new TypeError(`${path} must be an object`);
|
|
355
|
+
}
|
|
356
|
+
const allowed = new Set([
|
|
357
|
+
'classification',
|
|
358
|
+
'baselineHead',
|
|
359
|
+
'afterHead',
|
|
360
|
+
'commitOid',
|
|
361
|
+
]);
|
|
362
|
+
const unknown = Object.keys(raw).find((key) => !allowed.has(key));
|
|
363
|
+
if (unknown !== undefined) {
|
|
364
|
+
throw new TypeError(`${path} carries undeclared ${unknown}`);
|
|
365
|
+
}
|
|
366
|
+
if (typeof raw.classification !== 'string' ||
|
|
367
|
+
!SETTLEMENT_UNRESOLVED_CLASSIFICATIONS.has(raw.classification)) {
|
|
368
|
+
throw new TypeError(`${path} classification is not supported`);
|
|
369
|
+
}
|
|
370
|
+
if (typeof raw.baselineHead !== 'string' ||
|
|
371
|
+
!SETTLEMENT_GIT_OID_PATTERN.test(raw.baselineHead)) {
|
|
372
|
+
throw new TypeError(`${path} baselineHead must be a Git OID`);
|
|
373
|
+
}
|
|
374
|
+
if ('afterHead' in raw &&
|
|
375
|
+
(typeof raw.afterHead !== 'string' ||
|
|
376
|
+
!SETTLEMENT_GIT_OID_PATTERN.test(raw.afterHead))) {
|
|
377
|
+
throw new TypeError(`${path} afterHead must be a Git OID`);
|
|
378
|
+
}
|
|
379
|
+
if (raw.classification !== 'observation-ambiguous' &&
|
|
380
|
+
raw.classification !== 'incomplete' &&
|
|
381
|
+
!('afterHead' in raw)) {
|
|
382
|
+
throw new TypeError(`${path} afterHead is required for ${raw.classification}`);
|
|
383
|
+
}
|
|
384
|
+
if (raw.classification === 'one-descendant-commit') {
|
|
385
|
+
if (typeof raw.commitOid !== 'string' ||
|
|
386
|
+
!SETTLEMENT_GIT_OID_PATTERN.test(raw.commitOid) ||
|
|
387
|
+
raw.commitOid !== raw.afterHead) {
|
|
388
|
+
throw new TypeError(`${path} commitOid must equal afterHead for one-descendant-commit`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else if ('commitOid' in raw) {
|
|
392
|
+
throw new TypeError(`${path} commitOid is permitted only for one-descendant-commit`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return detached;
|
|
396
|
+
}
|
|
304
397
|
function validateSettlement(value) {
|
|
305
398
|
if (!isRecord(value)) {
|
|
306
399
|
throw new TypeError('controller settlement must be an object');
|
|
@@ -308,6 +401,7 @@ function validateSettlement(value) {
|
|
|
308
401
|
const allowed = new Set([
|
|
309
402
|
'status',
|
|
310
403
|
'facts',
|
|
404
|
+
'unresolvedEffects',
|
|
311
405
|
'reason',
|
|
312
406
|
'receipt',
|
|
313
407
|
'leafStateSummary',
|
|
@@ -326,6 +420,10 @@ function validateSettlement(value) {
|
|
|
326
420
|
value.facts.some((fact) => typeof fact !== 'string')) {
|
|
327
421
|
throw new TypeError('controller settlement facts must be a string array');
|
|
328
422
|
}
|
|
423
|
+
if (!Object.prototype.hasOwnProperty.call(value, 'unresolvedEffects')) {
|
|
424
|
+
throw new TypeError('controller settlement unresolvedEffects is required');
|
|
425
|
+
}
|
|
426
|
+
const unresolvedEffects = validateSettlementUnresolvedEffects(value.unresolvedEffects);
|
|
329
427
|
if ('reason' in value && typeof value.reason !== 'string') {
|
|
330
428
|
throw new TypeError('controller settlement reason must be a string');
|
|
331
429
|
}
|
|
@@ -372,6 +470,7 @@ function validateSettlement(value) {
|
|
|
372
470
|
return Object.freeze({
|
|
373
471
|
status: value.status,
|
|
374
472
|
facts: Object.freeze([...value.facts]),
|
|
473
|
+
unresolvedEffects,
|
|
375
474
|
...(typeof value.reason === 'string' ? { reason: value.reason } : {}),
|
|
376
475
|
...(receipt === undefined ? {} : { receipt }),
|
|
377
476
|
...(typeof value.leafStateSummary === 'string'
|
|
@@ -494,6 +593,7 @@ function statusesForState(state, context) {
|
|
|
494
593
|
return [];
|
|
495
594
|
}
|
|
496
595
|
}
|
|
596
|
+
const UNFINISHED_FINAL_STATE_IDS = new Set();
|
|
497
597
|
// Internal export surface for verification and tests. Not part of the
|
|
498
598
|
// stable public API; the leading underscore signals "subject to change."
|
|
499
599
|
export const _internal = {
|
|
@@ -509,6 +609,7 @@ export const _internal = {
|
|
|
509
609
|
validateParsedActingDecision,
|
|
510
610
|
statusesForState,
|
|
511
611
|
normalizeError,
|
|
612
|
+
UNFINISHED_FINAL_STATE_IDS,
|
|
512
613
|
};
|
|
513
614
|
// The Captain-specific spec handed to the shared runtime factory
|
|
514
615
|
// (slc/link.md §Output, DR-019). Generic machinery — actor wiring, boundary
|
|
@@ -516,7 +617,14 @@ export const _internal = {
|
|
|
516
617
|
// describe/apply control surface — lives in @sublang/playbook/xstate-runtime.
|
|
517
618
|
const runtimeSpec = {
|
|
518
619
|
label: 'CAPTAIN',
|
|
519
|
-
|
|
620
|
+
// DR-022 / slc/link.md: the declaration carries the value current at link
|
|
621
|
+
// time — a literal, never the loading engine's RUNTIME_ABI self-report,
|
|
622
|
+
// which would follow whatever engine loads the module and make the
|
|
623
|
+
// factory's skew check compare that engine with itself.
|
|
624
|
+
compat: { artifactSchema: 3, runtimeAbi: 1 },
|
|
625
|
+
// DR-040: the roleless session Captain has no delegated-player outcome,
|
|
626
|
+
// but schema 3 still makes that absence explicit rather than inferred.
|
|
627
|
+
outcomeAuthority: { governedPlayerStates: {} },
|
|
520
628
|
snapshotOptions: snapshotCaptainOptions,
|
|
521
629
|
machineInput: (options) => ({ enabledPlaybooks: options.enabledPlaybooks }),
|
|
522
630
|
classifyBossText: (text, ports, signal, snapshotOrState, boundary, options) => classifyControllerTurn(text, ports, signal, snapshotOrState, boundary, options),
|
|
@@ -540,12 +648,45 @@ const runtimeSpec = {
|
|
|
540
648
|
'receiptReason',
|
|
541
649
|
'receiptError',
|
|
542
650
|
'leafStateSummary',
|
|
651
|
+
'settlementUnresolvedEffects',
|
|
543
652
|
],
|
|
653
|
+
unfinishedFinalStateIds: UNFINISHED_FINAL_STATE_IDS,
|
|
544
654
|
statusesForState,
|
|
545
655
|
};
|
|
546
|
-
|
|
656
|
+
// DR-022's compat check fails fast at factory construction. The Captain is
|
|
657
|
+
// statically imported by the shell and both CLI front ends, so constructing
|
|
658
|
+
// here at module evaluation would turn a future compat mismatch into an
|
|
659
|
+
// uncaught ESM-load error that takes even `--help` down; constructing on
|
|
660
|
+
// the first runtime request keeps the failure inside the caught
|
|
661
|
+
// host-construction boundary that owes the Boss a setup diagnostic.
|
|
662
|
+
const rejectInternalCaptainRepositoryWork = async () => {
|
|
663
|
+
throw new Error('the roleless session Captain cannot perform repository-governed work');
|
|
664
|
+
};
|
|
665
|
+
const INTERNAL_CAPTAIN_HOST_CAPABILITIES = Object.freeze({
|
|
666
|
+
// The engine does not consult this member for an explicitly empty governed
|
|
667
|
+
// set. Keeping a fail-closed implementation satisfies the schema-3 factory
|
|
668
|
+
// boundary without granting the internal controller repository authority.
|
|
669
|
+
repository: Object.freeze({
|
|
670
|
+
runExclusive: rejectInternalCaptainRepositoryWork,
|
|
671
|
+
runDeferred: rejectInternalCaptainRepositoryWork,
|
|
672
|
+
}),
|
|
673
|
+
effectLedger: Object.freeze({
|
|
674
|
+
snapshot: emptyPlaybookEffectLedger,
|
|
675
|
+
writeAhead: async () => {
|
|
676
|
+
throw new Error('the roleless session Captain cannot write an effect ledger');
|
|
677
|
+
},
|
|
678
|
+
}),
|
|
679
|
+
});
|
|
680
|
+
function buildCaptainPlaybookRuntimeFactory() {
|
|
681
|
+
return createXStatePlaybookRuntime(captainMachine, runtimeSpec);
|
|
682
|
+
}
|
|
683
|
+
let createCaptainPlaybookRuntime;
|
|
547
684
|
export function createPlaybookRuntime(options) {
|
|
548
|
-
|
|
685
|
+
createCaptainPlaybookRuntime ??= buildCaptainPlaybookRuntimeFactory();
|
|
686
|
+
return createCaptainPlaybookRuntime({
|
|
687
|
+
configuredOptions: options,
|
|
688
|
+
hostCapabilities: INTERNAL_CAPTAIN_HOST_CAPABILITIES,
|
|
689
|
+
});
|
|
549
690
|
}
|
|
550
691
|
const factory = createPlaybookRuntime;
|
|
551
692
|
export default factory;
|
|
@@ -31,22 +31,23 @@
|
|
|
31
31
|
// deterministic entry mapping, the controller captain-call
|
|
32
32
|
// strategy with its single corrective re-ask (CAPPLAY-18),
|
|
33
33
|
// controller-port submission, and status formatting.
|
|
34
|
-
// Compat: spec.compat = { artifactSchema:
|
|
34
|
+
// Compat: spec.compat = { artifactSchema: 3, runtimeAbi: 1 }
|
|
35
35
|
// (DR-022; checked at construction by the loading engine).
|
|
36
36
|
|
|
37
37
|
import {
|
|
38
38
|
createXStatePlaybookRuntime,
|
|
39
39
|
defaultComposeCaptainPrompt,
|
|
40
|
+
emptyPlaybookEffectLedger,
|
|
40
41
|
normalizeError,
|
|
41
42
|
normalizeErrorCompact,
|
|
42
43
|
parseJudgeJson,
|
|
43
44
|
snapshotJsonValue,
|
|
44
|
-
RUNTIME_ABI,
|
|
45
45
|
type PlaybookActorOutput,
|
|
46
46
|
type PlaybookCaptainInput,
|
|
47
47
|
type ScheduledStatus,
|
|
48
48
|
type XStateCaptainStrategyRun,
|
|
49
|
-
type
|
|
49
|
+
type XStatePlaybookRuntimeSpecV3,
|
|
50
|
+
type XStateRepositoryCapability,
|
|
50
51
|
} from '../../../src/xstate-runtime.js';
|
|
51
52
|
import {
|
|
52
53
|
captainMachine,
|
|
@@ -57,6 +58,7 @@ import {
|
|
|
57
58
|
type ParsedActingDecision,
|
|
58
59
|
type SettlementEvidence,
|
|
59
60
|
type SettlementReceiptEvidence,
|
|
61
|
+
type SettlementUnresolvedEffectEvidence,
|
|
60
62
|
} from './captain.fsm.js';
|
|
61
63
|
import type {
|
|
62
64
|
CaptainCallOptions,
|
|
@@ -124,9 +126,10 @@ export type {
|
|
|
124
126
|
/** One nonempty complete standalone request selected for `start` or `switch`. */
|
|
125
127
|
export type CaptainControllerInput = string;
|
|
126
128
|
|
|
127
|
-
/** One validated controller selection submitted through the port (DR-029). */
|
|
129
|
+
/** One validated controller selection submitted through the port (DR-029, DR-038). */
|
|
128
130
|
export type CaptainControllerSelection =
|
|
129
131
|
| { readonly action: 'respond'; readonly text: string }
|
|
132
|
+
| { readonly action: 'resume'; readonly playbookId: string }
|
|
130
133
|
| {
|
|
131
134
|
readonly action: 'start' | 'switch';
|
|
132
135
|
readonly playbookId: string;
|
|
@@ -295,6 +298,21 @@ function validateParsedActingDecision(value: unknown): ParsedActingDecision {
|
|
|
295
298
|
}
|
|
296
299
|
return { action: 'deliver' };
|
|
297
300
|
}
|
|
301
|
+
if (value.action === 'resume') {
|
|
302
|
+
const allowed = new Set(['action', 'playbookId']);
|
|
303
|
+
for (const key of Object.keys(value)) {
|
|
304
|
+
if (!allowed.has(key)) {
|
|
305
|
+
throw new TypeError(`parse-resolved decision carries undeclared ${key}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
action: 'resume',
|
|
310
|
+
playbookId: assertNonEmptyString(
|
|
311
|
+
value.playbookId,
|
|
312
|
+
'parse-resolved decision playbookId',
|
|
313
|
+
),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
298
316
|
if (value.action !== 'start' && value.action !== 'switch') {
|
|
299
317
|
throw new TypeError(
|
|
300
318
|
`parse-resolved decision names unknown action ${String(value.action)}`,
|
|
@@ -364,8 +382,8 @@ function classifyControllerTurn(
|
|
|
364
382
|
// ---------------------------------------------------------------------------
|
|
365
383
|
|
|
366
384
|
const RESTATED_REPLY_CONTRACT = [
|
|
367
|
-
'Reply again with exactly one JSON object `{ "action": …, … }` and no other text, selecting exactly one action from the closed set `respond` | `start` | `switch` | `dismiss` | `deliver` | `runtime`:',
|
|
368
|
-
'`{ "action": "respond", "text": … }`, `{ "action": "start", "playbookId": …, "input": … }`, `{ "action": "switch", "playbookId": …, "input": … }`, `{ "action": "dismiss" }`, `{ "action": "deliver" }`, or `{ "action": "runtime", "actionId": … }`; every `input` is one nonempty complete standalone request.',
|
|
385
|
+
'Reply again with exactly one JSON object `{ "action": …, … }` and no other text, selecting exactly one action from the closed set `respond` | `resume` | `start` | `switch` | `dismiss` | `deliver` | `runtime`:',
|
|
386
|
+
'`{ "action": "respond", "text": … }`, `{ "action": "resume", "playbookId": … }`, `{ "action": "start", "playbookId": …, "input": … }`, `{ "action": "switch", "playbookId": …, "input": … }`, `{ "action": "dismiss" }`, `{ "action": "deliver" }`, or `{ "action": "runtime", "actionId": … }`; every `input` is one nonempty complete standalone request.',
|
|
369
387
|
].join('\n');
|
|
370
388
|
|
|
371
389
|
function correctiveDecisionPrompt(prompt: string, reason: string): string {
|
|
@@ -432,6 +450,24 @@ function readDecisionReply(
|
|
|
432
450
|
if (shape !== undefined) return { reason: shape };
|
|
433
451
|
return { selection: { action, text: parsed.text as string } };
|
|
434
452
|
}
|
|
453
|
+
case 'resume': {
|
|
454
|
+
const shape = requireKeys(['playbookId']) ?? nonEmpty('playbookId');
|
|
455
|
+
if (shape !== undefined) return { reason: shape };
|
|
456
|
+
const playbookId = parsed.playbookId as string;
|
|
457
|
+
if (playbookId === selfPlaybookId) {
|
|
458
|
+
return {
|
|
459
|
+
reason: `the ${action} target may never be this Captain playbook itself`,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
if (
|
|
463
|
+
!options.enabledPlaybooks.some((entry) => entry.id === playbookId)
|
|
464
|
+
) {
|
|
465
|
+
return {
|
|
466
|
+
reason: `the ${action} target ${JSON.stringify(playbookId)} is not an enabled catalog id`,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
return { selection: { action, playbookId } };
|
|
470
|
+
}
|
|
435
471
|
case 'start':
|
|
436
472
|
case 'switch': {
|
|
437
473
|
const shape =
|
|
@@ -489,6 +525,9 @@ function selectionFromParsedDecision(
|
|
|
489
525
|
if (decision.action === 'deliver') {
|
|
490
526
|
return { action: 'deliver' };
|
|
491
527
|
}
|
|
528
|
+
if (decision.action === 'resume') {
|
|
529
|
+
return { action: 'resume', playbookId: decision.playbookId };
|
|
530
|
+
}
|
|
492
531
|
// A parse-resolved `start` / `switch` is always the exact command remainder
|
|
493
532
|
// supplied by the host (CAPTAIN-7 command table).
|
|
494
533
|
return {
|
|
@@ -514,11 +553,100 @@ function decisionOutputOf(
|
|
|
514
553
|
|
|
515
554
|
// ---------------------------------------------------------------------------
|
|
516
555
|
// Settlement validation: the returned settlement is the only evidence of
|
|
517
|
-
// effects, and the machine retains only its status, facts,
|
|
518
|
-
// disposition, and leaf-state summary (CAPPLAY-10). A
|
|
519
|
-
// is a host control-plane failure.
|
|
556
|
+
// effects, and the machine retains only its status, facts, bounded unresolved
|
|
557
|
+
// effects, receipt disposition, and leaf-state summary (CAPPLAY-10). A
|
|
558
|
+
// malformed settlement is a host control-plane failure.
|
|
520
559
|
// ---------------------------------------------------------------------------
|
|
521
560
|
|
|
561
|
+
const SETTLEMENT_GIT_OID_PATTERN = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
562
|
+
const SETTLEMENT_UNRESOLVED_CLASSIFICATIONS = new Set<
|
|
563
|
+
SettlementUnresolvedEffectEvidence['classification']
|
|
564
|
+
>([
|
|
565
|
+
'one-descendant-commit',
|
|
566
|
+
'multiple-commits',
|
|
567
|
+
'rewritten-or-non-descendant',
|
|
568
|
+
'worktree-only-change',
|
|
569
|
+
'concurrent-or-foreign-change',
|
|
570
|
+
'observation-ambiguous',
|
|
571
|
+
'incomplete',
|
|
572
|
+
]);
|
|
573
|
+
|
|
574
|
+
function validateSettlementUnresolvedEffects(
|
|
575
|
+
value: unknown,
|
|
576
|
+
): readonly SettlementUnresolvedEffectEvidence[] {
|
|
577
|
+
const detached = snapshotJsonValue(
|
|
578
|
+
value,
|
|
579
|
+
'controller settlement unresolvedEffects',
|
|
580
|
+
);
|
|
581
|
+
if (!Array.isArray(detached)) {
|
|
582
|
+
throw new TypeError(
|
|
583
|
+
'controller settlement unresolvedEffects must be an array',
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
for (const [index, raw] of detached.entries()) {
|
|
587
|
+
const path = `controller settlement unresolvedEffects[${index}]`;
|
|
588
|
+
if (!isRecord(raw)) {
|
|
589
|
+
throw new TypeError(`${path} must be an object`);
|
|
590
|
+
}
|
|
591
|
+
const allowed = new Set([
|
|
592
|
+
'classification',
|
|
593
|
+
'baselineHead',
|
|
594
|
+
'afterHead',
|
|
595
|
+
'commitOid',
|
|
596
|
+
]);
|
|
597
|
+
const unknown = Object.keys(raw).find((key) => !allowed.has(key));
|
|
598
|
+
if (unknown !== undefined) {
|
|
599
|
+
throw new TypeError(`${path} carries undeclared ${unknown}`);
|
|
600
|
+
}
|
|
601
|
+
if (
|
|
602
|
+
typeof raw.classification !== 'string' ||
|
|
603
|
+
!SETTLEMENT_UNRESOLVED_CLASSIFICATIONS.has(
|
|
604
|
+
raw.classification as SettlementUnresolvedEffectEvidence['classification'],
|
|
605
|
+
)
|
|
606
|
+
) {
|
|
607
|
+
throw new TypeError(`${path} classification is not supported`);
|
|
608
|
+
}
|
|
609
|
+
if (
|
|
610
|
+
typeof raw.baselineHead !== 'string' ||
|
|
611
|
+
!SETTLEMENT_GIT_OID_PATTERN.test(raw.baselineHead)
|
|
612
|
+
) {
|
|
613
|
+
throw new TypeError(`${path} baselineHead must be a Git OID`);
|
|
614
|
+
}
|
|
615
|
+
if (
|
|
616
|
+
'afterHead' in raw &&
|
|
617
|
+
(typeof raw.afterHead !== 'string' ||
|
|
618
|
+
!SETTLEMENT_GIT_OID_PATTERN.test(raw.afterHead))
|
|
619
|
+
) {
|
|
620
|
+
throw new TypeError(`${path} afterHead must be a Git OID`);
|
|
621
|
+
}
|
|
622
|
+
if (
|
|
623
|
+
raw.classification !== 'observation-ambiguous' &&
|
|
624
|
+
raw.classification !== 'incomplete' &&
|
|
625
|
+
!('afterHead' in raw)
|
|
626
|
+
) {
|
|
627
|
+
throw new TypeError(
|
|
628
|
+
`${path} afterHead is required for ${raw.classification}`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
if (raw.classification === 'one-descendant-commit') {
|
|
632
|
+
if (
|
|
633
|
+
typeof raw.commitOid !== 'string' ||
|
|
634
|
+
!SETTLEMENT_GIT_OID_PATTERN.test(raw.commitOid) ||
|
|
635
|
+
raw.commitOid !== raw.afterHead
|
|
636
|
+
) {
|
|
637
|
+
throw new TypeError(
|
|
638
|
+
`${path} commitOid must equal afterHead for one-descendant-commit`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
} else if ('commitOid' in raw) {
|
|
642
|
+
throw new TypeError(
|
|
643
|
+
`${path} commitOid is permitted only for one-descendant-commit`,
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return detached as unknown as readonly SettlementUnresolvedEffectEvidence[];
|
|
648
|
+
}
|
|
649
|
+
|
|
522
650
|
function validateSettlement(value: unknown): SettlementEvidence {
|
|
523
651
|
if (!isRecord(value)) {
|
|
524
652
|
throw new TypeError('controller settlement must be an object');
|
|
@@ -526,6 +654,7 @@ function validateSettlement(value: unknown): SettlementEvidence {
|
|
|
526
654
|
const allowed = new Set([
|
|
527
655
|
'status',
|
|
528
656
|
'facts',
|
|
657
|
+
'unresolvedEffects',
|
|
529
658
|
'reason',
|
|
530
659
|
'receipt',
|
|
531
660
|
'leafStateSummary',
|
|
@@ -550,6 +679,12 @@ function validateSettlement(value: unknown): SettlementEvidence {
|
|
|
550
679
|
) {
|
|
551
680
|
throw new TypeError('controller settlement facts must be a string array');
|
|
552
681
|
}
|
|
682
|
+
if (!Object.prototype.hasOwnProperty.call(value, 'unresolvedEffects')) {
|
|
683
|
+
throw new TypeError('controller settlement unresolvedEffects is required');
|
|
684
|
+
}
|
|
685
|
+
const unresolvedEffects = validateSettlementUnresolvedEffects(
|
|
686
|
+
value.unresolvedEffects,
|
|
687
|
+
);
|
|
553
688
|
if ('reason' in value && typeof value.reason !== 'string') {
|
|
554
689
|
throw new TypeError('controller settlement reason must be a string');
|
|
555
690
|
}
|
|
@@ -610,6 +745,7 @@ function validateSettlement(value: unknown): SettlementEvidence {
|
|
|
610
745
|
return Object.freeze({
|
|
611
746
|
status: value.status,
|
|
612
747
|
facts: Object.freeze([...(value.facts as readonly string[])]),
|
|
748
|
+
unresolvedEffects,
|
|
613
749
|
...(typeof value.reason === 'string' ? { reason: value.reason } : {}),
|
|
614
750
|
...(receipt === undefined ? {} : { receipt }),
|
|
615
751
|
...(typeof value.leafStateSummary === 'string'
|
|
@@ -774,6 +910,8 @@ function statusesForState(
|
|
|
774
910
|
}
|
|
775
911
|
}
|
|
776
912
|
|
|
913
|
+
const UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string> = new Set();
|
|
914
|
+
|
|
777
915
|
// Internal export surface for verification and tests. Not part of the
|
|
778
916
|
// stable public API; the leading underscore signals "subject to change."
|
|
779
917
|
export const _internal = {
|
|
@@ -790,15 +928,23 @@ export const _internal = {
|
|
|
790
928
|
validateParsedActingDecision,
|
|
791
929
|
statusesForState,
|
|
792
930
|
normalizeError,
|
|
931
|
+
UNFINISHED_FINAL_STATE_IDS,
|
|
793
932
|
};
|
|
794
933
|
|
|
795
934
|
// The Captain-specific spec handed to the shared runtime factory
|
|
796
935
|
// (slc/link.md §Output, DR-019). Generic machinery — actor wiring, boundary
|
|
797
936
|
// tracing, lifecycle, abort, the parked-session snapshot, and the DR-029
|
|
798
937
|
// describe/apply control surface — lives in @sublang/playbook/xstate-runtime.
|
|
799
|
-
const runtimeSpec:
|
|
938
|
+
const runtimeSpec: XStatePlaybookRuntimeSpecV3<ValidatedCaptainOptions> = {
|
|
800
939
|
label: 'CAPTAIN',
|
|
801
|
-
|
|
940
|
+
// DR-022 / slc/link.md: the declaration carries the value current at link
|
|
941
|
+
// time — a literal, never the loading engine's RUNTIME_ABI self-report,
|
|
942
|
+
// which would follow whatever engine loads the module and make the
|
|
943
|
+
// factory's skew check compare that engine with itself.
|
|
944
|
+
compat: { artifactSchema: 3, runtimeAbi: 1 },
|
|
945
|
+
// DR-040: the roleless session Captain has no delegated-player outcome,
|
|
946
|
+
// but schema 3 still makes that absence explicit rather than inferred.
|
|
947
|
+
outcomeAuthority: { governedPlayerStates: {} },
|
|
802
948
|
snapshotOptions: snapshotCaptainOptions,
|
|
803
949
|
machineInput: (options) => ({ enabledPlaybooks: options.enabledPlaybooks }),
|
|
804
950
|
classifyBossText: (text, ports, signal, snapshotOrState, boundary, options) =>
|
|
@@ -830,17 +976,57 @@ const runtimeSpec: XStatePlaybookRuntimeSpec<ValidatedCaptainOptions> = {
|
|
|
830
976
|
'receiptReason',
|
|
831
977
|
'receiptError',
|
|
832
978
|
'leafStateSummary',
|
|
979
|
+
'settlementUnresolvedEffects',
|
|
833
980
|
],
|
|
981
|
+
unfinishedFinalStateIds: UNFINISHED_FINAL_STATE_IDS,
|
|
834
982
|
statusesForState,
|
|
835
983
|
};
|
|
836
984
|
|
|
837
|
-
|
|
838
|
-
|
|
985
|
+
// DR-022's compat check fails fast at factory construction. The Captain is
|
|
986
|
+
// statically imported by the shell and both CLI front ends, so constructing
|
|
987
|
+
// here at module evaluation would turn a future compat mismatch into an
|
|
988
|
+
// uncaught ESM-load error that takes even `--help` down; constructing on
|
|
989
|
+
// the first runtime request keeps the failure inside the caught
|
|
990
|
+
// host-construction boundary that owes the Boss a setup diagnostic.
|
|
991
|
+
const rejectInternalCaptainRepositoryWork = async (): Promise<never> => {
|
|
992
|
+
throw new Error(
|
|
993
|
+
'the roleless session Captain cannot perform repository-governed work',
|
|
994
|
+
);
|
|
995
|
+
};
|
|
996
|
+
const INTERNAL_CAPTAIN_HOST_CAPABILITIES = Object.freeze({
|
|
997
|
+
// The engine does not consult this member for an explicitly empty governed
|
|
998
|
+
// set. Keeping a fail-closed implementation satisfies the schema-3 factory
|
|
999
|
+
// boundary without granting the internal controller repository authority.
|
|
1000
|
+
repository: Object.freeze({
|
|
1001
|
+
runExclusive: rejectInternalCaptainRepositoryWork,
|
|
1002
|
+
runDeferred: rejectInternalCaptainRepositoryWork,
|
|
1003
|
+
} satisfies XStateRepositoryCapability),
|
|
1004
|
+
effectLedger: Object.freeze({
|
|
1005
|
+
snapshot: emptyPlaybookEffectLedger,
|
|
1006
|
+
writeAhead: async (): Promise<never> => {
|
|
1007
|
+
throw new Error(
|
|
1008
|
+
'the roleless session Captain cannot write an effect ledger',
|
|
1009
|
+
);
|
|
1010
|
+
},
|
|
1011
|
+
}),
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
function buildCaptainPlaybookRuntimeFactory() {
|
|
1015
|
+
return createXStatePlaybookRuntime(captainMachine, runtimeSpec);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
let createCaptainPlaybookRuntime:
|
|
1019
|
+
| ReturnType<typeof buildCaptainPlaybookRuntimeFactory>
|
|
1020
|
+
| undefined;
|
|
839
1021
|
|
|
840
1022
|
export function createPlaybookRuntime(
|
|
841
1023
|
options: PlaybookRuntimeOptions,
|
|
842
1024
|
): PlaybookRuntime {
|
|
843
|
-
|
|
1025
|
+
createCaptainPlaybookRuntime ??= buildCaptainPlaybookRuntimeFactory();
|
|
1026
|
+
return createCaptainPlaybookRuntime({
|
|
1027
|
+
configuredOptions: options,
|
|
1028
|
+
hostCapabilities: INTERNAL_CAPTAIN_HOST_CAPABILITIES,
|
|
1029
|
+
});
|
|
844
1030
|
}
|
|
845
1031
|
|
|
846
1032
|
const factory: PlaybookRuntimeFactory<PlaybookRuntimeOptions> =
|
package/reference/sdlc/code.md
CHANGED
|
@@ -48,7 +48,6 @@ At the start of *every* phase, Captain shall append the following instruction:
|
|
|
48
48
|
Do not re-run tests or builds whose inputs have not changed since any previous reported run.
|
|
49
49
|
Make the phase's minimal changes and then one new commit, following @specs/packages/git.md; never amend an existing commit.
|
|
50
50
|
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
51
|
Coder is <coder-llm>; format the model token in conventional human form.
|
|
53
52
|
```
|
|
54
53
|
|