@wemuda/launchrail 1.15.0 → 1.16.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 +2 -1
- package/assets/ralph.workflow.js +28 -7
- package/assets/skills/launchrail/launch-browser-smoke/SKILL.md +18 -11
- package/assets/skills/launchrail/launch-browser-smoke/composed-stack.md +46 -0
- package/assets/skills/launchrail/launch-implement/SKILL.md +1 -1
- package/assets/skills/launchrail/launch-loop-readiness/SKILL.md +1 -0
- package/assets/skills/launchrail/launch-ralph/SKILL.md +2 -2
- package/dist/commands/add.js +3 -3
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/dev.d.ts +35 -0
- package/dist/commands/dev.js +158 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/doctor.js +20 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.js +3 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/verify.js +7 -0
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/browser-testing.d.ts +2 -3
- package/dist/lib/browser-testing.js +9 -59
- package/dist/lib/browser-testing.js.map +1 -1
- package/dist/lib/manifest.d.ts +14 -0
- package/dist/lib/manifest.js +29 -1
- package/dist/lib/manifest.js.map +1 -1
- package/dist/lib/seeds.js +3 -2
- package/dist/lib/seeds.js.map +1 -1
- package/dist/lib/stack.d.ts +77 -0
- package/dist/lib/stack.js +210 -0
- package/dist/lib/stack.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ From there, the day-to-day driver is not the CLI — it's the **`launch` skill**
|
|
|
60
60
|
- **`launch-design-handoff`** — the design→code on-ramp: drop a Claude Design export (a zip, a folder, artboards) and it becomes a committed handoff package under `docs/design/` — read against the code and design system, gaps recorded as the grill's agenda — then sized into the loop ([ADR-0024](https://github.com/wemuda/launchrail/blob/master/docs/adr/0024-design-handoff-onramp.md))
|
|
61
61
|
- **`launch-vision-creation`**, **`launch-discovery`**, **`launch-grill`**, **`launch-research`** — vision, then the divergent landscape scan, the grill (the convergent interview that runs both as the foundation's complexity grill and per-feature before speccing, keeping the glossary and ADRs honest as it goes), and primary-source research
|
|
62
62
|
- **`launch-wayfinder`**, **`launch-spec`**, **`launch-tickets`**, **`launch-design-validation`** — break big work into decision maps, synthesize the spec, validate it visually, and cut tracer-bullet tickets with blocking edges
|
|
63
|
-
- **`launch-browser-smoke`** — drives the running app in a real browser to see a just-built change working — one-off, from the shell with `agent-browser
|
|
63
|
+
- **`launch-browser-smoke`** — drives the running app in a real browser to see a just-built change working — one-off, from the shell with `agent-browser` against a stack `launchrail dev` started, no test suite and no evidence bundle ([ADR-0034](https://github.com/wemuda/launchrail/blob/master/docs/adr/0034-browser-smoke-one-off-driving.md), [ADR-0036](https://github.com/wemuda/launchrail/blob/master/docs/adr/0036-cli-owned-stack-start.md); with the browser-testing module)
|
|
64
64
|
- **`launch-implement`** — the one door to building: `/launch-implement` drives ready tickets to verified merges through the Ralph loop (a ticket number builds just that one; "the next 5 of spec #2" scopes and caps a run)
|
|
65
65
|
- **`launch-ralph`**, **`launch-ralph-implement`**, **`launch-code-review`**, **`launch-resolving-merge-conflicts`** — the verification-gated loop engine behind that door, installed by `init`
|
|
66
66
|
- **`launch-loop-readiness`** — checks and tunes a repo for the loop: measures the gates, sets the fast per-land gate, parallelizes the e2e specs, shares caches for parallel builders, narrows CI triggers, creates the tracker labels, adds hosted-session setup ([ADR-0033](https://github.com/wemuda/launchrail/blob/master/docs/adr/0033-loop-readiness.md)); `doctor` shows the same findings as warn-only readiness lines
|
|
@@ -74,6 +74,7 @@ npx @wemuda/launchrail sync # apply managed updates + run migra
|
|
|
74
74
|
npx @wemuda/launchrail add browser-testing # enable a module
|
|
75
75
|
npx @wemuda/launchrail doctor # repository and environment checks
|
|
76
76
|
npx @wemuda/launchrail verify # deterministic verification gate
|
|
77
|
+
npx @wemuda/launchrail dev --background # start the smokeable stack from the manifest; --check proves the contract
|
|
77
78
|
npx @wemuda/launchrail eject <module|file> # opt out of management (vendor mode: --all)
|
|
78
79
|
```
|
|
79
80
|
|
package/assets/ralph.workflow.js
CHANGED
|
@@ -139,6 +139,15 @@ const PREFLIGHT_SCHEMA = {
|
|
|
139
139
|
base: { type: 'string', description: "the run's integration base: the declared target branch when one is set, else the default branch" },
|
|
140
140
|
defaultBranch: { type: 'string', description: 'the repository default branch name' },
|
|
141
141
|
targetCreated: { type: 'boolean', description: 'true when a declared target branch was missing from the remote and was created from the default branch tip' },
|
|
142
|
+
baseOnRemote: {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description:
|
|
145
|
+
'the base tip as `git ls-remote --heads origin <base>` reports it on the LIVE remote (after `git fetch --prune`), or "" when the base is absent from origin. NEVER inferred from `git branch -r` or a local refs/remotes/origin/<base> tracking ref — those go stale and make a local-only base look present. green requires this non-empty and equal to headSha.',
|
|
146
|
+
},
|
|
147
|
+
basePushed: {
|
|
148
|
+
type: 'boolean',
|
|
149
|
+
description: 'true when the base existed only locally (a session-pinned working branch with unpushed commits) and preflight pushed it to origin so the run can land onto it',
|
|
150
|
+
},
|
|
142
151
|
issueTracker: { type: 'string', description: 'issueTracker from .launchrail.yml (github | linear | none)' },
|
|
143
152
|
trackerAccess: {
|
|
144
153
|
type: 'string',
|
|
@@ -684,13 +693,13 @@ async function drive(pre, ticket, pushed) {
|
|
|
684
693
|
// ---------------------------------------------------------------------------
|
|
685
694
|
phase('Preflight')
|
|
686
695
|
const pre = await agent(
|
|
687
|
-
`Preflight for a Ralph loop run in THIS checkout — the loop lands tickets here (one land at a time), while builders use their own worktrees. Report actual state; fix nothing — the permitted mutations are
|
|
696
|
+
`Preflight for a Ralph loop run in THIS checkout — the loop lands tickets here (one land at a time), while builders use their own worktrees. Report actual state; fix nothing — the permitted mutations are publishing the declared integration base to origin (step 2: creating it from the default branch tip, or pushing a base that exists only locally) and syncing this checkout onto it.
|
|
688
697
|
1. Read .launchrail.yml (issueTracker, testing commands, modules) and AGENTS.md (verbatim commands).
|
|
689
|
-
2. Identify the repo (git remote) and its default branch; report the default branch name as defaultBranch. ${
|
|
698
|
+
2. Identify the repo (git remote) and its default branch; report the default branch name as defaultBranch. Prune stale remote-tracking refs first: \`git fetch --prune origin\` — a leftover refs/remotes/origin/<base> from an earlier fetch otherwise makes a local-only base look present. Then judge the base's presence ON THE LIVE REMOTE with \`git ls-remote --heads origin <base>\`, NEVER \`git branch -r\` or a local tracking ref: a base that is not actually on origin passes on a stale ref here and then fails every land at \`git fetch origin <base>\`. ${
|
|
690
699
|
POLICY.target
|
|
691
|
-
? `This run
|
|
692
|
-
: `This run lands onto the default branch (trunk) — that branch is the base. If
|
|
693
|
-
} Sync the base INTO THIS CHECKOUT: the tree must be clean (\`git status --porcelain\` shows no modified or staged files — untracked files are fine; a dirty tree is not green: say so, stash nothing);
|
|
700
|
+
? `This run's integration base is the branch "${POLICY.target}". If \`git ls-remote --heads origin ${POLICY.target}\` returns it, that is the base. If it is ABSENT from origin: when a local "${POLICY.target}" branch holds commits not yet on origin (a session-pinned working branch), those commits ARE the base — push it (\`git push -u origin ${POLICY.target}\`) and report basePushed: true; otherwise mint it from the default branch's tip (\`git branch ${POLICY.target} origin/<default> && git push -u origin ${POLICY.target}\`; no force, the default branch itself is never touched) and report targetCreated: true. If you can neither push nor create it, report not green with "push the base first: ${POLICY.target} exists only locally". A missing DEFAULT branch is still not green — do not guess.`
|
|
701
|
+
: `This run lands onto the default branch (trunk) — that branch is the base. If \`git ls-remote --heads origin <default>\` does not return it, report not green and say the base is missing — do not guess another branch.`
|
|
702
|
+
} Sync the base INTO THIS CHECKOUT: the tree must be clean (\`git status --porcelain\` shows no modified or staged files — untracked files are fine; a dirty tree is not green: say so, stash nothing); with origin now carrying the base, check out the base (tracking origin) and \`git merge --ff-only origin/<base>\` — a local base that has diverged from origin is not green (say "push or reset it first"). Report the base name as base and its tip as headSha, then re-run \`git ls-remote --heads origin <base>\` and report the sha it returns as baseOnRemote — green REQUIRES baseOnRemote non-empty and equal to headSha (origin carries the base at exactly the tip this checkout builds against).
|
|
694
703
|
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.
|
|
695
704
|
4. List in-flight work from previous sessions: \`git ls-remote --heads origin 'ralph/*'\` — report every branch with its sha as pushedBranches (the loop adopts them; delete nothing).
|
|
696
705
|
5. Run the project's install command (report it verbatim as installCommand). ${
|
|
@@ -699,7 +708,7 @@ const pre = await agent(
|
|
|
699
708
|
: 'Then run the FULL verification gate'
|
|
700
709
|
}: 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.
|
|
701
710
|
Report verifyCommand as "npx @wemuda/launchrail verify" and fastGateCommand as "npx @wemuda/launchrail verify --fast" (the fast tier: testing.checkCommand, else the unit command — never e2e).
|
|
702
|
-
green means: base synced in this checkout AND (the full gate exited 0 OR it was skipped as known green).`,
|
|
711
|
+
green means: the base is confirmed on origin (baseOnRemote non-empty and equal to headSha — proven by \`git ls-remote\`, never inferred from a local tracking ref) AND synced in this checkout AND (the full gate exited 0 OR it was skipped as known green).`,
|
|
703
712
|
{ label: 'preflight', phase: 'Preflight', schema: PREFLIGHT_SCHEMA },
|
|
704
713
|
)
|
|
705
714
|
if (!pre) throw new Error('preflight agent died — refusing to start')
|
|
@@ -708,6 +717,18 @@ if (!pre.green) {
|
|
|
708
717
|
// can only end with unverifiable results.
|
|
709
718
|
return { refused: true, reason: 'preflight not green', failures: pre.failures }
|
|
710
719
|
}
|
|
720
|
+
// The green verdict must be grounded in the LIVE remote, not inferred locally: a stale
|
|
721
|
+
// refs/remotes/origin/<base> tracking ref makes a local-only base look present, preflight
|
|
722
|
+
// reports green, and then EVERY land fails at `git fetch origin <base>` (with canary, no
|
|
723
|
+
// land ever succeeds and the whole run is wasted). Refuse unless preflight confirmed the
|
|
724
|
+
// base on origin — via `git ls-remote` — at exactly the tip this checkout builds against.
|
|
725
|
+
if (!pre.baseOnRemote || pre.baseOnRemote !== (pre.headSha ?? '')) {
|
|
726
|
+
return {
|
|
727
|
+
refused: true,
|
|
728
|
+
reason: `preflight reported green but did not confirm the base ${pre.base} on origin at the checkout tip (git ls-remote --heads origin ${pre.base} → ${pre.baseOnRemote || 'absent'}, headSha ${pre.headSha || 'unset'}) — refusing to dispatch; every land would fail at git fetch origin ${pre.base}`,
|
|
729
|
+
failures: pre.failures,
|
|
730
|
+
}
|
|
731
|
+
}
|
|
711
732
|
if ((pre.issueTracker ?? 'none') === 'none') {
|
|
712
733
|
return { refused: true, reason: 'no issue tracker configured (.launchrail.yml issueTracker: none) — Ralph needs tickets' }
|
|
713
734
|
}
|
|
@@ -727,7 +748,7 @@ phase('Graph')
|
|
|
727
748
|
log(
|
|
728
749
|
`Base green at ${pre.headSha ?? pre.base} on ${pre.base}${pre.skippedGate ? ' (known green — gate skipped)' : ''}. ` +
|
|
729
750
|
(POLICY.target
|
|
730
|
-
? `Consolidating onto ${pre.base}${pre.targetCreated ? ' (created from the default branch tip)' : ''}; ${pre.defaultBranch || 'the default branch'} stays untouched. `
|
|
751
|
+
? `Consolidating onto ${pre.base}${pre.targetCreated ? ' (created from the default branch tip)' : pre.basePushed ? ' (pushed to origin from local-only commits)' : ''}; ${pre.defaultBranch || 'the default branch'} stays untouched. `
|
|
731
752
|
: `Trunk mode — each ticket lands on ${pre.base}. `) +
|
|
732
753
|
(POLICY.only.length > 0
|
|
733
754
|
? `Scoped to ${POLICY.only.map((n) => `#${n}`).join(', ')}.`
|
|
@@ -7,23 +7,30 @@ description: Drive the running app in a real browser to see a just-built change
|
|
|
7
7
|
|
|
8
8
|
A browser smoke is you driving the real stack in a real browser to check that what you just built looks right and works. It is **one-off**: it lives for this change and ends in a verdict, not in a test file, a journeys catalogue, or a committed report ([ADR-0034](https://github.com/wemuda/launchrail/blob/master/docs/adr/0034-browser-smoke-one-off-driving.md)). Deterministic coverage is the other lane — `npx @wemuda/launchrail verify` with the unit command and the thin Playwright e2e specs — and a smoke never grows it by accident.
|
|
9
9
|
|
|
10
|
+
The stack you drive starts through the CLI, never through a hand-adapted script ([ADR-0036](https://github.com/wemuda/launchrail/blob/master/docs/adr/0036-cli-owned-stack-start.md)): `launchrail dev` owns the start command, the readiness wait, the state files and the driver's browser, so what this skill relies on is the same in every project.
|
|
11
|
+
|
|
12
|
+
## Step zero — a fresh clone has nothing installed
|
|
13
|
+
|
|
14
|
+
A hosted session (`CLAUDE_CODE_REMOTE=true`) is always a fresh clone, and a missing `node_modules` fails every later step with a misleading error. Run `node scripts/setup.mjs` first, unconditionally, in any hosted session and whenever `node_modules` is missing. It installs dependencies, Playwright's Chromium, and `agent-browser`.
|
|
15
|
+
|
|
10
16
|
## Preconditions
|
|
11
17
|
|
|
12
18
|
1. `.launchrail.yml` has `modules.browser-testing: true`. If not, stop and suggest `npx @wemuda/launchrail add browser-testing`.
|
|
13
|
-
2. The
|
|
14
|
-
3.
|
|
15
|
-
4.
|
|
19
|
+
2. **The gate, proportionate.** If `testing.checkCommand` is set, run `npx @wemuda/launchrail verify --fast` and fix red before smoking. If it is not set, do **not** run the full suite as a stand-in; say in your handoff that no fast gate is configured and the smoke ran without one (`launch-loop-readiness` sets it). Inside a Ralph dispatch the fast gate already ran before this step.
|
|
20
|
+
3. **Start the stack from the manifest:** `npx @wemuda/launchrail dev --background` (add `--port <n>` with a port nobody else uses when other builders share the machine). It runs `smoke.start`, else `testing.devCommand`, waits until every origin answers, and writes `.launchrail/state/stack.json` (origins, pid, log — plus whatever a composed start command adds: keys, fixture ids, a mailbox path), `dev.url`, `dev.pid`, and `browser.env`. Read your URLs from `stack.json`; never assume `testing.appUrl`.
|
|
21
|
+
4. **If the stack cannot start from the manifest, the smoke is blocked — do not rebuild the stack by hand inside a ticket.** Frontends that come up without their backend, a start command that needs Docker, a page that says "not configured": these mean the project has no `smoke.start` for its composed stack. Report the smoke as blocked with the exact gap, and point at [composed-stack.md](composed-stack.md) — the fix is a start command plus a manifest entry, proven once with `launchrail dev --check`, not fifteen tool calls of archaeology in every smoke.
|
|
22
|
+
5. **The driver.** `set -a; . .launchrail/state/browser.env; set +a` before the first `agent-browser` command: it points the driver at Playwright's Chromium and, in a root container, turns the sandbox off. Then `npx agent-browser --version` must answer. Sharp edge: the env vars are the reliable form — `--executable-path` is ignored once a daemon is running and only warns, so a first `open` can look fine and the next `snapshot` fail with "Chrome not found". Change the browser only after `npx agent-browser close`. If no Chromium resolves at all, `npx agent-browser install` fetches its own; if the driver still cannot run, use the fallback at the end.
|
|
16
23
|
|
|
17
24
|
## The run
|
|
18
25
|
|
|
19
26
|
1. **Decide what to check.** From the ticket's acceptance criteria and your diff, write three to six steps for *this change*: where to start, what to click or type, what must be visible at the end. They live in your head and your handoff — never in a repository file.
|
|
20
27
|
2. **Drive them, one session per ticket.** `export AGENT_BROWSER_SESSION=<branch-or-ticket>` keeps your browser apart from other builders'. Then, from the shell:
|
|
21
|
-
- `npx agent-browser open <url>` — start on the
|
|
22
|
-
- `npx agent-browser snapshot -i` — the interactive elements with refs (`@e1`, `@e2`, …).
|
|
23
|
-
- `npx agent-browser click @e3`, `fill @e5 "text"`, `type`, `press Enter` — act as a user would.
|
|
24
|
-
- `npx agent-browser wait --load networkidle`, `wait --text "Saved"`, `wait <selector>` — **after every action that triggers a request or opens a modal, wait before the next snapshot**; a snapshot taken too early is the usual false failure.
|
|
28
|
+
- `npx agent-browser open <url>` — start on the origin the change lives on, from `stack.json`.
|
|
29
|
+
- `npx agent-browser snapshot -i` — the interactive elements with refs (`@e1`, `@e2`, …). **Refs die with navigation:** re-snapshot after every `open`, reload, or route change before clicking anything.
|
|
30
|
+
- `npx agent-browser click @e3`, `fill @e5 "text"`, `type`, `press Enter` — act as a user would. Prefer refs, or role-based finds, over text finds: a text find for a word that appears in a menu can open the menu instead of the control you meant.
|
|
31
|
+
- `npx agent-browser wait --load networkidle`, `wait --text "Saved"`, `wait <selector>` — **after every action that triggers a request or opens a modal, wait before the next snapshot**; a snapshot taken too early is the usual false failure. **Success toasts auto-dismiss:** a `wait --text` on one can time out on a success that did happen, so confirm the outcome through `network requests` (the request and its status) or the resulting state, not the toast alone.
|
|
25
32
|
- `npx agent-browser screenshot <path>` — and look at the image yourself (open it with the Read tool). Layout, copy, empty states, and wrong-but-rendering are what a script cannot judge; that judgment is the point of this lane.
|
|
26
|
-
- `npx agent-browser errors`, `console`, `network requests` — after each step, not only at the end.
|
|
33
|
+
- `npx agent-browser errors`, `console`, `network requests` — after each step, not only at the end. `errors` is uncaught exceptions only; `console` carries the page's own error logs.
|
|
27
34
|
3. **Click around beyond the happy path.** The obvious wrong input, the empty state, a reload, the back button. You are looking for what the ticket did not spell out.
|
|
28
35
|
4. **The standard checks, every time:** no uncaught exceptions, no failed requests, the success state visible in a screenshot you looked at, the data still there after a reload.
|
|
29
36
|
5. **Design:** compare the screen to a design only when the ticket or spec names one (a `docs/design/` handoff, a mockup). Never invent a comparison.
|
|
@@ -36,10 +43,10 @@ A browser smoke is you driving the real stack in a real browser to check that wh
|
|
|
36
43
|
|
|
37
44
|
## Done
|
|
38
45
|
|
|
39
|
-
- `npx agent-browser close`,
|
|
40
|
-
- Your handoff states, in a few lines, what you drove and what you saw — that is the record. Nothing is committed for the smoke: no journeys file, no `artifacts/` bundle, no screenshots in the repo.
|
|
46
|
+
- `npx agent-browser close`, then `npx @wemuda/launchrail dev --stop`.
|
|
47
|
+
- Your handoff states, in a few lines, what you drove and what you saw — that is the record. Nothing is committed for the smoke: no journeys file, no `artifacts/` bundle, no screenshots in the repo. Everything you wrote lives under `.launchrail/state/`, which ignores itself in git.
|
|
41
48
|
- A smoke you could not drive is a failure, not a pass. Never report one you did not run.
|
|
42
49
|
|
|
43
50
|
## Fallback — no driver
|
|
44
51
|
|
|
45
|
-
If `agent-browser` cannot
|
|
52
|
+
If `agent-browser` cannot run here even after `browser.env` and `install`, write a throwaway `playwright-core` script under `.launchrail/state/` that drives the same steps and prints what it saw, and run it with the project's Playwright. It stays there — it never moves under `tests/`, and it is no substitute for looking at the screenshots.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Starting a composed stack for the browser smoke
|
|
2
|
+
|
|
3
|
+
`launchrail dev` needs one command that brings up everything the smoke drives, and a list of the origins that must answer. For a single-process app that is `testing.devCommand` and `testing.appUrl`, and nothing else is needed. This page is for the other kind: a customer page running an SDK, an API, a dashboard on a third origin, a worker, fixtures — where "start the dev server" starts five frontends and no backend, and the backend's own start needs Docker that a hosted session does not have.
|
|
4
|
+
|
|
5
|
+
## The contract
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
# .launchrail.yml
|
|
9
|
+
smoke:
|
|
10
|
+
start: node scripts/smoke-stack.mjs # boots the whole smokeable stack, in-process where it can
|
|
11
|
+
origins: # every one must answer HTTP before the stack is "ready"
|
|
12
|
+
dashboard: http://localhost:5173
|
|
13
|
+
demo: http://localhost:4173
|
|
14
|
+
api: http://localhost:3001/health
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`launchrail dev --background` runs `start`, polls the origins, and writes `.launchrail/state/stack.json`. The start command **merges** anything the smoke will need into that same file — read it, add your keys, write it back; never replace it, the CLI merges too:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"origins": { "dashboard": "http://localhost:5173", "demo": "http://localhost:4173" },
|
|
22
|
+
"pid": 41337,
|
|
23
|
+
"fixtures": { "orgId": "org_test", "projectId": "prj_test", "publishableKey": "pk_test_…" },
|
|
24
|
+
"mailbox": ".launchrail/state/mail/"
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`launchrail dev --check` proves the contract once — start, wait, assert the files, tear down — at adoption and whenever the stack changes. Nothing in a smoke should discover the stack; the smoke reads `stack.json` and drives.
|
|
29
|
+
|
|
30
|
+
## The pattern that works without Docker
|
|
31
|
+
|
|
32
|
+
Every platform arrives at the same shape once it needs to run in a fresh container:
|
|
33
|
+
|
|
34
|
+
1. **Boot the real backend in-process** against an in-process database (Postgres-in-WASM, SQLite, an embedded engine) — the real app module, not a mock server. Migrate and seed on start.
|
|
35
|
+
2. **Stub only the outbound seams:** mail to a captured sender, object storage to memory, queues to a synchronous stub, third-party APIs to recorded responses. Everything the user's click touches stays real.
|
|
36
|
+
3. **Seed fixtures directly**, through the backend's own services or repository layer, not through the UI: a member, an org, a project, a publishable key, an allowlisted origin. Write their ids and keys into `stack.json`.
|
|
37
|
+
4. **Run the real frontend dev servers** on their real origins, so CORS, cookies across ports, and credentialed requests are exercised for real. Point them at the in-process API through their normal env vars.
|
|
38
|
+
5. **Solve auth for the driver.** Every product has one of these. Magic-link sign-in: the captured mail sender writes each message to `mailbox`, and the smoke reads the link from the newest file and opens it. Password or session sign-in: seed the credential and put it in `fixtures`. Never bypass auth with a test-only backdoor in the app — the smoke should sign in the way a user does.
|
|
39
|
+
6. **Write the customer-facing page** the SDK runs on as a small static host (a demo page) that reads the publishable key from `stack.json` or an env var, so it comes up configured, not "not configured".
|
|
40
|
+
7. **Exit cleanly on SIGTERM** so `launchrail dev --stop` tears everything down: one process group, child servers closed in a handler.
|
|
41
|
+
|
|
42
|
+
A project that already has such a harness for its own full-stack tests is one script away: wrap the harness in `scripts/smoke-stack.mjs`, keep it running instead of exiting after a test, and merge the URLs and fixtures into `stack.json`.
|
|
43
|
+
|
|
44
|
+
## What the smoke then does
|
|
45
|
+
|
|
46
|
+
`set -a; . .launchrail/state/browser.env; set +a`, read `stack.json`, `agent-browser open` the origin the ticket touches, sign in via the `mailbox` or `fixtures`, and drive the change. Nothing here is written back to the repository; `.launchrail/state/` ignores itself.
|
|
@@ -18,7 +18,7 @@ From `.launchrail.yml`: `issueTracker` and the `testing` commands. From the argu
|
|
|
18
18
|
- **a count** — "the next 5", "max 5" → the loop with a merge cap. Don't hand-pick which five: the cap is a stop condition, and the frontier decides the order — the loop stops after that many *verified merges* and leaves the rest ready;
|
|
19
19
|
- **a spec, slice, or epic reference** — "spec #2's tickets", "the rest of slice 1" → resolve it to explicit numbers against the live tracker: the open tickets that belong to it (a "Part of: #n" line, the spec issue's ticket list, or a label), plus any open in-set blockers so the scope stays dependency-closed. Combinations compose: "the next 5 of spec #2" → that spec's tickets *and* a cap of 5.
|
|
20
20
|
|
|
21
|
-
**Resolve the integration target with the scope.** Every multi-ticket run **consolidates by default** (ADR-0022, ADR-0026): its per-ticket branches are landed onto one integration branch by the loop's own local gate, the default branch stays untouched, and the run ends by *offering* one release PR `<target> → <default>` for the user to review and merge — the one place cloud CI runs — never merging to the default branch on its own. You name that branch before anything launches, scope-native: the branch the user named if they gave one ("collect spec #44 on `spec/44-mvp`"); else **the session's designated working branch** when the environment pinned one at start — a hosted session's `claude/...` branch exists to receive exactly this run, so use it rather than minting a `spec/*` twin beside it (ADR-0028); else the scope's own name when it maps to a spec, epic, or slice (`spec/<n>-<slug>`); else a generated fallback for an ad-hoc frontier (`launch/frontier-<today>`, or `launch/tickets-<n>-<m>` for a handful of loose numbers). A named target the remote lacks is created from the default branch's tip
|
|
21
|
+
**Resolve the integration target with the scope.** Every multi-ticket run **consolidates by default** (ADR-0022, ADR-0026): its per-ticket branches are landed onto one integration branch by the loop's own local gate, the default branch stays untouched, and the run ends by *offering* one release PR `<target> → <default>` for the user to review and merge — the one place cloud CI runs — never merging to the default branch on its own. You name that branch before anything launches, scope-native: the branch the user named if they gave one ("collect spec #44 on `spec/44-mvp`"); else **the session's designated working branch** when the environment pinned one at start — a hosted session's `claude/...` branch exists to receive exactly this run, so use it rather than minting a `spec/*` twin beside it (ADR-0028); else the scope's own name when it maps to a spec, epic, or slice (`spec/<n>-<slug>`); else a generated fallback for an ad-hoc frontier (`launch/frontier-<today>`, or `launch/tickets-<n>-<m>` for a handful of loose numbers). A named target the remote lacks is put on origin by the run's preflight — created from the default branch's tip, or pushed as-is when it exists only locally with committed work (a pinned session's branch), the presence check made against the live remote, never a stale tracking ref (ADR-0035). **Trunk** — each ticket merged straight to the default branch, live the moment it lands — is now the explicit opt-in: choose it only when the user asks in so many words ("land each ticket on master as it goes"), and never when the environment forbids pushing to the default branch. A pinned-branch environment ("develop on this branch; no unprompted PRs") constrains the *deliverable*, which consolidation already honors — the default branch stays untouched, the release PR stays offered-only. It never constrains the *engine*: the user typing `/launch-implement` is the explicit go-ahead for the loop's own mechanics — per-ticket `ralph/*` branches, pushed as they grow and landed on the target by the loop, no PR among them — and is never a reason to drop to sequential in-session building. Whichever target, restate it in the echo — a choice the user should recognize, never silent.
|
|
22
22
|
|
|
23
23
|
**Resolve prose to data before anything launches.** The loop's inputs are ticket numbers and policy values (`only`, `max`, `width`, `target`) — the workflow takes them as JSON args and refuses a natural-language string by design. Translating the user's words into that scope, against live tracker state, is *your* job, and it ends with an echo before any dispatch: "Scope: #14, #15, #19 — the remaining slice-1 tickets; #19 builds after #14. Cap: none. Target: consolidate on `spec/2-checkout` (`master` untouched; one release PR offered at the end). Engine: the `ralph` workflow." A misread scope corrected here costs a sentence; corrected after launch it costs a run. That echo is your one pre-launch report — resolve the scope, repair setup (Step 2), and route (Step 3) without narrating the checks in between; surface a step only when it fails or changes the scope.
|
|
24
24
|
|
|
@@ -26,6 +26,7 @@ It is advice, never a gate: `/launch-implement` runs whether or not this skill e
|
|
|
26
26
|
5. Data: the migration tool and its generate/renumber command; whether tests need a database, ports, or external services, and how they isolate per run.
|
|
27
27
|
6. Tracker: the labels the loop uses (`ready-for-agent`, `ralph:building`, `needs-info`, `spec`) exist — check with the tracker tools named in `docs/agents/issue-tracker.md`.
|
|
28
28
|
7. `.claude/settings.json`: hooks (the Ralph guard, any `SessionStart`), permissions; the seeded `scripts/setup.mjs` when the browser-testing module is on.
|
|
29
|
+
8. With the browser-testing module on: `npx @wemuda/launchrail dev --check` — starts the smokeable stack from the manifest, waits for every origin, asserts the state files, tears down. A failure here is a readiness finding: the project needs `testing.devCommand`, or `smoke.start` plus `smoke.origins` for a composed stack (`.claude/skills/launch-browser-smoke/composed-stack.md`) — fix it in this pass, so no smoke ever discovers the stack by hand.
|
|
29
30
|
|
|
30
31
|
## Step 2 — Measure
|
|
31
32
|
|
|
@@ -17,7 +17,7 @@ This skill is the loop's contract and its supervisor. The loop itself runs as th
|
|
|
17
17
|
|
|
18
18
|
## Policies
|
|
19
19
|
|
|
20
|
-
- **Integration target: declared, singular, restated.** Every run lands its per-ticket branches onto exactly one base, named before anything dispatches and again in the close-out. **Consolidation** (the default, ADR-0026): one integration branch (e.g. `spec/44-mvp`) collects the whole campaign and the default branch is never touched; the front door names it scope-native — the session's designated working branch when the environment pinned one at start, else the scope's own spec/epic/slice name when it maps to one, else a generated `launch/*` fallback — and passes it as the `target` arg. A pinned-branch session (ADR-0028) changes only that name: the user's start authorizes the loop's mechanics, per-ticket `ralph/*` branches included, and the pin's real demands — default branch untouched, release PR offered not opened — are exactly what consolidation already does. The run ends by *offering* one release PR `<target> → <default>` — opened only when the user says so; that PR is where cloud CI runs, once. **Trunk** (the explicit opt-in): the repository's default branch — each verified land is immediately on mainline, and when the run ends there is nothing left to integrate; select it only when the user asks for per-ticket lands on the default branch, and never when the environment forbids pushing there.
|
|
20
|
+
- **Integration target: declared, singular, restated.** Every run lands its per-ticket branches onto exactly one base, named before anything dispatches and again in the close-out. **Consolidation** (the default, ADR-0026): one integration branch (e.g. `spec/44-mvp`) collects the whole campaign and the default branch is never touched; the front door names it scope-native — the session's designated working branch when the environment pinned one at start, else the scope's own spec/epic/slice name when it maps to one, else a generated `launch/*` fallback — and passes it as the `target` arg. A pinned-branch session (ADR-0028) changes only that name: the user's start authorizes the loop's mechanics, per-ticket `ralph/*` branches included, and the pin's real demands — default branch untouched, release PR offered not opened — are exactly what consolidation already does. The run ends by *offering* one release PR `<target> → <default>` — opened only when the user says so; that PR is where cloud CI runs, once. **Trunk** (the explicit opt-in): the repository's default branch — each verified land is immediately on mainline, and when the run ends there is nothing left to integrate; select it only when the user asks for per-ticket lands on the default branch, and never when the environment forbids pushing there. Whether the base is on the remote is judged against the live remote (`git ls-remote` after a pruning fetch, never a stale `origin/<base>` tracking ref): a named consolidation target absent there is created from the default branch's tip, a session-pinned base that exists only locally is pushed to origin, and only then does preflight verify — its green verdict asserts the base is on origin, it never infers it from a local ref. A missing *default* branch stays a refusal. `Closes #n` never auto-fires off the default branch, so the explicit post-land close is load-bearing, not belt-and-suspenders.
|
|
21
21
|
- **The loop lands in the checkout it was launched from.** Preflight syncs the base into it (clean tree required — commit or stash first; a local base that has diverged from the remote is a refusal), builders work in their own worktrees, and every land and checkpoint runs there one at a time — the warm caches are what make the gate cheap. At the end the checkout sits on the target at its landed tip.
|
|
22
22
|
- **Width: 3** implementers at once, kept busy by a **work pool** — when one finishes, the next ready ticket dispatches; a slow ticket never holds the others. Width multiplies conflict rate and shared-machine load, not just throughput — use 1 until a run has landed tickets cleanly on this project (the workflow's `canary: true` encodes exactly that). Cut a batch below width when its tickets would obviously collide (same module, same files); when in doubt, narrow. Tickets that add DB migrations are a known collision (two parallel implementers both claim the next migration number): the workflow keeps **one migration-adding ticket in flight at a time** from the graph reader's flag; in skill mode, serialize them yourself.
|
|
23
23
|
- **Ordering: critical path first.** Among ready tickets, the one with the most transitive dependents dispatches first, so a wide graph unblocks quickly — computed from the parsed edges, never by a model.
|
|
@@ -40,7 +40,7 @@ Verify them silently: a green base and a reachable tracker are the expected case
|
|
|
40
40
|
|
|
41
41
|
1. `.launchrail.yml` exists with `issueTracker` not `none`, and the tracker is reachable **from this environment**: check whether the CLI the project docs assume (e.g. `gh`) is installed here; if not, identify the substitute (e.g. GitHub MCP tools) and name it in every dispatch.
|
|
42
42
|
2. Open tickets labeled `ready-for-agent` exist and carry explicit `Blocked by: #n` edges (or the tracker's native blocking relations). No tickets with edges → nothing to orchestrate; point the user at `launch-tickets`. If anything wearing `ready-for-agent` is plainly not an implementable ticket — a published spec, research notes, an epic — stop and have it relabeled (e.g. `spec`) before starting: the frontier is computed from the label alone and cannot tell prose from work.
|
|
43
|
-
3. The integration target is resolved (a consolidation branch by default, or trunk when the user opts in — see Policies) and its branch is green **in this checkout**: the tree is clean, the base is synced (
|
|
43
|
+
3. The integration target is resolved (a consolidation branch by default, or trunk when the user opts in — see Policies) and its branch is green **in this checkout**: the tree is clean, the base is synced and confirmed on origin against the live remote (`git ls-remote` after a pruning fetch — never a stale `origin/<base>` tracking ref, which makes a local-only base look present and then fails every land at `git fetch origin <base>`): a named consolidation branch the remote lacks is created from the default branch's tip, a session-pinned base that exists only locally is pushed, and a local base that has diverged from the remote is a refusal — push or reset it first; the install command has run, and `npx @wemuda/launchrail verify` exited 0 — report actual exit codes, not the reassuring summary line; a broken base poisons every implementer after it. A missing *default* branch is a refusal, not a cue to guess another. An **empty verification contract fails `verify` and is a refusal condition**: a run whose completion nothing can verify must not start. Tell the user to configure `testing` commands in `.launchrail.yml` first. **On a relaunch** whose base still sits at a sha a previous run verified green, pass that sha as `knownGreen`: preflight syncs and installs but skips re-proving the base.
|
|
44
44
|
4. The verbatim local commands are known (from `AGENTS.md` / `.launchrail.yml`): install, the fast gate, the full gate, and which checks belong to the shared local machine. If `testing.checkCommand` is unset the fast gate is the unit command — fine, but a project that names a quicker lint/typecheck/unit gate there makes every land cheaper.
|
|
45
45
|
|
|
46
46
|
## The loop
|
package/dist/commands/add.js
CHANGED
|
@@ -65,12 +65,12 @@ function planBrowserTesting(parsed, detection, answers) {
|
|
|
65
65
|
notes.push(`Existing ${detection.playwrightConfigFile} detected — keeping it; no config or baseline spec seeded.`);
|
|
66
66
|
}
|
|
67
67
|
const nextSteps = [
|
|
68
|
-
`Run \`node scripts/setup.mjs\` — installs @playwright/test, ${BROWSER_DRIVER_PACKAGE} (the browser smoke's driver
|
|
68
|
+
`Run \`node scripts/setup.mjs\` — installs @playwright/test, its Chromium, and ${BROWSER_DRIVER_PACKAGE} (the browser smoke's driver, which reuses that Chromium).`,
|
|
69
69
|
];
|
|
70
70
|
if (!answers.devCommand) {
|
|
71
|
-
nextSteps.push("Set the
|
|
71
|
+
nextSteps.push("Set the start command: testing.devCommand in .launchrail.yml — or, for a composed stack (several origins, an in-process backend, fixtures), smoke.start plus smoke.origins; see .claude/skills/launch-browser-smoke/composed-stack.md.");
|
|
72
72
|
}
|
|
73
|
-
nextSteps.push("Review the seeded files — they are yours: playwright config, tests/e2e/baseline.spec.ts, scripts
|
|
73
|
+
nextSteps.push("Prove the start contract: `npx @wemuda/launchrail dev --check` starts the stack, waits for every origin, asserts the state files, and tears down. Do this now, not inside someone's smoke.", "Review the seeded files — they are yours: playwright config, tests/e2e/baseline.spec.ts, scripts/ (dev/verify/doctor delegate to the CLI).", "Verify: `node scripts/verify.mjs` runs the deterministic gate (unit + the e2e baseline).", "Browser smoke: after building user-facing behavior, the launch-browser-smoke skill drives the running app with agent-browser — a one-off check of the change, not a test suite (ADR-0034).");
|
|
74
74
|
return {
|
|
75
75
|
manifest,
|
|
76
76
|
testing: { ...answers },
|
package/dist/commands/add.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add.js","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"add.js","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type OriginStatus } from "../lib/stack.js";
|
|
2
|
+
export interface DevOptions {
|
|
3
|
+
cwd: string;
|
|
4
|
+
/** Start detached, log to .launchrail/state/dev.log, wait for readiness, leave it running. */
|
|
5
|
+
background: boolean;
|
|
6
|
+
/** Re-address the single derived origin and expose PORT to the start command. */
|
|
7
|
+
port: string | null;
|
|
8
|
+
/** Stop the background stack recorded in dev.pid. */
|
|
9
|
+
stop: boolean;
|
|
10
|
+
/** Start, wait for every origin, assert the state files, tear down — the adoption-time proof. */
|
|
11
|
+
check: boolean;
|
|
12
|
+
/** Readiness deadline for --background and --check. */
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface DevOutcome {
|
|
16
|
+
code: number;
|
|
17
|
+
origins: Record<string, string>;
|
|
18
|
+
/** Per-origin readiness after the wait (background/check only). */
|
|
19
|
+
ready: Record<string, OriginStatus>;
|
|
20
|
+
pid: number | null;
|
|
21
|
+
}
|
|
22
|
+
/** Stop the detached stack: the whole process group when the platform has one, the pid otherwise. */
|
|
23
|
+
export declare function stopStack(cwd: string): Promise<{
|
|
24
|
+
code: number;
|
|
25
|
+
pid: number | null;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Start the smokeable stack from the manifest and own its state (ADR-0036):
|
|
29
|
+
* `.launchrail/state/stack.json` (origins, pid, log — merged with whatever a
|
|
30
|
+
* composed start command writes there), `dev.url`/`dev.pid` for shells, and
|
|
31
|
+
* `browser.env` for the smoke's driver. The seeded `scripts/dev.mjs` delegates
|
|
32
|
+
* here, so adapted copies in older projects can no longer drift away from the
|
|
33
|
+
* contract the `launch-browser-smoke` skill relies on.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runDev(opts: DevOptions): Promise<DevOutcome>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { MANIFEST_FILENAME, parseManifest } from "../lib/manifest.js";
|
|
5
|
+
import { BROWSER_ENV_FILE, DEV_LOG_FILE, DEV_PID_FILE, DEV_URL_FILE, ensureStateDir, INSTALL_HINT, mergeStackState, missingDependencies, readStackState, resolveStackStart, STACK_STATE_FILE, waitForOrigins, writeBrowserEnv, } from "../lib/stack.js";
|
|
6
|
+
const NONE = { code: 1, origins: {}, ready: {}, pid: null };
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
8
|
+
function loadManifest(cwd) {
|
|
9
|
+
const path = join(cwd, MANIFEST_FILENAME);
|
|
10
|
+
if (!existsSync(path)) {
|
|
11
|
+
console.error(`launchrail: ${MANIFEST_FILENAME} not found — run \`launchrail init\` first.`);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const parsed = parseManifest(readFileSync(path, "utf8"));
|
|
15
|
+
if (!parsed.manifest) {
|
|
16
|
+
console.error(`launchrail: ${MANIFEST_FILENAME} is invalid:`);
|
|
17
|
+
for (const error of parsed.errors)
|
|
18
|
+
console.error(` - ${error}`);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return parsed.manifest;
|
|
22
|
+
}
|
|
23
|
+
function readPid(cwd) {
|
|
24
|
+
const path = join(cwd, DEV_PID_FILE);
|
|
25
|
+
if (!existsSync(path))
|
|
26
|
+
return readStackState(cwd)?.pid ?? null;
|
|
27
|
+
const pid = Number.parseInt(readFileSync(path, "utf8").trim(), 10);
|
|
28
|
+
return Number.isFinite(pid) ? pid : null;
|
|
29
|
+
}
|
|
30
|
+
function alive(pid) {
|
|
31
|
+
try {
|
|
32
|
+
process.kill(pid, 0);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Stop the detached stack: the whole process group when the platform has one, the pid otherwise. */
|
|
40
|
+
export async function stopStack(cwd) {
|
|
41
|
+
const pid = readPid(cwd);
|
|
42
|
+
if (pid === null) {
|
|
43
|
+
console.log("No background stack recorded (no .launchrail/state/dev.pid) — nothing to stop.");
|
|
44
|
+
return { code: 0, pid: null };
|
|
45
|
+
}
|
|
46
|
+
for (const signal of ["SIGTERM", "SIGKILL"]) {
|
|
47
|
+
try {
|
|
48
|
+
process.kill(-pid, signal);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
try {
|
|
52
|
+
process.kill(pid, signal);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Already gone.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (let i = 0; i < 20 && alive(pid); i += 1)
|
|
59
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
60
|
+
if (!alive(pid))
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
rmSync(join(cwd, DEV_PID_FILE), { force: true });
|
|
64
|
+
mergeStackState(cwd, { pid: null });
|
|
65
|
+
console.log(alive(pid) ? `launchrail: pid ${pid} did not exit.` : `Stopped the background stack (pid ${pid}).`);
|
|
66
|
+
return { code: alive(pid) ? 1 : 0, pid };
|
|
67
|
+
}
|
|
68
|
+
function printReadiness(origins, ready) {
|
|
69
|
+
for (const [name, url] of Object.entries(origins)) {
|
|
70
|
+
const status = ready[name];
|
|
71
|
+
console.log(` ${status ? "✓" : "✗"} ${name}: ${url}${status ? ` (${status})` : " — not answering"}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Start the smokeable stack from the manifest and own its state (ADR-0036):
|
|
76
|
+
* `.launchrail/state/stack.json` (origins, pid, log — merged with whatever a
|
|
77
|
+
* composed start command writes there), `dev.url`/`dev.pid` for shells, and
|
|
78
|
+
* `browser.env` for the smoke's driver. The seeded `scripts/dev.mjs` delegates
|
|
79
|
+
* here, so adapted copies in older projects can no longer drift away from the
|
|
80
|
+
* contract the `launch-browser-smoke` skill relies on.
|
|
81
|
+
*/
|
|
82
|
+
export async function runDev(opts) {
|
|
83
|
+
const manifest = loadManifest(opts.cwd);
|
|
84
|
+
if (!manifest)
|
|
85
|
+
return NONE;
|
|
86
|
+
if (opts.stop) {
|
|
87
|
+
const stopped = await stopStack(opts.cwd);
|
|
88
|
+
return { ...NONE, code: stopped.code, pid: stopped.pid };
|
|
89
|
+
}
|
|
90
|
+
if (missingDependencies(opts.cwd)) {
|
|
91
|
+
console.error(`launchrail: dependencies are not installed (no node_modules) — ${INSTALL_HINT}.`);
|
|
92
|
+
return NONE;
|
|
93
|
+
}
|
|
94
|
+
const stack = resolveStackStart(manifest, opts.port);
|
|
95
|
+
if (!stack.command) {
|
|
96
|
+
console.error(`launchrail: no start command — set testing.devCommand in ${MANIFEST_FILENAME}, or smoke.start (with smoke.origins) for a composed stack.`);
|
|
97
|
+
return NONE;
|
|
98
|
+
}
|
|
99
|
+
ensureStateDir(opts.cwd);
|
|
100
|
+
const browser = writeBrowserEnv(opts.cwd);
|
|
101
|
+
const env = opts.port ? { ...process.env, PORT: opts.port } : { ...process.env };
|
|
102
|
+
const primary = Object.values(stack.origins)[0] ?? null;
|
|
103
|
+
const detached = opts.background || opts.check;
|
|
104
|
+
console.log(`Starting the stack via ${stack.source}: ${stack.command}`);
|
|
105
|
+
if (stack.composed && opts.port) {
|
|
106
|
+
console.log(" --port is passed through as PORT; a composed stack's origins are the declared ones.");
|
|
107
|
+
}
|
|
108
|
+
if (!detached) {
|
|
109
|
+
mergeStackState(opts.cwd, { command: stack.command, origins: stack.origins, pid: null, log: null, startedAt: new Date().toISOString() });
|
|
110
|
+
if (primary)
|
|
111
|
+
writeFileSync(join(opts.cwd, DEV_URL_FILE), primary + "\n", "utf8");
|
|
112
|
+
const result = spawnSync(stack.command, { cwd: opts.cwd, shell: true, stdio: "inherit", env });
|
|
113
|
+
return { code: result.status ?? 0, origins: stack.origins, ready: {}, pid: null };
|
|
114
|
+
}
|
|
115
|
+
const log = openSync(join(opts.cwd, DEV_LOG_FILE), "a");
|
|
116
|
+
const child = spawn(stack.command, { cwd: opts.cwd, shell: true, detached: true, stdio: ["ignore", log, log], env });
|
|
117
|
+
child.unref();
|
|
118
|
+
const pid = child.pid ?? null;
|
|
119
|
+
if (pid !== null)
|
|
120
|
+
writeFileSync(join(opts.cwd, DEV_PID_FILE), String(pid) + "\n", "utf8");
|
|
121
|
+
if (primary)
|
|
122
|
+
writeFileSync(join(opts.cwd, DEV_URL_FILE), primary + "\n", "utf8");
|
|
123
|
+
mergeStackState(opts.cwd, {
|
|
124
|
+
command: stack.command,
|
|
125
|
+
origins: stack.origins,
|
|
126
|
+
pid,
|
|
127
|
+
log: DEV_LOG_FILE,
|
|
128
|
+
startedAt: new Date().toISOString(),
|
|
129
|
+
});
|
|
130
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
131
|
+
console.log(` pid ${pid ?? "?"} · log ${DEV_LOG_FILE} · waiting up to ${Math.round(timeoutMs / 1000)}s for ${Object.keys(stack.origins).length} origin(s)`);
|
|
132
|
+
const ready = await waitForOrigins(stack.origins, timeoutMs);
|
|
133
|
+
printReadiness(stack.origins, ready);
|
|
134
|
+
const allReady = Object.values(ready).every((s) => s !== null);
|
|
135
|
+
// A composed start command may have merged its real origins/keys into stack.json meanwhile.
|
|
136
|
+
const state = readStackState(opts.cwd);
|
|
137
|
+
console.log(` state: ${STACK_STATE_FILE}${state && Object.keys(state).some((k) => !["schemaVersion", "command", "origins", "pid", "log", "startedAt"].includes(k)) ? " (the start command added fields)" : ""}` +
|
|
138
|
+
` · ${DEV_URL_FILE} · ${DEV_PID_FILE}`);
|
|
139
|
+
console.log(` browser: ${browser.executablePath ? `${browser.executablePath} (${browser.source})` : "no Playwright Chromium found — agent-browser will use its own (npx agent-browser install)"}` +
|
|
140
|
+
`${browser.args ? ` · args ${browser.args}` : ""} → ${BROWSER_ENV_FILE}`);
|
|
141
|
+
if (opts.check) {
|
|
142
|
+
const filesOk = [STACK_STATE_FILE, DEV_URL_FILE, DEV_PID_FILE, BROWSER_ENV_FILE].every((f) => existsSync(join(opts.cwd, f)));
|
|
143
|
+
await stopStack(opts.cwd);
|
|
144
|
+
if (allReady && filesOk) {
|
|
145
|
+
console.log("\nStack check passed: every origin answered and the state files were written.");
|
|
146
|
+
return { code: 0, origins: stack.origins, ready, pid };
|
|
147
|
+
}
|
|
148
|
+
console.error(`\nStack check failed: ${allReady ? "state files missing" : "not every origin answered before the deadline"} — see ${DEV_LOG_FILE}.`);
|
|
149
|
+
return { code: 1, origins: stack.origins, ready, pid };
|
|
150
|
+
}
|
|
151
|
+
if (!allReady) {
|
|
152
|
+
console.error(`\nlaunchrail: the stack did not answer on every origin within ${Math.round(timeoutMs / 1000)}s — it is still running; read ${DEV_LOG_FILE}, or stop it with \`launchrail dev --stop\`.`);
|
|
153
|
+
return { code: 1, origins: stack.origins, ready, pid };
|
|
154
|
+
}
|
|
155
|
+
console.log(`\nStack ready. Stop it with \`launchrail dev --stop\`.`);
|
|
156
|
+
return { code: 0, origins: stack.origins, ready, pid };
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=dev.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -12,6 +12,7 @@ import { MANIFEST_FILENAME, parseManifest } from "../lib/manifest.js";
|
|
|
12
12
|
import { RALPH_GUARD_HOOK_PATH, RALPH_MODULE, RALPH_WORKFLOW_PATH } from "../lib/ralph.js";
|
|
13
13
|
import { agentsCommandsState, ciTriggerReadiness, fastGateReadiness, e2eReadiness, READINESS_SKILL, sessionStartHookState, } from "../lib/readiness.js";
|
|
14
14
|
import { skillNames } from "../lib/skills.js";
|
|
15
|
+
import { resolveBrowserEnv, resolveStackStart, stateDirUnignored } from "../lib/stack.js";
|
|
15
16
|
export function runDoctor(cwd) {
|
|
16
17
|
const checks = [];
|
|
17
18
|
const add = (status, name, message = "") => {
|
|
@@ -167,14 +168,30 @@ export function runDoctor(cwd) {
|
|
|
167
168
|
else {
|
|
168
169
|
add("fail", "playwright config", "no playwright.config.* found — re-run `launchrail add browser-testing`");
|
|
169
170
|
}
|
|
170
|
-
// The
|
|
171
|
-
//
|
|
171
|
+
// The smokeable stack's start contract (ADR-0036) — file-based here; the
|
|
172
|
+
// run-it-and-see proof is `launchrail dev --check`, never doctor.
|
|
173
|
+
const stack = resolveStackStart(manifest);
|
|
174
|
+
if (stack.command) {
|
|
175
|
+
add("pass", "app start", `${stack.source}: ${stack.command} → ${Object.keys(stack.origins).length} origin(s)${stack.composed ? " (composed stack)" : ""}; prove it with \`launchrail dev --check\``);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
add("warn", "app start", `no start command — set testing.devCommand in ${MANIFEST_FILENAME}, or smoke.start with smoke.origins for a composed stack`);
|
|
179
|
+
}
|
|
180
|
+
// The browser smoke's driver (ADR-0034) installs through scripts/setup.mjs
|
|
181
|
+
// and reuses Playwright's Chromium (ADR-0036); both are warnings because the
|
|
182
|
+
// skill carries a fallback for each.
|
|
183
|
+
const browser = resolveBrowserEnv();
|
|
172
184
|
if (detection.hasBrowserDriverDep) {
|
|
173
|
-
add("pass", "browser driver",
|
|
185
|
+
add(browser.executablePath ? "pass" : "warn", "browser driver", browser.executablePath
|
|
186
|
+
? `${BROWSER_DRIVER_PACKAGE} declared; Chromium: ${browser.executablePath} (${browser.source})`
|
|
187
|
+
: `${BROWSER_DRIVER_PACKAGE} declared; no Playwright Chromium found — it will download its own (\`npx ${BROWSER_DRIVER_PACKAGE} install\`)`);
|
|
174
188
|
}
|
|
175
189
|
else {
|
|
176
190
|
add("warn", "browser driver", `${BROWSER_DRIVER_PACKAGE} not declared — run \`node scripts/setup.mjs\` so the browser smoke has its driver`);
|
|
177
191
|
}
|
|
192
|
+
if (stateDirUnignored(cwd)) {
|
|
193
|
+
add("warn", "state directory", ".launchrail/state/ exists but does not ignore itself — `launchrail dev` writes the ignore; delete stray files or run it once");
|
|
194
|
+
}
|
|
178
195
|
const missingScripts = SEMANTIC_SCRIPTS.filter((name) => !existsSync(join(cwd, "scripts", `${name}.mjs`)));
|
|
179
196
|
if (missingScripts.length === 0) {
|
|
180
197
|
add("pass", "semantic scripts", "scripts/{setup,dev,verify,doctor}.mjs");
|