@houseofwolvesllc/claude-scrum-skill 2.1.0 → 2.1.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-scrum-skill",
3
3
  "description": "Complete scrum pipeline — PRD to production release with Claude as scrum master. Includes project scaffolding, sprint planning, status tracking, sprint releases, and full-project emulation testing.",
4
- "version": "2.1.0",
4
+ "version": "2.1.1",
5
5
  "author": {
6
6
  "name": "House of Wolves LLC"
7
7
  },
@@ -1,8 +1,33 @@
1
- // sprint_pipeline.js — per-story sprint execution as a pipeline.
1
+ // sprint_pipeline.js — per-story sprint execution.
2
2
  //
3
- // Invoked by /project-orchestrate Phase 1 Step 3. Replaces the v1.x
4
- // Task-spawning prose with per-stage barrier removal and concurrency up
5
- // to min(16, cpu_cores - 2) per the Workflow tool's cap.
3
+ // Invoked by /project-orchestrate Phase 1 Step 3. Each story runs its own
4
+ // independent chain implement review verify → open PR — with no
5
+ // per-stage barriers and concurrency up to min(16, cpu_cores - 2) per the
6
+ // Workflow tool's cap.
7
+ //
8
+ // Concurrency safety. Stories run in parallel against one git repository, so
9
+ // the model is built around a single invariant: the main working tree is
10
+ // mutated only by the serialized local-mode merge step. Everything else stays
11
+ // off it.
12
+ // - Implement and verify run in isolated worktrees (`isolation: 'worktree'`).
13
+ // They create/build the story branch in their own tree, so concurrent
14
+ // `git checkout -b` and commits never race in the shared tree. Branches and
15
+ // commits land in the shared object store and are visible afterward.
16
+ // - Review only diffs two refs (object-store comparison, no checkout), so it
17
+ // never touches the working tree.
18
+ // - Local-mode finalize merges each story branch into the shared release
19
+ // branch in the main tree; these merges are serialized behind a lock so
20
+ // concurrent fan-in merges cannot race. GitHub mode opens independent PRs
21
+ // and needs no lock.
22
+ //
23
+ // Dependency gating. A story does not start until every blocker that is also in
24
+ // this batch has finished `done` (see `blocked_by`). Because a dependent only
25
+ // branches after its in-batch blockers have merged into the release branch, it
26
+ // builds on their work. Blockers outside this batch are the orchestrator's
27
+ // concern — it passes only stories whose external blockers are already resolved
28
+ // (SKILL.md "Independence check"). The in-batch graph is assumed acyclic; the
29
+ // orchestrate skill validates the dependency DAG (cycle + missing-edge checks)
30
+ // before any story reaches this workflow.
6
31
  //
7
32
  // args: {
8
33
  // stories: Story[], // StorySchema-shaped
@@ -151,7 +176,7 @@ ${(story.acceptance_criteria || []).map(c => ` - ${c}`).join('\n')}
151
176
 
152
177
  **Technical context:** ${story.technical_context || '(none provided)'}
153
178
 
154
- **Branch strategy:** Create branch \`story/${story.slug}\` from \`${releaseBranch}\`. Implement. Commit with a clear message. Return the branch name, the commit SHAs, and any notes.
179
+ **Branch strategy:** You are in an isolated git worktree — work only here; do not touch other branches. Create branch \`story/${story.slug}\` from the latest \`${releaseBranch}\` (\`git checkout -b story/${story.slug} ${releaseBranch}\`); it already contains any in-sprint dependency this story builds on. Implement. Commit with a clear message. Return the branch name, the commit SHAs, and any notes.
155
180
 
156
181
  Do NOT open a PR yet. Do NOT merge. The next stage handles review.`
157
182
  }
@@ -159,7 +184,7 @@ Do NOT open a PR yet. Do NOT merge. The next stage handles review.`
159
184
  function buildReviewPrompt(impl, story) {
160
185
  return `You are reviewing the implementation of story ${story.slug} on branch ${impl.branch}.
161
186
 
162
- Read the diff between ${releaseBranch} and ${impl.branch}.
187
+ Read the diff between ${releaseBranch} and ${impl.branch} with \`git diff ${releaseBranch}...${impl.branch}\` — do NOT check out either branch (other stories run concurrently in this repository; checking out would disturb the shared working tree).
163
188
 
164
189
  Story acceptance criteria:
165
190
  ${(story.acceptance_criteria || []).map(c => ` - ${c}`).join('\n')}
@@ -172,7 +197,7 @@ Return a ReviewVerdict: recommendation (accept | accept-with-followups | block),
172
197
  function buildVerifyPrompt(review, story) {
173
198
  return `You are running a lightweight verification on story ${story.slug}.
174
199
 
175
- If the project has a build/lint/test command, run it on the story branch. If not, perform a smoke read of the changed files to confirm no obvious runtime defects (broken imports, syntax errors, dangling references).
200
+ You are in an isolated git worktree — check out \`story/${story.slug}\` here (\`git checkout story/${story.slug}\`) so any build artifacts stay off the shared working tree. If the project has a build/lint/test command, run it on the story branch. If not, perform a smoke read of the changed files to confirm no obvious runtime defects (broken imports, syntax errors, dangling references).
176
201
 
177
202
  Return verifyStatus (pass | warn | fail) and brief notes.`
178
203
  }
@@ -196,69 +221,140 @@ phase('Review')
196
221
  phase('Verify')
197
222
  phase('Open PR')
198
223
 
199
- const results = await pipeline(
200
- stories,
201
- // Stage 1: Implement
202
- story =>
203
- agent(buildImplementPrompt(story), {
204
- label: `impl:${story.slug}`,
205
- phase: 'Implement',
206
- schema: IMPL_RETURN_SCHEMA,
207
- }),
208
- // Stage 2: Review
209
- (impl, story) =>
210
- impl
211
- ? agent(buildReviewPrompt(impl, story), {
212
- label: `review:${story.slug}`,
213
- phase: 'Review',
214
- schema: REVIEW_VERDICT_SCHEMA,
215
- }).then(review => ({ impl, review }))
216
- : null,
217
- // Stage 3: Verify
218
- (prev, story) =>
219
- prev
220
- ? agent(buildVerifyPrompt(prev.review, story), {
221
- label: `verify:${story.slug}`,
222
- phase: 'Verify',
223
- schema: VERIFY_RETURN_SCHEMA,
224
- }).then(verify => ({ ...prev, verify }))
225
- : null,
226
- // Stage 4: Open PR (or local merge)
227
- (prev, story) => {
228
- if (!prev) return null
229
- const { impl, review, verify } = prev
230
- // If the reviewer said block, short-circuit to blocked status without an openPR call.
231
- if (review.recommendation === 'block') {
232
- const blockers = [
233
- ...review.findings.critical.map(f => f.title),
234
- ...review.findings.warning.map(f => f.title),
235
- ]
236
- return makeSprintStoryReturn({
237
- storySlug: story.slug,
238
- status: 'blocked',
239
- branch: impl.branch,
240
- commits: impl.commits,
241
- blockers,
242
- reason: 'Review recommended block.',
243
- })
244
- }
245
- // If verify failed, also short-circuit.
246
- if (verify.verifyStatus === 'fail') {
247
- return makeSprintStoryReturn({
248
- storySlug: story.slug,
249
- status: 'blocked',
250
- branch: impl.branch,
251
- commits: impl.commits,
252
- blockers: [verify.notes || 'verification failed'],
253
- reason: 'Verification failed.',
254
- })
224
+ const batchSlugs = new Set(stories.map(story => story.slug))
225
+
226
+ /**
227
+ * Blockers of `story` that are also in this batch, by trailing slug.
228
+ * blocked_by entries may be bare slugs or "<epic>/<slug>" references. A story
229
+ * cannot block itself, and blockers outside the batch are the orchestrator's
230
+ * concern (it only passes stories whose external blockers are resolved).
231
+ */
232
+ function inBatchBlockers(story) {
233
+ return (story.blocked_by || [])
234
+ .map(reference => reference.split('/').pop())
235
+ .filter(slug => slug !== story.slug && batchSlugs.has(slug))
236
+ }
237
+
238
+ // Serializes local-mode merges onto the shared release branch: each merge runs
239
+ // only after the previous one settles, success or failure, so a failed merge
240
+ // never stalls the rest. GitHub mode opens independent PRs and does not use it.
241
+ let mergeChain = Promise.resolve()
242
+ function serializeMerge(runMerge) {
243
+ const merged = mergeChain.then(runMerge, runMerge)
244
+ mergeChain = merged.then(() => {}, () => {})
245
+ return merged
246
+ }
247
+
248
+ function reviewBlocked(story, impl, review) {
249
+ return makeSprintStoryReturn({
250
+ storySlug: story.slug,
251
+ status: 'blocked',
252
+ branch: impl.branch,
253
+ commits: impl.commits,
254
+ blockers: [
255
+ ...review.findings.critical.map(finding => finding.title),
256
+ ...review.findings.warning.map(finding => finding.title),
257
+ ],
258
+ reason: 'Review recommended block.',
259
+ })
260
+ }
261
+
262
+ function verifyBlocked(story, impl, verify) {
263
+ return makeSprintStoryReturn({
264
+ storySlug: story.slug,
265
+ status: 'blocked',
266
+ branch: impl.branch,
267
+ commits: impl.commits,
268
+ blockers: [verify.notes || 'verification failed'],
269
+ reason: 'Verification failed.',
270
+ })
271
+ }
272
+
273
+ function dependencyBlocked(story, unmet) {
274
+ return makeSprintStoryReturn({
275
+ storySlug: story.slug,
276
+ status: 'blocked',
277
+ blockers: unmet.map(slug => `upstream story ${slug} did not complete`),
278
+ reason: 'Upstream in-sprint dependency did not complete.',
279
+ })
280
+ }
281
+
282
+ // Each story's terminal promise, registered before any chain runs so a
283
+ // dependent can always await its blocker regardless of array order.
284
+ const terminal = new Map()
285
+
286
+ async function runStory(story) {
287
+ const blockers = inBatchBlockers(story)
288
+ if (blockers.length) {
289
+ const outcomes = await Promise.all(blockers.map(slug => terminal.get(slug)))
290
+ const unmet = blockers.filter((_, index) => outcomes[index]?.status !== 'done')
291
+ if (unmet.length) {
292
+ log(`Skipping ${story.slug}: upstream ${unmet.join(', ')} did not complete.`)
293
+ return dependencyBlocked(story, unmet)
255
294
  }
256
- return agent(buildOpenPRPrompt(verify, story, impl), {
295
+ }
296
+
297
+ const impl = await agent(buildImplementPrompt(story), {
298
+ label: `impl:${story.slug}`,
299
+ phase: 'Implement',
300
+ schema: IMPL_RETURN_SCHEMA,
301
+ isolation: 'worktree',
302
+ })
303
+ if (!impl) return null
304
+
305
+ const review = await agent(buildReviewPrompt(impl, story), {
306
+ label: `review:${story.slug}`,
307
+ phase: 'Review',
308
+ schema: REVIEW_VERDICT_SCHEMA,
309
+ })
310
+ if (!review) return null
311
+ if (review.recommendation === 'block') return reviewBlocked(story, impl, review)
312
+
313
+ const verify = await agent(buildVerifyPrompt(review, story), {
314
+ label: `verify:${story.slug}`,
315
+ phase: 'Verify',
316
+ schema: VERIFY_RETURN_SCHEMA,
317
+ isolation: 'worktree',
318
+ })
319
+ if (!verify) return null
320
+ if (verify.verifyStatus === 'fail') return verifyBlocked(story, impl, verify)
321
+
322
+ const finalize = () =>
323
+ agent(buildOpenPRPrompt(verify, story, impl), {
257
324
  label: `pr:${story.slug}`,
258
325
  phase: 'Open PR',
259
326
  schema: SPRINT_STORY_RETURN_SCHEMA,
260
327
  })
261
- }
262
- )
263
328
 
329
+ // Local mode mutates the shared release branch — serialize. GitHub mode opens
330
+ // an independent PR and can run concurrently.
331
+ return backendMode === 'github' ? finalize() : serializeMerge(finalize)
332
+ }
333
+
334
+ // Open the gate only after every terminal promise is registered, so a
335
+ // dependent's blocker lookup never races an unregistered entry.
336
+ let openGate
337
+ const gate = new Promise(resolve => {
338
+ openGate = resolve
339
+ })
340
+ for (const story of stories) {
341
+ terminal.set(
342
+ story.slug,
343
+ // Isolate each chain: a thrown agent or stage becomes a 'failed' result
344
+ // rather than rejecting Promise.all and tearing down the whole batch. A
345
+ // failed (not 'done') outcome also correctly blocks any dependent.
346
+ gate.then(() => runStory(story)).catch(error => {
347
+ const detail = error?.message || String(error)
348
+ log(`Story ${story.slug} errored: ${detail}`)
349
+ return makeSprintStoryReturn({
350
+ storySlug: story.slug,
351
+ status: 'failed',
352
+ reason: `Unhandled error: ${detail}`,
353
+ })
354
+ })
355
+ )
356
+ }
357
+ openGate()
358
+
359
+ const results = await Promise.all(stories.map(story => terminal.get(story.slug)))
264
360
  return results.filter(Boolean)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houseofwolvesllc/claude-scrum-skill",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Claude Code skills for scrum project management — PRD to production release pipeline with project scaffolding, sprint planning, status tracking, sprint releases, full-project emulation testing, autonomous orchestration, and project cleanup.",
5
5
  "bin": {},
6
6
  "publishConfig": {
@@ -425,9 +425,11 @@ For each entry in the workflow's return:
425
425
  - `status: "blocked"` — Record `blockers[]` and `reason` in the state file. Add the `blocked` label to the story (or mark blocked locally). Continue.
426
426
  - `status: "failed"` — Same persistence as blocked, plus log the failure for sprint-release to roll over.
427
427
 
428
- #### Concurrency and barriers
428
+ #### Concurrency, isolation, and barriers
429
429
 
430
- The workflow runs up to `min(16, cpu_cores - 2)` stories concurrently with no per-stage barriers — each story's pipeline is independent, so one slow review doesn't gate other stories' implementations. The barrier-removal benefit is unconditional; the concurrency lift is conditional on host cores (a 4-core host gets concurrency 2, an 8-core host gets 6, a 18+-core host gets the full 16). On all hosts this still beats v1.x's hardcoded 3 + barriers model on most sprint sizes.
430
+ The workflow runs up to `min(16, cpu_cores - 2)` stories concurrently with no per-stage barriers — each story's chain is independent, so one slow review doesn't gate other stories' implementations. The barrier-removal benefit is unconditional; the concurrency lift is conditional on host cores (a 4-core host gets concurrency 2, an 8-core host gets 6, a 18+-core host gets the full 16). On all hosts this still beats v1.x's hardcoded 3 + barriers model on most sprint sizes.
431
+
432
+ Concurrent stories share one git repository, so the workflow keeps them off each other's toes with a single invariant: **the main working tree is mutated only by the serialized local-mode merge step.** Implement and verify run in isolated git worktrees (each story branches, commits, and builds in its own tree); review only diffs refs without checking out; and in local mode the per-story merges into the shared release branch are serialized behind a lock. This is why the **Independence check** (above) need not be perfect: even if two file-overlapping stories run together, they cannot corrupt each other's branch or commits. The workflow also re-enforces dependencies internally — a story whose `blocked_by` names another story *in the same batch* waits for that story to finish `done` before it starts, and branches from the release branch only after the blocker has merged.
431
433
 
432
434
  #### Progress updates
433
435