@valescoagency/runway 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -7
- package/dist/cli.js +7 -0
- package/dist/commands/planner.js +265 -0
- package/dist/commands/run.js +35 -3
- package/dist/config.js +22 -17
- package/dist/dashboard/projector.js +175 -88
- package/dist/dashboard/server.js +226 -44
- package/dist/dashboard/storage.js +233 -0
- package/dist/dashboard/views.js +112 -47
- package/dist/orchestrator.js +19 -45
- package/dist/planner-capability-mode.js +42 -0
- package/dist/planner-ci.js +283 -0
- package/dist/planner-drain.js +224 -0
- package/dist/planner-http.js +374 -0
- package/dist/planner-mergeability.js +344 -0
- package/dist/planner-queue-selection.js +104 -0
- package/dist/planner-reviewer.js +433 -0
- package/dist/planner-storage.js +339 -0
- package/dist/planner.js +150 -0
- package/package.json +1 -1
- package/templates/com.valescoagency.runway-planner.plist +88 -0
- package/dist/shepherd.js +0 -707
package/README.md
CHANGED
|
@@ -8,13 +8,14 @@ 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
|
+
## Eight commands
|
|
12
12
|
|
|
13
13
|
| | |
|
|
14
14
|
|---|---|
|
|
15
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. |
|
|
16
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. |
|
|
17
17
|
| `runway init` | Scaffold the cwd repo for runway: write `.sandcastle/Dockerfile` + (tier 2) `.env.schema` with op:// references. Run **once per target repo**. |
|
|
18
|
+
| `runway planner` | Run the planner daemon — a long-running host process that will own cross-PR shepherding and queue selection per [ADR 0004](docs/adr/0004-planner-daemon-architecture.md). VA-472 epic in flight; VA-473 ships the foreground scaffold (`runway planner run` boots a tick loop, no capabilities wired yet). |
|
|
18
19
|
| `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
20
|
| `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**. |
|
|
20
21
|
| `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. |
|
|
@@ -218,13 +219,18 @@ export LINEAR_API_KEY=lin_api_...
|
|
|
218
219
|
# export RUNWAY_READY_LABEL="ready-for-agent"
|
|
219
220
|
# export RUNWAY_HITL_LABEL="ready-for-human"
|
|
220
221
|
# export RUNWAY_MAX_ITERATIONS=5
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
222
|
+
# VA-482: the four RUNWAY_SHEPHERD_* env vars have been retired.
|
|
223
|
+
# Mergeability / CI / reviewer-feedback work now flows through the
|
|
224
|
+
# `runway planner` daemon (capabilities VA-477..479). Boot the
|
|
225
|
+
# planner separately (`runway planner run`) and configure it with
|
|
226
|
+
# the RUNWAY_PLANNER_* env vars; the planner reads from the
|
|
227
|
+
# dashboard's SQLite file and emits OTLP back to the dashboard.
|
|
225
228
|
# export RUNWAY_PR_REVIEWER_BOT_LOGIN="runway-reviewer-bot"
|
|
226
|
-
# VA-463: GitHub login of the adversarial PR-reviewer
|
|
227
|
-
#
|
|
229
|
+
# VA-463 / VA-479: GitHub login of the adversarial PR-reviewer
|
|
230
|
+
# agent. Consumed by the planner's reviewer dispatcher to
|
|
231
|
+
# distinguish bot CHANGES-REQUESTED comments from human feedback
|
|
232
|
+
# (the latter routes through the LLM interpretation path).
|
|
233
|
+
# Unset → every comment lands on the human-feedback path.
|
|
228
234
|
# export RUNWAY_COMMENT_AUTHOR_ALLOWLIST="Reviewer Bot,Jane Reviewer"
|
|
229
235
|
# optional, comma-separated Linear user names whose comments on a
|
|
230
236
|
# re-queued issue surface as "Review feedback from prior attempts"
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { dashCommand, printDashUsage } from "./commands/dash.js";
|
|
3
3
|
import { doctorCommand, printDoctorUsage } from "./commands/doctor.js";
|
|
4
4
|
import { initCommand, printInitUsage } from "./commands/init.js";
|
|
5
|
+
import { plannerCommand, printPlannerUsage } from "./commands/planner.js";
|
|
5
6
|
import { reviewCommand, printReviewUsage } from "./commands/review.js";
|
|
6
7
|
import { runCommand, printRunUsage } from "./commands/run.js";
|
|
7
8
|
import { upgradeCommand, printUpgradeUsage } from "./commands/upgrade.js";
|
|
@@ -25,6 +26,12 @@ const SUBCOMMANDS = [
|
|
|
25
26
|
run: initCommand,
|
|
26
27
|
help: printInitUsage,
|
|
27
28
|
},
|
|
29
|
+
{
|
|
30
|
+
name: "planner",
|
|
31
|
+
summary: "Run the planner daemon (queue selection + PR shepherding).",
|
|
32
|
+
run: plannerCommand,
|
|
33
|
+
help: printPlannerUsage,
|
|
34
|
+
},
|
|
28
35
|
{
|
|
29
36
|
name: "review",
|
|
30
37
|
summary: "Run an IRA retrospective pass (run / drain / weekly).",
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { Deferred, Effect, Layer, Logger, Runtime } from "effect";
|
|
5
|
+
import { ConfigLive, ConfigTag } from "../config.js";
|
|
6
|
+
import { installSignalHandlers, makeHealthReader, makePlannerLoop, makePlannerState, } from "../planner.js";
|
|
7
|
+
import { PLANNER_CAPABILITY_MODE_CHANGED_LOG, PLANNER_RECOMMENDATION_ACCEPTED_LOG, PLANNER_RECOMMENDATION_REJECTED_LOG, } from "../dashboard/projector.js";
|
|
8
|
+
import { createGithubGateway } from "../github.js";
|
|
9
|
+
import { startPlannerHttpServer, } from "../planner-http.js";
|
|
10
|
+
import { makeAnthropicCommentInterpreter } from "../planner-reviewer.js";
|
|
11
|
+
import { createPlannerReadClient } from "../planner-storage.js";
|
|
12
|
+
import { TelemetryLive } from "../telemetry.js";
|
|
13
|
+
export function printPlannerUsage() {
|
|
14
|
+
console.log(`runway planner — long-running daemon for queue selection and PR shepherding
|
|
15
|
+
|
|
16
|
+
Sub-1 scaffold (VA-473): boots the tick loop with no capabilities wired.
|
|
17
|
+
Capabilities arrive in VA-474..VA-482 per ADR 0004.
|
|
18
|
+
|
|
19
|
+
USAGE
|
|
20
|
+
runway planner run
|
|
21
|
+
|
|
22
|
+
VERBS
|
|
23
|
+
run Boot the planner in the foreground. Emits a planner.tick
|
|
24
|
+
heartbeat each RUNWAY_PLANNER_TICK_SECONDS. SIGINT / SIGTERM
|
|
25
|
+
triggers graceful shutdown (OTLP flush, then exit).
|
|
26
|
+
|
|
27
|
+
OPTIONS
|
|
28
|
+
--help, -h Show this help.
|
|
29
|
+
|
|
30
|
+
ENDPOINTS (VA-475 / VA-481)
|
|
31
|
+
GET /planner/health liveness probe
|
|
32
|
+
GET /planner/recommendations[?status=&limit=N] list (status: proposed|accepted|rejected|all)
|
|
33
|
+
GET /planner/recommendations/:id single
|
|
34
|
+
POST /planner/recommendations/:id/accept emits accepted
|
|
35
|
+
POST /planner/recommendations/:id/reject emits rejected (requires { reason })
|
|
36
|
+
GET /planner/actions[?status=] list (status: started|completed|failed|all)
|
|
37
|
+
GET /planner/capabilities per-capability modes (VA-481)
|
|
38
|
+
PUT /planner/capabilities/:capability body { mode: "manual"|"suggest"|"auto" }
|
|
39
|
+
|
|
40
|
+
Auth is network-layer (Tailscale ACLs). Same-origin-only CORS —
|
|
41
|
+
browsers at a different origin are rejected; route through the
|
|
42
|
+
dashboard's backend.
|
|
43
|
+
|
|
44
|
+
ENVIRONMENT
|
|
45
|
+
RUNWAY_PLANNER_TICK_SECONDS default 30 — tick cadence.
|
|
46
|
+
RUNWAY_PLANNER_HOST default 127.0.0.1 — HTTP bind host.
|
|
47
|
+
Set to a tailnet IP to expose the API
|
|
48
|
+
across your Tailscale network.
|
|
49
|
+
RUNWAY_PLANNER_PORT default 3002 — HTTP bind port.
|
|
50
|
+
RUNWAY_PLANNER_SQLITE_PATH optional — path to the dashboard's
|
|
51
|
+
SQLite file (the planner opens it
|
|
52
|
+
read-only). When unset, GET endpoints
|
|
53
|
+
return 503; POST endpoints still emit
|
|
54
|
+
OTLP (the dashboard validates on
|
|
55
|
+
receive).
|
|
56
|
+
|
|
57
|
+
LINEAR_API_KEY required (planner reads the queue).
|
|
58
|
+
OTEL_EXPORTER_OTLP_ENDPOINT optional — when set, planner emissions
|
|
59
|
+
stream to the dashboard's OTLP receiver.
|
|
60
|
+
|
|
61
|
+
SUPERVISION
|
|
62
|
+
Mac mini operators: install the bundled launchd plist at
|
|
63
|
+
~/Library/LaunchAgents/com.valescoagency.runway-planner.plist
|
|
64
|
+
(template ships at templates/com.valescoagency.runway-planner.plist
|
|
65
|
+
in the runway package) and run \`launchctl load\` once. launchd
|
|
66
|
+
restarts the planner on crash and on login.
|
|
67
|
+
|
|
68
|
+
REFERENCES
|
|
69
|
+
ADR 0004 — Planner daemon architecture.
|
|
70
|
+
Epic VA-472 — Planner daemon (replatform shepherd).
|
|
71
|
+
`);
|
|
72
|
+
}
|
|
73
|
+
export function parsePlannerArgs(argv) {
|
|
74
|
+
if (argv.length === 0) {
|
|
75
|
+
throw new Error("missing verb — expected `run`. Run `runway planner --help`.");
|
|
76
|
+
}
|
|
77
|
+
const [verbRaw, ...rest] = argv;
|
|
78
|
+
if (verbRaw === "--help" || verbRaw === "-h") {
|
|
79
|
+
printPlannerUsage();
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
if (verbRaw !== "run") {
|
|
83
|
+
throw new Error(`unknown verb "${verbRaw}" — expected \`run\`. Run \`runway planner --help\`.`);
|
|
84
|
+
}
|
|
85
|
+
for (const arg of rest) {
|
|
86
|
+
if (arg === "--help" || arg === "-h") {
|
|
87
|
+
printPlannerUsage();
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
91
|
+
}
|
|
92
|
+
return { verb: "run" };
|
|
93
|
+
}
|
|
94
|
+
export async function plannerCommand(argv) {
|
|
95
|
+
parsePlannerArgs(argv);
|
|
96
|
+
const LoggerLive = process.env.RUNWAY_JSON_LOGS === "1"
|
|
97
|
+
? Logger.replace(Logger.defaultLogger, Logger.jsonLogger)
|
|
98
|
+
: Layer.empty;
|
|
99
|
+
const MainLayer = Layer.mergeAll(ConfigLive, TelemetryLive, LoggerLive);
|
|
100
|
+
const program = Effect.gen(function* () {
|
|
101
|
+
const config = yield* ConfigTag;
|
|
102
|
+
yield* Effect.logInfo(`[runway planner] boot host=${config.plannerHost} port=${config.plannerPort} tick=${config.plannerTickSeconds}s sqlite=${config.plannerSqlitePath ?? "(unset — GET endpoints will 503)"}`);
|
|
103
|
+
const shutdownSignal = yield* Deferred.make();
|
|
104
|
+
yield* installSignalHandlers(shutdownSignal);
|
|
105
|
+
const stateRef = yield* makePlannerState(Date.now());
|
|
106
|
+
// Capture the current runtime so the HTTP request handlers (which
|
|
107
|
+
// are non-Effect functions) can fire-and-forget OTLP log emissions
|
|
108
|
+
// through the same TelemetryLive layer the loop uses.
|
|
109
|
+
const runtime = yield* Effect.runtime();
|
|
110
|
+
const runFork = Runtime.runFork(runtime);
|
|
111
|
+
const emitter = makeDecisionEmitter(runFork);
|
|
112
|
+
// Open the read-only SQLite client when configured. Absent path
|
|
113
|
+
// is non-fatal — the daemon stays useful for POST endpoints (the
|
|
114
|
+
// dashboard validates against its own storage on receive); GET
|
|
115
|
+
// endpoints will return 503 with a clear remediation message.
|
|
116
|
+
const readClient = yield* Effect.acquireRelease(Effect.sync(() => config.plannerSqlitePath
|
|
117
|
+
? createPlannerReadClient({ sqlitePath: config.plannerSqlitePath })
|
|
118
|
+
: null), (client) => Effect.sync(() => client?.close()));
|
|
119
|
+
const httpServer = yield* Effect.acquireRelease(Effect.promise(() => startPlannerHttpServer({
|
|
120
|
+
host: config.plannerHost,
|
|
121
|
+
port: config.plannerPort,
|
|
122
|
+
readClient,
|
|
123
|
+
getHealth: makeHealthReader(stateRef, packageVersion()),
|
|
124
|
+
emitter,
|
|
125
|
+
})), (server) => Effect.promise(() => server.close()));
|
|
126
|
+
yield* Effect.logInfo(`[runway planner] http listening on ${config.plannerHost}:${httpServer.port}`);
|
|
127
|
+
// VA-476: queue-selection capability is wired when we have a
|
|
128
|
+
// SQLite read client. Absent → tick body skips it (the daemon
|
|
129
|
+
// still serves /planner/health and the POST endpoints, just no
|
|
130
|
+
// recommendations get proposed).
|
|
131
|
+
//
|
|
132
|
+
// VA-477: mergeability capability needs both the SQLite read
|
|
133
|
+
// client AND a `baseBranch` configured. Without baseBranch the
|
|
134
|
+
// rebase-target is unknown, so the capability is dormant rather
|
|
135
|
+
// than rebasing against a guessed default.
|
|
136
|
+
const nowNano = () => String(Date.now() * 1_000_000);
|
|
137
|
+
const capabilities = readClient
|
|
138
|
+
? {
|
|
139
|
+
queueSelection: {
|
|
140
|
+
readClient,
|
|
141
|
+
readyLabel: config.readyLabel,
|
|
142
|
+
now: nowNano,
|
|
143
|
+
},
|
|
144
|
+
// VA-480: drain spawn doesn't need baseBranch — it just
|
|
145
|
+
// shells out to `runway run --target` which resolves its
|
|
146
|
+
// own base branch. Wire it whenever the read client is
|
|
147
|
+
// available so queue-selection accepts can complete the
|
|
148
|
+
// recommendation → action lifecycle regardless of whether
|
|
149
|
+
// shepherd capabilities are configured.
|
|
150
|
+
drain: {
|
|
151
|
+
readClient,
|
|
152
|
+
cwd: process.cwd(),
|
|
153
|
+
now: nowNano,
|
|
154
|
+
},
|
|
155
|
+
...(config.baseBranch
|
|
156
|
+
? {
|
|
157
|
+
mergeability: {
|
|
158
|
+
readClient,
|
|
159
|
+
github: createGithubGateway(),
|
|
160
|
+
config,
|
|
161
|
+
cwd: process.cwd(),
|
|
162
|
+
baseBranch: config.baseBranch,
|
|
163
|
+
now: nowNano,
|
|
164
|
+
},
|
|
165
|
+
// VA-478: CI capability shares the same gating
|
|
166
|
+
// (SQLite + baseBranch) so all per-PR shepherd
|
|
167
|
+
// capabilities light up or stay dark together.
|
|
168
|
+
ci: {
|
|
169
|
+
readClient,
|
|
170
|
+
github: createGithubGateway(),
|
|
171
|
+
config,
|
|
172
|
+
cwd: process.cwd(),
|
|
173
|
+
baseBranch: config.baseBranch,
|
|
174
|
+
now: nowNano,
|
|
175
|
+
},
|
|
176
|
+
// VA-479: reviewer capability. LLM interpreter is
|
|
177
|
+
// wired only when ANTHROPIC_API_KEY is in the env;
|
|
178
|
+
// absent → verbatim-comment fallback (operator sees
|
|
179
|
+
// the raw body in evidence).
|
|
180
|
+
reviewer: {
|
|
181
|
+
readClient,
|
|
182
|
+
github: createGithubGateway(),
|
|
183
|
+
config,
|
|
184
|
+
cwd: process.cwd(),
|
|
185
|
+
baseBranch: config.baseBranch,
|
|
186
|
+
interpretComment: process.env.ANTHROPIC_API_KEY
|
|
187
|
+
? makeAnthropicCommentInterpreter({
|
|
188
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
189
|
+
})
|
|
190
|
+
: undefined,
|
|
191
|
+
now: nowNano,
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
: {}),
|
|
195
|
+
}
|
|
196
|
+
: {};
|
|
197
|
+
yield* makePlannerLoop(shutdownSignal, stateRef, capabilities);
|
|
198
|
+
}).pipe(Effect.scoped, Effect.provide(MainLayer));
|
|
199
|
+
await Effect.runPromise(program);
|
|
200
|
+
console.log("[runway planner] shutdown complete");
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* VA-475: turn the captured Effect runtime into a synchronous emitter
|
|
205
|
+
* the HTTP handlers can call without re-entering Effect.gen. Each
|
|
206
|
+
* call schedules a single log emission on the runtime; the
|
|
207
|
+
* TelemetryLive layer flushes it through the OTLP exporter to the
|
|
208
|
+
* dashboard's projector.
|
|
209
|
+
*/
|
|
210
|
+
export function makeDecisionEmitter(runFork) {
|
|
211
|
+
return {
|
|
212
|
+
emitAccepted: ({ recommendationId, source, reason, timestampUnixNano }) => {
|
|
213
|
+
const attrs = {
|
|
214
|
+
recommendationId,
|
|
215
|
+
source,
|
|
216
|
+
};
|
|
217
|
+
if (reason)
|
|
218
|
+
attrs.reason = reason;
|
|
219
|
+
runFork(Effect.logInfo(PLANNER_RECOMMENDATION_ACCEPTED_LOG).pipe(Effect.annotateLogs(attrs),
|
|
220
|
+
// Force the log timestamp so the dashboard projector sees
|
|
221
|
+
// the operator-perceived decision moment, not the OTLP
|
|
222
|
+
// emit moment (close in practice; explicit is clearer).
|
|
223
|
+
Effect.annotateLogs({ timestampUnixNano })));
|
|
224
|
+
},
|
|
225
|
+
emitRejected: ({ recommendationId, source, reason, timestampUnixNano }) => {
|
|
226
|
+
runFork(Effect.logInfo(PLANNER_RECOMMENDATION_REJECTED_LOG).pipe(Effect.annotateLogs({
|
|
227
|
+
recommendationId,
|
|
228
|
+
source,
|
|
229
|
+
reason,
|
|
230
|
+
timestampUnixNano,
|
|
231
|
+
})));
|
|
232
|
+
},
|
|
233
|
+
emitCapabilityModeChanged: ({ capability, mode, updatedBy, timestampUnixNano, }) => {
|
|
234
|
+
runFork(Effect.logInfo(PLANNER_CAPABILITY_MODE_CHANGED_LOG).pipe(Effect.annotateLogs({
|
|
235
|
+
capability,
|
|
236
|
+
mode,
|
|
237
|
+
updatedBy,
|
|
238
|
+
timestampUnixNano,
|
|
239
|
+
})));
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Resolve `package.json#version` for the health endpoint. Reads
|
|
245
|
+
* synchronously at boot — runtime cost is one stat + parse. Cached
|
|
246
|
+
* after first call.
|
|
247
|
+
*/
|
|
248
|
+
let cachedVersion = null;
|
|
249
|
+
function packageVersion() {
|
|
250
|
+
if (cachedVersion !== null)
|
|
251
|
+
return cachedVersion;
|
|
252
|
+
try {
|
|
253
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
254
|
+
const pkg = JSON.parse(readFileSync(join(here, "..", "..", "package.json"), "utf8"));
|
|
255
|
+
cachedVersion = String(pkg.version ?? "unknown");
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
// Surface the read failure to stderr so an operator running the
|
|
259
|
+
// foreground daemon can see what's wrong; the "unknown" fallback
|
|
260
|
+
// keeps the health endpoint usable.
|
|
261
|
+
console.error("[runway planner] could not resolve package version:", err instanceof Error ? err.message : String(err));
|
|
262
|
+
cachedVersion = "unknown";
|
|
263
|
+
}
|
|
264
|
+
return cachedVersion;
|
|
265
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -87,6 +87,23 @@ export function parseRunArgs(argv) {
|
|
|
87
87
|
}
|
|
88
88
|
opts.reviewRetries = n;
|
|
89
89
|
}
|
|
90
|
+
else if (a === "--target") {
|
|
91
|
+
const v = argv[i + 1];
|
|
92
|
+
if (!v)
|
|
93
|
+
throw new Error("--target requires a Linear issue identifier");
|
|
94
|
+
if (!/^[A-Z]+-\d+$/.test(v)) {
|
|
95
|
+
throw new Error(`--target must be a Linear issue identifier like VA-123, got "${v}"`);
|
|
96
|
+
}
|
|
97
|
+
opts.target = v;
|
|
98
|
+
i += 1;
|
|
99
|
+
}
|
|
100
|
+
else if (a?.startsWith("--target=")) {
|
|
101
|
+
const v = a.slice("--target=".length);
|
|
102
|
+
if (!/^[A-Z]+-\d+$/.test(v)) {
|
|
103
|
+
throw new Error(`--target must be a Linear issue identifier like VA-123, got "${v}"`);
|
|
104
|
+
}
|
|
105
|
+
opts.target = v;
|
|
106
|
+
}
|
|
90
107
|
else if (a === "--help" || a === "-h") {
|
|
91
108
|
printRunUsage();
|
|
92
109
|
process.exit(0);
|
|
@@ -129,6 +146,15 @@ OPTIONS
|
|
|
129
146
|
and re-runs review. N caps the extra impl+review
|
|
130
147
|
pairs per drain pickup. 0 disables retries entirely.
|
|
131
148
|
Overrides RUNWAY_REVIEW_RETRIES. Default: 1.
|
|
149
|
+
--target VA-NNN
|
|
150
|
+
Restrict the drain to one Linear issue. Skips the
|
|
151
|
+
FIFO pick and claims this identifier instead. Used
|
|
152
|
+
by the planner daemon (VA-480) when accepting a
|
|
153
|
+
queue-selection recommendation; manual operators
|
|
154
|
+
can also use it to re-attempt a specific issue
|
|
155
|
+
without picking up everything else in the queue.
|
|
156
|
+
Fails fast if the issue isn't currently in the
|
|
157
|
+
ready queue.
|
|
132
158
|
--help, -h Show this help.
|
|
133
159
|
|
|
134
160
|
ENVIRONMENT
|
|
@@ -212,20 +238,26 @@ export async function runCommand(argv) {
|
|
|
212
238
|
});
|
|
213
239
|
const linear = createLinearGateway(config, linearLimiter);
|
|
214
240
|
const github = createGithubGateway();
|
|
215
|
-
return yield* drainQueue({ config, linear, github, cwd }, { max: opts.max, allowPaths: opts.allowPaths });
|
|
241
|
+
return yield* drainQueue({ config, linear, github, cwd }, { max: opts.max, allowPaths: opts.allowPaths, target: opts.target });
|
|
216
242
|
}).pipe(Effect.scoped, Effect.provide(MainLayer));
|
|
217
243
|
const result = await Effect.runPromise(program);
|
|
218
244
|
console.log(`[runway] done — attempts=${result.attempts} opened=${result.opened} hitl=${result.hitl} errored=${result.errored} skipped=${result.skipped}`);
|
|
245
|
+
// VA-480: when `--target` was specified but no issue was attempted,
|
|
246
|
+
// the target wasn't in the ready queue. Surface as a non-zero
|
|
247
|
+
// exit so the planner's drain-spawn dispatcher routes the action
|
|
248
|
+
// to `action.failed` instead of `action.completed`. The drainQueue
|
|
249
|
+
// Effect already logged the warning; this is the exit-code half.
|
|
250
|
+
const targetMissed = opts.target !== undefined && result.attempts === 0;
|
|
219
251
|
// Single-line, parser-friendly completion marker. Background
|
|
220
252
|
// watchers (Claude Code's `run_in_background` bash task, CI,
|
|
221
253
|
// scripts) can grep for `[runway:exit]` instead of guessing
|
|
222
254
|
// whether the drain is still in flight.
|
|
223
|
-
console.log(`[runway:exit] status
|
|
255
|
+
console.log(`[runway:exit] status=${targetMissed ? "target_not_in_queue" : "success"} attempts=${result.attempts} opened=${result.opened} hitl=${result.hitl} errored=${result.errored} skipped=${result.skipped}`);
|
|
224
256
|
// Hard exit so any lingering handle (OTel BatchSpanProcessor's
|
|
225
257
|
// interval when OTEL_EXPORTER_OTLP_ENDPOINT is set, a Docker
|
|
226
258
|
// stream Sandcastle left open, etc.) can't keep the process — and
|
|
227
259
|
// the background task that launched it — alive after the drain is
|
|
228
260
|
// logically done. By this point `Effect.scoped` has already torn
|
|
229
261
|
// down its finalizers.
|
|
230
|
-
process.exit(0);
|
|
262
|
+
process.exit(targetMissed ? 1 : 0);
|
|
231
263
|
}
|
package/dist/config.js
CHANGED
|
@@ -45,23 +45,28 @@ const configEffect = EConfig.all({
|
|
|
45
45
|
metaFilterThresholdsJson: EConfig.option(EConfig.string("RUNWAY_META_FILTER_THRESHOLDS")),
|
|
46
46
|
runwayRepoProjectName: EConfig.option(EConfig.string("RUNWAY_REPO_PROJECT_NAME")),
|
|
47
47
|
metaWeeklyModel: EConfig.option(EConfig.string("RUNWAY_META_WEEKLY_MODEL")),
|
|
48
|
-
// VA-
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
})),
|
|
48
|
+
// VA-482: inline shepherd is retired — the four `RUNWAY_SHEPHERD_*`
|
|
49
|
+
// env vars (ENABLED / POLL_INTERVAL_SECONDS / MAX_ITERATIONS /
|
|
50
|
+
// MAX_WALL_SECONDS) are intentionally removed. Their work moved to
|
|
51
|
+
// the long-running `runway planner` daemon (capabilities VA-477..479)
|
|
52
|
+
// which has its own tick + budget knobs (`RUNWAY_PLANNER_*`).
|
|
53
|
+
// `RUNWAY_PR_REVIEWER_BOT_LOGIN` is preserved because the planner's
|
|
54
|
+
// reviewer dispatcher still consumes it.
|
|
56
55
|
prReviewerBotLogin: EConfig.option(EConfig.string("RUNWAY_PR_REVIEWER_BOT_LOGIN")),
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
// VA-473: planner daemon scaffold. Tick cadence + bind address for
|
|
57
|
+
// the long-running process. No capabilities wired yet — these
|
|
58
|
+
// values are consumed by `runway planner run` (heartbeat loop) and
|
|
59
|
+
// by the HTTP listener wired in VA-475.
|
|
60
|
+
plannerTickSeconds: EConfig.integer("RUNWAY_PLANNER_TICK_SECONDS").pipe(EConfig.withDefault(30), EConfig.validate({
|
|
61
|
+
message: "RUNWAY_PLANNER_TICK_SECONDS must be a positive integer",
|
|
59
62
|
validation: (n) => n > 0,
|
|
60
63
|
})),
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
plannerHost: EConfig.string("RUNWAY_PLANNER_HOST").pipe(EConfig.withDefault("127.0.0.1")),
|
|
65
|
+
plannerPort: EConfig.integer("RUNWAY_PLANNER_PORT").pipe(EConfig.withDefault(3002), EConfig.validate({
|
|
66
|
+
message: "RUNWAY_PLANNER_PORT must be a positive integer",
|
|
67
|
+
validation: (n) => n > 0 && n < 65536,
|
|
64
68
|
})),
|
|
69
|
+
plannerSqlitePath: EConfig.option(EConfig.string("RUNWAY_PLANNER_SQLITE_PATH")),
|
|
65
70
|
}).pipe(Effect.map((raw) => ({
|
|
66
71
|
linearApiKey: raw.linearApiKey,
|
|
67
72
|
opServiceAccountToken: Option.getOrUndefined(raw.opServiceAccountToken),
|
|
@@ -83,11 +88,11 @@ const configEffect = EConfig.all({
|
|
|
83
88
|
metaFilterThresholds: parseMetaFilterThresholdsJson(Option.getOrUndefined(raw.metaFilterThresholdsJson)),
|
|
84
89
|
runwayRepoProjectName: Option.getOrUndefined(raw.runwayRepoProjectName),
|
|
85
90
|
metaWeeklyModel: Option.getOrUndefined(raw.metaWeeklyModel),
|
|
86
|
-
shepherdEnabled: raw.shepherdEnabled,
|
|
87
|
-
shepherdPollIntervalSeconds: raw.shepherdPollIntervalSeconds,
|
|
88
91
|
prReviewerBotLogin: Option.getOrUndefined(raw.prReviewerBotLogin),
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
plannerTickSeconds: raw.plannerTickSeconds,
|
|
93
|
+
plannerHost: raw.plannerHost,
|
|
94
|
+
plannerPort: raw.plannerPort,
|
|
95
|
+
plannerSqlitePath: Option.getOrUndefined(raw.plannerSqlitePath),
|
|
91
96
|
})));
|
|
92
97
|
/**
|
|
93
98
|
* VA-359: Context tag for the resolved RunwayConfig. Provided by
|