Repository navigation
Single-call mutation ergonomics: pm create/update --body-file, pm close -m/-r/-d, --resolution as close reason - #230
Conversation
…/-d, --resolution as close reason Reduce agent round-trips on the core mutation surface (project management = context management). Three changes from the 2026-06-12 dogfooding audit: - pm create/update --body-file <path> (GH-214, pm-7c48): load a long markdown body from a file instead of escaping a huge inline --body or chaining a second pm append. New src/core/io/body-file.ts resolveBodyFileContent helper is a CLI-only input alias onto the existing body field (no MCP surface — file paths are meaningless over MCP). Mutually exclusive with --body; actionable errors for conflict / empty-path / unreadable file. Wired into create+update actions before normalize, CREATE/UPDATE_FLAG_CONTRACTS, zsh/fish completion, contracts golden, and docs. - pm close short aliases -m/-r/-d (GH-226, pm-i1mu) for --message/--reason/ --duplicate-of. No semantic change. - pm close --resolution as close reason (pm-7x8d): under governance.require_close_reason, when no positional/--reason text is given the close reason is derived from the next-best signal — explicit reason > --duplicate-of ("Duplicate of <id>") > --resolution. The resolution still persists to metadata.resolution, so `pm close <id> --resolution "..."` no longer hard-blocks. Tests: 100%-covered src/core/io/body-file.ts unit spec (in tests/unit/core/io/ to respect the 120-file-per-dir cap); in-process integration coverage for the create/update/close wiring; close-command precedence unit tests. Full coverage suite green (3275 tests). Closes pm-7c48, pm-i1mu, pm-7x8d. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewer's GuideAdds a CLI-only --body-file input alias for pm create/update to load markdown bodies from files, improves pm close ergonomics (short flags and using --resolution as a fallback close reason), and fixes pm-changelog prepend-merge semantics around Unreleased promotion and release ordering, with tests, docs, contracts, completion, and changelog updates. Sequence diagram for pm create/update with --body-filesequenceDiagram
actor User
participant pm_cli
participant resolveBodyFileContent
participant BodyFileReader
participant runCreate
participant runUpdate
User->>pm_cli: pm create ... --body-file path
pm_cli->>resolveBodyFileContent: resolveBodyFileContent(bodyFile, body)
resolveBodyFileContent->>resolveBodyFileContent: [inlineBody undefined]
resolveBodyFileContent->>BodyFileReader: readFile(path)
BodyFileReader-->>resolveBodyFileContent: bodyContent
resolveBodyFileContent-->>pm_cli: bodyContent
pm_cli->>pm_cli: options.body = bodyContent
alt create command
pm_cli->>runCreate: runCreate(normalizeCreateOptions)
else update command
pm_cli->>runUpdate: runUpdate(id, normalizeUpdateOptions)
end
runCreate-->>User: mutation result
runUpdate-->>User: mutation result
User->>pm_cli: pm update ... --body-file path
pm_cli->>resolveBodyFileContent: resolveBodyFileContent(bodyFile, body)
resolveBodyFileContent->>BodyFileReader: readFile(path)
BodyFileReader-->>resolveBodyFileContent: bodyContent
resolveBodyFileContent-->>pm_cli: bodyContent
pm_cli->>runUpdate: runUpdate(id, normalizeUpdateOptions)
Sequence diagram for pm close close-reason precedencesequenceDiagram
actor User
participant pm_cli
participant runClose
participant assertDuplicateTargetExists
participant normalizeCloseReason
User->>pm_cli: pm close id [text] [flags]
pm_cli->>runClose: runClose(id, options, settings)
runClose->>assertDuplicateTargetExists: assertDuplicateTargetExists(pmRoot, settings, options.duplicateOf, id)
assertDuplicateTargetExists-->>runClose: duplicateOf
runClose->>runClose: hasExplicitReason
runClose->>runClose: trimmedResolution
alt [has explicit reason]
runClose->>runClose: effectiveCloseReasonText = closeReasonText
else [duplicateOf defined]
runClose->>runClose: effectiveCloseReasonText = Duplicate of duplicateOf
else [resolution provided]
runClose->>runClose: effectiveCloseReasonText = trimmedResolution
else [no source]
runClose->>runClose: effectiveCloseReasonText = closeReasonText
end
runClose->>normalizeCloseReason: normalizeCloseReason(effectiveCloseReasonText, require_close_reason)
normalizeCloseReason-->>runClose: closeReason
runClose-->>User: closed item result
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Warning Review limit reached
More reviews will be available in 51 minutes and 44 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR implements two CLI enhancements for pm-cli: a ChangesCLI Feature Implementation and Enhancement
Project Management and Extension Updates
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@codex review |
|
/gemini review |
|
To use Codex here, create a Codex account and connect to github. |
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- For consistency with other flags like
--close-reason/--close_reason, consider adding the--body_filealias toCREATE_FLAG_CONTRACTS/UPDATE_FLAG_CONTRACTS(and any related parsing paths) so the new--body-fileoption also supports the underscore variant.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- For consistency with other flags like `--close-reason`/`--close_reason`, consider adding the `--body_file` alias to `CREATE_FLAG_CONTRACTS`/`UPDATE_FLAG_CONTRACTS` (and any related parsing paths) so the new `--body-file` option also supports the underscore variant.
## Individual Comments
### Comment 1
<location path="src/core/io/body-file.ts" line_range="51-38" />
<code_context>
+ }
+ try {
+ return await readFileImpl(path);
+ } catch {
+ throw new PmCliError(
+ `--body-file could not read "${path}". Check that the file exists and is readable.`,
+ EXIT_CODE.NOT_FOUND,
+ {
+ code: "body_file_unreadable",
+ required: "Point --body-file at an existing, readable file.",
+ why: "The body content is loaded from the file at the supplied path, so an unreadable path cannot produce a body.",
+ nextSteps: [`Verify the path: ls -l "${path}"`, "Or pass the body inline with --body <text>."],
+ },
+ );
+ }
</code_context>
<issue_to_address>
**suggestion:** Preserve the underlying file error as context on the PmCliError
The catch block replaces all underlying errors with a generic NOT_FOUND, which hides important distinctions (e.g., permission denied vs ENOENT vs EISDIR) that are useful for debugging and support. Consider catching `error: unknown` and either setting it as the `cause` of `PmCliError` or including a minimal, sanitized snippet (e.g., `String(error)` or `(error as NodeJS.ErrnoException).code`) in the metadata to retain root-cause detail without compromising UX.
Suggested implementation:
```typescript
const path = bodyFile.trim();
if (path.length === 0) {
throw new PmCliError("--body-file requires a file path.", EXIT_CODE.USAGE, {
code: "body_file_missing_path",
required: "Pass a readable file path: --body-file <path>.",
why: "An empty --body-file value has no file to read the body from.",
});
}
try {
return await readFileImpl(path);
} catch (error: unknown) {
const fileError = error as NodeJS.ErrnoException | Error | undefined;
throw new PmCliError(
`--body-file could not read "${path}". Check that the file exists and is readable.`,
EXIT_CODE.NOT_FOUND,
{
code: "body_file_unreadable",
required: "Point --body-file at an existing, readable file.",
why: "The body content is loaded from the file at the supplied path, so an unreadable path cannot produce a body.",
nextSteps: [`Verify the path: ls -l "${path}"`, "Or pass the body inline with --body <text>."],
fileErrorCode: (fileError as NodeJS.ErrnoException | undefined)?.code,
fileErrorMessage: fileError && String(fileError),
},
);
}
```
If `PmCliError` supports a `cause` option (e.g., via `ErrorOptions`), you may also want to:
1. Extend the metadata object or constructor usage to include `cause: error`.
2. Or, if `PmCliError`’s signature is `(message, exitCode, options)` where `options` already supports `cause`, add `cause: error` to the options object alongside `code`, `required`, etc.
</issue_to_address>
### Comment 2
<location path="tests/unit/core/io/body-file.spec.ts" line_range="8-14" />
<code_context>
+import { resolveBodyFileContent } from "../../../../src/core/io/body-file.js";
+import { PmCliError } from "../../../../src/core/shared/errors.js";
+
+async function expectRejectionCode(promise: Promise<unknown>, code: string): Promise<void> {
+ await expect(promise).rejects.toBeInstanceOf(PmCliError);
+ await promise.catch((error: unknown) => {
+ expect(error).toBeInstanceOf(PmCliError);
+ expect((error as PmCliError).context.code).toBe(code);
+ });
+}
+
</code_context>
<issue_to_address>
**suggestion:** Simplify `expectRejectionCode` to avoid double-consuming the promise and tighten failure diagnostics
This helper both awaits `expect(promise).rejects...` and then `promise.catch(...)`, effectively consuming the rejection twice. To avoid surprising behavior and keep the assertion path single and clearer, consider using only `await expect(promise).rejects.toMatchObject({ context: { code } });` (or similar) and dropping the manual `catch`. This keeps diagnostics focused and easier to interpret.
```suggestion
async function expectRejectionCode(promise: Promise<unknown>, code: string): Promise<void> {
await expect(promise).rejects.toMatchObject({
context: expect.objectContaining({ code }),
});
}
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| async function expectRejectionCode(promise: Promise<unknown>, code: string): Promise<void> { | ||
| await expect(promise).rejects.toBeInstanceOf(PmCliError); | ||
| await promise.catch((error: unknown) => { | ||
| expect(error).toBeInstanceOf(PmCliError); | ||
| expect((error as PmCliError).context.code).toBe(code); | ||
| }); | ||
| } |
There was a problem hiding this comment.
suggestion: Simplify expectRejectionCode to avoid double-consuming the promise and tighten failure diagnostics
This helper both awaits expect(promise).rejects... and then promise.catch(...), effectively consuming the rejection twice. To avoid surprising behavior and keep the assertion path single and clearer, consider using only await expect(promise).rejects.toMatchObject({ context: { code } }); (or similar) and dropping the manual catch. This keeps diagnostics focused and easier to interpret.
| async function expectRejectionCode(promise: Promise<unknown>, code: string): Promise<void> { | |
| await expect(promise).rejects.toBeInstanceOf(PmCliError); | |
| await promise.catch((error: unknown) => { | |
| expect(error).toBeInstanceOf(PmCliError); | |
| expect((error as PmCliError).context.code).toBe(code); | |
| }); | |
| } | |
| async function expectRejectionCode(promise: Promise<unknown>, code: string): Promise<void> { | |
| await expect(promise).rejects.toMatchObject({ | |
| context: expect.objectContaining({ code }), | |
| }); | |
| } |
There was a problem hiding this comment.
Code Review
This pull request introduces several enhancements to the project management CLI, including a new --body-file option for loading item bodies from files, short aliases for pm close flags, and fallback logic to derive close reasons from resolutions or duplicates under strict governance. It also improves the changelog generator to chronologically insert release sections and promote the Unreleased section during releases. The review feedback suggests making the mutual exclusion validation for --body and --body-file more robust by checking if options.body is defined rather than strictly checking if it is a string, which prevents non-string values from bypassing conflict checks.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if (typeof options.bodyFile === "string") { | ||
| options.body = await resolveBodyFileContent( | ||
| options.bodyFile, | ||
| typeof options.body === "string" ? options.body : undefined, |
There was a problem hiding this comment.
Using options.body !== undefined instead of typeof options.body === "string" makes the mutual exclusion robust against cases where options.body might be parsed as a non-string value (such as a boolean). If options.body is defined but not a string, the current implementation passes undefined to resolveBodyFileContent, which silently bypasses the conflict validation.
| typeof options.body === "string" ? options.body : undefined, | |
| options.body !== undefined ? String(options.body) : undefined, |
| if (typeof options.bodyFile === "string") { | ||
| options.body = await resolveBodyFileContent( | ||
| options.bodyFile, | ||
| typeof options.body === "string" ? options.body : undefined, |
There was a problem hiding this comment.
Using options.body !== undefined instead of typeof options.body === "string" makes the mutual exclusion robust against cases where options.body might be parsed as a non-string value (such as a boolean). If options.body is defined but not a string, the current implementation passes undefined to resolveBodyFileContent, which silently bypasses the conflict validation.
| typeof options.body === "string" ? options.body : undefined, | |
| options.body !== undefined ? String(options.body) : undefined, |
There was a problem hiding this comment.
Code Review
This pull request introduces several enhancements to the pm CLI tool. Key additions include a new --body-file <path> option for pm create and pm update to load markdown bodies from a file, and short aliases (-m, -r, -d) for the pm close command. Additionally, pm close now fallback-derives the close reason from the --resolution summary when require_close_reason is enabled and no explicit reason is provided. The pm-changelog extension was also updated to version 2026.6.13-1 to promote the Unreleased section correctly and sort release sections chronologically during merges. I have no feedback to provide.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tests/unit/completion-command.spec.ts (1)
98-117:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winMirror the new flag in the zsh/fish completion assertions too.
Right now this spec only verifies
--body-filein the bash generator. Since the PR wires the flag into zsh and fish as well, add matching assertions there to avoid shell-specific regressions slipping through.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/completion-command.spec.ts` around lines 98 - 117, The tests currently assert that the bash completion script (via generateBashScript) contains "--body-file" but the zsh and fish completion assertions were not updated; add matching assertions to the zsh and fish checks by calling generateZshScript and generateFishScript (or the existing zsh/fish script helpers in the file) and adding expect(zshScript).toContain("--body-file") and expect(fishScript).toContain("--body-file") in their respective "includes ..." tests so all three shells verify the new flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/integration/cli-inprocess-runner.integration.spec.ts`:
- Around line 22-24: The test is asserting the body at the top-level payload but
the CLI returns an envelope { item: ... }, so update the assertions to read the
body from fetched.json.item.body (not fetched.json.body); specifically change
the assertion that uses fetched.json as { body?: string } to access
fetched.json.item.body and do the same for the second occurrence (lines 32–33)
so the tests compare bodyContent against fetched.json.item.body.
---
Outside diff comments:
In `@tests/unit/completion-command.spec.ts`:
- Around line 98-117: The tests currently assert that the bash completion script
(via generateBashScript) contains "--body-file" but the zsh and fish completion
assertions were not updated; add matching assertions to the zsh and fish checks
by calling generateZshScript and generateFishScript (or the existing zsh/fish
script helpers in the file) and adding
expect(zshScript).toContain("--body-file") and
expect(fishScript).toContain("--body-file") in their respective "includes ..."
tests so all three shells verify the new flag.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: 5d7629a0-638d-4ba1-be55-82eb90bc15d1
⛔ Files ignored due to path filters (5)
.agents/pm/extensions/pm-changelog/dist/extension.jsis excluded by!**/dist/**.agents/pm/extensions/pm-changelog/dist/extension.js.mapis excluded by!**/dist/**,!**/*.map.agents/pm/extensions/pm-changelog/dist/generator.d.ts.mapis excluded by!**/dist/**,!**/*.map.agents/pm/extensions/pm-changelog/dist/generator.jsis excluded by!**/dist/**.agents/pm/extensions/pm-changelog/dist/generator.js.mapis excluded by!**/dist/**,!**/*.map
📒 Files selected for processing (25)
.agents/pm/extensions/.managed-extensions.json.agents/pm/extensions/pm-changelog/CHANGELOG.md.agents/pm/extensions/pm-changelog/manifest.json.agents/pm/extensions/pm-changelog/package.json.agents/pm/features/pm-7c48.toon.agents/pm/features/pm-i1mu.toon.agents/pm/history/pm-7c48.jsonl.agents/pm/history/pm-7x8d.jsonl.agents/pm/history/pm-i1mu.jsonl.agents/pm/history/pm-kanu.jsonl.agents/pm/tasks/pm-7x8d.toon.agents/pm/tasks/pm-kanu.toonCHANGELOG.mddocs/AGENT_GUIDE.mddocs/COMMANDS.mdsrc/cli/commands/close.tssrc/cli/commands/completion.tssrc/cli/register-mutation.tssrc/core/io/body-file.tssrc/sdk/cli-contracts.tstests/fixtures/contracts/full.jsontests/integration/cli-inprocess-runner.integration.spec.tstests/unit/close-command.spec.tstests/unit/completion-command.spec.tstests/unit/core/io/body-file.spec.ts
| const fetched = await context.runCliInProcess(["get", createdId, "--json"], { expectJson: true }); | ||
| expect((fetched.json as { body?: string }).body).toBe(bodyContent); | ||
|
|
There was a problem hiding this comment.
Fix get --json body assertions to read from item.body.
These assertions read body from the top-level payload, but this suite also treats get --json as an { item: ... } envelope. Using the wrong path can fail the test even when --body-file behavior is correct.
💡 Proposed fix
- expect((fetched.json as { body?: string }).body).toBe(bodyContent);
+ expect((fetched.json as { item: { body?: string } }).item.body).toBe(bodyContent);
@@
- expect((refetched.json as { body?: string }).body).toBe("updated from file");
+ expect((refetched.json as { item: { body?: string } }).item.body).toBe("updated from file");Also applies to: 32-33
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/integration/cli-inprocess-runner.integration.spec.ts` around lines 22 -
24, The test is asserting the body at the top-level payload but the CLI returns
an envelope { item: ... }, so update the assertions to read the body from
fetched.json.item.body (not fetched.json.body); specifically change the
assertion that uses fetched.json as { body?: string } to access
fetched.json.item.body and do the same for the second occurrence (lines 32–33)
so the tests compare bodyContent against fetched.json.item.body.
…ntext) - register-mutation: use `options.body !== undefined ? String(options.body)` for the --body/--body-file mutual-exclusion check so a non-string body value can't silently bypass the conflict guard (Gemini). - body-file: preserve the underlying errno in the unreadable-file error and give a directory-specific message for EISDIR (a real footgun when pointing --body-file at a folder); keeps the typed PmCliError context shape (Sourcery). - body-file.spec: simplify expectRejectionCode to a single rejects assertion (no double-consume) and cover the new errno/EISDIR branches. CodeRabbit's "read item.body" suggestion is a false positive: `pm get --json` returns `body` at the top level (siblings: item/body/linked/claim_state), which the passing tests confirm. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the reviews — addressed in 3f92bcc:
@codex review |
|
To use Codex here, create a Codex account and connect to github. |
There was a problem hiding this comment.
Code Review
This pull request introduces several enhancements to the PM CLI. It adds a --body-file option to pm create and pm update for loading markdown bodies from files, introduces short aliases (-m, -r, -d) for pm close, and allows deriving the close reason from the resolution summary when a close reason is required. Additionally, the pm-changelog extension is updated to promote stale Unreleased sections and insert release sections chronologically. Feedback is provided regarding the robustness of the version comparison logic in the changelog generator, suggesting a fallback mechanism to handle non-SemVer strings safely.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| for (const [index, releaseSection] of releaseSections.entries()) { | ||
| const replacement = releaseSection.markdown.trimEnd(); | ||
| const replaced = replaceReleaseSection(next, releaseSection.heading, replacement); | ||
| if (replaced.replaced) { | ||
| next = replaced.markdown; | ||
| action = "replaced"; | ||
| continue; | ||
| } | ||
| next = insertAfterTitle(next, replacement); | ||
| // Restrict promotion to the newest generated section (index 0). Otherwise, | ||
| // when the newest version already exists (replaced above) an older section | ||
| // that is missing could steal the pending `## Unreleased` and overwrite it, | ||
| // losing the pending changes (GH #48 review). | ||
| if (canPromoteUnreleased && index === 0) { | ||
| const promoted = replaceReleaseSection(next, "Unreleased", replacement); |
There was a problem hiding this comment.
If compareVersionStrings is called with version strings that do not strictly conform to SemVer (for example, custom release tags or unbracketed date-based versions), it might throw an error and crash the changelog generation process. Wrapping the version comparison in a try-catch block with a fallback (such as localeCompare) would make the sorting more robust against non-standard version headings.
for (const [index, releaseSection] of releaseSections.entries()) {
const replacement = releaseSection.markdown.trimEnd();
const replaced = replaceReleaseSection(next, releaseSection.heading, replacement);
if (replaced.replaced) {
next = replaced.markdown;
action = "replaced";
continue;
}
// Restrict promotion to the newest generated section (index 0). Otherwise,
// when the newest version already exists (replaced above) an older section
// that is missing could steal the pending `## Unreleased` and overwrite it,
// losing the pending changes (GH #48 review).
if (canPromoteUnreleased && index === 0) {
const promoted = replaceReleaseSection(next, "Unreleased", replacement);
if (promoted.replaced) {
next = promoted.markdown;
action = "replaced";
continue;
}
}
next = insertReleaseSection(next, releaseSection.heading, replacement);
if (action !== "replaced")
action = "inserted";
}|
Re: Gemini's note on Codex is not connected to this repo (the connector replies with a setup link), so no Codex review is available. All actionable review findings are addressed and every check is green. |
Summary
Reduces agent round-trips on the core mutation surface (our project management = context management philosophy). Three closely-related ergonomics improvements surfaced by the 2026-06-12 dogfooding audit of a downstream project:
pm create/update --body-file <path>pm closeshort aliases-m/-r/-dpm close --resolutionaccepted as the close reason--body-file(GH-214)Load a long markdown body (multi-paragraph specs with code blocks/tables) from a file instead of escaping a huge inline
--bodystring or chaining a secondpm append:pm create Feature "Search overhaul" --body-file ./specs/search.md pm update pm-a1b2 --body-file ./specs/updated.mdsrc/core/io/body-file.tsresolveBodyFileContenthelper — a CLI-only input alias onto the existingbodyfield (no MCP surface: file paths are meaningless over MCP, mirroringpm history --diff).--body; actionable guided errors for conflict / empty-path / unreadable file.CREATE/UPDATE_FLAG_CONTRACTS, zsh/fish completion, the contracts golden file, and docs.pm closeshort aliases (GH-226)-m(--message),-r(--reason),-d(--duplicate-of). No semantic change — verified no flag conflicts.--resolutionas close reason (pm-7x8d)Under
governance.require_close_reason, when no positional/--reasontext is given the close reason is derived from the next-best signal in priority order: explicit reason →--duplicate-of(Duplicate of <id>) →--resolution. The resolution still persists tometadata.resolution, sopm close <id> --resolution "<summary>"no longer hard-blocks.Testing
tests/unit/core/io/body-file.spec.ts(placed in a subdirectory to respect the 120-files-per-dir static-gate cap).cli-inprocess-runner.integration.spec.ts).pnpm typecheck,pnpm quality:static,pnpm changelog:pm:checkall pass.Notes
--comment/--file/--docunder--allow-audit-update) was deferred (tracked in pm-kanu): that surface was already deliberately routed through dedicated audit commands with exact retry guidance; reversing it changes a documented audit-scope decision and warrants explicit maintainer sign-off.pm-changelog(the 3 items land under a new## Unreleasedsection).🤖 Generated with Claude Code
Summary by Sourcery
Add CLI ergonomics for single-call mutations, improve close reason handling, and fix changelog prepend merge behavior.
New Features:
Bug Fixes:
Enhancements:
Build:
Documentation:
Tests:
Chores: