@ikunin/sprintpilot 2.2.30 → 2.3.0
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/README.md +232 -413
- package/_Sprintpilot/Sprintpilot.md +76 -6
- package/_Sprintpilot/bin/autopilot.js +752 -66
- package/_Sprintpilot/lib/orchestrator/action-ledger.js +208 -0
- package/_Sprintpilot/lib/orchestrator/adapt.js +93 -15
- package/_Sprintpilot/lib/orchestrator/profile-rules.js +7 -16
- package/_Sprintpilot/lib/orchestrator/sprint-plan.js +488 -0
- package/_Sprintpilot/lib/orchestrator/state-store.js +9 -5
- package/_Sprintpilot/lib/orchestrator/user-command-applier.js +107 -0
- package/_Sprintpilot/lib/orchestrator/user-commands.js +124 -1
- package/_Sprintpilot/lib/orchestrator/verify.js +10 -17
- package/_Sprintpilot/manifest.yaml +4 -1
- package/_Sprintpilot/modules/autopilot/profiles/_base.yaml +18 -4
- package/_Sprintpilot/modules/git/config.yaml +15 -9
- package/_Sprintpilot/modules/ma/config.yaml +29 -27
- package/_Sprintpilot/scripts/dispatch-layer.js +12 -15
- package/_Sprintpilot/scripts/infer-dependencies.js +706 -254
- package/_Sprintpilot/scripts/log-timing.js +6 -10
- package/_Sprintpilot/scripts/merge-shards.js +21 -23
- package/_Sprintpilot/scripts/post-green-gates.js +3 -1
- package/_Sprintpilot/scripts/resolve-dag.js +452 -280
- package/_Sprintpilot/scripts/sprint-plan.js +1068 -0
- package/_Sprintpilot/scripts/state-shard.js +13 -5
- package/_Sprintpilot/scripts/summarize-timings.js +2 -3
- package/_Sprintpilot/skills/sprint-autopilot-on/SKILL.md +30 -2
- package/_Sprintpilot/skills/sprint-autopilot-on/workflow.orchestrator.md +36 -10
- package/_Sprintpilot/skills/sprintpilot-dependency-graph/SKILL.md +63 -0
- package/_Sprintpilot/skills/sprintpilot-dependency-graph/workflow.md +227 -0
- package/_Sprintpilot/skills/sprintpilot-plan-sprint/SKILL.md +67 -0
- package/_Sprintpilot/skills/sprintpilot-plan-sprint/workflow.md +435 -0
- package/_Sprintpilot/skills/sprintpilot-sprint-progress/SKILL.md +53 -0
- package/_Sprintpilot/skills/sprintpilot-sprint-progress/workflow.md +169 -0
- package/lib/commands/install.js +186 -10
- package/package.json +1 -1
|
@@ -19,6 +19,32 @@
|
|
|
19
19
|
// the stored alternative as the next action and clears the pending
|
|
20
20
|
// entry. Validation rejects this kind when no alternative is pending
|
|
21
21
|
// in state — see user-command-applier.js for the runtime check.
|
|
22
|
+
// trigger_retrospective { reason?: string }
|
|
23
|
+
// Force-routes the orchestrator into RETROSPECTIVE for the current
|
|
24
|
+
// epic regardless of `remaining_stories_in_epic`. Used when the user
|
|
25
|
+
// explicitly wants to close out an epic with deferred stories still
|
|
26
|
+
// in the queue (BMad has no formal `skipped`/`deferred` status for
|
|
27
|
+
// stories, so the orchestrator otherwise counts them as remaining
|
|
28
|
+
// and routes to next-story instead of retro).
|
|
29
|
+
//
|
|
30
|
+
// v2.3.0 — plan-aware mid-flight commands. These operate on
|
|
31
|
+
// sprint-plan.yaml via the Phase 2 primitives. DAG-aware validation
|
|
32
|
+
// lives in the applier (it needs the live plan + helper).
|
|
33
|
+
// reorder_queue { order: string[] }
|
|
34
|
+
// Rewrite priorities so the plan's pending stories match `order`.
|
|
35
|
+
// Validated against the DAG: every upstream of each story must be
|
|
36
|
+
// positioned BEFORE it OR plan-terminal. Inline edit — no phase
|
|
37
|
+
// change.
|
|
38
|
+
// add_to_sprint { story_keys: string[], position?: 'end'|'after:<key>'|<int>, issue_ids?: object }
|
|
39
|
+
// Add stories to plan.stories[]. Each key must exist in sprint-status,
|
|
40
|
+
// be non-terminal there, and not already in plan. Optional issue_ids
|
|
41
|
+
// map populates issue_id per added story.
|
|
42
|
+
// remove_from_sprint { story_keys: string[], mark_status?: 'skipped'|'deferred' }
|
|
43
|
+
// Mark stories with plan_status=skipped (default) or 'deferred'.
|
|
44
|
+
// Downstream-in-plan stories get a warning side effect.
|
|
45
|
+
// replan_sprint { reason?: string }
|
|
46
|
+
// Halt at next story_done boundary and emit invoke_skill for
|
|
47
|
+
// /sprintpilot-plan-sprint. The skill rebuilds the plan from scratch.
|
|
22
48
|
//
|
|
23
49
|
// Validation returns { ok: true, command } | { ok: false, errors: string[] }.
|
|
24
50
|
|
|
@@ -34,10 +60,17 @@ const COMMAND_KINDS = [
|
|
|
34
60
|
'change_profile',
|
|
35
61
|
'pause',
|
|
36
62
|
'accept_alternative',
|
|
63
|
+
'trigger_retrospective',
|
|
64
|
+
// v2.3.0 — plan-aware mid-flight commands.
|
|
65
|
+
'reorder_queue',
|
|
66
|
+
'add_to_sprint',
|
|
67
|
+
'remove_from_sprint',
|
|
68
|
+
'replan_sprint',
|
|
37
69
|
];
|
|
38
70
|
|
|
39
71
|
const STORY_KEY_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
40
72
|
const DECISION_ID_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
73
|
+
const VALID_REMOVE_STATUSES = ['skipped', 'deferred'];
|
|
41
74
|
|
|
42
75
|
function isPlainObject(v) {
|
|
43
76
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
@@ -73,11 +106,99 @@ function validateOne(cmd) {
|
|
|
73
106
|
case 'abort_sprint':
|
|
74
107
|
case 'force_continue':
|
|
75
108
|
case 'pause':
|
|
76
|
-
case 'accept_alternative':
|
|
109
|
+
case 'accept_alternative':
|
|
110
|
+
case 'trigger_retrospective': {
|
|
77
111
|
if ('reason' in cmd && cmd.reason !== undefined && typeof cmd.reason !== 'string')
|
|
78
112
|
errors.push(`${cmd.kind}.reason must be string when present`);
|
|
79
113
|
break;
|
|
80
114
|
}
|
|
115
|
+
case 'reorder_queue': {
|
|
116
|
+
if (!Array.isArray(cmd.order) || cmd.order.length === 0) {
|
|
117
|
+
errors.push('reorder_queue.order must be a non-empty array of story keys');
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
for (const k of cmd.order) {
|
|
122
|
+
if (!nonEmptyString(k) || !STORY_KEY_RE.test(k)) {
|
|
123
|
+
errors.push(`reorder_queue.order entry ${JSON.stringify(k)} must match [A-Za-z0-9._-]{1,64}`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (seen.has(k)) {
|
|
127
|
+
errors.push(`reorder_queue.order contains duplicate key ${JSON.stringify(k)}`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
seen.add(k);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case 'add_to_sprint': {
|
|
135
|
+
if (!Array.isArray(cmd.story_keys) || cmd.story_keys.length === 0) {
|
|
136
|
+
errors.push('add_to_sprint.story_keys must be a non-empty array');
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
for (const k of cmd.story_keys) {
|
|
140
|
+
if (!nonEmptyString(k) || !STORY_KEY_RE.test(k)) {
|
|
141
|
+
errors.push(
|
|
142
|
+
`add_to_sprint.story_keys entry ${JSON.stringify(k)} must match [A-Za-z0-9._-]{1,64}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (cmd.position !== undefined && cmd.position !== null) {
|
|
147
|
+
const p = cmd.position;
|
|
148
|
+
const isEnd = p === 'end';
|
|
149
|
+
const isAfter = typeof p === 'string' && p.startsWith('after:') && p.length > 6;
|
|
150
|
+
const isInt = typeof p === 'number' && Number.isFinite(p);
|
|
151
|
+
if (!isEnd && !isAfter && !isInt) {
|
|
152
|
+
errors.push(
|
|
153
|
+
"add_to_sprint.position must be 'end', 'after:<key>', or an integer index",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (cmd.issue_ids !== undefined && cmd.issue_ids !== null) {
|
|
158
|
+
if (!isPlainObject(cmd.issue_ids)) {
|
|
159
|
+
errors.push('add_to_sprint.issue_ids must be an object map { story_key: issue_id }');
|
|
160
|
+
} else {
|
|
161
|
+
for (const [k, v] of Object.entries(cmd.issue_ids)) {
|
|
162
|
+
if (!STORY_KEY_RE.test(k)) {
|
|
163
|
+
errors.push(
|
|
164
|
+
`add_to_sprint.issue_ids key ${JSON.stringify(k)} must match [A-Za-z0-9._-]{1,64}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (typeof v !== 'string' && v !== null) {
|
|
168
|
+
errors.push(`add_to_sprint.issue_ids[${k}] must be a string or null`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case 'remove_from_sprint': {
|
|
176
|
+
if (!Array.isArray(cmd.story_keys) || cmd.story_keys.length === 0) {
|
|
177
|
+
errors.push('remove_from_sprint.story_keys must be a non-empty array');
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
for (const k of cmd.story_keys) {
|
|
181
|
+
if (!nonEmptyString(k) || !STORY_KEY_RE.test(k)) {
|
|
182
|
+
errors.push(
|
|
183
|
+
`remove_from_sprint.story_keys entry ${JSON.stringify(k)} must match [A-Za-z0-9._-]{1,64}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (cmd.mark_status !== undefined && cmd.mark_status !== null) {
|
|
188
|
+
if (!VALID_REMOVE_STATUSES.includes(cmd.mark_status)) {
|
|
189
|
+
errors.push(
|
|
190
|
+
`remove_from_sprint.mark_status must be one of ${VALID_REMOVE_STATUSES.join(', ')}`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case 'replan_sprint': {
|
|
197
|
+
if ('reason' in cmd && cmd.reason !== undefined && typeof cmd.reason !== 'string') {
|
|
198
|
+
errors.push('replan_sprint.reason must be string when present');
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
81
202
|
case 'override_decision': {
|
|
82
203
|
if (!nonEmptyString(cmd.decision_id)) errors.push('override_decision.decision_id required');
|
|
83
204
|
else if (!DECISION_ID_RE.test(cmd.decision_id))
|
|
@@ -118,6 +239,8 @@ function validate(input) {
|
|
|
118
239
|
module.exports = {
|
|
119
240
|
COMMAND_KINDS,
|
|
120
241
|
VALID_PROFILE_NAMES,
|
|
242
|
+
VALID_REMOVE_STATUSES,
|
|
243
|
+
STORY_KEY_RE,
|
|
121
244
|
validate,
|
|
122
245
|
validateOne,
|
|
123
246
|
};
|
|
@@ -188,12 +188,11 @@ function runPostGreenGates(ctx) {
|
|
|
188
188
|
}
|
|
189
189
|
const cp = require('node:child_process');
|
|
190
190
|
const args = [scriptAbs, '--json', '--project-root', ctx.projectRoot];
|
|
191
|
-
// Forward output_limit
|
|
192
|
-
// existed but lint-changed.js used its hardcoded default of 100.
|
|
191
|
+
// Forward output_limit so lint-changed.js honors git.lint.output_limit.
|
|
193
192
|
if (typeof ctx.profile.lint_output_limit === 'number' && ctx.profile.lint_output_limit > 0) {
|
|
194
193
|
args.push('--output-limit', String(ctx.profile.lint_output_limit));
|
|
195
194
|
}
|
|
196
|
-
// Forward per-language linter map
|
|
195
|
+
// Forward the per-language linter map. Users reorder priorities or
|
|
197
196
|
// disable linters via git.lint.linters.{language}: [list].
|
|
198
197
|
if (ctx.profile.lint_linters && typeof ctx.profile.lint_linters === 'object') {
|
|
199
198
|
try {
|
|
@@ -459,10 +458,8 @@ function verifyDevGreen(state, out, ctx) {
|
|
|
459
458
|
}
|
|
460
459
|
// Post-GREEN gates: lint-changed + lint-test-pitfalls + ci-parity scan.
|
|
461
460
|
// Composed pipeline lives in scripts/post-green-gates.js. Only fires
|
|
462
|
-
// when profile.lint_enabled === true. Blocking vs non-blocking
|
|
463
|
-
// governed by profile.lint_blocking.
|
|
464
|
-
// and was documented as "called by the orchestrator after GREEN
|
|
465
|
-
// verify" but nothing actually invoked it.
|
|
461
|
+
// when profile.lint_enabled === true. Blocking vs non-blocking is
|
|
462
|
+
// governed by profile.lint_blocking.
|
|
466
463
|
const lintResult = runPostGreenGates(ctx);
|
|
467
464
|
if (lintResult) {
|
|
468
465
|
if (lintResult.failed && (ctx.profile && ctx.profile.lint_blocking)) {
|
|
@@ -476,17 +473,13 @@ function verifyDevGreen(state, out, ctx) {
|
|
|
476
473
|
|
|
477
474
|
function verifyCodeReview(state, out, ctx) {
|
|
478
475
|
const issues = [];
|
|
479
|
-
// bmad-code-review
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
// skill never creates one (recurring user pain: "review artifact missing:
|
|
484
|
-
// <path>" halts).
|
|
485
|
-
//
|
|
486
|
-
// Accept any of:
|
|
476
|
+
// bmad-code-review writes findings as a "### Review Findings"
|
|
477
|
+
// subsection inside the story file's Tasks/Subtasks block (see
|
|
478
|
+
// .claude/skills/bmad-code-review/steps/step-04-present.md). Older
|
|
479
|
+
// repo layouts also use a separate review file. Accept any of:
|
|
487
480
|
// - story file contains a `### Review Findings` section
|
|
488
|
-
// -
|
|
489
|
-
// -
|
|
481
|
+
// - `_bmad-output/reviews/<key>.md` exists
|
|
482
|
+
// - `_bmad-output/implementation-artifacts/code-review-<key>.md` exists
|
|
490
483
|
// Reject only when NONE of the above exist AND the LLM didn't supply
|
|
491
484
|
// findings[] inline.
|
|
492
485
|
const storyKey = state.story_key || 'unknown';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
addon:
|
|
2
2
|
name: sprintpilot
|
|
3
|
-
version: 2.
|
|
3
|
+
version: 2.3.0
|
|
4
4
|
description: Sprintpilot — autopilot and multi-agent addon for BMad Method (git workflow, parallel agents, autonomous story execution)
|
|
5
5
|
bmad_compatibility: ">=6.2.0"
|
|
6
6
|
modules:
|
|
@@ -24,3 +24,6 @@ addon:
|
|
|
24
24
|
- sprintpilot-migrate
|
|
25
25
|
- sprintpilot-research
|
|
26
26
|
- sprintpilot-party-mode
|
|
27
|
+
- sprintpilot-plan-sprint
|
|
28
|
+
- sprintpilot-sprint-progress
|
|
29
|
+
- sprintpilot-dependency-graph
|
|
@@ -18,10 +18,24 @@ autopilot:
|
|
|
18
18
|
coalesce_state_writes: true # M3, PR 6 — batch non-critical writes, flush at story boundary
|
|
19
19
|
conditional_boot_work: true # M4, PR 7 — skip health-check + branch reconciliation on a clean repo
|
|
20
20
|
cache_shared_reads: true # M5, PR 8 — memoize sprint-status / git-status / decision-log reads per loop iteration
|
|
21
|
-
# 2.0.2 —
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
|
|
21
|
+
# 2.0.2 — original dependency-inference knob. Pre-v2.3.0 this triggered
|
|
22
|
+
# an LLM-driven inference pass once after bmad-sprint-planning and wrote
|
|
23
|
+
# `_Sprintpilot/sprints/dependencies.yaml`. v2.3.0 replaces that flow:
|
|
24
|
+
# - The plan now lives at `_bmad-output/implementation-artifacts/sprint-plan.yaml`.
|
|
25
|
+
# - Inference is opt-in via /sprintpilot-plan-sprint, NOT automatic.
|
|
26
|
+
# - Greenfield runs (no plan, no --stories/--epic) execute stories in
|
|
27
|
+
# sprint-status order — same as today's resolveNextStoryKey path.
|
|
28
|
+
# Default flipped to false; users who relied on the old behavior should
|
|
29
|
+
# set `autopilot.auto_plan_on_start: true` (see below).
|
|
30
|
+
auto_infer_dependencies: false
|
|
31
|
+
|
|
32
|
+
# v2.3.0 — when true, cmdStart emits an `invoke_skill` action for
|
|
33
|
+
# /sprintpilot-plan-sprint on first run if no sprint-plan.yaml exists.
|
|
34
|
+
# Default `false`: missing plan → fall back to sprint-status order
|
|
35
|
+
# (existing behavior). Once a plan exists, staleness is detected and
|
|
36
|
+
# re-derive runs automatically regardless of this knob — flipping back
|
|
37
|
+
# to false is a one-shot opt-out for net-new projects only.
|
|
38
|
+
auto_plan_on_start: false
|
|
25
39
|
|
|
26
40
|
git:
|
|
27
41
|
granularity: story # story | epic
|
|
@@ -65,13 +65,18 @@ git:
|
|
|
65
65
|
story-title: "from story file title, fallback to story-key"
|
|
66
66
|
patch-title: "from review finding title, fallback to 'code review fix'"
|
|
67
67
|
|
|
68
|
-
# Linting
|
|
68
|
+
# Linting — runs after dev_green verify passes via
|
|
69
|
+
# scripts/post-green-gates.js (lint-changed + lint-test-pitfalls +
|
|
70
|
+
# ci-parity scan).
|
|
69
71
|
lint:
|
|
70
|
-
enabled: true
|
|
71
|
-
blocking: false # true = lint errors
|
|
72
|
-
output_limit: 100 # max lines of lint output injected into context
|
|
73
|
-
#
|
|
74
|
-
#
|
|
72
|
+
enabled: true # gate runs only when true
|
|
73
|
+
blocking: false # true = lint errors reject verify (LLM fix-loops); false = recorded but non-gating
|
|
74
|
+
output_limit: 100 # max lines of lint output injected back into context
|
|
75
|
+
# Per-language linter preference, first-installed wins. Empty list
|
|
76
|
+
# disables linting for that language. `javascript` and `typescript`
|
|
77
|
+
# merge into a single js-ts bucket (both share eslint/biome tooling).
|
|
78
|
+
# To add a new language, add an entry here AND a matching resolver
|
|
79
|
+
# block in scripts/lint-changed.js.
|
|
75
80
|
linters:
|
|
76
81
|
python: [ruff, flake8, pylint]
|
|
77
82
|
javascript: [eslint, biome]
|
|
@@ -87,7 +92,6 @@ git:
|
|
|
87
92
|
plsql: [sqlfluff]
|
|
88
93
|
kotlin: [ktlint, detekt]
|
|
89
94
|
php: [phpstan, phpcs]
|
|
90
|
-
# To add a new language: add an entry here and a matching block in scripts/lint-changed.js
|
|
91
95
|
|
|
92
96
|
# Push & PR
|
|
93
97
|
push:
|
|
@@ -103,9 +107,11 @@ git:
|
|
|
103
107
|
cleanup_on_merge: true # false = keep worktrees after epic completion for inspection
|
|
104
108
|
health_check_on_boot: true # check for orphaned worktrees from crashed sessions
|
|
105
109
|
|
|
106
|
-
# Lock file (.autopilot.lock — prevents concurrent autopilot sessions
|
|
110
|
+
# Lock file (.autopilot.lock — prevents concurrent autopilot sessions
|
|
111
|
+
# on the same project). Acquired by `autopilot start`; released by
|
|
112
|
+
# `/sprint-autopilot-off`.
|
|
107
113
|
lock:
|
|
108
|
-
stale_timeout_minutes: 30 # auto-
|
|
114
|
+
stale_timeout_minutes: 30 # auto-take-over locks older than this (0 disables)
|
|
109
115
|
|
|
110
116
|
# Platform detection
|
|
111
117
|
platform:
|
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
# Multi-Agent Configuration
|
|
2
2
|
#
|
|
3
|
-
# Top-level key MUST be `ma
|
|
3
|
+
# Top-level key MUST be `ma:`. resolve-profile.js merges this under the
|
|
4
4
|
# `ma` namespace, and profile-rules.js reads `ma.parallel_stories` /
|
|
5
|
-
# `ma.max_parallel_stories`.
|
|
6
|
-
# pre-2.2.16 versions was silently ignored (deep-merge produced
|
|
7
|
-
# `resolved.ma.multi_agent.*` instead of `resolved.ma.*`).
|
|
5
|
+
# `ma.max_parallel_stories`.
|
|
8
6
|
|
|
9
7
|
ma:
|
|
10
8
|
enabled: true
|
|
11
9
|
max_parallel_review_layers: 3 # Always 3: blind, edge-case, acceptance
|
|
12
10
|
max_parallel_research: 3 # Max concurrent research agents per batch
|
|
13
11
|
max_parallel_analysis: 5 # Max concurrent codebase analysis agents
|
|
14
|
-
# session_story_limit
|
|
12
|
+
# session_story_limit lives under autopilot.* — single source of truth.
|
|
15
13
|
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
14
|
+
# Intra-epic parallel story execution.
|
|
15
|
+
#
|
|
16
|
+
# parallel_stories: true enables the dispatch-layer.js building blocks
|
|
17
|
+
# (planBatch, resolve-dag.js, merge-shards.js, agent-adapter.js). The
|
|
18
|
+
# state machine emits stories one at a time; the autopilot logs a
|
|
19
|
+
# clear notice at session start when this flag is set so behavior is
|
|
20
|
+
# unambiguous. Intra-epic parallel emission is planned for a future
|
|
21
|
+
# minor release.
|
|
22
|
+
#
|
|
23
|
+
# max_parallel_stories: cap on concurrent sub-agents per layer.
|
|
24
|
+
# min_epic_duration_for_parallel_sec: skip parallelism for epics whose
|
|
25
|
+
# estimated wall-clock is below this threshold (saves dispatch overhead).
|
|
26
|
+
# max_consecutive_conflicts: disable parallelism for the rest of the
|
|
27
|
+
# session once N consecutive merge conflicts occur.
|
|
28
|
+
# effective_parallel_floor: never drop below this mid-session even
|
|
29
|
+
# after failure-driven concurrency reduction.
|
|
28
30
|
parallel_stories: false
|
|
29
31
|
max_parallel_stories: 2
|
|
30
32
|
min_epic_duration_for_parallel_sec: 300
|
|
@@ -32,25 +34,25 @@ ma:
|
|
|
32
34
|
max_consecutive_conflicts: 2
|
|
33
35
|
effective_parallel_floor: 1
|
|
34
36
|
|
|
35
|
-
#
|
|
37
|
+
# Experimental: parallel_stories dispatch on Gemini CLI.
|
|
36
38
|
#
|
|
37
39
|
# Gemini CLI has a subagent primitive (invoke_subagent) but its
|
|
38
|
-
# worktree-scoped variant is
|
|
39
|
-
# github.com/google-gemini/gemini-cli#22967)
|
|
40
|
+
# worktree-scoped variant is tracked upstream
|
|
41
|
+
# (github.com/google-gemini/gemini-cli#22967). Real-world parallelism
|
|
40
42
|
# reports serialization + quota throttling. Sprintpilot detects Gemini
|
|
41
43
|
# CLI via GEMINI_CLI=1 (env, HIGH confidence) or parent process `gemini`
|
|
42
|
-
# (MEDIUM)
|
|
44
|
+
# (MEDIUM); supports_parallel stays false by default.
|
|
43
45
|
#
|
|
44
|
-
# Flip to true PER PROJECT
|
|
45
|
-
#
|
|
46
|
-
#
|
|
46
|
+
# Flip to true PER PROJECT to opt into the experimental path. The
|
|
47
|
+
# workflow logs an "experimental parallel" warning once per session
|
|
48
|
+
# when this is true AND host=gemini-cli.
|
|
47
49
|
experimental_parallel_on_gemini: false
|
|
48
50
|
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
+
# Cross-epic parallelism (experimental). Off by default on every
|
|
52
|
+
# profile, including large. Enabling requires:
|
|
51
53
|
# 1. Both epics carry `independent: true` in dependencies.yaml.
|
|
52
54
|
# 2. preflight-merge.js reports no conflicts between them.
|
|
53
|
-
# 3. max_parallel_epics is
|
|
55
|
+
# 3. max_parallel_epics is fixed at 2 — no tuning knob.
|
|
54
56
|
# A single cross-epic merge conflict in a session disables parallel_epics
|
|
55
57
|
# for the rest of the session.
|
|
56
58
|
parallel_epics: false
|
|
@@ -79,10 +79,9 @@ function planLayer({ keys, maxParallel, projectRoot, branchPrefix, baseBranch })
|
|
|
79
79
|
}
|
|
80
80
|
const effectiveParallel = Math.max(1, Math.min(maxParallel | 0, dedupedKeys.length));
|
|
81
81
|
// CAP: only dispatch the first `effectiveParallel` stories. The
|
|
82
|
-
// remaining keys are deferred — the autopilot loop
|
|
83
|
-
// in the next iteration after this batch completes.
|
|
84
|
-
//
|
|
85
|
-
// the workflow spawned N agents anyway, fully ignoring --max-parallel.
|
|
82
|
+
// remaining keys are deferred — the autopilot loop picks them up
|
|
83
|
+
// in the next iteration after this batch completes. Honors
|
|
84
|
+
// --max-parallel as the upper bound on concurrent worktree creation.
|
|
86
85
|
const dispatchedKeys = dedupedKeys.slice(0, effectiveParallel);
|
|
87
86
|
const deferredKeys = dedupedKeys.slice(effectiveParallel);
|
|
88
87
|
const worktrees = dispatchedKeys.map((key) => ({
|
|
@@ -112,10 +111,10 @@ function writePlan(projectRoot, plan) {
|
|
|
112
111
|
}
|
|
113
112
|
|
|
114
113
|
// Match git's "branch already exists" diagnostic. We retry without -b
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
114
|
+
// ONLY when the first attempt failed for this specific reason. A bare
|
|
115
|
+
// retry on any other failure would silently check out whatever stale
|
|
116
|
+
// branch happened to exist at the requested name (e.g. last week's
|
|
117
|
+
// commits from an abandoned story).
|
|
119
118
|
const BRANCH_EXISTS_RE = /a branch named .* already exists/i;
|
|
120
119
|
|
|
121
120
|
function createWorktree({ projectRoot, worktree, branch, baseBranch }) {
|
|
@@ -147,11 +146,9 @@ function createWorktree({ projectRoot, worktree, branch, baseBranch }) {
|
|
|
147
146
|
};
|
|
148
147
|
}
|
|
149
148
|
|
|
150
|
-
// After a worktree is created, disable gc.auto on it
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// trigger gc on each worktree mid-dispatch. Best-effort — never block
|
|
154
|
-
// dispatch on a config write.
|
|
149
|
+
// After a worktree is created, disable gc.auto on it so concurrent
|
|
150
|
+
// sub-agents in heavy repos don't trigger gc on each worktree mid-
|
|
151
|
+
// dispatch. Best-effort — never block dispatch on a config write.
|
|
155
152
|
function disableGcAutoOnWorktree(worktree) {
|
|
156
153
|
spawnSync('git', ['-C', worktree, 'config', '--local', 'gc.auto', '0'], {
|
|
157
154
|
encoding: 'utf8',
|
|
@@ -197,8 +194,8 @@ function dispatch({ keys, maxParallel, projectRoot, branchPrefix, baseBranch, dr
|
|
|
197
194
|
return results;
|
|
198
195
|
}
|
|
199
196
|
// Real dispatch. Track successful creates so we can roll them back if
|
|
200
|
-
// a later create fails —
|
|
201
|
-
//
|
|
197
|
+
// a later create fails — partial success would leave orphan worktrees
|
|
198
|
+
// alongside a plan file that claims everything succeeded.
|
|
202
199
|
const succeeded = [];
|
|
203
200
|
let failureIndex = -1;
|
|
204
201
|
for (let i = 0; i < plan.stories.length; i++) {
|