From planning-tools
Synthesizes a progress entry for the current branch and writes it to a configured destination (markdown, Linear, or GitHub).
How this command is triggered — by the user, by Claude, or both
Slash command
/planning-tools:plan-progress [--final] [--destination <type>:<id>] [--style-from <path>] [--ticket <id>]The summary Claude sees in its command listing — used to decide when to auto-load this command
You are **synthesizing a progress entry** for the current branch and writing it to the configured destination. The output style is codified by the `planning-tools:progress-methodology` skill — one dense paragraph per branch, evidence-rich, with inline sub-markers (`Piggybacked:`, `Verification:`, `Out of scope:`). This command is **safe to re-run** — SHA-based idempotency means a second run between phases only narrates the new commits; the existing prose is preserved. **Input:** `$ARGUMENTS` Parse arguments: - `--final` → flip the entry to shipped state (prepend `**Shipped <today> via PR...
You are synthesizing a progress entry for the current branch and writing it to the configured destination. The output style is codified by the planning-tools:progress-methodology skill — one dense paragraph per branch, evidence-rich, with inline sub-markers (Piggybacked:, Verification:, Out of scope:).
This command is safe to re-run — SHA-based idempotency means a second run between phases only narrates the new commits; the existing prose is preserved.
Input: $ARGUMENTS
Parse arguments:
--final → flip the entry to shipped state (prepend **Shipped <today> via PR #<N>.**, append ✅ to heading)--destination <type>:<id> → one-off destination override. Examples: markdown:<path-to-progress-file> (any path; the plugin does not assume context/ or any other directory), linear:<ticket-id> (e.g., linear:AIA-1234), github:<owner>/<repo>#<number> (e.g., github:oborchers/fractional-cto#42). Does not persist to settings.--style-from <path> → use the entries in this file as style exemplars instead of pulling from the destination--ticket <id> → explicit source-ticket override (otherwise auto-detected from branch name)Read the planning-tools:progress-methodology skill before Step 1 if you have not seen it this session — it owns the conventions every subsequent step depends on.
Run one bash block. Lift from /planning-tools:plan-tick Step 1:
git rev-parse --is-inside-work-tree || { echo "ERROR: not in a git repo"; exit 1; }
ROOT="$(git rev-parse --show-toplevel)"
BRANCH="$(git branch --show-current)"
[ -z "$BRANCH" ] && { echo "ERROR: detached HEAD — /planning-tools:plan-progress requires a named branch"; exit 1; }
BASE="$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')"
[ -z "$BASE" ] && git rev-parse --verify --quiet origin/main >/dev/null && BASE=main
[ -z "$BASE" ] && git rev-parse --verify --quiet origin/master >/dev/null && BASE=master
[ -z "$BASE" ] && BASE=main
MERGE_BASE="$(git merge-base HEAD "$BASE" 2>/dev/null || git merge-base HEAD "origin/$BASE" 2>/dev/null)"
echo "ROOT=$ROOT"
echo "BRANCH=$BRANCH"
echo "BASE=$BASE"
echo "MERGE_BASE=$MERGE_BASE"
If not in a git repo or BRANCH is empty (detached HEAD), error with the message above and stop.
Compute the branch slug (used as the entry-key): strip leading feature/, fix/, chore/, bugfix/, hotfix/, or <user>/ prefixes; lowercase; replace _ and / with -. Save this as BRANCH_SLUG.
Reuse the discovery from /planning-tools:plan-tick Step 2 (branch-match + recency fallback):
*.md under $ROOT/context/tickets/, $ROOT/docs/plans/, $ROOT/.claude/plans/master/. Filter to files containing ## Implementation Phases.No master plan matched branch <BRANCH>. Synthesizing progress from commits + style exemplars only — piggyback detection disabled.If a plan is found, read it and extract:
phase_scopes[]Read $ROOT/.claude/planning-tools.local.md if it exists. Parse YAML frontmatter:
SETTINGS="$ROOT/.claude/planning-tools.local.md"
if [ -f "$SETTINGS" ]; then
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$SETTINGS")
CONFIGURED_DEST_TYPE=$(echo "$FRONTMATTER" | grep '^progress_destination:' | sed 's/progress_destination: *//' | sed 's/^"\(.*\)"$/\1/')
CONFIGURED_DEST_ID=$(echo "$FRONTMATTER" | grep '^progress_destination_id:' | sed 's/progress_destination_id: *//' | sed 's/^"\(.*\)"$/\1/')
CONFIGURED_TICKET_PROVIDER=$(echo "$FRONTMATTER" | grep '^ticket_provider:' | sed 's/ticket_provider: *//' | sed 's/^"\(.*\)"$/\1/')
fi
Treat missing values as empty.
Priority order:
--destination <type>:<id> flag → use it; do not persist.AskUserQuestion pattern. Probe the repo for an existing PROGRESS file first so the recommendation matches reality.Probe environment availability before offering destinations:
markdown — always available.linear — available iff the Linear MCP tools are loaded (mcp__linear-server__get_issue is callable). If not loaded, suppress from the offered list.github — available iff gh is on PATH (command -v gh returns success).Before recommending a markdown path, probe the repo with a shallow glob (no hardcoded context/ assumption — the user may keep progress at root, docs/, notes/, or somewhere else entirely):
EXISTING_PROGRESS=$(find "$ROOT" -maxdepth 3 -type f \
\( -iname 'PROGRESS.md' -o -iname 'CHANGELOG.md' -o -iname 'CHANGES.md' \) \
-not -path '*/.git/*' -not -path '*/node_modules/*' -not -path '*/.venv/*' \
-print 2>/dev/null | head -5)
If one or more matches → pick the most-recently-modified (ls -t) as the recommended default. If multiple matches exist, mention the others in the printed list so the user can override via --destination.
If zero matches → no recommendation can presume a path; the binary prompt's recommended option becomes "Cancel — I'll supply a path via --destination".
Print the available destinations as a plain-text numbered list with one-line hints. The markdown row's hint mentions the discovered path (if any), not a hardcoded one:
Choose a progress destination:
1. markdown — Append entries to a project-local markdown file. Discovered: <discovered-path or "no PROGRESS file found in the repo">.
2. linear — Post entries as comments on a Linear ticket (auto-detected from branch or set via --ticket).
3. github — Post entries as comments on a GitHub issue or PR.
(Omit the rows for unavailable providers.)
Then call AskUserQuestion with exactly two options. The phrasing of Option 1 branches on whether a PROGRESS file was discovered:
If an existing PROGRESS file was discovered:
"Use existing markdown file at <discovered-path>" — description: "Save markdown: as the destination. Future /plan-progress runs skip this prompt.""Cancel — I'll specify a different destination" — description: "Stop now. Re-run /planning-tools:plan-progress --destination : to choose a different option (e.g., linear:AIA-1234, github:org/repo#42, markdown:)."If no PROGRESS file was discovered:
"Cancel — I'll specify a path via --destination" — description: "Stop now. Re-run /planning-tools:plan-progress --destination markdown: with the path where this project keeps progress logs (e.g., markdown:PROGRESS.md at root, markdown:docs/PROGRESS.md, markdown:context/PROGRESS.md — your choice).""Create new file at PROGRESS.md (repo root)" — description: "Save markdown:PROGRESS.md as the destination and create the file at the repository root. Use this only if you want a fresh PROGRESS.md at the top level — most projects have a conventional location, in which case Cancel and re-run with --destination."Per the AskUserQuestion schema, the harness adds an "Other" path automatically — the user can free-text any path there as well.
On Proceed (Option 1 with discovered path, OR Option 2 with PROGRESS.md fallback): persist the chosen destination to $ROOT/.claude/planning-tools.local.md:
---
progress_destination: markdown
progress_destination_id: <resolved-path>
---
# planning-tools settings
Generated by /planning-tools:plan-progress on <today>. Edit this file directly to switch
destinations, or run /planning-tools:plan-progress --destination <type>:<id> for a one-off override.
Create .claude/ if it does not exist. After writing, check whether .claude/*.local.md is in $ROOT/.gitignore. If not, emit an instruction line for the user (do not modify .gitignore automatically):
Note: add `.claude/*.local.md` to .gitignore — this file should not be committed.
On Cancel: stop with Cancelled. Re-run with --destination <type>:<id> to skip this prompt — e.g., /planning-tools:plan-progress --destination markdown:<your-progress-path>.
Nothing in this command hardcodes context/ or any other directory as the progress-file location. The only exception is the fallback PROGRESS.md at the repository root offered in the no-PROGRESS-discovered branch — and even there, the user must opt in by choosing Option 2; the default (Option 1) is Cancel. The user's project convention drives the path; the plugin discovers, suggests, or asks — it does not assume.
After resolution, parse <type>:<id> into DEST_TYPE and DEST_ID. For markdown, DEST_ID is a file path (relative to $ROOT unless absolute). For linear, DEST_ID is the ticket identifier (AIA-1234). For github, DEST_ID is <owner>/<repo>#<number>.
Priority order:
--ticket <id> flag → use it.BRANCH for ticket patterns in this order:
[A-Z]{2,6}-\d+ (e.g., AIA-4174)#\d+ (after a slash, e.g., feat/#42)If a ticket is resolved:
CONFIGURED_TICKET_PROVIDER (if set), else from the destination type (if DEST_TYPE=linear or =github), else infer from the ID format.progress-methodology skill's adapter contract):
linear: mcp__linear-server__get_issue(id) → title, body, status, url; then mcp__linear-server__list_comments(issueId) → all comments, no cap (per the comment-fetch policy in progress-methodology).github: gh issue view <N> --repo <owner>/<repo> --json title,body,state,url,comments (or gh pr view).{ title, body, comments[], status, url } to pass to the synthesizer.If the required MCP/CLI is unavailable, fail loudly: Linear MCP not loaded — cannot fetch <ticket>. Re-run with --ticket <id> on a session that has the Linear MCP, or omit ticket context. Do not proceed.
Use the destination adapter's find_existing_entry(identifier) (per the progress-methodology skill):
Read DEST_ID (the file path; relative paths resolve under $ROOT). If the file does not exist, treat as no match. Otherwise grep the file for <!-- planning-tools:plan-progress entry-key:$BRANCH_SLUG -->. If found, capture the entry body from the marker line through the next ## heading or EOF (whichever comes first). Otherwise, no match.
Call mcp__linear-server__list_comments(issueId=DEST_ID). For each comment body, grep for the marker. If found, capture { comment_id, body }. Otherwise, no match.
Call gh issue view <N> --repo <owner>/<repo> --comments --json comments (or gh pr view if the ID resolves to a PR). For each comment, grep for the marker. If found, capture { comment_id, body } (comment ID from the GitHub REST API node ID). Otherwise, no match.
| Detection | Action |
|---|---|
| No match | Set verdict=NEW_ENTRY. No user prompt yet (apply gate handles it). |
Match, no ✅ | Set verdict=IN_FLIGHT_UPDATE. No user prompt yet. |
Match, has ✅ | 3-option AskUserQuestion before synthesis: "Refresh completed entry" / "Create new entry" / "Cancel". On Refresh → verdict=COMPLETED_REFRESH. On Create new entry → verdict=NEW_ENTRY (the existing entry stays in place; new entry gets a -v2 suffix on the entry-key marker to avoid collision). On Cancel → stop. |
If --style-from <path> was passed, read that file and use the entries inside as exemplars.
Otherwise:
DEST_ID. Parse out the 2 most-recent entries (top of file, since entries are prepended). Exclude the in-flight entry currently being updated.planning-tools:plan-progress entry-key: marker.If fewer than 1 prior entry is available, fall back to <plugin>/skills/progress-methodology/examples/sample-entry.md.
--final)If --final was passed, fetch PR metadata:
gh pr list --head "$BRANCH" --json number,url --limit 1 2>/dev/null
If the result is non-empty, capture pr_number and pr_url. If empty (no PR open for the branch yet), set both to null — the synthesizer will emit **Shipped <date>.** without the PR clause.
Compose the dense paragraph entry in the main conversation. No subagent is dispatched. See [[no-subagents-for-procedural-wrappers]] for the design choice.
git log <MERGE_BASE>..HEAD --pretty=format:'%h %an %ad%n%s%n%b%n---' nowgit diff <MERGE_BASE>..HEAD --statgit diff <MERGE_BASE>..HEAD --name-onlyprogress-methodology/examples/sample-entry.md)markdown / linear / github)--final flag + PR metadata (from Step 9; only when --final was passed)NEW_ENTRY / IN_FLIGHT_UPDATE / COMPLETED_REFRESH from Step 7)Read the progress-methodology skill via the Skill tool to learn the style spec (sub-marker order, entry-key marker format, SHA-tracking convention, comment-fetch policy). Do this before composing.
Extract the covered-SHA set from the existing entry body. Regex: \b[0-9a-f]{7,12}\b. Treat case-insensitively. Cross-check against the branch's full SHA list — only keep tokens that match a real commit.
Compute new commits = branch commit SHAs not in the covered-SHA set.
Idempotency early-exit: if the existing entry contains ✅ AND new commits is empty → set verdict to NO_CHANGES and skip composition. Step 11 will report and exit cleanly.
Plan piggyback comparison (only if phase scopes were extracted in Step 2):
git diff --name-only)../, lowercase).**Piggybacked:**).**Piggybacked?:**).Compose the entry body in this order:
Heading line — exact format:
## <ticket-id-or-branch-slug>: <one-line synopsis>
Append ✅ to the heading only when --final was passed.
First content line — the entry-key marker as an HTML comment, on its own line, immediately above the heading:
<!-- planning-tools:plan-progress entry-key:<branch-slug> -->
Body — one dense paragraph in the style spec (see progress-methodology skill). Open with:
--final): **Branch:** \` (N commits, ready to PR).` then narrate scope.--final): **Shipped <YYYY-MM-DD> via PR #<N>.** (omit via PR #<N> if pr_number is null), then narrate scope.Sub-markers in this order, omitting any whose content would be empty: **Piggybacked:** / **Piggybacked?:**, **Verification:**, **Out of scope:**, **Operational rollout:**, **Rollback:**.
Preserve already-cited SHAs. If extending an in-flight entry, the sentences citing earlier commits stay in the new body verbatim where possible. Append new sentences for new commits; only refine prior prose if a new commit explicitly invalidates a prior claim (rare — flag with [updated] parenthetical when you do).
Cite every new commit by short SHA in the prose. Do not list SHAs in a footer or appendix. They go in the sentences that narrate the work, e.g., "...refactored the foo orchestrator (commit 1a2b3c4)..."
Verification sub-marker — for in-flight entries, narrate gates that the commits themselves claim to have passed (read commit messages for Verification: / Tests: / make test mentions). For shipped entries with --final, the user typically supplies these via the entry context; if absent, include what is observable from the commit messages with a hedging phrase ("Verification claimed in commit messages: ...").
| Verdict | Conditions |
|---|---|
NEW_ENTRY | No existing entry body (Step 6 returned no match). |
IN_FLIGHT_UPDATE | Existing entry present, no ✅, ≥1 new commit. |
COMPLETED_REFRESH | Step 7 prompted the user; they chose "Refresh completed entry"; existing body has ✅. |
NO_CHANGES | Existing entry has ✅ AND zero new commits (step 4 above). |
processBatch into three pure functions wired by a thin orchestrator in src/foo/orchestrate.ts (commit 1a2b3c4)" is right.Piggybacked: marker. If no verification gates are observable, no Verification: marker.Track any low-confidence Piggybacked?: items separately — surface them above the apply gate in Step 11 so the user can resolve before approving.
If verdict is NO_CHANGES (entry has ✅, no new commits): report Nothing to do — entry is complete and no new commits since last update. and stop.
Otherwise, present the proposed body to the user:
NEW_ENTRY: Print the proposed entry body in a fenced block. Ask via binary AskUserQuestion: "Append to <destination>" / "Discard".IN_FLIGHT_UPDATE: Print a unified diff of the existing entry vs the new body (use diff -u over temp files, or hand-roll a brief diff if diff unavailable). Ask via binary AskUserQuestion: "Update <destination>" / "Discard".COMPLETED_REFRESH: Print the proposed refreshed body. Ask "Apply refresh to <destination>" / "Discard".Surface any low-confidence Piggybacked?: notes from Step 10 above the AskUserQuestion so the user can edit before approving (the user edits the proposed body manually after Discard-ing and re-running).
On Discard: stop without writing. On approval: proceed to Step 12.
DEST_ID (newest-first ordering). Add a trailing --- separator before the next entry if the file is non-empty. If the file does not exist, create it with the entry as the only content.## or EOF) as old_string and the new body as new_string. Anchor by entry-key marker for uniqueness.mcp__linear-server__save_comment(issueId=DEST_ID, body=<new body>).mcp__linear-server__save_comment(id=<existing comment_id>, body=<new body>) — save_comment updates when id is supplied.gh issue comment <N> --repo <owner>/<repo> --body <body> (or gh pr comment if the ID is a PR).gh api repos/<owner>/<repo>/issues/comments/<comment-id> -X PATCH -f body="$NEW_BODY"
For all destinations: write the entry-key marker as the literal first line of the body.
Output a single concluding line summarizing what was applied:
<verdict>: <N new commits narrated> → <destination type>:<destination id-or-path> (<url-if-applicable>).
If a plan was not matched, also note: (no plan matched branch; piggyback detection skipped).
If --final was applied, also note the PR number / URL or (no PR found).
The main conversation owns all user interaction.
--destination flag and no saved destination) — binary confirm.Per [[askuserquestion-4option-cap]], never use multi-select with dynamic-length options.
.gitignore. Emits a note for the user to do it manually.The composition routine in Step 10 runs entirely in the main conversation. No subagent is dispatched. See [[no-subagents-for-procedural-wrappers]] for the design choice — reads + ruleset application + style transfer for a single dense paragraph is procedural-wrapper territory, not real isolation.
planning-tools:progress-methodology skill./planning-tools:plan-tick — same normalization, same fallback chain.npx claudepluginhub oborchers/fractional-cto --plugin planning-tools/pushCommits staged changes, pushes to remote, and posts a progress comment on the associated PR or issue using session context or branch detection.
/prCreates a pull request from the current feature branch using GitHub CLI, with auto-generated title, summary, and testing notes based on project conventions. Supports draft, manual confirmation, and rebase options.
/generate-linear-worklogGenerates a technical work log comment on a Linear issue using recent git commits from the current branch, including file changes and line counts.
/finishCreates a pull request with a comprehensive description by loading project configuration, verifying git state, gathering changed files, and prompting for user input.
/issue-to-implImplements a feature from a GitHub issue — creates a branch, writes documentation and tests, implements the solution incrementally, and commits milestones.