-
Notifications
You must be signed in to change notification settings - Fork 565
🔧 Major Bash Script Architecture Optimization #960
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Convert long one-liner bash commands to multiline format to prevent truncation - Fix awk syntax errors in epic-sync.md (replace literal \n with proper newlines) - Add missing directory separators in file path patterns - Replace chained && operators with proper if-then blocks - Break down complex pipe chains into step-by-step operations Key changes: * ccpm/scripts/pm/*.sh: Convert complex dependency parsing from one-liners to readable multiline blocks * ccpm/ccpm.config: Break down repository URL processing into clear steps * ccpm/commands/pm/epic-sync.md: Fix awk print statements for proper newline handling Fixes command truncation errors like: - Error: (eval):1: parse error near ')' - awk: syntax error at source line 1 - command not found: ! 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
- epic-merge.md: Convert complex nested commands to multiline format - Fix feature list generation with proper error handling - Break down epic issue number extraction - Add proper file existence checks - epic-refresh.md: Fix task issue extraction with error handling - epic-sync.md: Re-fix awk syntax for proper newline handling - test-runner.md: Remove MUXI-specific reference Prevents additional command truncation in: - Complex subshell operations with cd and loops - Chained grep operations for GitHub issue extraction - Nested command substitution patterns 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
…ndling - ccpm.config: Add support for ssh://[email protected]/ and ssh://github.com/ URL schemes - Ensures all GitHub URL variants are properly normalized - All PM scripts: Fix dependency parsing to handle whitespace-only cases - Trim leading/trailing whitespace after bracket removal - Properly handle empty dependency lists like "depends_on: [ ]" - Normalize whitespace-only strings to empty strings - Prevents false positive dependency detection Fixes issues where: - SSH URLs starting with ssh://[email protected]/ would fail parsing - Empty dependency arrays with whitespace were treated as non-empty - Tasks with "depends_on: [ ]" were incorrectly blocked 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
This major refactoring addresses bash script duplication and improves maintainability: ## New Utility Libraries (ccpm/lib/): - frontmatter.sh: YAML parsing/updating (eliminates 200+ lines of duplication) - dependencies.sh: Task dependency management and validation - datetime.sh: Cross-platform timestamp handling (GNU/macOS compatibility) - github.sh: Standardized GitHub CLI operations with error handling - error.sh: Consistent validation and error handling across all scripts - discovery.sh: Epic/task navigation and progress calculation ## New Operation Scripts (ccpm/scripts/pm/): - clean.sh: Automated system cleanup with archiving and dry-run support - file-management.sh: File renaming by issue numbers and organization - close-issue.sh: Complete issue closing workflow with GitHub sync - close-epic.sh: Epic completion with archiving and PRD status updates ## Command Simplifications: - clean.md: 102 → 41 lines (-60%) - issue-close.md: 102 → 41 lines (-60%) - epic-close.md: 69 → 48 lines (-30%) - Replaced complex inline bash with simple script calls ## Key Improvements: - 30-40% reduction in total codebase size - Eliminated duplication across 16+ scripts - Added comprehensive input validation and error handling - Cross-platform compatibility (macOS/Linux) - All scripts syntax-validated and executable - Modular, reusable, and unit-testable components ## Files Consolidated: - Removed epic-refresh.md (functionality merged into epic-sync.md) - Extracted large bash blocks from commands into dedicated scripts - Updated help documentation to reflect new command structure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
WalkthroughIntroduces a script-driven CCPM PM workflow: new Bash libraries (datetime, dependencies, discovery, error, frontmatter, github), new/updated PM commands and scripts (epic sync 2.0, close/reopen epic and issues, start/merge workflows, cleanup, file management), expanded GitHub repo normalization and function exports, documentation updates, and removal of epic-refresh docs. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant CLI as pm:epic-sync
participant GH as GitHub (gh)
participant FS as Local FS
participant Plan as Planner
User->>CLI: pm:epic-sync <epic> [--repo ...]
CLI->>GH: fetch-github-issues.sh
CLI->>FS: build-local-inventory.sh
CLI->>Plan: plan-sync-actions.sh
alt Update local from GH
CLI->>FS: sync-local-files.sh
end
alt Update GH from local
CLI->>GH: sync-github-issues.sh
end
CLI->>FS: worktree-and-mappings.sh
CLI->>GH: post-progress-reports.sh
CLI-->>User: Summary (created/updated, progress)
sequenceDiagram
autonumber
actor User
participant Close as close-epic.sh
participant FS as Local FS
participant GH as GitHub (gh)
participant Arc as Archive
User->>Close: close --epic <name> [--archive]
Close->>FS: Verify all tasks closed
Close->>FS: Update epic.md (status/progress/timestamps)
Close->>FS: Update linked PRD (if present)
Close->>GH: Close epic issue with comment (if linked)
Close->>Close: Calculate duration
opt --archive
Close->>Arc: Move epic to archive and summarize
end
Close-->>User: Completion summary
sequenceDiagram
autonumber
actor User
participant CI as close-issue.sh
participant FS as Local FS
participant GH as GitHub (gh)
participant Disc as Discovery
User->>CI: close <issue> [notes]
CI->>Disc: Locate epic/task file
CI->>FS: Update task frontmatter (status=closed, updated)
CI->>FS: Update progress file (if present)
CI->>GH: Close issue with comment
CI->>GH: Update epic task list (checkboxes)
CI->>FS: Recalculate epic progress
CI-->>User: Progress and next steps
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 42
🧹 Nitpick comments (60)
ccpm/scripts/pm/help.sh (1)
25-25: Good: updated epic-sync to bidirectional. Also align Quick Start wording.Line 25 reflects Epic Sync 2.0. Consider updating the Quick Start step (Line 14) from “Push to GitHub” to the same bidirectional phrasing to avoid mixed messaging.
Apply this change:
-echo " 4. /pm:epic-sync <name> - Push to GitHub" +echo " 4. /pm:epic-sync <name> - Bidirectional sync between local epics/tasks and GitHub issues"ccpm/scripts/pm/epic-start/setup-monitoring.sh (1)
10-24: Prefer here-doc for multi-line output.This avoids embedded variable expansion surprises and is easier to edit.
Apply this diff:
-echo " -✅ Agents launched successfully! - -Monitor progress: - /pm:epic-status $ARGUMENTS - -View branch changes: - git status - -Stop all agents: - /pm:epic-stop $ARGUMENTS - -Merge when complete: - /pm:epic-merge $ARGUMENTS -" +cat <<EOF +✅ Agents launched successfully! + +Monitor progress: + /pm:epic-status $ARGUMENTS + +View branch changes: + git status + +Stop all agents: + /pm:epic-stop $ARGUMENTS + +Merge when complete: + /pm:epic-merge $ARGUMENTS +EOFccpm/scripts/pm/epic-start/check-analysis.sh (1)
7-11: Validate that ISSUE is numeric.Prevents invalid paths and user error early.
Insert after Line 11:
+# Validate issue number format +if ! [[ "$ISSUE" =~ ^[0-9]+$ ]]; then + echo "❌ Error: Issue number must be numeric" + exit 1 +ficcpm/scripts/pm/issue-sync/check-repo-protection.sh (1)
5-10: Make template-repo detection robust (avoid false positives).Substring match can block forks like automazeio/ccpm-fork. Use a regex that matches the repo name as a terminal path segment.
Apply this diff:
-remote_url=$(git remote get-url origin 2>/dev/null || echo "") -if [[ "$remote_url" == *"automazeio/ccpm"* ]]; then +remote_url=$(git remote get-url origin 2>/dev/null || echo "") +# Match .../automazeio/ccpm[.git] at end of URL (HTTPS or SSH) +if [[ "$remote_url" =~ (^|[:/])automazeio/ccpm(\.git)?$ ]]; then echo "❌ ERROR: Cannot sync to CCPM template repository!" echo "Update your remote: git remote set-url origin https://github.com/YOUR_USERNAME/YOUR_REPO.git" exit 1 ficcpm/commands/pm/issue-close.md (1)
18-25: Simplify argument parsing; avoid external processes.Use read to split first token (issue) and assign the remainder to notes.
Apply this diff:
-# Extract issue number and completion notes from arguments -issue_number=$(echo "$ARGUMENTS" | awk '{print $1}') -completion_notes=$(echo "$ARGUMENTS" | cut -d' ' -f2-) +# Extract issue number and completion notes from arguments +read -r issue_number completion_notes <<< "$ARGUMENTS" # Close the issue bash ccpm/scripts/pm/close-issue.sh close "$issue_number" "$completion_notes"ccpm/scripts/pm/epic-start/preflight-checks.sh (3)
16-20: Avoid brittle YAML parsing with grep; use frontmatter helper.Direct grep on “^github:” can miss valid YAML (indentation, case). Prefer a shared helper to read frontmatter values.
Proposed approach (assuming lib/frontmatter.sh provides frontmatter_get):
+# shellcheck source=ccpm/lib/frontmatter.sh +source ccpm/lib/frontmatter.sh ... -# 2. Check GitHub sync (look for github: field) -if ! grep -q "^github:" ".claude/epics/$ARGUMENTS/epic.md"; then +# 2. Check GitHub sync (look for github field) +github_id="$(frontmatter_get ".claude/epics/$ARGUMENTS/epic.md" "github" || true)" +if [ -z "$github_id" ]; then echo "❌ Epic not synced. Run: /pm:epic-sync $ARGUMENTS first" exit 1 fi
22-25: Branch existence check: match exact ref to avoid substring collisions.Use show-ref for precise local branch detection.
Apply this diff:
-# 3. Check for branch -if ! git branch -a | grep -q "epic/$ARGUMENTS"; then +# 3. Check for branch +if ! git show-ref --verify --quiet "refs/heads/epic/$ARGUMENTS"; then echo "ℹ️ Branch epic/$ARGUMENTS does not exist - will be created" fi
27-31: Uncommitted changes check is effective. Optionally ignore untracked.If you want to allow untracked files, filter “??”.
Alternative:
if git status --porcelain | grep -qv '^??'; then echo "❌ You have uncommitted changes (excluding untracked). Commit or stash before starting an epic." exit 1 ficcpm/scripts/pm/epic-list.sh (2)
6-14: Avoid parsing with ls; use find for robustness and portability.This prevents issues with spaces/newlines and missing matches.
Apply this diff:
-if [ ! -d ".claude/epics" ]; then +if [ ! -d ".claude/epics" ]; then echo "📁 No epics directory found. Create your first epic with: /pm:prd-parse <feature-name>" exit 0 fi -epic_dirs=$(ls -d .claude/epics/*/ 2>/dev/null || true) +epic_dirs=$(find .claude/epics -mindepth 1 -maxdepth 1 -type d -print 2>/dev/null || true) if [ -z "$epic_dirs" ]; then echo "📁 No epics found. Create your first epic with: /pm:prd-parse <feature-name>" exit 0 fi
41-41: Task counting is fine; consider frontmatter helpers for metadata above.Counting via quoted glob is good. For fields (name/status/progress/github), consider sourcing ccpm/lib/frontmatter.sh for YAML-safe parsing to handle indentation/case.
ccpm/scripts/pm/epic-merge/validate-worktree.sh (2)
20-25: Use explicit non-empty check for git statusSlightly clearer and avoids subshell command substitution pitfalls.
-if [[ $(git status --porcelain) ]]; then +if [ -n "$(git status --porcelain)" ]; then echo "⚠️ Uncommitted changes in worktree:" git status --short echo "Commit or stash changes before merging" exit 1 fi
28-29: Consider pruning/fast-fetch flagsOptional: add --prune --tags for fresher state in mono-repos.
ccpm/scripts/pm/validate.sh (1)
45-56: Dependency parsing is YAML-fragile; prefer central deps/frontmatter libThis regex/sed approach breaks on quoted IDs, multiline lists, or inline comments. Source the new libraries (frontmatter.sh/dependencies.sh) and reuse a parser.
If the libs exist, refactor like:
- source ccpm/lib/dependencies.sh
- deps="$(ccpm_deps_from_task "$task_file")"
Would you like me to draft this change once I see the lib API?ccpm/scripts/pm/epic-merge/run-tests.sh (1)
1-5: Add strict mode and tool presence hintsEnsure early exit and reduce surprises.
#!/bin/bash +# Fail fast +set -euo pipefail +IFS=$'\n\t' # Run tests based on project typeccpm/scripts/pm/epic-start/create-execution-status.sh (1)
4-8: Optional: validate epic existsCheck for ".claude/epics/$ARGUMENTS/epic.md" and warn if missing.
ccpm/scripts/pm/issue-sync/post-comment.sh (2)
1-3: Enable strict mode#!/bin/bash +# Fail fast +set -euo pipefail +IFS=$'\n\t' # Post formatted update comment to GitHub issue
26-35: Preflight GitHub CLI availability and auth-# Post comment using GitHub CLI -if gh issue comment "$ARGUMENTS" --body-file "$TEMP_FILE"; then +# Post comment using GitHub CLI +if ! command -v gh >/dev/null 2>&1; then + echo "❌ Error: GitHub CLI (gh) not found" + exit 1 +fi +if ! gh auth status >/dev/null 2>&1; then + echo "❌ Error: gh is not authenticated. Run 'gh auth login' first." + exit 1 +fi +if gh issue comment "$ARGUMENTS" --body-file "$TEMP_FILE"; then echo "✅ Comment posted successfully to issue #$ARGUMENTS" # Clean up temp file rm -f "$TEMP_FILE" elseccpm/scripts/pm/issue-sync/update-frontmatter.sh (4)
1-3: Enable strict mode#!/bin/bash +# Fail fast +set -euo pipefail +IFS=$'\n\t' # Update frontmatter in progress and task files after sync
58-60: Tighten task matching to avoid false positives on issue numbersCurrent grep can match “12” in “112”. Anchor to path/ID patterns.
- if [ -f "$task" ] && grep -q "github.*$ARGUMENTS" "$task"; then + if [ -f "$task" ] && grep -Eq '^github:.*([/#]|issues/)'"$ARGUMENTS"'([[:space:]]|$)' "$task"; then
70-75: Derive status from sanitized numeric completion- if [ "$COMPLETION" = "100" ]; then + if [ "$COMPLETION" -eq 100 ]; then status="closed" else status="open" fi
38-51: Frontmatter rewrite is brittle; consider centralized frontmatter libRegex parsing can corrupt YAML (quotes, multiline). Prefer ccpm/lib/frontmatter.sh utilities if available.
Can we switch to the new frontmatter lib in this PR for consistency?
ccpm/scripts/pm/epic-start/manage-branch.sh (1)
16-26: Branch existence check and default branch handlingUse git’s plumbing for accuracy and fetch default branch dynamically; also fetch before operations.
-# If branch doesn't exist, create it -if ! git branch -a | grep -q "epic/$ARGUMENTS"; then - git checkout main - git pull origin main - git checkout -b "epic/$ARGUMENTS" +# Fetch latest refs +git fetch origin --prune + +# Determine default branch +DEFAULT_BRANCH="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD | sed 's#^origin/##')" +[ -z "${DEFAULT_BRANCH:-}" ] && DEFAULT_BRANCH="main" + +# If local branch doesn't exist, create it off default +if ! git rev-parse --verify --quiet "epic/$ARGUMENTS" >/dev/null; then + git checkout "$DEFAULT_BRANCH" + git pull --ff-only origin "$DEFAULT_BRANCH" + git checkout -b "epic/$ARGUMENTS" git push -u origin "epic/$ARGUMENTS" echo "✅ Created branch: epic/$ARGUMENTS" else git checkout "epic/$ARGUMENTS" - git pull origin "epic/$ARGUMENTS" + git pull --ff-only origin "epic/$ARGUMENTS" echo "✅ Using existing branch: epic/$ARGUMENTS" ficcpm/scripts/pm/epic-merge/handle-conflicts.sh (2)
1-3: Enable strict mode#!/bin/bash +# Fail fast +set -euo pipefail +IFS=$'\n\t' # Handle merge conflicts detection and guidance
10-33: Only emit conflict guidance when conflicts existGuard against false positives and provide a clean exit when none found.
-# Check conflict status -git status - -echo " -❌ Merge conflicts detected! -... -Worktree preserved at: ../epic-$ARGUMENTS -" -exit 1 +# Check conflict status +git status +if git diff --name-only --diff-filter=U | grep -q .; then + echo " +❌ Merge conflicts detected! + +Conflicts in: +$(git diff --name-only --diff-filter=U) + +Options: +1. Resolve manually: + - Edit conflicted files + - git add {files} + - git commit + +2. Abort merge: + git merge --abort + +3. Get help: + /pm:epic-resolve $ARGUMENTS + +Worktree preserved at: ../epic-$ARGUMENTS +" + exit 1 +else + echo "✅ No merge conflicts detected" +ficcpm/scripts/pm/issue-sync/calculate-epic-progress.sh (3)
24-27: Normalize status check; be tolerant to casing/whitespaceGuard against “Closed”, “CLOSED”, or extra spaces to avoid undercounting completed tasks.
Apply:
- status=$(grep '^status:' "$task_file" | head -1 | cut -d: -f2- | sed 's/^ *//') - if [ "$status" = "closed" ]; then + status=$(grep -m1 '^status:' "$task_file" | cut -d: -f2- | sed 's/^[[:space:]]*//' | tr '[:upper:]' '[:lower:]') + if [ "$status" = "closed" ]; then closed_tasks=$((closed_tasks + 1)) fi
41-67: Frontmatter update robustness: preserve missing fields, use mktemp, and reliably strip frontmatter
- Missing name/created currently emit empty keys.
- sed block to drop frontmatter is brittle across files; use a more precise extraction.
- Use mktemp for atomicity; avoid clobbering on concurrent runs.
- name=$(grep '^name:' "$epic_file" | head -1 | cut -d: -f2- | sed 's/^ *//') - status=$(grep '^status:' "$epic_file" | head -1 | cut -d: -f2- | sed 's/^ *//') - created=$(grep '^created:' "$epic_file" | head -1 | cut -d: -f2- | sed 's/^ *//') - prd=$(grep '^prd:' "$epic_file" | head -1 | cut -d: -f2- | sed 's/^ *//') - github_url=$(grep '^github:' "$epic_file" | head -1 | cut -d: -f2- | sed 's/^ *//') + name=$(grep -m1 '^name:' "$epic_file" | cut -d: -f2- | sed 's/^[[:space:]]*//') + status=$(grep -m1 '^status:' "$epic_file" | cut -d: -f2- | sed 's/^[[:space:]]*//') + created=$(grep -m1 '^created:' "$epic_file" | cut -d: -f2- | sed 's/^[[:space:]]*//') + prd=$(grep -m1 '^prd:' "$epic_file" | cut -d: -f2- | sed 's/^[[:space:]]*//') + github_url=$(grep -m1 '^github:' "$epic_file" | cut -d: -f2- | sed 's/^[[:space:]]*//') @@ - { + tmpfile="$(mktemp "${epic_file}.XXXXXX")" + { echo "---" - echo "name: $name" + [ -n "$name" ] && echo "name: $name" echo "status: $status" - echo "created: $created" + [ -n "$created" ] && echo "created: $created" echo "progress: ${progress}%" [ -n "$prd" ] && echo "prd: $prd" [ -n "$github_url" ] && echo "github: $github_url" echo "---" - # Add content after frontmatter - sed '1,/^---$/d; 1,/^---$/d' "$epic_file" - } > "$epic_file.tmp" && mv "$epic_file.tmp" "$epic_file" + # Append content after the first frontmatter block + awk 'BEGIN{fm=0} /^---[[:space:]]*$/{fm++ ; next} { if (fm>=2) print }' "$epic_file" + } > "$tmpfile" && mv "$tmpfile" "$epic_file"
30-33: Consider updating epic to 0% instead of early exit when no tasksOptional UX tweak: set progress: 0% on epic.md so downstream tools see an explicit 0 rather than missing progress.
-if [ $total_tasks -eq 0 ]; then - echo "ℹ️ No tasks found in epic: $EPIC_NAME" - exit 0 -fi +if [ $total_tasks -eq 0 ]; then + echo "ℹ️ No tasks found in epic: $EPIC_NAME" + epic_file=".claude/epics/$EPIC_NAME/epic.md" + if [ -f "$epic_file" ]; then + progress=0 + # reuse the frontmatter update block here to write 0% + fi + exit 0 +ficcpm/scripts/pm/next.sh (1)
22-24: Clarify policy: treat empty status as open; this is a behavior changeThis now surfaces tasks with missing status. If intentional, add a comment; otherwise revert to also skipping empty.
- if [ "$status" != "open" ] && [ -n "$status" ]; then + # Treat empty status as open; change to: if [ "$status" != "open" ] to also skip empty + if [ "$status" != "open" ] && [ -n "$status" ]; then continue ficcpm/scripts/pm/epic-sync/build-local-inventory.sh (1)
17-20: Nullglob to avoid literal iteration when no matchesWithout nullglob, the loop will run with a literal pattern if no files exist.
+# Avoid iterating on a literal when no files match +shopt -s nullglob for file_path in ".claude/epics/$EPIC_NAME"/*.md; do [ -f "$file_path" ] || continueccpm/scripts/pm/epic-merge/cleanup-worktree.sh (3)
1-8: Add basic safety: strict mode and clearer arg namingImprove resilience and consistency with other scripts.
-#!/bin/bash +#!/bin/bash +# shellcheck disable=SC2086 +set -euo pipefail @@ -ARGUMENTS="$1" -if [ -z "$ARGUMENTS" ]; then +EPIC_NAME="${1:-}" +if [ -z "$EPIC_NAME" ]; then echo "❌ Error: Epic name required" exit 1 fi
11-16: Guard git operations and force worktree removal when appropriateAvoid aborting on benign failures and ensure removal of stale worktree.
-# Push to remote -git push origin main +# Push to remote (best-effort) +git rev-parse --is-inside-work-tree >/dev/null 2>&1 && git push origin main || true @@ -# Clean up worktree -git worktree remove "../epic-$ARGUMENTS" -echo "✅ Worktree removed: ../epic-$ARGUMENTS" +# Clean up worktree +if [ -d "../epic-$EPIC_NAME" ]; then + git worktree remove -f "../epic-$EPIC_NAME" || true + echo "✅ Worktree removed: ../epic-$EPIC_NAME" +fi
18-24: Safer branch and archive handling
- Branch delete may fail if not merged; tolerate errors.
- Ensure epic dir exists; avoid overwriting an existing archived epic.
-# Delete branch -git branch -d "epic/$ARGUMENTS" -git push origin --delete "epic/$ARGUMENTS" 2>/dev/null || true +# Delete branch (best-effort) +git branch -d "epic/$EPIC_NAME" 2>/dev/null || true +git push origin --delete "epic/$EPIC_NAME" 2>/dev/null || true @@ -# Archive epic locally -mkdir -p .claude/epics/archived/ -mv ".claude/epics/$ARGUMENTS" ".claude/epics/archived/" -echo "✅ Epic archived: .claude/epics/archived/$ARGUMENTS" +# Archive epic locally +mkdir -p .claude/epics/archived/ +if [ -d ".claude/epics/$EPIC_NAME" ]; then + if [ -e ".claude/epics/archived/$EPIC_NAME" ]; then + echo "⚠️ Archived epic already exists: .claude/epics/archived/$EPIC_NAME" + else + mv ".claude/epics/$EPIC_NAME" ".claude/epics/archived/" + echo "✅ Epic archived: .claude/epics/archived/$EPIC_NAME" + fi +else + echo "ℹ️ Epic directory not found: .claude/epics/$EPIC_NAME" +ficcpm/scripts/pm/issue-sync/preflight-validation.sh (2)
1-15: Add strict mode and verify gh availabilityHarden the script and fail fast before using gh.
-#!/bin/bash +#!/bin/bash +set -euo pipefail @@ -# 1. GitHub Authentication +# 1. GitHub Authentication +if ! command -v gh >/dev/null 2>&1; then + echo "❌ GitHub CLI not installed. See: https://cli.github.com/" + exit 1 +fi
24-26: Avoid implying incomplete work on CLOSED issuesThe script doesn’t validate “incomplete” here; make the message neutral.
-if [ "$issue_state" = "CLOSED" ]; then - echo "⚠️ Issue is closed but work incomplete" -fi +if [ "$issue_state" = "CLOSED" ]; then + echo "ℹ️ Issue is CLOSED on GitHub" +ficcpm/scripts/pm/issue-sync/check-sync-timing.sh (2)
39-47: Use datetime utility for cross‑platform parsing; fix macOS fallbackPrefer central datetime helpers. Current fallback on macOS without gdate silently treats parse as 0 (always “old enough”).
-# Calculate time difference (basic check - 5 minutes = 300 seconds) -current_time=$(date -u +%s) -if command -v gdate >/dev/null 2>&1; then - # macOS with GNU coreutils - last_sync_time=$(gdate -d "$last_sync" +%s 2>/dev/null || echo "0") -else - # Linux date - last_sync_time=$(date -d "$last_sync" +%s 2>/dev/null || echo "0") -fi +# Calculate time difference (basic check - 5 minutes = 300 seconds) +# If ccpm/lib/datetime.sh exists, use it; otherwise fallback. +if [ -f "ccpm/lib/datetime.sh" ]; then + # shellcheck disable=SC1091 + . ccpm/lib/datetime.sh + current_time=$(datetime_now_epoch) + last_sync_time=$(datetime_to_epoch "$last_sync" || echo "0") +else + current_time=$(date -u +%s) + if command -v gdate >/dev/null 2>&1; then + last_sync_time=$(gdate -d "$last_sync" +%s 2>/dev/null || echo "0") + else + # macOS BSD date fallback – treat as unknown if cannot parse + last_sync_time=$(date -d "$last_sync" +%s 2>/dev/null || echo "0") + fi +fi
51-55: Message suggests an interactive prompt but script doesn’t read inputReword to a warning to avoid confusion in automation.
- echo "⚠️ Recently synced $minutes_ago minutes ago. Force sync anyway? (yes/no)" - # In automated context, we'll proceed but warn + echo "⚠️ Recently synced $minutes_ago minutes ago. Proceeding; consider delaying to reduce churn."ccpm/commands/pm/epic-close.md (1)
22-23: Example should show placeholders, not $ARGUMENTSUse an explicit placeholder to avoid confusion.
-bash ccpm/scripts/pm/close-epic.sh close $ARGUMENTS +bash ccpm/scripts/pm/close-epic.sh close <epic_name> [--archive]ccpm/scripts/pm/epic-merge/close-github-issues.sh (1)
10-16: Harden epic issue extraction (handle trailing slash/multiple matches)Current grep may miss IDs if URL ends with a slash or capture multiple numbers. Prefer matching issues/ and taking the last.
-epic_github_line=$(grep 'github:' ".claude/epics/archived/$ARGUMENTS/epic.md" 2>/dev/null || true) -if [ -n "$epic_github_line" ]; then - epic_issue=$(echo "$epic_github_line" | grep -oE '[0-9]+$' || true) +epic_github_line=$(grep '^github:' ".claude/epics/archived/$ARGUMENTS/epic.md" 2>/dev/null || true) +if [ -n "$epic_github_line" ]; then + epic_issue=$(echo "$epic_github_line" | grep -oE 'issues/([0-9]+)' | awk -F/ 'NF{num=$NF} END{print num}' || true) else epic_issue="" ficcpm/ccpm.config (1)
41-42: Quote variables when cutting to avoid word-splitting/globbingQuote GITHUB_REPO in cut pipelines.
-GITHUB_OWNER=$(echo $GITHUB_REPO | cut -d/ -f1) -GITHUB_REPO_NAME=$(echo $GITHUB_REPO | cut -d/ -f2) +GITHUB_OWNER=$(echo "$GITHUB_REPO" | cut -d/ -f1) +GITHUB_REPO_NAME=$(echo "$GITHUB_REPO" | cut -d/ -f2)ccpm/scripts/pm/standup.sh (1)
61-71: Centralize frontmatter/dependency parsing via libsReplace ad‑hoc grep/sed with ccpm/lib/frontmatter.sh and ccpm/lib/dependencies.sh to improve robustness (multi-line arrays, quoting) and reduce duplication.
-# Extract dependencies from task file -deps_line=$(grep "^depends_on:" "$task_file" | head -1) -if [ -n "$deps_line" ]; then - deps=$(echo "$deps_line" | sed 's/^depends_on: *//') - deps=$(echo "$deps" | sed 's/^\[//' | sed 's/\]$//') - # Trim whitespace and handle empty cases - deps=$(echo "$deps" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') - [ -z "$deps" ] && deps="" -else - deps="" -fi +# shellcheck disable=SC1091 +source ccpm/lib/dependencies.sh +deps="$(get_task_dependencies_line "$task_file")"ccpm/scripts/pm/epic-sync/worktree-and-mappings.sh (3)
1-4: Enable pipefail and nounset for safer executionStrengthen error handling.
-#!/bin/bash -# Create worktree and update mappings -set -e +#!/bin/bash +# Create worktree and update mappings +set -euo pipefail
27-37: Avoid extra gh call; use GH_REPO from config if availableSource config and reuse GH_REPO to reduce external calls and ensure consistency.
-echo "📋 Updating GitHub mapping file..." -repo=$(gh repo view --json nameWithOwner -q .nameWithOwner) +echo "📋 Updating GitHub mapping file..." +if [ -f "ccpm/ccpm.config" ]; then + # shellcheck disable=SC1091 + source ccpm/ccpm.config +fi +repo="${GH_REPO:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}"
45-55: Use head -1 when extracting name/status to avoid multi-line matchesEnsure single values in mapping.
- task_name=$(grep '^name:' "$task_file" | sed 's/^name: *//') - task_status=$(grep '^status:' "$task_file" | sed 's/^status: *//') + task_name=$(grep '^name:' "$task_file" | head -1 | sed 's/^name: *//') + task_status=$(grep '^status:' "$task_file" | head -1 | sed 's/^status: *//')ccpm/scripts/pm/epic-merge/execute-merge.sh (1)
48-56: Use merge command status directly; avoid $?Simplify and ensure proper failure handling.
echo "Merging epic/$ARGUMENTS to main..." -git merge "epic/$ARGUMENTS" --no-ff -m "$commit_message" - -if [ $? -eq 0 ]; then +if git merge "epic/$ARGUMENTS" --no-ff -m "$commit_message"; then echo "✅ Merge completed successfully" else echo "❌ Merge failed - conflicts detected" exit 1 ficcpm/scripts/pm/blocked.sh (1)
25-37: Reuse dependencies/frontmatter libs for correctness and DRYParsing YAML arrays manually is fragile (multi-line arrays, quotes). Prefer ccpm/lib/dependencies.sh and ccpm/lib/frontmatter.sh helpers.
-# Extract dependencies from task file -deps_line=$(grep "^depends_on:" "$task_file" | head -1) -if [ -n "$deps_line" ]; then - deps=$(echo "$deps_line" | sed 's/^depends_on: *//') - deps=$(echo "$deps" | sed 's/^\[//' | sed 's/\]$//') - deps=$(echo "$deps" | sed 's/,/ /g') - # Trim whitespace and handle empty cases - deps=$(echo "$deps" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') - [ -z "$deps" ] && deps="" -else - deps="" -fi +# shellcheck disable=SC1091 +source ccpm/lib/dependencies.sh +deps="$(get_task_dependencies_space_separated "$task_file")"ccpm/scripts/pm/epic-sync/sync-github-issues.sh (4)
1-8: Enable pipefail and default USE_SUBISSUESPipefail catches pipeline errors; default USE_SUBISSUES to false.
#!/bin/bash # Update GitHub issues from local data -set -e +set -euo pipefail @@ -USE_SUBISSUES="$3" +USE_SUBISSUES="${3:-false}"
86-93: Reuse REPO variable; avoid extra gh call and ensure stabilityAvoid unnecessary gh repo view; use provided REPO.
- repo=$(gh repo view --json nameWithOwner -q .nameWithOwner) - epic_url="https://github.com/$repo/issues/$new_epic_number" + epic_url="https://github.com/$REPO/issues/$new_epic_number"
115-121: Guard sub-issues command; provide fallback if extension is missinggh sub-issue may not be available. Detect and fallback to gh issue create.
- if [ "$USE_SUBISSUES" = "true" ] && [ -n "$current_epic_number" ]; then - task_number=$(gh sub-issue create \ + if [ "$USE_SUBISSUES" = "true" ] && [ -n "$current_epic_number" ] && command -v gh >/dev/null 2>&1 && gh sub-issue --help >/dev/null 2>&1; then + task_number=$(gh sub-issue create \ --parent "$current_epic_number" \ --title "$task_title" \ --body-file /tmp/epic-sync/task-body.md \ --label "task,epic:$EPIC_NAME" \ --json number -q .number) else
131-141: Reuse REPO for URLs and ensure keys exist in frontmatterAvoid gh call and ensure frontmatter gets updated even if keys missing.
- repo=$(gh repo view --json nameWithOwner -q .nameWithOwner) - task_url="https://github.com/$repo/issues/$task_number" + task_url="https://github.com/$REPO/issues/$task_number" @@ - sed "/^github:/c\\github: $task_url" "$task_file" | \ - sed "/^updated:/c\\updated: $current_date" > "$new_task_file" + if grep -q '^github:' "$task_file"; then + tmpfm=$(sed "/^github:/c\\github: $task_url" "$task_file") + else + tmpfm=$(awk -v url="$task_url" '1; NR==1{print ""} /^---$/ && ++c==2{print "github: " url}' "$task_file") + fi + if printf "%s" "$tmpfm" | grep -q '^updated:'; then + printf "%s" "$tmpfm" | sed "/^updated:/c\\updated: $current_date" > "$new_task_file" + else + printf "%s\nupdated: %s\n" "$tmpfm" "$current_date" > "$new_task_file" + ficcpm/scripts/pm/epic-sync/plan-sync-actions.sh (1)
83-83: Remove unused variables.The variables
file_typeandfile_pathare assigned but never used.Apply this fix:
-grep '^task:' /tmp/epic-sync/local-files.txt 2>/dev/null | while IFS=: read file_type file_id file_path local_updated local_status github_issue; do +grep '^task:' /tmp/epic-sync/local-files.txt 2>/dev/null | while IFS=: read _ file_id _ local_updated local_status github_issue; doAlso applies to: 83-83
ccpm/scripts/pm/epic-sync/post-progress-reports.sh (2)
30-30: Remove unused variable.The
actionvariable is assigned but never used in the while loop.Apply this fix:
-grep "^check_status:" /tmp/epic-sync/sync-actions.txt 2>/dev/null | while IFS=: read action item_type issue_num local_status; do +grep "^check_status:" /tmp/epic-sync/sync-actions.txt 2>/dev/null | while IFS=: read _ item_type issue_num local_status; do
75-77: Consider using library function for frontmatter updates.Since
ccpm/lib/frontmatter.shprovidesupdate_frontmatter_field(), consider using it for consistency.Replace the manual sed operations:
- sed -i.bak "/^progress:/c\\progress: ${progress}%" "$epic_file" - sed -i.bak "/^updated:/c\\updated: $current_date" "$epic_file" - rm "${epic_file}.bak" + # Source frontmatter library if not already loaded + if ! type update_frontmatter_field >/dev/null 2>&1; then + script_dir="$(dirname "${BASH_SOURCE[0]}")" + source "$script_dir/../../lib/frontmatter.sh" + fi + + update_frontmatter_field "$epic_file" "progress" "${progress}%" + update_frontmatter_field "$epic_file" "updated" "$current_date"ccpm/scripts/pm/epic-sync/sync-local-files.sh (3)
47-49: Use printf to preserve exact body content (avoid echo pitfalls)
echocan mangle leading dashes, backslashes, and newlines. Use printf.- echo "" >> /tmp/epic-sync/new-epic.md - echo "$gh_body" >> /tmp/epic-sync/new-epic.md + printf "\n%s\n" "$gh_body" >> /tmp/epic-sync/new-epic.md ... - echo "" >> /tmp/epic-sync/new-task.md - echo "$gh_body" >> /tmp/epic-sync/new-task.md + printf "\n%s\n" "$gh_body" >> /tmp/epic-sync/new-task.mdAlso applies to: 95-97
15-16: Avoid unused ‘action’ var from read; parse into throwawayShellCheck SC2034: ‘action’ is never used. Avoid assigning.
-grep "^update_local:" /tmp/epic-sync/sync-actions.txt 2>/dev/null | while IFS=: read action item_type issue_num; do +grep "^update_local:" /tmp/epic-sync/sync-actions.txt 2>/dev/null | while IFS=: read -r _ item_type issue_num; do ... -grep "^create_local:" /tmp/epic-sync/sync-actions.txt 2>/dev/null | while IFS=: read action item_type issue_num; do +grep "^create_local:" /tmp/epic-sync/sync-actions.txt 2>/dev/null | while IFS=: read -r _ item_type issue_num; doAlso applies to: 107-108
3-3: Harden script mode and preflight checksUse
-u -o pipefail. Also verifyghauth early to fail fast.-set -e +set -euo pipefail ... -echo "📥 Syncing local files from GitHub..." +# Basic preflight +if ! gh auth status >/dev/null 2>&1; then + echo "❌ GitHub CLI not authenticated. Run: gh auth login" >&2 + exit 1 +fi +echo "📥 Syncing local files from GitHub..."Also applies to: 12-13
ccpm/lib/github.sh (1)
200-217: Generalize: remove eval in remaining helpers (comment/close/reopen/labels)The same injection risk exists in comment/close/reopen/add/remove label helpers. Refactor to argv arrays consistently.
Would you like me to push a full patch touching these functions to remove all evals and switch to mktemp where applicable?
Also applies to: 364-393, 204-214
ccpm/lib/error.sh (2)
121-123: SC2155: declare and assign separatelyMinor ShellCheck cleanliness; avoids masking return codes.
- local script_dir="$(dirname "${BASH_SOURCE[0]}")" + local script_dir + script_dir="$(dirname "${BASH_SOURCE[0]}")"
346-348: Trap expansion warning (SC2064)Current form expands variables at definition time. Either suppress with a note or wrap via a small shim function.
- trap "$cleanup_function" EXIT INT TERM + # shellcheck disable=SC2064 + trap "$cleanup_function" EXIT INT TERMccpm/scripts/pm/file-management.sh (1)
304-305: Remove unused variable
total_fixesis declared but never used.- local total_fixes=0ccpm/lib/discovery.sh (1)
21-28: Whitespace-safe iteration (dir/task names with spaces)Loops over command substitutions split on whitespace. Prefer find -print0 with while/read -d '' for robustness. Optional.
Would you like a follow-up patch to make all directory/task iterations null-delimited and whitespace-safe?
Also applies to: 222-238
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (51)
README.md(1 hunks)ccpm/agents/test-runner.md(1 hunks)ccpm/ccpm.config(1 hunks)ccpm/commands/pm/clean.md(2 hunks)ccpm/commands/pm/epic-close.md(2 hunks)ccpm/commands/pm/epic-merge.md(2 hunks)ccpm/commands/pm/epic-refresh.md(0 hunks)ccpm/commands/pm/epic-start.md(4 hunks)ccpm/commands/pm/epic-sync.md(1 hunks)ccpm/commands/pm/issue-close.md(2 hunks)ccpm/commands/pm/issue-sync.md(3 hunks)ccpm/lib/datetime.sh(1 hunks)ccpm/lib/dependencies.sh(1 hunks)ccpm/lib/discovery.sh(1 hunks)ccpm/lib/error.sh(1 hunks)ccpm/lib/frontmatter.sh(1 hunks)ccpm/lib/github.sh(1 hunks)ccpm/scripts/pm/blocked.sh(1 hunks)ccpm/scripts/pm/clean.sh(1 hunks)ccpm/scripts/pm/close-epic.sh(1 hunks)ccpm/scripts/pm/close-issue.sh(1 hunks)ccpm/scripts/pm/epic-list.sh(2 hunks)ccpm/scripts/pm/epic-merge/cleanup-worktree.sh(1 hunks)ccpm/scripts/pm/epic-merge/close-github-issues.sh(1 hunks)ccpm/scripts/pm/epic-merge/execute-merge.sh(1 hunks)ccpm/scripts/pm/epic-merge/handle-conflicts.sh(1 hunks)ccpm/scripts/pm/epic-merge/run-tests.sh(1 hunks)ccpm/scripts/pm/epic-merge/validate-worktree.sh(1 hunks)ccpm/scripts/pm/epic-start/check-analysis.sh(1 hunks)ccpm/scripts/pm/epic-start/create-execution-status.sh(1 hunks)ccpm/scripts/pm/epic-start/manage-branch.sh(1 hunks)ccpm/scripts/pm/epic-start/preflight-checks.sh(1 hunks)ccpm/scripts/pm/epic-start/setup-monitoring.sh(1 hunks)ccpm/scripts/pm/epic-sync/build-local-inventory.sh(1 hunks)ccpm/scripts/pm/epic-sync/fetch-github-issues.sh(1 hunks)ccpm/scripts/pm/epic-sync/plan-sync-actions.sh(1 hunks)ccpm/scripts/pm/epic-sync/post-progress-reports.sh(1 hunks)ccpm/scripts/pm/epic-sync/sync-github-issues.sh(1 hunks)ccpm/scripts/pm/epic-sync/sync-local-files.sh(1 hunks)ccpm/scripts/pm/epic-sync/worktree-and-mappings.sh(1 hunks)ccpm/scripts/pm/file-management.sh(1 hunks)ccpm/scripts/pm/help.sh(1 hunks)ccpm/scripts/pm/issue-sync/calculate-epic-progress.sh(1 hunks)ccpm/scripts/pm/issue-sync/check-repo-protection.sh(1 hunks)ccpm/scripts/pm/issue-sync/check-sync-timing.sh(1 hunks)ccpm/scripts/pm/issue-sync/post-comment.sh(1 hunks)ccpm/scripts/pm/issue-sync/preflight-validation.sh(1 hunks)ccpm/scripts/pm/issue-sync/update-frontmatter.sh(1 hunks)ccpm/scripts/pm/next.sh(1 hunks)ccpm/scripts/pm/standup.sh(1 hunks)ccpm/scripts/pm/validate.sh(2 hunks)
💤 Files with no reviewable changes (1)
- ccpm/commands/pm/epic-refresh.md
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: gsornsen
PR: automazeio/ccpm#545
File: .github/workflows/release-test.yml:54-65
Timestamp: 2025-08-28T00:16:38.208Z
Learning: In gsornsen's CCPM project, the GitHub Actions workflows deliberately install Git Bash and Chocolatey on Windows runners to normalize shell commands across all platforms (Windows, macOS, Linux), creating a unified bash environment that works consistently across the OS matrix.
📚 Learning: 2025-09-24T14:18:03.934Z
Learnt from: CR
PR: automazeio/ccpm#0
File: AGENTS.md:0-0
Timestamp: 2025-09-24T14:18:03.934Z
Learning: Integrate with the PM command system: /pm:issue-analyze identifies work streams, /pm:issue-start spawns parallel-worker, which then spawns sub-agents; results are consolidated back to the main thread
Applied to files:
ccpm/commands/pm/epic-start.mdREADME.md
🧬 Code graph analysis (10)
ccpm/lib/datetime.sh (1)
ccpm/lib/frontmatter.sh (1)
get_frontmatter_field(9-29)
ccpm/lib/dependencies.sh (1)
ccpm/lib/frontmatter.sh (2)
get_frontmatter_field(9-29)update_frontmatter_field(34-62)
ccpm/scripts/pm/clean.sh (4)
ccpm/lib/error.sh (8)
set_strict_mode(9-14)error_exit(19-25)validate_directory_structure(146-162)info(36-39)warning(29-32)find_task_file_for_issue(185-216)success(43-46)confirm(287-311)ccpm/lib/frontmatter.sh (1)
get_frontmatter_field(9-29)ccpm/lib/discovery.sh (1)
get_epic_task_count_by_status(77-100)ccpm/lib/datetime.sh (3)
get_file_age_days(201-237)get_current_iso_timestamp(9-11)format_timestamp_display(260-298)
ccpm/lib/discovery.sh (3)
ccpm/lib/frontmatter.sh (1)
get_frontmatter_field(9-29)ccpm/lib/dependencies.sh (3)
get_task_dependencies(13-54)are_dependencies_satisfied(85-119)has_dependencies(74-80)ccpm/lib/datetime.sh (2)
get_file_age_days(201-237)is_file_older_than_days(242-255)
ccpm/scripts/pm/close-epic.sh (5)
ccpm/lib/error.sh (8)
set_strict_mode(9-14)error_exit(19-25)validate_epic_name(69-88)validate_github_auth(118-130)info(36-39)confirm(287-311)success(43-46)warning(29-32)ccpm/lib/frontmatter.sh (2)
get_frontmatter_field(9-29)update_frontmatter_bulk(176-194)ccpm/lib/discovery.sh (3)
get_epic_task_files(46-58)get_epic_task_count(63-72)get_epic_progress(105-127)ccpm/lib/datetime.sh (3)
get_current_iso_timestamp(9-11)iso_to_timestamp(16-63)format_timestamp_display(260-298)ccpm/lib/github.sh (3)
extract_issue_number_from_url(303-313)close_github_issue(222-245)reopen_github_issue(250-276)
ccpm/scripts/pm/epic-merge/cleanup-worktree.sh (2)
ccpm/scripts/pm/close-epic.sh (1)
main(421-454)ccpm/scripts/pm/file-management.sh (1)
main(379-432)
ccpm/lib/error.sh (1)
ccpm/lib/github.sh (1)
check_github_auth(9-11)
ccpm/scripts/pm/close-issue.sh (5)
ccpm/lib/error.sh (8)
set_strict_mode(9-14)error_exit(19-25)validate_issue_number(167-180)validate_github_auth(118-130)info(36-39)find_task_file_for_issue(185-216)success(43-46)warning(29-32)ccpm/lib/discovery.sh (4)
find_epic_containing_issue(132-174)get_epic_progress(105-127)get_epic_task_count(63-72)get_epic_task_count_by_status(77-100)ccpm/lib/datetime.sh (2)
get_current_iso_timestamp(9-11)format_timestamp_display(260-298)ccpm/lib/frontmatter.sh (2)
update_frontmatter_bulk(176-194)get_frontmatter_field(9-29)ccpm/lib/github.sh (5)
close_github_issue(222-245)extract_issue_number_from_url(303-313)get_github_issue(169-185)update_github_issue(122-164)reopen_github_issue(250-276)
ccpm/scripts/pm/epic-sync/worktree-and-mappings.sh (1)
ccpm/scripts/pm/file-management.sh (1)
main(379-432)
ccpm/scripts/pm/file-management.sh (4)
ccpm/lib/error.sh (6)
set_strict_mode(9-14)error_exit(19-25)info(36-39)warning(29-32)success(43-46)confirm(287-311)ccpm/lib/frontmatter.sh (1)
get_frontmatter_field(9-29)ccpm/lib/github.sh (1)
extract_issue_number_from_url(303-313)ccpm/lib/dependencies.sh (2)
get_task_dependencies(13-54)update_task_dependencies(211-231)
🪛 Shellcheck (0.11.0)
ccpm/lib/datetime.sh
[warning] 212-212: Declare and assign separately to avoid masking return values.
(SC2155)
ccpm/scripts/pm/epic-sync/build-local-inventory.sh
[warning] 15-15: This redirection doesn't have a command. Move to its command (or use 'true' as no-op).
(SC2188)
ccpm/scripts/pm/epic-sync/sync-local-files.sh
[warning] 107-107: action appears unused. Verify use (or export if used externally).
(SC2034)
ccpm/scripts/pm/epic-merge/execute-merge.sh
[warning] 26-26: Use 'cd ... || exit' or 'cd ... || return' in case cd fails.
(SC2164)
ccpm/scripts/pm/epic-sync/plan-sync-actions.sh
[warning] 18-18: This redirection doesn't have a command. Move to its command (or use 'true' as no-op).
(SC2188)
[warning] 83-83: file_type appears unused. Verify use (or export if used externally).
(SC2034)
[warning] 83-83: file_path appears unused. Verify use (or export if used externally).
(SC2034)
ccpm/lib/discovery.sh
[warning] 21-21: epic_names appears unused. Verify use (or export if used externally).
(SC2034)
[warning] 250-250: matching_tasks appears unused. Verify use (or export if used externally).
(SC2034)
ccpm/scripts/pm/epic-sync/sync-github-issues.sh
[warning] 17-17: epic_number appears unused. Verify use (or export if used externally).
(SC2034)
[warning] 59-59: action appears unused. Verify use (or export if used externally).
(SC2034)
[warning] 59-59: local_status appears unused. Verify use (or export if used externally).
(SC2034)
ccpm/scripts/pm/epic-sync/fetch-github-issues.sh
[warning] 17-17: This redirection doesn't have a command. Move to its command (or use 'true' as no-op).
(SC2188)
ccpm/scripts/pm/epic-merge/run-tests.sh
[error] 13-13: -f doesn't work with globs. Use a for loop.
(SC2144)
[error] 13-13: -f doesn't work with globs. Use a for loop.
(SC2144)
ccpm/lib/error.sh
[warning] 121-121: Declare and assign separately to avoid masking return values.
(SC2155)
[warning] 347-347: Use single quotes, otherwise this expands now rather than when signalled.
(SC2064)
ccpm/scripts/pm/file-management.sh
[warning] 304-304: total_fixes appears unused. Verify use (or export if used externally).
(SC2034)
[warning] 343-343: Declare and assign separately to avoid masking return values.
(SC2155)
ccpm/scripts/pm/epic-sync/post-progress-reports.sh
[warning] 30-30: action appears unused. Verify use (or export if used externally).
(SC2034)
🪛 markdownlint-cli2 (0.18.1)
ccpm/commands/pm/epic-sync.md
10-10: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
ccpm/commands/pm/epic-start.md
16-16: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3
(MD001, heading-increment)
🔇 Additional comments (44)
ccpm/agents/test-runner.md (1)
9-9: Updated role description reads cleanThe streamlined wording keeps the agent mandate clear and consistent. Looks good.
ccpm/scripts/pm/help.sh (1)
32-34: Spacing/readability improvement looks good.The added blank line before epic-start improves readability.
ccpm/scripts/pm/epic-start/check-analysis.sh (1)
14-20: LGTM: safe, quoted path check for analysis file.Good use of quotes; clear messages and exit behavior.
ccpm/commands/pm/issue-close.md (1)
27-34: Clear, end-to-end behavior description.Good coverage of local updates, GitHub sync, and progress recalculation.
Confirm close-issue.sh emits non-zero on any failure so docs’ guarantees hold atomically.
README.md (1)
218-219: README epic-sync wording aligned. Cross-check other spots for consistency.This change matches Epic Sync 2.0. Ensure the “GitHub Synchronization” section still describing “Pushes epic and tasks to GitHub” (Lines 185–192) is updated to reflect bidirectional sync.
ccpm/scripts/pm/validate.sh (1)
68-70: Good: success banner gated on zero warnings/errorsCondition is clear and side‑effect free.
ccpm/scripts/pm/issue-sync/update-frontmatter.sh (1)
46-46: Use sanitized completion when writing frontmatter- echo "completion: ${COMPLETION}%" + echo "completion: ${COMPLETION}%"Note: This line remains but now uses sanitized COMPACTION from above.
ccpm/ccpm.config (2)
14-22: URL normalization: good coverage of HTTPS/SSH/SCP stylesCovers common git remote forms and strips .git. Looks solid.
If you also expect git://github.com/owner/repo or http:// URLs, consider adding those prefixes similarly.
47-55: Optional: make repo validation opt-out faster for CIValidation already guarded by CCPM_SKIP_REPO_VALIDATION. LGTM.
ccpm/scripts/pm/standup.sh (1)
54-60: Glob fix is correctSwitch to "$epic_dir"/[0-9]*.md avoids accidental concatenation. Good change.
ccpm/commands/pm/epic-merge.md (1)
29-38: Docs shift to script-driven flow looks goodClear step-wise invocations with dedicated scripts. LGTM.
ccpm/scripts/pm/blocked.sh (1)
16-23: Glob/path and explicit conditional look goodSafer glob and clearer conditional. LGTM.
ccpm/commands/pm/epic-sync.md (4)
5-5: LGTM: Clear title upgrade to Epic Sync 2.0.The title clearly indicates this is a major version upgrade with bidirectional synchronization capabilities.
34-46: Excellent repository protection check.This safety mechanism prevents accidental modifications to the template repository, which is critical for maintaining the integrity of the CCPM template.
75-222: Well-architected modular script approach.The refactoring from inline bash logic to modular scripts provides several benefits:
- Better maintainability and testability
- Clear separation of concerns
- Reusable components across commands
- Improved debugging capabilities
The sequential script execution follows a logical flow from fetch → build → plan → execute → report.
134-180: Comprehensive cleanup and summary reporting.The final cleanup section provides excellent visibility into the sync operation results, including detailed statistics and next steps for the user.
ccpm/commands/pm/issue-sync.md (4)
24-29: Excellent modular preflight approach.The refactoring to use dedicated scripts for preflight checks improves maintainability and allows for better error handling and reusability across commands.
105-106: Good script-driven comment posting.Using a dedicated script for posting comments provides better error handling and standardized formatting.
109-112: Consistent frontmatter management.The use of a dedicated script for frontmatter updates ensures consistent handling across the system.
245-248: Good modular epic progress calculation.Moving epic progress calculation to a dedicated script promotes reusability and easier testing.
ccpm/scripts/pm/epic-sync/fetch-github-issues.sh (3)
1-11: LGTM: Good script structure and input validation.The script has proper error handling with
set -eand validates required parameters with helpful usage information.
19-37: Well-structured epic issue handling.The logic properly handles both existing and missing epic issues, with appropriate error messages and fallback behavior.
39-51: Comprehensive task issue fetching.The search uses appropriate GitHub labels and state filters to capture all relevant task issues for the epic.
ccpm/lib/datetime.sh (4)
6-11: LGTM: Simple and effective UTC timestamp function.The function correctly generates ISO 8601 timestamps in UTC format.
24-63: Excellent cross-platform date handling.Based on the web search results, date command behaves differently between Linux and macOS, with the -d option displaying time as described by a string on Linux GNU version while used to set daylight saving time on macOS. Your implementation handles both GNU date (Linux) and macOS date formats with appropriate fallbacks.
210-214: Smart lazy loading of dependencies.The conditional sourcing of frontmatter.sh only when needed is an excellent performance optimization that avoids unnecessary dependencies.
168-196: Comprehensive relative time formatting.The function handles future dates, proper pluralization, and converts to appropriate time units. This provides excellent user experience for displaying time differences.
ccpm/commands/pm/epic-start.md (4)
16-19: Good modular preflight approach.The consolidation of preflight checks into a single script improves maintainability and provides consistent validation across the epic start workflow.
28-29: Centralized branch management.Moving branch operations to a dedicated script ensures consistent git operations and better error handling.
48-50: Systematic issue analysis checking.The script-driven approach to checking analysis ensures consistent behavior and better error reporting.
96-99: Well-structured monitoring and status management.The dedicated scripts for execution status and monitoring setup provide better organization and reusability.
Also applies to: 105-106
ccpm/scripts/pm/epic-sync/plan-sync-actions.sh (3)
1-11: LGTM: Good script foundation.Proper error handling with
set -eand input validation with usage message.
26-28: Excellent cross-platform timestamp handling.The timestamp conversion logic handles both GNU date (Linux) and macOS date formats, providing robust cross-platform compatibility as evidenced by the web search results showing different date options between macOS and Linux.
90-107: Comprehensive sync planning summary.The detailed breakdown of planned actions provides excellent visibility into what the sync operation will accomplish.
ccpm/lib/frontmatter.sh (5)
6-29: Robust field extraction with defaults.The function properly handles missing files and fields, with flexible default value handling.
47-51: Good cross-platform sed handling.The conditional logic properly handles the differences between macOS and Linux sed implementations, ensuring compatibility across platforms.
Also applies to: 54-58
84-109: Comprehensive frontmatter validation.The validation function checks for both frontmatter existence and required fields, providing clear error messages.
122-137: Well-structured AWK parsing.The AWK script properly extracts frontmatter sections and converts to key=value format with appropriate field handling.
173-194: Efficient bulk operations.The bulk update function provides an efficient way to update multiple frontmatter fields while reusing the single-field update logic.
ccpm/scripts/pm/epic-sync/post-progress-reports.sh (5)
1-11: LGTM: Standard script structure.Proper error handling and input validation are in place.
14-27: Good progress calculation and persistence.The progress calculation logic is sound, and saving results to temp files allows other scripts to access the computed values.
48-63: Intelligent issue state management.The logic properly handles both closing completed tasks and reopening tasks that were closed on GitHub but are still open locally, with informative comments.
85-95: Dynamic task status generation.The inline command substitution creates a comprehensive task status list, though it's complex. The logic properly handles missing files and extracts task metadata.
104-112: Smart epic completion handling.The logic to close the epic when 100% complete is excellent, with proper state checking to avoid redundant operations.
| 5. Archive completed epics to `.claude/epics/.archived/` | ||
| 6. Remove stale files and empty directories | ||
| 7. Create detailed archive log |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Archive path inconsistent with scripts (.archived vs archived)
Docs say .claude/epics/.archived/ but scripts (e.g., cleanup-worktree.sh) use .claude/epics/archived/. Align to avoid confusion.
-5. Archive completed epics to `.claude/epics/.archived/`
+5. Archive completed epics to `.claude/epics/archived/`Run to confirm all archive path usages and update docs/scripts uniformly:
🏁 Script executed:
#!/bin/bash
rg -n -C1 -e '\.claude/epics/\.archived/' -e '\.claude/epics/archived/' ccpm | sed 's/^/HIT: /'Length of output: 1517
Unify archive directory naming to .claude/epics/archived/
- ccpm/commands/pm/clean.md line 31: change
.claude/epics/.archived/→.claude/epics/archived/ - ccpm/scripts/pm/close-epic.sh line 352: change
to
local archived_path=".claude/epics/.archived/$epic_name"
local archived_path=".claude/epics/archived/$epic_name"
|
|
||
| # Source frontmatter library if not already loaded | ||
| if ! type get_frontmatter_field >/dev/null 2>&1; then | ||
| local script_dir="$(dirname "${BASH_SOURCE[0]}")" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix variable declaration to avoid masking return values.
Declare and assign the variable separately to avoid masking the return value of the command substitution.
Apply this fix:
- local script_dir="$(dirname "${BASH_SOURCE[0]}")"
+ local script_dir
+ script_dir="$(dirname "${BASH_SOURCE[0]}")"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| local script_dir="$(dirname "${BASH_SOURCE[0]}")" | |
| local script_dir | |
| script_dir="$(dirname "${BASH_SOURCE[0]}")" |
🧰 Tools
🪛 Shellcheck (0.11.0)
[warning] 212-212: Declare and assign separately to avoid masking return values.
(SC2155)
🤖 Prompt for AI Agents
In ccpm/lib/datetime.sh around line 212, the current single-line declaration
local script_dir="$(dirname "${BASH_SOURCE[0]}")" masks the command
substitution's return value; fix it by declaring the variable and assigning its
value in two steps: first declare the local variable (local script_dir) on its
own line, then assign script_dir="$(dirname "${BASH_SOURCE[0]}")" on the next
line so the command's exit status is preserved.
| if echo "$current_deps" | grep -q "$new_dep"; then | ||
| return 0 # Already exists, nothing to do | ||
| fi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Boundary-safe dependency existence check
Plain grep -q "$new_dep" can false‑match substrings (e.g., “12” matches “112”). Use word-ish boundaries for the space‑separated list.
- if echo "$current_deps" | grep -q "$new_dep"; then
+ if printf '%s\n' $current_deps | grep -q -E "^${new_dep}$"; then
return 0 # Already exists, nothing to do
fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if echo "$current_deps" | grep -q "$new_dep"; then | |
| return 0 # Already exists, nothing to do | |
| fi | |
| if printf '%s\n' $current_deps | grep -q -E "^${new_dep}$"; then | |
| return 0 # Already exists, nothing to do | |
| fi |
🤖 Prompt for AI Agents
In ccpm/lib/dependencies.sh around lines 249 to 251, the current existence check
uses plain grep -q "$new_dep" which can false-match substrings; change it to
check whole "words" by padding the list and using fixed-string search — e.g.
echo " $current_deps " | grep -qF " $new_dep " — so only space-delimited matches
succeed (ensure variables remain quoted).
| # Remove the dependency from the list | ||
| local new_deps | ||
| new_deps=$(echo "$current_deps" | sed "s/\\b${dep_to_remove}\\b//g" | tr -s ' ' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') | ||
|
|
||
| update_task_dependencies "$file" "$new_deps" | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Robust removal of a single dependency token
The current sed with \b isn’t portable and can remove substrings. Rebuild the list excluding the target token.
- new_deps=$(echo "$current_deps" | sed "s/\\b${dep_to_remove}\\b//g" | tr -s ' ' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
+ new_deps=$(printf '%s\n' $current_deps | awk -v id="$dep_to_remove" '$0!=id' | paste -sd' ' -)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Remove the dependency from the list | |
| local new_deps | |
| new_deps=$(echo "$current_deps" | sed "s/\\b${dep_to_remove}\\b//g" | tr -s ' ' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') | |
| update_task_dependencies "$file" "$new_deps" | |
| # Remove the dependency from the list | |
| local new_deps | |
| new_deps=$(printf '%s\n' $current_deps | awk -v id="$dep_to_remove" '$0!=id' | paste -sd' ' -) | |
| update_task_dependencies "$file" "$new_deps" |
🤖 Prompt for AI Agents
In ccpm/lib/dependencies.sh around lines 278 to 283, the current sed using \b is
non‑portable and can remove substrings; instead split current_deps into tokens
and rebuild new_deps by iterating/filtering out exactly the dep_to_remove token
(e.g., read into an array, for each token append to a new list only if it !=
dep_to_remove), then trim/normalize whitespace and pass that exact-token-safe
new_deps to update_task_dependencies.
| for task_file in "$epic_dir"/[0-9]*.md; do | ||
| [ -f "$task_file" ] && echo "$task_file" | ||
| done | sort | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Include closed/ subdirectory in task discovery (if used by file-management)
If closed tasks are moved under closed/, they won’t be counted. Expand the search paths.
- for task_file in "$epic_dir"/[0-9]*.md; do
+ for task_file in "$epic_dir"/[0-9]*.md "$epic_dir"/closed/[0-9]*.md; do
[ -f "$task_file" ] && echo "$task_file"
done | sort📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for task_file in "$epic_dir"/[0-9]*.md; do | |
| [ -f "$task_file" ] && echo "$task_file" | |
| done | sort | |
| } | |
| for task_file in "$epic_dir"/[0-9]*.md "$epic_dir"/closed/[0-9]*.md; do | |
| [ -f "$task_file" ] && echo "$task_file" | |
| done | sort | |
| } |
🤖 Prompt for AI Agents
In ccpm/lib/discovery.sh around lines 55 to 58, the task discovery loop only
searches "$epic_dir"/[0-9]*.md so tasks moved into a closed/ subdirectory are
ignored; update the loop to iterate over both "$epic_dir"/[0-9]*.md and
"$epic_dir"/closed/[0-9]*.md (or a combined glob) and keep the existing [ -f
"$task_file" ] guard so files in closed/ are emitted and then sorted.
| SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" | ||
| LIB_DIR="$(dirname "$SCRIPT_DIR")/lib" | ||
|
|
||
| source "$LIB_DIR/error.sh" | ||
| source "$LIB_DIR/frontmatter.sh" | ||
| source "$LIB_DIR/datetime.sh" | ||
| source "$LIB_DIR/discovery.sh" | ||
| source "$LIB_DIR/github.sh" | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incorrect LIB_DIR path; libraries won’t load
Same path bug across PM scripts.
-SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
-LIB_DIR="$(dirname "$SCRIPT_DIR")/lib"
+SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
+ROOT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
+LIB_DIR="$ROOT_DIR/lib"🤖 Prompt for AI Agents
In ccpm/scripts/pm/file-management.sh around lines 7 to 15 the LIB_DIR is
computed incorrectly (using dirname on SCRIPT_DIR) so it points to the wrong
directory and the library files fail to load; change the LIB_DIR assignment to
reference the lib directory relative to SCRIPT_DIR (e.g., set LIB_DIR to
"$SCRIPT_DIR/../lib" or equivalent path resolution) and update the other PM
scripts with the same fix so the subsequent source "$LIB_DIR/..." lines load the
correct files.
| if echo "$deps" | grep -q "\\b$old_id\\b"; then | ||
| # Replace old ID with new ID in dependencies | ||
| local new_deps | ||
| new_deps=$(echo "$deps" | sed "s/\\b$old_id\\b/$new_id/g") | ||
|
|
||
| update_task_dependencies "$task_file" "$new_deps" | ||
| info "Updated dependencies in $(basename "$task_file"): $old_id → $new_id" | ||
| fi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Word-boundary matching for dependency IDs; avoid non-portable \b
Use a token-aware approach to detect and rewrite dependency IDs.
- if echo "$deps" | grep -q "\\b$old_id\\b"; then
- # Replace old ID with new ID in dependencies
- local new_deps
- new_deps=$(echo "$deps" | sed "s/\\b$old_id\\b/$new_id/g")
+ if printf '%s\n' $deps | grep -q -E "^${old_id}$"; then
+ # Replace old ID with new ID in dependencies (token-aware)
+ local new_deps
+ new_deps=$(printf '%s\n' $deps | awk -v old="$old_id" -v new="$new_id" '{for(i=1;i<=NF;i++){if($i==old){$i=new}} print}' | paste -sd' ' -)
update_task_dependencies "$task_file" "$new_deps"
info "Updated dependencies in $(basename "$task_file"): $old_id → $new_id"
fiAlso applies to: 120-122
| for task_file in "$epic_dir"/[0-9]*.md; do | ||
| [ -f "$task_file" ] || continue | ||
|
|
||
| local task_status | ||
| task_status=$(get_frontmatter_field "$task_file" "status" "open") | ||
|
|
||
| if [ "$task_status" = "closed" ]; then | ||
| local filename | ||
| filename=$(basename "$task_file") | ||
| local target_path="$closed_dir/$filename" | ||
|
|
||
| if [ "$dry_run" = true ]; then | ||
| echo "Would move: $task_file → $target_path" | ||
| else | ||
| mv "$task_file" "$target_path" | ||
| success "Moved closed task: $filename → closed/" | ||
| fi | ||
|
|
||
| moved_count=$((moved_count + 1)) | ||
| fi | ||
| done | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❓ Verification inconclusive
Relocating closed tasks breaks discovery/progress (not recursive)
Discovery functions (e.g., get_epic_task_files) only scan "$epic_dir"/[0-9]*.md. Moving closed tasks to a subdirectory will hide them from counts/progress.
Either:
- Don’t move files; or
- Update discovery.sh to include "$epic_dir/closed" (and all scripts that assume a flat layout).
Do you want me to patch discovery.sh to include closed/ paths?
Include closed/ in discovery patterns
get_epic_task_files only globs "$epic_dir"/[0-9]*.md, so any files moved into closed/ will be ignored. Either keep closed tasks in the root or update ccpm/scripts/pm/discovery.sh (and related scripts) to also scan "$epic_dir/closed"/[0-9]*.md. Let me know if you’d like a patch.
| # Default completion to 0 if not provided | ||
| if [ -z "$COMPLETION" ]; then | ||
| COMPLETION="0" | ||
| fi | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sanitize completion input to numeric 0–100 and normalize percent rendering
Handles inputs like “75%” and prevents “%%”.
-# Default completion to 0 if not provided
-if [ -z "$COMPLETION" ]; then
- COMPLETION="0"
-fi
+# Default completion and sanitize to 0–100 integer
+raw_completion="${COMPLETION:-0}"
+completion_num="$(printf '%s' "$raw_completion" | tr -d '%' | sed -E 's/[^0-9]//g')"
+[ -z "$completion_num" ] && completion_num=0
+if [ "$completion_num" -gt 100 ]; then completion_num=100; fi
+if [ "$completion_num" -lt 0 ]; then completion_num=0; fi
+COMPLETION="$completion_num"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Default completion to 0 if not provided | |
| if [ -z "$COMPLETION" ]; then | |
| COMPLETION="0" | |
| fi | |
| # Default completion and sanitize to 0–100 integer | |
| raw_completion="${COMPLETION:-0}" | |
| completion_num="$(printf '%s' "$raw_completion" | tr -d '%' | sed -E 's/[^0-9]//g')" | |
| [ -z "$completion_num" ] && completion_num=0 | |
| if [ "$completion_num" -gt 100 ]; then completion_num=100; fi | |
| if [ "$completion_num" -lt 0 ]; then completion_num=0; fi | |
| COMPLETION="$completion_num" |
| # Extract dependencies from task file | ||
| deps_line=$(grep "^depends_on:" "$task_file" | head -1) | ||
| if [ -n "$deps_line" ]; then | ||
| deps=$(echo "$deps_line" | sed 's/^depends_on: *//') | ||
| deps=$(echo "$deps" | sed 's/^\[//' | sed 's/\]$//') | ||
| # Trim whitespace and handle empty cases | ||
| deps=$(echo "$deps" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') | ||
| [ -z "$deps" ] && deps="" | ||
| else | ||
| deps="" | ||
| fi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dependency parsing: remove dead check and actually validate closed deps
- The check for deps == "depends_on:" is unreachable after stripping the prefix.
- To match the header comment, verify that listed deps are closed before marking task as ready. Prefer using the new dependencies lib.
- # If no dependencies or empty, task is available
- if [ -z "$deps" ] || [ "$deps" = "depends_on:" ]; then
+ # If no dependencies or all dependencies are closed, task is available
+ if [ -z "$deps" ]; then
task_name=$(grep "^name:" "$task_file" | head -1 | sed 's/^name: *//')
task_num=$(basename "$task_file" .md)
parallel=$(grep "^parallel:" "$task_file" | head -1 | sed 's/^parallel: *//')
@@
- ((found++))
- fi
+ ((found++))
+ else
+ # Check each dependency's status
+ all_closed=1
+ IFS=',' read -r -a dep_arr <<< "$deps"
+ for dep in "${dep_arr[@]}"; do
+ dep="$(echo "$dep" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')"
+ dep_file="$epic_dir/${dep}.md"
+ if [ -f "$dep_file" ]; then
+ dep_status=$(grep -m1 "^status:" "$dep_file" | sed 's/^status: *//' | tr '[:upper:]' '[:lower:]')
+ [ "$dep_status" = "closed" ] || all_closed=0
+ else
+ all_closed=0
+ fi
+ done
+ if [ "$all_closed" -eq 1 ]; then
+ task_name=$(grep "^name:" "$task_file" | head -1 | sed 's/^name: *//')
+ task_num=$(basename "$task_file" .md)
+ parallel=$(grep "^parallel:" "$task_file" | head -1 | sed 's/^parallel: *//')
+ echo "✅ Ready: #$task_num - $task_name"
+ echo " Epic: $epic_name"
+ [ "$parallel" = "true" ] && echo " 🔄 Can run in parallel"
+ echo ""
+ ((found++))
+ fi
+ fiAlternatively, source ccpm/lib/dependencies.sh and use a helper if available.
Also applies to: 39-41
🤖 Prompt for AI Agents
In ccpm/scripts/pm/next.sh around lines 27 to 37 (and similarly 39 to 41),
remove the dead check for deps == "depends_on:" and replace the simplistic
string-only parsing with real dependency validation: source
ccpm/lib/dependencies.sh (or the new dependencies lib) and after extracting the
list ensure you call the library helper to verify each listed dependency is
closed before marking the task ready; if none of the listed deps are closed, set
deps to empty string. Ensure you strip brackets/whitespace as before, then call
the lib's function (e.g., deps_all_closed or equivalent) to filter/validate and
only keep closed deps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review continued from previous batch...
| # Build gh issue create command | ||
| local gh_cmd="gh issue create --title \"$title\"" | ||
|
|
||
| if [ -n "$body" ]; then | ||
| # Create temp file for body | ||
| local body_file="/tmp/gh_issue_body_$$" | ||
| echo "$body" > "$body_file" | ||
| gh_cmd="$gh_cmd --body-file \"$body_file\"" | ||
| fi | ||
|
|
||
| if [ -n "$labels" ]; then | ||
| gh_cmd="$gh_cmd --label \"$labels\"" | ||
| fi | ||
|
|
||
| if [ -n "$assignee" ]; then | ||
| gh_cmd="$gh_cmd --assignee \"$assignee\"" | ||
| fi | ||
|
|
||
| if [ -n "$repo" ]; then | ||
| gh_cmd="$gh_cmd --repo \"$repo\"" | ||
| fi | ||
|
|
||
| # Execute command and extract issue number | ||
| local result | ||
| result=$(eval "$gh_cmd" 2>/dev/null) | ||
|
|
||
| # Clean up temp file | ||
| [ -n "$body" ] && rm -f "$body_file" 2>/dev/null | ||
|
|
||
| if [ $? -eq 0 ] && [ -n "$result" ]; then | ||
| # Extract issue number from URL | ||
| echo "$result" | grep -oE '[0-9]+$' | ||
| return 0 | ||
| else | ||
| echo "" | ||
| return 1 | ||
| fi | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove eval; build argv array to prevent shell injection
Constructing a command string and using eval is unsafe. Build an array and call gh directly. Also prefer mktemp for temp files.
-create_github_issue() {
+create_github_issue() {
local title="$1"
local body="$2"
local labels="$3"
local assignee="$4"
local repo="${5:-}"
if [ -z "$title" ]; then
echo "Error: Issue title required" >&2
return 1
fi
- # Build gh issue create command
- local gh_cmd="gh issue create --title \"$title\""
+ # Build gh issue create command safely
+ local body_file=""
+ local cmd=(gh issue create --title "$title")
if [ -n "$body" ]; then
- # Create temp file for body
- local body_file="/tmp/gh_issue_body_$$"
- echo "$body" > "$body_file"
- gh_cmd="$gh_cmd --body-file \"$body_file\""
+ body_file="$(mktemp /tmp/gh_issue_body.XXXXXX)"
+ printf "%s" "$body" > "$body_file"
+ cmd+=(--body-file "$body_file")
fi
if [ -n "$labels" ]; then
- gh_cmd="$gh_cmd --label \"$labels\""
+ cmd+=(--label "$labels")
fi
if [ -n "$assignee" ]; then
- gh_cmd="$gh_cmd --assignee \"$assignee\""
+ cmd+=(--assignee "$assignee")
fi
if [ -n "$repo" ]; then
- gh_cmd="$gh_cmd --repo \"$repo\""
+ cmd+=(--repo "$repo")
fi
- # Execute command and extract issue number
- local result
- result=$(eval "$gh_cmd" 2>/dev/null)
+ # Execute command and extract issue number
+ local result
+ if ! result="$("${cmd[@]}" 2>/dev/null)"; then
+ [ -n "$body_file" ] && rm -f "$body_file"
+ echo ""
+ return 1
+ fi
# Clean up temp file
- [ -n "$body" ] && rm -f "$body_file" 2>/dev/null
+ [ -n "$body_file" ] && rm -f "$body_file" 2>/dev/null
- if [ $? -eq 0 ] && [ -n "$result" ]; then
+ if [ -n "$result" ]; then
# Extract issue number from URL
echo "$result" | grep -oE '[0-9]+$'
return 0
else
echo ""
return 1
fi
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Build gh issue create command | |
| local gh_cmd="gh issue create --title \"$title\"" | |
| if [ -n "$body" ]; then | |
| # Create temp file for body | |
| local body_file="/tmp/gh_issue_body_$$" | |
| echo "$body" > "$body_file" | |
| gh_cmd="$gh_cmd --body-file \"$body_file\"" | |
| fi | |
| if [ -n "$labels" ]; then | |
| gh_cmd="$gh_cmd --label \"$labels\"" | |
| fi | |
| if [ -n "$assignee" ]; then | |
| gh_cmd="$gh_cmd --assignee \"$assignee\"" | |
| fi | |
| if [ -n "$repo" ]; then | |
| gh_cmd="$gh_cmd --repo \"$repo\"" | |
| fi | |
| # Execute command and extract issue number | |
| local result | |
| result=$(eval "$gh_cmd" 2>/dev/null) | |
| # Clean up temp file | |
| [ -n "$body" ] && rm -f "$body_file" 2>/dev/null | |
| if [ $? -eq 0 ] && [ -n "$result" ]; then | |
| # Extract issue number from URL | |
| echo "$result" | grep -oE '[0-9]+$' | |
| return 0 | |
| else | |
| echo "" | |
| return 1 | |
| fi | |
| } | |
| create_github_issue() { | |
| local title="$1" | |
| local body="$2" | |
| local labels="$3" | |
| local assignee="$4" | |
| local repo="${5:-}" | |
| if [ -z "$title" ]; then | |
| echo "Error: Issue title required" >&2 | |
| return 1 | |
| fi | |
| # Build gh issue create command safely | |
| local body_file="" | |
| local cmd=(gh issue create --title "$title") | |
| if [ -n "$body" ]; then | |
| body_file="$(mktemp /tmp/gh_issue_body.XXXXXX)" | |
| printf "%s" "$body" > "$body_file" | |
| cmd+=(--body-file "$body_file") | |
| fi | |
| if [ -n "$labels" ]; then | |
| cmd+=(--label "$labels") | |
| fi | |
| if [ -n "$assignee" ]; then | |
| cmd+=(--assignee "$assignee") | |
| fi | |
| if [ -n "$repo" ]; then | |
| cmd+=(--repo "$repo") | |
| fi | |
| # Execute command and extract issue number | |
| local result | |
| if ! result="$("${cmd[@]}" 2>/dev/null)"; then | |
| [ -n "$body_file" ] && rm -f "$body_file" | |
| echo "" | |
| return 1 | |
| fi | |
| # Clean up temp file | |
| [ -n "$body_file" ] && rm -f "$body_file" 2>/dev/null | |
| if [ -n "$result" ]; then | |
| # Extract issue number from URL | |
| echo "$result" | grep -oE '[0-9]+$' | |
| return 0 | |
| else | |
| echo "" | |
| return 1 | |
| fi | |
| } |
🤖 Prompt for AI Agents
In ccpm/lib/github.sh around lines 80 to 117, the function builds a command
string and uses eval (unsafe) and creates a temp file insecurely; replace this
with a safe argv array and mktemp: build a bash array like args=(gh issue create
--title "$title") and conditionally append --body-file "$body_file", --label,
--assignee, --repo elements to the array instead of concatenating strings;
create the body temp file using mktemp and write $body to it when needed, run
the command directly via "${args[@]}" capturing stdout/stderr into a variable,
record the command exit status from the direct invocation (not from $?), ensure
the temp file is removed in a finally/cleanup path even on failure, and parse
the captured output to extract the issue number as before.
| SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" | ||
| LIB_DIR="$(dirname "$SCRIPT_DIR")/lib" | ||
|
|
||
| source "$LIB_DIR/error.sh" | ||
| source "$LIB_DIR/frontmatter.sh" | ||
| source "$LIB_DIR/datetime.sh" | ||
| source "$LIB_DIR/discovery.sh" | ||
| source "$LIB_DIR/github.sh" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix library path resolution – sourcing currently fails
LIB_DIR="$(dirname "$SCRIPT_DIR")/lib" resolves to ccpm/scripts/lib, but the shared libraries live in ccpm/lib. With set -e in effect, every source "$LIB_DIR/…" now terminates the script before any logic runs. Please repoint LIB_DIR to the actual ccpm/lib directory (e.g., SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" combined with LIB_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)/lib"), so the script can start.
Apply this diff to correct the path:
-SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
-LIB_DIR="$(dirname "$SCRIPT_DIR")/lib"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+LIB_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)/lib"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" | |
| LIB_DIR="$(dirname "$SCRIPT_DIR")/lib" | |
| source "$LIB_DIR/error.sh" | |
| source "$LIB_DIR/frontmatter.sh" | |
| source "$LIB_DIR/datetime.sh" | |
| source "$LIB_DIR/discovery.sh" | |
| source "$LIB_DIR/github.sh" | |
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| LIB_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)/lib" | |
| source "$LIB_DIR/error.sh" | |
| source "$LIB_DIR/frontmatter.sh" | |
| source "$LIB_DIR/datetime.sh" | |
| source "$LIB_DIR/discovery.sh" | |
| source "$LIB_DIR/github.sh" |
🤖 Prompt for AI Agents
In ccpm/scripts/pm/close-epic.sh around lines 7 to 14, LIB_DIR is computed as
"$(dirname "$SCRIPT_DIR")/lib" which resolves to ccpm/scripts/lib instead of the
shared ccpm/lib, causing all subsequent source calls to fail under set -e; fix
by making SCRIPT_DIR an absolute path (resolve BASH_SOURCE to its directory with
cd && pwd) and set LIB_DIR to the repo-level lib (cd from SCRIPT_DIR up two
levels and pwd)/lib so that source "$LIB_DIR/..." points to ccpm/lib and the
script can successfully load the shared libraries.
|
Would it be possible to move all the CCPM-related files to |
|
does this fix the install? |
|
are some of the files meant to live in ~/.claude/... and not just the project directory/.claude ? because if you split into a git worktree and your .claude folder is .gitignored it loses access to the commands, scripts, etc. |
Summary
This PR delivers a comprehensive architectural refactoring of the CCPM bash script system, addressing code duplication, improving maintainability, and creating a modular, reusable foundation.
🎯 Key Achievements
📚 New Utility Libraries (
ccpm/lib/)frontmatter.sh- YAML Operationsget_frontmatter_field(),update_frontmatter_field(),validate_frontmatter()dependencies.sh- Task Dependency Managementget_task_dependencies(),are_dependencies_satisfied(),validate_task_dependencies()datetime.sh- Cross-Platform Timestamp Handlingget_current_iso_timestamp(),iso_to_timestamp(),get_relative_time()github.sh- GitHub CLI Operationscreate_github_issue(),close_github_issue(),post_github_comment()error.sh- Validation & Error Handlingvalidate_epic_name(),validate_github_auth(),find_task_file_for_issue()discovery.sh- Epic/Task Navigationget_epic_progress(),find_available_tasks(),find_blocked_tasks()🔧 New Operation Scripts (
ccpm/scripts/pm/)clean.sh- System Cleanup Automationfile-management.sh- File Organization & Renamingrename_by_issue,organize_by_status,fix_inconsistenciesclose-issue.sh- Complete Issue Managementclose-epic.sh- Epic Completion Management📝 Command Simplifications
clean.mdissue-close.mdepic-close.mdAll commands now simply call the appropriate scripts instead of containing complex inline logic.
🏗️ Architectural Improvements
Before
After
✅ Quality Assurance
bash -n)🚀 Impact & Benefits
For Developers
For Users
📋 Files Changed Summary
epic-refresh.md(functionality consolidated intoepic-sync.md)🧪 Test Plan
This refactoring provides a solid foundation for future development while maintaining full backward compatibility with existing workflows.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation