claude-autopm 3.25.3 → 3.25.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/.github/review-context.md +11 -0
  2. package/.github/workflows/copilot-review-required.yml +128 -0
  3. package/.github/workflows/deploy-docs.yml +1 -1
  4. package/.github/workflows/enforce-main-source.yml +24 -0
  5. package/autopm/.claude/commands/pm:epic-sync-original.md +3 -3
  6. package/autopm/.claude/commands/pm:issue-start.md +104 -57
  7. package/autopm/.claude/rules/frontmatter-operations.md +10 -3
  8. package/autopm/.claude/scripts/lib/frontmatter-utils.sh +17 -2
  9. package/autopm/.claude/scripts/lib/github-utils.sh +19 -3
  10. package/lib/cli/commands/obsidian.js +45 -0
  11. package/lib/plugins/PluginManager.js +20 -6
  12. package/package.json +6 -6
  13. package/packages/plugin-core/package.json +1 -1
  14. package/packages/plugin-core/rules/strip-frontmatter.md +59 -24
  15. package/packages/plugin-core/scripts/lib/frontmatter-utils.sh +17 -2
  16. package/packages/plugin-core/scripts/lib/github-utils.sh +19 -3
  17. package/packages/plugin-obsidian/claude-md/obsidian-section.md +14 -0
  18. package/packages/plugin-obsidian/commands/obsidian:init.md +25 -3
  19. package/packages/plugin-obsidian/commands/obsidian:link.md +53 -0
  20. package/packages/plugin-obsidian/plugin.json +9 -0
  21. package/packages/plugin-obsidian/scripts/obsidian/link-vault.js +234 -0
  22. package/packages/plugin-pm/package.json +1 -1
  23. package/packages/plugin-pm/scripts/pm/epic-sync/create-epic-issue.sh +5 -4
  24. package/packages/plugin-pm/scripts/pm/epic-sync/create-task-issues.sh +2 -1
  25. package/packages/plugin-pm-github/commands/pm:epic-sync-original.md +3 -3
  26. package/packages/plugin-pm-github/commands/pm:import.md +30 -3
  27. package/packages/plugin-pm-github/commands/pm:issue-start.md +10 -1
  28. package/packages/plugin-pm-github/package.json +1 -1
@@ -0,0 +1,11 @@
1
+ # PROJECT_CONTEXT_BRIEF — ClaudeAutoPM
2
+
3
+ ## Stack
4
+ - Node.js / JavaScript CLI tool (npm package: project-management automation). Shell scripts. Some Python + TypeScript.
5
+
6
+ ## OUT OF SCOPE (do not flag)
7
+ - Style nits (linters/prettier enforce). Pre-existing tech debt outside the diff hunk. Doc/markdown wording.
8
+
9
+ ## Severity
10
+ - HIGH: data loss, security (esp. arbitrary command exec in a CLI), broken contract, crash on happy path.
11
+ - MEDIUM: edge-case bug, missing test for a new code path. LOW: maintainability nit.
@@ -0,0 +1,128 @@
1
+ name: AI Review Gate (Sonnet via OpenRouter) — ClaudeAutoPM
2
+ on:
3
+ pull_request:
4
+ types: [opened, reopened, synchronize, ready_for_review]
5
+ workflow_dispatch:
6
+
7
+ permissions:
8
+ contents: read
9
+ pull-requests: write
10
+ checks: write
11
+
12
+ concurrency:
13
+ group: ai-review-${{ github.event.pull_request.number || github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ ai-review:
18
+ name: ai-review
19
+ runs-on: ubuntu-latest
20
+ timeout-minutes: 10
21
+ if: >-
22
+ github.event.pull_request.draft == false &&
23
+ github.event.pull_request.head.repo.full_name == github.repository
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ with:
27
+ fetch-depth: 0
28
+
29
+ - name: Compute PR diff
30
+ id: diff
31
+ env:
32
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
33
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
34
+ run: |
35
+ git diff "$BASE_SHA" "$HEAD_SHA" > "$RUNNER_TEMP/pr.diff" 2>/dev/null || true
36
+ echo "bytes=$(wc -c < "$RUNNER_TEMP/pr.diff" 2>/dev/null || echo 0)" >> "$GITHUB_OUTPUT"
37
+
38
+ - name: Single-call review via OpenRouter
39
+ uses: actions/github-script@v9
40
+ env:
41
+ PR_DIFF_PATH: ${{ runner.temp }}/pr.diff
42
+ OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
43
+ REVIEW_MODEL: anthropic/claude-sonnet-4.6
44
+ with:
45
+ script: |
46
+ const fs = require('fs');
47
+ const sha = context.payload.pull_request.head.sha;
48
+ const CHECK = 'copilot-review';
49
+
50
+ const redactSecrets = (s) => String(s || '')
51
+ .replace(/("(?:access_token|id_token|refresh_token|api_key|apikey|secret|client_secret|private_key|aws_secret_access_key|OPENROUTER_API_KEY|OPENAI_API_KEY|account_id|password|passwd|token)"\s*:\s*")[^"]+/gi, '$1[REDACTED]')
52
+ .replace(/eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, '[REDACTED-JWT]')
53
+ .replace(/sk-or-[A-Za-z0-9_-]{20,}/g, 'sk-or-[REDACTED]')
54
+ .replace(/sk-ant-[A-Za-z0-9_-]{20,}/g, 'sk-ant-[REDACTED]')
55
+ .replace(/sk-[A-Za-z0-9_-]{20,}/g, 'sk-[REDACTED]')
56
+ .replace(/ghp_[A-Za-z0-9]{36}/g, 'ghp_[REDACTED]')
57
+ .replace(/github_pat_[A-Za-z0-9_]{50,}/g, 'github_pat_[REDACTED]');
58
+
59
+ let BRIEF = '';
60
+ try { BRIEF = fs.readFileSync('.github/review-context.md', 'utf8'); } catch (e) {}
61
+ let diff = '';
62
+ try { diff = fs.readFileSync(process.env.PR_DIFF_PATH, 'utf8'); } catch (e) {}
63
+ if (diff.length > 200000) diff = diff.slice(0, 200000) + '\n…[diff truncated]';
64
+
65
+ const prompt = [
66
+ BRIEF, '',
67
+ 'You are a senior code reviewer. Review ONLY the diff below for HIGH/MEDIUM/LOW issues',
68
+ '(security, correctness, data-loss, broken contracts). Ignore style nits and pre-existing',
69
+ 'tech debt outside the diff. If unsure, do not invent findings.', '',
70
+ 'Respond with ONLY a JSON object, no prose:',
71
+ '{"verdict":"approve|request_changes","summary":"<=600 chars","findings":[{"severity":"HIGH|MEDIUM|LOW","file":"path","line":<int|null>,"message":"<=300 chars"}]}',
72
+ '', '--- DIFF ---', diff || '(empty diff)'
73
+ ].join('\n');
74
+
75
+ let raw = '';
76
+ try {
77
+ const resp = await fetch('https://openrouter.ai/api/v1/chat/completions', {
78
+ method: 'POST',
79
+ headers: { 'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`, 'Content-Type': 'application/json' },
80
+ body: JSON.stringify({
81
+ model: process.env.REVIEW_MODEL,
82
+ messages: [{ role: 'user', content: prompt }],
83
+ temperature: 0.1,
84
+ max_tokens: 1500
85
+ })
86
+ });
87
+ const data = await resp.json();
88
+ raw = data?.choices?.[0]?.message?.content || '';
89
+ } catch (e) { raw = ''; }
90
+
91
+ let obj = null;
92
+ try { obj = JSON.parse(raw); } catch (e) {
93
+ const i = raw.indexOf('{');
94
+ if (i >= 0) { let d=0; for (let j=i;j<raw.length;j++){ const c=raw[j]; if(c==='{')d++; else if(c==='}'){d--; if(d===0){ try{obj=JSON.parse(raw.slice(i,j+1));}catch(_){} break; }}}}
95
+ }
96
+
97
+ let conclusion, title, body;
98
+ if (!obj || !obj.verdict) {
99
+ conclusion = 'neutral';
100
+ title = 'AI review — call failed / malformed (human review required)';
101
+ body = `<!-- gemini-ai-review:${sha} -->\n<!-- gemini-verdict:malformed -->\n\n🤖 **${process.env.REVIEW_MODEL}** automated review via OpenRouter:\n\n**Verdict:** (unparseable — neutral, not blocking)`;
102
+ } else {
103
+ const approve = obj.verdict === 'approve';
104
+ conclusion = approve ? 'success' : 'failure';
105
+ const findings = Array.isArray(obj.findings) ? obj.findings : [];
106
+ const fblock = findings.length
107
+ ? '\n\n### Findings\n\n' + findings.map(f => `- **[${redactSecrets(f.severity)}]** \`${redactSecrets(f.file)}${f.line?':'+f.line:''}\` — ${redactSecrets(f.message)}`).join('\n')
108
+ : '';
109
+ title = `AI review: ${approve ? 'APPROVE' : 'REQUEST_CHANGES'}`;
110
+ body = [
111
+ `<!-- gemini-ai-review:${sha} -->`,
112
+ `<!-- gemini-verdict:${approve ? 'approve' : 'reject'} -->`, '',
113
+ `🤖 **${process.env.REVIEW_MODEL}** automated review via OpenRouter:`, '',
114
+ `**Verdict:** ${approve ? 'APPROVE' : 'REQUEST_CHANGES'}`, '',
115
+ `**Summary:** ${redactSecrets(obj.summary || '')}`, fblock
116
+ ].join('\n');
117
+ }
118
+
119
+ await github.rest.checks.create({
120
+ owner: context.repo.owner, repo: context.repo.repo,
121
+ name: CHECK, head_sha: sha, status: 'completed', conclusion,
122
+ output: { title, summary: redactSecrets((body||'').slice(0, 60000)) }
123
+ });
124
+ await github.rest.pulls.createReview({
125
+ owner: context.repo.owner, repo: context.repo.repo,
126
+ pull_number: context.payload.pull_request.number,
127
+ event: 'COMMENT', body
128
+ });
@@ -46,7 +46,7 @@ jobs:
46
46
  working-directory: docs-site
47
47
 
48
48
  - name: Upload artifact
49
- uses: actions/upload-pages-artifact@v3
49
+ uses: actions/upload-pages-artifact@v5
50
50
  with:
51
51
  path: docs-site/docs/.vitepress/dist
52
52
 
@@ -0,0 +1,24 @@
1
+ name: Enforce main source
2
+ # Fires on ALL PRs so the required-status-check `Verify PR source is develop`
3
+ # reports a result on every PR (including PRs into `develop`). The enforcement
4
+ # itself only runs when the base is `main` — promotion-only into main, free
5
+ # branch names everywhere else.
6
+ on:
7
+ pull_request:
8
+ jobs:
9
+ verify:
10
+ name: Verify PR source is develop
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Skip when not targeting main
14
+ if: github.base_ref != 'main'
15
+ run: |
16
+ echo "Base is '${{ github.base_ref }}', not 'main' — promotion gate not applicable."
17
+ - name: Check head is develop
18
+ if: github.base_ref == 'main'
19
+ run: |
20
+ if [ "${{ github.head_ref }}" != "develop" ]; then
21
+ echo "::error::PRs into main must come from develop (head: ${{ github.head_ref }}). Promotion-only."
22
+ exit 1
23
+ fi
24
+ echo "OK — promotion from develop."
@@ -75,7 +75,7 @@ fi
75
75
  Strip frontmatter and prepare GitHub issue body:
76
76
  ```bash
77
77
  # Extract content without frontmatter
78
- sed '1,/^---$/d; 1,/^---$/d' .claude/epics/$ARGUMENTS/epic.md > /tmp/epic-body-raw.md
78
+ awk 'BEGIN{p=0; done=0} /^---$/ && !done {p++; if(p==2) done=1; next} p>=2{print}' ".claude/epics/$ARGUMENTS/epic.md" > /tmp/epic-body-raw.md
79
79
 
80
80
  # Remove "## Tasks Created" section and replace with Stats
81
81
  awk '
@@ -162,7 +162,7 @@ if [ "$task_count" -lt 5 ]; then
162
162
  task_name=$(grep '^name:' "$task_file" | sed 's/^name: *//')
163
163
 
164
164
  # Strip frontmatter from task content
165
- sed '1,/^---$/d; 1,/^---$/d' "$task_file" > /tmp/task-body.md
165
+ awk 'BEGIN{p=0; done=0} /^---$/ && !done {p++; if(p==2) done=1; next} p>=2{print}' "$task_file" > /tmp/task-body.md
166
166
 
167
167
  # Create sub-issue with labels
168
168
  if [ "$use_subissues" = true ]; then
@@ -222,7 +222,7 @@ Task:
222
222
 
223
223
  For each task file:
224
224
  1. Extract task name from frontmatter
225
- 2. Strip frontmatter using: sed '1,/^---$/d; 1,/^---$/d'
225
+ 2. Strip frontmatter using: awk 'BEGIN{p=0; done=0} /^---$/ && !done {p++; if(p==2) done=1; next} p>=2{print}'
226
226
  3. Create sub-issue using:
227
227
  - If gh-sub-issue available:
228
228
  gh sub-issue create --parent $epic_number --title "$task_name" \
@@ -4,30 +4,77 @@ allowed-tools: Bash, Read, Write, LS, Task
4
4
 
5
5
  # Issue Start
6
6
 
7
- Begin work on a GitHub issue with parallel agents based on work stream analysis.
7
+ Begin work on an issue. Auto-detects local (Lite) or GitHub mode from git remote.
8
8
 
9
9
  ## Usage
10
- ` ``
11
- /pm:issue-start <issue_number>
12
- ` ``
10
+ ```
11
+ /pm:issue-start <issue_id> [--analyze]
12
+ ```
13
13
 
14
- ## Quick Check
14
+ `issue_id` can be a local file ID (`demo-001`, `001`) or a GitHub issue number.
15
+
16
+ ## Step 0 — Detect provider
17
+
18
+ ```bash
19
+ ISSUE_ID=$(echo "$ARGUMENTS" | awk '{print $1}')
20
+ HAS_ANALYZE=$(echo "$ARGUMENTS" | grep -q '\-\-analyze' && echo "true" || echo "false")
21
+
22
+ PROVIDER="local"
23
+ if git remote get-url origin 2>/dev/null | grep -q "github.com"; then
24
+ PROVIDER="github"
25
+ elif [ -n "$AZURE_DEVOPS_ORG" ] || [ -f ".azure" ]; then
26
+ PROVIDER="azure"
27
+ fi
28
+ echo "Provider: $PROVIDER"
29
+ ```
30
+
31
+ **PROVIDER=`local`** → LOCAL FLOW · **PROVIDER=`github`** → GITHUB FLOW
32
+
33
+ ---
34
+
35
+ ## LOCAL FLOW (Lite — no GitHub remote)
36
+
37
+ ```bash
38
+ ISSUE_FILE=$(find .claude/issues -name "${ISSUE_ID}.md" 2>/dev/null | head -1)
39
+ [ -z "$ISSUE_FILE" ] && echo "❌ Not found: .claude/issues/${ISSUE_ID}.md — ls .claude/issues/" && exit 1
40
+ ```
41
+
42
+ 1. Update `status: open` → `status: in-progress` in frontmatter
43
+ 2. Update `updated:` with `date -u +"%Y-%m-%dT%H:%M:%SZ"`
44
+ 3. Add entry to `.claude/active-work.json`
45
+ 4. Show issue content and begin working on it
46
+ 5. Output: `🚀 Local issue $ISSUE_ID started — close with: /pm:issue-close $ISSUE_ID`
47
+
48
+ ---
49
+
50
+ ## GITHUB FLOW
51
+
52
+ ### Quick Check
15
53
 
16
54
  1. **Get issue details:**
17
- ` ``bash
18
- gh issue view $ARGUMENTS --json state,title,labels,body
19
- ` ``
20
- If it fails: "❌ Cannot access issue #$ARGUMENTS. Check number or run: gh auth login"
55
+ ```bash
56
+ ISSUE_NUMBER=$ISSUE_ID
57
+ gh issue view $ISSUE_NUMBER --json state,title,labels,body
58
+ ```
59
+ If it fails: "❌ Cannot access issue #$ISSUE_NUMBER. Check number or run: gh auth login"
21
60
 
22
61
  2. **Find local task file:**
23
- - First check if `.claude/epics/*/$ARGUMENTS.md` exists (new naming)
24
- - If not found, search for file containing `github:.*issues/$ARGUMENTS` in frontmatter (old naming)
25
- - If not found: " No local task for issue #$ARGUMENTS. This issue may have been created outside the PM system."
62
+ ```bash
63
+ # Primary: search frontmatter for github issue URL (works with any filename)
64
+ task_file=$(grep -rl "github:.*issues/$ISSUE_NUMBER" .claude/epics/ 2>/dev/null | head -1)
65
+ if [ -z "$task_file" ]; then
66
+ # Fallback: check for issue-number filename
67
+ task_file=$(find .claude/epics -name "$ISSUE_NUMBER.md" 2>/dev/null | head -1)
68
+ fi
69
+ ```
70
+ - If not found: "❌ No local task for issue #$ISSUE_NUMBER. This issue may have been created outside the PM system."
71
+ - Use `$task_file` as the canonical path for ALL subsequent steps
26
72
 
27
73
  3. **Check for analysis (when NOT using --analyze flag):**
28
- - If user didn't use `--analyze` flag, check if analysis file exists
29
- - Analysis file location: `.claude/epics/{epic_name}/$ARGUMENTS-analysis.md`
30
- - If no analysis AND no `--analyze` flag: Stop and suggest using `--analyze` flag
74
+ - Extract epic name from `$task_file` path: `epic_name=$(echo "$task_file" | sed 's|.claude/epics/||' | cut -d/ -f1)`
75
+ - If `$HAS_ANALYZE` is "false", check if analysis file exists
76
+ - Analysis file location: `.claude/epics/$epic_name/$ISSUE_NUMBER-analysis.md`
77
+ - If no analysis AND `$HAS_ANALYZE` is "false": Stop and suggest using `--analyze` flag
31
78
 
32
79
  ## Required Documentation Access
33
80
 
@@ -45,7 +92,7 @@ Begin work on a GitHub issue with parallel agents based on work stream analysis.
45
92
  - Validates task coordination strategies
46
93
  - Prevents common pitfalls in distributed work
47
94
 
48
- ## ⚠️ TDD REMINDER - READ THIS FIRST
95
+ ## TDD REMINDER - READ THIS FIRST
49
96
 
50
97
  **CRITICAL: This project follows Test-Driven Development (TDD).**
51
98
 
@@ -69,10 +116,10 @@ See `.claude/rules/tdd.enforcement.md` for complete TDD requirements.
69
116
 
70
117
  ### 0. Handle --analyze Flag (if provided)
71
118
 
72
- If user provided `--analyze` flag, delegate to the Node.js script:
73
- ` ``bash
74
- node .claude/scripts/pm/issue-start.cjs $ARGUMENTS --analyze
75
- ` ``
119
+ If `$HAS_ANALYZE` is "true", delegate to the Node.js script:
120
+ ```bash
121
+ node .claude/scripts/pm/issue-start.cjs $ISSUE_NUMBER --analyze
122
+ ```
76
123
 
77
124
  This script will:
78
125
  1. Find the task file for the issue
@@ -88,7 +135,7 @@ This script will:
88
135
  ### 1. Ensure Branch Exists (Non-analyze workflow)
89
136
 
90
137
  Check if epic branch exists:
91
- ` ``bash
138
+ ```bash
92
139
  # Find epic name from task file
93
140
  epic_name={extracted_from_path}
94
141
 
@@ -101,11 +148,11 @@ fi
101
148
  # Check out the branch
102
149
  git checkout epic/$epic_name
103
150
  git pull origin epic/$epic_name
104
- ` ``
151
+ ```
105
152
 
106
153
  ### 2. Read Analysis
107
154
 
108
- Read `.claude/epics/{epic_name}/$ARGUMENTS-analysis.md`:
155
+ Read `.claude/epics/{epic_name}/$ISSUE_NUMBER-analysis.md`:
109
156
  - Parse parallel streams
110
157
  - Identify which can start immediately
111
158
  - Note dependencies between streams
@@ -115,9 +162,9 @@ Read `.claude/epics/{epic_name}/$ARGUMENTS-analysis.md`:
115
162
  Get current datetime: `date -u +"%Y-%m-%dT%H:%M:%SZ"`
116
163
 
117
164
  Create workspace structure:
118
- ` ``bash
119
- mkdir -p .claude/epics/{epic_name}/updates/$ARGUMENTS
120
- ` ``
165
+ ```bash
166
+ mkdir -p .claude/epics/{epic_name}/updates/$ISSUE_NUMBER
167
+ ```
121
168
 
122
169
  Update task file frontmatter `updated` field with current datetime.
123
170
 
@@ -125,10 +172,10 @@ Update task file frontmatter `updated` field with current datetime.
125
172
 
126
173
  For each stream that can start immediately:
127
174
 
128
- Create `.claude/epics/{epic_name}/updates/$ARGUMENTS/stream-{X}.md`:
129
- ` ``markdown
175
+ Create `.claude/epics/{epic_name}/updates/$ISSUE_NUMBER/stream-{X}.md`:
176
+ ```markdown
130
177
  ---
131
- issue: $ARGUMENTS
178
+ issue: $ISSUE_NUMBER
132
179
  stream: {stream_name}
133
180
  agent: {agent_type}
134
181
  started: {current_datetime}
@@ -145,15 +192,15 @@ status: in_progress
145
192
 
146
193
  ## Progress
147
194
  - Starting implementation
148
- ` ``
195
+ ```
149
196
 
150
197
  Launch agent using Task tool:
151
- ` ``yaml
198
+ ```yaml
152
199
  Task:
153
- description: "Issue #$ARGUMENTS Stream {X}"
200
+ description: "Issue #$ISSUE_NUMBER Stream {X}"
154
201
  subagent_type: "{agent_type}"
155
202
  prompt: |
156
- **🚨 CRITICAL RULE #1: Test-Driven Development (TDD) is MANDATORY**
203
+ **CRITICAL RULE #1: Test-Driven Development (TDD) is MANDATORY**
157
204
 
158
205
  You MUST follow the RED-GREEN-REFACTOR cycle:
159
206
  1. **RED**: Write a FAILING test first that describes the desired behavior
@@ -177,63 +224,63 @@ Task:
177
224
 
178
225
  ---
179
226
 
180
- You are working on Issue #$ARGUMENTS in the epic branch.
227
+ You are working on Issue #$ISSUE_NUMBER in the epic branch.
181
228
 
182
229
  Branch: epic/{epic_name}
183
230
  Your stream: {stream_name}
184
-
231
+
185
232
  Your scope:
186
233
  - Files to modify: {file_patterns}
187
234
  - Work to complete: {stream_description}
188
-
235
+
189
236
  Requirements:
190
237
  1. Read full task from: .claude/epics/{epic_name}/{task_file}
191
238
  2. **START WITH TESTS**: Write failing tests BEFORE any implementation
192
239
  3. Work ONLY in your assigned files
193
240
  4. Follow TDD cycle: RED (test fails) → GREEN (minimal code) → REFACTOR (cleanup)
194
- 5. Commit frequently with format: "Issue #$ARGUMENTS: {specific change}"
195
- 6. Update progress in: .claude/epics/{epic_name}/updates/$ARGUMENTS/stream-{X}.md
241
+ 5. Commit frequently with format: "Issue #$ISSUE_NUMBER: {specific change}"
242
+ 6. Update progress in: .claude/epics/{epic_name}/updates/$ISSUE_NUMBER/stream-{X}.md
196
243
  7. Follow coordination rules in /rules/agent-coordination.md
197
-
244
+
198
245
  If you need to modify files outside your scope:
199
246
  - Check if another stream owns them
200
247
  - Wait if necessary
201
248
  - Update your progress file with coordination notes
202
-
249
+
203
250
  Complete your stream's work and mark as completed when done.
204
- ` ``
251
+ ```
205
252
 
206
253
  ### 5. GitHub Assignment
207
254
 
208
- ` ``bash
255
+ ```bash
209
256
  # Assign to self and mark in-progress
210
- gh issue edit $ARGUMENTS --add-assignee @me --add-label "in-progress"
211
- ` ``
257
+ gh issue edit $ISSUE_NUMBER --add-assignee @me --add-label "in-progress"
258
+ ```
212
259
 
213
260
  ### 6. Output
214
261
 
215
- ` ``
216
- Started parallel work on issue #$ARGUMENTS
262
+ ```
263
+ Started parallel work on issue #$ISSUE_NUMBER
217
264
 
218
265
  Epic: {epic_name}
219
- Worktree: ../epic-{epic_name}/
266
+ Branch: epic/{epic_name}
220
267
 
221
268
  Launching {count} parallel agents:
222
- Stream A: {name} (Agent-1) Started
223
- Stream B: {name} (Agent-2) Started
269
+ Stream A: {name} (Agent-1) - Started
270
+ Stream B: {name} (Agent-2) - Started
224
271
  Stream C: {name} - Waiting (depends on A)
225
272
 
226
273
  Progress tracking:
227
- .claude/epics/{epic_name}/updates/$ARGUMENTS/
274
+ .claude/epics/{epic_name}/updates/$ISSUE_NUMBER/
228
275
 
229
- ⚠️ TDD CHECKLIST - All agents MUST follow:
230
- 1. RED: Write failing test
231
- 2. GREEN: Make test pass (minimal code)
232
- 3. REFACTOR: Clean up code
276
+ TDD CHECKLIST - All agents MUST follow:
277
+ 1. RED: Write failing test
278
+ 2. GREEN: Make test pass (minimal code)
279
+ 3. REFACTOR: Clean up code
233
280
 
234
281
  Monitor with: /pm:epic-status {epic_name}
235
- Sync updates: /pm:issue-sync $ARGUMENTS
236
- ` ``
282
+ Sync updates: /pm:issue-sync $ISSUE_NUMBER
283
+ ```
237
284
 
238
285
  ## Error Handling
239
286
 
@@ -245,4 +292,4 @@ If any step fails, report clearly:
245
292
  ## Important Notes
246
293
 
247
294
  Follow `/rules/datetime.md` for timestamps.
248
- Keep it simple - trust that GitHub and file system work.
295
+ Keep it simple - trust that GitHub and file system work.
@@ -61,10 +61,17 @@ updated: {current_datetime}
61
61
  YAML frontmatter MUST be removed before sending content to GitHub (issues, comments, external systems):
62
62
 
63
63
  ```bash
64
- # Strip frontmatter (everything between first two --- lines)
65
- sed '1,/^---$/d; 1,/^---$/d' input.md > output.md
64
+ # Strip frontmatter, keep full body (including any body '---' horizontal rules).
65
+ # ⚠️ Produces EMPTY output when the input has no leading '---'. If the input
66
+ # may be a plain markdown file, use the `strip_frontmatter` helper in
67
+ # `frontmatter-utils.sh` instead — it passes such files through unchanged.
68
+ awk 'BEGIN{p=0; done=0} /^---$/ && !done {p++; if(p==2) done=1; next} p>=2{print}' input.md > output.md
66
69
  ```
67
70
 
71
+ Do NOT use the naive `sed '1,/^---$/d; 1,/^---$/d'` idiom — it counts every
72
+ `---` line, so it destroys the body when there is no body or when the body
73
+ contains a horizontal rule (see issue #599 and `strip-frontmatter.md`).
74
+
68
75
  Always strip when:
69
76
  - Creating GitHub issues from markdown files (`gh issue create --body-file`)
70
77
  - Posting file content as comments
@@ -72,7 +79,7 @@ Always strip when:
72
79
 
73
80
  ```bash
74
81
  # Example: create issue from file
75
- sed '1,/^---$/d; 1,/^---$/d' task.md > /tmp/clean.md
82
+ awk 'BEGIN{p=0; done=0} /^---$/ && !done {p++; if(p==2) done=1; next} p>=2{print}' task.md > /tmp/clean.md
76
83
  gh issue create --body-file /tmp/clean.md
77
84
  ```
78
85
 
@@ -117,8 +117,23 @@ strip_frontmatter() {
117
117
  return 1
118
118
  fi
119
119
 
120
- # Remove frontmatter (everything between first two --- lines)
121
- sed '1,/^---$/d; 1,/^---$/d' "$input_file" > "$output_file"
120
+ # Passthrough when there is no frontmatter at all. Without this guard, the
121
+ # awk below (which prints only after seeing two `---` delimiters) produces
122
+ # empty output for files with no leading `---`, which would silently
123
+ # produce empty GitHub issue bodies. Flagged in the PR review for #599.
124
+ if [[ "$(head -n 1 "$input_file")" != "---" ]]; then
125
+ cp "$input_file" "$output_file"
126
+ log_debug "No leading frontmatter in $input_file; passthrough to $output_file"
127
+ log_function_exit "strip_frontmatter"
128
+ return 0
129
+ fi
130
+
131
+ # Remove frontmatter (everything between first two --- lines).
132
+ # Naive `sed '1,/^---$/d; 1,/^---$/d'` is BROKEN: it counts every '---' line,
133
+ # so it destroys the body when there is no body or when the body contains a
134
+ # horizontal rule. The `done` flag below freezes the counter after the
135
+ # second delimiter is consumed. See issue #599.
136
+ awk 'BEGIN{p=0; done=0} /^---$/ && !done {p++; if(p==2) done=1; next} p>=2{print}' "$input_file" > "$output_file"
122
137
 
123
138
  log_debug "Stripped frontmatter from $input_file to $output_file"
124
139
  log_function_exit "strip_frontmatter"
@@ -60,6 +60,16 @@ check_repo_protection() {
60
60
  return 0
61
61
  }
62
62
 
63
+ # Build separate --label flags from a comma-separated label string
64
+ build_label_flags() {
65
+ local labels="$1"
66
+ local IFS=','
67
+ for label in $labels; do
68
+ echo "--label"
69
+ echo "$label"
70
+ done
71
+ }
72
+
63
73
  # Create GitHub issue with proper error handling
64
74
  create_github_issue() {
65
75
  local title="$1"
@@ -82,12 +92,14 @@ create_github_issue() {
82
92
  # Check authentication
83
93
  check_gh_auth
84
94
 
85
- # Create issue
95
+ # Create issue with split label flags
96
+ local -a label_flags
97
+ mapfile -t label_flags < <(build_label_flags "$labels")
86
98
  local issue_number
87
99
  if ! issue_output=$(gh issue create \
88
100
  --title "$title" \
89
101
  --body-file "$body_file" \
90
- --label "$labels" \
102
+ "${label_flags[@]}" \
91
103
  --json number 2>/dev/null); then
92
104
  log_error "Failed to create GitHub issue"
93
105
  return 1
@@ -110,6 +122,10 @@ create_github_subissue() {
110
122
 
111
123
  log_info "Creating GitHub sub-issue under #$parent_issue: $title"
112
124
 
125
+ # Build split label flags
126
+ local -a label_flags
127
+ mapfile -t label_flags < <(build_label_flags "$labels")
128
+
113
129
  # Check if gh-sub-issue extension is available
114
130
  if gh extension list | grep -q "yahsan2/gh-sub-issue"; then
115
131
  local issue_number
@@ -117,7 +133,7 @@ create_github_subissue() {
117
133
  --parent "$parent_issue" \
118
134
  --title "$title" \
119
135
  --body-file "$body_file" \
120
- --label "$labels" \
136
+ "${label_flags[@]}" \
121
137
  --json number -q .number 2>/dev/null); then
122
138
  log_error "Failed to create GitHub sub-issue"
123
139
  return 1
@@ -113,6 +113,35 @@ async function obsidianSync(argv) {
113
113
  child.on('close', (code) => process.exit(code || 0));
114
114
  }
115
115
 
116
+ /**
117
+ * autopm obsidian link
118
+ */
119
+ async function obsidianLink(argv) {
120
+ const root = findProjectRoot();
121
+ checkPlugin(root);
122
+
123
+ // Check multiple locations for link-vault.js
124
+ const candidates = [
125
+ path.join(root, '.claude', 'scripts', 'obsidian', 'link-vault.js'),
126
+ path.join(root, 'packages', 'plugin-obsidian', 'scripts', 'obsidian', 'link-vault.js'),
127
+ ];
128
+ const scriptPath = candidates.find(p => fs.existsSync(p));
129
+ if (!scriptPath) {
130
+ console.error('❌ Link script not found. Reinstall plugin-obsidian.');
131
+ process.exit(1);
132
+ }
133
+
134
+ const args = ['--project-root', root];
135
+ if (argv.dryRun) args.push('--dry-run');
136
+
137
+ const child = spawn('node', [scriptPath, ...args], {
138
+ stdio: 'inherit',
139
+ cwd: root
140
+ });
141
+
142
+ child.on('close', (code) => process.exit(code || 0));
143
+ }
144
+
116
145
  /**
117
146
  * autopm obsidian doctor
118
147
  */
@@ -191,6 +220,21 @@ function builder(yargs) {
191
220
  },
192
221
  obsidianSync
193
222
  )
223
+ .command(
224
+ 'link',
225
+ 'Inject [[wikilinks]] into project files for Obsidian Graph View',
226
+ (yargs) => {
227
+ return yargs
228
+ .option('dry-run', {
229
+ describe: 'Show what would be linked without modifying files',
230
+ type: 'boolean',
231
+ default: false
232
+ })
233
+ .example('autopm obsidian link', 'Link all project files')
234
+ .example('autopm obsidian link --dry-run', 'Preview changes');
235
+ },
236
+ obsidianLink
237
+ )
194
238
  .command(
195
239
  'doctor',
196
240
  'Diagnose common Obsidian integration issues',
@@ -214,6 +258,7 @@ module.exports = {
214
258
  console.log('Commands:');
215
259
  console.log(' setup Configure Obsidian vault integration');
216
260
  console.log(' sync Sync project files to Obsidian vault');
261
+ console.log(' link Inject [[wikilinks]] for Graph View');
217
262
  console.log(' doctor Diagnose common integration issues');
218
263
  console.log('\nRun autopm obsidian <command> --help for details\n');
219
264
  }