@valescoagency/runway 0.2.0 → 0.3.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 +87 -3
- package/dist/commands/doctor.js +79 -4
- package/dist/commands/init.js +32 -6
- package/dist/commands/run.js +5 -1
- package/dist/commands/upgrade-repo.js +42 -14
- package/dist/config.js +11 -0
- package/dist/git.js +41 -0
- package/dist/github.js +2 -2
- package/dist/orchestrator.js +20 -12
- package/package.json +9 -3
- package/templates/.env.schema.target-repo +8 -1
package/README.md
CHANGED
|
@@ -61,8 +61,12 @@ runway (this CLI, on your Mac, run from inside the target repo)
|
|
|
61
61
|
- Node 22+
|
|
62
62
|
- `gh` CLI authenticated against the org that hosts your target repo
|
|
63
63
|
- Linear API key with read+write on the team you're targeting
|
|
64
|
-
-
|
|
65
|
-
|
|
64
|
+
- A Claude Code credential — **either** an Anthropic API key
|
|
65
|
+
(`sk-ant-api03-…`, pay-per-token) **or** a Pro/Max OAuth token
|
|
66
|
+
(`sk-ant-oat01-…`, generated via `claude setup-token`). The two are
|
|
67
|
+
not interchangeable — see "Claude Code auth modes" below. Stored in
|
|
68
|
+
the **target repo's** `.sandcastle/.env` (tier 1) or 1Password
|
|
69
|
+
(tier 2); never in runway's own env.
|
|
66
70
|
|
|
67
71
|
## One-time setup per target repo
|
|
68
72
|
|
|
@@ -71,7 +75,8 @@ cd /path/to/your/repo
|
|
|
71
75
|
runway init \
|
|
72
76
|
--op-vault=runway \
|
|
73
77
|
--anthropic-item=anthropic-api-key \
|
|
74
|
-
--gh-token-item=gh-token
|
|
78
|
+
--gh-token-item=gh-token \
|
|
79
|
+
--auth-mode=api-key # or --auth-mode=oauth for Pro/Max tokens
|
|
75
80
|
```
|
|
76
81
|
|
|
77
82
|
(No `--op-account` — runway uses 1Password service-account auth
|
|
@@ -92,6 +97,37 @@ and no varlock (faster but secrets land on disk).
|
|
|
92
97
|
|
|
93
98
|
Architecture walkthrough: [`docs/secrets-with-varlock.md`](docs/secrets-with-varlock.md).
|
|
94
99
|
|
|
100
|
+
## Claude Code auth modes
|
|
101
|
+
|
|
102
|
+
Claude Code accepts two distinct credentials, and they are **not
|
|
103
|
+
interchangeable** — passing one as the other yields a generic
|
|
104
|
+
`Invalid API key` inside the container with no useful diagnostic.
|
|
105
|
+
|
|
106
|
+
| Mode | Env var | Token shape | Source |
|
|
107
|
+
|---|---|---|---|
|
|
108
|
+
| `api-key` (default) | `ANTHROPIC_API_KEY` | `sk-ant-api03-…` | [Anthropic console](https://console.anthropic.com), pay-per-token |
|
|
109
|
+
| `oauth` | `CLAUDE_CODE_OAUTH_TOKEN` | `sk-ant-oat01-…` | `claude setup-token` on your Pro/Max account |
|
|
110
|
+
|
|
111
|
+
Pick whichever matches what's stored in your 1Password item:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
runway init --tier=2 --op-vault=runway \
|
|
115
|
+
--anthropic-item=claude-pro-oauth-token \
|
|
116
|
+
--gh-token-item=gh-token \
|
|
117
|
+
--auth-mode=oauth
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The `--anthropic-item` flag is the 1Password item name regardless of
|
|
121
|
+
mode; only the env var written into `.env.schema` changes. `runway
|
|
122
|
+
doctor` surfaces the resolved mode under Environment (`claude auth
|
|
123
|
+
mode: oauth (…)`), and fails fast if `.env.schema` ends up with both
|
|
124
|
+
env vars at once.
|
|
125
|
+
|
|
126
|
+
If you switch modes later, run `runway upgrade-repo` — it extracts
|
|
127
|
+
the existing op:// references, re-renders the template with the new
|
|
128
|
+
mode (detected automatically from the schema), and writes back. You
|
|
129
|
+
do not need to re-pass the op:// flags.
|
|
130
|
+
|
|
95
131
|
## Secrets — recommended: varlock + 1Password
|
|
96
132
|
|
|
97
133
|
If you don't want any secret sitting at rest in any `.env` file,
|
|
@@ -121,6 +157,7 @@ export LINEAR_API_KEY=lin_api_...
|
|
|
121
157
|
# Optional overrides:
|
|
122
158
|
# export RUNWAY_LINEAR_TEAM=VA
|
|
123
159
|
# export RUNWAY_LINEAR_PROJECT=<project-id-or-slug> # optional, scopes queue to one project
|
|
160
|
+
# export RUNWAY_BASE_BRANCH=master # optional, overrides auto-detected default branch
|
|
124
161
|
# export RUNWAY_READY_STATUS="Todo"
|
|
125
162
|
# export RUNWAY_IN_PROGRESS_STATUS="In Progress"
|
|
126
163
|
# export RUNWAY_IN_REVIEW_STATUS="In Review"
|
|
@@ -140,6 +177,38 @@ pnpm link --global # so `runway` is on your $PATH
|
|
|
140
177
|
|
|
141
178
|
`pnpm dev -- <args>` runs the TypeScript source via `tsx` without building, useful while iterating on runway itself.
|
|
142
179
|
|
|
180
|
+
#### Tests
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
pnpm test # one-shot run, used by CI
|
|
184
|
+
pnpm test:watch # watch mode for local iteration
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Vitest is the harness; tests live colocated with the source as
|
|
188
|
+
`*.test.ts` files (e.g. `src/git.test.ts` next to `src/git.ts`). CI
|
|
189
|
+
runs `pnpm typecheck && pnpm test` on every PR via
|
|
190
|
+
`.github/workflows/ci.yml`.
|
|
191
|
+
|
|
192
|
+
When adding logic that has a sharp pass/fail signal, add a test next
|
|
193
|
+
to it. The seed suite covers `parseRunArgs`, `detectBaseBranch`, the
|
|
194
|
+
`parseOpRefs` regex extraction, and the `drainQueue` error-handler
|
|
195
|
+
branches — copy any of those as a shape for new tests.
|
|
196
|
+
|
|
197
|
+
#### Git hooks (lefthook + commitlint)
|
|
198
|
+
|
|
199
|
+
Hooks install automatically on `pnpm install` via the `prepare`
|
|
200
|
+
script. What runs and when:
|
|
201
|
+
|
|
202
|
+
| Hook | Runs | Why |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| `pre-commit` | `pnpm typecheck` | Catch TS errors before they land on a branch. |
|
|
205
|
+
| `commit-msg` | `pnpm exec commitlint --edit` | Reject non-conventional commit messages (CLAUDE.md convention). |
|
|
206
|
+
| `pre-push` | `pnpm test` | Block pushing red. |
|
|
207
|
+
|
|
208
|
+
Skip a single hook invocation with `LEFTHOOK=0 git commit …` (or
|
|
209
|
+
`… git push …`). To re-install after editing `lefthook.yml`, run
|
|
210
|
+
`pnpm exec lefthook install -f`.
|
|
211
|
+
|
|
143
212
|
## Usage
|
|
144
213
|
|
|
145
214
|
```bash
|
|
@@ -177,6 +246,21 @@ These names are configurable per env var; the queries match by name so
|
|
|
177
246
|
your Linear workspace's actual state names need to line up with what
|
|
178
247
|
you set.
|
|
179
248
|
|
|
249
|
+
## Base branch
|
|
250
|
+
|
|
251
|
+
Runway auto-detects the repo's default branch at the start of every
|
|
252
|
+
`runway run` by reading `origin/HEAD` (with `git remote show origin`
|
|
253
|
+
as a fallback for fresh clones). That branch is used for diffing the
|
|
254
|
+
agent's work, counting commits when deciding whether a startup
|
|
255
|
+
failure should revert to `Todo`, and as the `--base` for the PR.
|
|
256
|
+
|
|
257
|
+
Set `RUNWAY_BASE_BRANCH=<name>` to override detection — useful when
|
|
258
|
+
you want runway to target a release branch instead of the default, or
|
|
259
|
+
when `origin/HEAD` isn't set and you don't want to run
|
|
260
|
+
`git remote set-head origin --auto`. `runway doctor` surfaces the
|
|
261
|
+
resolved base branch (detected or overridden) in its Environment
|
|
262
|
+
section.
|
|
263
|
+
|
|
180
264
|
## Sub-agent review
|
|
181
265
|
|
|
182
266
|
Every implementation run is followed by a fresh Sandcastle run with
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { execa } from "execa";
|
|
4
|
+
import { detectBaseBranch } from "../git.js";
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// Usage
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
@@ -83,7 +84,7 @@ export async function doctorCommand(argv) {
|
|
|
83
84
|
const sections = [];
|
|
84
85
|
sections.push(await checkHostTooling(tierForToolingChecks));
|
|
85
86
|
if (initialised || opts.tierOverride !== undefined) {
|
|
86
|
-
sections.push(checkEnvironment(tierForToolingChecks));
|
|
87
|
+
sections.push(await checkEnvironment(tierForToolingChecks, cwd, repo));
|
|
87
88
|
sections.push(await checkRepoState(cwd, repo));
|
|
88
89
|
sections.push(await checkDockerImage(cwd));
|
|
89
90
|
}
|
|
@@ -113,11 +114,24 @@ function detectRepoState(cwd) {
|
|
|
113
114
|
const hasDockerfile = existsSync(join(cwd, ".sandcastle", "Dockerfile"));
|
|
114
115
|
const hasSchema = existsSync(join(cwd, ".env.schema"));
|
|
115
116
|
let tier = null;
|
|
117
|
+
let authMode = null;
|
|
118
|
+
let hasConflictingAuthVars = false;
|
|
116
119
|
if (hasSchema) {
|
|
117
120
|
try {
|
|
118
121
|
const schema = readFileSync(join(cwd, ".env.schema"), "utf8");
|
|
119
|
-
|
|
122
|
+
const hasApiKey = /ANTHROPIC_API_KEY\s*=\s*exec\(/.test(schema);
|
|
123
|
+
const hasOauth = /CLAUDE_CODE_OAUTH_TOKEN\s*=\s*exec\(/.test(schema);
|
|
124
|
+
if (hasApiKey && hasOauth) {
|
|
120
125
|
tier = 2;
|
|
126
|
+
hasConflictingAuthVars = true;
|
|
127
|
+
}
|
|
128
|
+
else if (hasApiKey) {
|
|
129
|
+
tier = 2;
|
|
130
|
+
authMode = "api-key";
|
|
131
|
+
}
|
|
132
|
+
else if (hasOauth) {
|
|
133
|
+
tier = 2;
|
|
134
|
+
authMode = "oauth";
|
|
121
135
|
}
|
|
122
136
|
else if (hasDockerfile) {
|
|
123
137
|
tier = 1;
|
|
@@ -130,7 +144,7 @@ function detectRepoState(cwd) {
|
|
|
130
144
|
else if (hasDockerfile) {
|
|
131
145
|
tier = 1;
|
|
132
146
|
}
|
|
133
|
-
return { tier, hasDockerfile, hasSchema };
|
|
147
|
+
return { tier, hasDockerfile, hasSchema, authMode, hasConflictingAuthVars };
|
|
134
148
|
}
|
|
135
149
|
// ---------------------------------------------------------------------------
|
|
136
150
|
// Section: Host tooling
|
|
@@ -229,7 +243,7 @@ async function checkGhAuth() {
|
|
|
229
243
|
// ---------------------------------------------------------------------------
|
|
230
244
|
// Section: Environment
|
|
231
245
|
// ---------------------------------------------------------------------------
|
|
232
|
-
function checkEnvironment(tier) {
|
|
246
|
+
async function checkEnvironment(tier, cwd, repo) {
|
|
233
247
|
const checks = new Map();
|
|
234
248
|
checks.set("LINEAR_API_KEY", envSet("LINEAR_API_KEY", "fail"));
|
|
235
249
|
// Informational: which Linear scope a `runway run` would use.
|
|
@@ -242,9 +256,70 @@ function checkEnvironment(tier) {
|
|
|
242
256
|
? `team ${team} / project ${project}`
|
|
243
257
|
: `team ${team} (team-wide — RUNWAY_LINEAR_PROJECT unset)`,
|
|
244
258
|
});
|
|
259
|
+
// Informational: which base branch a `runway run` would diff against
|
|
260
|
+
// and target with PRs. Detection failure here is a real problem —
|
|
261
|
+
// surface it as a fail so the user knows up front.
|
|
262
|
+
const override = process.env.RUNWAY_BASE_BRANCH?.trim();
|
|
263
|
+
if (override) {
|
|
264
|
+
checks.set("base_branch", {
|
|
265
|
+
status: "ok",
|
|
266
|
+
label: "base branch",
|
|
267
|
+
detail: `${override} (RUNWAY_BASE_BRANCH override)`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
try {
|
|
272
|
+
const detected = await detectBaseBranch(cwd);
|
|
273
|
+
checks.set("base_branch", {
|
|
274
|
+
status: "ok",
|
|
275
|
+
label: "base branch",
|
|
276
|
+
detail: `${detected} (detected from origin/HEAD)`,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
checks.set("base_branch", {
|
|
281
|
+
status: "fail",
|
|
282
|
+
label: "base branch",
|
|
283
|
+
detail: errMsg(err),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
245
287
|
if (tier === 2) {
|
|
246
288
|
// Tier 2: needed by varlock to resolve op:// refs in the container.
|
|
247
289
|
checks.set("OP_SERVICE_ACCOUNT_TOKEN", envSet("OP_SERVICE_ACCOUNT_TOKEN", "fail"));
|
|
290
|
+
// Surface which Claude Code auth env var the .env.schema declares.
|
|
291
|
+
// ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN aren't
|
|
292
|
+
// interchangeable; a mismatch between this and what's stored in
|
|
293
|
+
// 1Password yields a generic "Invalid API key" inside the
|
|
294
|
+
// container with no useful diagnostic.
|
|
295
|
+
if (repo.hasConflictingAuthVars) {
|
|
296
|
+
checks.set("auth_mode", {
|
|
297
|
+
status: "fail",
|
|
298
|
+
label: "claude auth mode",
|
|
299
|
+
detail: ".env.schema declares both ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN — pick one (they are not interchangeable)",
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
else if (repo.authMode === "oauth") {
|
|
303
|
+
checks.set("auth_mode", {
|
|
304
|
+
status: "ok",
|
|
305
|
+
label: "claude auth mode",
|
|
306
|
+
detail: "oauth (CLAUDE_CODE_OAUTH_TOKEN — Pro/Max subscription)",
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
else if (repo.authMode === "api-key") {
|
|
310
|
+
checks.set("auth_mode", {
|
|
311
|
+
status: "ok",
|
|
312
|
+
label: "claude auth mode",
|
|
313
|
+
detail: "api-key (ANTHROPIC_API_KEY — pay-per-token)",
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
checks.set("auth_mode", {
|
|
318
|
+
status: "fail",
|
|
319
|
+
label: "claude auth mode",
|
|
320
|
+
detail: ".env.schema declares neither ANTHROPIC_API_KEY nor CLAUDE_CODE_OAUTH_TOKEN",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
248
323
|
}
|
|
249
324
|
return { title: "Environment", checks, ran: true };
|
|
250
325
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -5,6 +5,10 @@ import { execa } from "execa";
|
|
|
5
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
// runway/src/commands/init.ts → runway/templates/
|
|
7
7
|
const TEMPLATES_DIR = join(__dirname, "..", "..", "templates");
|
|
8
|
+
const AUTH_MODE_ENV_VAR = {
|
|
9
|
+
"api-key": "ANTHROPIC_API_KEY",
|
|
10
|
+
oauth: "CLAUDE_CODE_OAUTH_TOKEN",
|
|
11
|
+
};
|
|
8
12
|
export function printInitUsage() {
|
|
9
13
|
console.log(`runway init — scaffold a target repo for runway consumption
|
|
10
14
|
|
|
@@ -23,8 +27,18 @@ OPTIONS
|
|
|
23
27
|
--tier=2 DEFAULT. Adds varlock + 1Password CLI inside the
|
|
24
28
|
container. Zero secrets at rest.
|
|
25
29
|
--op-vault=NAME 1Password vault name (e.g. "runway"). Required for tier 2.
|
|
26
|
-
--anthropic-item=N Item name in the vault that holds
|
|
30
|
+
--anthropic-item=N Item name in the vault that holds the Claude Code
|
|
31
|
+
credential (ANTHROPIC_API_KEY or
|
|
32
|
+
CLAUDE_CODE_OAUTH_TOKEN — see --auth-mode).
|
|
33
|
+
Required for tier 2.
|
|
27
34
|
--gh-token-item=N Item name in the vault that holds GH_TOKEN. Required for tier 2.
|
|
35
|
+
--auth-mode=MODE How Claude Code authenticates inside the
|
|
36
|
+
container. \`api-key\` (default) writes the
|
|
37
|
+
ANTHROPIC_API_KEY env var for pay-per-token API
|
|
38
|
+
keys (sk-ant-api03-…). \`oauth\` writes
|
|
39
|
+
CLAUDE_CODE_OAUTH_TOKEN for Pro/Max
|
|
40
|
+
subscription tokens from \`claude setup-token\`
|
|
41
|
+
(sk-ant-oat01-…). They are NOT interchangeable.
|
|
28
42
|
--allow-dirty Skip the "working tree clean" preflight check.
|
|
29
43
|
--force Overwrite an existing .sandcastle/Dockerfile.
|
|
30
44
|
--skip-build Don't \`docker build\` the agent image. Faster init,
|
|
@@ -63,6 +77,7 @@ function parseInitArgs(argv) {
|
|
|
63
77
|
let opVault;
|
|
64
78
|
let anthropicItem;
|
|
65
79
|
let ghTokenItem;
|
|
80
|
+
let authMode = "api-key";
|
|
66
81
|
let allowDirty = false;
|
|
67
82
|
let force = false;
|
|
68
83
|
let skipBuild = false;
|
|
@@ -95,6 +110,13 @@ function parseInitArgs(argv) {
|
|
|
95
110
|
else if (arg.startsWith("--gh-token-item=")) {
|
|
96
111
|
ghTokenItem = arg.slice("--gh-token-item=".length);
|
|
97
112
|
}
|
|
113
|
+
else if (arg.startsWith("--auth-mode=")) {
|
|
114
|
+
const v = arg.slice("--auth-mode=".length);
|
|
115
|
+
if (v !== "api-key" && v !== "oauth") {
|
|
116
|
+
throw new Error(`--auth-mode must be "api-key" or "oauth", got "${v}"`);
|
|
117
|
+
}
|
|
118
|
+
authMode = v;
|
|
119
|
+
}
|
|
98
120
|
else {
|
|
99
121
|
throw new Error(`unknown argument: ${arg}`);
|
|
100
122
|
}
|
|
@@ -116,6 +138,7 @@ function parseInitArgs(argv) {
|
|
|
116
138
|
opVault,
|
|
117
139
|
anthropicItem,
|
|
118
140
|
ghTokenItem,
|
|
141
|
+
authMode,
|
|
119
142
|
allowDirty,
|
|
120
143
|
force,
|
|
121
144
|
skipBuild,
|
|
@@ -277,12 +300,14 @@ export async function applyVarlockLayer(cwd, opts) {
|
|
|
277
300
|
writeFileSync(`${schemaPath}.bak`, readFileSync(schemaPath, "utf8"));
|
|
278
301
|
}
|
|
279
302
|
const schemaTemplate = readFileSync(join(TEMPLATES_DIR, ".env.schema.target-repo"), "utf8");
|
|
303
|
+
const anthropicEnvVar = AUTH_MODE_ENV_VAR[opts.authMode];
|
|
280
304
|
const rendered = schemaTemplate
|
|
281
305
|
.replaceAll("{{OP_VAULT}}", opts.opVault)
|
|
282
306
|
.replaceAll("{{ANTHROPIC_ITEM}}", opts.anthropicItem)
|
|
283
|
-
.replaceAll("{{GH_TOKEN_ITEM}}", opts.ghTokenItem)
|
|
307
|
+
.replaceAll("{{GH_TOKEN_ITEM}}", opts.ghTokenItem)
|
|
308
|
+
.replaceAll("{{ANTHROPIC_ENV_VAR}}", anthropicEnvVar);
|
|
284
309
|
writeFileSync(schemaPath, rendered);
|
|
285
|
-
console.log(` ✓ wrote .env.schema (op://${opts.opVault}/...)`);
|
|
310
|
+
console.log(` ✓ wrote .env.schema (auth-mode=${opts.authMode}, ${anthropicEnvVar}, op://${opts.opVault}/...)`);
|
|
286
311
|
// 2. Patch Dockerfile.
|
|
287
312
|
const dockerfilePath = join(cwd, ".sandcastle", "Dockerfile");
|
|
288
313
|
if (!existsSync(dockerfilePath)) {
|
|
@@ -361,11 +386,12 @@ export async function verify(cwd, opts) {
|
|
|
361
386
|
if (!existsSync(schemaPath))
|
|
362
387
|
fail(".env.schema missing at repo root (tier 2 requires it)");
|
|
363
388
|
const schema = readFileSync(schemaPath, "utf8");
|
|
364
|
-
|
|
365
|
-
|
|
389
|
+
const anthropicEnvVar = AUTH_MODE_ENV_VAR[opts.authMode];
|
|
390
|
+
if (!schema.includes(`${anthropicEnvVar}=`))
|
|
391
|
+
fail(`.env.schema missing ${anthropicEnvVar} (auth-mode=${opts.authMode})`);
|
|
366
392
|
if (!schema.includes("GH_TOKEN="))
|
|
367
393
|
fail(".env.schema missing GH_TOKEN");
|
|
368
|
-
ok(
|
|
394
|
+
ok(`.env.schema declares ${anthropicEnvVar} + GH_TOKEN`);
|
|
369
395
|
// Inline secret shape check.
|
|
370
396
|
const secretRe = /(sk-ant-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{20,}|lin_api_[A-Za-z0-9]{20,})/;
|
|
371
397
|
if (secretRe.test(schema)) {
|
package/dist/commands/run.js
CHANGED
|
@@ -2,7 +2,7 @@ import { loadConfig } from "../config.js";
|
|
|
2
2
|
import { createLinearGateway } from "../linear.js";
|
|
3
3
|
import { createGithubGateway } from "../github.js";
|
|
4
4
|
import { assertSandcastleInitialised, drainQueue, } from "../orchestrator.js";
|
|
5
|
-
function parseRunArgs(argv) {
|
|
5
|
+
export function parseRunArgs(argv) {
|
|
6
6
|
const opts = {};
|
|
7
7
|
for (let i = 0; i < argv.length; i += 1) {
|
|
8
8
|
const a = argv[i];
|
|
@@ -55,6 +55,10 @@ ENVIRONMENT
|
|
|
55
55
|
LINEAR_API_KEY required
|
|
56
56
|
RUNWAY_LINEAR_TEAM default "VA"
|
|
57
57
|
RUNWAY_LINEAR_PROJECT optional — scope to one project
|
|
58
|
+
RUNWAY_BASE_BRANCH optional — override the auto-detected base
|
|
59
|
+
branch (the branch runway diffs against
|
|
60
|
+
and targets with PRs). Detected from
|
|
61
|
+
origin/HEAD when unset.
|
|
58
62
|
RUNWAY_READY_STATUS default "Todo"
|
|
59
63
|
RUNWAY_IN_PROGRESS_STATUS default "In Progress"
|
|
60
64
|
RUNWAY_IN_REVIEW_STATUS default "In Review"
|
|
@@ -27,7 +27,9 @@ OPTIONS
|
|
|
27
27
|
lines outside the runway templates (manual edits).
|
|
28
28
|
--op-vault=NAME Override the 1Password vault. By default, upgrade-repo
|
|
29
29
|
extracts this from the existing .env.schema.
|
|
30
|
-
--anthropic-item=N Override the
|
|
30
|
+
--anthropic-item=N Override the Claude Code credential item name
|
|
31
|
+
(i.e. the ANTHROPIC_API_KEY or
|
|
32
|
+
CLAUDE_CODE_OAUTH_TOKEN 1Password item).
|
|
31
33
|
--gh-token-item=N Override the GH_TOKEN item name.
|
|
32
34
|
--help, -h Show this help.
|
|
33
35
|
|
|
@@ -118,6 +120,7 @@ export async function upgradeRepoCommand(argv) {
|
|
|
118
120
|
opVault: "placeholder",
|
|
119
121
|
anthropicItem: "placeholder",
|
|
120
122
|
ghTokenItem: "placeholder",
|
|
123
|
+
authMode: "api-key",
|
|
121
124
|
};
|
|
122
125
|
await preflight(cwd, preflightOpts);
|
|
123
126
|
// Render new file contents in memory.
|
|
@@ -165,6 +168,7 @@ export async function upgradeRepoCommand(argv) {
|
|
|
165
168
|
opVault: resolved?.opVault ?? "placeholder",
|
|
166
169
|
anthropicItem: resolved?.anthropicItem ?? "placeholder",
|
|
167
170
|
ghTokenItem: resolved?.ghTokenItem ?? "placeholder",
|
|
171
|
+
authMode: resolved?.authMode ?? "api-key",
|
|
168
172
|
};
|
|
169
173
|
await verify(cwd, verifyOpts);
|
|
170
174
|
console.log(`[runway upgrade-repo] done — tier ${tier} scaffold refreshed`);
|
|
@@ -180,8 +184,10 @@ function detectTier(cwd) {
|
|
|
180
184
|
}
|
|
181
185
|
if (existsSync(schemaPath)) {
|
|
182
186
|
const schema = readFileSync(schemaPath, "utf8");
|
|
183
|
-
// Tier-2 marker:
|
|
184
|
-
|
|
187
|
+
// Tier-2 marker: the Claude Code credential (whichever env var
|
|
188
|
+
// name the user picked at init time) uses the varlock shell-call
|
|
189
|
+
// form.
|
|
190
|
+
const tier2Marker = new RegExp(`(ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)\\s*=\\s*${execName()}\\(`);
|
|
185
191
|
if (tier2Marker.test(schema)) {
|
|
186
192
|
return 2;
|
|
187
193
|
}
|
|
@@ -197,17 +203,29 @@ function execName() {
|
|
|
197
203
|
// ---------------------------------------------------------------------------
|
|
198
204
|
// op:// extraction
|
|
199
205
|
// ---------------------------------------------------------------------------
|
|
200
|
-
|
|
206
|
+
// Either ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN — capture which
|
|
207
|
+
// it is so upgrade-repo can re-render in the same auth mode.
|
|
208
|
+
const ANTHROPIC_RE = new RegExp(`^\\s*(ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)\\s*=\\s*${execName()}\\(\\s*['"]op read "op://([^/"]+)/([^"]+)"['"]\\s*\\)\\s*$`, "m");
|
|
201
209
|
const GH_TOKEN_RE = new RegExp(`^\\s*GH_TOKEN\\s*=\\s*${execName()}\\(\\s*['"]op read "op://([^/"]+)/([^"]+)"['"]\\s*\\)\\s*$`, "m");
|
|
202
210
|
function resolveOpRefs(cwd, opts) {
|
|
203
211
|
const schemaPath = join(cwd, ".env.schema");
|
|
204
212
|
const schema = readFileSync(schemaPath, "utf8");
|
|
213
|
+
return parseOpRefs(schema, opts);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Pure schema-string → ResolvedOpRefs parser. Split out from
|
|
217
|
+
* `resolveOpRefs` so it can be unit-tested without touching the
|
|
218
|
+
* filesystem. The regex captures + override / vault-mismatch logic
|
|
219
|
+
* live here; the disk read lives in `resolveOpRefs`.
|
|
220
|
+
*/
|
|
221
|
+
export function parseOpRefs(schema, opts) {
|
|
205
222
|
const anthropicMatch = schema.match(ANTHROPIC_RE);
|
|
206
223
|
const ghTokenMatch = schema.match(GH_TOKEN_RE);
|
|
207
224
|
// Per-field override > extracted > error.
|
|
208
|
-
const opVault = opts.opVault ?? anthropicMatch?.[
|
|
209
|
-
const anthropicItem = opts.anthropicItem ?? anthropicMatch?.[
|
|
225
|
+
const opVault = opts.opVault ?? anthropicMatch?.[2] ?? ghTokenMatch?.[1] ?? null;
|
|
226
|
+
const anthropicItem = opts.anthropicItem ?? anthropicMatch?.[3] ?? null;
|
|
210
227
|
const ghTokenItem = opts.ghTokenItem ?? ghTokenMatch?.[2] ?? null;
|
|
228
|
+
const authMode = anthropicMatch?.[1] === "CLAUDE_CODE_OAUTH_TOKEN" ? "oauth" : "api-key";
|
|
211
229
|
if (!opVault || !anthropicItem || !ghTokenItem) {
|
|
212
230
|
throw new Error("could not parse existing .env.schema; pass --op-vault, --anthropic-item, --gh-token-item explicitly to override.");
|
|
213
231
|
}
|
|
@@ -217,10 +235,10 @@ function resolveOpRefs(cwd, opts) {
|
|
|
217
235
|
if (!opts.opVault &&
|
|
218
236
|
anthropicMatch &&
|
|
219
237
|
ghTokenMatch &&
|
|
220
|
-
anthropicMatch[
|
|
221
|
-
throw new Error(`vault mismatch in .env.schema:
|
|
238
|
+
anthropicMatch[2] !== ghTokenMatch[1]) {
|
|
239
|
+
throw new Error(`vault mismatch in .env.schema: ${anthropicMatch[1]} uses "${anthropicMatch[2]}", GH_TOKEN uses "${ghTokenMatch[1]}". Pass --op-vault to disambiguate.`);
|
|
222
240
|
}
|
|
223
|
-
return { opVault, anthropicItem, ghTokenItem };
|
|
241
|
+
return { opVault, anthropicItem, ghTokenItem, authMode };
|
|
224
242
|
}
|
|
225
243
|
// ---------------------------------------------------------------------------
|
|
226
244
|
// Render: Dockerfile
|
|
@@ -270,15 +288,25 @@ function renderEnvSchema(cwd, resolved) {
|
|
|
270
288
|
const schemaPath = join(cwd, ".env.schema");
|
|
271
289
|
const before = readFileSync(schemaPath, "utf8");
|
|
272
290
|
const tmpl = readFileSync(join(TEMPLATES_DIR, ".env.schema.target-repo"), "utf8");
|
|
291
|
+
const anthropicEnvVar = resolved.authMode === "oauth" ? "CLAUDE_CODE_OAUTH_TOKEN" : "ANTHROPIC_API_KEY";
|
|
273
292
|
let body = tmpl
|
|
274
293
|
.replaceAll("{{OP_VAULT}}", resolved.opVault)
|
|
275
294
|
.replaceAll("{{ANTHROPIC_ITEM}}", resolved.anthropicItem)
|
|
276
|
-
.replaceAll("{{GH_TOKEN_ITEM}}", resolved.ghTokenItem)
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
//
|
|
295
|
+
.replaceAll("{{GH_TOKEN_ITEM}}", resolved.ghTokenItem)
|
|
296
|
+
.replaceAll("{{ANTHROPIC_ENV_VAR}}", anthropicEnvVar);
|
|
297
|
+
// Preserve user-added `KEY=<call>(...)` lines for keys other than the
|
|
298
|
+
// ones we own. Match any `KEY = <execName>(` line in the existing
|
|
299
|
+
// schema and append the whole line if its key isn't the active
|
|
300
|
+
// Claude Code auth var or GH_TOKEN.
|
|
280
301
|
const userExecRe = new RegExp(`^([A-Z_][A-Z0-9_]*)\\s*=\\s*${execName()}\\(`, "gm");
|
|
281
|
-
|
|
302
|
+
// Both possible auth-mode vars are reserved so a user who switches
|
|
303
|
+
// modes doesn't end up with the dead one duplicated as a "preserved"
|
|
304
|
+
// line.
|
|
305
|
+
const ownedKeys = new Set([
|
|
306
|
+
"ANTHROPIC_API_KEY",
|
|
307
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
308
|
+
"GH_TOKEN",
|
|
309
|
+
]);
|
|
282
310
|
const preservedLines = [];
|
|
283
311
|
for (const match of before.matchAll(userExecRe)) {
|
|
284
312
|
const key = match[1];
|
package/dist/config.js
CHANGED
|
@@ -33,6 +33,16 @@ const ConfigSchema = z.object({
|
|
|
33
33
|
* `--project` CLI flag on `runway run`.
|
|
34
34
|
*/
|
|
35
35
|
linearProject: z.string().optional(),
|
|
36
|
+
/**
|
|
37
|
+
* Optional. Override the auto-detected base branch — the branch
|
|
38
|
+
* runway diffs against, opens PRs against, and uses to count
|
|
39
|
+
* agent-branch commits. Source: `RUNWAY_BASE_BRANCH` env var. When
|
|
40
|
+
* unset, runway resolves the default branch from `origin/HEAD` at
|
|
41
|
+
* orchestrator startup. Set this when the repo's default branch is
|
|
42
|
+
* not on the origin (rare) or when you want to target a release
|
|
43
|
+
* branch instead.
|
|
44
|
+
*/
|
|
45
|
+
baseBranch: z.string().optional(),
|
|
36
46
|
readyStatus: z.string().default("Todo"),
|
|
37
47
|
inProgressStatus: z.string().default("In Progress"),
|
|
38
48
|
inReviewStatus: z.string().default("In Review"),
|
|
@@ -45,6 +55,7 @@ export function loadConfig() {
|
|
|
45
55
|
opServiceAccountToken: process.env.OP_SERVICE_ACCOUNT_TOKEN,
|
|
46
56
|
linearTeam: process.env.RUNWAY_LINEAR_TEAM,
|
|
47
57
|
linearProject: process.env.RUNWAY_LINEAR_PROJECT,
|
|
58
|
+
baseBranch: process.env.RUNWAY_BASE_BRANCH,
|
|
48
59
|
readyStatus: process.env.RUNWAY_READY_STATUS,
|
|
49
60
|
inProgressStatus: process.env.RUNWAY_IN_PROGRESS_STATUS,
|
|
50
61
|
inReviewStatus: process.env.RUNWAY_IN_REVIEW_STATUS,
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the default branch name of the cwd repo. Tries
|
|
4
|
+
* `git symbolic-ref` against `origin/HEAD` first (fast, works on any
|
|
5
|
+
* clone where the symbolic ref has been set), then falls back to
|
|
6
|
+
* `git remote show origin` (slower, hits the network but works on
|
|
7
|
+
* fresh clones that never had `origin/HEAD` set locally).
|
|
8
|
+
*
|
|
9
|
+
* Throws if neither path resolves a branch name — better to fail
|
|
10
|
+
* fast at orchestrator startup than to crash mid-diff with a stale
|
|
11
|
+
* "ambiguous argument" git error.
|
|
12
|
+
*/
|
|
13
|
+
export async function detectBaseBranch(repoPath) {
|
|
14
|
+
// Fast path: local symbolic ref. Returns e.g. `origin/main` or `origin/master`.
|
|
15
|
+
try {
|
|
16
|
+
const { stdout, exitCode } = await execa("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], { cwd: repoPath, reject: false });
|
|
17
|
+
if (exitCode === 0) {
|
|
18
|
+
const name = stdout.trim().replace(/^origin\//, "");
|
|
19
|
+
if (name)
|
|
20
|
+
return name;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// fall through to remote-show fallback
|
|
25
|
+
}
|
|
26
|
+
// Slow path: ask the remote. Output line looks like ` HEAD branch: master`.
|
|
27
|
+
try {
|
|
28
|
+
const { stdout } = await execa("git", ["remote", "show", "origin"], {
|
|
29
|
+
cwd: repoPath,
|
|
30
|
+
});
|
|
31
|
+
const match = stdout.match(/^\s*HEAD branch:\s*(\S+)\s*$/m);
|
|
32
|
+
if (match?.[1])
|
|
33
|
+
return match[1];
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// fall through to error
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Could not detect the default branch of ${repoPath}. ` +
|
|
39
|
+
`Set RUNWAY_BASE_BRANCH explicitly, or run ` +
|
|
40
|
+
`\`git remote set-head origin --auto\` to populate origin/HEAD.`);
|
|
41
|
+
}
|
package/dist/github.js
CHANGED
|
@@ -12,13 +12,13 @@ export function createGithubGateway() {
|
|
|
12
12
|
stdio: "inherit",
|
|
13
13
|
});
|
|
14
14
|
},
|
|
15
|
-
async openPullRequest({ repoPath, branch, issue, body }) {
|
|
15
|
+
async openPullRequest({ repoPath, branch, base, issue, body }) {
|
|
16
16
|
const title = `${issue.identifier}: ${issue.title}`;
|
|
17
17
|
const { stdout } = await execa("gh", [
|
|
18
18
|
"pr",
|
|
19
19
|
"create",
|
|
20
20
|
"--base",
|
|
21
|
-
|
|
21
|
+
base,
|
|
22
22
|
"--head",
|
|
23
23
|
branch,
|
|
24
24
|
"--title",
|
package/dist/orchestrator.js
CHANGED
|
@@ -4,6 +4,7 @@ import { run, claudeCode } from "@ai-hero/sandcastle";
|
|
|
4
4
|
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
|
|
5
5
|
import { execa } from "execa";
|
|
6
6
|
import { implementVars, loadImplementPrompt, loadReviewPrompt, renderPrompt, reviewVars, } from "./prompts.js";
|
|
7
|
+
import { detectBaseBranch } from "./git.js";
|
|
7
8
|
const REVIEW_VERDICT_RE = /^REVIEW:\s*(APPROVED|REJECTED)(?:\s+—\s+(.*))?$/m;
|
|
8
9
|
/**
|
|
9
10
|
* Confirms the cwd looks like a sandcastle-initialised repo. If not,
|
|
@@ -27,13 +28,19 @@ export async function drainQueue(deps, opts = {}) {
|
|
|
27
28
|
let opened = 0;
|
|
28
29
|
let hitl = 0;
|
|
29
30
|
let errored = 0;
|
|
31
|
+
// Resolve the base branch once at startup so every issue in the
|
|
32
|
+
// drain sees the same answer (and so a misconfigured repo fails
|
|
33
|
+
// fast, before we touch any Linear state).
|
|
34
|
+
const baseBranch = config.baseBranch ?? (await detectBaseBranch(deps.cwd));
|
|
35
|
+
console.log(`[runway] base branch resolved to "${baseBranch}"`);
|
|
36
|
+
const runDeps = { ...deps, baseBranch };
|
|
30
37
|
while (processed < max) {
|
|
31
38
|
const queue = await linear.fetchReady();
|
|
32
39
|
if (queue.length === 0)
|
|
33
40
|
break;
|
|
34
41
|
const issue = queue[0];
|
|
35
42
|
try {
|
|
36
|
-
const verdict = await processIssue(issue,
|
|
43
|
+
const verdict = await processIssue(issue, runDeps);
|
|
37
44
|
processed += 1;
|
|
38
45
|
if (verdict === "opened")
|
|
39
46
|
opened += 1;
|
|
@@ -50,7 +57,7 @@ export async function drainQueue(deps, opts = {}) {
|
|
|
50
57
|
// can pick it up cleanly. `In Progress` is reserved for "agent
|
|
51
58
|
// has committed to the branch".
|
|
52
59
|
const branch = `agent/${issue.identifier.toLowerCase()}`;
|
|
53
|
-
const startedRealWork = await hasCommits(deps.cwd, branch);
|
|
60
|
+
const startedRealWork = await hasCommits(deps.cwd, baseBranch, branch);
|
|
54
61
|
if (!startedRealWork) {
|
|
55
62
|
await linear
|
|
56
63
|
.transition(issue.id, config.readyStatus)
|
|
@@ -72,7 +79,7 @@ export async function drainQueue(deps, opts = {}) {
|
|
|
72
79
|
return { processed, opened, hitl, errored };
|
|
73
80
|
}
|
|
74
81
|
async function processIssue(issue, deps) {
|
|
75
|
-
const { config, linear, github, cwd } = deps;
|
|
82
|
+
const { config, linear, github, cwd, baseBranch } = deps;
|
|
76
83
|
const branch = `agent/${issue.identifier.toLowerCase()}`;
|
|
77
84
|
await linear.transition(issue.id, config.inProgressStatus);
|
|
78
85
|
await linear.comment(issue.id, `Runway picked up this issue. Branch: \`${branch}\`.`);
|
|
@@ -94,8 +101,8 @@ async function processIssue(issue, deps) {
|
|
|
94
101
|
return "hitl";
|
|
95
102
|
}
|
|
96
103
|
// 2. Review pass — read-only-ish, just looking at the diff.
|
|
97
|
-
const diff = await captureDiff(cwd, branch);
|
|
98
|
-
const commitLog = await captureCommitLog(cwd, branch);
|
|
104
|
+
const diff = await captureDiff(cwd, baseBranch, branch);
|
|
105
|
+
const commitLog = await captureCommitLog(cwd, baseBranch, branch);
|
|
99
106
|
const reviewPrompt = renderPrompt(await loadReviewPrompt(), reviewVars({ issue, diff, commits: commitLog }));
|
|
100
107
|
const reviewResult = await run({
|
|
101
108
|
agent: claudeCode("claude-opus-4-6"),
|
|
@@ -119,6 +126,7 @@ async function processIssue(issue, deps) {
|
|
|
119
126
|
const prUrl = await github.openPullRequest({
|
|
120
127
|
repoPath: cwd,
|
|
121
128
|
branch,
|
|
129
|
+
base: baseBranch,
|
|
122
130
|
issue,
|
|
123
131
|
body: prBody,
|
|
124
132
|
});
|
|
@@ -132,30 +140,30 @@ async function flagHitl(issue, deps, reason) {
|
|
|
132
140
|
await linear.comment(issue.id, `Runway flagged for human review: ${reason}`);
|
|
133
141
|
}
|
|
134
142
|
/**
|
|
135
|
-
* Whether the agent branch has any commits beyond `
|
|
143
|
+
* Whether the agent branch has any commits beyond `base`. Used by the
|
|
136
144
|
* drain loop to distinguish "agent crashed mid-run, after producing
|
|
137
145
|
* real work" (→ HITL) from "agent crashed during startup, no work
|
|
138
146
|
* done" (→ revert to Todo). If the branch doesn't exist or git fails,
|
|
139
147
|
* treat as "no commits" so we revert rather than strand the issue.
|
|
140
148
|
*/
|
|
141
|
-
async function hasCommits(repoPath, branch) {
|
|
149
|
+
async function hasCommits(repoPath, base, branch) {
|
|
142
150
|
try {
|
|
143
|
-
const { stdout } = await execa("git", ["rev-list", "--count",
|
|
151
|
+
const { stdout } = await execa("git", ["rev-list", "--count", `${base}..${branch}`], { cwd: repoPath, reject: false });
|
|
144
152
|
return Number.parseInt(stdout.trim(), 10) > 0;
|
|
145
153
|
}
|
|
146
154
|
catch {
|
|
147
155
|
return false;
|
|
148
156
|
}
|
|
149
157
|
}
|
|
150
|
-
async function captureDiff(repoPath, branch) {
|
|
151
|
-
const { stdout } = await execa("git", ["diff",
|
|
158
|
+
async function captureDiff(repoPath, base, branch) {
|
|
159
|
+
const { stdout } = await execa("git", ["diff", `${base}...${branch}`], {
|
|
152
160
|
cwd: repoPath,
|
|
153
161
|
});
|
|
154
162
|
// Truncate to keep the review prompt under the model's context budget.
|
|
155
163
|
return stdout.length > 60_000 ? `${stdout.slice(0, 60_000)}\n…(truncated)` : stdout;
|
|
156
164
|
}
|
|
157
|
-
async function captureCommitLog(repoPath, branch) {
|
|
158
|
-
const { stdout } = await execa("git", ["log", "--oneline",
|
|
165
|
+
async function captureCommitLog(repoPath, base, branch) {
|
|
166
|
+
const { stdout } = await execa("git", ["log", "--oneline", `${base}..${branch}`], { cwd: repoPath });
|
|
159
167
|
return stdout;
|
|
160
168
|
}
|
|
161
169
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valescoagency/runway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Linear-driven orchestrator + scaffolder for coding agents on Sandcastle. `runway init` scaffolds a target repo (sandcastle + varlock + 1Password); `runway run` drains a Linear queue against it; `runway doctor`, `runway upgrade`, `runway upgrade-repo` round out the lifecycle.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -45,9 +45,13 @@
|
|
|
45
45
|
"zod": "^3.23.8"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
+
"@commitlint/cli": "^21.0.0",
|
|
49
|
+
"@commitlint/config-conventional": "^21.0.0",
|
|
48
50
|
"@types/node": "^22.10.0",
|
|
51
|
+
"lefthook": "^2.1.6",
|
|
49
52
|
"tsx": "^4.19.2",
|
|
50
|
-
"typescript": "^5.7.2"
|
|
53
|
+
"typescript": "^5.7.2",
|
|
54
|
+
"vitest": "^4.1.5"
|
|
51
55
|
},
|
|
52
56
|
"engines": {
|
|
53
57
|
"node": ">=22"
|
|
@@ -56,9 +60,11 @@
|
|
|
56
60
|
"access": "public"
|
|
57
61
|
},
|
|
58
62
|
"scripts": {
|
|
59
|
-
"build": "tsc && chmod +x dist/cli.js",
|
|
63
|
+
"build": "tsc -p tsconfig.build.json && chmod +x dist/cli.js",
|
|
60
64
|
"typecheck": "tsc --noEmit",
|
|
61
65
|
"dev": "tsx src/cli.ts",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"test:watch": "vitest",
|
|
62
68
|
"lint": "echo 'lint not configured yet'"
|
|
63
69
|
}
|
|
64
70
|
}
|
|
@@ -18,13 +18,20 @@
|
|
|
18
18
|
# `op://<account>/<vault>/<item>/<field>`. For API_CREDENTIAL items
|
|
19
19
|
# (the natural category for API keys), the field is `credential`.
|
|
20
20
|
#
|
|
21
|
+
# Note on Claude Code auth: ANTHROPIC_API_KEY is a pay-per-token API
|
|
22
|
+
# key (sk-ant-api03-…). CLAUDE_CODE_OAUTH_TOKEN is a Pro/Max
|
|
23
|
+
# subscription token from `claude setup-token` (sk-ant-oat01-…). They
|
|
24
|
+
# are NOT interchangeable. Runway init writes whichever the user
|
|
25
|
+
# selected with --auth-mode; see runway's README "Claude Code auth"
|
|
26
|
+
# section for details.
|
|
27
|
+
#
|
|
21
28
|
# To add another secret, copy one of the two live entries below. Do
|
|
22
29
|
# NOT leave a commented-out example block here: varlock parses any
|
|
23
30
|
# `# @decorator` line as a real decorator, and a decorator with no
|
|
24
31
|
# attached config line fails validation ("detached comment block").
|
|
25
32
|
|
|
26
33
|
# @sensitive @required
|
|
27
|
-
|
|
34
|
+
{{ANTHROPIC_ENV_VAR}}=exec('op read "op://{{OP_VAULT}}/{{ANTHROPIC_ITEM}}/credential"')
|
|
28
35
|
|
|
29
36
|
# @sensitive @required
|
|
30
37
|
GH_TOKEN=exec('op read "op://{{OP_VAULT}}/{{GH_TOKEN_ITEM}}/credential"')
|