@valescoagency/runway 0.10.0 → 0.10.1
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 +1 -1
- package/dist/commands/run.js +17 -2
- package/dist/git.js +22 -0
- package/dist/linear.js +45 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -392,7 +392,7 @@ These are tractable, just not v1.
|
|
|
392
392
|
|
|
393
393
|
## Status
|
|
394
394
|
|
|
395
|
-
0.10.
|
|
395
|
+
0.10.1 — production-shaped and dogfooded against live Linear queues.
|
|
396
396
|
The end-to-end pipeline (init → run → review → PR) is stable; surface
|
|
397
397
|
may still shift as the orchestrator's policy and iteration mechanics
|
|
398
398
|
mature. See [CHANGELOG.md](./CHANGELOG.md) for per-release detail.
|
package/dist/commands/run.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Effect, Layer, Logger, RateLimiter } from "effect";
|
|
|
2
2
|
import { ConfigLive, ConfigTag } from "../config.js";
|
|
3
3
|
import { createLinearGateway } from "../linear.js";
|
|
4
4
|
import { createGithubGateway } from "../github.js";
|
|
5
|
+
import { remoteRefExists } from "../git.js";
|
|
5
6
|
import { assertSandcastleInitialised, drainQueue, } from "../orchestrator.js";
|
|
6
7
|
import { TelemetryLive } from "../telemetry.js";
|
|
7
8
|
export function parseRunArgs(argv) {
|
|
@@ -140,7 +141,14 @@ ENVIRONMENT
|
|
|
140
141
|
and targets with PRs). Detected from
|
|
141
142
|
origin/HEAD when unset.
|
|
142
143
|
RUNWAY_READY_STATUS default "Todo"
|
|
143
|
-
RUNWAY_IN_PROGRESS_STATUS default "In Progress"
|
|
144
|
+
RUNWAY_IN_PROGRESS_STATUS default "In Progress" — also the
|
|
145
|
+
auxiliary drain bucket (VA-421): runway
|
|
146
|
+
accepts issues in this status when no
|
|
147
|
+
agent/<id> branch exists on origin, so
|
|
148
|
+
Linear's GitHub auto-transitions (e.g.
|
|
149
|
+
an unrelated PR mentioning the issue in
|
|
150
|
+
its body) can't silently drop the issue
|
|
151
|
+
from the queue.
|
|
144
152
|
RUNWAY_IN_REVIEW_STATUS default "In Review"
|
|
145
153
|
RUNWAY_HITL_LABEL default "ready-for-human"
|
|
146
154
|
RUNWAY_MAX_ITERATIONS default 5 — outer impl re-prompt loop
|
|
@@ -196,7 +204,14 @@ export async function runCommand(argv) {
|
|
|
196
204
|
limit: 30,
|
|
197
205
|
interval: "1 minute",
|
|
198
206
|
});
|
|
199
|
-
|
|
207
|
+
// VA-421: inject a git-side predicate so `fetchReady` can accept
|
|
208
|
+
// In-Progress issues whose `agent/<id>` branch hasn't yet been
|
|
209
|
+
// pushed to origin. Closes the Linear-auto-transition loophole
|
|
210
|
+
// where an unrelated PR-body mention silently drops an issue from
|
|
211
|
+
// the drain queue.
|
|
212
|
+
const linear = createLinearGateway(config, linearLimiter, {
|
|
213
|
+
remoteAgentBranchExists: (branch) => Effect.runPromise(remoteRefExists(cwd, branch)),
|
|
214
|
+
});
|
|
200
215
|
const github = createGithubGateway();
|
|
201
216
|
return yield* drainQueue({ config, linear, github, cwd }, { max: opts.max, allowPaths: opts.allowPaths });
|
|
202
217
|
}).pipe(Effect.scoped, Effect.provide(MainLayer));
|
package/dist/git.js
CHANGED
|
@@ -77,6 +77,28 @@ export const captureCommitLog = (repoPath, base, branch) => runExecaScoped("git"
|
|
|
77
77
|
const raw = res.stdout;
|
|
78
78
|
return typeof raw === "string" ? raw : "";
|
|
79
79
|
}));
|
|
80
|
+
/**
|
|
81
|
+
* VA-421: Whether a branch ref exists on `origin`. Used by
|
|
82
|
+
* `fetchReady` to distinguish "an agent branch has been pushed (real
|
|
83
|
+
* work in flight, leave alone)" from "Linear auto-transitioned an
|
|
84
|
+
* issue to In-Progress without any work actually starting (drain
|
|
85
|
+
* it)". `git ls-remote --heads origin <branch>` is the cheapest
|
|
86
|
+
* authoritative check — one round-trip, no clone needed, SSH
|
|
87
|
+
* multiplexing usually caches the connection.
|
|
88
|
+
*
|
|
89
|
+
* Non-zero exit (network error, no remote configured, auth failure)
|
|
90
|
+
* surfaces as an Effect failure so the caller can route it through
|
|
91
|
+
* the usual error path rather than silently falling back to "doesn't
|
|
92
|
+
* exist" — operator should fix the env, not have runway quietly
|
|
93
|
+
* misclassify candidates.
|
|
94
|
+
*/
|
|
95
|
+
export const remoteRefExists = (repoPath, branch) => runExecaScoped("git", ["ls-remote", "--heads", "origin", branch], { cwd: repoPath }, (err) => ({
|
|
96
|
+
message: err instanceof Error ? err.message : String(err),
|
|
97
|
+
})).pipe(Effect.map((res) => {
|
|
98
|
+
const raw = res.stdout;
|
|
99
|
+
const out = typeof raw === "string" ? raw : "";
|
|
100
|
+
return out.trim().length > 0;
|
|
101
|
+
}));
|
|
80
102
|
/**
|
|
81
103
|
* VA-358: Whether the agent branch has any commits beyond `base`.
|
|
82
104
|
* Used by failure-routing to distinguish "agent crashed mid-run,
|
package/dist/linear.js
CHANGED
|
@@ -232,7 +232,7 @@ function classifyLinearError(err, context) {
|
|
|
232
232
|
* Production: `commands/run.ts` builds a limiter inside `Effect.scoped`
|
|
233
233
|
* and passes it here.
|
|
234
234
|
*/
|
|
235
|
-
export function createLinearGateway(config, limiter = null) {
|
|
235
|
+
export function createLinearGateway(config, limiter = null, opts = {}) {
|
|
236
236
|
const client = new LinearClient({ apiKey: Redacted.value(config.linearApiKey) });
|
|
237
237
|
const gate = (eff) => (limiter ? limiter(eff) : eff);
|
|
238
238
|
async function findStateId(teamId, name) {
|
|
@@ -300,18 +300,40 @@ export function createLinearGateway(config, limiter = null) {
|
|
|
300
300
|
const projectId = config.linearProject
|
|
301
301
|
? await findProjectId(config.linearProject)
|
|
302
302
|
: null;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
303
|
+
// VA-421: optionally accept In-Progress issues whose
|
|
304
|
+
// `agent/<id>` branch hasn't been pushed to origin yet.
|
|
305
|
+
// Linear's GitHub integration auto-transitions issues to
|
|
306
|
+
// In-Progress on unrelated PR-body mentions, which used
|
|
307
|
+
// to silently drop them from the drain queue. The
|
|
308
|
+
// git-side check is the authoritative signal that no
|
|
309
|
+
// successful drain attempt is in flight.
|
|
310
|
+
const inProgressStateId = opts.remoteAgentBranchExists
|
|
311
|
+
? await findStateId(teamId, config.inProgressStatus)
|
|
312
|
+
: null;
|
|
313
|
+
const issuesFilter = (stateId) => ({
|
|
314
|
+
team: { id: { eq: teamId } },
|
|
315
|
+
state: { id: { eq: stateId } },
|
|
316
|
+
...(projectId ? { project: { id: { eq: projectId } } } : {}),
|
|
317
|
+
});
|
|
318
|
+
const todoBatch = await client.issues({
|
|
319
|
+
filter: issuesFilter(readyStateId),
|
|
309
320
|
// VA-420: order doesn't matter at the SDK call — Linear's
|
|
310
321
|
// `IssueOrderBy` enum doesn't expose `priority`, and the
|
|
311
322
|
// SDK type for this argument hides the ASC/DESC direction
|
|
312
323
|
// anyway. We re-sort the candidate set in JS below.
|
|
313
324
|
orderBy: "createdAt",
|
|
314
325
|
});
|
|
326
|
+
const inProgressBatch = inProgressStateId
|
|
327
|
+
? await client.issues({
|
|
328
|
+
filter: issuesFilter(inProgressStateId),
|
|
329
|
+
orderBy: "createdAt",
|
|
330
|
+
})
|
|
331
|
+
: { nodes: [] };
|
|
332
|
+
const tagged = [
|
|
333
|
+
...todoBatch.nodes.map((raw) => ({ raw, kind: "todo" })),
|
|
334
|
+
...inProgressBatch.nodes.map((raw) => ({ raw, kind: "inProgress" })),
|
|
335
|
+
];
|
|
336
|
+
const issues = { nodes: tagged };
|
|
315
337
|
// VA-420: drain by Linear priority first, then by createdAt
|
|
316
338
|
// within a priority bucket. Linear's priority encoding is
|
|
317
339
|
// non-monotonic — `1 = Urgent, 2 = High, 3 = Medium,
|
|
@@ -320,12 +342,12 @@ export function createLinearGateway(config, limiter = null) {
|
|
|
320
342
|
// (operator intuition: Urgent first, unprioritised last).
|
|
321
343
|
// Within a bucket, oldest-first restores the FIFO intent.
|
|
322
344
|
const candidates = [...issues.nodes].sort((a, b) => {
|
|
323
|
-
const pa = a.priority === 0 ? Infinity : a.priority;
|
|
324
|
-
const pb = b.priority === 0 ? Infinity : b.priority;
|
|
345
|
+
const pa = a.raw.priority === 0 ? Infinity : a.raw.priority;
|
|
346
|
+
const pb = b.raw.priority === 0 ? Infinity : b.raw.priority;
|
|
325
347
|
if (pa !== pb)
|
|
326
348
|
return pa - pb;
|
|
327
|
-
return (new Date(a.createdAt).getTime() -
|
|
328
|
-
new Date(b.createdAt).getTime());
|
|
349
|
+
return (new Date(a.raw.createdAt).getTime() -
|
|
350
|
+
new Date(b.raw.createdAt).getTime());
|
|
329
351
|
});
|
|
330
352
|
// VA-360: validate every issue node through the schema —
|
|
331
353
|
// a single drifted issue surfaces as `LinearSchemaError`
|
|
@@ -340,13 +362,24 @@ export function createLinearGateway(config, limiter = null) {
|
|
|
340
362
|
// candidate go through the rate limiter and the retry
|
|
341
363
|
// policy alongside everything else.
|
|
342
364
|
const eligible = [];
|
|
343
|
-
for (const raw of candidates) {
|
|
365
|
+
for (const { raw, kind } of candidates) {
|
|
344
366
|
const i = decodeIssueNode(raw);
|
|
345
367
|
const labels = await fetchIssueLabelNames(raw);
|
|
346
368
|
if (labels.includes(config.hitlLabel))
|
|
347
369
|
continue;
|
|
348
370
|
if (await hasActiveBlocker(raw))
|
|
349
371
|
continue;
|
|
372
|
+
// VA-421: In-Progress candidates pass only if no
|
|
373
|
+
// `agent/<id>` ref exists on origin — that's the
|
|
374
|
+
// authoritative "no drain attempt in flight" signal,
|
|
375
|
+
// immune to Linear's auto-transitions. Todo candidates
|
|
376
|
+
// skip this check (no behavior change for the common
|
|
377
|
+
// path).
|
|
378
|
+
if (kind === "inProgress" && opts.remoteAgentBranchExists) {
|
|
379
|
+
const branch = `agent/${i.identifier.toLowerCase()}`;
|
|
380
|
+
if (await opts.remoteAgentBranchExists(branch))
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
350
383
|
eligible.push({
|
|
351
384
|
id: i.id,
|
|
352
385
|
identifier: i.identifier,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valescoagency/runway",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
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": {
|