@ikunin/sprintpilot 2.1.2 → 2.1.4
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/_Sprintpilot/bin/autopilot.js +353 -47
- package/_Sprintpilot/lib/orchestrator/adapt.js +21 -2
- package/_Sprintpilot/lib/orchestrator/git-plan.js +522 -36
- package/_Sprintpilot/lib/orchestrator/land.js +11 -1
- package/_Sprintpilot/lib/orchestrator/profile-rules.js +65 -1
- package/_Sprintpilot/lib/orchestrator/state-machine.js +183 -4
- package/_Sprintpilot/manifest.yaml +1 -1
- package/_Sprintpilot/modules/git/config.yaml +8 -0
- package/_Sprintpilot/scripts/create-pr.js +178 -7
- package/_Sprintpilot/scripts/run-step.js +221 -0
- package/_Sprintpilot/skills/sprint-autopilot-on/workflow.orchestrator.md +35 -1
- package/package.json +1 -1
|
@@ -71,18 +71,28 @@ function planLand(state, profile, options) {
|
|
|
71
71
|
description: `snapshot stack for ${branch}`,
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
// Step 2: wait for CI / review depending on land_when.
|
|
74
|
+
// Step 2: wait for CI / review depending on land_when. Honors
|
|
75
|
+
// git.platform.provider (forwarded via --platform) so non-github
|
|
76
|
+
// providers route to the correct CLI / API path inside create-pr.js.
|
|
75
77
|
if (landWhen === 'ci_pass' || landWhen === 'ci_and_review') {
|
|
78
|
+
const platform = (profile.platform_provider || options.platform || 'auto');
|
|
76
79
|
const checkArgs = [
|
|
77
80
|
'node',
|
|
78
81
|
createPr,
|
|
79
82
|
'--mode',
|
|
80
83
|
'checks',
|
|
84
|
+
'--platform',
|
|
85
|
+
platform,
|
|
81
86
|
'--branch',
|
|
82
87
|
branch,
|
|
88
|
+
'--base',
|
|
89
|
+
baseBranch,
|
|
83
90
|
'--wait-minutes',
|
|
84
91
|
String(waitMinutes),
|
|
85
92
|
];
|
|
93
|
+
if (profile.platform_base_url) {
|
|
94
|
+
checkArgs.push('--base-url', profile.platform_base_url);
|
|
95
|
+
}
|
|
86
96
|
if (landWhen === 'ci_and_review') {
|
|
87
97
|
checkArgs.push('--require-approved-review');
|
|
88
98
|
}
|
|
@@ -18,6 +18,10 @@ const VALID_RETRO_MODES = ['auto', 'stop', 'skip'];
|
|
|
18
18
|
const VALID_GRANULARITIES = ['story', 'epic'];
|
|
19
19
|
const VALID_MERGE_STRATEGIES = ['stacked', 'land_as_you_go'];
|
|
20
20
|
const VALID_LAND_WHENS = ['no_wait', 'ci_pass', 'ci_and_review'];
|
|
21
|
+
const VALID_PLATFORM_PROVIDERS = ['auto', 'github', 'gitlab', 'bitbucket', 'gitea', 'git_only'];
|
|
22
|
+
|
|
23
|
+
const DEFAULT_COMMIT_TEMPLATE_STORY = 'feat({epic}): {story-title} ({story-key})';
|
|
24
|
+
const DEFAULT_COMMIT_TEMPLATE_PATCH = 'fix({story-key}): {patch-title}';
|
|
21
25
|
|
|
22
26
|
// Per-profile defaults for fields the orchestrator manages directly
|
|
23
27
|
// (verify_reject_budget, retry_budget_per_action). These are orchestrator-
|
|
@@ -59,6 +63,16 @@ function coerceEnum(v, allowed, fallback) {
|
|
|
59
63
|
return fallback;
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
// Compute land_wait_minutes (default 30) and epic_merge_wait_minutes
|
|
67
|
+
// (falls back to land_wait_minutes). Kept as a helper so the fallback
|
|
68
|
+
// chain is readable; inlining produced a nested coerceInt call that
|
|
69
|
+
// was hard to scan.
|
|
70
|
+
function resolveWaitMinutes(resolved) {
|
|
71
|
+
const land = coerceInt(get(resolved, 'git.land_wait_minutes'), 30);
|
|
72
|
+
const epic = coerceInt(get(resolved, 'git.epic_merge_wait_minutes'), land);
|
|
73
|
+
return { land_wait_minutes: land, epic_merge_wait_minutes: epic };
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
// Convert the flat resolved-config tree (from resolve-profile.js) into a
|
|
63
77
|
// typed Profile. Missing keys fall back to documented defaults.
|
|
64
78
|
function flatToProfile(resolved, profileName) {
|
|
@@ -90,7 +104,7 @@ function flatToProfile(resolved, profileName) {
|
|
|
90
104
|
'stacked',
|
|
91
105
|
),
|
|
92
106
|
land_when: coerceEnum(get(resolved, 'git.land_when'), VALID_LAND_WHENS, 'ci_pass'),
|
|
93
|
-
|
|
107
|
+
...resolveWaitMinutes(resolved),
|
|
94
108
|
base_branch:
|
|
95
109
|
typeof get(resolved, 'git.base_branch') === 'string'
|
|
96
110
|
? get(resolved, 'git.base_branch')
|
|
@@ -99,6 +113,53 @@ function flatToProfile(resolved, profileName) {
|
|
|
99
113
|
typeof get(resolved, 'git.branch_prefix') === 'string'
|
|
100
114
|
? get(resolved, 'git.branch_prefix')
|
|
101
115
|
: 'story/',
|
|
116
|
+
// git.enabled — when false, every `git_op` action emitted by the
|
|
117
|
+
// state machine is replaced with a `noop` at decorateGitOp time.
|
|
118
|
+
// Used for evaluation / dry-run setups where the user wants the
|
|
119
|
+
// BMad cycle to run but doesn't want any commits / pushes / PRs.
|
|
120
|
+
enabled: coerceBool(get(resolved, 'git.enabled'), true),
|
|
121
|
+
// git.push.auto — when false, planCommitAndPush drops the push
|
|
122
|
+
// steps (both story-branch and base-branch). Branches stay local.
|
|
123
|
+
push_auto: coerceBool(get(resolved, 'git.push.auto'), true),
|
|
124
|
+
// git.push.create_pr — when true (and merge_strategy=stacked),
|
|
125
|
+
// planCommitAndPush appends a `create-pr.js` step after the push so
|
|
126
|
+
// each story branch gets one PR opened automatically. land_as_you_go
|
|
127
|
+
// already opens its own PRs via land.js, so this knob doesn't gate
|
|
128
|
+
// that path.
|
|
129
|
+
push_create_pr: coerceBool(get(resolved, 'git.push.create_pr'), true),
|
|
130
|
+
pr_template_path:
|
|
131
|
+
typeof get(resolved, 'git.push.pr_template') === 'string'
|
|
132
|
+
? get(resolved, 'git.push.pr_template')
|
|
133
|
+
: null,
|
|
134
|
+
// Commit message templates. Placeholders expanded in git-plan.js:
|
|
135
|
+
// {story-key} — state.story_key
|
|
136
|
+
// {epic} — state.current_epic (or derived from story_key)
|
|
137
|
+
// {story-title} — state.ac_summary or story_key as fallback
|
|
138
|
+
// {patch-title} — set on patch commits (bmad-dev-story owns those)
|
|
139
|
+
commit_template_story:
|
|
140
|
+
typeof get(resolved, 'git.commit_templates.story') === 'string'
|
|
141
|
+
? get(resolved, 'git.commit_templates.story')
|
|
142
|
+
: DEFAULT_COMMIT_TEMPLATE_STORY,
|
|
143
|
+
commit_template_patch:
|
|
144
|
+
typeof get(resolved, 'git.commit_templates.patch') === 'string'
|
|
145
|
+
? get(resolved, 'git.commit_templates.patch')
|
|
146
|
+
: DEFAULT_COMMIT_TEMPLATE_PATCH,
|
|
147
|
+
// git.max_branch_length — branchName() truncates long branch names
|
|
148
|
+
// (story keys + prefix) to this length with a 6-char hash suffix to
|
|
149
|
+
// keep the name unique. Honors the contract advertised in config.yaml.
|
|
150
|
+
max_branch_length: coerceInt(get(resolved, 'git.max_branch_length'), 60),
|
|
151
|
+
// git.platform.provider + base_url — forwarded to create-pr.js when
|
|
152
|
+
// the orchestrator opens or polls PRs. 'auto' delegates platform
|
|
153
|
+
// detection to create-pr.js (currently defaults to github).
|
|
154
|
+
platform_provider: coerceEnum(
|
|
155
|
+
get(resolved, 'git.platform.provider'),
|
|
156
|
+
VALID_PLATFORM_PROVIDERS,
|
|
157
|
+
'auto',
|
|
158
|
+
),
|
|
159
|
+
platform_base_url:
|
|
160
|
+
typeof get(resolved, 'git.platform.base_url') === 'string'
|
|
161
|
+
? get(resolved, 'git.platform.base_url')
|
|
162
|
+
: null,
|
|
102
163
|
parallel_stories: coerceBool(get(resolved, 'ma.parallel_stories'), false),
|
|
103
164
|
max_parallel_stories: coerceInt(get(resolved, 'ma.max_parallel_stories'), 2),
|
|
104
165
|
fallback_on_tests_fail: coerceBool(
|
|
@@ -161,6 +222,9 @@ module.exports = {
|
|
|
161
222
|
VALID_GRANULARITIES,
|
|
162
223
|
VALID_MERGE_STRATEGIES,
|
|
163
224
|
VALID_LAND_WHENS,
|
|
225
|
+
VALID_PLATFORM_PROVIDERS,
|
|
226
|
+
DEFAULT_COMMIT_TEMPLATE_STORY,
|
|
227
|
+
DEFAULT_COMMIT_TEMPLATE_PATCH,
|
|
164
228
|
ORCHESTRATOR_DEFAULTS_BY_PROFILE,
|
|
165
229
|
flatToProfile,
|
|
166
230
|
escalateOnFailure,
|
|
@@ -41,6 +41,14 @@
|
|
|
41
41
|
'use strict';
|
|
42
42
|
|
|
43
43
|
const STATES = Object.freeze({
|
|
44
|
+
// PREPARE_STORY_BRANCH — emitted as the first state of a fresh story
|
|
45
|
+
// when the active git settings require a per-story or per-epic branch
|
|
46
|
+
// (granularity ∈ {story, epic} AND !reuse_user_branch). Resolves to a
|
|
47
|
+
// git_op with op: 'create_branch' so the story file itself is authored
|
|
48
|
+
// on the story branch. Under `reuse_user_branch: true` this state is
|
|
49
|
+
// skipped and CREATE_STORY / NANO_QUICK_DEV runs directly on the
|
|
50
|
+
// user-locked branch.
|
|
51
|
+
PREPARE_STORY_BRANCH: 'prepare_story_branch',
|
|
44
52
|
CREATE_STORY: 'create_story',
|
|
45
53
|
CHECK_READINESS: 'check_readiness',
|
|
46
54
|
DEV_RED: 'dev_red',
|
|
@@ -54,6 +62,16 @@ const STATES = Object.freeze({
|
|
|
54
62
|
// story's PR into base. Skipped (STORY_DONE → EPIC_BOUNDARY_CHECK directly)
|
|
55
63
|
// under the default 'stacked' strategy.
|
|
56
64
|
STORY_LAND: 'story_land',
|
|
65
|
+
// MERGE_EPIC — entered from EPIC_BOUNDARY_CHECK when:
|
|
66
|
+
// • end-of-epic (remaining_stories_in_epic === 0)
|
|
67
|
+
// • granularity === 'epic'
|
|
68
|
+
// • merge_strategy === 'stacked'
|
|
69
|
+
// • push_auto === true, has_origin !== false, !reuse_user_branch
|
|
70
|
+
// Closes out the epic branch by either merging its PR (push_create_pr=
|
|
71
|
+
// true) or local-merging directly to base (push_create_pr=false). The
|
|
72
|
+
// existing planMergeEpic builds the local-merge sequence; planMergeEpicPr
|
|
73
|
+
// builds the gh-cli sequence.
|
|
74
|
+
MERGE_EPIC: 'merge_epic',
|
|
57
75
|
EPIC_BOUNDARY_CHECK: 'epic_boundary_check',
|
|
58
76
|
RETROSPECTIVE: 'retrospective',
|
|
59
77
|
SPRINT_FINALIZE_PENDING: 'sprint_finalize_pending',
|
|
@@ -68,6 +86,7 @@ const TERMINAL_STATES = new Set([STATES.SPRINT_FINALIZE_PENDING]);
|
|
|
68
86
|
// edges (e.g. patch_apply only when findings.action==='patch') are
|
|
69
87
|
// enforced in `nextStateAfterSuccess`.
|
|
70
88
|
const FULL_FLOW_SUCCESSORS = {
|
|
89
|
+
[STATES.PREPARE_STORY_BRANCH]: [STATES.CREATE_STORY],
|
|
71
90
|
[STATES.CREATE_STORY]: [STATES.CHECK_READINESS],
|
|
72
91
|
[STATES.CHECK_READINESS]: [STATES.DEV_RED],
|
|
73
92
|
[STATES.DEV_RED]: [STATES.DEV_GREEN],
|
|
@@ -77,16 +96,39 @@ const FULL_FLOW_SUCCESSORS = {
|
|
|
77
96
|
[STATES.PATCH_RETEST]: [STATES.CODE_REVIEW, STATES.STORY_DONE], // conditional (re-review if still blocking)
|
|
78
97
|
[STATES.STORY_DONE]: [STATES.STORY_LAND, STATES.EPIC_BOUNDARY_CHECK], // STORY_LAND only under land_as_you_go
|
|
79
98
|
[STATES.STORY_LAND]: [STATES.EPIC_BOUNDARY_CHECK],
|
|
80
|
-
[STATES.EPIC_BOUNDARY_CHECK]: [
|
|
81
|
-
|
|
99
|
+
[STATES.EPIC_BOUNDARY_CHECK]: [
|
|
100
|
+
STATES.MERGE_EPIC,
|
|
101
|
+
STATES.RETROSPECTIVE,
|
|
102
|
+
STATES.PREPARE_STORY_BRANCH,
|
|
103
|
+
STATES.CREATE_STORY,
|
|
104
|
+
STATES.SPRINT_FINALIZE_PENDING,
|
|
105
|
+
],
|
|
106
|
+
[STATES.MERGE_EPIC]: [STATES.RETROSPECTIVE, STATES.SPRINT_FINALIZE_PENDING],
|
|
107
|
+
[STATES.RETROSPECTIVE]: [
|
|
108
|
+
STATES.PREPARE_STORY_BRANCH,
|
|
109
|
+
STATES.CREATE_STORY,
|
|
110
|
+
STATES.SPRINT_FINALIZE_PENDING,
|
|
111
|
+
],
|
|
82
112
|
};
|
|
83
113
|
|
|
84
114
|
const NANO_FLOW_SUCCESSORS = {
|
|
115
|
+
[STATES.PREPARE_STORY_BRANCH]: [STATES.NANO_QUICK_DEV],
|
|
85
116
|
[STATES.NANO_QUICK_DEV]: [STATES.STORY_DONE],
|
|
86
117
|
[STATES.STORY_DONE]: [STATES.STORY_LAND, STATES.EPIC_BOUNDARY_CHECK],
|
|
87
118
|
[STATES.STORY_LAND]: [STATES.EPIC_BOUNDARY_CHECK],
|
|
88
|
-
[STATES.EPIC_BOUNDARY_CHECK]: [
|
|
89
|
-
|
|
119
|
+
[STATES.EPIC_BOUNDARY_CHECK]: [
|
|
120
|
+
STATES.MERGE_EPIC,
|
|
121
|
+
STATES.RETROSPECTIVE,
|
|
122
|
+
STATES.PREPARE_STORY_BRANCH,
|
|
123
|
+
STATES.NANO_QUICK_DEV,
|
|
124
|
+
STATES.SPRINT_FINALIZE_PENDING,
|
|
125
|
+
],
|
|
126
|
+
[STATES.MERGE_EPIC]: [STATES.RETROSPECTIVE, STATES.SPRINT_FINALIZE_PENDING],
|
|
127
|
+
[STATES.RETROSPECTIVE]: [
|
|
128
|
+
STATES.PREPARE_STORY_BRANCH,
|
|
129
|
+
STATES.NANO_QUICK_DEV,
|
|
130
|
+
STATES.SPRINT_FINALIZE_PENDING,
|
|
131
|
+
],
|
|
90
132
|
};
|
|
91
133
|
|
|
92
134
|
// Build instruction template content slots from state + profile. This is the
|
|
@@ -130,6 +172,45 @@ function nextAction(state, profile) {
|
|
|
130
172
|
}
|
|
131
173
|
|
|
132
174
|
switch (state.phase) {
|
|
175
|
+
case STATES.PREPARE_STORY_BRANCH: {
|
|
176
|
+
// Safety net: PREPARE_STORY_BRANCH needs a known story_key (and,
|
|
177
|
+
// under granularity=epic, a current_epic) so git-plan.branchName
|
|
178
|
+
// can compute a real branch. composeRuntimeState resolves these
|
|
179
|
+
// from sprint-status.yaml before we get here — but if BOTH are
|
|
180
|
+
// null (sprint-status was empty, unreadable, or doesn't exist
|
|
181
|
+
// yet) we'd emit `branch: story/unknown` and confuse the runner.
|
|
182
|
+
// Emit a user_prompt instead so the user fixes the upstream
|
|
183
|
+
// condition (run BMad sprint-planning) rather than acting on a
|
|
184
|
+
// garbage action.
|
|
185
|
+
const haveStoryKey = !!state.story_key;
|
|
186
|
+
const haveEpicForBranch =
|
|
187
|
+
profile.granularity === 'epic' && !!state.current_epic;
|
|
188
|
+
if (!haveStoryKey && !haveEpicForBranch) {
|
|
189
|
+
return {
|
|
190
|
+
type: 'user_prompt',
|
|
191
|
+
phase: state.phase,
|
|
192
|
+
reason: 'prepare_story_branch_no_story_key',
|
|
193
|
+
prompt:
|
|
194
|
+
'PREPARE_STORY_BRANCH was emitted but the orchestrator could not resolve a next story_key from sprint-status.yaml. ' +
|
|
195
|
+
'Either run BMad sprint-planning to populate sprint-status.yaml, set `git.reuse_user_branch: true` in modules/git/config.yaml to commit on the current branch, ' +
|
|
196
|
+
'or set `git.enabled: false` for a dry run without git operations.',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// The edge layer (autopilot.js#decorateGitOp) inlines the planned
|
|
200
|
+
// argv steps via git-plan.js#planCreateBranch. It also probes git
|
|
201
|
+
// for branch existence and threads `state.branch_exists` through
|
|
202
|
+
// so the plan can degrade `git switch -c` to `git switch` when the
|
|
203
|
+
// branch already exists (e.g. second story under granularity=epic,
|
|
204
|
+
// or resume after partial failure).
|
|
205
|
+
return {
|
|
206
|
+
type: 'git_op',
|
|
207
|
+
phase: state.phase,
|
|
208
|
+
op: 'create_branch',
|
|
209
|
+
story_key: state.story_key,
|
|
210
|
+
epic_key: state.current_epic,
|
|
211
|
+
profile: profile.name,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
133
214
|
case STATES.CREATE_STORY:
|
|
134
215
|
return {
|
|
135
216
|
type: 'invoke_skill',
|
|
@@ -194,6 +275,19 @@ function nextAction(state, profile) {
|
|
|
194
275
|
story_key: state.story_key,
|
|
195
276
|
profile: profile.name,
|
|
196
277
|
};
|
|
278
|
+
case STATES.MERGE_EPIC:
|
|
279
|
+
// Edge layer (decorateGitOp) inlines the argv steps via
|
|
280
|
+
// git-plan.js#planMergeEpic. The plan branches internally on
|
|
281
|
+
// profile.push_create_pr to choose `gh pr merge --squash` vs the
|
|
282
|
+
// local-merge sequence.
|
|
283
|
+
return {
|
|
284
|
+
type: 'git_op',
|
|
285
|
+
phase: state.phase,
|
|
286
|
+
op: 'merge_epic',
|
|
287
|
+
story_key: state.story_key,
|
|
288
|
+
epic_key: state.current_epic,
|
|
289
|
+
profile: profile.name,
|
|
290
|
+
};
|
|
197
291
|
case STATES.STORY_LAND:
|
|
198
292
|
// Land-as-you-go: orchestrator plumbing emits a `run_script` that
|
|
199
293
|
// wraps the existing stack-snapshot.js + land-this-pr.js scripts.
|
|
@@ -288,6 +382,13 @@ function nextStateAfterSuccess(currentState, profile, signal) {
|
|
|
288
382
|
function deterministicNext(state, profile, output) {
|
|
289
383
|
const phase = state.phase;
|
|
290
384
|
switch (phase) {
|
|
385
|
+
case STATES.PREPARE_STORY_BRANCH: {
|
|
386
|
+
// Branch is on disk → enter the actual story work (flow-dependent).
|
|
387
|
+
const next = profile.implementation_flow === 'quick'
|
|
388
|
+
? STATES.NANO_QUICK_DEV
|
|
389
|
+
: STATES.CREATE_STORY;
|
|
390
|
+
return { chosen: next, allValid: [next] };
|
|
391
|
+
}
|
|
291
392
|
case STATES.CREATE_STORY:
|
|
292
393
|
return { chosen: STATES.CHECK_READINESS, allValid: [STATES.CHECK_READINESS] };
|
|
293
394
|
case STATES.CHECK_READINESS:
|
|
@@ -330,6 +431,12 @@ function deterministicNext(state, profile, output) {
|
|
|
330
431
|
const sprintDone = !!state.sprint_is_complete;
|
|
331
432
|
// End of epic?
|
|
332
433
|
if (remainingInEpic <= 0) {
|
|
434
|
+
// Epic merge: granularity=epic + stacked + autoremote-push +
|
|
435
|
+
// !reuse_user_branch + git enabled → close out the epic branch
|
|
436
|
+
// (MERGE_EPIC) before retrospective / next-epic routing.
|
|
437
|
+
if (epicMergeNeeded(profile)) {
|
|
438
|
+
return { chosen: STATES.MERGE_EPIC, allValid: [STATES.MERGE_EPIC] };
|
|
439
|
+
}
|
|
333
440
|
if (profile.retrospective_mode === 'skip') {
|
|
334
441
|
return {
|
|
335
442
|
chosen: sprintDone ? STATES.SPRINT_FINALIZE_PENDING : nextStoryStart(profile),
|
|
@@ -341,6 +448,22 @@ function deterministicNext(state, profile, output) {
|
|
|
341
448
|
// More stories in the same epic.
|
|
342
449
|
return { chosen: nextStoryStart(profile), allValid: [nextStoryStart(profile)] };
|
|
343
450
|
}
|
|
451
|
+
case STATES.MERGE_EPIC: {
|
|
452
|
+
const sprintDone = !!state.sprint_is_complete;
|
|
453
|
+
const successor = nextStoryStart(profile);
|
|
454
|
+
// Structurally-valid successors per FULL/NANO_FLOW_SUCCESSORS:
|
|
455
|
+
// [RETROSPECTIVE, SPRINT_FINALIZE_PENDING]. Include the next-story
|
|
456
|
+
// start under retro=skip so the hint tiebreaker can route to it
|
|
457
|
+
// when the LLM has a strong opinion.
|
|
458
|
+
const allValid = [STATES.RETROSPECTIVE, STATES.SPRINT_FINALIZE_PENDING, successor];
|
|
459
|
+
if (profile.retrospective_mode === 'skip') {
|
|
460
|
+
return {
|
|
461
|
+
chosen: sprintDone ? STATES.SPRINT_FINALIZE_PENDING : successor,
|
|
462
|
+
allValid,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return { chosen: STATES.RETROSPECTIVE, allValid };
|
|
466
|
+
}
|
|
344
467
|
case STATES.RETROSPECTIVE: {
|
|
345
468
|
const sprintDone = !!state.sprint_is_complete;
|
|
346
469
|
const chosen = sprintDone ? STATES.SPRINT_FINALIZE_PENDING : nextStoryStart(profile);
|
|
@@ -355,13 +478,66 @@ function deterministicNext(state, profile, output) {
|
|
|
355
478
|
}
|
|
356
479
|
}
|
|
357
480
|
|
|
481
|
+
// nextStoryStart(profile) — the first phase of a fresh story.
|
|
482
|
+
//
|
|
483
|
+
// Under settings that require a per-story or per-epic branch
|
|
484
|
+
// (granularity ∈ {story, epic} AND !reuse_user_branch) the very first
|
|
485
|
+
// phase is PREPARE_STORY_BRANCH so the branch exists before the story
|
|
486
|
+
// file is authored. Otherwise we skip straight to the flow-appropriate
|
|
487
|
+
// implementation step.
|
|
488
|
+
//
|
|
489
|
+
// The `reuse_user_branch: true` path is handled by autopilot.js#cmdStart
|
|
490
|
+
// which detects the current branch and locks it in via state.user_branch;
|
|
491
|
+
// PREPARE_STORY_BRANCH is unnecessary in that mode (every story commits
|
|
492
|
+
// to the same already-checked-out branch).
|
|
493
|
+
// epicMergeNeeded(profile) — true when EPIC_BOUNDARY_CHECK at end-of-epic
|
|
494
|
+
// should route through MERGE_EPIC instead of jumping straight to
|
|
495
|
+
// retrospective / next-story. Matches the same triggers as the per-story
|
|
496
|
+
// PR/merge in planCommitAndPush, just shifted to the epic-branch flow.
|
|
497
|
+
function epicMergeNeeded(profile) {
|
|
498
|
+
return (
|
|
499
|
+
profile &&
|
|
500
|
+
profile.enabled !== false &&
|
|
501
|
+
!profile.reuse_user_branch &&
|
|
502
|
+
profile.granularity === 'epic' &&
|
|
503
|
+
(profile.merge_strategy || 'stacked') === 'stacked' &&
|
|
504
|
+
profile.push_auto !== false &&
|
|
505
|
+
profile.has_origin !== false
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// shouldSkipVerifyWhenGitDisabled(phase) — true when verify.js should
|
|
510
|
+
// be bypassed under `git.enabled: false`. This covers phases that emit
|
|
511
|
+
// a git_op (PREPARE_STORY_BRANCH, STORY_DONE, MERGE_EPIC) and STORY_LAND
|
|
512
|
+
// (which emits a run_script but reports the same kind of post-merge
|
|
513
|
+
// bookkeeping that requires git operations to have happened).
|
|
514
|
+
//
|
|
515
|
+
// Centralizing the list here means new phases of either kind won't
|
|
516
|
+
// silently miss the verify-skip wiring — add them to the set below.
|
|
517
|
+
// (Previously named `isGitOpPhase`; renamed because STORY_LAND is not
|
|
518
|
+
// strictly a git_op and the old name lied.)
|
|
519
|
+
const GIT_INTERACTING_PHASES = new Set([
|
|
520
|
+
STATES.PREPARE_STORY_BRANCH,
|
|
521
|
+
STATES.STORY_DONE,
|
|
522
|
+
STATES.MERGE_EPIC,
|
|
523
|
+
STATES.STORY_LAND,
|
|
524
|
+
]);
|
|
525
|
+
function shouldSkipVerifyWhenGitDisabled(phase) {
|
|
526
|
+
return GIT_INTERACTING_PHASES.has(phase);
|
|
527
|
+
}
|
|
528
|
+
|
|
358
529
|
function nextStoryStart(profile) {
|
|
530
|
+
const needsBranchPrep =
|
|
531
|
+
!profile.reuse_user_branch &&
|
|
532
|
+
(profile.granularity === 'story' || profile.granularity === 'epic');
|
|
533
|
+
if (needsBranchPrep) return STATES.PREPARE_STORY_BRANCH;
|
|
359
534
|
return profile.implementation_flow === 'quick' ? STATES.NANO_QUICK_DEV : STATES.CREATE_STORY;
|
|
360
535
|
}
|
|
361
536
|
|
|
362
537
|
// Best-effort mapping from a next_skill_hint string (e.g. "bmad-code-review")
|
|
363
538
|
// to a phase identifier. Used only as a tiebreaker.
|
|
364
539
|
const HINT_TO_PHASE = {
|
|
540
|
+
prepare_story_branch: STATES.PREPARE_STORY_BRANCH,
|
|
365
541
|
'bmad-create-story': STATES.CREATE_STORY,
|
|
366
542
|
'bmad-check-implementation-readiness': STATES.CHECK_READINESS,
|
|
367
543
|
'bmad-dev-story:red': STATES.DEV_RED,
|
|
@@ -372,6 +548,7 @@ const HINT_TO_PHASE = {
|
|
|
372
548
|
'bmad-retrospective': STATES.RETROSPECTIVE,
|
|
373
549
|
'bmad-quick-dev': STATES.NANO_QUICK_DEV,
|
|
374
550
|
story_done: STATES.STORY_DONE,
|
|
551
|
+
merge_epic: STATES.MERGE_EPIC,
|
|
375
552
|
sprint_finalize_pending: STATES.SPRINT_FINALIZE_PENDING,
|
|
376
553
|
};
|
|
377
554
|
|
|
@@ -396,6 +573,8 @@ module.exports = {
|
|
|
396
573
|
nextStateAfterSuccess,
|
|
397
574
|
// Exposed for adapt.js to construct fresh-story states.
|
|
398
575
|
nextStoryStart,
|
|
576
|
+
// Exposed for autopilot.js verify-skip routing.
|
|
577
|
+
shouldSkipVerifyWhenGitDisabled,
|
|
399
578
|
// Exposed for tests / inspection.
|
|
400
579
|
buildTemplateSlots,
|
|
401
580
|
HINT_TO_PHASE,
|
|
@@ -40,6 +40,14 @@ git:
|
|
|
40
40
|
# the orchestrator halts and prompts the user.
|
|
41
41
|
land_wait_minutes: 30
|
|
42
42
|
|
|
43
|
+
# Max minutes to wait for CI green on the epic branch before MERGE_EPIC
|
|
44
|
+
# invokes `gh pr merge`. Only consulted under granularity=epic +
|
|
45
|
+
# merge_strategy=stacked. Falls back to `land_wait_minutes` if unset,
|
|
46
|
+
# so existing configs with only the land knob keep working. Effective
|
|
47
|
+
# wall time can exceed this by up to ~60s due to the polling cycle
|
|
48
|
+
# (gh pr checks timeout + 30s±5s sleep) — see create-pr.js#runChecksMode.
|
|
49
|
+
epic_merge_wait_minutes: 30
|
|
50
|
+
|
|
43
51
|
# Branch naming
|
|
44
52
|
branch_prefix: "story/" # prefix for story branches (e.g., story/1-2-user-auth)
|
|
45
53
|
max_branch_length: 60 # truncate + 6-char hash if longer
|