@wemuda/launchrail 1.7.0 → 1.9.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.
Files changed (70) hide show
  1. package/README.md +36 -20
  2. package/assets/agents-docs/domain.md +59 -0
  3. package/assets/agents-docs/issue-tracker-github.md +51 -0
  4. package/assets/agents-docs/issue-tracker-gitlab.md +52 -0
  5. package/assets/agents-docs/issue-tracker-linear.md +52 -0
  6. package/assets/agents-docs/issue-tracker-local.md +45 -0
  7. package/assets/ralph.permission-guard.py +90 -0
  8. package/assets/ralph.workflow.js +129 -30
  9. package/assets/skills/NOTICE.md +41 -0
  10. package/assets/skills/launchrail/launch/SKILL.md +67 -0
  11. package/assets/skills/launchrail/launch/workflow.md +77 -0
  12. package/assets/skills/launchrail/launch-browser-smoke/SKILL.md +49 -0
  13. package/assets/skills/launchrail/launch-code-review/SKILL.md +89 -0
  14. package/assets/skills/launchrail/launch-design-validation/SKILL.md +44 -0
  15. package/assets/skills/launchrail/launch-discovery/SKILL.md +33 -0
  16. package/assets/skills/launchrail/launch-grill/CONTEXT-FORMAT.md +62 -0
  17. package/assets/skills/launchrail/launch-grill/SKILL.md +48 -0
  18. package/assets/skills/launchrail/launch-grill/domain-modeling.md +45 -0
  19. package/assets/skills/launchrail/launch-implement/SKILL.md +49 -0
  20. package/assets/skills/launchrail/launch-project-alignment/SKILL.md +48 -0
  21. package/assets/skills/launchrail/launch-ralph/SKILL.md +115 -0
  22. package/assets/skills/launchrail/launch-ralph-implement/SKILL.md +17 -0
  23. package/assets/skills/launchrail/launch-research/SKILL.md +16 -0
  24. package/assets/skills/launchrail/launch-resolving-merge-conflicts/SKILL.md +15 -0
  25. package/assets/skills/launchrail/launch-spec/SKILL.md +77 -0
  26. package/assets/skills/launchrail/launch-tickets/SKILL.md +107 -0
  27. package/assets/skills/launchrail/launch-vision-creation/SKILL.md +60 -0
  28. package/assets/skills/launchrail/launch-wayfinder/SKILL.md +130 -0
  29. package/dist/commands/add.js +21 -4
  30. package/dist/commands/add.js.map +1 -1
  31. package/dist/commands/doctor.js +42 -32
  32. package/dist/commands/doctor.js.map +1 -1
  33. package/dist/commands/init.d.ts +3 -7
  34. package/dist/commands/init.js +62 -85
  35. package/dist/commands/init.js.map +1 -1
  36. package/dist/commands/sync.js +11 -0
  37. package/dist/commands/sync.js.map +1 -1
  38. package/dist/index.js +0 -5
  39. package/dist/index.js.map +1 -1
  40. package/dist/lib/agentsDocs.d.ts +4 -0
  41. package/dist/lib/agentsDocs.js +32 -0
  42. package/dist/lib/agentsDocs.js.map +1 -0
  43. package/dist/lib/claudeSettings.d.ts +74 -11
  44. package/dist/lib/claudeSettings.js +185 -17
  45. package/dist/lib/claudeSettings.js.map +1 -1
  46. package/dist/lib/detect.d.ts +0 -2
  47. package/dist/lib/detect.js +0 -1
  48. package/dist/lib/detect.js.map +1 -1
  49. package/dist/lib/manifest.d.ts +14 -1
  50. package/dist/lib/manifest.js +18 -1
  51. package/dist/lib/manifest.js.map +1 -1
  52. package/dist/lib/migrations.js +202 -1
  53. package/dist/lib/migrations.js.map +1 -1
  54. package/dist/lib/project.js +9 -1
  55. package/dist/lib/project.js.map +1 -1
  56. package/dist/lib/ralph.d.ts +16 -5
  57. package/dist/lib/ralph.js +32 -9
  58. package/dist/lib/ralph.js.map +1 -1
  59. package/dist/lib/seeds.js +4 -1
  60. package/dist/lib/seeds.js.map +1 -1
  61. package/dist/lib/skills.d.ts +12 -0
  62. package/dist/lib/skills.js +57 -0
  63. package/dist/lib/skills.js.map +1 -0
  64. package/dist/lib/upstream.d.ts +6 -6
  65. package/dist/lib/upstream.js +1 -1
  66. package/dist/lib/upstream.js.map +1 -1
  67. package/package.json +1 -1
  68. package/dist/lib/claudeCli.d.ts +0 -51
  69. package/dist/lib/claudeCli.js +0 -71
  70. package/dist/lib/claudeCli.js.map +0 -1
@@ -1,28 +1,30 @@
1
1
  // Managed by Launchrail. Do not hand-edit — `launchrail sync` may replace this file.
2
- // Override policy per run via args instead, e.g. { width: 1, only: [9, 10] }.
2
+ // Override policy per run via args instead, e.g. { width: 1, only: [9, 10], max: 5 }.
3
3
  //
4
4
  // The Ralph loop as a deterministic workflow: the plan, the frontier bookkeeping,
5
5
  // and every intermediate report live in script variables — not in any context window —
6
- // so long or wide runs cannot compact away their own state. The watchable, checkpointed
7
- // variant of the same loop is the launchrail:ralph skill; the two share one policy block,
8
- // and a policy change belongs in both places (ADR-0005, field-revised by ADR-0010).
6
+ // so long or wide runs cannot compact away their own state. This is the engine for
7
+ // every multi-ticket run (ADR-0022); the launch-ralph skill carries the same policy
8
+ // block as the supervisor's contract and the declared-exception watchable mode, and
9
+ // a policy change belongs in both places (ADR-0005, field-revised by ADR-0010, ADR-0022).
9
10
  export const meta = {
10
11
  name: 'ralph',
11
12
  description: 'Autonomous Ralph loop: implement ready tickets with fresh-context subagents, verification-gated',
12
13
  whenToUse:
13
- 'Run the Ralph implementation loop over the ticket backlog when the dependency graph is wide or the run is long. Scope a run via args: { only: [9, 10], width: 2 } or just [9, 10]. For a watchable, checkpointed run (or when something is already going wrong), use the launchrail:ralph skill instead.',
14
+ 'The engine for any multi-ticket Ralph run. Scope a run via args: { only: [9, 10], width: 2 }, just [9, 10], or { max: 5 } to stop after 5 verified merges ("the next five" — the frontier picks which, in dependency order). Declare the integration target with { target: "spec/44-mvp" } to consolidate the campaign onto that branch (default branch untouched; release later with one PR) — omit it to merge each ticket into the default branch. { canary: true } holds width at 1 until the first verified merge. Args must be JSON — resolve any natural-language scope to ticket numbers, a cap, and a target before launching. For a watchable run (an explicit user ask, or a targeted intervention), use the launch-ralph skill instead — and say why.',
14
15
  phases: [
15
- { title: 'Preflight', detail: 'read project config, sync the base, run the verification gate' },
16
+ { title: 'Preflight', detail: 'read project config, resolve the integration target, run the verification gate' },
16
17
  { title: 'Graph', detail: 'list ready tickets and their blocking edges, verbatim' },
17
- { title: 'Build', detail: 'one fresh-context implementer per ticket, merge included' },
18
+ { title: 'Build', detail: 'one fresh-context implementer per ticket, handing off at PR-open' },
19
+ { title: 'Gate', detail: 'per-ticket merge gate: CI wait, squash-merge, explicit close' },
18
20
  { title: 'Verify', detail: 'remote ground truth for every claimed merge' },
19
21
  { title: 'Park', detail: 'comment failure history, label needs-info' },
20
- { title: 'Release', detail: 'final verification gate and evidence summary' },
22
+ { title: 'Release', detail: 'final verification gate and the where-it-lives recap' },
21
23
  ],
22
24
  }
23
25
 
24
26
  // ---------------------------------------------------------------------------
25
- // Policy — the launchrail:ralph policy block, as code. Override via args.
27
+ // Policy — the launch-ralph policy block, as code. Override via args.
26
28
  // ---------------------------------------------------------------------------
27
29
 
28
30
  // args may arrive as an object ({ only, width, ... }), a bare array of ticket numbers,
@@ -46,10 +48,23 @@ const A = resolveArgs(args)
46
48
  const POLICY = {
47
49
  // Scope the run to specific ticket numbers ([] = the whole ready frontier).
48
50
  only: A.only ?? [],
51
+ // Stop after this many verified merges (0 = no cap). "The next five": the frontier
52
+ // decides which five, in dependency order. Batches never exceed the remainder, so a
53
+ // run has at most `max` merges and leaves the rest of the frontier ready, not parked.
54
+ max: A.max ?? 0,
49
55
  // Parallel implementers. Width also caps local build concurrency — several implementers
50
56
  // share one machine, and fanning out test runs buys backpressure, not speed. Use 1 until
51
- // a run has landed tickets cleanly on this project.
57
+ // a run has landed tickets cleanly on this project — or pass canary: true, which does it
58
+ // for you. Tickets that add DB migrations collide on the next migration number when run
59
+ // in parallel; the pre-PR sync renumbers, but serializing them is cheaper.
52
60
  width: A.width ?? 3,
61
+ // Integration target: '' (trunk) merges each ticket PR into the default branch; a branch
62
+ // name consolidates the whole campaign onto that branch and never touches the default
63
+ // branch — the run ends by offering ONE release PR target -> default (ADR-0022).
64
+ target: A.target ?? '',
65
+ // Canary: hold width at 1 until the run's first verified merge proves the plumbing
66
+ // end to end (branch, PR, CI, merge gate, close). For a project's first campaign.
67
+ canary: A.canary ?? false,
53
68
  // Tries per ticket: 1 attempt + 1 retry with a fresh context, then park. Deferrals
54
69
  // (a declared blocker had not landed yet) hand their attempt back, capped separately.
55
70
  attempts: A.attempts ?? 2,
@@ -80,12 +95,14 @@ where it left off — do not start over. Never open a second PR for the same tic
80
95
  const PREFLIGHT_SCHEMA = {
81
96
  type: 'object',
82
97
  additionalProperties: false,
83
- required: ['green', 'base', 'trackerAccess', 'verifyCommand', 'localCommands', 'failures'],
98
+ required: ['green', 'base', 'defaultBranch', 'trackerAccess', 'verifyCommand', 'localCommands', 'failures'],
84
99
  properties: {
85
100
  green: { type: 'boolean', description: 'base is synced and the verification gate passed' },
86
101
  headSha: { type: 'string', description: 'commit sha the gate ran against' },
87
102
  repo: { type: 'string', description: 'owner/name from the git remote, or empty' },
88
- base: { type: 'string', description: 'default branch name' },
103
+ base: { type: 'string', description: "the run's integration base: the declared target branch when one is set, else the default branch" },
104
+ defaultBranch: { type: 'string', description: 'the repository default branch name' },
105
+ targetCreated: { type: 'boolean', description: 'true when a declared target branch was missing from the remote and was created from the default branch tip' },
89
106
  issueTracker: { type: 'string', description: 'issueTracker from .launchrail.yml (github | linear | none)' },
90
107
  trackerAccess: {
91
108
  type: 'string',
@@ -141,7 +158,9 @@ const BUILD_SCHEMA = {
141
158
  properties: {
142
159
  status: {
143
160
  type: 'string',
144
- enum: ['merged', 'already-done', 'blocked', 'ci-red', 'ci-timeout', 'conflict', 'verify-failed', 'failed'],
161
+ enum: ['pr-open', 'merged', 'already-done', 'blocked', 'conflict', 'verify-failed', 'failed'],
162
+ description:
163
+ '"pr-open" is the normal hand-off (the loop owns CI and merge); "merged" only when an adopted PR turned out to be merged already (idempotency)',
145
164
  },
146
165
  pr: { type: 'integer', description: 'PR number, when one was opened or adopted' },
147
166
  mergeCommit: { type: 'string' },
@@ -158,6 +177,24 @@ const BUILD_SCHEMA = {
158
177
  },
159
178
  }
160
179
 
180
+ const GATE_SCHEMA = {
181
+ type: 'object',
182
+ additionalProperties: false,
183
+ required: ['status', 'summary'],
184
+ properties: {
185
+ status: {
186
+ type: 'string',
187
+ enum: ['merged', 'ci-failed', 'ci-timeout', 'not-mergeable', 'failed'],
188
+ },
189
+ mergeCommit: { type: 'string' },
190
+ issueClosed: { type: 'boolean' },
191
+ summary: {
192
+ type: 'string',
193
+ description: 'on merged: the API facts; on failure: the failing check or conflicting files, enough for a fresh implementer to act on',
194
+ },
195
+ },
196
+ }
197
+
161
198
  const VERIFY_SCHEMA = {
162
199
  type: 'object',
163
200
  additionalProperties: false,
@@ -207,24 +244,32 @@ Start clean: delete the failed ralph/${ticket.number}-* branch first, re-sync th
207
244
  : ''
208
245
  return `${preamble(pre)}
209
246
 
210
- Implement ticket #${ticket.number} ("${ticket.title}") end to end — merge included. You own it alone; assume no knowledge of any other session. Other implementers are working on other tickets against the same base right now, so ${pre.base} will move under you. That is expected.
247
+ Implement ticket #${ticket.number} ("${ticket.title}") through to an open PR. You own the build alone; assume no knowledge of any other session. Other implementers are working on other tickets against the same base right now, so ${pre.base} will move under you. That is expected.
211
248
  ${retry}
212
249
  Steps, in order:
213
250
  1. Dependency gate: before anything else, confirm every ticket on this ticket's "Blocked by" line is CLOSED with its work merged into ${pre.base}. If any blocker is still open, do NOT build on a missing dependency — report status "blocked", name the open blocker in "failure", and stop. That is a deferral, not a failure; the loop retries you after the blocker lands.
214
- 2. Read the ticket and everything it links (spec sections, ADRs, journeys). Report status "already-done" if it is already closed.
251
+ 2. Read the ticket and everything it links (spec sections, ADRs, journeys). If the tracker tool truncates the body (long code spans are a known trigger), fetch the full text by another route — the tracker's search API, the spec file in the repo — and never implement from a truncated ticket. Report status "already-done" if the ticket is already closed.
215
252
  3. Label the ticket ralph:building so a lost session leaves a trace.
216
253
  4. Branch from a fresh sync of ${pre.base}: ralph/${ticket.number}-<short-slug>.
217
- 5. Implement by invoking the launchrail:ralph-implement skill — it owns the per-ticket contract: TDD, the verification gate, browser smoke for user-facing changes, self-review via /code-review, commit conventions.
218
- 6. Pre-PR sync: merge the latest ${pre.base} into your branch. Conflicts are ordinary work — resolve them with the launchrail:resolving-merge-conflicts skill and re-run the verification gate if anything changed.
219
- 7. Open a PR titled from the ticket, with "Closes #${ticket.number}" in the body. Never open a second PR if one already exists — adopt it. Opening against an up-to-date base means CI tests the state that will actually land.
220
- 8. Wait for CI if the repository has it, spacing polls with the Monitor tool or a background sleep — never a foreground sleep, never a busy loop; treat ~20 minutes as the budget and report status "ci-timeout" beyond it. Fix what your branch broke and push. If a failure reproduces on ${pre.base} itself, report "ci-red" and stop — that is systemic, not this ticket's problem.
221
- 9. Immediately before merging, re-sync with ${pre.base} once more (retry up to 3 times if the base keeps moving), then squash-merge. Squash-merge does not reliably fire "Closes" — read the issue back, close it explicitly if it is still open, and remove the ralph:building label. Never push to ${pre.base} directly; the PR is the only door.
254
+ 5. Implement by invoking the launch-ralph-implement skill — it owns the per-ticket contract: TDD, the verification gate, browser smoke for user-facing changes, self-review via /code-review, commit conventions.
255
+ 6. Pre-PR sync: merge the latest ${pre.base} into your branch. Conflicts are ordinary work — resolve them with the launch-resolving-merge-conflicts skill. If ${pre.base} gained DB migrations since you branched, regenerate yours to follow them with the project's migration tool — never hand-edit the migration journal. Re-run the verification gate if anything changed.
256
+ 7. Open a PR against ${pre.base}, titled from the ticket, with "Closes #${ticket.number}" in the body. Never open a second PR if one already exists — adopt it. Opening against an up-to-date base means CI tests the state that will actually land. Then report status "pr-open" with the PR number and STOP: the CI wait, the merge, and the issue close belong to the loop's merge gate, not to you — a subagent cannot wait on CI (a background sleep will not resume you). Never push to ${pre.base} directly; the PR is the only door.
222
257
 
223
258
  ${INTEGRITY}
224
259
 
225
260
  ${IDEMPOTENCY}
226
261
 
227
- Report honestly via the schema: "merged" only after the squash-merge API call succeeded; "blocked" when a declared blocker had not landed; "verify-failed" when the verification gate would not go green; "conflict" when a conflict was too ambiguous to resolve without losing behavior (say which files and why); "ci-red" / "ci-timeout" / "failed" otherwise, with a summary a fresh retry can act on. List deliberately-out-of-scope discoveries in "punted".`
262
+ Report honestly via the schema: "pr-open" once the PR exists against ${pre.base}; "merged" only when an adopted PR turned out to be already merged; "blocked" when a declared blocker had not landed; "verify-failed" when the verification gate would not go green; "conflict" when a conflict was too ambiguous to resolve without losing behavior (say which files and why); "failed" otherwise, with a summary a fresh retry can act on. List deliberately-out-of-scope discoveries in "punted".`
263
+ }
264
+
265
+ function gatePrompt(pre, ticket, build) {
266
+ return `You are the merge gate for ticket #${ticket.number}: PR #${build.pr} is open against ${pre.base}.
267
+ Tracker access from this environment: ${pre.trackerAccess}
268
+ You own the CI wait, the squash-merge, and the tracker bookkeeping — and nothing else. You never write code, never push commits, never repair a failing branch; a failing PR is reported, not fixed here.
269
+ 1. Wait for CI on the PR, if the repository has it. Space checks with the Monitor tool — NEVER a bare background sleep (it will not resume you) and never a busy loop. Treat ~20 minutes as the budget; beyond it report status "ci-timeout".
270
+ 2. CI green (or absent): check mergeability against ${pre.base} — the base may have moved since CI started. Mergeable: squash-merge via the tracker API; if the base moves between check and merge, re-check and retry up to 3 times. A real conflict is status "not-mergeable" — name the conflicting files if the API reports them.
271
+ 3. Merged: read issue #${ticket.number} back and close it explicitly if it is still open — "Closes #n" only auto-fires from the default branch${POLICY.target ? ', and this run does not merge there' : ', and squash-merge does not reliably fire it even there'} — then remove the ralph:building label. Report status "merged" with the merge commit sha.
272
+ 4. CI failed on the PR: report status "ci-failed" with the failing check and a summary a fresh implementer can act on. Fix nothing.`
228
273
  }
229
274
 
230
275
  function verifyPrompt(pre, ticket, build) {
@@ -304,17 +349,39 @@ async function drive(pre, ticket) {
304
349
  s.failures.push(`still blocked after ${s.defers} deferrals: ${build.failure ?? build.summary}`)
305
350
  return { ticket, ok: false }
306
351
  }
307
- if (build.status !== 'merged') {
352
+ if (build.status !== 'pr-open' && build.status !== 'merged') {
308
353
  s.failures.push(`[attempt ${s.attempts}] ${build.status}: ${build.failure ?? build.summary}`)
309
354
  return { ticket, ok: false }
310
355
  }
311
356
  if (!build.pr) {
312
- s.failures.push(`[attempt ${s.attempts}] reported merged but returned no PR number`)
357
+ s.failures.push(`[attempt ${s.attempts}] reported ${build.status} but returned no PR number`)
313
358
  return { ticket, ok: false }
314
359
  }
360
+ let mergeCommit = build.mergeCommit
361
+ if (build.status === 'pr-open') {
362
+ // The loop owns the merge gate (ADR-0022): an implementer cannot wait on CI (a
363
+ // subagent's background sleep never resumes it), and a single gate owner keeps
364
+ // merge ordering sane. A failing gate hands the ticket back as a failed attempt;
365
+ // the fresh retry adopts the PR via the idempotency clause, repairs, hands off again.
366
+ const gate = await agent(gatePrompt(pre, ticket, build), {
367
+ label: `gate:#${ticket.number}`,
368
+ phase: 'Gate',
369
+ schema: GATE_SCHEMA,
370
+ effort: 'low',
371
+ })
372
+ if (!gate) {
373
+ s.failures.push('gate agent died (infrastructure)')
374
+ return { ticket, ok: false, dead: true }
375
+ }
376
+ if (gate.status !== 'merged') {
377
+ s.failures.push(`[attempt ${s.attempts}] PR #${build.pr} ${gate.status}: ${gate.summary}`)
378
+ return { ticket, ok: false }
379
+ }
380
+ mergeCommit = gate.mergeCommit || mergeCommit
381
+ }
315
382
  // Nothing is trusted from a report — a claimed merge is checked against the remote
316
383
  // by a separate, cheap agent with tracker access only.
317
- const verdict = await agent(verifyPrompt(pre, ticket, build), {
384
+ const verdict = await agent(verifyPrompt(pre, ticket, { pr: build.pr, mergeCommit }), {
318
385
  label: `verify:#${ticket.number}`,
319
386
  phase: 'Verify',
320
387
  schema: VERIFY_SCHEMA,
@@ -324,7 +391,7 @@ async function drive(pre, ticket) {
324
391
  if (verdict?.merged && verdict.issueClosed) {
325
392
  s.status = 'merged'
326
393
  s.pr = build.pr
327
- s.mergeCommit = verdict.mergeCommit || build.mergeCommit
394
+ s.mergeCommit = verdict.mergeCommit || mergeCommit
328
395
  return { ticket, ok: true }
329
396
  }
330
397
  // Merged-but-issue-open fails verification too: the retry adopts the merged PR (the
@@ -359,9 +426,13 @@ function frontier(tickets, closedBefore) {
359
426
  // ---------------------------------------------------------------------------
360
427
  phase('Preflight')
361
428
  const pre = await agent(
362
- `Preflight for a Ralph loop run in this repository. Fix nothing; report actual state.
429
+ `Preflight for a Ralph loop run in this repository. Report actual state; fix nothing — the one permitted mutation is creating the declared integration branch in step 2.
363
430
  1. Read .launchrail.yml (issueTracker, testing commands, modules) and AGENTS.md (verbatim commands).
364
- 2. Identify the repo (git remote) and the default/base branch; sync it fresh (clean tree). If the base branch does not exist on the remote, report not green and say the base is missing — do not guess another branch.
431
+ 2. Identify the repo (git remote) and its default branch; report the default branch name as defaultBranch. ${
432
+ POLICY.target
433
+ ? `This run consolidates onto the integration branch "${POLICY.target}" — that branch is the base. If it does not exist on the remote, create it from the default branch's tip (no force; the default branch itself is never touched) and report targetCreated: true. A missing DEFAULT branch is still not green — do not guess.`
434
+ : `This run merges into the default branch (trunk) — that branch is the base. If it does not exist on the remote, report not green and say the base is missing — do not guess another branch.`
435
+ } Sync the base fresh (clean tree) and report its name as base.
365
436
  3. Determine how the tracker is reachable from THIS environment: check whether the CLI the project docs assume (e.g. gh) is installed; if not, name the concrete substitute available here (e.g. GitHub MCP tools) as an instruction future agents can follow.
366
437
  4. Run the project's install command, then the verification gate: npx @wemuda/launchrail verify. Report the actual exit codes, not the reassuring summary line. An empty verification contract failing the gate is a refusal condition, not something to work around.
367
438
  green means: base synced AND the verification gate exited 0.`,
@@ -380,10 +451,14 @@ if ((pre.issueTracker ?? 'none') === 'none') {
380
451
  phase('Graph')
381
452
  log(
382
453
  `Base green at ${pre.headSha ?? pre.base} on ${pre.base}. ` +
454
+ (POLICY.target
455
+ ? `Consolidating onto ${pre.base}${pre.targetCreated ? ' (created from the default branch tip)' : ''}; ${pre.defaultBranch || 'the default branch'} stays untouched. `
456
+ : `Trunk mode — each ticket merges into ${pre.base}. `) +
383
457
  (POLICY.only.length > 0
384
458
  ? `Scoped to ${POLICY.only.map((n) => `#${n}`).join(', ')}.`
385
459
  : 'No scope — building the whole ready frontier.') +
386
- ` Width ${POLICY.width}, ${POLICY.attempts} attempts per ticket.`,
460
+ (POLICY.max > 0 ? ` Stopping after ${POLICY.max} verified merge(s).` : '') +
461
+ ` Width ${POLICY.width}${POLICY.canary ? ' (canary: width 1 until the first verified merge)' : ''}, ${POLICY.attempts} attempts per ticket.`,
387
462
  )
388
463
  let graph = await agent(graphPrompt(pre), { label: 'read-graph', phase: 'Graph', schema: GRAPH_SCHEMA, model: 'haiku', effort: 'low' })
389
464
  if (!graph) throw new Error('graph agent died — refusing to start')
@@ -398,16 +473,31 @@ for (const t of tickets) {
398
473
  }
399
474
  }
400
475
 
476
+ const mergedCount = () => [...state.values()].filter((s) => s.status === 'merged').length
477
+
401
478
  let rounds = 0
479
+ let maxReached = false
402
480
  while (rounds < POLICY.maxRounds) {
403
481
  if (budget.total && budget.remaining() < POLICY.reserve) {
404
482
  log(`token budget at reserve (${Math.round(budget.remaining() / 1000)}k left) — stopping before a new round`)
405
483
  break
406
484
  }
485
+ // The cap counts verified merges only — a failed or deferred dispatch frees its slot
486
+ // for a different ticket next round. Capping the batch at the remainder means even a
487
+ // fully successful round cannot overshoot.
488
+ const capLeft = POLICY.max > 0 ? POLICY.max - mergedCount() : Infinity
489
+ if (capLeft <= 0) {
490
+ maxReached = true
491
+ log(`cap reached: ${POLICY.max} verified merge(s) — stopping; the rest of the frontier stays ready`)
492
+ break
493
+ }
407
494
  const ready = frontier(tickets, closedBefore)
408
495
  if (ready.length === 0) break
409
496
  rounds += 1
410
- const batch = ready.slice(0, POLICY.width)
497
+ // Canary: the first verified merge proves the plumbing end to end (branch, PR, CI,
498
+ // merge gate, explicit close); until it lands, dispatch one ticket at a time.
499
+ const width = POLICY.canary && mergedCount() === 0 ? 1 : POLICY.width
500
+ const batch = ready.slice(0, Math.min(width, capLeft))
411
501
  log(`round ${rounds}: dispatching ${batch.map((t) => `#${t.number}`).join(', ')} (${ready.length} unblocked)`)
412
502
  const results = await parallel(batch.map((t) => () => drive(pre, t)))
413
503
  const landed = results.filter((r) => r?.ok)
@@ -471,16 +561,25 @@ const release = await agent(
471
561
  2. Run the verification gate: npx @wemuda/launchrail verify. Report the actual exit code.
472
562
  ${
473
563
  pre.browserTesting && merged.length > 0
474
- ? `3. The browser-testing module is enabled: start the app (node scripts/dev.mjs --background), scaffold an evidence bundle (npx @wemuda/launchrail smoke), and drive the smoke journeys from docs/testing/smoke-journeys.md per the launchrail:browser-smoke skill. Report the bundle path. A journey you could not complete is a failure, never a pass.`
564
+ ? `3. The browser-testing module is enabled: start the app (node scripts/dev.mjs --background), scaffold an evidence bundle (npx @wemuda/launchrail smoke), and drive the smoke journeys from docs/testing/smoke-journeys.md per the launch-browser-smoke skill. Report the bundle path. A journey you could not complete is a failure, never a pass.`
475
565
  : ''
476
566
  }
477
567
  verified means: the verification gate exited 0${pre.browserTesting && merged.length > 0 ? ' AND no smoke journey failed' : ''}.`,
478
568
  { label: 'release-verification', phase: 'Release', schema: RELEASE_SCHEMA },
479
569
  )
480
570
 
571
+ // The recap is part of the contract (ADR-0022): where the work lives and the one
572
+ // next step, as data — the supervisor relays it, never reconstructs it.
573
+ const mode = POLICY.target ? 'consolidation' : 'trunk'
481
574
  return {
482
575
  rounds,
483
576
  verified: release?.verified ?? false,
577
+ maxReached,
578
+ target: { mode, base: pre.base, defaultBranch: pre.defaultBranch ?? '', headSha: release?.headSha ?? '' },
579
+ nextStep:
580
+ mode === 'consolidation'
581
+ ? `All campaign work is on ${pre.base}; ${pre.defaultBranch || 'the default branch'} is untouched. Release it with one PR ${pre.base} -> ${pre.defaultBranch || 'the default branch'} — offer it, and open it only when the user says so.`
582
+ : `Every merged ticket is live on ${pre.base}; nothing is left to integrate.`,
484
583
  release,
485
584
  merged: merged.map((s) => ({ ticket: s.ticket.number, title: s.ticket.title, pr: s.pr, mergeCommit: s.mergeCommit })),
486
585
  parked: parked.map((s) => ({ ticket: s.ticket.number, title: s.ticket.title, failures: s.failures })),
@@ -0,0 +1,41 @@
1
+ # Attribution
2
+
3
+ The skills in this directory are Launchrail's own — one complete, `launch-`
4
+ prefixed workflow (ADR-0020), managed by `launchrail sync`.
5
+
6
+ Several of them absorb methodology and contain text derived from
7
+ [Matt Pocock's skills](https://github.com/mattpocock/skills), used and adapted
8
+ under the MIT License reproduced below:
9
+
10
+ - `launch-research`, `launch-grill` (including its `domain-modeling.md` and
11
+ `CONTEXT-FORMAT.md`), `launch-wayfinder`, `launch-spec`, `launch-tickets`,
12
+ `launch-code-review`
13
+
14
+ The same applies to the issue-tracker and domain-doc files Launchrail seeds
15
+ into `docs/agents/`. Each derived file carries its own derivation note. If
16
+ Launchrail is useful to you, the inspiration credit belongs upstream — see the
17
+ Launchrail README's Credits section.
18
+
19
+ ---
20
+
21
+ MIT License
22
+
23
+ Copyright (c) 2026 Matt Pocock
24
+
25
+ Permission is hereby granted, free of charge, to any person obtaining a copy
26
+ of this software and associated documentation files (the "Software"), to deal
27
+ in the Software without restriction, including without limitation the rights
28
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
29
+ copies of the Software, and to permit persons to whom the Software is
30
+ furnished to do so, subject to the following conditions:
31
+
32
+ The above copyright notice and this permission notice shall be included in all
33
+ copies or substantial portions of the Software.
34
+
35
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
36
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
37
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
38
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
39
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
40
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
41
+ SOFTWARE.
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: launch
3
+ description: The planning conductor and single entry point to the Launchrail loop. Detects how far a project has moved from idea toward release — setup, vision, visual exploration, discovery, grill, research, ADRs, spec, design validation, tickets — then runs or routes to the stage that owns the next step, and hands implementation to /launch-implement. Once the foundation exists it also sizes each new feature (large / semi / small) and routes the planning subset that size needs. Use to start or continue the workflow, ask what stage a project is at, size a new feature, or jump straight to a named stage.
4
+ ---
5
+
6
+ # Launch — the loop conductor
7
+
8
+ One command for the whole rail. You are the **conductor**, not a stage: find where the project sits, then run or hand off to the skill that owns the next step. Every stage has exactly one owner; [`workflow.md`](workflow.md) is the contract for who owns what and for the conductor rules — compose owners by name, gate on committed artifacts, prepare handoffs for user-typed stages, keep detection read-only. Follow those rules; this file only tells you how to route.
9
+
10
+ ## The stage map
11
+
12
+ Read `.launchrail.yml` (`mode`, `origin`, `modules`, `issueTracker`) first — it is the source of truth for configuration; `npx @wemuda/launchrail status` for what's installed and current.
13
+
14
+ | # | Stage | Owner (invoke / run) | Done when |
15
+ |---|---|---|---|
16
+ | 0 | Setup | `npx @wemuda/launchrail init` | Manifest + lockfile committed; `doctor` green (`docs/agents/` is seeded by init) |
17
+ | 1 | Vision | `launch-vision-creation` — via `launch-project-alignment` when `origin: existing` | `docs/vision.md` exists and is real (not the bare template) |
18
+ | 2 | Visual exploration | Claude Design | Exploration artifacts linked from `docs/vision.md` |
19
+ | 3 | Discovery | `launch-discovery` | Landscape map committed under `docs/research/` (`discovery-*.md`) |
20
+ | 4 | Complexity grill | `launch-grill` | Grill constraints committed under `docs/research/` |
21
+ | 5 | Technical research | `launch-research`, fed the grill constraints | Research notes committed under `docs/research/` |
22
+ | 6 | Architecture decisions | ADRs (`docs/adr/0000-template.md`) | `docs/adr/NNNN-*.md` beyond the template |
23
+ | 7 | MVP specification | `launch-wayfinder` / `launch-spec` † | A spec exists under `docs/specs/` |
24
+ | 8 | Design validation | `launch-design-validation` (fidelity chosen inside the skill) | The spec carries a `## Design validation` section (a recorded skip counts) |
25
+ | 9 | Tickets | `launch-tickets` † | Tracker has `ready-for-agent` tickets with `Blocked by: #n` edges |
26
+ | 10 | Implementation | `/launch-implement` † — drives the Ralph loop | The ready frontier is drained; PRs merged and verified |
27
+ | 11 | Verification | `npx @wemuda/launchrail verify` · `launch-browser-smoke` | The gate is green; smoke evidence where behavior is user-facing |
28
+ | 12 | Release | The project's release setup | The release is cut |
29
+
30
+ † User-typed (`disable-model-invocation`): prepare the handoff — inputs committed, exact fully-argumented command handed over, resume when the artifact lands (see the conductor rules). Never call one and get refused, and never reverse-engineer it.
31
+
32
+ `deep-research` = stages 3 → 4 → 5 as one arc: discovery widens the option space, the grill narrows it, research de-risks what survives. The workflow doc's stage notes carry the judgment calls for stages 3, 4, 8, and 10.
33
+
34
+ ## Sizing the next feature
35
+
36
+ Once the foundation exists (a real vision, ADRs beyond the template) and the user brings **one new feature**, the frontier question changes from "what stage is next" to "how much planning does this feature deserve." Size it — propose with your reasoning, let the user correct you; they see scope the artifacts don't show:
37
+
38
+ | Size | Looks like | Planning path |
39
+ |---|---|---|
40
+ | **Large** | A new subsystem or cross-cutting change; real unknowns; decisions worth an ADR | discovery *(new tech territory only)* → `launch-wayfinder` → grill → `launch-spec` → design validation → `launch-tickets` |
41
+ | **Semi** | Self-contained feature, some design surface, a handful of tickets | grill → `launch-spec` → design validation *(optional)* → `launch-tickets` |
42
+ | **Small** | Well-understood change, little or no design surface, one or few tickets | grill → `launch-tickets` |
43
+
44
+ Judgment calls: the grill here is feature-scoped (same `launch-grill`, narrower brief); discovery earns a place only when the feature opens genuinely new tech territory — a vendor category or storage engine the project hasn't used; design validation is for real UI surface; a genuine architecture decision gets an ADR before tickets. Between two sizes pick the smaller — it's cheaper to add a stage than to over-plan a small change. `mode` calibrates on top: `spike` may drop `launch-spec` and design validation (record the skip); `high-rigor` bumps one notch. Every size ends at `/launch-implement`.
45
+
46
+ ## Running it
47
+
48
+ 1. **Did the user name a stage or a feature?** A stage keyword (below): sanity-check its inputs exist, offer the earlier stage if one is missing, but honor the jump if they insist — then invoke or hand off and stop. A new feature on a founded project: size it (above) and run the path.
49
+ 2. **Otherwise orient, then find the frontier.** A cheap read-only look first: `git status`, current branch, recent commits — is something already in flight for the stage you're about to start? If the tracker is configured and reachable, read the live discussion on relevant tickets and PRs, not just titles; skip what isn't there (orientation sharpens routing, never gates it). Then close stage-0 gaps yourself without asking (init, commit init output, `sync` — it seeds `docs/agents/` too). Walk stages 1 → 12 and stop at the first whose "done when" fails, skipping only what `mode` permits. `origin: existing` with no real vision → route to `launch-project-alignment`, not a blank vision.
50
+ 3. **Confirm the read.** Say where you think the project is and why — which artifacts you found and which you didn't. Ambiguous signals (template-only vision, several specs) are questions, not guesses.
51
+ 4. **Route.** Invoke the owner by exact name, or prepare the handoff for a user-typed stage (†). For stage 7, name the authoritative inputs in order; if the stack isn't stood up yet, tell it to name its seams but leave harness mechanics to the foundation work. For stage 10, hand over `/launch-implement` — never start it yourself.
52
+ 5. **Leave an explained map.** Current stage, the next one with a sentence on what it does and whether it's optional here, then the rest of the arc to the destination — and that any stage is reachable by keyword. A bare stage name reads as a turnstile; explain, don't gate.
53
+
54
+ ## Stage keywords
55
+
56
+ Case-insensitive direct jumps:
57
+
58
+ - `status` / `where` — report the detected stage and stop.
59
+ - `next` — detect the frontier and drive it (the default).
60
+ - `setup` / `init` — 0 · `align` / `adopt` — the existing-project on-ramp · `vision` — 1 · `explore` — 2 · `discovery` / `landscape` — 3 · `grill` — 4 · `research` — 5 · `deep-research` — 3→5 · `adr` / `architecture` — 6 · `spec` — 7 · `design-validation` / `validate` — 8 · `tickets` — 9 · `implement` / `build` / `ralph` / `loop` — hand over `/launch-implement` · `verify` / `smoke` — 11 · `release` — 12.
61
+ - `feature` / `size` — size a described feature (recommend a path; route on request).
62
+
63
+ Unrecognized keyword → show this list and ask.
64
+
65
+ ## Mode calibration
66
+
67
+ `mode` calibrates rigor, not stage order: `spike` may skip stages 2–5 and 8 when the vision's non-goals record it (don't nag); `standard-mvp` skips nothing silently; `high-rigor` skips nothing, wants an ADR per stage-6 decision, and design validation covers error and edge states. When a stage looks skipped, check the vision's non-goals before deciding — and if you can't tell, ask.
@@ -0,0 +1,77 @@
1
+ # The Launchrail core workflow
2
+
3
+ How a project moves from idea to verified, released software through committed artifacts. The rail is one complete, self-contained skill set — every stage owner is a Launchrail `launch-*` skill, written to the rail's artifact contract ([ADR-0020](https://github.com/wemuda/launchrail/blob/master/docs/adr/0020-independent-skill-set.md); several absorb methodology from [Matt Pocock's skills](https://github.com/mattpocock/skills), credited in `NOTICE.md`). The stage table below is the contract for which skill owns which stage and what artifact it must leave behind, and the conductor rules further down are the contract for how the `launch` conductor (and any agent working the rail) behaves between stages.
4
+
5
+ ## Running it
6
+
7
+ Two commands cover the whole rail:
8
+
9
+ - **`/launch`** — plan. It detects which stage the project has reached from its committed artifacts and runs or routes to that stage's owner; it takes a stage name (`vision`, `discovery`, `design-validation`, …) to jump straight there, and it sizes each new feature once the foundation exists ([ADR-0009](https://github.com/wemuda/launchrail/blob/master/docs/adr/0009-launch-orchestrator-skill.md), [ADR-0018](https://github.com/wemuda/launchrail/blob/master/docs/adr/0018-implement-front-door.md)).
10
+ - **`/launch-implement`** — build. The single entry point for stage 10: it drives ready tickets to verified merges through the project's selected loop — the whole frontier, a spec's tickets, the next N ("max 5"), or one ticket at a time.
11
+
12
+ ## Prerequisites
13
+
14
+ - The repository is initialized (`npx @wemuda/launchrail init`) and healthy (`npx @wemuda/launchrail doctor`). Init writes the workflow skills, the implementation loop's materials, *and* the `docs/agents/` configuration (issue-tracker conventions and domain-doc rules, seeded from the manifest's answers) — there is no separate install or setup step on the golden path.
15
+
16
+ ## Stages
17
+
18
+ | # | Stage | Tool | Input | Committed artifact |
19
+ |---|---|---|---|---|
20
+ | 1 | Vision | Launchrail `vision-creation` skill | The idea, the user | `docs/vision.md` |
21
+ | 2 | Visual exploration | Claude Design | Vision | Exploration artifacts (linked from the vision) |
22
+ | 3 | Discovery research | Launchrail `discovery` skill (composes `launch-research`) | Vision + intended stack | Landscape/options map in `docs/research/` (`discovery-*.md`) |
23
+ | 4 | Complexity grill | `launch-grill` | Vision + exploration + discovery | Grill constraints in `docs/research/` |
24
+ | 5 | Technical research | `launch-research` | **Grill constraints** | Research notes in `docs/research/` |
25
+ | 6 | Architecture decisions | ADRs (seeded template) | Research | `docs/adr/NNNN-*.md` |
26
+ | 7 | MVP specification | `launch-wayfinder` / `launch-spec` † | Vision, ADRs, research | `docs/specs/` |
27
+ | 8 | Design validation | Launchrail `design-validation` skill | Spec (+ Claude Design at the top fidelity) | Revised spec with `## Design validation` section |
28
+ | 9 | Tickets | `launch-tickets` † | Validated spec | Tickets in the tracker: `ready-for-agent` label, `Blocked by: #n` edges |
29
+ | 10 | Implementation | `/launch-implement` † → the Ralph loop | Ready tickets | PRs merged and verified; the frontier drained |
30
+ | 11 | Verification | `npx @wemuda/launchrail verify` · Launchrail `browser-smoke` skill | Merged work | The gate green; smoke evidence where behavior is user-facing |
31
+ | 12 | Release | The project's release setup | Verified base | The release cut |
32
+
33
+ † **User-typed by design** — `disable-model-invocation`: only the user can start these. `launch-wayfinder`/`launch-spec` and `launch-tickets` publish to the tracker; `/launch-implement` spawns agents and merges PRs. A conductor prepares the handoff instead of calling them — see the conductor rules.
34
+
35
+ Stage notes:
36
+
37
+ - **Stages 3 → 4 → 5 are one arc** (`deep-research`): discovery *diverges* — it maps the real option space for the vision's hard parts (all the auth vendors, not one) and never picks winners; the grill *converges* — it narrows that landscape into constraints; research de-risks what survives. Don't collapse discovery into the grill outside `spike` mode: a grill with no discovery narrows whatever stack was assumed upstream, the exact failure discovery exists to prevent ([ADR-0015](https://github.com/wemuda/launchrail/blob/master/docs/adr/0015-discovery-research-stage.md)).
38
+ - **Stage 4 ends in a committed file, always.** `launch-grill` closes its interview by writing the surviving constraints to `docs/research/` — the conversation alone never closes the stage, and the skill treats the committed doc as part of its own contract.
39
+ - **Stage 8 scales to the spec's design surface** through a fidelity ladder ([ADR-0016](https://github.com/wemuda/launchrail/blob/master/docs/adr/0016-design-validation-fidelity-ladder.md)): recorded skip, flow diagrams, screen mockups, or Claude Design. The level choice lives inside `design-validation` (recommend, user confirms). It exists to catch "specified but wrong on screen" while the finding still costs a spec edit rather than re-cut tickets — that's why it precedes stage 9 and is not stage 11, which checks the *built* product. Even a skip is recorded through the skill, so the gate stays artifact-based.
40
+ - **Stage 10 is one door.** `/launch-implement` drives the Ralph loop ([ADR-0017](https://github.com/wemuda/launchrail/blob/master/docs/adr/0017-implementation-loop-provider.md) as amended by [ADR-0020](https://github.com/wemuda/launchrail/blob/master/docs/adr/0020-independent-skill-set.md)). Launchrail owns both edges of the loop: `ready-for-agent` tickets with `Blocked by: #n` edges in, `launchrail verify` (+ browser smoke where enabled) gating every merge.
41
+
42
+ ## Sizing the work in the delivery loop
43
+
44
+ The stages above take a fresh project to its first release. After that, the delivery loop repeats once per feature, and `launch` sizes each feature so its planning depth matches the work ([ADR-0014](https://github.com/wemuda/launchrail/blob/master/docs/adr/0014-start-feature-conductor.md), folded into `launch` by [ADR-0018](https://github.com/wemuda/launchrail/blob/master/docs/adr/0018-implement-front-door.md)):
45
+
46
+ - **Large feature** — discovery when it opens new tech territory, `launch-wayfinder` to break it down, a grill, `launch-spec`, design validation, then `launch-tickets`.
47
+ - **Semi feature** — a grill, `launch-spec`, optionally design validation, then `launch-tickets`.
48
+ - **Small feature** — a grill straight to `launch-tickets`.
49
+
50
+ Every size ends the same way: `/launch-implement`, gated by `launchrail verify`. Sizing changes *how many* planning stages a feature needs, never *who owns* them.
51
+
52
+ ## Adopting an existing project
53
+
54
+ When `.launchrail.yml` records `origin: existing`, stage 1 is reached through the Launchrail `project-alignment` skill: it inventories what the codebase already has, infers a draft vision from the code, interviews only the gaps, and detects the existing design system as the baseline for stages 2 and 8, then hands to `vision-creation` to commit ([ADR-0013](https://github.com/wemuda/launchrail/blob/master/docs/adr/0013-existing-project-alignment.md)). Alignment is an on-ramp onto the same rail, not a second workflow.
55
+
56
+ ## Conductor rules
57
+
58
+ The contract for `launch`, `/launch-implement`, and any agent driving the rail. The conductors execute these rules; this document owns them.
59
+
60
+ - **One owner per stage, invoked by name.** Every stage has exactly one owning skill. Invoke it by name; do not paraphrase, wrap, re-prompt, or re-derive its work inline — the skill is the only place its stage's behavior lives.
61
+ - **Artifacts gate stages, not chat memory.** A stage is done only when its committed artifact exists; detect by reading the repository, and when a signal is ambiguous (a template-only vision, an abandoned spec draft), ask rather than assume. Detection and sizing are read-only — every write happens inside the stage owner.
62
+ - **User-typed stages get a prepared handoff, never reverse-engineering.** A `disable-model-invocation` refusal is the cue to hand over, not to reproduce the skill's work by hand or grep vendored skill files. A prepared handoff is three moves: confirm the stage's input artifacts are committed; hand the user the exact, fully-argumented command naming those inputs (a bare `/skill` sends it re-deriving what your inputs already settle — arguments that point at committed inputs are parameters, not paraphrase); pick up automatically once the stage's artifact lands.
63
+ - **The grill feeds research.** Run the grill before technical research and hand research the grill's surviving constraints as its brief.
64
+ - **`ready-for-agent` marks tickets, never specs.** The implementation loop's frontier is every open issue wearing that label, and it cannot tell prose from work — a spec or research note published to the tracker takes a different label (e.g. `spec`), or the loop will dispatch the document as work. Relabel before anyone starts the loop.
65
+ - **Implementation is never started unprompted.** Stage 10 belongs to the user: conductors hand over `/launch-implement` and explain; they do not launch it.
66
+ - **Everything the workflow produces is project-owned.** Vision, research, ADRs, specs, tickets — Launchrail tooling never overwrites them.
67
+ - **Setup gaps are action, not conversation.** Known, additive fixes (commit untracked init output, run `init` when the manifest is missing, `sync` when loop materials are absent) get applied and reported; questions are saved for product artifacts, where intent is genuinely unknowable. Init owns installs — never improvise a dependency install from the web.
68
+
69
+ ## Stage-skipping by project mode
70
+
71
+ The manifest's `mode` calibrates rigor, not stage order:
72
+
73
+ - `spike` — stages 2–5 and 8 may be skipped deliberately; record the skip in the vision's non-goals.
74
+ - `standard-mvp` — the default path; skip nothing silently.
75
+ - `high-rigor` — no skips; ADRs for every stage-6 decision, and design validation covers error and edge states, not just happy paths.
76
+
77
+ When a stage looks skipped, check the vision's non-goals before deciding whether it's deliberate — and if you still can't tell, ask.
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: launch-browser-smoke
3
+ description: Drive the running app through its defined smoke journeys in a real browser and produce a Launchrail evidence bundle. Use when user-facing work needs verification beyond deterministic tests, when the user asks to smoke-test the app, or before declaring user-facing work done in a project with the browser-testing module enabled (.launchrail.yml modules.browser-testing).
4
+ ---
5
+
6
+ # Browser smoke testing
7
+
8
+ Drive the real application through its user journeys in a browser and record evidence. Agentic smoke testing supplements deterministic tests — it never replaces them, and it never substitutes assertion for evidence.
9
+
10
+ ## Preconditions
11
+
12
+ 1. `.launchrail.yml` has `modules.browser-testing: true`. If not, stop and suggest `npx @wemuda/launchrail add browser-testing`.
13
+ 2. Deterministic checks pass first: run `node scripts/verify.mjs`. If verify fails, fix that before smoke testing — smoke runs on top of a green build.
14
+ 3. The app is running. Start it with `node scripts/dev.mjs` (use `--background` in cloud or CI sessions; logs land in `.launchrail/state/dev.log`). In a fresh clone, run `node scripts/setup.mjs` first.
15
+
16
+ ## Run contract
17
+
18
+ 1. **Collect the journeys.** Read `docs/testing/smoke-journeys.md` (sections headed `## Journey:`) plus any journeys defined in the ticket or spec under verification. Each journey has a start point, steps, and verify checks.
19
+ 2. **Scaffold the evidence bundle.** Run `npx @wemuda/launchrail smoke` (add `--url <url>` for a preview environment). It confirms the app responds and creates `artifacts/verification/<run-id>/` containing `meta.json`, a `summary.md` skeleton, and `screenshots/` + `traces/` directories. If it reports the app unreachable, start the app — do not skip the journey.
20
+ 3. **Drive each journey in a real browser** — Playwright MCP, browser tools, or a Playwright script, whichever is available. The browser-testing module seeds a Playwright MCP server (`.mcp.json`); approve it once in Claude Code to drive the browser interactively, or fall back to a Playwright script in headless CI. Follow the steps as a user would: click, type, navigate. Try realistic variations and obvious edge cases, and watch the console and network panel as you go.
21
+ 4. **Capture evidence while testing, not afterwards:**
22
+ - Screenshots of each key state → `screenshots/`
23
+ - Console errors and warnings → `console.log`
24
+ - Failed or unexpected requests → `network-errors.json`
25
+ - Playwright traces where available → `traces/`
26
+ 5. **Apply the standard checks to every journey:**
27
+ - No uncaught console errors
28
+ - No failed API requests
29
+ - The success state is visible
30
+ - Data remains after refresh
31
+
32
+ ## When you find a real bug
33
+
34
+ 1. Record the precise reproduction in the evidence bundle.
35
+ 2. Add or update a deterministic test that fails on the bug.
36
+ 3. Fix the bug.
37
+ 4. Prove the deterministic test passes.
38
+ 5. Re-run the affected journey.
39
+ 6. Keep the trace or screenshot that shows the failure.
40
+
41
+ This turns exploratory findings into permanent regression coverage instead of forgotten discoveries.
42
+
43
+ ## Completing the run
44
+
45
+ - Fill in `summary.md` completely: journey outcomes, standard checks, evidence references, deviations, newly added tests, remaining blockers. Check only boxes you actually verified.
46
+ - Record any deviation from the spec or design in `deviations.md` next to the summary.
47
+ - A journey you could not complete is a failure or a blocker, never a pass.
48
+ - Never mark a journey passed while it has unexplained console or network errors.
49
+ - The committed record is `summary.md`, `deviations.md`, and `meta.json`; bulky evidence stays local or becomes a CI artifact.