@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.
|
|
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
|
|
1
|
+
// sprint_pipeline.js — per-story sprint execution.
|
|
2
2
|
//
|
|
3
|
-
// Invoked by /project-orchestrate Phase 1 Step 3.
|
|
4
|
-
//
|
|
5
|
-
// to min(16, cpu_cores - 2) per the
|
|
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}
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|