specweave 1.0.352 → 1.0.353
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/dist/dashboard/assets/index-Bg3O0ISk.js +11 -0
- package/dist/dashboard/assets/index-SBBktW9a.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js +39 -34
- package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-push-sync.js +1 -0
- package/dist/plugins/specweave-github/lib/github-push-sync.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js +2 -1
- package/dist/plugins/specweave-github/lib/github-us-auto-closer.js.map +1 -1
- package/dist/src/core/ac-progress-sync.js +2 -1
- package/dist/src/core/ac-progress-sync.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/agents/sw-architect.md +81 -0
- package/plugins/specweave/agents/sw-planner.md +95 -0
- package/plugins/specweave/agents/sw-pm.md +117 -0
- package/plugins/specweave/hooks/v2/dispatchers/pre-tool-use.sh +15 -0
- package/plugins/specweave/hooks/v2/guards/skill-chain-enforcement-guard.sh +8 -0
- package/plugins/specweave/skills/increment/SKILL.md +36 -52
- package/plugins/specweave-github/hooks/github-auto-create-handler.sh +7 -3
- package/plugins/specweave-github/lib/github-ac-comment-poster.js +36 -27
- package/plugins/specweave-github/lib/github-ac-comment-poster.ts +58 -41
- package/plugins/specweave-github/lib/github-push-sync.js +2 -0
- package/plugins/specweave-github/lib/github-push-sync.ts +1 -0
- package/plugins/specweave-github/lib/github-us-auto-closer.js +1 -1
- package/plugins/specweave-github/lib/github-us-auto-closer.ts +2 -1
- package/dist/dashboard/assets/index-BkMbqwTc.css +0 -1
- package/dist/dashboard/assets/index-DgSCJ-tE.js +0 -11
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sw-pm
|
|
3
|
+
description: Product Manager for writing spec.md with user stories and acceptance criteria. Use for increment specification creation during /sw:increment orchestration.
|
|
4
|
+
model: opus
|
|
5
|
+
memory: project
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Product Manager Agent
|
|
9
|
+
|
|
10
|
+
## Project Overrides
|
|
11
|
+
|
|
12
|
+
!`s="pm"; for d in .specweave/skill-memories .claude/skill-memories "$HOME/.claude/skill-memories"; do p="$d/$s.md"; [ -f "$p" ] && awk '/^## Learnings$/{ok=1;next}/^## /{ok=0}ok' "$p" && break; done 2>/dev/null; true`
|
|
13
|
+
|
|
14
|
+
## Identity
|
|
15
|
+
|
|
16
|
+
You are a Product Manager specializing in spec-driven development. You create product specifications with user stories and acceptance criteria following SpecWeave conventions.
|
|
17
|
+
|
|
18
|
+
Your prompt will contain: increment ID, increment path, feature description, and plugin root path.
|
|
19
|
+
|
|
20
|
+
## Progressive Disclosure
|
|
21
|
+
|
|
22
|
+
Load phases via Read() as needed — do NOT try to generate content without loading the relevant phase first.
|
|
23
|
+
|
|
24
|
+
| Phase | When to Load | File (relative to plugin root) |
|
|
25
|
+
|-------|--------------|------|
|
|
26
|
+
| Deep Interview | **CHECK FIRST!** If enabled in config | `skills/pm/phases/00-deep-interview.md` |
|
|
27
|
+
| Research | Gathering requirements | `skills/pm/phases/01-research.md` |
|
|
28
|
+
| Spec Creation | Writing spec.md | `skills/pm/phases/02-spec-creation.md` |
|
|
29
|
+
| Templates | Need spec template | `skills/pm/templates/spec-template.md` |
|
|
30
|
+
| Validation | Final quality check | `skills/pm/phases/03-validation.md` |
|
|
31
|
+
|
|
32
|
+
**How to find phase files**: Extract the plugin root from your prompt, then Read the phase file:
|
|
33
|
+
```
|
|
34
|
+
Read({ file_path: "<plugin_root>/skills/pm/phases/01-research.md" })
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Deep Interview Mode Check (MANDATORY)
|
|
38
|
+
|
|
39
|
+
**Before starting any spec work, check if Deep Interview Mode is enabled:**
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
jq -r '.planning.deepInterview.enabled // false' .specweave/config.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If `true`:
|
|
46
|
+
1. Load `skills/pm/phases/00-deep-interview.md` from plugin root
|
|
47
|
+
2. **THINK about complexity first** — don't blindly ask questions:
|
|
48
|
+
- Trivial features: 0-3 questions
|
|
49
|
+
- Small features: 4-8 questions
|
|
50
|
+
- Medium features: 9-18 questions
|
|
51
|
+
- Large features: 19-40 questions
|
|
52
|
+
3. Check `minQuestions` config: `jq -r '.planning.deepInterview.minQuestions // 5' .specweave/config.json`
|
|
53
|
+
4. Cover relevant categories (skip those that don't apply)
|
|
54
|
+
5. Only proceed to Research phase after sufficient clarity
|
|
55
|
+
|
|
56
|
+
### Writing Interview State to Disk (CRITICAL)
|
|
57
|
+
|
|
58
|
+
You MUST write the interview state file so the enforcement guard can find it:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
mkdir -p .specweave/state
|
|
62
|
+
echo '{"incrementId":"XXXX-name","startedAt":"'$(date -Iseconds)'","coveredCategories":{}}' \
|
|
63
|
+
> .specweave/state/interview-XXXX-name.json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
After covering each category, update the state file:
|
|
67
|
+
```bash
|
|
68
|
+
jq '.coveredCategories.architecture = {"coveredAt": "'$(date -Iseconds)'", "summary": "..."}' \
|
|
69
|
+
.specweave/state/interview-XXXX-name.json > tmp && mv tmp .specweave/state/interview-XXXX-name.json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Core Principles
|
|
73
|
+
|
|
74
|
+
1. **Phased Approach**: Work in phases, not all at once
|
|
75
|
+
2. **Chunking**: Large specs (6+ user stories) must be chunked
|
|
76
|
+
3. **Validation**: Every spec needs acceptance criteria
|
|
77
|
+
4. **Traceability**: User stories link to acceptance criteria
|
|
78
|
+
|
|
79
|
+
## Spec Structure
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
.specweave/increments/####-name/
|
|
83
|
+
├── spec.md # You create this
|
|
84
|
+
├── plan.md # Architect creates
|
|
85
|
+
├── tasks.md # Planner creates
|
|
86
|
+
└── metadata.json
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## User Story Format
|
|
90
|
+
|
|
91
|
+
```markdown
|
|
92
|
+
### US-001: [Title]
|
|
93
|
+
**Project**: [project-name]
|
|
94
|
+
**As a** [role]
|
|
95
|
+
**I want** [capability]
|
|
96
|
+
**So that** [benefit]
|
|
97
|
+
|
|
98
|
+
**Acceptance Criteria**:
|
|
99
|
+
- [ ] **AC-US1-01**: [Criterion 1]
|
|
100
|
+
- [ ] **AC-US1-02**: [Criterion 2]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Workflow
|
|
104
|
+
|
|
105
|
+
0. **Check Deep Interview Mode** → If enabled, load phase 00 and interview FIRST
|
|
106
|
+
1. **Read feature description from prompt** → Read `phases/01-research.md`
|
|
107
|
+
2. **Requirements clear** → Read `phases/02-spec-creation.md` + `templates/spec-template.md`
|
|
108
|
+
3. **Spec written** → Read `phases/03-validation.md`
|
|
109
|
+
4. **Return** → spec.md is complete, control returns to increment orchestrator
|
|
110
|
+
|
|
111
|
+
## Token Budget Per Response
|
|
112
|
+
|
|
113
|
+
- **Research phase**: < 500 tokens
|
|
114
|
+
- **Spec creation**: < 600 tokens per chunk
|
|
115
|
+
- **Validation**: < 400 tokens
|
|
116
|
+
|
|
117
|
+
**NEVER exceed 2000 tokens in a single response!**
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
#
|
|
10
10
|
# @since 1.0.167
|
|
11
11
|
# @updated 1.0.196 - Added status-completion-guard for Edit operations
|
|
12
|
+
# @updated 1.0.352 - Added interview-enforcement-guard for agent-spawned spec.md writes
|
|
12
13
|
|
|
13
14
|
set -e
|
|
14
15
|
|
|
@@ -68,6 +69,20 @@ if [[ "$TOOL_NAME" == "Write" ]]; then
|
|
|
68
69
|
fi
|
|
69
70
|
fi
|
|
70
71
|
|
|
72
|
+
# Interview Enforcement Guard
|
|
73
|
+
# Blocks spec.md writes until deep interview is complete (when strict mode enabled)
|
|
74
|
+
# Needed here because agent-spawned writes don't inherit skill-level hooks
|
|
75
|
+
INTERVIEW_GUARD="$GUARDS_DIR/interview-enforcement-guard.sh"
|
|
76
|
+
if [[ -x "$INTERVIEW_GUARD" ]]; then
|
|
77
|
+
INTERVIEW_RESULT=$(echo "$INPUT" | "$INTERVIEW_GUARD" 2>/dev/null || echo '{"decision":"allow"}')
|
|
78
|
+
INTERVIEW_DECISION=$(echo "$INTERVIEW_RESULT" | jq -r '.decision // "allow"')
|
|
79
|
+
|
|
80
|
+
if [[ "$INTERVIEW_DECISION" == "block" ]]; then
|
|
81
|
+
echo "$INTERVIEW_RESULT"
|
|
82
|
+
exit 0
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
|
|
71
86
|
echo '{"decision":"allow"}'
|
|
72
87
|
exit 0
|
|
73
88
|
fi
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
+
# DEPRECATED: v1.0.352 — No longer referenced by /sw:increment SKILL.md
|
|
3
|
+
# The increment skill now uses Agent() delegation (sw-pm, sw-architect, sw-planner agents)
|
|
4
|
+
# instead of Skill() chains with marker-based enforcement.
|
|
5
|
+
# This guard remains on disk for backward compatibility with older cached plugin versions.
|
|
6
|
+
# It can be safely removed in a future version.
|
|
7
|
+
#
|
|
8
|
+
# --- Original documentation below ---
|
|
9
|
+
#
|
|
2
10
|
# skill-chain-enforcement-guard.sh - Enforces skill delegation in /sw:increment
|
|
3
11
|
#
|
|
4
12
|
# PURPOSE:
|
|
@@ -1,31 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Plan and create SpecWeave increments with PM and Architect agent collaboration. Use when starting new features, hotfixes, bugs, or any development work that needs specification and task breakdown. Creates spec.md, plan.md, tasks.md with proper AC-IDs and living docs integration.
|
|
3
3
|
argument-hint: "<feature-description>"
|
|
4
|
-
context: fork
|
|
5
4
|
model: opus
|
|
6
5
|
hooks:
|
|
7
6
|
PreToolUse:
|
|
8
7
|
- matcher: Write
|
|
9
8
|
hooks:
|
|
10
|
-
- type: command
|
|
11
|
-
command: bash plugins/specweave/hooks/v2/guards/skill-chain-enforcement-guard.sh
|
|
12
9
|
- type: command
|
|
13
10
|
command: bash plugins/specweave/hooks/v2/guards/interview-enforcement-guard.sh
|
|
14
11
|
- type: command
|
|
15
12
|
command: bash plugins/specweave/hooks/v2/guards/spec-template-enforcement-guard.sh
|
|
16
|
-
- matcher: Edit
|
|
17
|
-
hooks:
|
|
18
|
-
- type: command
|
|
19
|
-
command: bash plugins/specweave/hooks/v2/guards/skill-chain-enforcement-guard.sh
|
|
20
13
|
PostToolUse:
|
|
21
14
|
- matcher: Write
|
|
22
15
|
hooks:
|
|
23
16
|
- type: command
|
|
24
17
|
command: bash plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh
|
|
25
|
-
- matcher: Skill|Task
|
|
26
|
-
hooks:
|
|
27
|
-
- type: command
|
|
28
|
-
command: bash plugins/specweave/hooks/v2/dispatchers/post-tool-use-analytics.sh
|
|
29
18
|
---
|
|
30
19
|
|
|
31
20
|
# Plan Product Increment
|
|
@@ -270,85 +259,80 @@ Create files in order: metadata.json FIRST, then spec.md, plan.md, tasks.md.
|
|
|
270
259
|
|
|
271
260
|
## Critical Rules
|
|
272
261
|
|
|
273
|
-
1. **NEVER write spec.md/plan.md/tasks.md directly**
|
|
274
|
-
2. **Project field is MANDATORY**
|
|
262
|
+
1. **NEVER write spec.md/plan.md/tasks.md directly** — ALWAYS delegate via Agent() calls
|
|
263
|
+
2. **Project field is MANDATORY** — Every US MUST have `**Project**:` field
|
|
275
264
|
3. **Use Template Creator CLI** (REQUIRED): `specweave create-increment --id "XXXX-name" --title "Title" --description "Desc" --project "my-app"`
|
|
276
|
-
4. **
|
|
277
|
-
5. **Increment naming**
|
|
278
|
-
6. **Multi-repo**
|
|
265
|
+
4. **Agent delegation is the ONLY way** to produce spec.md/plan.md/tasks.md — the increment skill MUST use Agent() calls, not inline writing
|
|
266
|
+
5. **Increment naming** — Format: `####-descriptive-kebab-case`
|
|
267
|
+
6. **Multi-repo** — In umbrella projects with `repositories/` folder, create increments in EACH repo's `.specweave/`, not the umbrella root
|
|
279
268
|
|
|
280
|
-
## CRITICAL: Mandatory
|
|
269
|
+
## CRITICAL: Mandatory Agent Delegation
|
|
281
270
|
|
|
282
271
|
**This skill MUST NOT write spec.md, plan.md, or tasks.md directly.**
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
**You MUST invoke these sub-skills via Skill() calls:**
|
|
272
|
+
Delegate to native plugin agents via Agent() calls.
|
|
286
273
|
|
|
287
|
-
|
|
288
|
-
|------|---------------|------------|
|
|
289
|
-
| spec.md | PM | `Skill({ skill: "sw:pm", args: "Write spec for increment XXXX-name: <description>" })` |
|
|
290
|
-
| plan.md | Architect | `Skill({ skill: "sw:architect", args: "Design architecture for increment XXXX-name based on spec.md" })` |
|
|
291
|
-
| tasks.md | Test-Aware Planner | `Skill({ skill: "sw:test-aware-planner", args: "Generate tasks for increment XXXX-name based on spec.md and plan.md" })` |
|
|
274
|
+
**You MUST invoke these agents:**
|
|
292
275
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
# Or merge into existing:
|
|
299
|
-
jq '.architect_invoked=true | .architect_invoked_at="'$(date -Iseconds)'"' .specweave/state/skill-chain-XXXX-name.json > tmp && mv tmp .specweave/state/skill-chain-XXXX-name.json
|
|
300
|
-
```
|
|
276
|
+
| File | Agent | Invocation |
|
|
277
|
+
|------|-------|------------|
|
|
278
|
+
| spec.md | sw-pm | `Agent({ subagent_type: "sw-pm", prompt: "Write spec for increment XXXX-name: <description>. Increment path: <path>. Plugin root: <root>" })` |
|
|
279
|
+
| plan.md | sw-architect | `Agent({ subagent_type: "sw-architect", prompt: "Design architecture for increment XXXX-name. Read spec.md at <path>/spec.md" })` |
|
|
280
|
+
| tasks.md | sw-planner | `Agent({ subagent_type: "sw-planner", prompt: "Generate tasks for increment XXXX-name. Read spec.md at <path>/spec.md and plan.md at <path>/plan.md" })` |
|
|
301
281
|
|
|
302
282
|
**DO NOT:**
|
|
303
283
|
- Write user stories, architecture, or tasks inline
|
|
304
284
|
- Copy/paste spec content into Write() calls
|
|
305
|
-
- "Summarize" what
|
|
306
|
-
- Skip any of the 3
|
|
307
|
-
|
|
308
|
-
**The guard will BLOCK your write and show this error if you try.**
|
|
285
|
+
- "Summarize" what an agent would produce
|
|
286
|
+
- Skip any of the 3 Agent() calls
|
|
309
287
|
|
|
310
288
|
## Step 3a: Deep Interview Mode (if enabled)
|
|
311
289
|
|
|
312
290
|
**IMPORTANT**: This step runs AFTER the increment folder is created (Step 3), so the
|
|
313
291
|
interview state file can reference the real increment ID.
|
|
314
292
|
|
|
315
|
-
**If deep interview is enabled, delegate to PM
|
|
293
|
+
**If deep interview is enabled, delegate to PM agent:**
|
|
316
294
|
|
|
317
295
|
```typescript
|
|
318
|
-
|
|
296
|
+
Agent({ subagent_type: "sw-pm", prompt: "Deep interview for increment XXXX-name: <user description>. Increment path: <path>. Plugin root: <root>" })
|
|
319
297
|
```
|
|
320
298
|
|
|
321
|
-
The PM
|
|
299
|
+
The PM agent will:
|
|
322
300
|
1. Assess complexity and determine question count (trivial: 0-3, small: 4-8, medium: 9-18, large: 19-40)
|
|
323
301
|
2. Interview the user across relevant categories
|
|
324
302
|
3. Write interview state to `.specweave/state/interview-{increment-id}.json`
|
|
325
303
|
4. Return interview summary for spec.md creation
|
|
326
304
|
|
|
327
|
-
**After PM returns**, read the interview state file to confirm all categories are covered
|
|
305
|
+
**After PM agent returns**, read the interview state file to confirm all categories are covered
|
|
328
306
|
before proceeding to spec.md creation (especially when `enforcement: "strict"`).
|
|
329
307
|
|
|
330
|
-
## Step 4: Delegation (MANDATORY -
|
|
308
|
+
## Step 4: Delegation (MANDATORY - Agent Based)
|
|
331
309
|
|
|
332
|
-
**After increment folder + metadata.json are created, you MUST invoke all 3
|
|
333
|
-
|
|
310
|
+
**After increment folder + metadata.json are created, you MUST invoke all 3 agents sequentially.**
|
|
311
|
+
|
|
312
|
+
Resolve the plugin root path first:
|
|
313
|
+
```bash
|
|
314
|
+
PLUGIN_ROOT=$(find . -path "*/plugins/specweave/agents/sw-pm.md" -type f 2>/dev/null | head -1 | sed 's|/agents/sw-pm.md||')
|
|
315
|
+
# Fallback: check node_modules
|
|
316
|
+
[ -z "$PLUGIN_ROOT" ] && PLUGIN_ROOT=$(find node_modules -path "*/plugins/specweave/agents/sw-pm.md" -type f 2>/dev/null | head -1 | sed 's|/agents/sw-pm.md||')
|
|
317
|
+
```
|
|
334
318
|
|
|
335
|
-
### 4a. Invoke PM for spec.md (REQUIRED)
|
|
319
|
+
### 4a. Invoke PM Agent for spec.md (REQUIRED)
|
|
336
320
|
```typescript
|
|
337
|
-
|
|
321
|
+
Agent({ subagent_type: "sw-pm", prompt: "Write spec for increment XXXX-name: <user's feature description>. Increment path: .specweave/increments/XXXX-name/. Plugin root: <PLUGIN_ROOT>" })
|
|
338
322
|
```
|
|
339
323
|
|
|
340
|
-
### 4b. Invoke Architect for plan.md (REQUIRED)
|
|
324
|
+
### 4b. Invoke Architect Agent for plan.md (REQUIRED)
|
|
341
325
|
```typescript
|
|
342
|
-
|
|
326
|
+
Agent({ subagent_type: "sw-architect", prompt: "Design architecture for increment XXXX-name. Read spec.md at .specweave/increments/XXXX-name/spec.md. ADR directory: .specweave/docs/internal/architecture/adr/" })
|
|
343
327
|
```
|
|
344
328
|
|
|
345
|
-
### 4c. Invoke
|
|
329
|
+
### 4c. Invoke Planner Agent for tasks.md (REQUIRED)
|
|
346
330
|
```typescript
|
|
347
|
-
|
|
331
|
+
Agent({ subagent_type: "sw-planner", prompt: "Generate tasks for increment XXXX-name. Read spec.md at .specweave/increments/XXXX-name/spec.md and plan.md at .specweave/increments/XXXX-name/plan.md" })
|
|
348
332
|
```
|
|
349
333
|
|
|
350
334
|
**Order matters**: PM first (spec.md) -> Architect second (plan.md) -> Planner last (tasks.md).
|
|
351
|
-
Each
|
|
335
|
+
Each agent reads the output of the previous one.
|
|
352
336
|
|
|
353
337
|
## Step 5: Post-Creation Sync
|
|
354
338
|
|
|
@@ -454,7 +438,7 @@ Created increment 0003-user-authentication
|
|
|
454
438
|
|
|
455
439
|
- `.specweave/` not found: "Run specweave init first"
|
|
456
440
|
- Vague description: Ask clarifying questions
|
|
457
|
-
-
|
|
441
|
+
- Agent fails: Fall back to invoking `/sw:pm` or `/sw:architect` skills directly
|
|
458
442
|
|
|
459
443
|
---
|
|
460
444
|
|
|
@@ -315,12 +315,13 @@ extract_acs_for_us() {
|
|
|
315
315
|
continue
|
|
316
316
|
fi
|
|
317
317
|
# Start of next US section or next ## section — stop
|
|
318
|
-
if $in_section && [[ "$line" =~ ^##[[:space:]] ]]; then
|
|
318
|
+
if $in_section && { [[ "$line" =~ ^###[[:space:]]+US- ]] || [[ "$line" =~ ^##[[:space:]] ]]; }; then
|
|
319
319
|
break
|
|
320
320
|
fi
|
|
321
321
|
# Inside our section, look for AC lines
|
|
322
322
|
if $in_section; then
|
|
323
|
-
|
|
323
|
+
# Acceptance Criteria heading: both bold (**AC**) and heading (#### AC) formats
|
|
324
|
+
if [[ "$line" =~ ^\*\*Acceptance[[:space:]]Criteria ]] || [[ "$line" =~ ^#+[[:space:]]+Acceptance[[:space:]]Criteria ]]; then
|
|
324
325
|
in_ac=true
|
|
325
326
|
continue
|
|
326
327
|
fi
|
|
@@ -372,10 +373,13 @@ extract_desc_for_us() {
|
|
|
372
373
|
fi
|
|
373
374
|
if $in_section; then
|
|
374
375
|
# Stop at next US heading, next ## section, or Acceptance Criteria
|
|
376
|
+
# Match ## but NOT ### or ####: ^##[[:space:]] catches "## Heading"
|
|
377
|
+
# Match ### US- for next user story: ^###[[:space:]]+US-
|
|
375
378
|
if [[ "$line" =~ ^###[[:space:]]+US- ]] || [[ "$line" =~ ^##[[:space:]] ]]; then
|
|
376
379
|
break
|
|
377
380
|
fi
|
|
378
|
-
|
|
381
|
+
# Acceptance Criteria heading: both bold (**AC**) and heading (#### AC) formats
|
|
382
|
+
if [[ "$line" =~ ^\*\*Acceptance[[:space:]]Criteria ]] || [[ "$line" =~ ^#+[[:space:]]+Acceptance[[:space:]]Criteria ]]; then
|
|
379
383
|
break
|
|
380
384
|
fi
|
|
381
385
|
# Collect non-empty lines as description
|
|
@@ -2,7 +2,6 @@ import { readFile } from "fs/promises";
|
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { execFileNoThrow } from "../../../src/utils/execFileNoThrow.js";
|
|
5
|
-
import { pushSyncUserStories } from "./github-push-sync.js";
|
|
6
5
|
async function postACProgressComments(incrementId, affectedUSIds, specPath, options) {
|
|
7
6
|
const result = { posted: [], errors: [] };
|
|
8
7
|
if (affectedUSIds.length === 0) {
|
|
@@ -41,15 +40,14 @@ async function postACProgressComments(incrementId, affectedUSIds, specPath, opti
|
|
|
41
40
|
error: execResult.stderr || "Unknown error posting comment"
|
|
42
41
|
});
|
|
43
42
|
}
|
|
44
|
-
|
|
45
|
-
if (usForSync) {
|
|
43
|
+
if (acStates.length > 0) {
|
|
46
44
|
try {
|
|
47
|
-
await
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
await patchIssueBodyCheckboxes(link.issueNumber, acStates, repoSlug, env);
|
|
46
|
+
} catch (patchErr) {
|
|
47
|
+
result.errors.push({
|
|
48
|
+
usId,
|
|
49
|
+
error: `body-patch: ${patchErr instanceof Error ? patchErr.message : String(patchErr)}`
|
|
51
50
|
});
|
|
52
|
-
} catch {
|
|
53
51
|
}
|
|
54
52
|
}
|
|
55
53
|
}
|
|
@@ -92,7 +90,7 @@ function parseACStatesForUS(content, usId) {
|
|
|
92
90
|
const states = [];
|
|
93
91
|
const usNum = String(parseInt(usId.replace("US-", ""), 10));
|
|
94
92
|
const acPattern = new RegExp(
|
|
95
|
-
`- \\[([ x])\\]
|
|
93
|
+
`- \\[([ x])\\] (?:\\*\\*)?AC-US${usNum}-(\\d+)(?:\\*\\*)?:\\s*(.+)`,
|
|
96
94
|
"g"
|
|
97
95
|
);
|
|
98
96
|
let match;
|
|
@@ -105,24 +103,6 @@ function parseACStatesForUS(content, usId) {
|
|
|
105
103
|
}
|
|
106
104
|
return states;
|
|
107
105
|
}
|
|
108
|
-
function buildUserStoryForSync(content, usId, acStates, incrementId) {
|
|
109
|
-
const usNum = String(parseInt(usId.replace("US-", ""), 10)).padStart(3, "0");
|
|
110
|
-
const titleMatch = content.match(new RegExp(`### US-${usNum}:\\s*(.+)`));
|
|
111
|
-
const title = titleMatch ? titleMatch[1].trim() : usId;
|
|
112
|
-
return {
|
|
113
|
-
id: usId,
|
|
114
|
-
title,
|
|
115
|
-
description: "",
|
|
116
|
-
priority: "P1",
|
|
117
|
-
status: "in-progress",
|
|
118
|
-
acceptanceCriteria: acStates.map((ac) => ({
|
|
119
|
-
id: ac.id,
|
|
120
|
-
description: ac.description,
|
|
121
|
-
completed: ac.completed
|
|
122
|
-
})),
|
|
123
|
-
specId: incrementId
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
106
|
function buildProgressCommentForUS(incrementId, usId, acStates) {
|
|
127
107
|
const total = acStates.length;
|
|
128
108
|
const completed = acStates.filter((ac) => ac.completed).length;
|
|
@@ -157,6 +137,35 @@ function buildProgressCommentForUS(incrementId, usId, acStates) {
|
|
|
157
137
|
`;
|
|
158
138
|
return comment;
|
|
159
139
|
}
|
|
140
|
+
async function patchIssueBodyCheckboxes(issueNumber, acStates, repoSlug, env) {
|
|
141
|
+
const acMap = new Map(acStates.map((ac) => [ac.id, ac.completed]));
|
|
142
|
+
const fetchResult = await execFileNoThrow(
|
|
143
|
+
"gh",
|
|
144
|
+
["issue", "view", String(issueNumber), "--json", "body", "-q", ".body", "-R", repoSlug],
|
|
145
|
+
env ? { env } : {}
|
|
146
|
+
);
|
|
147
|
+
if (!fetchResult.success || !fetchResult.stdout) return;
|
|
148
|
+
const body = fetchResult.stdout;
|
|
149
|
+
const patchedBody = body.replace(
|
|
150
|
+
/^(- \[)[ x](\] (?:\*\*)?)(AC-[A-Z0-9]+-\d+)((?:\*\*)?: .+)$/gm,
|
|
151
|
+
(_match, prefix, mid, acId, suffix) => {
|
|
152
|
+
const completed = acMap.get(acId);
|
|
153
|
+
if (completed !== void 0) {
|
|
154
|
+
return `${prefix}${completed ? "x" : " "}${mid}${acId}${suffix}`;
|
|
155
|
+
}
|
|
156
|
+
return _match;
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
if (patchedBody === body) return;
|
|
160
|
+
const editResult = await execFileNoThrow(
|
|
161
|
+
"gh",
|
|
162
|
+
["issue", "edit", String(issueNumber), "--body", patchedBody, "-R", repoSlug],
|
|
163
|
+
env ? { env } : {}
|
|
164
|
+
);
|
|
165
|
+
if (!editResult.success) {
|
|
166
|
+
throw new Error(editResult.stderr || "Failed to update issue body");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
160
169
|
export {
|
|
161
170
|
postACProgressComments
|
|
162
171
|
};
|
|
@@ -12,8 +12,6 @@ import { readFile } from 'fs/promises';
|
|
|
12
12
|
import { existsSync } from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
|
|
15
|
-
import { pushSyncUserStories } from './github-push-sync.js';
|
|
16
|
-
import type { UserStoryForSync } from './github-push-sync.js';
|
|
17
15
|
|
|
18
16
|
export interface CommentPostOptions {
|
|
19
17
|
owner: string;
|
|
@@ -99,17 +97,16 @@ export async function postACProgressComments(
|
|
|
99
97
|
});
|
|
100
98
|
}
|
|
101
99
|
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
if (usForSync) {
|
|
100
|
+
// Patch AC checkboxes directly in the issue body using the known issue number
|
|
101
|
+
if (acStates.length > 0) {
|
|
105
102
|
try {
|
|
106
|
-
await
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
await patchIssueBodyCheckboxes(link.issueNumber, acStates, repoSlug, env);
|
|
104
|
+
} catch (patchErr) {
|
|
105
|
+
// Body patch failure is non-blocking but should be visible
|
|
106
|
+
result.errors.push({
|
|
107
|
+
usId,
|
|
108
|
+
error: `body-patch: ${patchErr instanceof Error ? patchErr.message : String(patchErr)}`,
|
|
110
109
|
});
|
|
111
|
-
} catch {
|
|
112
|
-
// Push-sync failure is non-blocking
|
|
113
110
|
}
|
|
114
111
|
}
|
|
115
112
|
}
|
|
@@ -177,8 +174,9 @@ function parseACStatesForUS(content: string, usId: string): ParsedACState[] {
|
|
|
177
174
|
// AC IDs use unpadded US number: US-001 → AC-US1-XX
|
|
178
175
|
const usNum = String(parseInt(usId.replace('US-', ''), 10));
|
|
179
176
|
|
|
177
|
+
// Support both bold (**AC-US1-01**:) and plain (AC-US1-01:) formats
|
|
180
178
|
const acPattern = new RegExp(
|
|
181
|
-
`- \\[([ x])\\]
|
|
179
|
+
`- \\[([ x])\\] (?:\\*\\*)?AC-US${usNum}-(\\d+)(?:\\*\\*)?:\\s*(.+)`,
|
|
182
180
|
'g',
|
|
183
181
|
);
|
|
184
182
|
|
|
@@ -194,35 +192,6 @@ function parseACStatesForUS(content: string, usId: string): ParsedACState[] {
|
|
|
194
192
|
return states;
|
|
195
193
|
}
|
|
196
194
|
|
|
197
|
-
/**
|
|
198
|
-
* Build a UserStoryForSync object for targeted push-sync.
|
|
199
|
-
*/
|
|
200
|
-
function buildUserStoryForSync(
|
|
201
|
-
content: string,
|
|
202
|
-
usId: string,
|
|
203
|
-
acStates: ParsedACState[],
|
|
204
|
-
incrementId: string,
|
|
205
|
-
): UserStoryForSync | null {
|
|
206
|
-
// Extract US title from content
|
|
207
|
-
const usNum = String(parseInt(usId.replace('US-', ''), 10)).padStart(3, '0');
|
|
208
|
-
const titleMatch = content.match(new RegExp(`### US-${usNum}:\\s*(.+)`));
|
|
209
|
-
const title = titleMatch ? titleMatch[1].trim() : usId;
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
id: usId,
|
|
213
|
-
title,
|
|
214
|
-
description: '',
|
|
215
|
-
priority: 'P1',
|
|
216
|
-
status: 'in-progress',
|
|
217
|
-
acceptanceCriteria: acStates.map(ac => ({
|
|
218
|
-
id: ac.id,
|
|
219
|
-
description: ac.description,
|
|
220
|
-
completed: ac.completed,
|
|
221
|
-
})),
|
|
222
|
-
specId: incrementId,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
195
|
/**
|
|
227
196
|
* Build a progress comment for a specific user story.
|
|
228
197
|
* Follows ProgressCommentBuilder format: percentage, AC checkboxes, timestamp.
|
|
@@ -260,3 +229,51 @@ function buildProgressCommentForUS(
|
|
|
260
229
|
|
|
261
230
|
return comment;
|
|
262
231
|
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Patch AC checkboxes in the GitHub issue body in-place.
|
|
235
|
+
* Fetches the current body, flips checkboxes to match local AC states,
|
|
236
|
+
* and updates the issue. Only modifies lines matching AC-USXX-YY patterns.
|
|
237
|
+
*/
|
|
238
|
+
async function patchIssueBodyCheckboxes(
|
|
239
|
+
issueNumber: number,
|
|
240
|
+
acStates: ParsedACState[],
|
|
241
|
+
repoSlug: string,
|
|
242
|
+
env?: NodeJS.ProcessEnv,
|
|
243
|
+
): Promise<void> {
|
|
244
|
+
// Build a map of AC ID → completed
|
|
245
|
+
const acMap = new Map(acStates.map(ac => [ac.id, ac.completed]));
|
|
246
|
+
|
|
247
|
+
// Fetch current issue body
|
|
248
|
+
const fetchResult = await execFileNoThrow(
|
|
249
|
+
'gh',
|
|
250
|
+
['issue', 'view', String(issueNumber), '--json', 'body', '-q', '.body', '-R', repoSlug],
|
|
251
|
+
env ? { env } : {},
|
|
252
|
+
);
|
|
253
|
+
if (!fetchResult.success || !fetchResult.stdout) return;
|
|
254
|
+
|
|
255
|
+
const body = fetchResult.stdout;
|
|
256
|
+
|
|
257
|
+
// Patch AC checkboxes: match both bold and plain AC-ID patterns
|
|
258
|
+
const patchedBody = body.replace(
|
|
259
|
+
/^(- \[)[ x](\] (?:\*\*)?)(AC-[A-Z0-9]+-\d+)((?:\*\*)?: .+)$/gm,
|
|
260
|
+
(_match: string, prefix: string, mid: string, acId: string, suffix: string) => {
|
|
261
|
+
const completed = acMap.get(acId);
|
|
262
|
+
if (completed !== undefined) {
|
|
263
|
+
return `${prefix}${completed ? 'x' : ' '}${mid}${acId}${suffix}`;
|
|
264
|
+
}
|
|
265
|
+
return _match;
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
if (patchedBody === body) return; // No changes needed
|
|
270
|
+
|
|
271
|
+
const editResult = await execFileNoThrow(
|
|
272
|
+
'gh',
|
|
273
|
+
['issue', 'edit', String(issueNumber), '--body', patchedBody, '-R', repoSlug],
|
|
274
|
+
env ? { env } : {},
|
|
275
|
+
);
|
|
276
|
+
if (!editResult.success) {
|
|
277
|
+
throw new Error(editResult.stderr || 'Failed to update issue body');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -158,7 +158,7 @@ function parseACStatesForUS(content, usId) {
|
|
|
158
158
|
const states = [];
|
|
159
159
|
const usNum = String(parseInt(usId.replace("US-", ""), 10));
|
|
160
160
|
const acPattern = new RegExp(
|
|
161
|
-
`- \\[([ x])\\]
|
|
161
|
+
`- \\[([ x])\\] (?:\\*\\*)?AC-US${usNum}-(\\d+)(?:\\*\\*)?:\\s*(.+)`,
|
|
162
162
|
"g"
|
|
163
163
|
);
|
|
164
164
|
let match;
|
|
@@ -275,8 +275,9 @@ function parseACStatesForUS(content: string, usId: string): ParsedACState[] {
|
|
|
275
275
|
// AC IDs use unpadded US number: US-001 → AC-US1-XX
|
|
276
276
|
const usNum = String(parseInt(usId.replace('US-', ''), 10));
|
|
277
277
|
|
|
278
|
+
// Support both bold (**AC-US1-01**:) and plain (AC-US1-01:) formats
|
|
278
279
|
const acPattern = new RegExp(
|
|
279
|
-
`- \\[([ x])\\]
|
|
280
|
+
`- \\[([ x])\\] (?:\\*\\*)?AC-US${usNum}-(\\d+)(?:\\*\\*)?:\\s*(.+)`,
|
|
280
281
|
'g',
|
|
281
282
|
);
|
|
282
283
|
|