@sublang/playbook 13.3.0 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/docs/configuration.md +10 -2
- package/docs/embedding.md +20 -1
- package/package.json +6 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +88 -5
- package/reference/sdlc/code.playbook/bin/playbook.js +4 -0
- package/reference/sdlc/code.playbook/bin/repository-effects.js +11 -3
- package/reference/sdlc/code.playbook/bin/run.js +4 -0
- package/reference/sdlc/code.playbook/bin/session-store.js +2 -2
- package/reference/sdlc/code.playbook/playbook-captain.js +17 -4
- package/reference/sdlc/code.playbook/playbook-captain.ts +19 -4
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +3 -10
- package/reference/sdlc/dev.playbook/dev.playbook.js +5 -5
- package/reference/sdlc/dev.playbook/dev.playbook.ts +7 -7
- package/reference/sdlc/pr.md +17 -4
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +17 -3
- package/reference/sdlc/pr.playbook/pr.fsm.js +49 -18
- package/reference/sdlc/pr.playbook/pr.fsm.ts +63 -20
- package/reference/sdlc/pr.playbook/pr.gears.md +20 -7
- package/reference/sdlc/pr.playbook/pr.playbook.js +2 -1
- package/reference/sdlc/pr.playbook/pr.playbook.ts +2 -1
- package/slc/experiments/fsm-scaffold-guidance.md +31 -0
- package/slc/gears2fsm.md +145 -58
- package/slc/link.md +158 -7
- package/slc/materialize-link.mjs +460 -0
- package/slc/optimize.md +46 -6
- package/slc/scaffold-fsm.mjs +225 -0
- package/slc/slc.pin-inputs.json +13 -0
- package/slc/text2gears.md +38 -3
- package/slc/workflow-contracts.json +371 -0
- package/src/xstate-playbook-runtime.js +35 -1
- package/src/xstate-playbook-runtime.ts +54 -1
|
@@ -20,9 +20,34 @@ const OPEN_PULL_REQUEST_RESULTS = {
|
|
|
20
20
|
notPublished: 'Coder reported that the branch could not be published or its pull request could not be opened, with the reason. Output shall include `coderOutput: <verbatim final text>`.',
|
|
21
21
|
needsBossReply: NEEDS_BOSS_REPLY_RESULT,
|
|
22
22
|
};
|
|
23
|
-
// The mechanical steps are static POSIX shell text (slc/optimize.md)
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// The mechanical steps are static POSIX shell text (slc/optimize.md) with no
|
|
24
|
+
// prose. `gh` infers the pull request from the checked-out branch, which the
|
|
25
|
+
// run does not own, so the two steps that act on the pull request itself relay
|
|
26
|
+
// one runtime value — the pull request PR-1 published — through the same
|
|
27
|
+
// placeholder relay an acting prompt uses.
|
|
28
|
+
// The placeholder occupies a single-quoted shell word, and binding replaces
|
|
29
|
+
// that whole word — quotes included — with the value's own quoted literal.
|
|
30
|
+
const QUOTED_PULL_REQUEST_URL = "'<pull-request-url>'";
|
|
31
|
+
// A retained value is reported text, so it reaches the shell as one
|
|
32
|
+
// single-quoted literal word: no command substitution, no expansion, no word
|
|
33
|
+
// splitting. A single quote inside the value closes the literal, escapes
|
|
34
|
+
// itself, and reopens it — the only character single quotes cannot hold.
|
|
35
|
+
function shellLiteral(value) {
|
|
36
|
+
return `'${value.split("'").join("'\\''")}'`;
|
|
37
|
+
}
|
|
38
|
+
// Bind the retained identity into a script command. Without it the quoted
|
|
39
|
+
// placeholder stays, and no `gh` output equals it, so the step refuses instead
|
|
40
|
+
// of acting on whatever the checkout now infers. Every script state is
|
|
41
|
+
// reachable only after `rememberPullRequest`, so the refusal is unreachable by
|
|
42
|
+
// construction and exists to keep the failure closed if that ever changes.
|
|
43
|
+
function scriptCommand(command, context) {
|
|
44
|
+
const url = context.pullRequestUrl;
|
|
45
|
+
if (!isNonEmptyString(url))
|
|
46
|
+
return command;
|
|
47
|
+
// A function replacement keeps `$`-patterns in the value from being read as
|
|
48
|
+
// replacement syntax.
|
|
49
|
+
return command.replaceAll(QUOTED_PULL_REQUEST_URL, () => shellLiteral(url));
|
|
50
|
+
}
|
|
26
51
|
const WAIT_FOR_CHECKS_COMMAND = [
|
|
27
52
|
'n=0',
|
|
28
53
|
"while gh pr checks 2>&1 | grep -q 'no checks reported'; do",
|
|
@@ -33,6 +58,7 @@ const WAIT_FOR_CHECKS_COMMAND = [
|
|
|
33
58
|
'gh pr checks --watch --fail-fast >/dev/null 2>&1',
|
|
34
59
|
].join('\n');
|
|
35
60
|
const PUBLISH_FIX_COMMAND = [
|
|
61
|
+
'[ "$(gh pr view --json url --jq .url)" = \'<pull-request-url>\' ] || exit 1',
|
|
36
62
|
'git push || exit 1',
|
|
37
63
|
'n=0',
|
|
38
64
|
'until [ "$(gh pr view --json headRefOid --jq .headRefOid 2>/dev/null)" = "$(git rev-parse HEAD)" ]; do',
|
|
@@ -41,7 +67,7 @@ const PUBLISH_FIX_COMMAND = [
|
|
|
41
67
|
'sleep 5',
|
|
42
68
|
'done',
|
|
43
69
|
].join('\n');
|
|
44
|
-
const MERGE_PULL_REQUEST_COMMAND = 'gh pr merge --merge --delete-branch --match-head-commit "$(git rev-parse HEAD)"
|
|
70
|
+
const MERGE_PULL_REQUEST_COMMAND = "pr=$(gh pr view --json url --jq .url) || exit 1\n[ \"$pr\" = '<pull-request-url>' ] || exit 1\nbase=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) || exit 1\n[ -n \"$base\" ] || exit 1\n[ \"$(gh pr view \"$pr\" --json baseRefName --jq .baseRefName)\" = \"$base\" ] || exit 1\ngh pr merge --merge --delete-branch --match-head-commit \"$(git rev-parse HEAD)\" || exit 1\n[ \"$(gh pr view \"$pr\" --json state --jq .state)\" = MERGED ] || exit 1\n[ \"$(git branch --show-current)\" = \"$base\" ]";
|
|
45
71
|
const UPDATE_LOCAL_DEFAULT_COMMAND = 'git pull --ff-only';
|
|
46
72
|
const WAIT_FOR_CHECKS_RESULTS = {
|
|
47
73
|
checksPassed: "The command exited with status zero: the pull request's checks passed, or the repository still reported no checks after a brief wait for them to register.",
|
|
@@ -49,15 +75,15 @@ const WAIT_FOR_CHECKS_RESULTS = {
|
|
|
49
75
|
};
|
|
50
76
|
const PUBLISH_FIX_RESULTS = {
|
|
51
77
|
fixPublished: "The command exited with status zero: the fix is pushed and the pull request's head is the pushed commit.",
|
|
52
|
-
fixNotPublished: "The command exited with a nonzero status: the push was rejected or the pull request's head did not advance to the pushed commit.",
|
|
78
|
+
fixNotPublished: "The command exited with a nonzero status: the checkout no longer infers the published pull request, the push was rejected, or the pull request's head did not advance to the pushed commit.",
|
|
53
79
|
};
|
|
54
80
|
const WAIT_FOR_CHECKS_AFTER_FIX_RESULTS = {
|
|
55
81
|
checksPassed: "The command exited with status zero: the pull request's checks passed after the fix, or the repository still reported no checks after a brief wait for them to register.",
|
|
56
82
|
checksStillFailing: "The command exited with a nonzero status: the pull request's checks are still failing after the one fix attempt.",
|
|
57
83
|
};
|
|
58
84
|
const MERGE_PULL_REQUEST_RESULTS = {
|
|
59
|
-
merged: 'The command exited with status zero: the pull request is merged with a merge commit
|
|
60
|
-
mergeRefused: 'The command exited with a nonzero status: GitHub refused the merge,
|
|
85
|
+
merged: 'The command exited with status zero: the pull request targeted the repository default branch and is merged into it with a merge commit, deletion of the remote and local branch was requested, and the local default branch is checked out.',
|
|
86
|
+
mergeRefused: 'The command exited with a nonzero status: the checkout no longer infers the published pull request, the pull request does not target the repository default branch, GitHub refused the merge, its merged state could not be confirmed, or the local switch to the default branch or the branch deletion failed.',
|
|
61
87
|
};
|
|
62
88
|
const UPDATE_LOCAL_DEFAULT_RESULTS = {
|
|
63
89
|
localDefaultUpdated: 'The command exited with status zero: the local default branch is fast-forwarded to the merged head.',
|
|
@@ -76,13 +102,13 @@ const STATE_DESCRIPTIONS = {
|
|
|
76
102
|
updateLocalDefault: 'Fast-forwarding the local default branch to the merged head.',
|
|
77
103
|
awaitBossReply: 'Waiting for Boss to answer Coder.',
|
|
78
104
|
failed: 'The pull-request delivery workflow failed and is waiting for new caller input.',
|
|
79
|
-
merged: 'The pull request is merged with a merge commit on the repository default branch,
|
|
80
|
-
mergedLocalBehind: 'The pull request is merged with a merge commit on the repository default branch
|
|
105
|
+
merged: 'The pull request is merged with a merge commit on the repository default branch, which is checked out and fast-forwarded to the merged head; the merge requested deletion of the remote and local branch.',
|
|
106
|
+
mergedLocalBehind: 'The pull request is merged with a merge commit on the repository default branch, which is checked out but could not be fast-forwarded to the merged head; the merge requested deletion of the remote and local branch.',
|
|
81
107
|
notPublished: 'The branch was not published or its pull request could not be opened; Coder reported the reason and no pull request is guaranteed to exist.',
|
|
82
108
|
fixFailed: "The pull request's checks were red and the single CODE fix attempt returned an authored abort, failure, or insufficient terminal result; the pull request remains open.",
|
|
83
|
-
fixNotPublished: "The fix could not be published: the push was rejected or the pull request's head did not advance to the pushed commit; the pull request remains open.",
|
|
109
|
+
fixNotPublished: "The fix could not be published: the checked-out branch no longer carries the published pull request, the push was rejected, or the pull request's head did not advance to the pushed commit; the pull request remains open.",
|
|
84
110
|
checksStillFailing: "The pull request's checks are still failing after the one fix attempt; the pull request remains open.",
|
|
85
|
-
mergeRefused: 'GitHub refused the merge, or the merge landed
|
|
111
|
+
mergeRefused: 'The merge did not complete: the checked-out branch no longer carries the published pull request, the pull request does not target the repository default branch, GitHub refused the merge, or the merge may have landed while its confirmation, the local switch to the default branch, or the branch deletion failed; the pull request is in the state GitHub reports.',
|
|
86
112
|
};
|
|
87
113
|
function playbookMeta(stateId, role) {
|
|
88
114
|
return {
|
|
@@ -489,7 +515,7 @@ const machineSetup = setup({
|
|
|
489
515
|
completion: 'checks-failed',
|
|
490
516
|
}),
|
|
491
517
|
completeMergeRefused: assign({
|
|
492
|
-
completion: 'merge-
|
|
518
|
+
completion: 'merge-unconfirmed',
|
|
493
519
|
}),
|
|
494
520
|
completeMerged: assign({
|
|
495
521
|
completion: 'merged',
|
|
@@ -543,9 +569,14 @@ export const prMachine = machineSetup.createMachine({
|
|
|
543
569
|
childResult: context.childFailure,
|
|
544
570
|
};
|
|
545
571
|
}
|
|
572
|
+
if (context.completion === 'merge-unconfirmed') {
|
|
573
|
+
return {
|
|
574
|
+
status: 'merge-unconfirmed',
|
|
575
|
+
...pullRequestIdentity(context),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
546
578
|
if (context.completion === 'fix-not-published' ||
|
|
547
|
-
context.completion === 'checks-failed'
|
|
548
|
-
context.completion === 'merge-refused') {
|
|
579
|
+
context.completion === 'checks-failed') {
|
|
549
580
|
return {
|
|
550
581
|
status: 'not-merged',
|
|
551
582
|
reason: context.completion,
|
|
@@ -703,10 +734,10 @@ export const prMachine = machineSetup.createMachine({
|
|
|
703
734
|
tags: ['playbook.busy'],
|
|
704
735
|
invoke: {
|
|
705
736
|
src: 'script',
|
|
706
|
-
input: () => ({
|
|
737
|
+
input: ({ context }) => ({
|
|
707
738
|
stateId: 'publishFix',
|
|
708
739
|
sourceItem: 'PR-4',
|
|
709
|
-
command: PUBLISH_FIX_COMMAND,
|
|
740
|
+
command: scriptCommand(PUBLISH_FIX_COMMAND, context),
|
|
710
741
|
result: PUBLISH_FIX_RESULTS,
|
|
711
742
|
}),
|
|
712
743
|
onDone: [
|
|
@@ -765,10 +796,10 @@ export const prMachine = machineSetup.createMachine({
|
|
|
765
796
|
tags: ['playbook.busy'],
|
|
766
797
|
invoke: {
|
|
767
798
|
src: 'script',
|
|
768
|
-
input: () => ({
|
|
799
|
+
input: ({ context }) => ({
|
|
769
800
|
stateId: 'mergePullRequest',
|
|
770
801
|
sourceItem: 'PR-6',
|
|
771
|
-
command: MERGE_PULL_REQUEST_COMMAND,
|
|
802
|
+
command: scriptCommand(MERGE_PULL_REQUEST_COMMAND, context),
|
|
772
803
|
result: MERGE_PULL_REQUEST_RESULTS,
|
|
773
804
|
}),
|
|
774
805
|
onDone: [
|
|
@@ -139,7 +139,17 @@ export type PrPlaybookOutput =
|
|
|
139
139
|
}
|
|
140
140
|
| {
|
|
141
141
|
readonly status: 'not-merged';
|
|
142
|
-
readonly reason: 'fix-not-published' | 'checks-failed'
|
|
142
|
+
readonly reason: 'fix-not-published' | 'checks-failed';
|
|
143
|
+
readonly pullRequest: string;
|
|
144
|
+
readonly pullRequestUrl: string;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The merge command exited nonzero. It ran, so whether GitHub merged the
|
|
148
|
+
* pull request is unknown — script output never enters context — and the
|
|
149
|
+
* result claims neither a merge nor its absence.
|
|
150
|
+
*/
|
|
151
|
+
| {
|
|
152
|
+
readonly status: 'merge-unconfirmed';
|
|
143
153
|
readonly pullRequest: string;
|
|
144
154
|
readonly pullRequestUrl: string;
|
|
145
155
|
};
|
|
@@ -150,7 +160,7 @@ export type PrCompletion =
|
|
|
150
160
|
| 'fix-failed'
|
|
151
161
|
| 'fix-not-published'
|
|
152
162
|
| 'checks-failed'
|
|
153
|
-
| 'merge-
|
|
163
|
+
| 'merge-unconfirmed';
|
|
154
164
|
|
|
155
165
|
/** The machine reads no input: the caller input arrives on `START_PR`. */
|
|
156
166
|
export type PrInput = Readonly<Record<never, never>>;
|
|
@@ -199,9 +209,36 @@ const OPEN_PULL_REQUEST_RESULTS = {
|
|
|
199
209
|
needsBossReply: NEEDS_BOSS_REPLY_RESULT,
|
|
200
210
|
} as const;
|
|
201
211
|
|
|
202
|
-
// The mechanical steps are static POSIX shell text (slc/optimize.md)
|
|
203
|
-
//
|
|
204
|
-
//
|
|
212
|
+
// The mechanical steps are static POSIX shell text (slc/optimize.md) with no
|
|
213
|
+
// prose. `gh` infers the pull request from the checked-out branch, which the
|
|
214
|
+
// run does not own, so the two steps that act on the pull request itself relay
|
|
215
|
+
// one runtime value — the pull request PR-1 published — through the same
|
|
216
|
+
// placeholder relay an acting prompt uses.
|
|
217
|
+
// The placeholder occupies a single-quoted shell word, and binding replaces
|
|
218
|
+
// that whole word — quotes included — with the value's own quoted literal.
|
|
219
|
+
const QUOTED_PULL_REQUEST_URL = "'<pull-request-url>'";
|
|
220
|
+
|
|
221
|
+
// A retained value is reported text, so it reaches the shell as one
|
|
222
|
+
// single-quoted literal word: no command substitution, no expansion, no word
|
|
223
|
+
// splitting. A single quote inside the value closes the literal, escapes
|
|
224
|
+
// itself, and reopens it — the only character single quotes cannot hold.
|
|
225
|
+
function shellLiteral(value: string): string {
|
|
226
|
+
return `'${value.split("'").join("'\\''")}'`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Bind the retained identity into a script command. Without it the quoted
|
|
230
|
+
// placeholder stays, and no `gh` output equals it, so the step refuses instead
|
|
231
|
+
// of acting on whatever the checkout now infers. Every script state is
|
|
232
|
+
// reachable only after `rememberPullRequest`, so the refusal is unreachable by
|
|
233
|
+
// construction and exists to keep the failure closed if that ever changes.
|
|
234
|
+
function scriptCommand(command: string, context: PrContext): string {
|
|
235
|
+
const url = context.pullRequestUrl;
|
|
236
|
+
if (!isNonEmptyString(url)) return command;
|
|
237
|
+
// A function replacement keeps `$`-patterns in the value from being read as
|
|
238
|
+
// replacement syntax.
|
|
239
|
+
return command.replaceAll(QUOTED_PULL_REQUEST_URL, () => shellLiteral(url));
|
|
240
|
+
}
|
|
241
|
+
|
|
205
242
|
const WAIT_FOR_CHECKS_COMMAND = [
|
|
206
243
|
'n=0',
|
|
207
244
|
"while gh pr checks 2>&1 | grep -q 'no checks reported'; do",
|
|
@@ -213,6 +250,7 @@ const WAIT_FOR_CHECKS_COMMAND = [
|
|
|
213
250
|
].join('\n');
|
|
214
251
|
|
|
215
252
|
const PUBLISH_FIX_COMMAND = [
|
|
253
|
+
'[ "$(gh pr view --json url --jq .url)" = \'<pull-request-url>\' ] || exit 1',
|
|
216
254
|
'git push || exit 1',
|
|
217
255
|
'n=0',
|
|
218
256
|
'until [ "$(gh pr view --json headRefOid --jq .headRefOid 2>/dev/null)" = "$(git rev-parse HEAD)" ]; do',
|
|
@@ -223,7 +261,7 @@ const PUBLISH_FIX_COMMAND = [
|
|
|
223
261
|
].join('\n');
|
|
224
262
|
|
|
225
263
|
const MERGE_PULL_REQUEST_COMMAND =
|
|
226
|
-
'gh pr merge --merge --delete-branch --match-head-commit "$(git rev-parse HEAD)"
|
|
264
|
+
"pr=$(gh pr view --json url --jq .url) || exit 1\n[ \"$pr\" = '<pull-request-url>' ] || exit 1\nbase=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) || exit 1\n[ -n \"$base\" ] || exit 1\n[ \"$(gh pr view \"$pr\" --json baseRefName --jq .baseRefName)\" = \"$base\" ] || exit 1\ngh pr merge --merge --delete-branch --match-head-commit \"$(git rev-parse HEAD)\" || exit 1\n[ \"$(gh pr view \"$pr\" --json state --jq .state)\" = MERGED ] || exit 1\n[ \"$(git branch --show-current)\" = \"$base\" ]";
|
|
227
265
|
|
|
228
266
|
const UPDATE_LOCAL_DEFAULT_COMMAND = 'git pull --ff-only';
|
|
229
267
|
|
|
@@ -238,7 +276,7 @@ const PUBLISH_FIX_RESULTS = {
|
|
|
238
276
|
fixPublished:
|
|
239
277
|
"The command exited with status zero: the fix is pushed and the pull request's head is the pushed commit.",
|
|
240
278
|
fixNotPublished:
|
|
241
|
-
"The command exited with a nonzero status: the push was rejected or the pull request's head did not advance to the pushed commit.",
|
|
279
|
+
"The command exited with a nonzero status: the checkout no longer infers the published pull request, the push was rejected, or the pull request's head did not advance to the pushed commit.",
|
|
242
280
|
} as const;
|
|
243
281
|
|
|
244
282
|
const WAIT_FOR_CHECKS_AFTER_FIX_RESULTS = {
|
|
@@ -250,9 +288,9 @@ const WAIT_FOR_CHECKS_AFTER_FIX_RESULTS = {
|
|
|
250
288
|
|
|
251
289
|
const MERGE_PULL_REQUEST_RESULTS = {
|
|
252
290
|
merged:
|
|
253
|
-
'The command exited with status zero: the pull request is merged with a merge commit
|
|
291
|
+
'The command exited with status zero: the pull request targeted the repository default branch and is merged into it with a merge commit, deletion of the remote and local branch was requested, and the local default branch is checked out.',
|
|
254
292
|
mergeRefused:
|
|
255
|
-
'The command exited with a nonzero status: GitHub refused the merge,
|
|
293
|
+
'The command exited with a nonzero status: the checkout no longer infers the published pull request, the pull request does not target the repository default branch, GitHub refused the merge, its merged state could not be confirmed, or the local switch to the default branch or the branch deletion failed.',
|
|
256
294
|
} as const;
|
|
257
295
|
|
|
258
296
|
const UPDATE_LOCAL_DEFAULT_RESULTS = {
|
|
@@ -286,19 +324,19 @@ const STATE_DESCRIPTIONS = {
|
|
|
286
324
|
failed:
|
|
287
325
|
'The pull-request delivery workflow failed and is waiting for new caller input.',
|
|
288
326
|
merged:
|
|
289
|
-
'The pull request is merged with a merge commit on the repository default branch,
|
|
327
|
+
'The pull request is merged with a merge commit on the repository default branch, which is checked out and fast-forwarded to the merged head; the merge requested deletion of the remote and local branch.',
|
|
290
328
|
mergedLocalBehind:
|
|
291
|
-
'The pull request is merged with a merge commit on the repository default branch
|
|
329
|
+
'The pull request is merged with a merge commit on the repository default branch, which is checked out but could not be fast-forwarded to the merged head; the merge requested deletion of the remote and local branch.',
|
|
292
330
|
notPublished:
|
|
293
331
|
'The branch was not published or its pull request could not be opened; Coder reported the reason and no pull request is guaranteed to exist.',
|
|
294
332
|
fixFailed:
|
|
295
333
|
"The pull request's checks were red and the single CODE fix attempt returned an authored abort, failure, or insufficient terminal result; the pull request remains open.",
|
|
296
334
|
fixNotPublished:
|
|
297
|
-
"The fix could not be published: the push was rejected or the pull request's head did not advance to the pushed commit; the pull request remains open.",
|
|
335
|
+
"The fix could not be published: the checked-out branch no longer carries the published pull request, the push was rejected, or the pull request's head did not advance to the pushed commit; the pull request remains open.",
|
|
298
336
|
checksStillFailing:
|
|
299
337
|
"The pull request's checks are still failing after the one fix attempt; the pull request remains open.",
|
|
300
338
|
mergeRefused:
|
|
301
|
-
'GitHub refused the merge, or the merge landed
|
|
339
|
+
'The merge did not complete: the checked-out branch no longer carries the published pull request, the pull request does not target the repository default branch, GitHub refused the merge, or the merge may have landed while its confirmation, the local switch to the default branch, or the branch deletion failed; the pull request is in the state GitHub reports.',
|
|
302
340
|
} as const;
|
|
303
341
|
|
|
304
342
|
function playbookMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
|
|
@@ -829,7 +867,7 @@ const machineSetup = setup({
|
|
|
829
867
|
completion: 'checks-failed' as const,
|
|
830
868
|
}),
|
|
831
869
|
completeMergeRefused: assign({
|
|
832
|
-
completion: 'merge-
|
|
870
|
+
completion: 'merge-unconfirmed' as const,
|
|
833
871
|
}),
|
|
834
872
|
completeMerged: assign({
|
|
835
873
|
completion: 'merged' as const,
|
|
@@ -889,10 +927,15 @@ export const prMachine = machineSetup.createMachine({
|
|
|
889
927
|
childResult: context.childFailure,
|
|
890
928
|
};
|
|
891
929
|
}
|
|
930
|
+
if (context.completion === 'merge-unconfirmed') {
|
|
931
|
+
return {
|
|
932
|
+
status: 'merge-unconfirmed',
|
|
933
|
+
...pullRequestIdentity(context),
|
|
934
|
+
};
|
|
935
|
+
}
|
|
892
936
|
if (
|
|
893
937
|
context.completion === 'fix-not-published' ||
|
|
894
|
-
context.completion === 'checks-failed'
|
|
895
|
-
context.completion === 'merge-refused'
|
|
938
|
+
context.completion === 'checks-failed'
|
|
896
939
|
) {
|
|
897
940
|
return {
|
|
898
941
|
status: 'not-merged',
|
|
@@ -1051,10 +1094,10 @@ export const prMachine = machineSetup.createMachine({
|
|
|
1051
1094
|
tags: ['playbook.busy'],
|
|
1052
1095
|
invoke: {
|
|
1053
1096
|
src: 'script',
|
|
1054
|
-
input: (): ScriptInput => ({
|
|
1097
|
+
input: ({ context }): ScriptInput => ({
|
|
1055
1098
|
stateId: 'publishFix',
|
|
1056
1099
|
sourceItem: 'PR-4',
|
|
1057
|
-
command: PUBLISH_FIX_COMMAND,
|
|
1100
|
+
command: scriptCommand(PUBLISH_FIX_COMMAND, context),
|
|
1058
1101
|
result: PUBLISH_FIX_RESULTS,
|
|
1059
1102
|
}),
|
|
1060
1103
|
onDone: [
|
|
@@ -1113,10 +1156,10 @@ export const prMachine = machineSetup.createMachine({
|
|
|
1113
1156
|
tags: ['playbook.busy'],
|
|
1114
1157
|
invoke: {
|
|
1115
1158
|
src: 'script',
|
|
1116
|
-
input: (): ScriptInput => ({
|
|
1159
|
+
input: ({ context }): ScriptInput => ({
|
|
1117
1160
|
stateId: 'mergePullRequest',
|
|
1118
1161
|
sourceItem: 'PR-6',
|
|
1119
|
-
command: MERGE_PULL_REQUEST_COMMAND,
|
|
1162
|
+
command: scriptCommand(MERGE_PULL_REQUEST_COMMAND, context),
|
|
1120
1163
|
result: MERGE_PULL_REQUEST_RESULTS,
|
|
1121
1164
|
}),
|
|
1122
1165
|
onDone: [
|
|
@@ -11,7 +11,9 @@ The caller supplies the original request including the issue it names, if any; t
|
|
|
11
11
|
`pr` delivers a reviewed branch into the repository default branch through a GitHub pull request: it publishes the branch, waits for the pull request's checks, fixes red checks through playbook `code` no more than once, and merges.
|
|
12
12
|
It changes no files and owns no repository commit; the one fix it may request is owned by `code`.
|
|
13
13
|
The check waits, the fix publication, the merge, and the local update are mechanical steps: each runs one fixed command whose exit status alone decides its two outcomes, reads no conversation, and produces no prose.
|
|
14
|
-
`gh` infers the pull request from the checked-out branch, so
|
|
14
|
+
`gh` infers the pull request from the checked-out branch, which the run does not own: the nested `code` call suspends across Boss turns, so the checkout can change before the fix is published or the merge runs.
|
|
15
|
+
The two steps that act on the pull request itself therefore carry one runtime value — the pull request `pr` published — and refuse unless the checkout still infers exactly it.
|
|
16
|
+
That identity is reported text, so it binds as a single-quoted shell literal: the command compares it as data and never executes it as shell syntax.
|
|
15
17
|
|
|
16
18
|
## Coder
|
|
17
19
|
|
|
@@ -80,6 +82,7 @@ Workflow outcomes:
|
|
|
80
82
|
|
|
81
83
|
When `code` succeeds, Captain shall run:
|
|
82
84
|
|
|
85
|
+
> [ "$(gh pr view --json url --jq .url)" = '<pull-request-url>' ] || exit 1
|
|
83
86
|
> git push || exit 1
|
|
84
87
|
> n=0
|
|
85
88
|
> until [ "$(gh pr view --json headRefOid --jq .headRefOid 2>/dev/null)" = "$(git rev-parse HEAD)" ]; do
|
|
@@ -90,10 +93,11 @@ When `code` succeeds, Captain shall run:
|
|
|
90
93
|
|
|
91
94
|
Results:
|
|
92
95
|
- `fixPublished`: The command exited with status zero: the fix is pushed and the pull request's head is the pushed commit.
|
|
93
|
-
- `fixNotPublished`: The command exited with a nonzero status: the push was rejected or the pull request's head did not advance to the pushed commit.
|
|
96
|
+
- `fixNotPublished`: The command exited with a nonzero status: the checkout no longer infers the published pull request, the push was rejected, or the pull request's head did not advance to the pushed commit.
|
|
94
97
|
|
|
95
98
|
Workflow outcomes:
|
|
96
99
|
- The publication has exactly two outcomes decided by the exit status alone: fix published on status zero, once the pull request's head is the pushed commit, and fix not published otherwise.
|
|
100
|
+
- The command pushes nothing until the checked-out branch infers the pull request `pr` published, so a checkout that changed while `code` ran publishes to no other branch.
|
|
97
101
|
- Fix not published is an authored failure that leaves the pull request open.
|
|
98
102
|
|
|
99
103
|
### PR-5
|
|
@@ -120,16 +124,25 @@ Workflow outcomes:
|
|
|
120
124
|
|
|
121
125
|
When the checks pass, before or after the one fix attempt, Captain shall run:
|
|
122
126
|
|
|
123
|
-
> gh pr
|
|
127
|
+
> pr=$(gh pr view --json url --jq .url) || exit 1
|
|
128
|
+
> [ "$pr" = '<pull-request-url>' ] || exit 1
|
|
129
|
+
> base=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) || exit 1
|
|
130
|
+
> [ -n "$base" ] || exit 1
|
|
131
|
+
> [ "$(gh pr view "$pr" --json baseRefName --jq .baseRefName)" = "$base" ] || exit 1
|
|
132
|
+
> gh pr merge --merge --delete-branch --match-head-commit "$(git rev-parse HEAD)" || exit 1
|
|
133
|
+
> [ "$(gh pr view "$pr" --json state --jq .state)" = MERGED ] || exit 1
|
|
134
|
+
> [ "$(git branch --show-current)" = "$base" ]
|
|
124
135
|
|
|
125
136
|
Results:
|
|
126
|
-
- `merged`: The command exited with status zero: the pull request is merged with a merge commit
|
|
127
|
-
- `mergeRefused`: The command exited with a nonzero status: GitHub refused the merge,
|
|
137
|
+
- `merged`: The command exited with status zero: the pull request targeted the repository default branch and is merged into it with a merge commit, deletion of the remote and local branch was requested, and the local default branch is checked out.
|
|
138
|
+
- `mergeRefused`: The command exited with a nonzero status: the checkout no longer infers the published pull request, the pull request does not target the repository default branch, GitHub refused the merge, its merged state could not be confirmed, or the local switch to the default branch or the branch deletion failed.
|
|
128
139
|
|
|
129
140
|
Workflow outcomes:
|
|
130
|
-
- The merge creates a merge commit on the repository default branch,
|
|
141
|
+
- The merge creates a merge commit on the repository default branch, requests deletion of the remote and local branch, and checks out the local default branch; GitHub closes the linked issue on merge.
|
|
131
142
|
- The merge has exactly two outcomes decided by the exit status alone: merged on status zero and merge refused otherwise.
|
|
132
|
-
-
|
|
143
|
+
- The command requires the inferred pull request to be the one `pr` published and to target the repository default branch before the irreversible merge, and confirms that same pull request's merged state and the default-branch checkout after the merge command succeeds; a queued pull request is not a merged result.
|
|
144
|
+
- Merge refused is an authored failure that leaves the pull request in the state GitHub reports: the checkout no longer infers the published pull request, the pull request targets another branch, GitHub refused the merge for a conflict, a branch protection, a forbidden merge method, or a head that moved, or the merge may have landed while its confirmation, the local switch to the default branch, or the branch deletion failed.
|
|
145
|
+
- A refused merge does not establish that the pull request is unmerged, so `pr` reports it as an unconfirmed merge rather than as not merged, and no outcome claims a branch was deleted, because `gh` skips the remote deletion for a pull request from another repository or one already merged and exits zero anyway.
|
|
133
146
|
|
|
134
147
|
### PR-7
|
|
135
148
|
|
|
@@ -21,7 +21,8 @@ const VERBATIM_PAYLOAD_FIELDS = new Set([
|
|
|
21
21
|
'coderOutput',
|
|
22
22
|
]);
|
|
23
23
|
// Every failure terminal leaves the delivery unfinished: no pull request is
|
|
24
|
-
// guaranteed to exist after notPublished,
|
|
24
|
+
// guaranteed to exist after notPublished, the three fix terminals leave it
|
|
25
|
+
// open, and mergeRefused leaves it in the state GitHub reports.
|
|
25
26
|
const UNFINISHED_FINAL_STATE_IDS = new Set([
|
|
26
27
|
'notPublished',
|
|
27
28
|
'fixFailed',
|
|
@@ -100,7 +100,8 @@ const VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string> = new Set([
|
|
|
100
100
|
'coderOutput',
|
|
101
101
|
]);
|
|
102
102
|
// Every failure terminal leaves the delivery unfinished: no pull request is
|
|
103
|
-
// guaranteed to exist after notPublished,
|
|
103
|
+
// guaranteed to exist after notPublished, the three fix terminals leave it
|
|
104
|
+
// open, and mergeRefused leaves it in the state GitHub reports.
|
|
104
105
|
const UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string> = new Set([
|
|
105
106
|
'notPublished',
|
|
106
107
|
'fixFailed',
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai> -->
|
|
3
|
+
|
|
4
|
+
## Optional FSM authoring initializer (unmeasured experiment)
|
|
5
|
+
|
|
6
|
+
For an LF English GEARS artifact with ordinary Captain/player items and static nested calls, you may initialize a **new** target with the adjacent [scaffold-fsm.mjs](../scaffold-fsm.mjs):
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
node <definition-directory>/scaffold-fsm.mjs --source <source> --out <target.fsm.ts>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Resolve the helper from the actual definition directory supplied with the phase invocation.
|
|
13
|
+
The target must resolve XState with `setup.extend` and `createStateConfig`; otherwise initialization refuses.
|
|
14
|
+
It copies exact prompt/result constants and emits an **incomplete** XState setup scaffold.
|
|
15
|
+
Script, dynamic-call, parallel, malformed or ambiguous layouts refuse without modifying Source or replacing the target; continue ordinary authoring on refusal.
|
|
16
|
+
The output intentionally contains unresolved `__AUTHOR_*` markers, so successful initialization is not successful compilation.
|
|
17
|
+
|
|
18
|
+
Read the full Source and this phase's normative rules, then complete the target directly:
|
|
19
|
+
|
|
20
|
+
- Keep acting `GEARS_ITEMS[id].prompt` literal and carry every Source-owned runtime value in typed `invoke.input` fields beside it. Nested constants are templates; compose child text as the nested-call contract requires.
|
|
21
|
+
- Exact `GEARS_ITEMS[id].result` values contain only authored outcomes. Apply the existing phase's single-outcome, universal question and controller rules where appropriate. Derive required payloads from Source and actual public child interfaces.
|
|
22
|
+
- Replace all type/code markers with the real context, events, inputs, outputs, field relays, assignments, routing, guards, public state metadata, continuation and failure behavior. Rename or remove the example assignment as appropriate.
|
|
23
|
+
- Register reusable assignments with `actorSetup.extend({ actions: { name: assign(...) } })` and reference their registered names. Narrow heterogeneous actor output from `unknown`; do not erase actor/event types with casts to reuse a standalone assignment on `onDone`.
|
|
24
|
+
- For a nested call, the scaffold also exports `authoredChildResult(error, expectedPlaybookId)`. It delegates public-envelope validation to the installed stateless validator. Use the actual selected target id, not a fabricated request or child-session id; Source still owns recovery routes, output predicates and compact evidence. Remove the helper if unused.
|
|
25
|
+
- Shared configuration fragments may use `machineSetup.createStateConfig(...)` to preserve their literal transition types.
|
|
26
|
+
|
|
27
|
+
Emit one self-contained `.fsm.ts`, with no imports from the initializer or auxiliary semantic files.
|
|
28
|
+
Its only generated imports are XState and, for a child actor, the selected Playbook public validator and public type; those stateless imports bind no runner or actor implementation.
|
|
29
|
+
All ordinary strict, Source, GEARS/FSM and later runtime checks remain authoritative.
|
|
30
|
+
No workflow route, child acceptance predicate or default context value is supplied by this helper.
|
|
31
|
+
This technique has no accepted performance result yet.
|