@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.
Files changed (32) hide show
  1. package/README.md +5 -5
  2. package/docs/configuration.md +10 -2
  3. package/docs/embedding.md +20 -1
  4. package/package.json +6 -1
  5. package/reference/sdlc/code.playbook/bin/launch-config.js +88 -5
  6. package/reference/sdlc/code.playbook/bin/playbook.js +4 -0
  7. package/reference/sdlc/code.playbook/bin/repository-effects.js +11 -3
  8. package/reference/sdlc/code.playbook/bin/run.js +4 -0
  9. package/reference/sdlc/code.playbook/bin/session-store.js +2 -2
  10. package/reference/sdlc/code.playbook/playbook-captain.js +17 -4
  11. package/reference/sdlc/code.playbook/playbook-captain.ts +19 -4
  12. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +3 -10
  13. package/reference/sdlc/dev.playbook/dev.playbook.js +5 -5
  14. package/reference/sdlc/dev.playbook/dev.playbook.ts +7 -7
  15. package/reference/sdlc/pr.md +17 -4
  16. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +17 -3
  17. package/reference/sdlc/pr.playbook/pr.fsm.js +49 -18
  18. package/reference/sdlc/pr.playbook/pr.fsm.ts +63 -20
  19. package/reference/sdlc/pr.playbook/pr.gears.md +20 -7
  20. package/reference/sdlc/pr.playbook/pr.playbook.js +2 -1
  21. package/reference/sdlc/pr.playbook/pr.playbook.ts +2 -1
  22. package/slc/experiments/fsm-scaffold-guidance.md +31 -0
  23. package/slc/gears2fsm.md +145 -58
  24. package/slc/link.md +158 -7
  25. package/slc/materialize-link.mjs +460 -0
  26. package/slc/optimize.md +46 -6
  27. package/slc/scaffold-fsm.mjs +225 -0
  28. package/slc/slc.pin-inputs.json +13 -0
  29. package/slc/text2gears.md +38 -3
  30. package/slc/workflow-contracts.json +371 -0
  31. package/src/xstate-playbook-runtime.js +35 -1
  32. 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): no
24
- // placeholder, no runtime value, and no prose — `gh` infers the pull request
25
- // from the checked-out branch.
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 on the repository default branch, the remote and local branch are deleted, and the local default branch is checked out.',
60
- mergeRefused: 'The command exited with a nonzero status: GitHub refused the merge, or the merge landed but the local switch to the default branch or the branch deletion failed.',
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, both branches are deleted, and the local default branch is fast-forwarded to the merged head.',
80
- mergedLocalBehind: 'The pull request is merged with a merge commit on the repository default branch and both branches are deleted, but the local default branch could not be fast-forwarded to the merged head.',
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 but the local switch to the default branch or the branch deletion failed; the pull request is in the state GitHub reports.',
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-refused',
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' | 'merge-refused';
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-refused';
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): no
203
- // placeholder, no runtime value, and no prose — `gh` infers the pull request
204
- // from the checked-out branch.
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 on the repository default branch, the remote and local branch are deleted, and the local default branch is checked out.',
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, or the merge landed but the local switch to the default branch or the branch deletion failed.',
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, both branches are deleted, and the local default branch is fast-forwarded to the merged head.',
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 and both branches are deleted, but the local default branch could not be fast-forwarded to the merged head.',
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 but the local switch to the default branch or the branch deletion failed; the pull request is in the state GitHub reports.',
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-refused' as const,
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 no mechanical step needs a runtime value.
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 merge --merge --delete-branch --match-head-commit "$(git rev-parse HEAD)"
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 on the repository default branch, the remote and local branch are deleted, and the local default branch is checked out.
127
- - `mergeRefused`: The command exited with a nonzero status: GitHub refused the merge, or the merge landed but the local switch to the default branch or the branch deletion failed.
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, deletes the remote and local branch, and checks out the local default branch; GitHub closes the linked issue on merge.
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
- - Merge refused is an authored failure that leaves the pull request in the state GitHub reports: GitHub refused the merge for a conflict, a branch protection, a forbidden merge method, or a head that moved, or the merge landed but the local switch to the default branch or the branch deletion failed.
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, and the other four leave it open.
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, and the other four leave it open.
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.