@sublang/playbook 13.1.0 → 13.3.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 (43) hide show
  1. package/README.md +7 -5
  2. package/docs/cli.md +28 -9
  3. package/docs/configuration.md +27 -14
  4. package/docs/embedding.md +1 -1
  5. package/package.json +40 -2
  6. package/reference/sdlc/branch.md +37 -0
  7. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +169 -0
  8. package/reference/sdlc/branch.playbook/branch.fsm.js +325 -0
  9. package/reference/sdlc/branch.playbook/branch.fsm.ts +458 -0
  10. package/reference/sdlc/branch.playbook/branch.gears.md +44 -0
  11. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +20 -0
  12. package/reference/sdlc/branch.playbook/branch.playbook.js +121 -0
  13. package/reference/sdlc/branch.playbook/branch.playbook.ts +217 -0
  14. package/reference/sdlc/branch.playbook/branch.registry.d.ts +40 -0
  15. package/reference/sdlc/branch.playbook/branch.registry.js +63 -0
  16. package/reference/sdlc/branch.playbook/branch.registry.ts +121 -0
  17. package/reference/sdlc/code.playbook/bin/session-host.js +13 -3
  18. package/reference/sdlc/code.playbook/playbook-captain.d.ts +26 -1
  19. package/reference/sdlc/code.playbook/playbook-captain.js +218 -6
  20. package/reference/sdlc/code.playbook/playbook-captain.ts +281 -6
  21. package/reference/sdlc/code.playbook/playbook.config.template.yaml +21 -8
  22. package/reference/sdlc/code.playbook/session-host.d.ts +26 -0
  23. package/reference/sdlc/dev.md +24 -3
  24. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +100 -9
  25. package/reference/sdlc/dev.playbook/dev.fsm.js +235 -18
  26. package/reference/sdlc/dev.playbook/dev.fsm.ts +368 -24
  27. package/reference/sdlc/dev.playbook/dev.gears.md +45 -7
  28. package/reference/sdlc/dev.playbook/dev.playbook.js +13 -1
  29. package/reference/sdlc/dev.playbook/dev.playbook.ts +13 -1
  30. package/reference/sdlc/dev.playbook/dev.registry.d.ts +1 -1
  31. package/reference/sdlc/dev.playbook/dev.registry.js +7 -2
  32. package/reference/sdlc/dev.playbook/dev.registry.ts +7 -2
  33. package/reference/sdlc/pr.md +108 -0
  34. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +352 -0
  35. package/reference/sdlc/pr.playbook/pr.fsm.js +902 -0
  36. package/reference/sdlc/pr.playbook/pr.fsm.ts +1251 -0
  37. package/reference/sdlc/pr.playbook/pr.gears.md +155 -0
  38. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +27 -0
  39. package/reference/sdlc/pr.playbook/pr.playbook.js +133 -0
  40. package/reference/sdlc/pr.playbook/pr.playbook.ts +235 -0
  41. package/reference/sdlc/pr.playbook/pr.registry.d.ts +40 -0
  42. package/reference/sdlc/pr.playbook/pr.registry.js +68 -0
  43. package/reference/sdlc/pr.playbook/pr.registry.ts +124 -0
@@ -0,0 +1,155 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai> -->
3
+
4
+ # PR: Pull-Request Delivery Workflow
5
+
6
+ Roles:
7
+
8
+ - Coder
9
+
10
+ The caller supplies the original request including the issue it names, if any; the issue summary; the branch to deliver and the base revision it was created from; the last `code`-owned commit and the exact evaluated repository revision; and optional relevant context.
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
+ It changes no files and owns no repository commit; the one fix it may request is owned by `code`.
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.
15
+
16
+ ## Coder
17
+
18
+ ### PR-1
19
+
20
+ When the caller gives its input, Captain shall relay the complete caller input in quotes (`>`) to Coder, along with the following instruction:
21
+
22
+ > > Original request: <caller-input>
23
+ >
24
+ > Publish the branch and open its pull request without changing any file or making any commit.
25
+ > Confirm that the working tree is clean and that the checked-out branch is the branch to deliver, not the repository default branch.
26
+ > Push the branch to the repository's GitHub remote with its upstream set; never force-push.
27
+ > If an open pull request for this branch already exists, use it; otherwise open one against the repository default branch with `gh pr create`.
28
+ > Give the pull request a title naming the change and a body with a summary of what changed and why from the base revision to the last commit, the verification the commits report, and a `Closes #N` line for issue number N when the request names one.
29
+ > Report the pull request number and URL exactly.
30
+ > If the working tree is not clean, the checked-out branch is wrong, the push is rejected, or the pull request cannot be opened, open nothing further and report the failure with its reason.
31
+
32
+ Results:
33
+ - `opened`: Coder pushed the branch with its upstream set and reported the open pull request for it. Output shall include `pullRequest: <pull request number>` and `pullRequestUrl: <pull request URL>`.
34
+ - `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>`.
35
+
36
+ Workflow outcomes:
37
+ - The result has two semantic outcomes: opened and not published.
38
+ - Each outcome requires affirmative support in Coder's result, and no outcome depends on a fixed presentation format of Coder's reply.
39
+ - Every outcome keeps the repository exact: pushing a branch and opening a pull request change neither HEAD's commit nor the working tree.
40
+ - For not published, `pr` fails and reports Coder's complete result with its reason to its caller.
41
+
42
+ ## Checks, the one fix, and the merge
43
+
44
+ ### PR-2
45
+
46
+ When the pull request is open and no fix has been attempted, Captain shall run:
47
+
48
+ > n=0
49
+ > while gh pr checks 2>&1 | grep -q 'no checks reported'; do
50
+ > n=$((n + 1))
51
+ > [ "$n" -ge 6 ] && exit 0
52
+ > sleep 10
53
+ > done
54
+ > gh pr checks --watch --fail-fast >/dev/null 2>&1
55
+
56
+ Results:
57
+ - `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.
58
+ - `checksFailed`: The command exited with a nonzero status: the pull request's checks failed.
59
+
60
+ Workflow outcomes:
61
+ - The wait has exactly two outcomes decided by the command's exit status alone: checks passed on status zero and checks failed otherwise.
62
+ - A pull request whose repository still reports no checks after a brief wait for them to register counts as passed.
63
+ - Checks passed continues with the merge; checks failed continues with the one `code` fix attempt.
64
+
65
+ ### PR-3
66
+
67
+ When the checks fail before any fix attempt, Captain shall call playbook `code`:
68
+
69
+ > > Original request: <caller-input>
70
+ > > Pull request: <pull-request-url>
71
+ > > Coding request: The pull request's checks are red on the checked-out branch. Inspect the failing checks with `gh pr checks` and `gh run view --log-failed`, fix their cause on this branch with a minimal change, and make the checks pass.
72
+
73
+ Workflow outcomes:
74
+ - `pr` makes no more than one fix attempt.
75
+ - Only after `code` succeeds does `pr` publish the fix and wait for the checks again.
76
+ - An authored `code` abort or failure, or a terminal `code` result that does not prove its success, terminates `pr` with that canonical result relayed and the pull request left open.
77
+ - Any other nested-call error parks `pr` as failed and retains the control-plane error.
78
+
79
+ ### PR-4
80
+
81
+ When `code` succeeds, Captain shall run:
82
+
83
+ > git push || exit 1
84
+ > n=0
85
+ > until [ "$(gh pr view --json headRefOid --jq .headRefOid 2>/dev/null)" = "$(git rev-parse HEAD)" ]; do
86
+ > n=$((n + 1))
87
+ > [ "$n" -ge 12 ] && exit 1
88
+ > sleep 5
89
+ > done
90
+
91
+ Results:
92
+ - `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.
94
+
95
+ Workflow outcomes:
96
+ - 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.
97
+ - Fix not published is an authored failure that leaves the pull request open.
98
+
99
+ ### PR-5
100
+
101
+ When the fix is published, Captain shall run:
102
+
103
+ > n=0
104
+ > while gh pr checks 2>&1 | grep -q 'no checks reported'; do
105
+ > n=$((n + 1))
106
+ > [ "$n" -ge 6 ] && exit 0
107
+ > sleep 10
108
+ > done
109
+ > gh pr checks --watch --fail-fast >/dev/null 2>&1
110
+
111
+ Results:
112
+ - `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.
113
+ - `checksStillFailing`: The command exited with a nonzero status: the pull request's checks are still failing after the one fix attempt.
114
+
115
+ Workflow outcomes:
116
+ - This wait has exactly two outcomes decided by the exit status alone: checks passed on status zero and checks still failing otherwise.
117
+ - Checks still failing is an authored failure that leaves the pull request open; there is no second fix attempt.
118
+
119
+ ### PR-6
120
+
121
+ When the checks pass, before or after the one fix attempt, Captain shall run:
122
+
123
+ > gh pr merge --merge --delete-branch --match-head-commit "$(git rev-parse HEAD)"
124
+
125
+ 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.
128
+
129
+ 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.
131
+ - 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.
133
+
134
+ ### PR-7
135
+
136
+ When the pull request is merged, Captain shall run:
137
+
138
+ > git pull --ff-only
139
+
140
+ Results:
141
+ - `localDefaultUpdated`: The command exited with status zero: the local default branch is fast-forwarded to the merged head.
142
+ - `localDefaultNotUpdated`: The command exited with a nonzero status: the local default branch was not fast-forwarded to the merged head.
143
+
144
+ Workflow outcomes:
145
+ - The update has exactly two outcomes decided by the exit status alone: local default updated on status zero and local default not updated otherwise.
146
+ - Both complete `pr`: the pull request is merged either way, and the result states whether the local default branch was fast-forwarded to the merged head.
147
+ - On completion, `pr` returns the pull request number and URL, the fact that the pull request is merged, and whether the local default branch was fast-forwarded to the merged head.
148
+
149
+ ## Optimizations
150
+
151
+ - PR-2: captain → script
152
+ - PR-4: captain → script
153
+ - PR-5: captain → script
154
+ - PR-6: captain → script
155
+ - PR-7: captain → script
@@ -0,0 +1,27 @@
1
+ import { type XStatePlaybookRuntimeFactory, type XStatePlaybookRuntimeConstruction } from '@sublang/playbook/xstate-runtime';
2
+ import { type PlayerInput, type PrInput } from './pr.fsm.js';
3
+ import type { PlaybookHostConstructionCapabilities } from '../code.playbook/playbook-captain.js';
4
+ import type { CaptainCallOptions, CaptainResult, JsonValue, NormalizedError, PlayerCallOptions, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookControlReceipt, PlaybookControlView, PlaybookPendingCall, PlaybookPorts, PlaybookRunResult, PlaybookRuntime, PlaybookRuntimeFactory, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookStateValue, PlaybookTraceEvent, PlaybookTraceType, PlayerResult, PlayerSessionStore } from '@sublang/playbook/runtime';
5
+ export type { CaptainCallOptions, CaptainResult, JsonValue, NormalizedError, PlayerCallOptions, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookControlReceipt, PlaybookControlView, PlaybookPendingCall, PlaybookPorts, PlaybookRunResult, PlaybookRuntime, PlaybookRuntimeFactory, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookStateValue, PlaybookTraceEvent, PlaybookTraceType, PlayerResult, PlayerSessionStore, };
6
+ /**
7
+ * PR's per-run options. The machine reads no input of its own; the one
8
+ * linker-owned option is the working directory its five script states run
9
+ * in (slc/link.md §Script execution). Absent, the process working directory.
10
+ */
11
+ export type PrPlaybookOptions = PrInput & {
12
+ readonly cwd?: string;
13
+ };
14
+ export type PrPlaybookHostCapabilities = PlaybookHostConstructionCapabilities & XStatePlaybookRuntimeConstruction<PrPlaybookOptions, object>['hostCapabilities'];
15
+ /**
16
+ * Preserve the authored Markdown quote marker around every line of the
17
+ * relayed caller input. The generic composer preserves the marker itself;
18
+ * PR's override additionally keeps a multiline value inside that quote.
19
+ */
20
+ declare function composePlayerPrompt(input: PlayerInput, resuming?: boolean): string;
21
+ export declare const _internal: {
22
+ composePlayerPrompt: typeof composePlayerPrompt;
23
+ VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string>;
24
+ UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string>;
25
+ };
26
+ declare const createPlaybookRuntime: XStatePlaybookRuntimeFactory<XStatePlaybookRuntimeConstruction<PrPlaybookOptions, PrPlaybookHostCapabilities>, 3>;
27
+ export default createPlaybookRuntime;
@@ -0,0 +1,133 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+ //
4
+ // Generated by slc/link.md (FSM-to-Runtime linker).
5
+ // Source FSM: ./pr.fsm.ts
6
+ // Role metadata: Coder→coder (canonical local role)
7
+ // Boss event: deterministic START_PR entry; exact Boss text becomes
8
+ // callerInput; pending Coder questions retain BOSS_REPLY
9
+ // Adjudication: LLM judge for the one Coder state; coderOutput is carried
10
+ // verbatim; pullRequest and pullRequestUrl are judge-authored
11
+ // semantic fields; every arm is governed `unchanged`
12
+ // Scripts: five DR-016 script states run through the shared factory's
13
+ // `sh -c` actor in the optional `cwd` option
14
+ // Nested calls: literal code target through the shared bridge
15
+ // Compat: artifact schema 3 / runtime ABI 1
16
+ import { createXStatePlaybookRuntime, composePlayerContinuation, snapshotJsonValue, } from '@sublang/playbook/xstate-runtime';
17
+ import { prMachine, } from './pr.fsm.js';
18
+ const OPTION_KEYS = new Set(['cwd']);
19
+ const PLACEHOLDER = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
20
+ const VERBATIM_PAYLOAD_FIELDS = new Set([
21
+ 'coderOutput',
22
+ ]);
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.
25
+ const UNFINISHED_FINAL_STATE_IDS = new Set([
26
+ 'notPublished',
27
+ 'fixFailed',
28
+ 'fixNotPublished',
29
+ 'checksStillFailing',
30
+ 'mergeRefused',
31
+ ]);
32
+ function snapshotPrOptions(value) {
33
+ const captured = snapshotJsonValue(value, 'PR runtime options');
34
+ if (captured === null ||
35
+ typeof captured !== 'object' ||
36
+ Array.isArray(captured)) {
37
+ throw new TypeError('PR runtime options must be an object');
38
+ }
39
+ const record = captured;
40
+ for (const [key, option] of Object.entries(record)) {
41
+ if (!OPTION_KEYS.has(key)) {
42
+ throw new TypeError(`PR runtime options.${key} is not declared`);
43
+ }
44
+ if (typeof option !== 'string' || option.length === 0) {
45
+ throw new TypeError(`PR runtime options.${key} must be a non-empty string`);
46
+ }
47
+ }
48
+ return captured;
49
+ }
50
+ function placeholderField(token) {
51
+ if (token === '#')
52
+ return 'irNumber';
53
+ return token.replace(/-([A-Za-z0-9_$])/g, (_match, next) => next.toUpperCase());
54
+ }
55
+ function quotedContinuation(value) {
56
+ return value.replaceAll('\n', '\n> ');
57
+ }
58
+ /**
59
+ * Preserve the authored Markdown quote marker around every line of the
60
+ * relayed caller input. The generic composer preserves the marker itself;
61
+ * PR's override additionally keeps a multiline value inside that quote.
62
+ */
63
+ function composePlayerPrompt(input, resuming = false) {
64
+ const fields = input;
65
+ const body = input.prompt.replace(PLACEHOLDER, (match, token, offset, source) => {
66
+ const value = fields[placeholderField(token)];
67
+ if (typeof value !== 'string')
68
+ return match;
69
+ const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
70
+ return source.slice(lineStart, offset).startsWith('> ')
71
+ ? quotedContinuation(value)
72
+ : value;
73
+ });
74
+ return composePlayerContinuation(input, body, resuming);
75
+ }
76
+ export const _internal = {
77
+ composePlayerPrompt,
78
+ VERBATIM_PAYLOAD_FIELDS,
79
+ UNFINISHED_FINAL_STATE_IDS,
80
+ };
81
+ const runtimeSpec = {
82
+ label: 'PR',
83
+ // DR-022 / slc/link.md: the declaration carries the value current at link
84
+ // time — a literal, never the loading engine's RUNTIME_ABI self-report,
85
+ // which would follow whatever engine loads the module and make the
86
+ // factory's skew check compare that engine with itself.
87
+ compat: { artifactSchema: 3, runtimeAbi: 1 },
88
+ snapshotOptions: snapshotPrOptions,
89
+ entryEvent: {
90
+ type: 'START_PR',
91
+ textField: 'callerInput',
92
+ // `startPr` copies the entry text here, so the failure-state retry reads
93
+ // it back from the persisted machine snapshot and survives a continued
94
+ // session (DR-034).
95
+ contextField: 'callerInput',
96
+ },
97
+ roleStates: {
98
+ openPullRequest: {
99
+ role: 'coder',
100
+ label: 'Coder is publishing the branch and opening its pull request.',
101
+ },
102
+ },
103
+ // DR-050: PR owns no repository commit — pushing a branch and opening a
104
+ // pull request move only remote-tracking refs and forge state, so every
105
+ // Coder outcome is governed with the `unchanged` repository disposition.
106
+ // The pull request number and URL are judge-authored semantic fields; the
107
+ // Coder's report on a refused publication is carried verbatim.
108
+ outcomeAuthority: {
109
+ governedPlayerStates: {
110
+ openPullRequest: {
111
+ opened: {
112
+ fields: { pullRequest: 'semantic', pullRequestUrl: 'semantic' },
113
+ repositoryDisposition: 'unchanged',
114
+ },
115
+ notPublished: {
116
+ fields: { coderOutput: 'presentation' },
117
+ repositoryDisposition: 'unchanged',
118
+ },
119
+ needsBossReply: {
120
+ fields: { question: 'presentation' },
121
+ repositoryDisposition: 'unchanged',
122
+ },
123
+ },
124
+ },
125
+ },
126
+ composePlayerPrompt: (input, _identity, resuming) => composePlayerPrompt(input, resuming),
127
+ verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
128
+ controlContextFields: [],
129
+ unfinishedFinalStateIds: UNFINISHED_FINAL_STATE_IDS,
130
+ transitionEventFields: ['callerInput', 'answer', 'questionId'],
131
+ };
132
+ const createPlaybookRuntime = createXStatePlaybookRuntime(prMachine, runtimeSpec);
133
+ export default createPlaybookRuntime;
@@ -0,0 +1,235 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+ //
4
+ // Generated by slc/link.md (FSM-to-Runtime linker).
5
+ // Source FSM: ./pr.fsm.ts
6
+ // Role metadata: Coder→coder (canonical local role)
7
+ // Boss event: deterministic START_PR entry; exact Boss text becomes
8
+ // callerInput; pending Coder questions retain BOSS_REPLY
9
+ // Adjudication: LLM judge for the one Coder state; coderOutput is carried
10
+ // verbatim; pullRequest and pullRequestUrl are judge-authored
11
+ // semantic fields; every arm is governed `unchanged`
12
+ // Scripts: five DR-016 script states run through the shared factory's
13
+ // `sh -c` actor in the optional `cwd` option
14
+ // Nested calls: literal code target through the shared bridge
15
+ // Compat: artifact schema 3 / runtime ABI 1
16
+
17
+ import {
18
+ createXStatePlaybookRuntime,
19
+ composePlayerContinuation,
20
+ snapshotJsonValue,
21
+ type PlaybookPlayerInput,
22
+ type XStatePlaybookRuntimeFactory,
23
+ type XStatePlaybookRuntimeConstruction,
24
+ type XStatePlaybookRuntimeSpecV3,
25
+ } from '@sublang/playbook/xstate-runtime';
26
+ import {
27
+ prMachine,
28
+ type PlayerInput,
29
+ type PrInput,
30
+ } from './pr.fsm.js';
31
+ import type { PlaybookHostConstructionCapabilities } from '../code.playbook/playbook-captain.js';
32
+ import type {
33
+ CaptainCallOptions,
34
+ CaptainResult,
35
+ JsonValue,
36
+ NormalizedError,
37
+ PlayerCallOptions,
38
+ PlaybookCallRequest,
39
+ PlaybookCallResult,
40
+ PlaybookCallStart,
41
+ PlaybookControlReceipt,
42
+ PlaybookControlView,
43
+ PlaybookPendingCall,
44
+ PlaybookPorts,
45
+ PlaybookRunResult,
46
+ PlaybookRuntime,
47
+ PlaybookRuntimeFactory,
48
+ PlaybookRuntimeSnapshot,
49
+ PlaybookSession,
50
+ PlaybookState,
51
+ PlaybookStateValue,
52
+ PlaybookTraceEvent,
53
+ PlaybookTraceType,
54
+ PlayerResult,
55
+ PlayerSessionStore,
56
+ } from '@sublang/playbook/runtime';
57
+
58
+ export type {
59
+ CaptainCallOptions,
60
+ CaptainResult,
61
+ JsonValue,
62
+ NormalizedError,
63
+ PlayerCallOptions,
64
+ PlaybookCallRequest,
65
+ PlaybookCallResult,
66
+ PlaybookCallStart,
67
+ PlaybookControlReceipt,
68
+ PlaybookControlView,
69
+ PlaybookPendingCall,
70
+ PlaybookPorts,
71
+ PlaybookRunResult,
72
+ PlaybookRuntime,
73
+ PlaybookRuntimeFactory,
74
+ PlaybookRuntimeSnapshot,
75
+ PlaybookSession,
76
+ PlaybookState,
77
+ PlaybookStateValue,
78
+ PlaybookTraceEvent,
79
+ PlaybookTraceType,
80
+ PlayerResult,
81
+ PlayerSessionStore,
82
+ };
83
+
84
+ /**
85
+ * PR's per-run options. The machine reads no input of its own; the one
86
+ * linker-owned option is the working directory its five script states run
87
+ * in (slc/link.md §Script execution). Absent, the process working directory.
88
+ */
89
+ export type PrPlaybookOptions = PrInput & {
90
+ readonly cwd?: string;
91
+ };
92
+ export type PrPlaybookHostCapabilities =
93
+ PlaybookHostConstructionCapabilities &
94
+ XStatePlaybookRuntimeConstruction<PrPlaybookOptions, object>['hostCapabilities'];
95
+
96
+ const OPTION_KEYS = new Set(['cwd']);
97
+ const PLACEHOLDER = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
98
+
99
+ const VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string> = new Set([
100
+ 'coderOutput',
101
+ ]);
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.
104
+ const UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string> = new Set([
105
+ 'notPublished',
106
+ 'fixFailed',
107
+ 'fixNotPublished',
108
+ 'checksStillFailing',
109
+ 'mergeRefused',
110
+ ]);
111
+
112
+ function snapshotPrOptions(value: unknown): PrPlaybookOptions {
113
+ const captured = snapshotJsonValue(value, 'PR runtime options');
114
+ if (
115
+ captured === null ||
116
+ typeof captured !== 'object' ||
117
+ Array.isArray(captured)
118
+ ) {
119
+ throw new TypeError('PR runtime options must be an object');
120
+ }
121
+ const record = captured as Readonly<Record<string, JsonValue>>;
122
+ for (const [key, option] of Object.entries(record)) {
123
+ if (!OPTION_KEYS.has(key)) {
124
+ throw new TypeError(`PR runtime options.${key} is not declared`);
125
+ }
126
+ if (typeof option !== 'string' || option.length === 0) {
127
+ throw new TypeError(`PR runtime options.${key} must be a non-empty string`);
128
+ }
129
+ }
130
+ return captured as unknown as PrPlaybookOptions;
131
+ }
132
+
133
+ function placeholderField(token: string): string {
134
+ if (token === '#') return 'irNumber';
135
+ return token.replace(/-([A-Za-z0-9_$])/g, (_match, next: string) =>
136
+ next.toUpperCase(),
137
+ );
138
+ }
139
+
140
+ function quotedContinuation(value: string): string {
141
+ return value.replaceAll('\n', '\n> ');
142
+ }
143
+
144
+ /**
145
+ * Preserve the authored Markdown quote marker around every line of the
146
+ * relayed caller input. The generic composer preserves the marker itself;
147
+ * PR's override additionally keeps a multiline value inside that quote.
148
+ */
149
+ function composePlayerPrompt(input: PlayerInput, resuming = false): string {
150
+ const fields = input as unknown as Record<string, unknown>;
151
+ const body = input.prompt.replace(
152
+ PLACEHOLDER,
153
+ (match: string, token: string, offset: number, source: string) => {
154
+ const value = fields[placeholderField(token)];
155
+ if (typeof value !== 'string') return match;
156
+ const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
157
+ return source.slice(lineStart, offset).startsWith('> ')
158
+ ? quotedContinuation(value)
159
+ : value;
160
+ },
161
+ );
162
+ return composePlayerContinuation(input, body, resuming);
163
+ }
164
+
165
+ export const _internal = {
166
+ composePlayerPrompt,
167
+ VERBATIM_PAYLOAD_FIELDS,
168
+ UNFINISHED_FINAL_STATE_IDS,
169
+ };
170
+
171
+ const runtimeSpec = {
172
+ label: 'PR',
173
+ // DR-022 / slc/link.md: the declaration carries the value current at link
174
+ // time — a literal, never the loading engine's RUNTIME_ABI self-report,
175
+ // which would follow whatever engine loads the module and make the
176
+ // factory's skew check compare that engine with itself.
177
+ compat: { artifactSchema: 3, runtimeAbi: 1 },
178
+ snapshotOptions: snapshotPrOptions,
179
+ entryEvent: {
180
+ type: 'START_PR',
181
+ textField: 'callerInput',
182
+ // `startPr` copies the entry text here, so the failure-state retry reads
183
+ // it back from the persisted machine snapshot and survives a continued
184
+ // session (DR-034).
185
+ contextField: 'callerInput',
186
+ },
187
+ roleStates: {
188
+ openPullRequest: {
189
+ role: 'coder',
190
+ label: 'Coder is publishing the branch and opening its pull request.',
191
+ },
192
+ },
193
+ // DR-050: PR owns no repository commit — pushing a branch and opening a
194
+ // pull request move only remote-tracking refs and forge state, so every
195
+ // Coder outcome is governed with the `unchanged` repository disposition.
196
+ // The pull request number and URL are judge-authored semantic fields; the
197
+ // Coder's report on a refused publication is carried verbatim.
198
+ outcomeAuthority: {
199
+ governedPlayerStates: {
200
+ openPullRequest: {
201
+ opened: {
202
+ fields: { pullRequest: 'semantic', pullRequestUrl: 'semantic' },
203
+ repositoryDisposition: 'unchanged',
204
+ },
205
+ notPublished: {
206
+ fields: { coderOutput: 'presentation' },
207
+ repositoryDisposition: 'unchanged',
208
+ },
209
+ needsBossReply: {
210
+ fields: { question: 'presentation' },
211
+ repositoryDisposition: 'unchanged',
212
+ },
213
+ },
214
+ },
215
+ },
216
+ composePlayerPrompt: (input: PlaybookPlayerInput, _identity, resuming) =>
217
+ composePlayerPrompt(input as PlayerInput, resuming),
218
+ verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
219
+ controlContextFields: [],
220
+ unfinishedFinalStateIds: UNFINISHED_FINAL_STATE_IDS,
221
+ transitionEventFields: ['callerInput', 'answer', 'questionId'],
222
+ } satisfies XStatePlaybookRuntimeSpecV3<PrPlaybookOptions>;
223
+
224
+ const createPlaybookRuntime: XStatePlaybookRuntimeFactory<
225
+ XStatePlaybookRuntimeConstruction<
226
+ PrPlaybookOptions,
227
+ PrPlaybookHostCapabilities
228
+ >,
229
+ 3
230
+ > = createXStatePlaybookRuntime<
231
+ PrPlaybookOptions,
232
+ PrPlaybookHostCapabilities
233
+ >(prMachine, runtimeSpec);
234
+
235
+ export default createPlaybookRuntime;
@@ -0,0 +1,40 @@
1
+ import { type PrPlaybookHostCapabilities, type PlaybookRuntime } from './pr.playbook.js';
2
+ export interface PlaybookSummaryPolicy {
3
+ stateCountLabels: Readonly<Record<string, string>>;
4
+ copyPasteGuardNames: readonly string[];
5
+ savedCountsLine(counts: {
6
+ interruptions: number;
7
+ copyPastes: number;
8
+ }, rounds: number): string;
9
+ }
10
+ export type PrOptions = Readonly<Record<string, never>>;
11
+ export interface PrPlaybookRegistryEntry {
12
+ id: 'pr';
13
+ command: 'pr';
14
+ intent: string;
15
+ artifactSchema: 3;
16
+ runtimeProfile: {
17
+ readonly kind: 'shared-factory';
18
+ readonly compat: {
19
+ readonly artifactSchema: 3;
20
+ readonly runtimeAbi: number;
21
+ };
22
+ };
23
+ requiredRoleIds: readonly ['coder'];
24
+ concurrentRoleSets: readonly [];
25
+ summaryPolicy: PlaybookSummaryPolicy;
26
+ validateOptions(optionSlice: unknown): PrOptions;
27
+ createRuntime(options: PrOptions, hostCapabilities: PrPlaybookHostCapabilities): PlaybookRuntime;
28
+ }
29
+ export declare const prStateCountLabels: {
30
+ readonly openPullRequest: "publication round";
31
+ };
32
+ export declare const prCopyPasteGuardNames: readonly ["opened"];
33
+ export declare function prSavedCountsLine(counts: {
34
+ interruptions: number;
35
+ copyPastes: number;
36
+ }, rounds: number): string;
37
+ export declare const prSummaryPolicy: PlaybookSummaryPolicy;
38
+ export declare function validatePrOptions(optionSlice: unknown): PrOptions;
39
+ export declare const prPlaybookRegistryEntry: PrPlaybookRegistryEntry;
40
+ export default prPlaybookRegistryEntry;
@@ -0,0 +1,68 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+ import createPlaybookRuntime from './pr.playbook.js';
4
+ // CODE owns and labels the rounds of the one fix it may make. PR's suspended
5
+ // call state only delegates to that child and must not double-count its
6
+ // rounds; its script states run no agent at all. Only the Coder publication
7
+ // round is PR's own.
8
+ export const prStateCountLabels = {
9
+ openPullRequest: 'publication round',
10
+ };
11
+ export const prCopyPasteGuardNames = ['opened'];
12
+ function countNoun(count, singular, plural = `${singular}s`) {
13
+ return `${count} ${count === 1 ? singular : plural}`;
14
+ }
15
+ export function prSavedCountsLine(counts, rounds) {
16
+ return [
17
+ 'Saved you',
18
+ countNoun(counts.interruptions, 'interruption'),
19
+ 'and',
20
+ countNoun(counts.copyPastes, 'copy-paste'),
21
+ 'across',
22
+ countNoun(rounds, 'round'),
23
+ 'of delivery.',
24
+ ].join(' ');
25
+ }
26
+ export const prSummaryPolicy = {
27
+ stateCountLabels: prStateCountLabels,
28
+ copyPasteGuardNames: prCopyPasteGuardNames,
29
+ savedCountsLine: prSavedCountsLine,
30
+ };
31
+ export function validatePrOptions(optionSlice) {
32
+ if (optionSlice === undefined)
33
+ return Object.freeze({});
34
+ if (optionSlice === null ||
35
+ typeof optionSlice !== 'object' ||
36
+ Array.isArray(optionSlice)) {
37
+ throw new Error('captain.options.playbooks.pr.options must be an object');
38
+ }
39
+ const keys = Object.keys(optionSlice);
40
+ if (keys.length > 0) {
41
+ throw new Error(`Unknown config field captain.options.playbooks.pr.options.${keys[0]}`);
42
+ }
43
+ return Object.freeze({});
44
+ }
45
+ export const prPlaybookRegistryEntry = {
46
+ id: 'pr',
47
+ command: 'pr',
48
+ intent: 'publish a reviewed branch as a pull request, wait for its checks, fix red checks once through code, and merge it',
49
+ artifactSchema: 3,
50
+ runtimeProfile: Object.freeze({
51
+ kind: 'shared-factory',
52
+ compat: createPlaybookRuntime.compat,
53
+ }),
54
+ requiredRoleIds: ['coder'],
55
+ concurrentRoleSets: [],
56
+ summaryPolicy: prSummaryPolicy,
57
+ validateOptions: validatePrOptions,
58
+ createRuntime(options, hostCapabilities) {
59
+ // The five script states run `gh` and `git` in the repository the host
60
+ // governs: the linked `cwd` option is composed here from the host's
61
+ // authority working directory (DR-040 §2), never from configuration.
62
+ return createPlaybookRuntime({
63
+ configuredOptions: { ...options, cwd: hostCapabilities.authority.cwd },
64
+ hostCapabilities,
65
+ });
66
+ },
67
+ };
68
+ export default prPlaybookRegistryEntry;