@sublang/playbook 14.0.0 → 14.1.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 +14 -14
- package/package.json +1 -1
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +1 -1
- package/reference/sdlc/code.playbook/bin/repository-effects.js +187 -14
- package/reference/sdlc/code.playbook/code.fsm.d.ts +1 -1
- package/reference/sdlc/code.playbook/host-capabilities.d.ts +6 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +3 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +357 -16
- package/reference/sdlc/code.playbook/playbook-captain.ts +459 -34
- package/reference/sdlc/code.playbook/session-host.d.ts +6 -3
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +1 -1
- package/reference/sdlc/decide.playbook/decide.playbook.js +25 -1
- package/reference/sdlc/decide.playbook/decide.playbook.ts +35 -2
- package/reference/sdlc/dev.md +15 -8
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +1 -1
- package/reference/sdlc/dev.playbook/dev.fsm.js +18 -12
- package/reference/sdlc/dev.playbook/dev.fsm.ts +18 -13
- package/reference/sdlc/dev.playbook/dev.gears.md +19 -12
- package/reference/sdlc/dev.playbook/dev.playbook.js +1 -1
- package/reference/sdlc/dev.playbook/dev.playbook.ts +1 -2
- package/reference/sdlc/dev.playbook/dev.registry.js +1 -1
- package/reference/sdlc/dev.playbook/dev.registry.ts +1 -1
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +1 -1
- package/reference/sdlc/review.playbook/review.fsm.d.ts +1 -1
- package/slc/link.md +32 -0
- package/src/runtime.d.ts +70 -0
- package/src/runtime.js +232 -1
- package/src/runtime.ts +358 -1
- package/src/xstate-playbook-runtime.d.ts +9 -10
- package/src/xstate-playbook-runtime.js +372 -20
- package/src/xstate-playbook-runtime.ts +460 -19
- package/src/xstate-runtime.d.ts +9 -1
- package/src/xstate-runtime.js +299 -19
- package/src/xstate-runtime.ts +362 -21
package/docs/cli.md
CHANGED
|
@@ -124,20 +124,20 @@ untouched
|
|
|
124
124
|
The current CODE, REVIEW, DECIDE, DEV, BRANCH, and PR workflows take their
|
|
125
125
|
deterministic initial event from the selecting Boss turn. CODE and DECIDE then
|
|
126
126
|
call REVIEW as a nested playbook, while DEV — the repository-aware planner
|
|
127
|
-
behind `/dev` —
|
|
128
|
-
DECIDE and then CODE, as nested playbooks. When the request names a
|
|
129
|
-
issue or asks for pull-request delivery, DEV reads the issue and its
|
|
130
|
-
while planning, calls BRANCH before that path and PR after CODE
|
|
131
|
-
ends with a merged pull request and a closed issue — or with a
|
|
132
|
-
that leaves the pull request open for a later `/pr`. Both are
|
|
133
|
-
own: `/branch <issue or request>` creates and checks out
|
|
134
|
-
request slug) at the current commit, changing nothing
|
|
135
|
-
checked-out branch, opens or reuses its pull request
|
|
136
|
-
branch, waits for the checks, fixes red checks through one
|
|
137
|
-
merges with a merge commit, and fast-forwards the local
|
|
138
|
-
repository without checks merges on CODE's nested review
|
|
139
|
-
and issue-aware DEV planning require the GitHub CLI `gh`,
|
|
140
|
-
the repository's GitHub remote
|
|
127
|
+
behind `/dev` — chooses the development path for a request and itself calls
|
|
128
|
+
CODE, or DECIDE and then CODE, as nested playbooks. When the request names a
|
|
129
|
+
GitHub issue or asks for pull-request delivery, DEV reads the issue and its
|
|
130
|
+
comments while planning, calls BRANCH before that path and PR after CODE
|
|
131
|
+
succeeds, and ends with a merged pull request and a closed issue — or with a
|
|
132
|
+
named failure that leaves the pull request open for a later `/pr`. Both are
|
|
133
|
+
usable on their own: `/branch <issue or request>` creates and checks out
|
|
134
|
+
`issue-N-slug` (or a request slug) at the current commit, changing nothing
|
|
135
|
+
else; `/pr` pushes the checked-out branch, opens or reuses its pull request
|
|
136
|
+
against the default branch, waits for the checks, fixes red checks through one
|
|
137
|
+
nested CODE call, merges with a merge commit, and fast-forwards the local
|
|
138
|
+
default branch. A repository without checks merges on CODE's nested review
|
|
139
|
+
alone. BRANCH, PR, and issue-aware DEV planning require the GitHub CLI `gh`,
|
|
140
|
+
authenticated for the repository's GitHub remote
|
|
141
141
|
([DR-050](https://github.com/sublang-ai/playbook/blob/main/specs/decisions/050-pull-request-delivery.md)).
|
|
142
142
|
Local role names do not imply continuity: each frame
|
|
143
143
|
uses the exact stable player IDs configured under its `roles` map. Equal IDs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sublang/playbook",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Composable XState v5 playbook runtime with compiled Captain, CODE, REVIEW, DECIDE, DEV, BRANCH, and PR workflows driven by GEARS specs.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -131,7 +131,7 @@ export declare const branchMachine: import("xstate").StateMachine<BranchContext,
|
|
|
131
131
|
} | {
|
|
132
132
|
type: "resumesCreateBranch";
|
|
133
133
|
params: unknown;
|
|
134
|
-
}, never, "failed" | "
|
|
134
|
+
}, never, "failed" | "ready" | "awaitBossReply" | "refused" | "createBranch" | "branched", string, Readonly<Record<string, never>>, {
|
|
135
135
|
readonly status: "branched";
|
|
136
136
|
/** Exact name of the new branch now checked out. */
|
|
137
137
|
readonly branch: string;
|
|
@@ -520,14 +520,169 @@ function projectionText(projection) {
|
|
|
520
520
|
return JSON.stringify(projection);
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
523
|
+
const MISSING_CONTENT_IDENTITY = Object.freeze({ kind: 'missing' });
|
|
524
|
+
|
|
525
|
+
function hasPath(projection, path) {
|
|
526
|
+
return Object.prototype.hasOwnProperty.call(projection, path);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// DR-062 §1: a rename, an unmerged path, a submodule, or a nested worktree is
|
|
530
|
+
// preserved when byte-equal and otherwise lost, because no content matching is
|
|
531
|
+
// attempted for them.
|
|
532
|
+
function nonComparableBaselineEntry(entry) {
|
|
533
|
+
return (
|
|
534
|
+
entry.kind === 'rename-or-copy' ||
|
|
535
|
+
entry.kind === 'unmerged' ||
|
|
536
|
+
(entry.submodule !== undefined && entry.submodule !== 'N...') ||
|
|
537
|
+
entry.worktree?.kind === 'directory'
|
|
528
538
|
);
|
|
529
539
|
}
|
|
530
540
|
|
|
541
|
+
// The entry the baseline HEAD tree held for a path: an ordinary porcelain-v2
|
|
542
|
+
// record carries its HEAD mode and OID, `000000` means the path did not exist
|
|
543
|
+
// there, an untracked path never did, and an unborn baseline HEAD holds none.
|
|
544
|
+
function baselineHeadEntry(baseline, entry) {
|
|
545
|
+
if (baseline.head === NULL_GIT_OID) return undefined;
|
|
546
|
+
if (entry.kind !== 'ordinary') return undefined;
|
|
547
|
+
if (entry.headMode === undefined || entry.headMode === '000000') {
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
return { mode: entry.headMode, oid: entry.headOid };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function sameTreeEntry(left, right) {
|
|
554
|
+
if (left === undefined || right === undefined) return left === right;
|
|
555
|
+
return left.mode === right.mode && left.oid === right.oid;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// The baseline recorded nothing at the path: a deleted worktree entry, or a
|
|
559
|
+
// staged deletion whose index holds no object.
|
|
560
|
+
function baselineEntryIsDeletion(entry) {
|
|
561
|
+
return entry.worktree === undefined
|
|
562
|
+
? entry.indexMode === '000000'
|
|
563
|
+
: entry.worktree.kind === 'missing';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// The entry one commit's tree holds for an exact path, or undefined when that
|
|
567
|
+
// tree holds none.
|
|
568
|
+
async function afterTreeEntry(worktree, head, path) {
|
|
569
|
+
const raw = await runGitText(worktree, [
|
|
570
|
+
'ls-tree',
|
|
571
|
+
'-z',
|
|
572
|
+
'--full-tree',
|
|
573
|
+
head,
|
|
574
|
+
'--',
|
|
575
|
+
path,
|
|
576
|
+
]);
|
|
577
|
+
for (const record of raw.split('\0')) {
|
|
578
|
+
if (record.length === 0) continue;
|
|
579
|
+
const tab = record.indexOf('\t');
|
|
580
|
+
const fields = tab < 0 ? [] : record.slice(0, tab).split(' ');
|
|
581
|
+
if (fields.length !== 3) {
|
|
582
|
+
throw new RepositoryObservationAmbiguousError(
|
|
583
|
+
'Git returned a malformed tree record',
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
if (record.slice(tab + 1) !== path) continue;
|
|
587
|
+
return { mode: fields[0], oid: fields[2] };
|
|
588
|
+
}
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// A blob addressed exactly as `worktreePathIdentity` addresses the same bytes
|
|
593
|
+
// in the working tree, so a committed object and the Boss's own content are
|
|
594
|
+
// comparable by the projection's content addressing rather than by Git OID.
|
|
595
|
+
async function blobContentIdentity(worktree, mode, oid) {
|
|
596
|
+
const bytes = await runGit(worktree, ['cat-file', 'blob', oid]);
|
|
597
|
+
return {
|
|
598
|
+
kind: mode === '120000' ? 'symlink' : 'file',
|
|
599
|
+
mode,
|
|
600
|
+
content: `sha256:${sha256(bytes)}`,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function baselineContentIdentity(worktree, entry) {
|
|
605
|
+
return entry.worktree === undefined
|
|
606
|
+
? blobContentIdentity(worktree, entry.indexMode, entry.indexOid)
|
|
607
|
+
: entry.worktree;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function sameContentIdentity(left, right) {
|
|
611
|
+
return (
|
|
612
|
+
left.kind === right.kind &&
|
|
613
|
+
left.mode === right.mode &&
|
|
614
|
+
left.content === right.content
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// DR-062 §1: the fate of one baseline projection entry at the end of a
|
|
619
|
+
// governed call — `preserved`, `altered-uncommitted`, `altered-committed`,
|
|
620
|
+
// `absorbed`, or `lost`.
|
|
621
|
+
async function baselineEntryFate(baseline, after, path, entry) {
|
|
622
|
+
if (hasPath(after.projection, path)) {
|
|
623
|
+
return JSON.stringify(after.projection[path]) === JSON.stringify(entry)
|
|
624
|
+
? 'preserved'
|
|
625
|
+
: 'altered-uncommitted';
|
|
626
|
+
}
|
|
627
|
+
// The entry left the working tree, so only a commit this call made can hold
|
|
628
|
+
// the Boss's content; a call that moved no HEAD made none.
|
|
629
|
+
if (baseline.head === after.head) return 'lost';
|
|
630
|
+
if (nonComparableBaselineEntry(entry)) return 'lost';
|
|
631
|
+
try {
|
|
632
|
+
const treeEntry = await afterTreeEntry(after.worktree, after.head, path);
|
|
633
|
+
if (sameTreeEntry(treeEntry, baselineHeadEntry(baseline, entry))) {
|
|
634
|
+
// The commit left the path exactly as the baseline HEAD held it, so it
|
|
635
|
+
// carried no change of the Boss's and his content is nowhere.
|
|
636
|
+
return 'lost';
|
|
637
|
+
}
|
|
638
|
+
if (baselineEntryIsDeletion(entry)) {
|
|
639
|
+
return treeEntry === undefined ? 'absorbed' : 'lost';
|
|
640
|
+
}
|
|
641
|
+
if (treeEntry?.mode === '160000') return 'lost';
|
|
642
|
+
const [baselineIdentity, treeIdentity] = await Promise.all([
|
|
643
|
+
baselineContentIdentity(after.worktree, entry),
|
|
644
|
+
treeEntry === undefined
|
|
645
|
+
? MISSING_CONTENT_IDENTITY
|
|
646
|
+
: blobContentIdentity(after.worktree, treeEntry.mode, treeEntry.oid),
|
|
647
|
+
]);
|
|
648
|
+
return sameContentIdentity(baselineIdentity, treeIdentity)
|
|
649
|
+
? 'absorbed'
|
|
650
|
+
: 'altered-committed';
|
|
651
|
+
} catch {
|
|
652
|
+
// Content the after HEAD cannot produce leaves the baseline entry
|
|
653
|
+
// unaccounted for, which fails closed exactly as a lost entry does.
|
|
654
|
+
return 'lost';
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function baselineEntryFates(baseline, after) {
|
|
659
|
+
const fates = [];
|
|
660
|
+
for (const [path, entry] of Object.entries(baseline.projection)) {
|
|
661
|
+
fates.push([path, await baselineEntryFate(baseline, after, path, entry)]);
|
|
662
|
+
}
|
|
663
|
+
return fates;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// DR-062 §3: the accounting a receipt records, present exactly when one list
|
|
667
|
+
// is nonempty so that an all-preserved call keeps today's receipt shape.
|
|
668
|
+
function preExistingFromFates(fates) {
|
|
669
|
+
const absorbed = [];
|
|
670
|
+
const altered = [];
|
|
671
|
+
const lost = [];
|
|
672
|
+
for (const [path, fate] of fates) {
|
|
673
|
+
if (fate === 'absorbed') absorbed.push(path);
|
|
674
|
+
else if (fate === 'altered-uncommitted' || fate === 'altered-committed') {
|
|
675
|
+
altered.push(path);
|
|
676
|
+
} else if (fate === 'lost') lost.push(path);
|
|
677
|
+
}
|
|
678
|
+
if (absorbed.length + altered.length + lost.length === 0) return undefined;
|
|
679
|
+
return {
|
|
680
|
+
absorbed: absorbed.sort(),
|
|
681
|
+
altered: altered.sort(),
|
|
682
|
+
lost: lost.sort(),
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
531
686
|
async function rawRepositoryStatus(worktree, env) {
|
|
532
687
|
return runGit(
|
|
533
688
|
worktree,
|
|
@@ -788,12 +943,13 @@ async function descendantCount(worktree, baselineHead, afterHead) {
|
|
|
788
943
|
return count;
|
|
789
944
|
}
|
|
790
945
|
|
|
791
|
-
function receipt(classification, baseline, after, commitOid) {
|
|
946
|
+
function receipt(classification, baseline, after, commitOid, preExisting) {
|
|
792
947
|
return deepFreeze({
|
|
793
948
|
classification,
|
|
794
949
|
baseline,
|
|
795
950
|
...(after === undefined ? {} : { after }),
|
|
796
951
|
...(commitOid === undefined ? {} : { commitOid }),
|
|
952
|
+
...(preExisting === undefined ? {} : { preExisting }),
|
|
797
953
|
});
|
|
798
954
|
}
|
|
799
955
|
|
|
@@ -821,16 +977,19 @@ export async function classifyRepositoryReceipt(
|
|
|
821
977
|
if (allowed.every((value) => value === 'unchanged')) {
|
|
822
978
|
return receipt('concurrent-or-foreign-change', baseline, after);
|
|
823
979
|
}
|
|
980
|
+
// DR-062 §2: a same-HEAD delta admits altered pre-existing entries beside
|
|
981
|
+
// new ones; only an entry whose change is nowhere keeps it ambiguous.
|
|
824
982
|
if (sameHead) {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
}
|
|
983
|
+
const fates = await baselineEntryFates(baseline, after);
|
|
984
|
+
const lost = fates.some(([, fate]) => fate === 'lost');
|
|
828
985
|
return receipt(
|
|
829
|
-
allowed.includes('one-descendant-commit')
|
|
986
|
+
!lost && allowed.includes('one-descendant-commit')
|
|
830
987
|
? 'worktree-only-change'
|
|
831
988
|
: 'observation-ambiguous',
|
|
832
989
|
baseline,
|
|
833
990
|
after,
|
|
991
|
+
undefined,
|
|
992
|
+
preExistingFromFates(fates),
|
|
834
993
|
);
|
|
835
994
|
}
|
|
836
995
|
// A HEAD that stopped naming a commit lost its history; every commit
|
|
@@ -847,10 +1006,24 @@ export async function classifyRepositoryReceipt(
|
|
|
847
1006
|
if (count !== 1) {
|
|
848
1007
|
return receipt('rewritten-or-non-descendant', baseline, after);
|
|
849
1008
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1009
|
+
// DR-062 §2: the one commit proves itself when every baseline entry is
|
|
1010
|
+
// preserved, absorbed, or altered and carried by it, and the after
|
|
1011
|
+
// projection holds nothing else.
|
|
1012
|
+
const fates = await baselineEntryFates(baseline, after);
|
|
1013
|
+
const unattributable =
|
|
1014
|
+
fates.some(
|
|
1015
|
+
([, fate]) => fate === 'lost' || fate === 'altered-uncommitted',
|
|
1016
|
+
) ||
|
|
1017
|
+
Object.keys(after.projection).some(
|
|
1018
|
+
(path) => !hasPath(baseline.projection, path),
|
|
1019
|
+
);
|
|
1020
|
+
return receipt(
|
|
1021
|
+
unattributable ? 'observation-ambiguous' : 'one-descendant-commit',
|
|
1022
|
+
baseline,
|
|
1023
|
+
after,
|
|
1024
|
+
unattributable ? undefined : after.head,
|
|
1025
|
+
preExistingFromFates(fates),
|
|
1026
|
+
);
|
|
854
1027
|
}
|
|
855
1028
|
|
|
856
1029
|
export async function captureRepositoryReceipt(baseline, options = {}) {
|
|
@@ -212,7 +212,7 @@ export declare const codingMachine: import("xstate").StateMachine<CodingContext,
|
|
|
212
212
|
} | {
|
|
213
213
|
type: "resumesIrTask";
|
|
214
214
|
params: unknown;
|
|
215
|
-
}, never, "done" | "failed" | "
|
|
215
|
+
}, never, "done" | "failed" | "ready" | "awaitBossReply" | "runFirstPhase" | "reviewFirstCommit" | "runIrTask" | "reviewIrTask" | "reportedReviewFailure", string, CodingInput, {
|
|
216
216
|
readonly status: "complete";
|
|
217
217
|
/** Exact identity of the last CODE-owned commit. */
|
|
218
218
|
readonly lastCodeCommit: string;
|
|
@@ -32,11 +32,17 @@ export interface PlaybookRepositoryObservation extends RepositoryIdentity {
|
|
|
32
32
|
readonly projection: Readonly<Record<string, JsonValue>>;
|
|
33
33
|
readonly projectionDigest: string;
|
|
34
34
|
}
|
|
35
|
+
export interface PlaybookRepositoryPreExistingChanges {
|
|
36
|
+
readonly absorbed: readonly string[];
|
|
37
|
+
readonly altered: readonly string[];
|
|
38
|
+
readonly lost: readonly string[];
|
|
39
|
+
}
|
|
35
40
|
export interface PlaybookRepositoryReceipt {
|
|
36
41
|
readonly classification: RepositoryReceiptClassification;
|
|
37
42
|
readonly baseline: PlaybookRepositoryObservation;
|
|
38
43
|
readonly after?: PlaybookRepositoryObservation;
|
|
39
44
|
readonly commitOid?: string;
|
|
45
|
+
readonly preExisting?: PlaybookRepositoryPreExistingChanges;
|
|
40
46
|
}
|
|
41
47
|
export interface RepositoryReceiptOptions {
|
|
42
48
|
readonly allowedDispositions: readonly PlaybookRepositoryDisposition[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Captain, type CaptainSession, type TuningSelection } from '@sublang/cligent/tmux-play';
|
|
2
2
|
import type { Effort, PermissionPolicy } from '@sublang/cligent';
|
|
3
|
-
import type { JsonValue, PlaybookEffectLedger, PlaybookEffectLedgerCommandBatch, PlaybookControlAction, PlaybookRuntime, PlaybookRuntimeSnapshot } from '@sublang/playbook/runtime';
|
|
3
|
+
import type { JsonValue, PlaybookEffectLedger, PlaybookEffectLedgerCommandBatch, PlaybookControlAction, PlaybookFailureCause, PlaybookRuntime, PlaybookRuntimeSnapshot } from '@sublang/playbook/runtime';
|
|
4
4
|
import { type CaptainControllerPort } from '../captain.playbook/captain.playbook.js';
|
|
5
5
|
import type { PlaybookSummaryPolicy } from './code.registry.js';
|
|
6
6
|
interface SessionAgent {
|
|
@@ -176,9 +176,11 @@ type PlaybookCaptainShellSnapshotValue = PlaybookCaptainShellSnapshotFields & ({
|
|
|
176
176
|
readonly frames: readonly PlaybookCaptainFrameSnapshot[];
|
|
177
177
|
readonly retainedEffectReconciliation?: PlaybookCaptainRetainedEffectReconciliation;
|
|
178
178
|
readonly pendingBossQuestions?: JsonValue;
|
|
179
|
+
/** DR-063 §2: the parked failure's cause travels with its error. */
|
|
179
180
|
readonly lastError?: {
|
|
180
181
|
readonly name: string;
|
|
181
182
|
readonly message: string;
|
|
183
|
+
readonly cause?: PlaybookFailureCause;
|
|
182
184
|
};
|
|
183
185
|
});
|
|
184
186
|
export type PlaybookCaptainShellSnapshot = DeepReadonly<PlaybookCaptainShellSnapshotValue>;
|