@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
|
@@ -1,7 +1,5 @@
|
|
|
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
|
|
|
6
4
|
import { assign, fromPromise, setup } from 'xstate';
|
|
7
5
|
|
|
@@ -9,83 +7,105 @@ export type BranchStateId = 'createBranch';
|
|
|
9
7
|
|
|
10
8
|
export type BranchSourceItem = 'BRANCH-1';
|
|
11
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Stable working-leaf ids that may suspend on a Boss question and be resumed
|
|
12
|
+
* by `BOSS_REPLY`. The machine has at most one active player task, so the
|
|
13
|
+
* scalar Boss-reply form applies.
|
|
14
|
+
*/
|
|
15
|
+
const RESUMABLE_STATE_IDS = ['createBranch'] as const;
|
|
16
|
+
export type ResumableStateId = (typeof RESUMABLE_STATE_IDS)[number];
|
|
17
|
+
|
|
18
|
+
/** JSON-safe normalization of a rejected invocation, retained for inspection. */
|
|
19
|
+
export type NormalizedError = {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly message: string;
|
|
22
|
+
readonly stack?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Question record raised by the suspended Coder working leaf. */
|
|
12
26
|
export type PendingBossQuestion = {
|
|
13
|
-
readonly questionId:
|
|
14
|
-
readonly resumeStateId:
|
|
15
|
-
readonly sourceItem:
|
|
27
|
+
readonly questionId: ResumableStateId;
|
|
28
|
+
readonly resumeStateId: ResumableStateId;
|
|
29
|
+
readonly sourceItem: BranchSourceItem;
|
|
16
30
|
readonly asker: { readonly kind: 'role'; readonly roleId: 'coder' };
|
|
17
31
|
readonly question: string;
|
|
18
32
|
};
|
|
19
33
|
|
|
34
|
+
/** Typed input for the delegated `player` actor. */
|
|
20
35
|
export type PlayerInput = {
|
|
21
36
|
readonly stateId: 'createBranch';
|
|
22
37
|
readonly role: 'coder';
|
|
23
38
|
readonly sourceItem: 'BRANCH-1';
|
|
24
39
|
readonly prompt: string;
|
|
25
40
|
readonly result: Readonly<Record<string, string>>;
|
|
41
|
+
/** Backs the prompt's `<caller-input>` placeholder with the exact caller input. */
|
|
26
42
|
readonly callerInput: string;
|
|
27
43
|
readonly pendingBossQuestion?: PendingBossQuestion;
|
|
28
44
|
readonly bossReply?: string;
|
|
29
45
|
};
|
|
30
46
|
|
|
47
|
+
/** Discriminated result contract of the delegated `player` actor. */
|
|
31
48
|
export type PlayerOutput =
|
|
32
49
|
| {
|
|
33
50
|
readonly guard: 'branched';
|
|
34
51
|
readonly branch: string;
|
|
35
|
-
/**
|
|
36
|
-
* Receipt-owned effect evidence (DR-045): the branching call's
|
|
37
|
-
* `unchanged` receipt observes the commit the branch was created from
|
|
38
|
-
* as HEAD, so the linked runtime injects it and never takes it from
|
|
39
|
-
* Coder's prose.
|
|
40
|
-
*/
|
|
52
|
+
/** Exact base revision from repository authority, not from Coder's prose. */
|
|
41
53
|
readonly baseRevision: string;
|
|
42
54
|
readonly issueSummary: string;
|
|
43
55
|
}
|
|
44
56
|
| { readonly guard: 'refused'; readonly coderOutput: string }
|
|
45
57
|
| { readonly guard: 'needsBossReply'; readonly question: string };
|
|
46
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Terminal result: exactly the packaged `branch` workflow's public output
|
|
61
|
+
* interface (workflow-contracts.json), derived from typed context.
|
|
62
|
+
*/
|
|
47
63
|
export type BranchPlaybookOutput =
|
|
48
64
|
| {
|
|
49
65
|
readonly status: 'branched';
|
|
50
|
-
/** Exact
|
|
66
|
+
/** Exact new branch name. */
|
|
51
67
|
readonly branch: string;
|
|
52
|
-
/** Exact receipt-observed revision the branch was created from. */
|
|
68
|
+
/** Exact receipt-observed repository revision the branch was created from. */
|
|
53
69
|
readonly baseRevision: string;
|
|
54
|
-
/**
|
|
70
|
+
/** Concise issue-and-comments or request summary. */
|
|
55
71
|
readonly issueSummary: string;
|
|
56
72
|
}
|
|
57
73
|
| {
|
|
58
74
|
readonly status: 'refused';
|
|
59
|
-
/**
|
|
75
|
+
/** Complete refusal report. */
|
|
60
76
|
readonly coderOutput: string;
|
|
61
77
|
};
|
|
62
78
|
|
|
63
|
-
|
|
79
|
+
/** The machine reads no input: the caller input arrives on `START_BRANCH`. */
|
|
80
|
+
export type BranchInput = Readonly<Record<never, never>>;
|
|
81
|
+
|
|
82
|
+
export type BranchCompletion = 'branched' | 'refused';
|
|
64
83
|
|
|
65
84
|
export type BranchContext = {
|
|
66
85
|
readonly callerInput?: string;
|
|
67
86
|
readonly branch?: string;
|
|
68
|
-
/** Observed HEAD of the branching call's unchanged receipt (DR-045). */
|
|
69
87
|
readonly baseRevision?: string;
|
|
70
88
|
readonly issueSummary?: string;
|
|
71
89
|
readonly coderOutput?: string;
|
|
72
|
-
readonly completion?:
|
|
73
|
-
readonly lastError?:
|
|
90
|
+
readonly completion?: BranchCompletion;
|
|
91
|
+
readonly lastError?: NormalizedError;
|
|
74
92
|
readonly pendingBossQuestion?: PendingBossQuestion;
|
|
75
93
|
readonly bossReply?: string;
|
|
76
94
|
};
|
|
77
95
|
|
|
96
|
+
/** Typed Boss surfaces: the caller's entry event and the Boss reply. */
|
|
78
97
|
export type BranchEvent =
|
|
79
98
|
| { readonly type: 'START_BRANCH'; readonly callerInput: string }
|
|
80
99
|
| {
|
|
81
100
|
readonly type: 'BOSS_REPLY';
|
|
82
101
|
readonly answer: string;
|
|
83
|
-
readonly questionId?:
|
|
102
|
+
readonly questionId?: ResumableStateId;
|
|
84
103
|
};
|
|
85
104
|
|
|
105
|
+
/** No parallel group in this playbook. */
|
|
106
|
+
export const concurrentRoleSets: readonly (readonly string[])[] = [];
|
|
107
|
+
|
|
86
108
|
const CREATE_BRANCH_PROMPT = [
|
|
87
|
-
'> Original request: <caller-input>',
|
|
88
|
-
'',
|
|
89
109
|
'Prepare a new branch for this work without changing any file or making any commit.',
|
|
90
110
|
'Identify the GitHub issue the request names, if any, and read it with its comments (`gh issue view --comments` with the issue number).',
|
|
91
111
|
'If the request could refer to more than one issue, or does not say which work to branch for, ask Boss before creating anything.',
|
|
@@ -94,34 +114,37 @@ const CREATE_BRANCH_PROMPT = [
|
|
|
94
114
|
'Create the branch from the current commit and check it out; do not pull, reset, stash, or move HEAD to another commit.',
|
|
95
115
|
'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.',
|
|
96
116
|
'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.',
|
|
117
|
+
'',
|
|
118
|
+
'> Original request: <caller-input>',
|
|
97
119
|
].join('\n');
|
|
98
120
|
|
|
121
|
+
const NEEDS_BOSS_REPLY_RESULT =
|
|
122
|
+
"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>`.";
|
|
123
|
+
|
|
99
124
|
const CREATE_BRANCH_RESULTS = {
|
|
100
125
|
branched:
|
|
101
|
-
|
|
126
|
+
"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>`.",
|
|
102
127
|
refused:
|
|
103
|
-
|
|
104
|
-
needsBossReply:
|
|
105
|
-
"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>`.",
|
|
128
|
+
"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>`.",
|
|
129
|
+
needsBossReply: NEEDS_BOSS_REPLY_RESULT,
|
|
106
130
|
} as const;
|
|
107
131
|
|
|
108
132
|
const STATE_DESCRIPTIONS = {
|
|
109
|
-
ready: 'Waiting for a development request to branch for.',
|
|
133
|
+
ready: 'Waiting for the caller to give a development request to branch for.',
|
|
110
134
|
createBranch:
|
|
111
|
-
'Coder is
|
|
112
|
-
awaitBossReply:
|
|
135
|
+
'Coder is identifying the request\'s issue and creating and checking out a new branch at the current commit.',
|
|
136
|
+
awaitBossReply: "Waiting for Boss to answer the acting agent's question.",
|
|
113
137
|
failed:
|
|
114
|
-
'The branch workflow failed and is waiting for
|
|
138
|
+
'The branch workflow failed and is waiting for the caller to give the request again.',
|
|
115
139
|
branched:
|
|
116
|
-
|
|
140
|
+
'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.',
|
|
117
141
|
refused:
|
|
118
|
-
|
|
142
|
+
"No branch was created: Coder reported the failure with its reason, and the workflow fails and reports Coder's complete result to its caller.",
|
|
119
143
|
} as const;
|
|
120
144
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
) {
|
|
145
|
+
type DescribedStateId = keyof typeof STATE_DESCRIPTIONS;
|
|
146
|
+
|
|
147
|
+
function playbookMeta(stateId: DescribedStateId, role?: 'coder') {
|
|
125
148
|
return {
|
|
126
149
|
playbook: {
|
|
127
150
|
stateId,
|
|
@@ -131,11 +154,9 @@ function playbookMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
|
|
|
131
154
|
};
|
|
132
155
|
}
|
|
133
156
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
function terminalMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
|
|
138
|
-
stateId: StateId,
|
|
157
|
+
/** A final state also publishes whether its outcome is success or failure. */
|
|
158
|
+
function terminalMeta(
|
|
159
|
+
stateId: DescribedStateId,
|
|
139
160
|
terminal: 'success' | 'failure',
|
|
140
161
|
) {
|
|
141
162
|
return {
|
|
@@ -159,43 +180,87 @@ function isNonEmptyString(value: unknown): value is string {
|
|
|
159
180
|
return typeof value === 'string' && value.trim().length > 0;
|
|
160
181
|
}
|
|
161
182
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Narrows an unknown event's `output` structurally to the declared `player`
|
|
185
|
+
* contract. Output missing a field its guard requires narrows to `undefined`.
|
|
186
|
+
*/
|
|
187
|
+
function playerOutputOf(event: unknown): PlayerOutput | undefined {
|
|
188
|
+
if (!isRecord(event) || !isRecord(event.output)) return undefined;
|
|
189
|
+
const output = event.output;
|
|
190
|
+
if (
|
|
191
|
+
output.guard === 'branched' &&
|
|
192
|
+
isNonEmptyString(output.branch) &&
|
|
193
|
+
isNonEmptyString(output.baseRevision) &&
|
|
194
|
+
isNonEmptyString(output.issueSummary)
|
|
195
|
+
) {
|
|
196
|
+
return {
|
|
197
|
+
guard: 'branched',
|
|
198
|
+
branch: output.branch,
|
|
199
|
+
baseRevision: output.baseRevision,
|
|
200
|
+
issueSummary: output.issueSummary,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (output.guard === 'refused' && isNonEmptyString(output.coderOutput)) {
|
|
204
|
+
return { guard: 'refused', coderOutput: output.coderOutput };
|
|
205
|
+
}
|
|
206
|
+
if (output.guard === 'needsBossReply' && isNonEmptyString(output.question)) {
|
|
207
|
+
return { guard: 'needsBossReply', question: output.question };
|
|
208
|
+
}
|
|
209
|
+
return undefined;
|
|
169
210
|
}
|
|
170
211
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
212
|
+
/** Narrows an unknown event's `error` structurally to a JSON-safe record. */
|
|
213
|
+
function normalizedErrorOf(event: unknown): NormalizedError {
|
|
214
|
+
const error = isRecord(event) ? event.error : undefined;
|
|
215
|
+
if (error instanceof Error) {
|
|
216
|
+
const name = error.name || 'Error';
|
|
217
|
+
return typeof error.stack === 'string'
|
|
218
|
+
? { name, message: error.message, stack: error.stack }
|
|
219
|
+
: { name, message: error.message };
|
|
220
|
+
}
|
|
221
|
+
if (isRecord(error)) {
|
|
222
|
+
const name = typeof error.name === 'string' ? error.name : 'Error';
|
|
223
|
+
const message =
|
|
224
|
+
typeof error.message === 'string'
|
|
225
|
+
? error.message
|
|
226
|
+
: 'player actor rejected without a normalized error';
|
|
227
|
+
return typeof error.stack === 'string'
|
|
228
|
+
? { name, message, stack: error.stack }
|
|
229
|
+
: { name, message };
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
name: 'Error',
|
|
233
|
+
message:
|
|
234
|
+
typeof error === 'string'
|
|
235
|
+
? error
|
|
236
|
+
: 'player actor rejected without a normalized error',
|
|
237
|
+
};
|
|
174
238
|
}
|
|
175
239
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
function isBranched({ event }: { event: unknown }): boolean {
|
|
179
|
-
return (
|
|
180
|
-
outputGuard(event, 'branched') &&
|
|
181
|
-
outputString(event, 'branch') !== undefined &&
|
|
182
|
-
outputString(event, 'baseRevision') !== undefined &&
|
|
183
|
-
outputString(event, 'issueSummary') !== undefined
|
|
184
|
-
);
|
|
240
|
+
function isResumableStateId(value: string): value is ResumableStateId {
|
|
241
|
+
return (RESUMABLE_STATE_IDS as readonly string[]).includes(value);
|
|
185
242
|
}
|
|
186
243
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
244
|
+
type ResumeTransition = {
|
|
245
|
+
readonly guard: {
|
|
246
|
+
readonly type: 'canResume';
|
|
247
|
+
readonly params: { readonly stateId: ResumableStateId };
|
|
248
|
+
};
|
|
249
|
+
readonly target: `#${ResumableStateId}`;
|
|
250
|
+
readonly reenter: true;
|
|
251
|
+
readonly actions: 'acceptBossReply';
|
|
252
|
+
};
|
|
193
253
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
)
|
|
254
|
+
/** Builds one guarded `BOSS_REPLY` resume arm per registered working-leaf id. */
|
|
255
|
+
function resumableStates(
|
|
256
|
+
ids: readonly ResumableStateId[],
|
|
257
|
+
): ResumeTransition[] {
|
|
258
|
+
return ids.map((id) => ({
|
|
259
|
+
guard: { type: 'canResume', params: { stateId: id } },
|
|
260
|
+
target: `#${id}`,
|
|
261
|
+
reenter: true,
|
|
262
|
+
actions: 'acceptBossReply',
|
|
263
|
+
}));
|
|
199
264
|
}
|
|
200
265
|
|
|
201
266
|
const machineSetup = setup({
|
|
@@ -211,16 +276,34 @@ const machineSetup = setup({
|
|
|
211
276
|
}),
|
|
212
277
|
},
|
|
213
278
|
guards: {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
event
|
|
222
|
-
|
|
223
|
-
|
|
279
|
+
// The entry guard validates the text the current event supplies, before
|
|
280
|
+
// the entry action copies it into context.
|
|
281
|
+
givesRequest: ({ event }) =>
|
|
282
|
+
event.type === 'START_BRANCH' && isNonEmptyString(event.callerInput),
|
|
283
|
+
isBranched: ({ event }) => playerOutputOf(event)?.guard === 'branched',
|
|
284
|
+
isRefused: ({ event }) => playerOutputOf(event)?.guard === 'refused',
|
|
285
|
+
needsBossReply: (
|
|
286
|
+
{ event },
|
|
287
|
+
params: { readonly stateId: ResumableStateId },
|
|
288
|
+
) =>
|
|
289
|
+
playerOutputOf(event)?.guard === 'needsBossReply' &&
|
|
290
|
+
isResumableStateId(params.stateId),
|
|
291
|
+
canResume: (
|
|
292
|
+
{ context, event },
|
|
293
|
+
params: { readonly stateId: ResumableStateId },
|
|
294
|
+
) => {
|
|
295
|
+
if (event.type !== 'BOSS_REPLY' || !isNonEmptyString(event.answer)) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
const pending = context.pendingBossQuestion;
|
|
299
|
+
if (pending === undefined || pending.resumeStateId !== params.stateId) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
return (
|
|
303
|
+
event.questionId === undefined ||
|
|
304
|
+
event.questionId === pending.questionId
|
|
305
|
+
);
|
|
306
|
+
},
|
|
224
307
|
},
|
|
225
308
|
actions: {
|
|
226
309
|
'playbook.acceptedOutcome': (
|
|
@@ -231,73 +314,98 @@ const machineSetup = setup({
|
|
|
231
314
|
readonly acceptedOutcome: string;
|
|
232
315
|
},
|
|
233
316
|
) => undefined,
|
|
234
|
-
startBranch: assign(({ event }) => {
|
|
317
|
+
startBranch: assign(({ context, event }): Partial<BranchContext> => {
|
|
235
318
|
if (event.type !== 'START_BRANCH') return {};
|
|
236
319
|
return {
|
|
237
320
|
callerInput: event.callerInput,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
lastError: undefined,
|
|
244
|
-
pendingBossQuestion: undefined,
|
|
245
|
-
bossReply: undefined,
|
|
321
|
+
...(context.lastError === undefined ? {} : { lastError: undefined }),
|
|
322
|
+
...(context.pendingBossQuestion === undefined
|
|
323
|
+
? {}
|
|
324
|
+
: { pendingBossQuestion: undefined }),
|
|
325
|
+
...(context.bossReply === undefined ? {} : { bossReply: undefined }),
|
|
246
326
|
};
|
|
247
327
|
}),
|
|
248
|
-
rememberBranched: assign(({ event }) =>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
baseRevision: outputString(event, 'baseRevision'),
|
|
252
|
-
issueSummary: outputString(event, 'issueSummary'),
|
|
253
|
-
completion: 'branched' as const,
|
|
254
|
-
pendingBossQuestion: undefined,
|
|
255
|
-
bossReply: undefined,
|
|
256
|
-
})),
|
|
257
|
-
rememberRefused: assign(({ event }) => ({
|
|
258
|
-
coderOutput: outputString(event, 'coderOutput'),
|
|
259
|
-
completion: 'refused' as const,
|
|
260
|
-
pendingBossQuestion: undefined,
|
|
261
|
-
bossReply: undefined,
|
|
262
|
-
})),
|
|
263
|
-
rememberPendingQuestion: assign(({ event }) => {
|
|
264
|
-
const question = outputString(event, 'question');
|
|
265
|
-
if (question === undefined || !outputGuard(event, 'needsBossReply')) {
|
|
266
|
-
return { lastError: new Error('Coder question output was malformed') };
|
|
267
|
-
}
|
|
328
|
+
rememberBranched: assign(({ context, event }): Partial<BranchContext> => {
|
|
329
|
+
const output = playerOutputOf(event);
|
|
330
|
+
if (output?.guard !== 'branched') return {};
|
|
268
331
|
return {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
bossReply: undefined,
|
|
332
|
+
branch: output.branch,
|
|
333
|
+
baseRevision: output.baseRevision,
|
|
334
|
+
issueSummary: output.issueSummary,
|
|
335
|
+
completion: 'branched',
|
|
336
|
+
...(context.pendingBossQuestion === undefined
|
|
337
|
+
? {}
|
|
338
|
+
: { pendingBossQuestion: undefined }),
|
|
339
|
+
...(context.bossReply === undefined ? {} : { bossReply: undefined }),
|
|
340
|
+
};
|
|
341
|
+
}),
|
|
342
|
+
rememberRefused: assign(({ context, event }): Partial<BranchContext> => {
|
|
343
|
+
const output = playerOutputOf(event);
|
|
344
|
+
if (output?.guard !== 'refused') return {};
|
|
345
|
+
return {
|
|
346
|
+
coderOutput: output.coderOutput,
|
|
347
|
+
completion: 'refused',
|
|
348
|
+
...(context.pendingBossQuestion === undefined
|
|
349
|
+
? {}
|
|
350
|
+
: { pendingBossQuestion: undefined }),
|
|
351
|
+
...(context.bossReply === undefined ? {} : { bossReply: undefined }),
|
|
277
352
|
};
|
|
278
353
|
}),
|
|
279
|
-
|
|
354
|
+
setPendingBossQuestion: assign(
|
|
355
|
+
(
|
|
356
|
+
{ context, event },
|
|
357
|
+
params: {
|
|
358
|
+
readonly stateId: ResumableStateId;
|
|
359
|
+
readonly sourceItem: BranchSourceItem;
|
|
360
|
+
},
|
|
361
|
+
): Partial<BranchContext> => {
|
|
362
|
+
const output = playerOutputOf(event);
|
|
363
|
+
if (output?.guard !== 'needsBossReply') return {};
|
|
364
|
+
return {
|
|
365
|
+
pendingBossQuestion: {
|
|
366
|
+
questionId: params.stateId,
|
|
367
|
+
resumeStateId: params.stateId,
|
|
368
|
+
sourceItem: params.sourceItem,
|
|
369
|
+
asker: { kind: 'role', roleId: 'coder' },
|
|
370
|
+
question: output.question,
|
|
371
|
+
},
|
|
372
|
+
...(context.bossReply === undefined ? {} : { bossReply: undefined }),
|
|
373
|
+
};
|
|
374
|
+
},
|
|
375
|
+
),
|
|
376
|
+
acceptBossReply: assign(({ event }): Partial<BranchContext> =>
|
|
280
377
|
event.type === 'BOSS_REPLY' ? { bossReply: event.answer } : {},
|
|
281
378
|
),
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
379
|
+
clearBossReplyContext: assign(({ context }): Partial<BranchContext> =>
|
|
380
|
+
context.pendingBossQuestion === undefined &&
|
|
381
|
+
context.bossReply === undefined
|
|
382
|
+
? {}
|
|
383
|
+
: { pendingBossQuestion: undefined, bossReply: undefined },
|
|
384
|
+
),
|
|
385
|
+
rememberActorError: assign(({ event }): Partial<BranchContext> => ({
|
|
386
|
+
lastError: normalizedErrorOf(event),
|
|
387
|
+
})),
|
|
388
|
+
rememberMalformedPlayerOutput: assign((): Partial<BranchContext> => ({
|
|
389
|
+
lastError: {
|
|
390
|
+
name: 'MalformedPlayerOutput',
|
|
391
|
+
message:
|
|
392
|
+
'Coder result for BRANCH-1 did not satisfy a declared outcome of its result contract.',
|
|
393
|
+
},
|
|
394
|
+
})),
|
|
395
|
+
rememberMalformedBossReply: assign((): Partial<BranchContext> => ({
|
|
396
|
+
lastError: {
|
|
397
|
+
name: 'MalformedBossReply',
|
|
398
|
+
message:
|
|
399
|
+
'BOSS_REPLY carried an empty answer or named no pending question.',
|
|
400
|
+
},
|
|
289
401
|
})),
|
|
290
|
-
rememberMalformedPlayerOutput: assign({
|
|
291
|
-
lastError: () =>
|
|
292
|
-
new Error('Coder result for BRANCH-1 did not match an available outcome.'),
|
|
293
|
-
}),
|
|
294
402
|
},
|
|
295
403
|
});
|
|
296
404
|
|
|
297
405
|
export const branchMachine = machineSetup.createMachine({
|
|
298
406
|
id: 'branch',
|
|
299
407
|
initial: 'ready',
|
|
300
|
-
context: {},
|
|
408
|
+
context: () => ({}),
|
|
301
409
|
output: ({ context }): BranchPlaybookOutput => {
|
|
302
410
|
if (context.completion === 'branched') {
|
|
303
411
|
if (
|
|
@@ -306,7 +414,7 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
306
414
|
!isNonEmptyString(context.issueSummary)
|
|
307
415
|
) {
|
|
308
416
|
throw new Error(
|
|
309
|
-
'BRANCH reached branched without the exact branch name,
|
|
417
|
+
'BRANCH reached branched without the exact branch name, base revision, and issue summary',
|
|
310
418
|
);
|
|
311
419
|
}
|
|
312
420
|
return {
|
|
@@ -318,7 +426,7 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
318
426
|
}
|
|
319
427
|
if (context.completion === 'refused') {
|
|
320
428
|
if (!isNonEmptyString(context.coderOutput)) {
|
|
321
|
-
throw new Error("BRANCH reached refused without Coder's
|
|
429
|
+
throw new Error("BRANCH reached refused without Coder's complete result");
|
|
322
430
|
}
|
|
323
431
|
return { status: 'refused', coderOutput: context.coderOutput };
|
|
324
432
|
}
|
|
@@ -331,7 +439,11 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
331
439
|
meta: playbookMeta('ready'),
|
|
332
440
|
tags: ['playbook.parked'],
|
|
333
441
|
on: {
|
|
334
|
-
START_BRANCH: {
|
|
442
|
+
START_BRANCH: {
|
|
443
|
+
guard: 'givesRequest',
|
|
444
|
+
target: 'createBranch',
|
|
445
|
+
actions: 'startBranch',
|
|
446
|
+
},
|
|
335
447
|
},
|
|
336
448
|
},
|
|
337
449
|
createBranch: {
|
|
@@ -347,6 +459,8 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
347
459
|
sourceItem: 'BRANCH-1',
|
|
348
460
|
prompt: CREATE_BRANCH_PROMPT,
|
|
349
461
|
result: CREATE_BRANCH_RESULTS,
|
|
462
|
+
// Entry is guarded on a non-empty caller input, so this state is
|
|
463
|
+
// never entered without it.
|
|
350
464
|
callerInput: context.callerInput ?? '',
|
|
351
465
|
...(context.pendingBossQuestion === undefined
|
|
352
466
|
? {}
|
|
@@ -387,7 +501,10 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
387
501
|
],
|
|
388
502
|
},
|
|
389
503
|
{
|
|
390
|
-
guard:
|
|
504
|
+
guard: {
|
|
505
|
+
type: 'needsBossReply',
|
|
506
|
+
params: { stateId: 'createBranch' },
|
|
507
|
+
},
|
|
391
508
|
target: 'awaitBossReply',
|
|
392
509
|
actions: [
|
|
393
510
|
{
|
|
@@ -398,7 +515,10 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
398
515
|
acceptedOutcome: 'needsBossReply',
|
|
399
516
|
},
|
|
400
517
|
},
|
|
401
|
-
|
|
518
|
+
{
|
|
519
|
+
type: 'setPendingBossQuestion',
|
|
520
|
+
params: { stateId: 'createBranch', sourceItem: 'BRANCH-1' },
|
|
521
|
+
},
|
|
402
522
|
],
|
|
403
523
|
},
|
|
404
524
|
{
|
|
@@ -416,19 +536,17 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
416
536
|
tags: ['playbook.parked'],
|
|
417
537
|
on: {
|
|
418
538
|
BOSS_REPLY: [
|
|
539
|
+
...resumableStates(RESUMABLE_STATE_IDS),
|
|
419
540
|
{
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
actions: 'rememberEmptyBossReplyError',
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
guard: 'resumesCreateBranch',
|
|
426
|
-
target: '#createBranch',
|
|
427
|
-
reenter: true,
|
|
428
|
-
actions: 'rememberBossReply',
|
|
541
|
+
target: 'failed',
|
|
542
|
+
actions: ['rememberMalformedBossReply', 'clearBossReplyContext'],
|
|
429
543
|
},
|
|
430
544
|
],
|
|
431
|
-
START_BRANCH: {
|
|
545
|
+
START_BRANCH: {
|
|
546
|
+
guard: 'givesRequest',
|
|
547
|
+
target: 'createBranch',
|
|
548
|
+
actions: 'startBranch',
|
|
549
|
+
},
|
|
432
550
|
},
|
|
433
551
|
},
|
|
434
552
|
failed: {
|
|
@@ -437,7 +555,11 @@ export const branchMachine = machineSetup.createMachine({
|
|
|
437
555
|
meta: playbookMeta('failed'),
|
|
438
556
|
tags: ['playbook.parked'],
|
|
439
557
|
on: {
|
|
440
|
-
START_BRANCH: {
|
|
558
|
+
START_BRANCH: {
|
|
559
|
+
guard: 'givesRequest',
|
|
560
|
+
target: 'createBranch',
|
|
561
|
+
actions: 'startBranch',
|
|
562
|
+
},
|
|
441
563
|
},
|
|
442
564
|
},
|
|
443
565
|
branched: {
|