claude-autopm 3.25.4 → 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.
- package/.github/review-context.md +11 -0
- package/.github/workflows/copilot-review-required.yml +128 -0
- package/.github/workflows/deploy-docs.yml +1 -1
- package/.github/workflows/enforce-main-source.yml +24 -0
- package/autopm/.claude/commands/pm:epic-sync-original.md +3 -3
- package/autopm/.claude/commands/pm:issue-start.md +104 -57
- package/autopm/.claude/rules/frontmatter-operations.md +10 -3
- package/autopm/.claude/scripts/lib/frontmatter-utils.sh +17 -2
- package/autopm/.claude/scripts/lib/github-utils.sh +19 -3
- package/lib/cli/commands/obsidian.js +45 -0
- package/package.json +6 -6
- package/packages/plugin-core/package.json +1 -1
- package/packages/plugin-core/rules/strip-frontmatter.md +59 -24
- package/packages/plugin-core/scripts/lib/frontmatter-utils.sh +17 -2
- package/packages/plugin-core/scripts/lib/github-utils.sh +19 -3
- package/packages/plugin-obsidian/claude-md/obsidian-section.md +14 -0
- package/packages/plugin-obsidian/commands/obsidian:init.md +25 -3
- package/packages/plugin-obsidian/commands/obsidian:link.md +53 -0
- package/packages/plugin-obsidian/plugin.json +9 -0
- package/packages/plugin-obsidian/scripts/obsidian/link-vault.js +234 -0
- package/packages/plugin-pm/package.json +1 -1
- package/packages/plugin-pm/scripts/pm/epic-sync/create-epic-issue.sh +5 -4
- package/packages/plugin-pm/scripts/pm/epic-sync/create-task-issues.sh +2 -1
- package/packages/plugin-pm-github/commands/pm:epic-sync-original.md +3 -3
- package/packages/plugin-pm-github/commands/pm:import.md +30 -3
- package/packages/plugin-pm-github/commands/pm:issue-start.md +10 -1
- 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
|
+
});
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 <
|
|
12
|
-
|
|
10
|
+
```
|
|
11
|
+
/pm:issue-start <issue_id> [--analyze]
|
|
12
|
+
```
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
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
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
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
|
-
##
|
|
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
|
|
73
|
-
|
|
74
|
-
node .claude/scripts/pm/issue-start.cjs $
|
|
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
|
-
|
|
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}/$
|
|
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
|
-
|
|
119
|
-
mkdir -p .claude/epics/{epic_name}/updates/$
|
|
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/$
|
|
129
|
-
|
|
175
|
+
Create `.claude/epics/{epic_name}/updates/$ISSUE_NUMBER/stream-{X}.md`:
|
|
176
|
+
```markdown
|
|
130
177
|
---
|
|
131
|
-
issue: $
|
|
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
|
-
|
|
198
|
+
```yaml
|
|
152
199
|
Task:
|
|
153
|
-
description: "Issue #$
|
|
200
|
+
description: "Issue #$ISSUE_NUMBER Stream {X}"
|
|
154
201
|
subagent_type: "{agent_type}"
|
|
155
202
|
prompt: |
|
|
156
|
-
|
|
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 #$
|
|
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 #$
|
|
195
|
-
6. Update progress in: .claude/epics/{epic_name}/updates/$
|
|
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
|
-
|
|
255
|
+
```bash
|
|
209
256
|
# Assign to self and mark in-progress
|
|
210
|
-
gh issue edit $
|
|
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
|
-
|
|
262
|
+
```
|
|
263
|
+
Started parallel work on issue #$ISSUE_NUMBER
|
|
217
264
|
|
|
218
265
|
Epic: {epic_name}
|
|
219
|
-
|
|
266
|
+
Branch: epic/{epic_name}
|
|
220
267
|
|
|
221
268
|
Launching {count} parallel agents:
|
|
222
|
-
Stream A: {name} (Agent-1)
|
|
223
|
-
Stream B: {name} (Agent-2)
|
|
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/$
|
|
274
|
+
.claude/epics/{epic_name}/updates/$ISSUE_NUMBER/
|
|
228
275
|
|
|
229
|
-
|
|
230
|
-
1.
|
|
231
|
-
2.
|
|
232
|
-
3.
|
|
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 $
|
|
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 (
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|