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.
Files changed (28) hide show
  1. package/dist/dashboard/assets/index-Bg3O0ISk.js +11 -0
  2. package/dist/dashboard/assets/index-SBBktW9a.css +1 -0
  3. package/dist/dashboard/index.html +2 -2
  4. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts.map +1 -1
  5. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js +39 -34
  6. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js.map +1 -1
  7. package/dist/plugins/specweave-github/lib/github-push-sync.js +1 -0
  8. package/dist/plugins/specweave-github/lib/github-push-sync.js.map +1 -1
  9. package/dist/plugins/specweave-github/lib/github-us-auto-closer.js +2 -1
  10. package/dist/plugins/specweave-github/lib/github-us-auto-closer.js.map +1 -1
  11. package/dist/src/core/ac-progress-sync.js +2 -1
  12. package/dist/src/core/ac-progress-sync.js.map +1 -1
  13. package/package.json +1 -1
  14. package/plugins/specweave/agents/sw-architect.md +81 -0
  15. package/plugins/specweave/agents/sw-planner.md +95 -0
  16. package/plugins/specweave/agents/sw-pm.md +117 -0
  17. package/plugins/specweave/hooks/v2/dispatchers/pre-tool-use.sh +15 -0
  18. package/plugins/specweave/hooks/v2/guards/skill-chain-enforcement-guard.sh +8 -0
  19. package/plugins/specweave/skills/increment/SKILL.md +36 -52
  20. package/plugins/specweave-github/hooks/github-auto-create-handler.sh +7 -3
  21. package/plugins/specweave-github/lib/github-ac-comment-poster.js +36 -27
  22. package/plugins/specweave-github/lib/github-ac-comment-poster.ts +58 -41
  23. package/plugins/specweave-github/lib/github-push-sync.js +2 -0
  24. package/plugins/specweave-github/lib/github-push-sync.ts +1 -0
  25. package/plugins/specweave-github/lib/github-us-auto-closer.js +1 -1
  26. package/plugins/specweave-github/lib/github-us-auto-closer.ts +2 -1
  27. package/dist/dashboard/assets/index-BkMbqwTc.css +0 -1
  28. 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** - ALWAYS delegate via Skill() calls (guard-enforced)
274
- 2. **Project field is MANDATORY** - Every US MUST have `**Project**:` field
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. **NO agent spawning** - Skills MUST NOT spawn Task() agents (causes crashes). Guide user in main conversation.
277
- 5. **Increment naming** - Format: `####-descriptive-kebab-case`
278
- 6. **Multi-repo** - In umbrella projects with `repositories/` folder, create increments in EACH repo's `.specweave/`, not the umbrella root
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 Skill Delegation (BLOCKING ENFORCEMENT)
269
+ ## CRITICAL: Mandatory Agent Delegation
281
270
 
282
271
  **This skill MUST NOT write spec.md, plan.md, or tasks.md directly.**
283
- A PreToolUse guard (`skill-chain-enforcement-guard.sh`) BLOCKS all direct writes.
284
-
285
- **You MUST invoke these sub-skills via Skill() calls:**
272
+ Delegate to native plugin agents via Agent() calls.
286
273
 
287
- | File | Required Skill | Invocation |
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
- **Each sub-skill MUST register its marker BEFORE writing its file:**
294
- ```bash
295
- mkdir -p .specweave/state
296
- # PM skill registers before writing spec.md:
297
- echo '{}' | jq '.pm_invoked=true | .pm_invoked_at="'$(date -Iseconds)'"' > .specweave/state/skill-chain-XXXX-name.json
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 a skill would produce
306
- - Skip any of the 3 Skill() calls
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 skill:**
293
+ **If deep interview is enabled, delegate to PM agent:**
316
294
 
317
295
  ```typescript
318
- Skill({ skill: "sw:pm", args: "Deep interview for increment XXXX-name: <user description>" })
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 skill will:
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 - Guard Enforced)
308
+ ## Step 4: Delegation (MANDATORY - Agent Based)
331
309
 
332
- **After increment folder + metadata.json are created, you MUST invoke all 3 sub-skills.**
333
- **A PreToolUse guard BLOCKS writes to spec.md/plan.md/tasks.md without skill markers.**
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
- Skill({ skill: "sw:pm", args: "Write spec for increment XXXX-name: <user's feature description>" })
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
- Skill({ skill: "sw:architect", args: "Design architecture for increment XXXX-name based on spec.md" })
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 Test-Aware Planner for tasks.md (REQUIRED)
329
+ ### 4c. Invoke Planner Agent for tasks.md (REQUIRED)
346
330
  ```typescript
347
- Skill({ skill: "sw:test-aware-planner", args: "Generate tasks for increment XXXX-name based on spec.md and plan.md" })
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 skill reads the output of the previous one.
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
- - Skill fails: Fall back to keyword prompts for PM/Architect skills
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
- if [[ "$line" =~ ^\*\*Acceptance[[:space:]]Criteria ]]; then
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
- if [[ "$line" =~ ^\*\*Acceptance[[:space:]]Criteria ]]; then
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
- const usForSync = buildUserStoryForSync(content, usId, acStates, incrementId);
45
- if (usForSync) {
43
+ if (acStates.length > 0) {
46
44
  try {
47
- await pushSyncUserStories([usForSync], {
48
- owner: options.owner,
49
- repo: options.repo,
50
- token: options.token
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])\\] \\*\\*AC-US${usNum}-(\\d+)\\*\\*:\\s*(.+)`,
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
- // Targeted push-sync: update issue body with current AC states
103
- const usForSync = buildUserStoryForSync(content, usId, acStates, incrementId);
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 pushSyncUserStories([usForSync], {
107
- owner: options.owner,
108
- repo: options.repo,
109
- token: options.token,
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])\\] \\*\\*AC-US${usNum}-(\\d+)\\*\\*:\\s*(.+)`,
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
+ }
@@ -50,6 +50,8 @@ async function searchIssueByPrefix(usId, repoSlug, env) {
50
50
  "list",
51
51
  "--repo",
52
52
  repoSlug,
53
+ "--state",
54
+ "all",
53
55
  "--search",
54
56
  `[${usId}] in:title`,
55
57
  "--json",
@@ -102,6 +102,7 @@ async function searchIssueByPrefix(
102
102
  const res = await execFileNoThrow('gh', [
103
103
  'issue', 'list',
104
104
  '--repo', repoSlug,
105
+ '--state', 'all',
105
106
  '--search', `[${usId}] in:title`,
106
107
  '--json', 'number,title,node_id',
107
108
  '--limit', '1',
@@ -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])\\] \\*\\*AC-US${usNum}-(\\d+)\\*\\*:\\s*(.+)`,
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])\\] \\*\\*AC-US${usNum}-(\\d+)\\*\\*:\\s*(.+)`,
280
+ `- \\[([ x])\\] (?:\\*\\*)?AC-US${usNum}-(\\d+)(?:\\*\\*)?:\\s*(.+)`,
280
281
  'g',
281
282
  );
282
283