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:
@@ -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',
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
@@ -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" }`).
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
"cross_ai_command": "",
|
||||
"cross_ai_timeout": 300
|
||||
},
|
||||
"ship": {
|
||||
"pr_body_sections": []
|
||||
},
|
||||
"planning": {
|
||||
"commit_docs": true,
|
||||
"search_gitignored": false,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user