@valescoagency/runway 0.1.2 → 0.2.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 +11 -3
- package/dist/commands/doctor.js +10 -0
- package/dist/commands/init.js +4 -2
- package/dist/commands/run.js +22 -2
- package/dist/config.js +9 -0
- package/dist/linear.js +25 -0
- package/dist/orchestrator.js +40 -6
- package/package.json +1 -1
- package/templates/.env.schema.target-repo +10 -12
package/README.md
CHANGED
|
@@ -77,7 +77,9 @@ runway init \
|
|
|
77
77
|
(No `--op-account` — runway uses 1Password service-account auth
|
|
78
78
|
(`OP_SERVICE_ACCOUNT_TOKEN`) exclusively, and the token already
|
|
79
79
|
encodes the tenant. `op://` URIs runway writes are
|
|
80
|
-
`op://<vault>/<item
|
|
80
|
+
`op://<vault>/<item>/credential`, not `op://<account>/<vault>/<item>`.
|
|
81
|
+
The `/credential` field selector is required for `API_CREDENTIAL`
|
|
82
|
+
items, which is the canonical 1Password category for API keys.)
|
|
81
83
|
|
|
82
84
|
This runs `npx sandcastle init`, patches the generated `.sandcastle/Dockerfile`
|
|
83
85
|
to bake in `varlock` + the 1Password CLI + a `claude` shim, scaffolds
|
|
@@ -118,6 +120,7 @@ Export runway's own env (in your shell rc, or wherever you keep API keys):
|
|
|
118
120
|
export LINEAR_API_KEY=lin_api_...
|
|
119
121
|
# Optional overrides:
|
|
120
122
|
# export RUNWAY_LINEAR_TEAM=VA
|
|
123
|
+
# export RUNWAY_LINEAR_PROJECT=<project-id-or-slug> # optional, scopes queue to one project
|
|
121
124
|
# export RUNWAY_READY_STATUS="Todo"
|
|
122
125
|
# export RUNWAY_IN_PROGRESS_STATUS="In Progress"
|
|
123
126
|
# export RUNWAY_IN_REVIEW_STATUS="In Review"
|
|
@@ -157,13 +160,18 @@ per-issue comments for what happened.
|
|
|
157
160
|
Runway picks up issues that are:
|
|
158
161
|
|
|
159
162
|
- in team `RUNWAY_LINEAR_TEAM` (default `VA`)
|
|
163
|
+
- (optionally) in project `RUNWAY_LINEAR_PROJECT` (override per-run
|
|
164
|
+
with `runway run --project=<id-or-slug-or-name>`; unset = team-wide)
|
|
160
165
|
- in workflow state `RUNWAY_READY_STATUS` (default `Todo`)
|
|
161
166
|
|
|
162
167
|
It transitions them through:
|
|
163
168
|
|
|
164
|
-
- `In Progress` while the agent is running
|
|
169
|
+
- `In Progress` while the agent is running (specifically: once the
|
|
170
|
+
agent has committed to its branch — startup failures before any
|
|
171
|
+
commits revert the issue back to `Todo` rather than stranding it)
|
|
165
172
|
- `In Review` when the PR opens
|
|
166
|
-
- (label `needs-human`) if the agent or reviewer can't finish
|
|
173
|
+
- (label `needs-human`) if the agent or reviewer can't finish *after*
|
|
174
|
+
the agent has committed real work
|
|
167
175
|
|
|
168
176
|
These names are configurable per env var; the queries match by name so
|
|
169
177
|
your Linear workspace's actual state names need to line up with what
|
package/dist/commands/doctor.js
CHANGED
|
@@ -232,6 +232,16 @@ async function checkGhAuth() {
|
|
|
232
232
|
function checkEnvironment(tier) {
|
|
233
233
|
const checks = new Map();
|
|
234
234
|
checks.set("LINEAR_API_KEY", envSet("LINEAR_API_KEY", "fail"));
|
|
235
|
+
// Informational: which Linear scope a `runway run` would use.
|
|
236
|
+
const team = process.env.RUNWAY_LINEAR_TEAM?.trim() || "VA";
|
|
237
|
+
const project = process.env.RUNWAY_LINEAR_PROJECT?.trim();
|
|
238
|
+
checks.set("linear_scope", {
|
|
239
|
+
status: "ok",
|
|
240
|
+
label: "linear scope",
|
|
241
|
+
detail: project
|
|
242
|
+
? `team ${team} / project ${project}`
|
|
243
|
+
: `team ${team} (team-wide — RUNWAY_LINEAR_PROJECT unset)`,
|
|
244
|
+
});
|
|
235
245
|
if (tier === 2) {
|
|
236
246
|
// Tier 2: needed by varlock to resolve op:// refs in the container.
|
|
237
247
|
checks.set("OP_SERVICE_ACCOUNT_TOKEN", envSet("OP_SERVICE_ACCOUNT_TOKEN", "fail"));
|
package/dist/commands/init.js
CHANGED
|
@@ -35,8 +35,10 @@ NOTE
|
|
|
35
35
|
No --op-account flag — runway uses 1Password service-account auth
|
|
36
36
|
exclusively (OP_SERVICE_ACCOUNT_TOKEN). The token already encodes
|
|
37
37
|
which 1Password tenant to talk to, so the op:// URI omits the
|
|
38
|
-
account segment: \`op://<vault>/<item>\` rather than
|
|
39
|
-
\`op://<account>/<vault>/<item>\`.
|
|
38
|
+
account segment: \`op://<vault>/<item>/<field>\` rather than
|
|
39
|
+
\`op://<account>/<vault>/<item>/<field>\`. Runway hard-codes the
|
|
40
|
+
\`credential\` field, which is the canonical field name on
|
|
41
|
+
1Password API_CREDENTIAL items.
|
|
40
42
|
|
|
41
43
|
WHAT THIS COMMAND DOES
|
|
42
44
|
1. Preflight: docker, gh, node, (tier 2) varlock + op CLI, git state.
|
package/dist/commands/run.js
CHANGED
|
@@ -17,6 +17,16 @@ function parseRunArgs(argv) {
|
|
|
17
17
|
opts.max = n;
|
|
18
18
|
i += 1;
|
|
19
19
|
}
|
|
20
|
+
else if (a === "--project") {
|
|
21
|
+
const v = argv[i + 1];
|
|
22
|
+
if (!v)
|
|
23
|
+
throw new Error("--project requires a value");
|
|
24
|
+
opts.project = v;
|
|
25
|
+
i += 1;
|
|
26
|
+
}
|
|
27
|
+
else if (a?.startsWith("--project=")) {
|
|
28
|
+
opts.project = a.slice("--project=".length);
|
|
29
|
+
}
|
|
20
30
|
else if (a === "--help" || a === "-h") {
|
|
21
31
|
printRunUsage();
|
|
22
32
|
process.exit(0);
|
|
@@ -36,11 +46,15 @@ USAGE
|
|
|
36
46
|
|
|
37
47
|
OPTIONS
|
|
38
48
|
--max, -n N Process at most N issues then exit. Default: drain queue.
|
|
49
|
+
--project ID Scope the queue to a single Linear project under the
|
|
50
|
+
team. Accepts project UUID, slug, or name. Overrides
|
|
51
|
+
RUNWAY_LINEAR_PROJECT. Default: team-wide.
|
|
39
52
|
--help, -h Show this help.
|
|
40
53
|
|
|
41
54
|
ENVIRONMENT
|
|
42
55
|
LINEAR_API_KEY required
|
|
43
56
|
RUNWAY_LINEAR_TEAM default "VA"
|
|
57
|
+
RUNWAY_LINEAR_PROJECT optional — scope to one project
|
|
44
58
|
RUNWAY_READY_STATUS default "Todo"
|
|
45
59
|
RUNWAY_IN_PROGRESS_STATUS default "In Progress"
|
|
46
60
|
RUNWAY_IN_REVIEW_STATUS default "In Review"
|
|
@@ -52,10 +66,16 @@ export async function runCommand(argv) {
|
|
|
52
66
|
const opts = parseRunArgs(argv);
|
|
53
67
|
const cwd = process.cwd();
|
|
54
68
|
assertSandcastleInitialised(cwd);
|
|
55
|
-
const
|
|
69
|
+
const baseConfig = loadConfig();
|
|
70
|
+
const config = opts.project
|
|
71
|
+
? { ...baseConfig, linearProject: opts.project }
|
|
72
|
+
: baseConfig;
|
|
56
73
|
const linear = createLinearGateway(config);
|
|
57
74
|
const github = createGithubGateway();
|
|
58
|
-
|
|
75
|
+
const scope = config.linearProject
|
|
76
|
+
? `team ${config.linearTeam} / project ${config.linearProject}`
|
|
77
|
+
: `team ${config.linearTeam}`;
|
|
78
|
+
console.log(`[runway] draining queue from ${scope} (status="${config.readyStatus}") against ${cwd}`);
|
|
59
79
|
const result = await drainQueue({ config, linear, github, cwd }, { max: opts.max });
|
|
60
80
|
console.log(`[runway] done — processed=${result.processed} opened=${result.opened} hitl=${result.hitl} errored=${result.errored}`);
|
|
61
81
|
}
|
package/dist/config.js
CHANGED
|
@@ -25,6 +25,14 @@ const ConfigSchema = z.object({
|
|
|
25
25
|
*/
|
|
26
26
|
opServiceAccountToken: z.string().optional(),
|
|
27
27
|
linearTeam: z.string().default("VA"),
|
|
28
|
+
/**
|
|
29
|
+
* Optional. Scopes the `runway run` queue to a single project under
|
|
30
|
+
* `linearTeam`. Resolved by Linear project ID, slug, or name. When
|
|
31
|
+
* unset, runway drains every `Todo` issue on the team (legacy
|
|
32
|
+
* behavior). Source: `RUNWAY_LINEAR_PROJECT` env var or
|
|
33
|
+
* `--project` CLI flag on `runway run`.
|
|
34
|
+
*/
|
|
35
|
+
linearProject: z.string().optional(),
|
|
28
36
|
readyStatus: z.string().default("Todo"),
|
|
29
37
|
inProgressStatus: z.string().default("In Progress"),
|
|
30
38
|
inReviewStatus: z.string().default("In Review"),
|
|
@@ -36,6 +44,7 @@ export function loadConfig() {
|
|
|
36
44
|
linearApiKey: process.env.LINEAR_API_KEY,
|
|
37
45
|
opServiceAccountToken: process.env.OP_SERVICE_ACCOUNT_TOKEN,
|
|
38
46
|
linearTeam: process.env.RUNWAY_LINEAR_TEAM,
|
|
47
|
+
linearProject: process.env.RUNWAY_LINEAR_PROJECT,
|
|
39
48
|
readyStatus: process.env.RUNWAY_READY_STATUS,
|
|
40
49
|
inProgressStatus: process.env.RUNWAY_IN_PROGRESS_STATUS,
|
|
41
50
|
inReviewStatus: process.env.RUNWAY_IN_REVIEW_STATUS,
|
package/dist/linear.js
CHANGED
|
@@ -25,14 +25,39 @@ export function createLinearGateway(config) {
|
|
|
25
25
|
}
|
|
26
26
|
return team.id;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a project identifier (UUID, slug, or name) to its Linear
|
|
30
|
+
* project ID. Tries each shape in order so user-facing flags like
|
|
31
|
+
* `--project=bedrock` work without forcing users to copy the UUID.
|
|
32
|
+
*/
|
|
33
|
+
async function findProjectId(identifier) {
|
|
34
|
+
const projects = await client.projects({
|
|
35
|
+
filter: {
|
|
36
|
+
or: [
|
|
37
|
+
{ id: { eq: identifier } },
|
|
38
|
+
{ slugId: { eq: identifier } },
|
|
39
|
+
{ name: { eq: identifier } },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const project = projects.nodes[0];
|
|
44
|
+
if (!project) {
|
|
45
|
+
throw new Error(`Linear project "${identifier}" not found`);
|
|
46
|
+
}
|
|
47
|
+
return project.id;
|
|
48
|
+
}
|
|
28
49
|
return {
|
|
29
50
|
async fetchReady() {
|
|
30
51
|
const teamId = await findTeamId();
|
|
31
52
|
const readyStateId = await findStateId(teamId, config.readyStatus);
|
|
53
|
+
const projectId = config.linearProject
|
|
54
|
+
? await findProjectId(config.linearProject)
|
|
55
|
+
: null;
|
|
32
56
|
const issues = await client.issues({
|
|
33
57
|
filter: {
|
|
34
58
|
team: { id: { eq: teamId } },
|
|
35
59
|
state: { id: { eq: readyStateId } },
|
|
60
|
+
...(projectId ? { project: { id: { eq: projectId } } } : {}),
|
|
36
61
|
},
|
|
37
62
|
// Stable order: oldest first so the queue drains FIFO.
|
|
38
63
|
orderBy: "createdAt",
|
package/dist/orchestrator.js
CHANGED
|
@@ -43,12 +43,30 @@ export async function drainQueue(deps, opts = {}) {
|
|
|
43
43
|
catch (err) {
|
|
44
44
|
errored += 1;
|
|
45
45
|
console.error(`[runway] error on ${issue.identifier}:`, err);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
// If the agent crashed before producing any commits (missing
|
|
47
|
+
// image, varlock validation, container failed to boot, etc.),
|
|
48
|
+
// it's an infrastructure failure — not a HITL. Revert the issue
|
|
49
|
+
// to `Todo` and skip the `needs-human` label so the next run
|
|
50
|
+
// can pick it up cleanly. `In Progress` is reserved for "agent
|
|
51
|
+
// has committed to the branch".
|
|
52
|
+
const branch = `agent/${issue.identifier.toLowerCase()}`;
|
|
53
|
+
const startedRealWork = await hasCommits(deps.cwd, branch);
|
|
54
|
+
if (!startedRealWork) {
|
|
55
|
+
await linear
|
|
56
|
+
.transition(issue.id, config.readyStatus)
|
|
57
|
+
.catch(() => undefined);
|
|
58
|
+
await linear
|
|
59
|
+
.comment(issue.id, `Runway hit a startup failure before the agent produced any commits — reverting to \`${config.readyStatus}\` for retry:\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\``)
|
|
60
|
+
.catch(() => undefined);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
await linear
|
|
64
|
+
.applyLabel(issue.id, config.hitlLabel)
|
|
65
|
+
.catch(() => undefined);
|
|
66
|
+
await linear
|
|
67
|
+
.comment(issue.id, `Runway hit an unrecoverable error and flagged for human review:\n\n\`\`\`\n${err instanceof Error ? err.message : String(err)}\n\`\`\``)
|
|
68
|
+
.catch(() => undefined);
|
|
69
|
+
}
|
|
52
70
|
}
|
|
53
71
|
}
|
|
54
72
|
return { processed, opened, hitl, errored };
|
|
@@ -113,6 +131,22 @@ async function flagHitl(issue, deps, reason) {
|
|
|
113
131
|
await linear.applyLabel(issue.id, config.hitlLabel);
|
|
114
132
|
await linear.comment(issue.id, `Runway flagged for human review: ${reason}`);
|
|
115
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Whether the agent branch has any commits beyond `main`. Used by the
|
|
136
|
+
* drain loop to distinguish "agent crashed mid-run, after producing
|
|
137
|
+
* real work" (→ HITL) from "agent crashed during startup, no work
|
|
138
|
+
* done" (→ revert to Todo). If the branch doesn't exist or git fails,
|
|
139
|
+
* treat as "no commits" so we revert rather than strand the issue.
|
|
140
|
+
*/
|
|
141
|
+
async function hasCommits(repoPath, branch) {
|
|
142
|
+
try {
|
|
143
|
+
const { stdout } = await execa("git", ["rev-list", "--count", `main..${branch}`], { cwd: repoPath, reject: false });
|
|
144
|
+
return Number.parseInt(stdout.trim(), 10) > 0;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
116
150
|
async function captureDiff(repoPath, branch) {
|
|
117
151
|
const { stdout } = await execa("git", ["diff", `main...${branch}`], {
|
|
118
152
|
cwd: repoPath,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valescoagency/runway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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": {
|
|
@@ -14,19 +14,17 @@
|
|
|
14
14
|
#
|
|
15
15
|
# Note on the op:// shape: with service-account auth (the only mode
|
|
16
16
|
# runway uses), the token already encodes the 1Password tenant, so the
|
|
17
|
-
# URI omits the account segment — `op://<vault>/<item>`, not
|
|
18
|
-
# `op://<account>/<vault>/<item>`.
|
|
19
|
-
|
|
20
|
-
#
|
|
21
|
-
|
|
17
|
+
# URI omits the account segment — `op://<vault>/<item>/<field>`, not
|
|
18
|
+
# `op://<account>/<vault>/<item>/<field>`. For API_CREDENTIAL items
|
|
19
|
+
# (the natural category for API keys), the field is `credential`.
|
|
20
|
+
#
|
|
21
|
+
# To add another secret, copy one of the two live entries below. Do
|
|
22
|
+
# NOT leave a commented-out example block here: varlock parses any
|
|
23
|
+
# `# @decorator` line as a real decorator, and a decorator with no
|
|
24
|
+
# attached config line fails validation ("detached comment block").
|
|
22
25
|
|
|
23
26
|
# @sensitive @required
|
|
24
|
-
|
|
27
|
+
ANTHROPIC_API_KEY=exec('op read "op://{{OP_VAULT}}/{{ANTHROPIC_ITEM}}/credential"')
|
|
25
28
|
|
|
26
|
-
# Add other secrets the agent needs at runtime here. Examples:
|
|
27
|
-
#
|
|
28
|
-
# @sensitive @required
|
|
29
|
-
# OPENAI_API_KEY=exec('op read "op://{{OP_VAULT}}/openai-api-key"')
|
|
30
|
-
#
|
|
31
29
|
# @sensitive @required
|
|
32
|
-
|
|
30
|
+
GH_TOKEN=exec('op read "op://{{OP_VAULT}}/{{GH_TOKEN_ITEM}}/credential"')
|