@sublang/playbook 9.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/docs/cli.md +51 -8
- package/docs/embedding.md +38 -12
- 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 +140 -10
- package/reference/sdlc/captain.playbook/captain.playbook.ts +188 -16
- 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 +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 +99 -7
- package/reference/sdlc/code.playbook/playbook-captain.js +1850 -72
- package/reference/sdlc/code.playbook/playbook-captain.ts +2759 -96
- package/reference/sdlc/decide.md +0 -1
- 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.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
|
@@ -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, } from '../../../src/xstate-runtime.js';
|
|
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
|
|
@@ -520,7 +621,10 @@ const runtimeSpec = {
|
|
|
520
621
|
// time — a literal, never the loading engine's RUNTIME_ABI self-report,
|
|
521
622
|
// which would follow whatever engine loads the module and make the
|
|
522
623
|
// factory's skew check compare that engine with itself.
|
|
523
|
-
compat: { artifactSchema:
|
|
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: {} },
|
|
524
628
|
snapshotOptions: snapshotCaptainOptions,
|
|
525
629
|
machineInput: (options) => ({ enabledPlaybooks: options.enabledPlaybooks }),
|
|
526
630
|
classifyBossText: (text, ports, signal, snapshotOrState, boundary, options) => classifyControllerTurn(text, ports, signal, snapshotOrState, boundary, options),
|
|
@@ -544,7 +648,9 @@ const runtimeSpec = {
|
|
|
544
648
|
'receiptReason',
|
|
545
649
|
'receiptError',
|
|
546
650
|
'leafStateSummary',
|
|
651
|
+
'settlementUnresolvedEffects',
|
|
547
652
|
],
|
|
653
|
+
unfinishedFinalStateIds: UNFINISHED_FINAL_STATE_IDS,
|
|
548
654
|
statusesForState,
|
|
549
655
|
};
|
|
550
656
|
// DR-022's compat check fails fast at factory construction. The Captain is
|
|
@@ -553,10 +659,34 @@ const runtimeSpec = {
|
|
|
553
659
|
// uncaught ESM-load error that takes even `--help` down; constructing on
|
|
554
660
|
// the first runtime request keeps the failure inside the caught
|
|
555
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
|
+
}
|
|
556
683
|
let createCaptainPlaybookRuntime;
|
|
557
684
|
export function createPlaybookRuntime(options) {
|
|
558
|
-
createCaptainPlaybookRuntime ??=
|
|
559
|
-
return createCaptainPlaybookRuntime(
|
|
685
|
+
createCaptainPlaybookRuntime ??= buildCaptainPlaybookRuntimeFactory();
|
|
686
|
+
return createCaptainPlaybookRuntime({
|
|
687
|
+
configuredOptions: options,
|
|
688
|
+
hostCapabilities: INTERNAL_CAPTAIN_HOST_CAPABILITIES,
|
|
689
|
+
});
|
|
560
690
|
}
|
|
561
691
|
const factory = createPlaybookRuntime;
|
|
562
692
|
export default factory;
|
|
@@ -31,12 +31,13 @@
|
|
|
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,
|
|
@@ -45,7 +46,8 @@ import {
|
|
|
45
46
|
type PlaybookCaptainInput,
|
|
46
47
|
type ScheduledStatus,
|
|
47
48
|
type XStateCaptainStrategyRun,
|
|
48
|
-
type
|
|
49
|
+
type XStatePlaybookRuntimeSpecV3,
|
|
50
|
+
type XStateRepositoryCapability,
|
|
49
51
|
} from '../../../src/xstate-runtime.js';
|
|
50
52
|
import {
|
|
51
53
|
captainMachine,
|
|
@@ -56,6 +58,7 @@ import {
|
|
|
56
58
|
type ParsedActingDecision,
|
|
57
59
|
type SettlementEvidence,
|
|
58
60
|
type SettlementReceiptEvidence,
|
|
61
|
+
type SettlementUnresolvedEffectEvidence,
|
|
59
62
|
} from './captain.fsm.js';
|
|
60
63
|
import type {
|
|
61
64
|
CaptainCallOptions,
|
|
@@ -123,9 +126,10 @@ export type {
|
|
|
123
126
|
/** One nonempty complete standalone request selected for `start` or `switch`. */
|
|
124
127
|
export type CaptainControllerInput = string;
|
|
125
128
|
|
|
126
|
-
/** One validated controller selection submitted through the port (DR-029). */
|
|
129
|
+
/** One validated controller selection submitted through the port (DR-029, DR-038). */
|
|
127
130
|
export type CaptainControllerSelection =
|
|
128
131
|
| { readonly action: 'respond'; readonly text: string }
|
|
132
|
+
| { readonly action: 'resume'; readonly playbookId: string }
|
|
129
133
|
| {
|
|
130
134
|
readonly action: 'start' | 'switch';
|
|
131
135
|
readonly playbookId: string;
|
|
@@ -294,6 +298,21 @@ function validateParsedActingDecision(value: unknown): ParsedActingDecision {
|
|
|
294
298
|
}
|
|
295
299
|
return { action: 'deliver' };
|
|
296
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
|
+
}
|
|
297
316
|
if (value.action !== 'start' && value.action !== 'switch') {
|
|
298
317
|
throw new TypeError(
|
|
299
318
|
`parse-resolved decision names unknown action ${String(value.action)}`,
|
|
@@ -363,8 +382,8 @@ function classifyControllerTurn(
|
|
|
363
382
|
// ---------------------------------------------------------------------------
|
|
364
383
|
|
|
365
384
|
const RESTATED_REPLY_CONTRACT = [
|
|
366
|
-
'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`:',
|
|
367
|
-
'`{ "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.',
|
|
368
387
|
].join('\n');
|
|
369
388
|
|
|
370
389
|
function correctiveDecisionPrompt(prompt: string, reason: string): string {
|
|
@@ -431,6 +450,24 @@ function readDecisionReply(
|
|
|
431
450
|
if (shape !== undefined) return { reason: shape };
|
|
432
451
|
return { selection: { action, text: parsed.text as string } };
|
|
433
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
|
+
}
|
|
434
471
|
case 'start':
|
|
435
472
|
case 'switch': {
|
|
436
473
|
const shape =
|
|
@@ -488,6 +525,9 @@ function selectionFromParsedDecision(
|
|
|
488
525
|
if (decision.action === 'deliver') {
|
|
489
526
|
return { action: 'deliver' };
|
|
490
527
|
}
|
|
528
|
+
if (decision.action === 'resume') {
|
|
529
|
+
return { action: 'resume', playbookId: decision.playbookId };
|
|
530
|
+
}
|
|
491
531
|
// A parse-resolved `start` / `switch` is always the exact command remainder
|
|
492
532
|
// supplied by the host (CAPTAIN-7 command table).
|
|
493
533
|
return {
|
|
@@ -513,11 +553,100 @@ function decisionOutputOf(
|
|
|
513
553
|
|
|
514
554
|
// ---------------------------------------------------------------------------
|
|
515
555
|
// Settlement validation: the returned settlement is the only evidence of
|
|
516
|
-
// effects, and the machine retains only its status, facts,
|
|
517
|
-
// disposition, and leaf-state summary (CAPPLAY-10). A
|
|
518
|
-
// 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.
|
|
519
559
|
// ---------------------------------------------------------------------------
|
|
520
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
|
+
|
|
521
650
|
function validateSettlement(value: unknown): SettlementEvidence {
|
|
522
651
|
if (!isRecord(value)) {
|
|
523
652
|
throw new TypeError('controller settlement must be an object');
|
|
@@ -525,6 +654,7 @@ function validateSettlement(value: unknown): SettlementEvidence {
|
|
|
525
654
|
const allowed = new Set([
|
|
526
655
|
'status',
|
|
527
656
|
'facts',
|
|
657
|
+
'unresolvedEffects',
|
|
528
658
|
'reason',
|
|
529
659
|
'receipt',
|
|
530
660
|
'leafStateSummary',
|
|
@@ -549,6 +679,12 @@ function validateSettlement(value: unknown): SettlementEvidence {
|
|
|
549
679
|
) {
|
|
550
680
|
throw new TypeError('controller settlement facts must be a string array');
|
|
551
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
|
+
);
|
|
552
688
|
if ('reason' in value && typeof value.reason !== 'string') {
|
|
553
689
|
throw new TypeError('controller settlement reason must be a string');
|
|
554
690
|
}
|
|
@@ -609,6 +745,7 @@ function validateSettlement(value: unknown): SettlementEvidence {
|
|
|
609
745
|
return Object.freeze({
|
|
610
746
|
status: value.status,
|
|
611
747
|
facts: Object.freeze([...(value.facts as readonly string[])]),
|
|
748
|
+
unresolvedEffects,
|
|
612
749
|
...(typeof value.reason === 'string' ? { reason: value.reason } : {}),
|
|
613
750
|
...(receipt === undefined ? {} : { receipt }),
|
|
614
751
|
...(typeof value.leafStateSummary === 'string'
|
|
@@ -773,6 +910,8 @@ function statusesForState(
|
|
|
773
910
|
}
|
|
774
911
|
}
|
|
775
912
|
|
|
913
|
+
const UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string> = new Set();
|
|
914
|
+
|
|
776
915
|
// Internal export surface for verification and tests. Not part of the
|
|
777
916
|
// stable public API; the leading underscore signals "subject to change."
|
|
778
917
|
export const _internal = {
|
|
@@ -789,19 +928,23 @@ export const _internal = {
|
|
|
789
928
|
validateParsedActingDecision,
|
|
790
929
|
statusesForState,
|
|
791
930
|
normalizeError,
|
|
931
|
+
UNFINISHED_FINAL_STATE_IDS,
|
|
792
932
|
};
|
|
793
933
|
|
|
794
934
|
// The Captain-specific spec handed to the shared runtime factory
|
|
795
935
|
// (slc/link.md §Output, DR-019). Generic machinery — actor wiring, boundary
|
|
796
936
|
// tracing, lifecycle, abort, the parked-session snapshot, and the DR-029
|
|
797
937
|
// describe/apply control surface — lives in @sublang/playbook/xstate-runtime.
|
|
798
|
-
const runtimeSpec:
|
|
938
|
+
const runtimeSpec: XStatePlaybookRuntimeSpecV3<ValidatedCaptainOptions> = {
|
|
799
939
|
label: 'CAPTAIN',
|
|
800
940
|
// DR-022 / slc/link.md: the declaration carries the value current at link
|
|
801
941
|
// time — a literal, never the loading engine's RUNTIME_ABI self-report,
|
|
802
942
|
// which would follow whatever engine loads the module and make the
|
|
803
943
|
// factory's skew check compare that engine with itself.
|
|
804
|
-
compat: { artifactSchema:
|
|
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: {} },
|
|
805
948
|
snapshotOptions: snapshotCaptainOptions,
|
|
806
949
|
machineInput: (options) => ({ enabledPlaybooks: options.enabledPlaybooks }),
|
|
807
950
|
classifyBossText: (text, ports, signal, snapshotOrState, boundary, options) =>
|
|
@@ -833,7 +976,9 @@ const runtimeSpec: XStatePlaybookRuntimeSpec<ValidatedCaptainOptions> = {
|
|
|
833
976
|
'receiptReason',
|
|
834
977
|
'receiptError',
|
|
835
978
|
'leafStateSummary',
|
|
979
|
+
'settlementUnresolvedEffects',
|
|
836
980
|
],
|
|
981
|
+
unfinishedFinalStateIds: UNFINISHED_FINAL_STATE_IDS,
|
|
837
982
|
statusesForState,
|
|
838
983
|
};
|
|
839
984
|
|
|
@@ -843,18 +988,45 @@ const runtimeSpec: XStatePlaybookRuntimeSpec<ValidatedCaptainOptions> = {
|
|
|
843
988
|
// uncaught ESM-load error that takes even `--help` down; constructing on
|
|
844
989
|
// the first runtime request keeps the failure inside the caught
|
|
845
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
|
+
|
|
846
1018
|
let createCaptainPlaybookRuntime:
|
|
847
|
-
|
|
|
1019
|
+
| ReturnType<typeof buildCaptainPlaybookRuntimeFactory>
|
|
848
1020
|
| undefined;
|
|
849
1021
|
|
|
850
1022
|
export function createPlaybookRuntime(
|
|
851
1023
|
options: PlaybookRuntimeOptions,
|
|
852
1024
|
): PlaybookRuntime {
|
|
853
|
-
createCaptainPlaybookRuntime ??=
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1025
|
+
createCaptainPlaybookRuntime ??= buildCaptainPlaybookRuntimeFactory();
|
|
1026
|
+
return createCaptainPlaybookRuntime({
|
|
1027
|
+
configuredOptions: options,
|
|
1028
|
+
hostCapabilities: INTERNAL_CAPTAIN_HOST_CAPABILITIES,
|
|
1029
|
+
});
|
|
858
1030
|
}
|
|
859
1031
|
|
|
860
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
|
|