@valescoagency/runway 0.10.0 → 0.11.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 +189 -40
- package/dist/cli.js +14 -0
- package/dist/commands/dash.js +324 -0
- package/dist/commands/review.js +315 -0
- package/dist/commands/run.js +21 -7
- package/dist/config.js +51 -6
- package/dist/dashboard/events.js +71 -0
- package/dist/dashboard/linear-sync.js +192 -0
- package/dist/dashboard/projector.js +77 -0
- package/dist/dashboard/server.js +468 -20
- package/dist/dashboard/storage.js +417 -16
- package/dist/dashboard/views.js +901 -8
- package/dist/diagnostics/git-signing.js +120 -0
- package/dist/diagnostics/index.js +2 -0
- package/dist/diagnostics/linear-config.js +19 -35
- package/dist/finalize.js +59 -13
- package/dist/git.js +48 -12
- package/dist/hitl.js +20 -28
- package/dist/implement.js +82 -1
- package/dist/linear.js +87 -73
- package/dist/meta/attribution.js +285 -0
- package/dist/meta/context.js +165 -0
- package/dist/meta/dashboard-read.js +609 -0
- package/dist/meta/format.js +49 -0
- package/dist/meta/heuristic-filter.js +53 -0
- package/dist/meta/hindsight.js +279 -0
- package/dist/meta/linear-meta.js +415 -0
- package/dist/meta/llm.js +205 -0
- package/dist/meta/out-of-scope.js +101 -0
- package/dist/meta/passes/drain-review.js +374 -0
- package/dist/meta/passes/run-review.js +475 -0
- package/dist/meta/passes/weekly-review.js +910 -0
- package/dist/meta/promoter.js +225 -0
- package/dist/meta/runner.js +221 -0
- package/dist/meta/span-attrs.js +65 -0
- package/dist/meta/templates.js +655 -0
- package/dist/orchestrator.js +54 -22
- package/dist/policy.js +6 -5
- package/dist/review.js +25 -8
- package/dist/runway-config-file.js +82 -0
- package/dist/scaffolder-varlock.js +9 -0
- package/dist/telemetry.js +38 -14
- package/package.json +6 -3
- package/prompts/implement.md +71 -0
- package/prompts/pr-review.md +127 -0
- package/prompts/review.md +64 -1
- package/templates/.env.schema.target-repo +26 -0
- package/templates/claude-shim.sh +47 -0
- package/templates/dockerfile-varlock.snippet +19 -12
package/README.md
CHANGED
|
@@ -8,13 +8,15 @@ coding-agent runs, then **drain** a Linear queue against it. Wraps
|
|
|
8
8
|
inside Docker), [varlock](https://varlock.dev) + 1Password for
|
|
9
9
|
zero-secrets-at-rest, and the `gh` CLI for PR creation.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Seven commands
|
|
12
12
|
|
|
13
13
|
| | |
|
|
14
14
|
|---|---|
|
|
15
|
+
| `runway dash` | Bring up the operations dashboard (`up` / `logs` / `stop`). Wraps the published `ghcr.io/valescoagency/runway-dashboard` image so any runway-using project can run the dashboard without cloning runway. Ports bind to `127.0.0.1` only. |
|
|
15
16
|
| `runway doctor` | Read-only preflight diagnostic: host tooling, env vars, repo state, and the agent docker image. Use when something stopped working and you want a sanity report. `--json` for CI / scripted health checks. |
|
|
16
17
|
| `runway init` | Scaffold the cwd repo for runway: write `.sandcastle/Dockerfile` + (tier 2) `.env.schema` with op:// references. Run **once per target repo**. |
|
|
17
|
-
| `runway
|
|
18
|
+
| `runway review` | Run an IRA retrospective pass. `runway review run --drain <trace-id> --issue <id>` grades one issue-process post-drain — loads the captured agent + reviewer reports from the dashboard, fetches hindsight (PR merge state, human review-thread comments, Linear follow-ups in the past 48h), pulls rolling norms for the issue's category (last 30 drains, via the VA-399 read-model), asks an Anthropic model for a structured `Run Review` with absolute + relative grading axes, writes it to a `runway-meta` Linear project + the dashboard's `meta_reviews` table. Scheduler-agnostic; usually invoked from cron or GitHub Actions ~18h after a drain. The drain-age delay is enforced by `RUNWAY_REVIEW_DELAY_HOURS` (default 18, constrained to the 12–24h band) — pass `--force` to override for one-off operator bypass. **`runway review drain --id <trace-id>`** (VA-406) grades a whole drain: reads every per-issue Run Review for the drain, asks the model for a structured `Drain Review` covering composition / sequencing / cross-issue patterns, files it in `runway-meta`. When the model marks a finding `severity: critical` (drain-unsafe or captured-data-lost), the IRA also escalates a `Bug` + `runway-meta-promoted` issue into the runway-repo project (set `RUNWAY_REPO_PROJECT_NAME` to scope by project; otherwise team-level). The Drain Review fires automatically in-process the moment every Run Review for a drain has landed — manual invocation is for scheduled / catch-up runs. |
|
|
19
|
+
| `runway run` | Drain a Linear queue. For each issue carrying the `ready-for-agent` label: branch, agent works, sub-agent reviews, PR opens (or `ready-for-human` label). Run **whenever you want a batch of work done**. |
|
|
18
20
|
| `runway upgrade` | Update the runway CLI itself: `git pull` the local clone, `pnpm install`, typecheck. `--check` for a dry-run, `--force` to override dirty/branch refusals. |
|
|
19
21
|
| `runway upgrade-repo` | Re-render the cwd repo's runway scaffold against the current vendored templates. Use after a runway version bump that changed the Dockerfile or template shape — `init` writes them, `upgrade-repo` keeps them current without re-prompting for op:// values. |
|
|
20
22
|
|
|
@@ -39,10 +41,12 @@ or use `--check` for a CI dry-run that exits 1 on drift.
|
|
|
39
41
|
## Architecture
|
|
40
42
|
|
|
41
43
|
```
|
|
42
|
-
Linear (
|
|
44
|
+
Linear (label=ready-for-agent, team=VA)
|
|
43
45
|
↓ poll
|
|
44
46
|
runway (this CLI, on your Mac, run from inside the target repo)
|
|
45
47
|
↓ for each issue
|
|
48
|
+
│ removeLabel(ready-for-agent) # claim signal — VA-423; skip
|
|
49
|
+
│ # if already absent (lost race)
|
|
46
50
|
│ sandcastle.run({ agent: claudeCode, sandbox: docker, cwd: process.cwd(), ... })
|
|
47
51
|
│ iter 1 → IMPL: DONE | IMPL: BLOCKED — <reason> | IMPL: CONTINUE
|
|
48
52
|
│ iter 2 → same, with previous iteration's summary injected
|
|
@@ -54,7 +58,9 @@ runway (this CLI, on your Mac, run from inside the target repo)
|
|
|
54
58
|
│ sandcastle.run({ ..., prompt: review template })
|
|
55
59
|
│ → REVIEW: APPROVED | REVIEW: REJECTED — <reason>
|
|
56
60
|
│
|
|
57
|
-
├── approved → git push → gh pr create
|
|
61
|
+
├── approved → git push → gh pr create (Linear's GitHub integration
|
|
62
|
+
│ auto-transitions: PR open
|
|
63
|
+
│ → In Progress, merge → Done)
|
|
58
64
|
└── rejected → Linear comment with reason, then `ready-for-human` label
|
|
59
65
|
↓ next issue
|
|
60
66
|
|
|
@@ -153,6 +159,48 @@ varlock run --schema /path/to/runway/.env.schema -- runway --max 3
|
|
|
153
159
|
Without varlock, runway falls back to plain `process.env` and
|
|
154
160
|
sandcastle reads `.sandcastle/.env` per its docs.
|
|
155
161
|
|
|
162
|
+
## Signed agent commits (optional)
|
|
163
|
+
|
|
164
|
+
Runway can sign every commit the agent produces so the PR lands on
|
|
165
|
+
GitHub with a **Verified** badge. Off by default; opt in per target
|
|
166
|
+
repo by populating four extra refs in `.env.schema`.
|
|
167
|
+
|
|
168
|
+
Setup (one-time per bot):
|
|
169
|
+
|
|
170
|
+
1. Generate an Ed25519 SSH signing keypair for the runway agent:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
ssh-keygen -t ed25519 -C "runway-agent" -N "" -f ~/.ssh/runway_bot
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
2. Store the keypair plus the bot's git identity in 1Password under
|
|
177
|
+
`op://<vault>/runway-signing-ssh`:
|
|
178
|
+
|
|
179
|
+
| field | value |
|
|
180
|
+
|-----------|--------------------------------------------|
|
|
181
|
+
| `private` | contents of `~/.ssh/runway_bot` |
|
|
182
|
+
| `public` | contents of `~/.ssh/runway_bot.pub` |
|
|
183
|
+
| `name` | the bot's git `user.name` (e.g. `Runway Agent`) |
|
|
184
|
+
| `email` | the bot's git `user.email` (must match the GitHub agent account) |
|
|
185
|
+
|
|
186
|
+
3. Register the public key as a **Signing Key** on the GitHub
|
|
187
|
+
account that owns the agent's `GH_TOKEN` (this is what makes
|
|
188
|
+
GitHub render Verified — both Authentication and Signing Keys
|
|
189
|
+
are managed under Settings → SSH and GPG keys).
|
|
190
|
+
|
|
191
|
+
4. Uncomment the four `RUNWAY_SIGNING_*` lines at the bottom of
|
|
192
|
+
`.env.schema` (the file `runway init --tier=2` wrote at your repo
|
|
193
|
+
root). The lines come pre-filled with `op://` references
|
|
194
|
+
matching the layout above.
|
|
195
|
+
|
|
196
|
+
Validate with `runway doctor`: the **Environment / agent commit
|
|
197
|
+
signing** check goes from `warn (off)` to `ok` once all four refs
|
|
198
|
+
are declared. Tear down by re-commenting the four lines — runway
|
|
199
|
+
falls back to plain unsigned commits.
|
|
200
|
+
|
|
201
|
+
Design rationale + future migration path (GitHub App identity) is in
|
|
202
|
+
[`docs/adr/0003-agent-commit-signing.md`](docs/adr/0003-agent-commit-signing.md).
|
|
203
|
+
|
|
156
204
|
## Install
|
|
157
205
|
|
|
158
206
|
```bash
|
|
@@ -167,9 +215,7 @@ export LINEAR_API_KEY=lin_api_...
|
|
|
167
215
|
# export RUNWAY_LINEAR_TEAM=VA
|
|
168
216
|
# export RUNWAY_LINEAR_PROJECT=<project-id-or-slug> # optional, scopes queue to one project
|
|
169
217
|
# export RUNWAY_BASE_BRANCH=master # optional, overrides auto-detected default branch
|
|
170
|
-
# export
|
|
171
|
-
# export RUNWAY_IN_PROGRESS_STATUS="In Progress"
|
|
172
|
-
# export RUNWAY_IN_REVIEW_STATUS="In Review"
|
|
218
|
+
# export RUNWAY_READY_LABEL="ready-for-agent"
|
|
173
219
|
# export RUNWAY_HITL_LABEL="ready-for-human"
|
|
174
220
|
# export RUNWAY_MAX_ITERATIONS=5
|
|
175
221
|
# export RUNWAY_COMMENT_AUTHOR_ALLOWLIST="Reviewer Bot,Jane Reviewer"
|
|
@@ -182,14 +228,33 @@ export LINEAR_API_KEY=lin_api_...
|
|
|
182
228
|
# Linear identities.
|
|
183
229
|
```
|
|
184
230
|
|
|
185
|
-
|
|
186
|
-
[Flightplan](https://github.com/valescoagency/flightplan)
|
|
187
|
-
state-label
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
231
|
+
Runway uses two labels from the
|
|
232
|
+
[Flightplan](https://github.com/valescoagency/flightplan) v1.1.0
|
|
233
|
+
state-label contract:
|
|
234
|
+
|
|
235
|
+
- `RUNWAY_READY_LABEL` (default `ready-for-agent`) marks an issue as
|
|
236
|
+
"ready for the agent to pick up." Runway's drain queue filters by
|
|
237
|
+
this label, **not** by workflow status — Linear's GitHub integration
|
|
238
|
+
auto-mutates status when a PR cross-references an issue, which would
|
|
239
|
+
drain a status-gated queue silently every time someone mentioned the
|
|
240
|
+
issue from a PR. Labels are immune to that integration. Runway
|
|
241
|
+
removes the label on pickup as the claim signal: the gateway returns
|
|
242
|
+
whether the label was actually present at write time, and a runner
|
|
243
|
+
that loses the claim race (label was already absent) skips the issue
|
|
244
|
+
cleanly without posting a pickup comment or pushing an agent branch.
|
|
245
|
+
Linear's API has no label-level compare-and-swap, so two runners
|
|
246
|
+
that both read the labels before either has written can still both
|
|
247
|
+
proceed — the design assumes the predominant operator pattern of a
|
|
248
|
+
single drain instance running sequentially.
|
|
249
|
+
- `RUNWAY_HITL_LABEL` (default `ready-for-human`) is applied when the
|
|
250
|
+
agent or reviewer can't finish, AND when a run fails outright.
|
|
251
|
+
Runway never re-applies the ready label on failure — terminal
|
|
252
|
+
failures shouldn't retry indefinitely. The operator triages and
|
|
253
|
+
re-applies `ready-for-agent` manually if the failure was transient.
|
|
254
|
+
|
|
255
|
+
`runway doctor` validates that the configured team and both labels
|
|
256
|
+
exist before any agent run — misconfiguration surfaces immediately
|
|
257
|
+
instead of mid-drain.
|
|
193
258
|
|
|
194
259
|
### From source (development)
|
|
195
260
|
|
|
@@ -247,18 +312,17 @@ runway --help
|
|
|
247
312
|
`runway` (no subcommand) is an alias for `runway run` for back-compat.
|
|
248
313
|
|
|
249
314
|
`--max N` bounds **attempts**, not successes. Every issue picked up
|
|
250
|
-
counts as one attempt, whether it ends in a PR, a `
|
|
251
|
-
or
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
it.
|
|
315
|
+
counts as one attempt, whether it ends in a PR, a `ready-for-human`
|
|
316
|
+
label, or an infrastructure-error flag. An issue picked up in this
|
|
317
|
+
invocation will not be re-picked in the same invocation — the
|
|
318
|
+
pickup-time `removeLabel(ready-for-agent)` plus a same-invocation
|
|
319
|
+
seen-set guard keep the drain from looping on it.
|
|
255
320
|
|
|
256
321
|
The CLI exits with 0 even if some issues hit HITL or errored — those
|
|
257
322
|
are normal outcomes. Every run prints a per-issue verdict trail on
|
|
258
323
|
exit (`APPROVED → PR opened <url>` / `HITL <reason>` /
|
|
259
|
-
`
|
|
260
|
-
|
|
261
|
-
issue as a Linear comment.
|
|
324
|
+
`INFRA_ERROR <reason>`) so you can scan results without opening Linear;
|
|
325
|
+
the same content also lives on the issue as a Linear comment.
|
|
262
326
|
|
|
263
327
|
## Linear conventions
|
|
264
328
|
|
|
@@ -267,20 +331,36 @@ Runway picks up issues that are:
|
|
|
267
331
|
- in team `RUNWAY_LINEAR_TEAM` (default `VA`)
|
|
268
332
|
- (optionally) in project `RUNWAY_LINEAR_PROJECT` (override per-run
|
|
269
333
|
with `runway run --project=<id-or-slug-or-name>`; unset = team-wide)
|
|
270
|
-
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
334
|
+
- carrying label `RUNWAY_READY_LABEL` (default `ready-for-agent`,
|
|
335
|
+
the flightplan v1.1.0 contract — see VA-423)
|
|
336
|
+
- not carrying `RUNWAY_HITL_LABEL` (default `ready-for-human`)
|
|
337
|
+
- not blocked by a non-terminal `blocks` relation
|
|
338
|
+
- **without any child sub-issues** — parent PRDs and umbrella tickets
|
|
339
|
+
are skipped. The right unit of agent work is the leaf ticket, not
|
|
340
|
+
the parent that spans it. If your ticket's scope is bigger than one
|
|
341
|
+
PR, file children for the individual deliverables and let runway
|
|
342
|
+
pick those up instead.
|
|
343
|
+
|
|
344
|
+
Workflow status (`Triage` / `Todo` / `In Progress` / etc.) is **not**
|
|
345
|
+
part of the queue contract: Linear's GitHub integration auto-mutates
|
|
346
|
+
status whenever a PR cross-references the issue, which would drain a
|
|
347
|
+
status-gated queue silently every time someone mentioned the issue
|
|
348
|
+
from a PR.
|
|
349
|
+
|
|
350
|
+
What runway does on each pickup:
|
|
351
|
+
|
|
352
|
+
- removes the `ready-for-agent` label (claim signal — takes the
|
|
353
|
+
issue out of the next drain's queue; a runner that finds the
|
|
354
|
+
label already gone skips the issue without doing visible work)
|
|
355
|
+
- comments and works the issue on `agent/<id>`
|
|
356
|
+
- on approve, pushes the branch, opens the PR; Linear's GitHub
|
|
357
|
+
integration then auto-transitions the issue (`In Progress` on PR
|
|
358
|
+
open, `Done` on merge via the `Closes <issue>` line in the PR body)
|
|
359
|
+
- on reject / HITL / startup failure / mid-run crash, applies
|
|
360
|
+
`ready-for-human`. Runway never re-applies `ready-for-agent` on
|
|
361
|
+
failure — terminal failures shouldn't retry indefinitely; the
|
|
362
|
+
operator triages and re-applies the label manually if the cause
|
|
363
|
+
was transient.
|
|
284
364
|
|
|
285
365
|
## Write-path policy
|
|
286
366
|
|
|
@@ -329,8 +409,7 @@ can see what an agent run can and can't touch (e.g. `impl policy:
|
|
|
329
409
|
Runway auto-detects the repo's default branch at the start of every
|
|
330
410
|
`runway run` by reading `origin/HEAD` (with `git remote show origin`
|
|
331
411
|
as a fallback for fresh clones). That branch is used for diffing the
|
|
332
|
-
agent's work
|
|
333
|
-
failure should revert to `Todo`, and as the `--base` for the PR.
|
|
412
|
+
agent's work and as the `--base` for the PR.
|
|
334
413
|
|
|
335
414
|
Set `RUNWAY_BASE_BRANCH=<name>` to override detection — useful when
|
|
336
415
|
you want runway to target a release branch instead of the default, or
|
|
@@ -380,6 +459,76 @@ the issue gets the HITL label and a comment with the reviewer's reason.
|
|
|
380
459
|
The reviewer is intentionally adversarial — its job is to find reasons
|
|
381
460
|
NOT to ship, not to rubber-stamp.
|
|
382
461
|
|
|
462
|
+
## Dashboard
|
|
463
|
+
|
|
464
|
+
`runway run` emits OpenTelemetry traces + logs for every drain. The
|
|
465
|
+
operations dashboard projects those into a local SQLite db and serves
|
|
466
|
+
a single-page web UI for browsing recent runs, drilling into per-issue
|
|
467
|
+
timelines, and filtering by outcome / drain / date range. Binds to
|
|
468
|
+
`127.0.0.1` only — no auth, no LAN exposure by default.
|
|
469
|
+
|
|
470
|
+
### v2: `runway dash` (recommended)
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
# Bring up the dashboard from any directory — no runway clone required.
|
|
474
|
+
runway dash up
|
|
475
|
+
|
|
476
|
+
# In another shell, point runway run at it:
|
|
477
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
|
478
|
+
runway run
|
|
479
|
+
|
|
480
|
+
# Open http://localhost:3001/ in a browser.
|
|
481
|
+
|
|
482
|
+
# Tail the container logs.
|
|
483
|
+
runway dash logs --follow
|
|
484
|
+
|
|
485
|
+
# Tear down (volume + history preserved).
|
|
486
|
+
runway dash stop
|
|
487
|
+
|
|
488
|
+
# Tear down AND drop history.
|
|
489
|
+
runway dash stop --purge
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
`runway dash up` pulls `ghcr.io/valescoagency/runway-dashboard:latest`,
|
|
493
|
+
creates a named volume (`runway-dashboard-data`) for the SQLite db,
|
|
494
|
+
and runs a detached container. Repeated `runway dash up` calls are
|
|
495
|
+
idempotent: an already-running container stays as-is, a stopped one
|
|
496
|
+
is started, otherwise a fresh container is created.
|
|
497
|
+
|
|
498
|
+
Override the image with `--image=…` or `RUNWAY_DASHBOARD_IMAGE`; the
|
|
499
|
+
ports with `--otlp-port=…` / `--dashboard-port=…`. Forward Linear
|
|
500
|
+
sync by exporting `LINEAR_API_KEY` (and optionally
|
|
501
|
+
`LINEAR_POLL_INTERVAL_SECONDS`, `RUNWAY_LINEAR_TEAM`,
|
|
502
|
+
`RUNWAY_READY_LABEL`) before `runway dash up` — the CLI passes them
|
|
503
|
+
through to the container without echoing the value through argv.
|
|
504
|
+
|
|
505
|
+
### v1: docker-compose (for hacking on the dashboard itself)
|
|
506
|
+
|
|
507
|
+
The runway repo also ships a `docker-compose.yml` that builds the
|
|
508
|
+
dashboard locally from `Dockerfile.dashboard`. Use this when you're
|
|
509
|
+
developing the dashboard code and want to iterate without publishing
|
|
510
|
+
an image:
|
|
511
|
+
|
|
512
|
+
```bash
|
|
513
|
+
git clone https://github.com/ValescoAgency/runway && cd runway
|
|
514
|
+
docker compose up # builds + runs the local image
|
|
515
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Migrating from compose to `runway dash`
|
|
519
|
+
|
|
520
|
+
1. `docker compose down` (in the runway repo).
|
|
521
|
+
2. `runway dash up` (anywhere).
|
|
522
|
+
|
|
523
|
+
The default named volume is different (`runway-dashboard-data` vs
|
|
524
|
+
compose's `runway-data`), so history doesn't carry over automatically.
|
|
525
|
+
To preserve it, copy the volume contents before switching:
|
|
526
|
+
|
|
527
|
+
```bash
|
|
528
|
+
docker run --rm -v runway-data:/from -v runway-dashboard-data:/to \
|
|
529
|
+
alpine sh -c 'cp -a /from/. /to/'
|
|
530
|
+
```
|
|
531
|
+
|
|
383
532
|
## What's deliberately missing in v1
|
|
384
533
|
|
|
385
534
|
- Parallel runs (one issue at a time)
|
|
@@ -392,7 +541,7 @@ These are tractable, just not v1.
|
|
|
392
541
|
|
|
393
542
|
## Status
|
|
394
543
|
|
|
395
|
-
0.
|
|
544
|
+
0.11.0 — production-shaped and dogfooded against live Linear queues.
|
|
396
545
|
The end-to-end pipeline (init → run → review → PR) is stable; surface
|
|
397
546
|
may still shift as the orchestrator's policy and iteration mechanics
|
|
398
547
|
mature. See [CHANGELOG.md](./CHANGELOG.md) for per-release detail.
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { dashCommand, printDashUsage } from "./commands/dash.js";
|
|
2
3
|
import { doctorCommand, printDoctorUsage } from "./commands/doctor.js";
|
|
3
4
|
import { initCommand, printInitUsage } from "./commands/init.js";
|
|
5
|
+
import { reviewCommand, printReviewUsage } from "./commands/review.js";
|
|
4
6
|
import { runCommand, printRunUsage } from "./commands/run.js";
|
|
5
7
|
import { upgradeCommand, printUpgradeUsage } from "./commands/upgrade.js";
|
|
6
8
|
import { upgradeRepoCommand, printUpgradeRepoUsage, } from "./commands/upgrade-repo.js";
|
|
7
9
|
const SUBCOMMANDS = [
|
|
10
|
+
{
|
|
11
|
+
name: "dash",
|
|
12
|
+
summary: "Operate the runway operations dashboard (up / logs / stop).",
|
|
13
|
+
run: dashCommand,
|
|
14
|
+
help: printDashUsage,
|
|
15
|
+
},
|
|
8
16
|
{
|
|
9
17
|
name: "doctor",
|
|
10
18
|
summary: "Read-only preflight: tooling, env, repo state, agent image.",
|
|
@@ -17,6 +25,12 @@ const SUBCOMMANDS = [
|
|
|
17
25
|
run: initCommand,
|
|
18
26
|
help: printInitUsage,
|
|
19
27
|
},
|
|
28
|
+
{
|
|
29
|
+
name: "review",
|
|
30
|
+
summary: "Run an IRA retrospective pass (run / drain / weekly).",
|
|
31
|
+
run: reviewCommand,
|
|
32
|
+
help: printReviewUsage,
|
|
33
|
+
},
|
|
20
34
|
{
|
|
21
35
|
name: "run",
|
|
22
36
|
summary: "Drain a Linear queue against the cwd repo (default verb).",
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
/**
|
|
3
|
+
* VA-393 (dashboard slice 8): `runway dash` subcommand. Wraps
|
|
4
|
+
* `docker run` so users can operate the dashboard from any
|
|
5
|
+
* runway-using project without cloning the runway repo or maintaining
|
|
6
|
+
* a docker-compose file.
|
|
7
|
+
*
|
|
8
|
+
* Three verbs:
|
|
9
|
+
* up pull the published image and run it as a detached
|
|
10
|
+
* container (`runway-dashboard`) with loopback ports and a
|
|
11
|
+
* named volume for the SQLite db.
|
|
12
|
+
* logs stream `docker logs` for the container (`--follow` toggles
|
|
13
|
+
* `-f`; default tails recent output without following).
|
|
14
|
+
* stop stop and `rm` the container. The named volume stays so
|
|
15
|
+
* history survives across restarts; explicit `--purge` drops
|
|
16
|
+
* the volume too.
|
|
17
|
+
*
|
|
18
|
+
* Defaults to the `:latest` tag published by `.github/workflows/dashboard-image.yml`.
|
|
19
|
+
* Override via `--image` or the `RUNWAY_DASHBOARD_IMAGE` env var.
|
|
20
|
+
*
|
|
21
|
+
* Verb-agnostic env passthrough: when set in the caller's environment,
|
|
22
|
+
* `LINEAR_API_KEY`, `LINEAR_POLL_INTERVAL_SECONDS`, `RUNWAY_LINEAR_TEAM`,
|
|
23
|
+
* `RUNWAY_READY_LABEL` are forwarded into the container via `-e`.
|
|
24
|
+
* Absent → the container falls back to the same defaults as
|
|
25
|
+
* `docker-compose.yml` (Linear sync disabled, etc.).
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_IMAGE = "ghcr.io/valescoagency/runway-dashboard:latest";
|
|
28
|
+
const DEFAULT_CONTAINER_NAME = "runway-dashboard";
|
|
29
|
+
const DEFAULT_VOLUME_NAME = "runway-dashboard-data";
|
|
30
|
+
const DEFAULT_OTLP_PORT = "4318";
|
|
31
|
+
const DEFAULT_DASHBOARD_PORT = "3001";
|
|
32
|
+
/**
|
|
33
|
+
* Linear sync envs we forward when present. Kept narrow on purpose —
|
|
34
|
+
* the dashboard reads its own env at boot and we don't want a stray
|
|
35
|
+
* shell variable leaking into the container.
|
|
36
|
+
*/
|
|
37
|
+
const FORWARDED_ENV_KEYS = [
|
|
38
|
+
"LINEAR_API_KEY",
|
|
39
|
+
"LINEAR_POLL_INTERVAL_SECONDS",
|
|
40
|
+
"RUNWAY_LINEAR_TEAM",
|
|
41
|
+
"RUNWAY_READY_LABEL",
|
|
42
|
+
];
|
|
43
|
+
export function printDashUsage() {
|
|
44
|
+
console.log(`runway dash — operate the runway operations dashboard
|
|
45
|
+
|
|
46
|
+
Wraps \`docker run\` so the dashboard works from any runway-using
|
|
47
|
+
project without cloning the runway repo. The published image is
|
|
48
|
+
${DEFAULT_IMAGE}; override via --image or RUNWAY_DASHBOARD_IMAGE.
|
|
49
|
+
|
|
50
|
+
USAGE
|
|
51
|
+
runway dash up [--image=…] [--otlp-port=N] [--dashboard-port=N]
|
|
52
|
+
runway dash logs [--follow]
|
|
53
|
+
runway dash stop [--purge]
|
|
54
|
+
|
|
55
|
+
VERBS
|
|
56
|
+
up Pull and start the dashboard as a detached container
|
|
57
|
+
(\`${DEFAULT_CONTAINER_NAME}\`). Ports publish to 127.0.0.1
|
|
58
|
+
only; a named volume (\`${DEFAULT_VOLUME_NAME}\`) persists
|
|
59
|
+
the SQLite db across runs.
|
|
60
|
+
logs Stream container logs (\`docker logs ${DEFAULT_CONTAINER_NAME}\`).
|
|
61
|
+
Pass --follow to tail with -f.
|
|
62
|
+
stop Stop and remove the container. The named volume stays unless
|
|
63
|
+
--purge is passed.
|
|
64
|
+
|
|
65
|
+
OPTIONS
|
|
66
|
+
--image=REF Override the dashboard image reference.
|
|
67
|
+
Default: ${DEFAULT_IMAGE} (env: RUNWAY_DASHBOARD_IMAGE).
|
|
68
|
+
--otlp-port=N Host port for the OTLP receiver. Default: ${DEFAULT_OTLP_PORT}.
|
|
69
|
+
--dashboard-port=N Host port for the dashboard UI. Default: ${DEFAULT_DASHBOARD_PORT}.
|
|
70
|
+
--follow, -f (logs) Tail container logs with -f.
|
|
71
|
+
--purge (stop) Also remove the named volume (deletes history).
|
|
72
|
+
--help, -h Show this help.
|
|
73
|
+
|
|
74
|
+
LINEAR SYNC (optional)
|
|
75
|
+
When LINEAR_API_KEY is set in the caller's environment, \`runway dash
|
|
76
|
+
up\` forwards it into the container so the dashboard polls Linear and
|
|
77
|
+
surfaces the Todo queue. LINEAR_POLL_INTERVAL_SECONDS,
|
|
78
|
+
RUNWAY_LINEAR_TEAM, and RUNWAY_READY_LABEL are forwarded too when
|
|
79
|
+
set. Absent → the dashboard runs without Linear surfaces.
|
|
80
|
+
|
|
81
|
+
OTEL EXPORTER
|
|
82
|
+
Point \`runway run\` at the dashboard by exporting:
|
|
83
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:${DEFAULT_OTLP_PORT}
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
export function parseDashArgs(argv) {
|
|
87
|
+
if (argv.length === 0) {
|
|
88
|
+
throw new Error("missing verb — expected one of: up, logs, stop. Run `runway dash --help`.");
|
|
89
|
+
}
|
|
90
|
+
const [verbRaw, ...rest] = argv;
|
|
91
|
+
if (verbRaw === "--help" || verbRaw === "-h") {
|
|
92
|
+
printDashUsage();
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
if (verbRaw !== "up" && verbRaw !== "logs" && verbRaw !== "stop") {
|
|
96
|
+
throw new Error(`unknown verb "${verbRaw}" — expected one of: up, logs, stop.`);
|
|
97
|
+
}
|
|
98
|
+
const verb = verbRaw;
|
|
99
|
+
let image;
|
|
100
|
+
let otlpPort;
|
|
101
|
+
let dashboardPort;
|
|
102
|
+
let follow = false;
|
|
103
|
+
let purge = false;
|
|
104
|
+
for (const arg of rest) {
|
|
105
|
+
if (arg === "--help" || arg === "-h") {
|
|
106
|
+
printDashUsage();
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
else if (arg.startsWith("--image=")) {
|
|
110
|
+
image = arg.slice("--image=".length);
|
|
111
|
+
}
|
|
112
|
+
else if (arg.startsWith("--otlp-port=")) {
|
|
113
|
+
otlpPort = arg.slice("--otlp-port=".length);
|
|
114
|
+
}
|
|
115
|
+
else if (arg.startsWith("--dashboard-port=")) {
|
|
116
|
+
dashboardPort = arg.slice("--dashboard-port=".length);
|
|
117
|
+
}
|
|
118
|
+
else if (arg === "--follow" || arg === "-f") {
|
|
119
|
+
follow = true;
|
|
120
|
+
}
|
|
121
|
+
else if (arg === "--purge") {
|
|
122
|
+
purge = true;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { verb, image, otlpPort, dashboardPort, follow, purge };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Resolve effective options by layering CLI flags over env vars over
|
|
132
|
+
* built-in defaults. The resolved shape is pure data so tests can
|
|
133
|
+
* assert on the command we'd hand to docker without invoking it.
|
|
134
|
+
*/
|
|
135
|
+
export function resolveDashOptions(parsed, env = process.env) {
|
|
136
|
+
const image = parsed.image ?? env.RUNWAY_DASHBOARD_IMAGE ?? DEFAULT_IMAGE;
|
|
137
|
+
const otlpPort = parsed.otlpPort ?? env.OTLP_PORT ?? DEFAULT_OTLP_PORT;
|
|
138
|
+
const dashboardPort = parsed.dashboardPort ?? env.DASHBOARD_PORT ?? DEFAULT_DASHBOARD_PORT;
|
|
139
|
+
return {
|
|
140
|
+
verb: parsed.verb,
|
|
141
|
+
image,
|
|
142
|
+
containerName: DEFAULT_CONTAINER_NAME,
|
|
143
|
+
volumeName: DEFAULT_VOLUME_NAME,
|
|
144
|
+
otlpPort,
|
|
145
|
+
dashboardPort,
|
|
146
|
+
follow: parsed.follow,
|
|
147
|
+
purge: parsed.purge,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Build the `docker run` argv for `runway dash up`. Extracted so
|
|
152
|
+
* tests can assert the bindings (loopback prefixes, named volume,
|
|
153
|
+
* forwarded env vars) without spawning docker. Caller decides how to
|
|
154
|
+
* execute the args.
|
|
155
|
+
*/
|
|
156
|
+
export function buildDockerRunArgs(opts, env = process.env) {
|
|
157
|
+
const args = [
|
|
158
|
+
"run",
|
|
159
|
+
"--detach",
|
|
160
|
+
"--name",
|
|
161
|
+
opts.containerName,
|
|
162
|
+
"--restart",
|
|
163
|
+
"unless-stopped",
|
|
164
|
+
// VA-393: host-side bindings stay on 127.0.0.1 by default so the
|
|
165
|
+
// dashboard is unreachable from the LAN. Container-side stays
|
|
166
|
+
// 0.0.0.0 because the Dockerfile sets DASHBOARD_HOST=0.0.0.0 so
|
|
167
|
+
// Docker's port-forward can reach the listener.
|
|
168
|
+
"-p",
|
|
169
|
+
`127.0.0.1:${opts.dashboardPort}:${opts.dashboardPort}`,
|
|
170
|
+
"-p",
|
|
171
|
+
`127.0.0.1:${opts.otlpPort}:${opts.otlpPort}`,
|
|
172
|
+
"-v",
|
|
173
|
+
`${opts.volumeName}:/data`,
|
|
174
|
+
"-e",
|
|
175
|
+
`DASHBOARD_PORT=${opts.dashboardPort}`,
|
|
176
|
+
"-e",
|
|
177
|
+
`OTLP_PORT=${opts.otlpPort}`,
|
|
178
|
+
];
|
|
179
|
+
// VA-393: forward Linear-sync env keys when set in the caller's
|
|
180
|
+
// environment. We pass each key with no value so docker reads the
|
|
181
|
+
// current process env — that way the key never echoes through the
|
|
182
|
+
// shell history.
|
|
183
|
+
for (const key of FORWARDED_ENV_KEYS) {
|
|
184
|
+
if (env[key] !== undefined && env[key] !== "") {
|
|
185
|
+
args.push("-e", key);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
args.push(opts.image);
|
|
189
|
+
return args;
|
|
190
|
+
}
|
|
191
|
+
export async function dashCommand(argv) {
|
|
192
|
+
const parsed = parseDashArgs(argv);
|
|
193
|
+
const opts = resolveDashOptions(parsed);
|
|
194
|
+
await ensureDockerAvailable();
|
|
195
|
+
switch (opts.verb) {
|
|
196
|
+
case "up":
|
|
197
|
+
await dashUp(opts);
|
|
198
|
+
return;
|
|
199
|
+
case "logs":
|
|
200
|
+
await dashLogs(opts);
|
|
201
|
+
return;
|
|
202
|
+
case "stop":
|
|
203
|
+
await dashStop(opts);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Surface a clear error when docker isn't on PATH or the daemon isn't
|
|
209
|
+
* reachable, instead of letting an opaque ENOENT bubble. `docker info`
|
|
210
|
+
* is the canonical "is the daemon up?" probe.
|
|
211
|
+
*/
|
|
212
|
+
async function ensureDockerAvailable() {
|
|
213
|
+
try {
|
|
214
|
+
await execa("docker", ["info"], { stdio: "ignore" });
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
const e = err;
|
|
218
|
+
if (e.code === "ENOENT") {
|
|
219
|
+
throw new Error("docker not found on PATH. Install Docker Desktop (or Podman with a `docker` shim) and retry.");
|
|
220
|
+
}
|
|
221
|
+
throw new Error("docker daemon not reachable (`docker info` failed). Start Docker and retry.");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function dashUp(opts) {
|
|
225
|
+
// If a container with this name already exists (running or stopped),
|
|
226
|
+
// print a helpful hint instead of letting docker error with a
|
|
227
|
+
// "container already in use" diagnostic. Up-on-running is a no-op.
|
|
228
|
+
const existing = await containerState(opts.containerName);
|
|
229
|
+
if (existing === "running") {
|
|
230
|
+
console.log(`[runway dash] container ${opts.containerName} already running.`);
|
|
231
|
+
await printAccessHints(opts);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (existing === "stopped") {
|
|
235
|
+
console.log(`[runway dash] container ${opts.containerName} exists but is stopped — starting it.`);
|
|
236
|
+
await execa("docker", ["start", opts.containerName], {
|
|
237
|
+
stdio: "inherit",
|
|
238
|
+
});
|
|
239
|
+
await printAccessHints(opts);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
console.log(`[runway dash] pulling ${opts.image}`);
|
|
243
|
+
try {
|
|
244
|
+
await execa("docker", ["pull", opts.image], { stdio: "inherit" });
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
throw new Error(`failed to pull ${opts.image}. ` +
|
|
248
|
+
"If the image is private, run `docker login ghcr.io` first.");
|
|
249
|
+
}
|
|
250
|
+
const args = buildDockerRunArgs(opts);
|
|
251
|
+
console.log(`[runway dash] starting container ${opts.containerName}`);
|
|
252
|
+
await execa("docker", args, { stdio: "inherit" });
|
|
253
|
+
await printAccessHints(opts);
|
|
254
|
+
}
|
|
255
|
+
async function dashLogs(opts) {
|
|
256
|
+
const args = ["logs"];
|
|
257
|
+
if (opts.follow)
|
|
258
|
+
args.push("-f");
|
|
259
|
+
args.push(opts.containerName);
|
|
260
|
+
try {
|
|
261
|
+
await execa("docker", args, { stdio: "inherit" });
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
const e = err;
|
|
265
|
+
// SIGINT during `docker logs -f` returns a non-zero exit; treat
|
|
266
|
+
// it as a clean operator-driven cancel rather than a failure.
|
|
267
|
+
if (opts.follow && e.signal === "SIGINT")
|
|
268
|
+
return;
|
|
269
|
+
throw err;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function dashStop(opts) {
|
|
273
|
+
const state = await containerState(opts.containerName);
|
|
274
|
+
if (state === "absent") {
|
|
275
|
+
console.log(`[runway dash] container ${opts.containerName} is not present — nothing to stop.`);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
if (state === "running") {
|
|
279
|
+
await execa("docker", ["stop", opts.containerName], {
|
|
280
|
+
stdio: "inherit",
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
await execa("docker", ["rm", opts.containerName], { stdio: "inherit" });
|
|
284
|
+
}
|
|
285
|
+
if (opts.purge) {
|
|
286
|
+
console.log(`[runway dash] --purge: removing volume ${opts.volumeName}`);
|
|
287
|
+
try {
|
|
288
|
+
await execa("docker", ["volume", "rm", opts.volumeName], {
|
|
289
|
+
stdio: "inherit",
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
console.log(`[runway dash] volume ${opts.volumeName} not present (already removed).`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
console.log(`[runway dash] volume ${opts.volumeName} kept — pass --purge to delete history.`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* `docker inspect` reports `.State.Status` (`running`, `exited`,
|
|
302
|
+
* `created`, etc.) when the container exists, and exits non-zero
|
|
303
|
+
* otherwise. We collapse the status set to three buckets.
|
|
304
|
+
*/
|
|
305
|
+
async function containerState(name) {
|
|
306
|
+
try {
|
|
307
|
+
const { stdout } = await execa("docker", [
|
|
308
|
+
"inspect",
|
|
309
|
+
"--format",
|
|
310
|
+
"{{.State.Status}}",
|
|
311
|
+
name,
|
|
312
|
+
]);
|
|
313
|
+
return stdout.trim() === "running" ? "running" : "stopped";
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return "absent";
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function printAccessHints(opts) {
|
|
320
|
+
console.log("");
|
|
321
|
+
console.log(`[runway dash] dashboard: http://localhost:${opts.dashboardPort}`);
|
|
322
|
+
console.log(`[runway dash] OTLP endpoint: http://localhost:${opts.otlpPort} (set as OTEL_EXPORTER_OTLP_ENDPOINT)`);
|
|
323
|
+
console.log("[runway dash] stop with: runway dash stop");
|
|
324
|
+
}
|