@sublang/playbook 15.1.0 → 16.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/embedding.md +2 -2
- package/package.json +1 -1
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
- package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
- package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
- package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
- package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
- package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
- package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
- package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
- package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
- package/reference/sdlc/code.playbook/code.fsm.d.ts +143 -90
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
- package/reference/sdlc/code.playbook/code.fsm.js +609 -649
- package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
- package/reference/sdlc/code.playbook/code.gears.md +39 -29
- package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
- package/reference/sdlc/code.playbook/code.playbook.js +166 -147
- package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
- package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
- package/reference/sdlc/code.playbook/code.registry.ts +6 -2
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +42 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +829 -194
- package/reference/sdlc/code.playbook/playbook-captain.ts +1227 -220
- package/reference/sdlc/decide.md +1 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +270 -154
- package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
- package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -59
- package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3696
- package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5244
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
- package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
- package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +113 -131
- package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
- package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
- package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
- package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
- package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
- package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
- package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +92 -85
- package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
- package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
- package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
- package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
- package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
- package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
- package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
- package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
- package/reference/sdlc/review.md +3 -3
- package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
- package/reference/sdlc/review.playbook/review.fsm.js +479 -550
- package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
- package/reference/sdlc/review.playbook/review.gears.md +32 -29
- package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
- package/reference/sdlc/review.playbook/review.playbook.js +180 -155
- package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
- package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
- package/reference/sdlc/review.playbook/review.registry.js +3 -3
- package/reference/sdlc/review.playbook/review.registry.ts +9 -5
- package/slc/gears2fsm.md +72 -3
- package/slc/link.md +50 -27
- package/slc/materialize-link.mjs +51 -13
- package/slc/optimize.md +4 -1
- package/slc/prefix-prompts.mjs +29 -28
- package/slc/prefix.md +27 -24
- package/slc/slc.pin-inputs.json +11 -2
- package/slc/text2gears.md +71 -11
- package/src/xstate-playbook-runtime.d.ts +54 -15
- package/src/xstate-playbook-runtime.js +1154 -248
- package/src/xstate-playbook-runtime.ts +1603 -317
package/docs/embedding.md
CHANGED
|
@@ -38,7 +38,7 @@ tree, which pnpm's strict linking will not allow.
|
|
|
38
38
|
|
|
39
39
|
```ts
|
|
40
40
|
import createPlaybookRuntime, {
|
|
41
|
-
type
|
|
41
|
+
type PlaybookHostCapabilities,
|
|
42
42
|
} from '@sublang/playbook/review/playbook';
|
|
43
43
|
import type {
|
|
44
44
|
CaptainCallOptions,
|
|
@@ -156,7 +156,7 @@ const playbookSessionId = randomUUID();
|
|
|
156
156
|
// machine input, or a persisted snapshot. A host outside the CLI constructs
|
|
157
157
|
// the repository and effect-ledger members through the facade described in
|
|
158
158
|
// "Constructing worktree host capabilities" below.
|
|
159
|
-
declare const hostCapabilities:
|
|
159
|
+
declare const hostCapabilities: PlaybookHostCapabilities;
|
|
160
160
|
|
|
161
161
|
const runtime = createPlaybookRuntime({
|
|
162
162
|
configuredOptions: {},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sublang/playbook",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "16.0.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",
|
|
@@ -1,34 +1,46 @@
|
|
|
1
1
|
export type BranchStateId = 'createBranch';
|
|
2
2
|
export type BranchSourceItem = 'BRANCH-1';
|
|
3
|
+
/**
|
|
4
|
+
* Stable working-leaf ids that may suspend on a Boss question and be resumed
|
|
5
|
+
* by `BOSS_REPLY`. The machine has at most one active player task, so the
|
|
6
|
+
* scalar Boss-reply form applies.
|
|
7
|
+
*/
|
|
8
|
+
declare const RESUMABLE_STATE_IDS: readonly ["createBranch"];
|
|
9
|
+
export type ResumableStateId = (typeof RESUMABLE_STATE_IDS)[number];
|
|
10
|
+
/** JSON-safe normalization of a rejected invocation, retained for inspection. */
|
|
11
|
+
export type NormalizedError = {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly message: string;
|
|
14
|
+
readonly stack?: string;
|
|
15
|
+
};
|
|
16
|
+
/** Question record raised by the suspended Coder working leaf. */
|
|
3
17
|
export type PendingBossQuestion = {
|
|
4
|
-
readonly questionId:
|
|
5
|
-
readonly resumeStateId:
|
|
6
|
-
readonly sourceItem:
|
|
18
|
+
readonly questionId: ResumableStateId;
|
|
19
|
+
readonly resumeStateId: ResumableStateId;
|
|
20
|
+
readonly sourceItem: BranchSourceItem;
|
|
7
21
|
readonly asker: {
|
|
8
22
|
readonly kind: 'role';
|
|
9
23
|
readonly roleId: 'coder';
|
|
10
24
|
};
|
|
11
25
|
readonly question: string;
|
|
12
26
|
};
|
|
27
|
+
/** Typed input for the delegated `player` actor. */
|
|
13
28
|
export type PlayerInput = {
|
|
14
29
|
readonly stateId: 'createBranch';
|
|
15
30
|
readonly role: 'coder';
|
|
16
31
|
readonly sourceItem: 'BRANCH-1';
|
|
17
32
|
readonly prompt: string;
|
|
18
33
|
readonly result: Readonly<Record<string, string>>;
|
|
34
|
+
/** Backs the prompt's `<caller-input>` placeholder with the exact caller input. */
|
|
19
35
|
readonly callerInput: string;
|
|
20
36
|
readonly pendingBossQuestion?: PendingBossQuestion;
|
|
21
37
|
readonly bossReply?: string;
|
|
22
38
|
};
|
|
39
|
+
/** Discriminated result contract of the delegated `player` actor. */
|
|
23
40
|
export type PlayerOutput = {
|
|
24
41
|
readonly guard: 'branched';
|
|
25
42
|
readonly branch: string;
|
|
26
|
-
/**
|
|
27
|
-
* Receipt-owned effect evidence (DR-045): the branching call's
|
|
28
|
-
* `unchanged` receipt observes the commit the branch was created from
|
|
29
|
-
* as HEAD, so the linked runtime injects it and never takes it from
|
|
30
|
-
* Coder's prose.
|
|
31
|
-
*/
|
|
43
|
+
/** Exact base revision from repository authority, not from Coder's prose. */
|
|
32
44
|
readonly baseRevision: string;
|
|
33
45
|
readonly issueSummary: string;
|
|
34
46
|
} | {
|
|
@@ -38,47 +50,55 @@ export type PlayerOutput = {
|
|
|
38
50
|
readonly guard: 'needsBossReply';
|
|
39
51
|
readonly question: string;
|
|
40
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* Terminal result: exactly the packaged `branch` workflow's public output
|
|
55
|
+
* interface (workflow-contracts.json), derived from typed context.
|
|
56
|
+
*/
|
|
41
57
|
export type BranchPlaybookOutput = {
|
|
42
58
|
readonly status: 'branched';
|
|
43
|
-
/** Exact
|
|
59
|
+
/** Exact new branch name. */
|
|
44
60
|
readonly branch: string;
|
|
45
|
-
/** Exact receipt-observed revision the branch was created from. */
|
|
61
|
+
/** Exact receipt-observed repository revision the branch was created from. */
|
|
46
62
|
readonly baseRevision: string;
|
|
47
|
-
/**
|
|
63
|
+
/** Concise issue-and-comments or request summary. */
|
|
48
64
|
readonly issueSummary: string;
|
|
49
65
|
} | {
|
|
50
66
|
readonly status: 'refused';
|
|
51
|
-
/**
|
|
67
|
+
/** Complete refusal report. */
|
|
52
68
|
readonly coderOutput: string;
|
|
53
69
|
};
|
|
54
|
-
|
|
70
|
+
/** The machine reads no input: the caller input arrives on `START_BRANCH`. */
|
|
71
|
+
export type BranchInput = Readonly<Record<never, never>>;
|
|
72
|
+
export type BranchCompletion = 'branched' | 'refused';
|
|
55
73
|
export type BranchContext = {
|
|
56
74
|
readonly callerInput?: string;
|
|
57
75
|
readonly branch?: string;
|
|
58
|
-
/** Observed HEAD of the branching call's unchanged receipt (DR-045). */
|
|
59
76
|
readonly baseRevision?: string;
|
|
60
77
|
readonly issueSummary?: string;
|
|
61
78
|
readonly coderOutput?: string;
|
|
62
|
-
readonly completion?:
|
|
63
|
-
readonly lastError?:
|
|
79
|
+
readonly completion?: BranchCompletion;
|
|
80
|
+
readonly lastError?: NormalizedError;
|
|
64
81
|
readonly pendingBossQuestion?: PendingBossQuestion;
|
|
65
82
|
readonly bossReply?: string;
|
|
66
83
|
};
|
|
84
|
+
/** Typed Boss surfaces: the caller's entry event and the Boss reply. */
|
|
67
85
|
export type BranchEvent = {
|
|
68
86
|
readonly type: 'START_BRANCH';
|
|
69
87
|
readonly callerInput: string;
|
|
70
88
|
} | {
|
|
71
89
|
readonly type: 'BOSS_REPLY';
|
|
72
90
|
readonly answer: string;
|
|
73
|
-
readonly questionId?:
|
|
91
|
+
readonly questionId?: ResumableStateId;
|
|
74
92
|
};
|
|
93
|
+
/** No parallel group in this playbook. */
|
|
94
|
+
export declare const concurrentRoleSets: readonly (readonly string[])[];
|
|
75
95
|
export declare const branchMachine: import("xstate").StateMachine<BranchContext, {
|
|
76
96
|
readonly type: "START_BRANCH";
|
|
77
97
|
readonly callerInput: string;
|
|
78
98
|
} | {
|
|
79
99
|
readonly type: "BOSS_REPLY";
|
|
80
100
|
readonly answer: string;
|
|
81
|
-
readonly questionId?:
|
|
101
|
+
readonly questionId?: ResumableStateId;
|
|
82
102
|
}, {
|
|
83
103
|
[x: string]: import("xstate").ActorRefFromLogic<import("xstate").PromiseActorLogic<PlayerOutput, PlayerInput, import("xstate").EventObject>> | undefined;
|
|
84
104
|
}, {
|
|
@@ -96,16 +116,22 @@ export declare const branchMachine: import("xstate").StateMachine<BranchContext,
|
|
|
96
116
|
type: "rememberActorError";
|
|
97
117
|
params: import("xstate").NonReducibleUnknown;
|
|
98
118
|
} | {
|
|
99
|
-
type: "
|
|
119
|
+
type: "rememberMalformedPlayerOutput";
|
|
100
120
|
params: import("xstate").NonReducibleUnknown;
|
|
101
121
|
} | {
|
|
102
|
-
type: "
|
|
122
|
+
type: "clearBossReplyContext";
|
|
103
123
|
params: import("xstate").NonReducibleUnknown;
|
|
104
124
|
} | {
|
|
105
|
-
type: "
|
|
125
|
+
type: "setPendingBossQuestion";
|
|
126
|
+
params: {
|
|
127
|
+
readonly stateId: ResumableStateId;
|
|
128
|
+
readonly sourceItem: BranchSourceItem;
|
|
129
|
+
};
|
|
130
|
+
} | {
|
|
131
|
+
type: "rememberMalformedBossReply";
|
|
106
132
|
params: import("xstate").NonReducibleUnknown;
|
|
107
133
|
} | {
|
|
108
|
-
type: "
|
|
134
|
+
type: "acceptBossReply";
|
|
109
135
|
params: import("xstate").NonReducibleUnknown;
|
|
110
136
|
} | {
|
|
111
137
|
type: "startBranch";
|
|
@@ -118,9 +144,16 @@ export declare const branchMachine: import("xstate").StateMachine<BranchContext,
|
|
|
118
144
|
params: import("xstate").NonReducibleUnknown;
|
|
119
145
|
}, {
|
|
120
146
|
type: "needsBossReply";
|
|
121
|
-
params:
|
|
147
|
+
params: {
|
|
148
|
+
readonly stateId: ResumableStateId;
|
|
149
|
+
};
|
|
122
150
|
} | {
|
|
123
|
-
type: "
|
|
151
|
+
type: "canResume";
|
|
152
|
+
params: {
|
|
153
|
+
readonly stateId: ResumableStateId;
|
|
154
|
+
};
|
|
155
|
+
} | {
|
|
156
|
+
type: "givesRequest";
|
|
124
157
|
params: unknown;
|
|
125
158
|
} | {
|
|
126
159
|
type: "isBranched";
|
|
@@ -128,20 +161,17 @@ export declare const branchMachine: import("xstate").StateMachine<BranchContext,
|
|
|
128
161
|
} | {
|
|
129
162
|
type: "isRefused";
|
|
130
163
|
params: unknown;
|
|
131
|
-
} | {
|
|
132
|
-
type: "resumesCreateBranch";
|
|
133
|
-
params: unknown;
|
|
134
|
-
}, never, "failed" | "ready" | "awaitBossReply" | "refused" | "createBranch" | "branched", string, Readonly<Record<string, never>>, {
|
|
164
|
+
}, never, "failed" | "ready" | "awaitBossReply" | "refused" | "createBranch" | "branched", string, Readonly<Record<never, never>>, {
|
|
135
165
|
readonly status: "branched";
|
|
136
|
-
/** Exact
|
|
166
|
+
/** Exact new branch name. */
|
|
137
167
|
readonly branch: string;
|
|
138
|
-
/** Exact receipt-observed revision the branch was created from. */
|
|
168
|
+
/** Exact receipt-observed repository revision the branch was created from. */
|
|
139
169
|
readonly baseRevision: string;
|
|
140
|
-
/**
|
|
170
|
+
/** Concise issue-and-comments or request summary. */
|
|
141
171
|
readonly issueSummary: string;
|
|
142
172
|
} | {
|
|
143
173
|
readonly status: "refused";
|
|
144
|
-
/**
|
|
174
|
+
/** Complete refusal report. */
|
|
145
175
|
readonly coderOutput: string;
|
|
146
176
|
}, import("xstate").EventObject, import("xstate").MetaObject, {
|
|
147
177
|
id: "branch";
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
-
//
|
|
4
|
-
// Generated by slc/gears2fsm.md from ./branch.gears.md.
|
|
5
3
|
import { assign, fromPromise, setup } from 'xstate';
|
|
4
|
+
/**
|
|
5
|
+
* Stable working-leaf ids that may suspend on a Boss question and be resumed
|
|
6
|
+
* by `BOSS_REPLY`. The machine has at most one active player task, so the
|
|
7
|
+
* scalar Boss-reply form applies.
|
|
8
|
+
*/
|
|
9
|
+
const RESUMABLE_STATE_IDS = ['createBranch'];
|
|
10
|
+
/** No parallel group in this playbook. */
|
|
11
|
+
export const concurrentRoleSets = [];
|
|
6
12
|
const CREATE_BRANCH_PROMPT = [
|
|
7
|
-
'> Original request: <caller-input>',
|
|
8
|
-
'',
|
|
9
13
|
'Prepare a new branch for this work without changing any file or making any commit.',
|
|
10
14
|
'Identify the GitHub issue the request names, if any, and read it with its comments (`gh issue view --comments` with the issue number).',
|
|
11
15
|
'If the request could refer to more than one issue, or does not say which work to branch for, ask Boss before creating anything.',
|
|
@@ -14,19 +18,22 @@ const CREATE_BRANCH_PROMPT = [
|
|
|
14
18
|
'Create the branch from the current commit and check it out; do not pull, reset, stash, or move HEAD to another commit.',
|
|
15
19
|
'Report the exact branch name, the commit it was created from, and a concise summary of the issue and its comments, or of the request when no issue is named.',
|
|
16
20
|
'If the working tree is not clean, `gh` is not authenticated, the named issue does not exist or cannot be read, or a branch with that name already exists locally or on the remote, create nothing and report the failure with its reason.',
|
|
21
|
+
'',
|
|
22
|
+
'> Original request: <caller-input>',
|
|
17
23
|
].join('\n');
|
|
24
|
+
const NEEDS_BOSS_REPLY_RESULT = "The acting agent's prose surfaces a clarifying question for Boss that the agent cannot answer alone. Output shall include `question: <verbatim question text from the acting agent's prose>`.";
|
|
18
25
|
const CREATE_BRANCH_RESULTS = {
|
|
19
|
-
branched:
|
|
20
|
-
refused:
|
|
21
|
-
needsBossReply:
|
|
26
|
+
branched: "Coder affirmatively reported that it created the new branch from the current commit and checked it out, with the exact branch name and a concise summary of the issue and its comments, or of the request when no issue is named; the absence of a reported obstacle is not support. The branch workflow is then complete and returns the exact branch name, the exact base revision taken from repository authority, and the issue summary to its caller. Output shall include `branch: <exact branch name>`, `baseRevision: <exact base revision from repository authority, not from Coder's prose>`, and `issueSummary: <concise summary of the issue and its comments, or of the request when no issue is named>`.",
|
|
27
|
+
refused: "Coder affirmatively reported that it created no branch and gave the failure with its reason, such as a working tree that is not clean, gh not being authenticated, a named issue that does not exist or cannot be read, or a branch with that name already existing locally or on the remote. The branch workflow then fails and reports Coder's complete result with its reason to its caller; no branch was created. Output shall include `coderOutput: <verbatim final text>`.",
|
|
28
|
+
needsBossReply: NEEDS_BOSS_REPLY_RESULT,
|
|
22
29
|
};
|
|
23
30
|
const STATE_DESCRIPTIONS = {
|
|
24
|
-
ready: 'Waiting for a development request to branch for.',
|
|
25
|
-
createBranch: 'Coder is
|
|
26
|
-
awaitBossReply:
|
|
27
|
-
failed: 'The branch workflow failed and is waiting for
|
|
28
|
-
branched:
|
|
29
|
-
refused:
|
|
31
|
+
ready: 'Waiting for the caller to give a development request to branch for.',
|
|
32
|
+
createBranch: 'Coder is identifying the request\'s issue and creating and checking out a new branch at the current commit.',
|
|
33
|
+
awaitBossReply: "Waiting for Boss to answer the acting agent's question.",
|
|
34
|
+
failed: 'The branch workflow failed and is waiting for the caller to give the request again.',
|
|
35
|
+
branched: 'A new branch for the request was created at the current commit and checked out; the workflow returns its exact name, the exact base revision taken from repository authority, and the issue summary.',
|
|
36
|
+
refused: "No branch was created: Coder reported the failure with its reason, and the workflow fails and reports Coder's complete result to its caller.",
|
|
30
37
|
};
|
|
31
38
|
function playbookMeta(stateId, role) {
|
|
32
39
|
return {
|
|
@@ -37,9 +44,7 @@ function playbookMeta(stateId, role) {
|
|
|
37
44
|
},
|
|
38
45
|
};
|
|
39
46
|
}
|
|
40
|
-
|
|
41
|
-
// workflow succeeded or failed, so BRANCH's own caller learns that from the
|
|
42
|
-
// machine rather than from BRANCH's output fields.
|
|
47
|
+
/** A final state also publishes whether its outcome is success or failure. */
|
|
43
48
|
function terminalMeta(stateId, terminal) {
|
|
44
49
|
return {
|
|
45
50
|
playbook: {
|
|
@@ -59,33 +64,69 @@ function isRecord(value) {
|
|
|
59
64
|
function isNonEmptyString(value) {
|
|
60
65
|
return typeof value === 'string' && value.trim().length > 0;
|
|
61
66
|
}
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Narrows an unknown event's `output` structurally to the declared `player`
|
|
69
|
+
* contract. Output missing a field its guard requires narrows to `undefined`.
|
|
70
|
+
*/
|
|
71
|
+
function playerOutputOf(event) {
|
|
72
|
+
if (!isRecord(event) || !isRecord(event.output))
|
|
64
73
|
return undefined;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
const output = event.output;
|
|
75
|
+
if (output.guard === 'branched' &&
|
|
76
|
+
isNonEmptyString(output.branch) &&
|
|
77
|
+
isNonEmptyString(output.baseRevision) &&
|
|
78
|
+
isNonEmptyString(output.issueSummary)) {
|
|
79
|
+
return {
|
|
80
|
+
guard: 'branched',
|
|
81
|
+
branch: output.branch,
|
|
82
|
+
baseRevision: output.baseRevision,
|
|
83
|
+
issueSummary: output.issueSummary,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (output.guard === 'refused' && isNonEmptyString(output.coderOutput)) {
|
|
87
|
+
return { guard: 'refused', coderOutput: output.coderOutput };
|
|
88
|
+
}
|
|
89
|
+
if (output.guard === 'needsBossReply' && isNonEmptyString(output.question)) {
|
|
90
|
+
return { guard: 'needsBossReply', question: output.question };
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
73
93
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
94
|
+
/** Narrows an unknown event's `error` structurally to a JSON-safe record. */
|
|
95
|
+
function normalizedErrorOf(event) {
|
|
96
|
+
const error = isRecord(event) ? event.error : undefined;
|
|
97
|
+
if (error instanceof Error) {
|
|
98
|
+
const name = error.name || 'Error';
|
|
99
|
+
return typeof error.stack === 'string'
|
|
100
|
+
? { name, message: error.message, stack: error.stack }
|
|
101
|
+
: { name, message: error.message };
|
|
102
|
+
}
|
|
103
|
+
if (isRecord(error)) {
|
|
104
|
+
const name = typeof error.name === 'string' ? error.name : 'Error';
|
|
105
|
+
const message = typeof error.message === 'string'
|
|
106
|
+
? error.message
|
|
107
|
+
: 'player actor rejected without a normalized error';
|
|
108
|
+
return typeof error.stack === 'string'
|
|
109
|
+
? { name, message, stack: error.stack }
|
|
110
|
+
: { name, message };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
name: 'Error',
|
|
114
|
+
message: typeof error === 'string'
|
|
115
|
+
? error
|
|
116
|
+
: 'player actor rejected without a normalized error',
|
|
117
|
+
};
|
|
81
118
|
}
|
|
82
|
-
function
|
|
83
|
-
return (
|
|
84
|
-
outputString(event, 'coderOutput') !== undefined);
|
|
119
|
+
function isResumableStateId(value) {
|
|
120
|
+
return RESUMABLE_STATE_IDS.includes(value);
|
|
85
121
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
122
|
+
/** Builds one guarded `BOSS_REPLY` resume arm per registered working-leaf id. */
|
|
123
|
+
function resumableStates(ids) {
|
|
124
|
+
return ids.map((id) => ({
|
|
125
|
+
guard: { type: 'canResume', params: { stateId: id } },
|
|
126
|
+
target: `#${id}`,
|
|
127
|
+
reenter: true,
|
|
128
|
+
actions: 'acceptBossReply',
|
|
129
|
+
}));
|
|
89
130
|
}
|
|
90
131
|
const machineSetup = setup({
|
|
91
132
|
types: {},
|
|
@@ -95,87 +136,114 @@ const machineSetup = setup({
|
|
|
95
136
|
}),
|
|
96
137
|
},
|
|
97
138
|
guards: {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
139
|
+
// The entry guard validates the text the current event supplies, before
|
|
140
|
+
// the entry action copies it into context.
|
|
141
|
+
givesRequest: ({ event }) => event.type === 'START_BRANCH' && isNonEmptyString(event.callerInput),
|
|
142
|
+
isBranched: ({ event }) => playerOutputOf(event)?.guard === 'branched',
|
|
143
|
+
isRefused: ({ event }) => playerOutputOf(event)?.guard === 'refused',
|
|
144
|
+
needsBossReply: ({ event }, params) => playerOutputOf(event)?.guard === 'needsBossReply' &&
|
|
145
|
+
isResumableStateId(params.stateId),
|
|
146
|
+
canResume: ({ context, event }, params) => {
|
|
147
|
+
if (event.type !== 'BOSS_REPLY' || !isNonEmptyString(event.answer)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const pending = context.pendingBossQuestion;
|
|
151
|
+
if (pending === undefined || pending.resumeStateId !== params.stateId) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return (event.questionId === undefined ||
|
|
155
|
+
event.questionId === pending.questionId);
|
|
156
|
+
},
|
|
106
157
|
},
|
|
107
158
|
actions: {
|
|
108
159
|
'playbook.acceptedOutcome': (_args, _params) => undefined,
|
|
109
|
-
startBranch: assign(({ event }) => {
|
|
160
|
+
startBranch: assign(({ context, event }) => {
|
|
110
161
|
if (event.type !== 'START_BRANCH')
|
|
111
162
|
return {};
|
|
112
163
|
return {
|
|
113
164
|
callerInput: event.callerInput,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
lastError: undefined,
|
|
120
|
-
pendingBossQuestion: undefined,
|
|
121
|
-
bossReply: undefined,
|
|
165
|
+
...(context.lastError === undefined ? {} : { lastError: undefined }),
|
|
166
|
+
...(context.pendingBossQuestion === undefined
|
|
167
|
+
? {}
|
|
168
|
+
: { pendingBossQuestion: undefined }),
|
|
169
|
+
...(context.bossReply === undefined ? {} : { bossReply: undefined }),
|
|
122
170
|
};
|
|
123
171
|
}),
|
|
124
|
-
rememberBranched: assign(({ event }) =>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
return {
|
|
143
|
-
|
|
172
|
+
rememberBranched: assign(({ context, event }) => {
|
|
173
|
+
const output = playerOutputOf(event);
|
|
174
|
+
if (output?.guard !== 'branched')
|
|
175
|
+
return {};
|
|
176
|
+
return {
|
|
177
|
+
branch: output.branch,
|
|
178
|
+
baseRevision: output.baseRevision,
|
|
179
|
+
issueSummary: output.issueSummary,
|
|
180
|
+
completion: 'branched',
|
|
181
|
+
...(context.pendingBossQuestion === undefined
|
|
182
|
+
? {}
|
|
183
|
+
: { pendingBossQuestion: undefined }),
|
|
184
|
+
...(context.bossReply === undefined ? {} : { bossReply: undefined }),
|
|
185
|
+
};
|
|
186
|
+
}),
|
|
187
|
+
rememberRefused: assign(({ context, event }) => {
|
|
188
|
+
const output = playerOutputOf(event);
|
|
189
|
+
if (output?.guard !== 'refused')
|
|
190
|
+
return {};
|
|
191
|
+
return {
|
|
192
|
+
coderOutput: output.coderOutput,
|
|
193
|
+
completion: 'refused',
|
|
194
|
+
...(context.pendingBossQuestion === undefined
|
|
195
|
+
? {}
|
|
196
|
+
: { pendingBossQuestion: undefined }),
|
|
197
|
+
...(context.bossReply === undefined ? {} : { bossReply: undefined }),
|
|
198
|
+
};
|
|
199
|
+
}),
|
|
200
|
+
setPendingBossQuestion: assign(({ context, event }, params) => {
|
|
201
|
+
const output = playerOutputOf(event);
|
|
202
|
+
if (output?.guard !== 'needsBossReply')
|
|
203
|
+
return {};
|
|
144
204
|
return {
|
|
145
205
|
pendingBossQuestion: {
|
|
146
|
-
questionId:
|
|
147
|
-
resumeStateId:
|
|
148
|
-
sourceItem:
|
|
206
|
+
questionId: params.stateId,
|
|
207
|
+
resumeStateId: params.stateId,
|
|
208
|
+
sourceItem: params.sourceItem,
|
|
149
209
|
asker: { kind: 'role', roleId: 'coder' },
|
|
150
|
-
question,
|
|
210
|
+
question: output.question,
|
|
151
211
|
},
|
|
152
|
-
bossReply: undefined,
|
|
212
|
+
...(context.bossReply === undefined ? {} : { bossReply: undefined }),
|
|
153
213
|
};
|
|
154
214
|
}),
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
bossReply: undefined,
|
|
160
|
-
}),
|
|
215
|
+
acceptBossReply: assign(({ event }) => event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {}),
|
|
216
|
+
clearBossReplyContext: assign(({ context }) => context.pendingBossQuestion === undefined &&
|
|
217
|
+
context.bossReply === undefined
|
|
218
|
+
? {}
|
|
219
|
+
: { pendingBossQuestion: undefined, bossReply: undefined }),
|
|
161
220
|
rememberActorError: assign(({ event }) => ({
|
|
162
|
-
lastError: event
|
|
221
|
+
lastError: normalizedErrorOf(event),
|
|
222
|
+
})),
|
|
223
|
+
rememberMalformedPlayerOutput: assign(() => ({
|
|
224
|
+
lastError: {
|
|
225
|
+
name: 'MalformedPlayerOutput',
|
|
226
|
+
message: 'Coder result for BRANCH-1 did not satisfy a declared outcome of its result contract.',
|
|
227
|
+
},
|
|
228
|
+
})),
|
|
229
|
+
rememberMalformedBossReply: assign(() => ({
|
|
230
|
+
lastError: {
|
|
231
|
+
name: 'MalformedBossReply',
|
|
232
|
+
message: 'BOSS_REPLY carried an empty answer or named no pending question.',
|
|
233
|
+
},
|
|
163
234
|
})),
|
|
164
|
-
rememberMalformedPlayerOutput: assign({
|
|
165
|
-
lastError: () => new Error('Coder result for BRANCH-1 did not match an available outcome.'),
|
|
166
|
-
}),
|
|
167
235
|
},
|
|
168
236
|
});
|
|
169
237
|
export const branchMachine = machineSetup.createMachine({
|
|
170
238
|
id: 'branch',
|
|
171
239
|
initial: 'ready',
|
|
172
|
-
context: {},
|
|
240
|
+
context: () => ({}),
|
|
173
241
|
output: ({ context }) => {
|
|
174
242
|
if (context.completion === 'branched') {
|
|
175
243
|
if (!isNonEmptyString(context.branch) ||
|
|
176
244
|
!isNonEmptyString(context.baseRevision) ||
|
|
177
245
|
!isNonEmptyString(context.issueSummary)) {
|
|
178
|
-
throw new Error('BRANCH reached branched without the exact branch name,
|
|
246
|
+
throw new Error('BRANCH reached branched without the exact branch name, base revision, and issue summary');
|
|
179
247
|
}
|
|
180
248
|
return {
|
|
181
249
|
status: 'branched',
|
|
@@ -186,7 +254,7 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
186
254
|
}
|
|
187
255
|
if (context.completion === 'refused') {
|
|
188
256
|
if (!isNonEmptyString(context.coderOutput)) {
|
|
189
|
-
throw new Error("BRANCH reached refused without Coder's
|
|
257
|
+
throw new Error("BRANCH reached refused without Coder's complete result");
|
|
190
258
|
}
|
|
191
259
|
return { status: 'refused', coderOutput: context.coderOutput };
|
|
192
260
|
}
|
|
@@ -199,7 +267,11 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
199
267
|
meta: playbookMeta('ready'),
|
|
200
268
|
tags: ['playbook.parked'],
|
|
201
269
|
on: {
|
|
202
|
-
START_BRANCH: {
|
|
270
|
+
START_BRANCH: {
|
|
271
|
+
guard: 'givesRequest',
|
|
272
|
+
target: 'createBranch',
|
|
273
|
+
actions: 'startBranch',
|
|
274
|
+
},
|
|
203
275
|
},
|
|
204
276
|
},
|
|
205
277
|
createBranch: {
|
|
@@ -215,6 +287,8 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
215
287
|
sourceItem: 'BRANCH-1',
|
|
216
288
|
prompt: CREATE_BRANCH_PROMPT,
|
|
217
289
|
result: CREATE_BRANCH_RESULTS,
|
|
290
|
+
// Entry is guarded on a non-empty caller input, so this state is
|
|
291
|
+
// never entered without it.
|
|
218
292
|
callerInput: context.callerInput ?? '',
|
|
219
293
|
...(context.pendingBossQuestion === undefined
|
|
220
294
|
? {}
|
|
@@ -255,7 +329,10 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
255
329
|
],
|
|
256
330
|
},
|
|
257
331
|
{
|
|
258
|
-
guard:
|
|
332
|
+
guard: {
|
|
333
|
+
type: 'needsBossReply',
|
|
334
|
+
params: { stateId: 'createBranch' },
|
|
335
|
+
},
|
|
259
336
|
target: 'awaitBossReply',
|
|
260
337
|
actions: [
|
|
261
338
|
{
|
|
@@ -266,7 +343,10 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
266
343
|
acceptedOutcome: 'needsBossReply',
|
|
267
344
|
},
|
|
268
345
|
},
|
|
269
|
-
|
|
346
|
+
{
|
|
347
|
+
type: 'setPendingBossQuestion',
|
|
348
|
+
params: { stateId: 'createBranch', sourceItem: 'BRANCH-1' },
|
|
349
|
+
},
|
|
270
350
|
],
|
|
271
351
|
},
|
|
272
352
|
{
|
|
@@ -284,19 +364,17 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
284
364
|
tags: ['playbook.parked'],
|
|
285
365
|
on: {
|
|
286
366
|
BOSS_REPLY: [
|
|
367
|
+
...resumableStates(RESUMABLE_STATE_IDS),
|
|
287
368
|
{
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
actions: 'rememberEmptyBossReplyError',
|
|
291
|
-
},
|
|
292
|
-
{
|
|
293
|
-
guard: 'resumesCreateBranch',
|
|
294
|
-
target: '#createBranch',
|
|
295
|
-
reenter: true,
|
|
296
|
-
actions: 'rememberBossReply',
|
|
369
|
+
target: 'failed',
|
|
370
|
+
actions: ['rememberMalformedBossReply', 'clearBossReplyContext'],
|
|
297
371
|
},
|
|
298
372
|
],
|
|
299
|
-
START_BRANCH: {
|
|
373
|
+
START_BRANCH: {
|
|
374
|
+
guard: 'givesRequest',
|
|
375
|
+
target: 'createBranch',
|
|
376
|
+
actions: 'startBranch',
|
|
377
|
+
},
|
|
300
378
|
},
|
|
301
379
|
},
|
|
302
380
|
failed: {
|
|
@@ -305,7 +383,11 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
305
383
|
meta: playbookMeta('failed'),
|
|
306
384
|
tags: ['playbook.parked'],
|
|
307
385
|
on: {
|
|
308
|
-
START_BRANCH: {
|
|
386
|
+
START_BRANCH: {
|
|
387
|
+
guard: 'givesRequest',
|
|
388
|
+
target: 'createBranch',
|
|
389
|
+
actions: 'startBranch',
|
|
390
|
+
},
|
|
309
391
|
},
|
|
310
392
|
},
|
|
311
393
|
branched: {
|