Feat(ship): add configurable PR body sections (#3391)

* feat: add configurable ship PR body sections

* chore: add changeset for ship PR sections

* docs: avoid prompt scanner trigger in PR body guide

* docs: escape pr body source separator
This commit is contained in:
Tom Boucher
2026-05-11 15:27:40 -04:00
committed by GitHub
parent fd20373cf4
commit e79c472d7b
17 changed files with 732 additions and 7 deletions

View File

@@ -43,6 +43,7 @@ const VALID_CONFIG_KEYS = new Set([
'workflow.security_block_on',
'workflow.drift_threshold',
'workflow.drift_action',
'ship.pr_body_sections',
'git.branching_strategy', 'git.base_branch', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
'planning.commit_docs', 'planning.search_gitignored', 'planning.sub_repos',
'review.ollama_host', 'review.lm_studio_host', 'review.llama_cpp_host',

View File

@@ -30,6 +30,16 @@ const CONFIG_KEY_SUGGESTIONS = {
'plan_checker': 'workflow.plan_check',
};
const SHIP_PR_BODY_SECTION_KEYS = new Set(['heading', 'enabled', 'source', 'fallback', 'template']);
const SHIP_PR_BODY_TEMPLATE_TOKENS = new Set([
'phase_number',
'phase_name',
'phase_dir',
'base_branch',
'padded_phase',
]);
const SHIP_PR_BODY_SOURCE_RE = /^(ROADMAP|PLAN|SUMMARY|VERIFICATION|STATE|REQUIREMENTS|CONTEXT)\.md\s+##\s+[^\r\n#][^\r\n]*$/;
function validateKnownConfigKeyPath(keyPath) {
const suggested = CONFIG_KEY_SUGGESTIONS[keyPath];
if (suggested) {
@@ -37,6 +47,64 @@ function validateKnownConfigKeyPath(keyPath) {
}
}
function validateShipPrBodySections(value) {
if (!Array.isArray(value)) {
error('Invalid ship.pr_body_sections value. Expected a JSON array of section objects.');
}
value.forEach((section, index) => {
const prefix = `Invalid ship.pr_body_sections[${index}]`;
if (!section || typeof section !== 'object' || Array.isArray(section)) {
error(`${prefix}. Expected an object.`);
}
const unknownKeys = Object.keys(section).filter((key) => !SHIP_PR_BODY_SECTION_KEYS.has(key));
if (unknownKeys.length > 0) {
error(`${prefix}. Unknown field(s): ${unknownKeys.join(', ')}.`);
}
if (typeof section.heading !== 'string' || section.heading.trim() === '') {
error(`${prefix}. heading must be a non-empty string.`);
}
if (/[\r\n]/.test(section.heading)) {
error(`${prefix}. heading must be a single line.`);
}
if ('enabled' in section && typeof section.enabled !== 'boolean') {
error(`${prefix}. enabled must be true or false.`);
}
for (const field of ['source', 'fallback', 'template']) {
if (field in section && typeof section[field] !== 'string') {
error(`${prefix}. ${field} must be a string.`);
}
}
const hasContent = ['source', 'fallback', 'template'].some((field) => {
return typeof section[field] === 'string' && section[field].trim() !== '';
});
if (!hasContent) {
error(`${prefix}. Provide at least one of source, fallback, or template.`);
}
if (typeof section.source === 'string' && section.source.trim() !== '') {
const selectors = section.source.split('||').map((selector) => selector.trim()).filter(Boolean);
if (selectors.length === 0 || selectors.some((selector) => !SHIP_PR_BODY_SOURCE_RE.test(selector))) {
error(`${prefix}. source must use selectors like "PLAN.md ## Risks", separated with "||".`);
}
}
if (typeof section.template === 'string') {
const tokens = section.template.matchAll(/\{([a-zA-Z][a-zA-Z0-9_]*)\}/g);
for (const match of tokens) {
if (!SHIP_PR_BODY_TEMPLATE_TOKENS.has(match[1])) {
error(`${prefix}. Unsupported template token: {${match[1]}}.`);
}
}
}
});
}
/**
* Build a fully-materialized config object for a new project.
*
@@ -127,6 +195,9 @@ function buildNewProjectConfig(userChoices) {
security_asvs_level: CONFIG_DEFAULTS.security_asvs_level,
security_block_on: CONFIG_DEFAULTS.security_block_on,
},
ship: {
pr_body_sections: [],
},
hooks: {
context_warnings: true,
},
@@ -137,7 +208,7 @@ function buildNewProjectConfig(userChoices) {
};
// Three-level deep merge: hardcoded <- userDefaults <- choices
return {
const config = {
...hardcoded,
...userDefaults,
...choices,
@@ -151,6 +222,11 @@ function buildNewProjectConfig(userChoices) {
...(userDefaults.workflow || {}),
...(choices.workflow || {}),
},
ship: {
...hardcoded.ship,
...(userDefaults.ship || {}),
...(choices.ship || {}),
},
hooks: {
...hardcoded.hooks,
...(userDefaults.hooks || {}),
@@ -162,6 +238,9 @@ function buildNewProjectConfig(userChoices) {
...(choices.agent_skills || {}),
},
};
validateShipPrBodySections(config.ship.pr_body_sections);
return config;
}
/**
@@ -355,6 +434,10 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
}
}
if (keyPath === 'ship.pr_body_sections') {
validateShipPrBodySections(parsedValue);
}
// Human verification checkpoint mode (#3309)
const VALID_HUMAN_VERIFY_MODES = ['mid-flight', 'end-of-phase'];
if (keyPath === 'workflow.human_verify_mode' && !VALID_HUMAN_VERIFY_MODES.includes(String(parsedValue))) {

View File

@@ -270,6 +270,14 @@ Set via `workflow.*` namespace in config.json (e.g., `"workflow": { "research":
| `workflow.security_block_on` | string | `"high"` | `"high"`, `"medium"`, `"low"` | Minimum severity that blocks phase advancement |
| `workflow.post_planning_gaps` | boolean | `true` | `true`, `false` | Post-planning gap report (#2493). After plans are generated, scans REQUIREMENTS.md and CONTEXT.md `<decisions>` against all PLAN.md files and emits a unified `Source \| Item \| Status` table. Non-blocking. Set to `false` to skip Step 13e of plan-phase. _Alias:_ `post_planning_gaps` is the flat-key form used in `CONFIG_DEFAULTS`; `workflow.post_planning_gaps` is the canonical namespaced form. |
### Ship Fields
Set via `ship.*` namespace in config.json. These fields affect `/gsd-ship` PRD-style pull request body composition only.
| Key | Type | Default | Allowed Values | Description |
|-----|------|---------|----------------|-------------|
| `ship.pr_body_sections` | array | `[]` | Array of section objects | Append-only project-specific PR body sections. Each entry has `heading`, optional `enabled`, and one or more of `source`, `template`, or `fallback`. Disabled entries remain in onboarding config but do not render. Core sections remain required and cannot be removed or replaced. |
### Git Fields
Set via `git.*` namespace (e.g., `"git": { "branching_strategy": "phase" }`).

View File

@@ -20,6 +20,9 @@
"cross_ai_command": "",
"cross_ai_timeout": 300
},
"ship": {
"pr_body_sections": []
},
"planning": {
"commit_docs": true,
"search_gitignored": false,

View File

@@ -235,11 +235,35 @@ AskUserQuestion([
])
```
**Round 3 — PR body onboarding:**
Ask which optional PRD-style sections `/gsd-ship` should append to generated PR bodies. These map to `ship.pr_body_sections`; selected sections are written with `"enabled": true`, unselected seeded sections are written with `"enabled": false` so the project can enable them later without editing `ship.md`.
Prefer lean/agile PRD sections that make the delivered increment clear: user stories, acceptance criteria, Definition of Done or release criteria, risks, dependencies, and stakeholder review.
```
AskUserQuestion([
{
header: "PR Body",
question: "Which optional PRD-style sections should /gsd-ship include in PR bodies?",
multiSelect: true,
options: [
{ label: "User Stories & Acceptance Criteria", description: "Append user-facing stories and acceptance checks from REQUIREMENTS.md" },
{ label: "Risks & Dependencies", description: "Append rollout risks, dependencies, and rollback notes from PLAN.md" },
{ label: "Success Metrics & Release Criteria", description: "Append measurable Definition of Done and release checks for stakeholder review" },
{ label: "Stakeholder Review & Approval", description: "Append approval checklist for projects that need sign-off traceability" }
]
}
])
```
Build `ship.pr_body_sections` from those choices. For selected options, set `enabled: true`; for seeded but unselected options, set `enabled: false`. If the user selects none, use `"ship":{"pr_body_sections":[]}`.
Create `.planning/config.json` with all settings (CLI fills in remaining defaults automatically):
```bash
mkdir -p .planning
gsd-sdk query config-new-project '{"mode":"yolo","granularity":"[selected]","parallelization":true|false,"commit_docs":true|false,"model_profile":"quality|balanced|budget|inherit","workflow":{"research":true|false,"plan_check":true|false,"verifier":true|false,"nyquist_validation":true|false,"auto_advance":true}}'
gsd-sdk query config-new-project '{"mode":"yolo","granularity":"[selected]","parallelization":true|false,"commit_docs":true|false,"model_profile":"quality|balanced|budget|inherit","workflow":{"research":true|false,"plan_check":true|false,"verifier":true|false,"nyquist_validation":true|false,"auto_advance":true},"ship":{"pr_body_sections":[{"heading":"User Stories & Acceptance Criteria","enabled":true|false,"source":"REQUIREMENTS.md ## User Stories || REQUIREMENTS.md ## Acceptance Criteria","fallback":"- Acceptance criteria are covered by the linked requirements and verification evidence."},{"heading":"Risks & Dependencies","enabled":true|false,"source":"PLAN.md ## Risks || PLAN.md ## Dependencies","fallback":"- No known high-risk rollout dependencies."},{"heading":"Success Metrics & Release Criteria","enabled":true|false,"source":"REQUIREMENTS.md ## Definition of Done || VERIFICATION.md ## Release Criteria","fallback":"- Release when automated verification and required manual checks pass."},{"heading":"Stakeholder Review & Approval","enabled":true|false,"template":"- Product owner approval pending for {phase_name}."}]}}'
```
**If commit_docs = No:** Add `.planning/` to `.gitignore`.
@@ -657,11 +681,20 @@ questions: [
]
```
**PR body onboarding:** Ask which optional PRD-style sections `/gsd-ship` should append to generated PR bodies. Use the same `ship.pr_body_sections` mapping as Step 2a: selected sections get `enabled: true`, seeded-but-unselected sections get `enabled: false`, and selecting none writes an empty list. Prefer lean/agile PRD sections that make user value, acceptance criteria, Definition of Done, and stakeholder traceability explicit.
Recommended options:
- `User Stories & Acceptance Criteria`
- `Risks & Dependencies`
- `Success Metrics & Release Criteria`
- `Stakeholder Review & Approval`
Create `.planning/config.json` with all settings (CLI fills in remaining defaults automatically):
```bash
mkdir -p .planning
gsd-sdk query config-new-project '{"mode":"[yolo|interactive]","granularity":"[selected]","parallelization":true|false,"commit_docs":true|false,"model_profile":"quality|balanced|budget|inherit","workflow":{"research":true|false,"plan_check":true|false,"verifier":true|false,"nyquist_validation":[false if granularity=coarse, true otherwise]}}'
gsd-sdk query config-new-project '{"mode":"[yolo|interactive]","granularity":"[selected]","parallelization":true|false,"commit_docs":true|false,"model_profile":"quality|balanced|budget|inherit","workflow":{"research":true|false,"plan_check":true|false,"verifier":true|false,"nyquist_validation":[false if granularity=coarse, true otherwise]},"ship":{"pr_body_sections":[{"heading":"User Stories & Acceptance Criteria","enabled":true|false,"source":"REQUIREMENTS.md ## User Stories || REQUIREMENTS.md ## Acceptance Criteria","fallback":"- Acceptance criteria are covered by the linked requirements and verification evidence."},{"heading":"Risks & Dependencies","enabled":true|false,"source":"PLAN.md ## Risks || PLAN.md ## Dependencies","fallback":"- No known high-risk rollout dependencies."},{"heading":"Success Metrics & Release Criteria","enabled":true|false,"source":"REQUIREMENTS.md ## Definition of Done || VERIFICATION.md ## Release Criteria","fallback":"- Release when automated verification and required manual checks pass."},{"heading":"Stakeholder Review & Approval","enabled":true|false,"template":"- Product owner approval pending for {phase_name}."}]}}'
```
**Note:** Run `/gsd-settings` anytime to update model profile, workflow agents, branching strategy, and other preferences.

View File

@@ -141,16 +141,69 @@ For each SUMMARY.md in the phase directory:
{Decisions from STATE.md accumulated context relevant to this phase}
```
**7. Configured project sections:**
Read append-only project-specific PRD/PR body sections from config:
```bash
CUSTOM_PR_SECTIONS=$(gsd-sdk query config-get ship.pr_body_sections --default '[]' 2>/dev/null || echo '[]')
```
`ship.pr_body_sections` is an onboarding-time extension point for teams that need extra PRD-style sections such as `User Stories & Acceptance Criteria`, `Risks & Dependencies`, `Success Metrics`, `Release Criteria`, or `Stakeholder Review & Approval`.
Use these sections for lean/agile PRD material that should travel with the PR without making the core `/gsd-ship` body configurable:
- User stories and acceptance criteria that explain the functional increment from the user's point of view.
- Definition of Done or release criteria that make the completion standard explicit.
- Risks, dependencies, stakeholder review, and traceability notes needed by regulated or approval-heavy projects.
Rules:
- Treat configured sections as append-only. They are rendered after `Key Decisions` and cannot replace, remove, or reorder the required core sections: `Summary`, `Changes`, `Requirements Addressed`, `Verification`, and `Key Decisions`.
- Each entry must have `heading` plus at least one of `source`, `template`, or `fallback`.
- `enabled` defaults to `true`; when `enabled` is `false`, skip the section without warning. This lets onboarding seed optional sections that a project can enable later.
- `source` is a fallback chain of planning artifact headings: `PLAN.md ## Risks || VERIFICATION.md ## Manual Checks`. Allowed artifacts are `ROADMAP.md`, `PLAN.md`, `SUMMARY.md`, `VERIFICATION.md`, `STATE.md`, `REQUIREMENTS.md`, and `CONTEXT.md`.
- `template` is literal Markdown with a closed token namespace only: `{phase_number}`, `{phase_name}`, `{phase_dir}`, `{base_branch}`, `{padded_phase}`.
- `fallback` is literal Markdown used when `source` finds no content and no `template` is present.
- Omit sections whose final rendered body is empty after trimming.
Example configured sections:
```json
[
{
"heading": "User Stories & Acceptance Criteria",
"enabled": true,
"source": "REQUIREMENTS.md ## User Stories || REQUIREMENTS.md ## Acceptance Criteria",
"fallback": "- Acceptance criteria are covered by the linked requirements and verification evidence."
},
{
"heading": "Risks & Dependencies",
"enabled": true,
"source": "PLAN.md ## Risks || PLAN.md ## Dependencies",
"fallback": "- No known high-risk rollout dependencies."
},
{
"heading": "Stakeholder Review & Approval",
"enabled": false,
"template": "- Product owner approval pending for {phase_name}."
}
]
```
</step>
<step name="create_pr">
Create the PR using the generated body:
Create the PR using the generated body. Write the body to a temp file first so large generated PRD sections do not hit shell argument limits:
```bash
PR_BODY_FILE=$(mktemp "${TMPDIR:-/tmp}/gsd-pr-body.XXXXXX.md")
trap 'rm -f "${PR_BODY_FILE:-}"' EXIT
printf '%s\n' "${PR_BODY}" > "${PR_BODY_FILE}"
gh pr create \
--title "Phase ${PHASE_NUMBER}: ${PHASE_NAME}" \
--body "${PR_BODY}" \
--base ${BASE_BRANCH}
--body-file "${PR_BODY_FILE}" \
--base "${BASE_BRANCH}"
```
If `--draft` flag was passed: add `--draft`.