@valescoagency/runway 0.1.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/LICENSE +21 -0
- package/README.md +199 -0
- package/dist/cli.js +88 -0
- package/dist/commands/doctor.js +464 -0
- package/dist/commands/init.js +421 -0
- package/dist/commands/run.js +61 -0
- package/dist/commands/upgrade-repo.js +325 -0
- package/dist/commands/upgrade.js +177 -0
- package/dist/config.js +45 -0
- package/dist/github.js +34 -0
- package/dist/linear.js +81 -0
- package/dist/orchestrator.js +191 -0
- package/dist/prompts.js +40 -0
- package/package.json +63 -0
- package/templates/.env.schema.target-repo +32 -0
- package/templates/Dockerfile.claude-code.base +55 -0
- package/templates/dockerfile-varlock.snippet +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Valesco Agency
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# runway
|
|
2
|
+
|
|
3
|
+
A small CLI for two jobs: **scaffold** a target repo for autonomous
|
|
4
|
+
coding-agent runs, then **drain** a Linear queue against it. Wraps
|
|
5
|
+
[Sandcastle](https://github.com/mattpocock/sandcastle) (Claude Code
|
|
6
|
+
inside Docker), [varlock](https://varlock.dev) + 1Password for
|
|
7
|
+
zero-secrets-at-rest, and the `gh` CLI for PR creation.
|
|
8
|
+
|
|
9
|
+
## Five commands
|
|
10
|
+
|
|
11
|
+
| | |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `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. |
|
|
14
|
+
| `runway init` | Scaffold the cwd repo for runway: write `.sandcastle/Dockerfile` + (tier 2) `.env.schema` with op:// references. Run **once per target repo**. |
|
|
15
|
+
| `runway run` | Drain a Linear queue. For each `Todo` issue: branch, agent works, sub-agent reviews, PR opens (or `needs-human` label). Run **whenever you want a batch of work done**. |
|
|
16
|
+
| `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. |
|
|
17
|
+
| `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. |
|
|
18
|
+
|
|
19
|
+
`runway --help` for the full usage; `runway <cmd> --help` per command.
|
|
20
|
+
|
|
21
|
+
`runway init` and `runway upgrade-repo` are siblings: `init` is the
|
|
22
|
+
first-time scaffold (you provide the op:// vault/items), `upgrade-repo`
|
|
23
|
+
is the re-render (extracts those values from the existing `.env.schema`
|
|
24
|
+
so you never re-type them). Override the extracted values via
|
|
25
|
+
`--op-vault=…`, `--anthropic-item=…`, `--gh-token-item=…` if needed,
|
|
26
|
+
or use `--check` for a CI dry-run that exits 1 on drift.
|
|
27
|
+
|
|
28
|
+
## What runway is and isn't
|
|
29
|
+
|
|
30
|
+
| It is | It isn't |
|
|
31
|
+
|---|---|
|
|
32
|
+
| A small Node CLI that orchestrates Sandcastle runs from a Linear queue | A sandbox or agent runtime — Sandcastle does that |
|
|
33
|
+
| The replacement for the AFK label-handler / preflight / contracts pipeline | A governance layer; trust comes from your review of the PR, not gates |
|
|
34
|
+
| Repo-agnostic — runs in whatever cwd you launch it from | Multi-repo (yet) — one repo per `runway run` invocation |
|
|
35
|
+
| Sequential | Parallel (yet) |
|
|
36
|
+
|
|
37
|
+
## Architecture
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Linear (Todo, team=VA)
|
|
41
|
+
↓ poll
|
|
42
|
+
runway (this CLI, on your Mac, run from inside the target repo)
|
|
43
|
+
↓ for each issue
|
|
44
|
+
│ sandcastle.run({ agent: claudeCode, sandbox: docker, cwd: process.cwd(), ... })
|
|
45
|
+
│ → branch agent/<issue-id>, commits, tests
|
|
46
|
+
│
|
|
47
|
+
│ sandcastle.run({ ..., prompt: review template })
|
|
48
|
+
│ → REVIEW: APPROVED | REVIEW: REJECTED — <reason>
|
|
49
|
+
│
|
|
50
|
+
├── approved → git push → gh pr create → Linear "In Review"
|
|
51
|
+
└── rejected → Linear label "needs-human", comment with reason
|
|
52
|
+
↓ next issue
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Prerequisites
|
|
56
|
+
|
|
57
|
+
- macOS or Linux
|
|
58
|
+
- Docker Desktop (or Podman)
|
|
59
|
+
- Node 22+
|
|
60
|
+
- `gh` CLI authenticated against the org that hosts your target repo
|
|
61
|
+
- Linear API key with read+write on the team you're targeting
|
|
62
|
+
- Anthropic API key (set in the **target repo's** `.sandcastle/.env`,
|
|
63
|
+
not in runway's env — Sandcastle reads it)
|
|
64
|
+
|
|
65
|
+
## One-time setup per target repo
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cd /path/to/your/repo
|
|
69
|
+
runway init \
|
|
70
|
+
--op-vault=runway \
|
|
71
|
+
--anthropic-item=anthropic-api-key \
|
|
72
|
+
--gh-token-item=gh-token
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
(No `--op-account` — runway uses 1Password service-account auth
|
|
76
|
+
(`OP_SERVICE_ACCOUNT_TOKEN`) exclusively, and the token already
|
|
77
|
+
encodes the tenant. `op://` URIs runway writes are
|
|
78
|
+
`op://<vault>/<item>`, not `op://<account>/<vault>/<item>`.)
|
|
79
|
+
|
|
80
|
+
This runs `npx sandcastle init`, patches the generated `.sandcastle/Dockerfile`
|
|
81
|
+
to bake in `varlock` + the 1Password CLI + a `claude` shim, scaffolds
|
|
82
|
+
`.env.schema` at the repo root with op:// references to your 1Password vault,
|
|
83
|
+
and deletes `.sandcastle/.env` so no secrets sit at rest. Review the diff,
|
|
84
|
+
commit on a feature branch, open a PR.
|
|
85
|
+
|
|
86
|
+
Pass `--tier=1` if you want the plain Sandcastle setup with `.sandcastle/.env`
|
|
87
|
+
and no varlock (faster but secrets land on disk).
|
|
88
|
+
|
|
89
|
+
Architecture walkthrough: [`docs/secrets-with-varlock.md`](docs/secrets-with-varlock.md).
|
|
90
|
+
|
|
91
|
+
## Secrets — recommended: varlock + 1Password
|
|
92
|
+
|
|
93
|
+
If you don't want any secret sitting at rest in any `.env` file,
|
|
94
|
+
runway integrates [varlock](https://varlock.dev). Two layers (host +
|
|
95
|
+
in-container), both opt-in. Full walk-through:
|
|
96
|
+
[`docs/secrets-with-varlock.md`](docs/secrets-with-varlock.md).
|
|
97
|
+
|
|
98
|
+
TL;DR for the host layer:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
varlock run --schema /path/to/runway/.env.schema -- runway --max 3
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Without varlock, runway falls back to plain `process.env` and
|
|
105
|
+
sandcastle reads `.sandcastle/.env` per its docs.
|
|
106
|
+
|
|
107
|
+
## Install
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pnpm install -g @valescoagency/runway # or npm i -g, yarn global add
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Export runway's own env (in your shell rc, or wherever you keep API keys):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
export LINEAR_API_KEY=lin_api_...
|
|
117
|
+
# Optional overrides:
|
|
118
|
+
# export RUNWAY_LINEAR_TEAM=VA
|
|
119
|
+
# export RUNWAY_READY_STATUS="Todo"
|
|
120
|
+
# export RUNWAY_IN_PROGRESS_STATUS="In Progress"
|
|
121
|
+
# export RUNWAY_IN_REVIEW_STATUS="In Review"
|
|
122
|
+
# export RUNWAY_HITL_LABEL="needs-human"
|
|
123
|
+
# export RUNWAY_MAX_ITERATIONS=5
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### From source (development)
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
git clone git@github.com:ValescoAgency/runway.git
|
|
130
|
+
cd runway
|
|
131
|
+
pnpm install
|
|
132
|
+
pnpm build # compiles src/ → dist/
|
|
133
|
+
pnpm link --global # so `runway` is on your $PATH
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`pnpm dev -- <args>` runs the TypeScript source via `tsx` without building, useful while iterating on runway itself.
|
|
137
|
+
|
|
138
|
+
## Usage
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
cd /path/to/the/repo/you/want/agents/working/on
|
|
142
|
+
runway run # drain the entire ready queue
|
|
143
|
+
runway run --max 3 # process at most 3 issues then exit
|
|
144
|
+
runway --help
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`runway` (no subcommand) is an alias for `runway run` for back-compat.
|
|
148
|
+
|
|
149
|
+
The CLI exits with 0 even if some issues hit HITL or errored — those
|
|
150
|
+
are normal outcomes. Check Linear for the `needs-human` label and the
|
|
151
|
+
per-issue comments for what happened.
|
|
152
|
+
|
|
153
|
+
## Linear conventions
|
|
154
|
+
|
|
155
|
+
Runway picks up issues that are:
|
|
156
|
+
|
|
157
|
+
- in team `RUNWAY_LINEAR_TEAM` (default `VA`)
|
|
158
|
+
- in workflow state `RUNWAY_READY_STATUS` (default `Todo`)
|
|
159
|
+
|
|
160
|
+
It transitions them through:
|
|
161
|
+
|
|
162
|
+
- `In Progress` while the agent is running
|
|
163
|
+
- `In Review` when the PR opens
|
|
164
|
+
- (label `needs-human`) if the agent or reviewer can't finish
|
|
165
|
+
|
|
166
|
+
These names are configurable per env var; the queries match by name so
|
|
167
|
+
your Linear workspace's actual state names need to line up with what
|
|
168
|
+
you set.
|
|
169
|
+
|
|
170
|
+
## Sub-agent review
|
|
171
|
+
|
|
172
|
+
Every implementation run is followed by a fresh Sandcastle run with
|
|
173
|
+
[`prompts/review.md`](prompts/review.md). The reviewer reads the diff
|
|
174
|
+
and the issue body and outputs one of:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
REVIEW: APPROVED
|
|
178
|
+
REVIEW: REJECTED — <one-line reason>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
If approved, runway pushes the branch and opens the PR. If rejected,
|
|
182
|
+
the issue gets the HITL label and a comment with the reviewer's reason.
|
|
183
|
+
|
|
184
|
+
The reviewer is intentionally adversarial — its job is to find reasons
|
|
185
|
+
NOT to ship, not to rubber-stamp.
|
|
186
|
+
|
|
187
|
+
## What's deliberately missing in v1
|
|
188
|
+
|
|
189
|
+
- Parallel runs (one issue at a time)
|
|
190
|
+
- Multi-repo dispatch from a single command (cd into each repo)
|
|
191
|
+
- Vercel Sandbox provider (Docker only — swap to `vercel()` later)
|
|
192
|
+
- DAG / dependency awareness across issues
|
|
193
|
+
- Auto-rebase / merge of approved PRs (you do that)
|
|
194
|
+
|
|
195
|
+
These are tractable, just not v1.
|
|
196
|
+
|
|
197
|
+
## Status
|
|
198
|
+
|
|
199
|
+
v0.1 — scaffold complete, untested against a live queue.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { doctorCommand, printDoctorUsage } from "./commands/doctor.js";
|
|
3
|
+
import { initCommand, printInitUsage } from "./commands/init.js";
|
|
4
|
+
import { runCommand, printRunUsage } from "./commands/run.js";
|
|
5
|
+
import { upgradeCommand, printUpgradeUsage } from "./commands/upgrade.js";
|
|
6
|
+
import { upgradeRepoCommand, printUpgradeRepoUsage, } from "./commands/upgrade-repo.js";
|
|
7
|
+
const SUBCOMMANDS = [
|
|
8
|
+
{
|
|
9
|
+
name: "doctor",
|
|
10
|
+
summary: "Read-only preflight: tooling, env, repo state, agent image.",
|
|
11
|
+
run: doctorCommand,
|
|
12
|
+
help: printDoctorUsage,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "init",
|
|
16
|
+
summary: "Scaffold a target repo (sandcastle init + varlock layer).",
|
|
17
|
+
run: initCommand,
|
|
18
|
+
help: printInitUsage,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "run",
|
|
22
|
+
summary: "Drain a Linear queue against the cwd repo (default verb).",
|
|
23
|
+
run: runCommand,
|
|
24
|
+
help: printRunUsage,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "upgrade",
|
|
28
|
+
summary: "Update the runway CLI itself (git pull + pnpm install).",
|
|
29
|
+
run: upgradeCommand,
|
|
30
|
+
help: printUpgradeUsage,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "upgrade-repo",
|
|
34
|
+
summary: "Refresh a target repo's runway scaffold against current templates.",
|
|
35
|
+
run: upgradeRepoCommand,
|
|
36
|
+
help: printUpgradeRepoUsage,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
function printRootUsage() {
|
|
40
|
+
console.log(`runway — Linear-driven coding agent orchestrator
|
|
41
|
+
|
|
42
|
+
USAGE
|
|
43
|
+
runway <command> [options]
|
|
44
|
+
|
|
45
|
+
COMMANDS`);
|
|
46
|
+
for (const cmd of SUBCOMMANDS) {
|
|
47
|
+
console.log(` ${cmd.name.padEnd(13)} ${cmd.summary}`);
|
|
48
|
+
}
|
|
49
|
+
console.log(`
|
|
50
|
+
Run \`runway <command> --help\` for command-specific options.
|
|
51
|
+
|
|
52
|
+
GLOBAL
|
|
53
|
+
--help, -h Show this help.
|
|
54
|
+
--version, -V Show version.
|
|
55
|
+
`);
|
|
56
|
+
}
|
|
57
|
+
async function main() {
|
|
58
|
+
const argv = process.argv.slice(2);
|
|
59
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
60
|
+
printRootUsage();
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
if (argv[0] === "--version" || argv[0] === "-V") {
|
|
64
|
+
// Read package.json at runtime — single source of truth.
|
|
65
|
+
const { readFile } = await import("node:fs/promises");
|
|
66
|
+
const { fileURLToPath } = await import("node:url");
|
|
67
|
+
const { dirname, join } = await import("node:path");
|
|
68
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
69
|
+
const pkg = JSON.parse(await readFile(join(here, "..", "package.json"), "utf8"));
|
|
70
|
+
console.log(pkg.version);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
// First arg may be a subcommand name. If it isn't, treat the whole
|
|
74
|
+
// argv as `run <argv>` (back-compat: `runway --max 3` still works).
|
|
75
|
+
const first = argv[0];
|
|
76
|
+
const matched = SUBCOMMANDS.find((c) => c.name === first);
|
|
77
|
+
if (matched) {
|
|
78
|
+
await matched.run(argv.slice(1));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Implicit `run` — back-compat for old invocations.
|
|
82
|
+
await runCommand(argv);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
main().catch((err) => {
|
|
86
|
+
console.error("[runway] fatal:", err instanceof Error ? err.message : err);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
});
|