@the-open-engine/zeroshot 6.39.0 → 6.39.1

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 CHANGED
@@ -106,6 +106,7 @@ zeroshot run <input> # issue, URL, markdown file, or inline text
106
106
  zeroshot run 123 --docker # container isolation
107
107
  zeroshot run 123 --pr # worktree + pull request
108
108
  zeroshot run 123 --ship # worktree + PR + merge after approval
109
+ zeroshot run 123 --pr --pr-body $'## Summary\n\nCustom text\n\n{{issue_reference}}'
109
110
  zeroshot run 123 -d # background run
110
111
 
111
112
  zeroshot list # tasks and clusters (--json)
@@ -120,6 +121,11 @@ zeroshot settings # effective settings
120
121
  zeroshot agents list # available agents
121
122
  ```
122
123
 
124
+ `--pr-body` supplies a deterministic pull-request body for `--pr` and `--ship` runs. The
125
+ template supports `{{issue_number}}`, `{{issue_title}}`, and `{{issue_reference}}`; all three
126
+ expand to empty text for tasks without an issue, so manual runs never emit `Closes #unknown`.
127
+ The unrendered template is retained for detached and resumed runs.
128
+
123
129
  </details>
124
130
 
125
131
  <details>
package/cli/index.js CHANGED
@@ -107,6 +107,8 @@ const {
107
107
  runLegacyUpdateIfRequested,
108
108
  } = require('./lib/update-checker');
109
109
  const { checkBinDirOnPath, printPathWarning } = require('../lib/path-check');
110
+ const { quoteShellArgument } = require('../lib/git-remote-utils');
111
+ const { renderPullRequestBody, resolveIssueContext } = require('../src/pr-body-template');
110
112
  const { StatusFooter, AGENT_STATE, ACTIVE_STATES } = require('../src/status-footer');
111
113
  const { EVENT_COPY, formatMergeStatus } = require('./event-copy');
112
114
 
@@ -2012,9 +2014,25 @@ function buildContextSummary({
2012
2014
  return contextSummary;
2013
2015
  }
2014
2016
 
2015
- function buildCompletionPrompt({ contextSummary, taskText, issueNumber, issueTitle }) {
2016
- const mergeGoal = 'CREATE PR AND MERGE IT';
2017
- const mergeStep = `
2017
+ function buildDefaultFinishPrBody({ taskText, issueNumber, issueTitle }) {
2018
+ const issueReference = renderPullRequestBody(undefined, { issueNumber, issueTitle });
2019
+ return [
2020
+ issueReference,
2021
+ issueReference ? '' : null,
2022
+ '## Summary',
2023
+ `${String(taskText || 'Unknown task').slice(0, 200)}...`,
2024
+ '',
2025
+ '## Changes',
2026
+ '- Implementation complete',
2027
+ '- All validations addressed',
2028
+ '',
2029
+ '🤖 Generated with zeroshot finish',
2030
+ ]
2031
+ .filter((line) => line !== null)
2032
+ .join('\n');
2033
+ }
2034
+
2035
+ const FINISH_MERGE_STEP = `
2018
2036
  8. MERGE THE PR - THIS IS MANDATORY:
2019
2037
  \`\`\`bash
2020
2038
  gh pr merge --merge --auto
@@ -2029,7 +2047,29 @@ function buildCompletionPrompt({ contextSummary, taskText, issueNumber, issueTit
2029
2047
 
2030
2048
  REPEAT UNTIL MERGED. DO NOT GIVE UP.`;
2031
2049
 
2032
- return `# YOUR MISSION: ${mergeGoal}
2050
+ function buildFinishCommandArguments({ taskText, issueNumber, issueTitle, prBody }) {
2051
+ const issueContext = resolveIssueContext({ issueNumber, issueTitle });
2052
+ const resolvedTitle = issueContext.issueTitle;
2053
+ const resolvedBody =
2054
+ typeof prBody === 'string'
2055
+ ? renderPullRequestBody(prBody, { issueNumber, issueTitle: resolvedTitle })
2056
+ : buildDefaultFinishPrBody({ taskText, issueNumber, issueTitle: resolvedTitle });
2057
+ return {
2058
+ commitMessage: quoteShellArgument(resolvedTitle || 'feat: implement task'),
2059
+ branch: quoteShellArgument(
2060
+ issueContext.issueNumber !== 'unknown'
2061
+ ? `issue-${issueContext.issueNumber}`
2062
+ : 'feature/implementation'
2063
+ ),
2064
+ prTitle: quoteShellArgument(resolvedTitle),
2065
+ prBody: quoteShellArgument(resolvedBody),
2066
+ };
2067
+ }
2068
+
2069
+ function buildCompletionPrompt({ contextSummary, taskText, issueNumber, issueTitle, prBody }) {
2070
+ const args = buildFinishCommandArguments({ taskText, issueNumber, issueTitle, prBody });
2071
+
2072
+ return `# YOUR MISSION: CREATE PR AND MERGE IT
2033
2073
 
2034
2074
  ${contextSummary}
2035
2075
 
@@ -2050,12 +2090,12 @@ You are the FINISHER. Your ONLY job is to take this cluster's work and push it a
2050
2090
  2. COMMIT ALL CHANGES - Stage and commit everything:
2051
2091
  \`\`\`bash
2052
2092
  git add .
2053
- git commit -m "${issueTitle || 'feat: implement task'}"
2093
+ git commit -m ${args.commitMessage}
2054
2094
  \`\`\`
2055
2095
 
2056
2096
  3. CREATE BRANCH - Use issue number if available:
2057
2097
  \`\`\`bash
2058
- ${issueNumber ? `git checkout -b issue-${issueNumber}` : 'git checkout -b feature/implementation'}
2098
+ git checkout -b ${args.branch}
2059
2099
  \`\`\`
2060
2100
 
2061
2101
  4. PUSH TO REMOTE:
@@ -2065,16 +2105,7 @@ You are the FINISHER. Your ONLY job is to take this cluster's work and push it a
2065
2105
 
2066
2106
  5. CREATE PULL REQUEST:
2067
2107
  \`\`\`bash
2068
- gh pr create --title "${issueTitle || 'Implementation'}" --body "Closes #${issueNumber || 'N/A'}
2069
-
2070
- ## Summary
2071
- ${taskText.slice(0, 200)}...
2072
-
2073
- ## Changes
2074
- - Implementation complete
2075
- - All validations addressed
2076
-
2077
- 🤖 Generated with zeroshot finish"
2108
+ gh pr create --title ${args.prTitle} --body ${args.prBody}
2078
2109
  \`\`\`
2079
2110
 
2080
2111
  6. GET PR URL:
@@ -2083,7 +2114,7 @@ ${taskText.slice(0, 200)}...
2083
2114
  \`\`\`
2084
2115
 
2085
2116
  7. OUTPUT THE PR URL - Print it clearly so user can see it
2086
- ${mergeStep}
2117
+ ${FINISH_MERGE_STEP}
2087
2118
 
2088
2119
  ## RULES
2089
2120
 
@@ -2707,6 +2738,10 @@ program
2707
2738
  'Full automation: worktree isolation + PR + auto-merge (use --docker for Docker)'
2708
2739
  )
2709
2740
  .option('--pr-base <branch>', 'Target branch for PRs (default: repo default branch)')
2741
+ .option(
2742
+ '--pr-body <template>',
2743
+ 'PR body template; supports {{issue_number}}, {{issue_title}}, and {{issue_reference}}'
2744
+ )
2710
2745
  .option('--merge-queue', 'Use GitHub merge queue instead of direct merge')
2711
2746
  .option(
2712
2747
  '--close-issue <mode>',
@@ -3714,6 +3749,10 @@ program
3714
3749
  .helpGroup('Control:')
3715
3750
  .description('Take existing cluster and create completion-focused task (creates PR and merges)')
3716
3751
  .option('-y, --yes', 'Skip confirmation if cluster is running')
3752
+ .option(
3753
+ '--pr-body <template>',
3754
+ 'PR body template; supports {{issue_number}}, {{issue_title}}, and {{issue_reference}}'
3755
+ )
3717
3756
  .action(async (id, options) => {
3718
3757
  try {
3719
3758
  const orchestrator = await getOrchestrator();
@@ -3732,6 +3771,7 @@ program
3732
3771
  taskText: context.taskText,
3733
3772
  issueNumber: context.issueNumber,
3734
3773
  issueTitle: context.issueTitle,
3774
+ prBody: options.prBody ?? cluster.prOptions?.prBody,
3735
3775
  });
3736
3776
  printCompletionPromptPreview(completionPrompt);
3737
3777
 
@@ -6190,6 +6230,9 @@ module.exports = {
6190
6230
  isStartupUpdateEligible,
6191
6231
  handleNoArgumentInvocation,
6192
6232
  shouldRunInitialSetup,
6233
+ extractFinishContext,
6234
+ buildDefaultFinishPrBody,
6235
+ buildCompletionPrompt,
6193
6236
  resolveRunMode,
6194
6237
  killRunningClusters,
6195
6238
  };
@@ -5,6 +5,7 @@ interface RunOptions extends Record<string, unknown> {
5
5
  mergeQueue?: unknown;
6
6
  pr?: unknown;
7
7
  prBase?: unknown;
8
+ prBody?: unknown;
8
9
  ship?: unknown;
9
10
  worktree?: unknown;
10
11
  }
@@ -117,9 +117,10 @@ async function registerDetachedSetupCluster({ clusterId, pid, storageDir, logPat
117
117
  setupStartedAt: Date.now(),
118
118
  setupStage: 'starting',
119
119
  autoPr: plan.delivery !== 'none',
120
- prOptions: runOptions.prBase
120
+ prOptions: plan.delivery !== 'none'
121
121
  ? {
122
122
  prBase: runOptions.prBase,
123
+ prBody: typeof runOptions.prBody === 'string' ? runOptions.prBody : null,
123
124
  mergeQueue: runOptions.mergeQueue || false,
124
125
  closeIssue: runOptions.closeIssue || null,
125
126
  autoMerge: plan.autoMerge,
@@ -7,6 +7,7 @@ interface RunOptions extends Record<string, unknown> {
7
7
  mount?: readonly string[] | null;
8
8
  noIsolation?: unknown;
9
9
  prBase?: unknown;
10
+ prBody?: unknown;
10
11
  }
11
12
  interface MountSpec {
12
13
  host: string;
@@ -25,6 +25,7 @@ interface RunOptions extends Record<string, unknown> {
25
25
  noMounts?: unknown;
26
26
  pr?: unknown;
27
27
  prBase?: unknown;
28
+ prBody?: unknown;
28
29
  preparedWorktree?: unknown;
29
30
  requiredQualityGates?: unknown;
30
31
  ship?: unknown;
@@ -89,6 +89,7 @@ function buildStartOptionsFromPlan({ clusterId, plan, options, settings, provide
89
89
  containerHome: optionalValue(options.containerHome),
90
90
  forceProvider: optionalValue(forceProvider),
91
91
  prBase: environment ? resolvePrBase(options) : optionalValue(options.prBase),
92
+ prBody: typeof options.prBody === 'string' ? options.prBody : undefined,
92
93
  mergeQueue: environment ? resolveMergeQueue(options) : optionalValue(options.mergeQueue),
93
94
  closeIssue: environment ? resolveCloseIssue(options) : optionalValue(options.closeIssue),
94
95
  ship: plan.delivery === 'ship',
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@the-open-engine/zeroshot",
3
- "version": "6.39.0",
3
+ "version": "6.39.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@the-open-engine/zeroshot",
9
- "version": "6.39.0",
9
+ "version": "6.39.1",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -17,6 +17,7 @@
17
17
  "bun": "1.3.14",
18
18
  "chalk": "^4.1.2",
19
19
  "commander": "^14.0.2",
20
+ "js-yaml": "^4.3.1",
20
21
  "node-pty": "^1.1.0",
21
22
  "omelette": "^0.4.17",
22
23
  "open": "^10.1.0",
@@ -43,7 +44,6 @@
43
44
  "eslint-plugin-sonarjs": "^3.0.5",
44
45
  "eslint-plugin-unused-imports": "^4.3.0",
45
46
  "husky": "^9.1.7",
46
- "js-yaml": "^4.2.0",
47
47
  "jscpd": "^3.5.10",
48
48
  "lint-staged": "^16.2.7",
49
49
  "mocha": "^11.7.5",
@@ -5155,7 +5155,6 @@
5155
5155
  "version": "2.0.1",
5156
5156
  "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
5157
5157
  "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
5158
- "dev": true,
5159
5158
  "license": "Python-2.0"
5160
5159
  },
5161
5160
  "node_modules/argv-formatter": {
@@ -9177,10 +9176,9 @@
9177
9176
  "license": "MIT"
9178
9177
  },
9179
9178
  "node_modules/js-yaml": {
9180
- "version": "4.2.0",
9181
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
9182
- "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
9183
- "dev": true,
9179
+ "version": "4.3.1",
9180
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
9181
+ "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
9184
9182
  "funding": [
9185
9183
  {
9186
9184
  "type": "github",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-open-engine/zeroshot",
3
- "version": "6.39.0",
3
+ "version": "6.39.1",
4
4
  "description": "Independent executor–verifier orchestration for software changes.",
5
5
  "main": "src/orchestrator.js",
6
6
  "bin": {
@@ -204,6 +204,7 @@
204
204
  "bun": "1.3.14",
205
205
  "chalk": "^4.1.2",
206
206
  "commander": "^14.0.2",
207
+ "js-yaml": "^4.3.1",
207
208
  "node-pty": "^1.1.0",
208
209
  "omelette": "^0.4.17",
209
210
  "open": "^10.1.0",
@@ -229,7 +230,6 @@
229
230
  "eslint-plugin-sonarjs": "^3.0.5",
230
231
  "eslint-plugin-unused-imports": "^4.3.0",
231
232
  "husky": "^9.1.7",
232
- "js-yaml": "^4.2.0",
233
233
  "jscpd": "^3.5.10",
234
234
  "lint-staged": "^16.2.7",
235
235
  "mocha": "^11.7.5",
@@ -244,7 +244,7 @@
244
244
  },
245
245
  "overrides": {
246
246
  "cosmiconfig": {
247
- "js-yaml": "4.2.0"
247
+ "js-yaml": "4.3.1"
248
248
  },
249
249
  "adm-zip": "0.6.0",
250
250
  "sharp": "0.35.3",
@@ -325,10 +325,7 @@ function stagedLockDependencies(cargoLock, workspaceCargoToml) {
325
325
 
326
326
  function stageCargoLock(cargoLock, version, workspaceCargoToml) {
327
327
  const targetPackage = workspaceLockPackage(cargoLock);
328
- let stagedPackage = targetPackage.text.replace(
329
- /^(version = ")[^"]+(")$/m,
330
- `$1${version}$2`
331
- );
328
+ let stagedPackage = targetPackage.text.replace(/^(version = ")[^"]+(")$/m, `$1${version}$2`);
332
329
  for (const dependency of stagedLockDependencies(cargoLock, workspaceCargoToml)) {
333
330
  const dependencyPattern = new RegExp(
334
331
  `^(\\s*")${escapeRegExp(dependency.name)}(?: [^"]+)?(",\\r?)$`,
@@ -339,10 +336,7 @@ function stageCargoLock(cargoLock, version, workspaceCargoToml) {
339
336
  `RUST_VERSION_STAGE_FAILED: Cargo.lock zeroshot-rust entry has no ${dependency.name} dependency`
340
337
  );
341
338
  }
342
- stagedPackage = stagedPackage.replace(
343
- dependencyPattern,
344
- `$1${dependency.reference}$2`
345
- );
339
+ stagedPackage = stagedPackage.replace(dependencyPattern, `$1${dependency.reference}$2`);
346
340
  }
347
341
  return (
348
342
  cargoLock.slice(0, targetPackage.start) +
@@ -359,10 +353,7 @@ function verifyStagedCargoLock(cargoLock, version, workspaceCargoToml) {
359
353
  );
360
354
  }
361
355
  for (const dependency of stagedLockDependencies(cargoLock, workspaceCargoToml)) {
362
- const dependencyPattern = new RegExp(
363
- `^\\s*"${escapeRegExp(dependency.reference)}",\\r?$`,
364
- 'm'
365
- );
356
+ const dependencyPattern = new RegExp(`^\\s*"${escapeRegExp(dependency.reference)}",\\r?$`, 'm');
366
357
  if (!dependencyPattern.test(targetPackage.text)) {
367
358
  throw new Error(
368
359
  `${VERSION_ERROR}: Cargo.lock zeroshot-rust dependency ${dependency.name} is not coupled to ${dependency.version}`
@@ -400,8 +391,7 @@ function stageVersion(
400
391
  function checkVersionCoupling(tag, cargoToml, cargoLock, workspaceCargoToml) {
401
392
  const useRepositoryFiles = cargoToml === undefined;
402
393
  const manifest =
403
- cargoToml ??
404
- fs.readFileSync(path.join(repositoryRoot, 'zeroshot-rust', 'Cargo.toml'), 'utf8');
394
+ cargoToml ?? fs.readFileSync(path.join(repositoryRoot, 'zeroshot-rust', 'Cargo.toml'), 'utf8');
405
395
  const releaseVersion = normalizeVersion(tag);
406
396
  const manifestVersion = cargoVersion(manifest);
407
397
  if (releaseVersion !== manifestVersion) {
@@ -416,8 +406,7 @@ function checkVersionCoupling(tag, cargoToml, cargoLock, workspaceCargoToml) {
416
406
  : undefined);
417
407
  if (lock !== undefined) {
418
408
  const workspace =
419
- workspaceCargoToml ??
420
- fs.readFileSync(path.join(repositoryRoot, 'Cargo.toml'), 'utf8');
409
+ workspaceCargoToml ?? fs.readFileSync(path.join(repositoryRoot, 'Cargo.toml'), 'utf8');
421
410
  verifyStagedCargoLock(lock, releaseVersion, workspace);
422
411
  }
423
412
  return releaseVersion;
@@ -483,9 +472,7 @@ const SCRIPT_INSTALL_CONTRACTS = Object.freeze([
483
472
  ]);
484
473
 
485
474
  function invokesRustDistribution(step) {
486
- return (
487
- typeof step.run === 'string' && RUST_DISTRIBUTION_INVOCATION.test(step.run)
488
- );
475
+ return typeof step.run === 'string' && RUST_DISTRIBUTION_INVOCATION.test(step.run);
489
476
  }
490
477
 
491
478
  function checkScriptInstall(job, { jobName, installName, command, checkoutRef }) {
@@ -507,9 +494,7 @@ function checkScriptInstall(job, { jobName, installName, command, checkoutRef })
507
494
  checkout.with.repository !== '${{ github.repository }}') ||
508
495
  checkout.with?.ref !== checkoutRef
509
496
  ) {
510
- failIntegrity(
511
- `${jobName} must checkout expected current repository source at workspace root`
512
- );
497
+ failIntegrity(`${jobName} must checkout expected current repository source at workspace root`);
513
498
  }
514
499
  const installIndex = job.steps.indexOf(install);
515
500
  const checkoutIndex = job.steps.indexOf(checkout);
@@ -745,11 +730,12 @@ function hasValidSri(integrity) {
745
730
  }
746
731
 
747
732
  function checkScriptDependencies(packageManifest, packageLock) {
748
- const directSpec = packageManifest.devDependencies?.['js-yaml'];
733
+ // js-yaml is a runtime dependency: lib/compose-utils.js requires it from the published package.
734
+ const directSpec = packageManifest.dependencies?.['js-yaml'];
749
735
  if (typeof directSpec !== 'string' || directSpec.length === 0) {
750
- failIntegrity('rust-distribution.js requires a direct js-yaml devDependency');
736
+ failIntegrity('rust-distribution.js requires a direct js-yaml dependency');
751
737
  }
752
- const lockSpec = packageLock.packages?.['']?.devDependencies?.['js-yaml'];
738
+ const lockSpec = packageLock.packages?.['']?.dependencies?.['js-yaml'];
753
739
  if (lockSpec !== directSpec) {
754
740
  failIntegrity('package-lock root js-yaml spec must match package.json');
755
741
  }
@@ -773,12 +759,8 @@ function checkRepository(
773
759
  shimTargets = JSON.parse(
774
760
  fs.readFileSync(path.join(repositoryRoot, 'npm', 'zeroshot-rust', 'targets.json'), 'utf8')
775
761
  ),
776
- packageManifest = JSON.parse(
777
- fs.readFileSync(path.join(repositoryRoot, 'package.json'), 'utf8')
778
- ),
779
- packageLock = JSON.parse(
780
- fs.readFileSync(path.join(repositoryRoot, 'package-lock.json'), 'utf8')
781
- )
762
+ packageManifest = JSON.parse(fs.readFileSync(path.join(repositoryRoot, 'package.json'), 'utf8')),
763
+ packageLock = JSON.parse(fs.readFileSync(path.join(repositoryRoot, 'package-lock.json'), 'utf8'))
782
764
  ) {
783
765
  let document;
784
766
  try {
@@ -277,6 +277,7 @@ return hasSufficientEvidence;`;
277
277
  const { readRepoSettings } = require('../../lib/repo-settings');
278
278
  const { normalizeGitRemoteName, quoteShellArgument } = require('../../lib/git-remote-utils');
279
279
  const { resolveRequiredQualityGates } = require('../quality-gates');
280
+ const { renderPullRequestBody, resolveIssueContext } = require('../pr-body-template');
280
281
 
281
282
  function getSafeBranchName(value) {
282
283
  if (typeof value !== 'string') {
@@ -314,35 +315,6 @@ function normalizeCloseIssueMode(value) {
314
315
  return null;
315
316
  }
316
317
 
317
- function normalizeIssueNumber(value) {
318
- const candidate = typeof value === 'number' ? String(value) : value;
319
- if (typeof candidate !== 'string') return 'unknown';
320
- const trimmed = candidate.trim();
321
- return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(trimmed) ? trimmed : 'unknown';
322
- }
323
-
324
- function normalizeIssueTitle(value) {
325
- if (typeof value !== 'string' || value.trim() === '') return 'Implementation';
326
- const normalized = [...value]
327
- .map((character) => {
328
- const codePoint = character.codePointAt(0);
329
- return codePoint < 0x20 || codePoint === 0x7f ? ' ' : character;
330
- })
331
- .join('')
332
- .trim();
333
- return normalized || 'Implementation';
334
- }
335
-
336
- function resolveIssueContext(options) {
337
- const issueNumber = normalizeIssueNumber(options.issueNumber);
338
- const issueTitle = normalizeIssueTitle(options.issueTitle);
339
- const issueReference =
340
- options.includeIssueReference === false || issueNumber === 'unknown'
341
- ? ''
342
- : `Closes #${issueNumber}`;
343
- return { issueNumber, issueTitle, issueReference };
344
- }
345
-
346
318
  /**
347
319
  * Resolve GitHub configuration from CLI options and repo settings.
348
320
  * Priority: CLI options > repo settings (.zeroshot/settings.json) > defaults
@@ -355,6 +327,7 @@ function resolveIssueContext(options) {
355
327
  * @param {string|number} [options.issueNumber] - Typed issue identifier for prompt commands
356
328
  * @param {string} [options.issueTitle] - Typed issue title for prompt commands
357
329
  * @param {boolean} [options.includeIssueReference] - Include the closing reference in PR text
330
+ * @param {string} [options.prBody] - Literal PR body template with supported issue tokens
358
331
  * @returns {Object} Resolved configuration
359
332
  */
360
333
  function resolveGitHubConfig(options = {}) {
@@ -385,16 +358,23 @@ function resolveGitHubConfig(options = {}) {
385
358
  options.autoMerge === true ||
386
359
  (options.autoMerge !== false && parseBool(repoGithub.autoMerge) === true);
387
360
 
361
+ const issueContext = resolveIssueContext(options);
362
+
388
363
  return {
389
364
  prBase,
390
365
  useMergeQueue,
391
366
  closeIssueMode,
392
367
  autoMerge,
393
368
  gitRemote,
394
- issueContext: resolveIssueContext(options),
369
+ issueContext,
370
+ prBody: renderPullRequestBody(options.prBody, options),
395
371
  };
396
372
  }
397
373
 
374
+ function resolvedPrBody(config, issueContext) {
375
+ return typeof config.prBody === 'string' ? config.prBody : issueContext.issueReference;
376
+ }
377
+
398
378
  /**
399
379
  * Generate platform-specific configuration based on resolved GitHub config.
400
380
  *
@@ -406,13 +386,15 @@ function getPlatformConfig(platform, config = {}) {
406
386
  const { prBase, useMergeQueue, closeIssueMode, autoMerge, gitRemote } = config;
407
387
  const issueContext = config.issueContext || resolveIssueContext({});
408
388
  const issueTitleArgument = quoteShellArgument(`feat: ${issueContext.issueTitle}`);
409
- const issueReferenceArgument = quoteShellArgument(issueContext.issueReference);
389
+ const prBodyArgument = quoteShellArgument(resolvedPrBody(config, issueContext));
410
390
 
411
391
  const PLATFORM_CONFIGS = {
412
392
  github: {
413
393
  prName: 'PR',
414
394
  prNameLower: 'pull request',
415
- createCmd: `gh pr create${prBase ? ` --base ${prBase}` : ''} --title ${issueTitleArgument} --body ${issueReferenceArgument}`,
395
+ createCmd:
396
+ `gh pr create${prBase ? ` --base ${prBase}` : ''} ` +
397
+ `--title ${issueTitleArgument} --body ${prBodyArgument}`,
416
398
  mergeCmd: useMergeQueue
417
399
  ? `PR_ID="$(timeout 30 gh pr view --json id --jq .id)"
418
400
  gh api graphql -f query='mutation($id:ID!){enqueuePullRequest(input:{pullRequestId:$id}){mergeQueueEntry{state}}}' -f id="$PR_ID"
@@ -434,7 +416,7 @@ for i in $(seq 1 90); do if timeout 30 gh pr view --json mergedAt --jq .mergedAt
434
416
  gitlab: {
435
417
  prName: 'MR',
436
418
  prNameLower: 'merge request',
437
- createCmd: `glab mr create --title ${issueTitleArgument} --description ${issueReferenceArgument}`,
419
+ createCmd: `glab mr create --title ${issueTitleArgument} --description ${prBodyArgument}`,
438
420
  mergeCmd: 'glab mr merge --auto-merge',
439
421
  mergeFallbackCmd: 'glab mr merge',
440
422
  prUrlExample: 'https://gitlab.com/owner/repo/-/merge_requests/123',
@@ -447,7 +429,7 @@ for i in $(seq 1 90); do if timeout 30 gh pr view --json mergedAt --jq .mergedAt
447
429
  'azure-devops': {
448
430
  prName: 'PR',
449
431
  prNameLower: 'pull request',
450
- createCmd: `az repos pr create --title ${issueTitleArgument} --description ${issueReferenceArgument}`,
432
+ createCmd: `az repos pr create --title ${issueTitleArgument} --description ${prBodyArgument}`,
451
433
  mergeCmd: 'az repos pr update --id <PR_ID> --auto-complete true',
452
434
  mergeFallbackCmd: 'az repos pr update --id <PR_ID> --status completed',
453
435
  prUrlExample: 'https://dev.azure.com/org/project/_git/repo/pullrequest/123',
@@ -803,6 +785,7 @@ If blocked before creating a ${prName}, output:
803
785
  * @param {string|number} [options.issueNumber] - Typed issue identifier for prompt commands
804
786
  * @param {string} [options.issueTitle] - Typed issue title for prompt commands
805
787
  * @param {boolean} [options.includeIssueReference] - Include the closing reference in PR text
788
+ * @param {string} [options.prBody] - Literal PR body template with supported issue tokens
806
789
  * @param {Array} [options.requiredQualityGates] - Required handoff quality gates
807
790
  * @param {boolean} [options.autoMerge] - Merge the PR (--ship). False stops after PR creation (--pr).
808
791
  * @returns {Object} Agent configuration object