@valescoagency/runway 0.6.0 → 0.7.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 +9 -1
- package/dist/config.js +5 -0
- package/dist/implement.js +7 -1
- package/dist/linear.js +136 -4
- package/dist/orchestrator.js +26 -2
- package/dist/prompts.js +62 -0
- package/package.json +11 -11
- package/prompts/implement.md +2 -0
package/README.md
CHANGED
|
@@ -172,6 +172,14 @@ export LINEAR_API_KEY=lin_api_...
|
|
|
172
172
|
# export RUNWAY_IN_REVIEW_STATUS="In Review"
|
|
173
173
|
# export RUNWAY_HITL_LABEL="ready-for-human"
|
|
174
174
|
# export RUNWAY_MAX_ITERATIONS=5
|
|
175
|
+
# export RUNWAY_COMMENT_AUTHOR_ALLOWLIST="Reviewer Bot,Jane Reviewer"
|
|
176
|
+
# optional, comma-separated Linear user names whose comments on a
|
|
177
|
+
# re-queued issue surface as "Review feedback from prior attempts"
|
|
178
|
+
# in the implement prompt. Defaults to the Linear user the API key
|
|
179
|
+
# authenticates as (which is both the reviewer-agent's identity
|
|
180
|
+
# and, in the solo-dev case, the repo owner). Set this only when
|
|
181
|
+
# the reviewer agent and the repo owner are split across distinct
|
|
182
|
+
# Linear identities.
|
|
175
183
|
```
|
|
176
184
|
|
|
177
185
|
`RUNWAY_HITL_LABEL` defaults to `ready-for-human`, matching the
|
|
@@ -384,7 +392,7 @@ These are tractable, just not v1.
|
|
|
384
392
|
|
|
385
393
|
## Status
|
|
386
394
|
|
|
387
|
-
0.
|
|
395
|
+
0.7.1 — production-shaped and dogfooded against live Linear queues.
|
|
388
396
|
The end-to-end pipeline (init → run → review → PR) is stable; surface
|
|
389
397
|
may still shift as the orchestrator's policy and iteration mechanics
|
|
390
398
|
mature. See [CHANGELOG.md](./CHANGELOG.md) for per-release detail.
|
package/dist/config.js
CHANGED
|
@@ -20,6 +20,7 @@ const configEffect = EConfig.all({
|
|
|
20
20
|
message: "RUNWAY_MAX_ITERATIONS must be a positive integer",
|
|
21
21
|
validation: (n) => n > 0,
|
|
22
22
|
})),
|
|
23
|
+
commentAuthorAllowlist: EConfig.option(EConfig.string("RUNWAY_COMMENT_AUTHOR_ALLOWLIST")),
|
|
23
24
|
}).pipe(Effect.map((raw) => ({
|
|
24
25
|
linearApiKey: raw.linearApiKey,
|
|
25
26
|
opServiceAccountToken: Option.getOrUndefined(raw.opServiceAccountToken),
|
|
@@ -31,6 +32,10 @@ const configEffect = EConfig.all({
|
|
|
31
32
|
inReviewStatus: raw.inReviewStatus,
|
|
32
33
|
hitlLabel: raw.hitlLabel,
|
|
33
34
|
maxIterations: raw.maxIterations,
|
|
35
|
+
commentAuthorAllowlist: Option.getOrUndefined(raw.commentAuthorAllowlist)
|
|
36
|
+
?.split(",")
|
|
37
|
+
.map((s) => s.trim())
|
|
38
|
+
.filter(Boolean),
|
|
34
39
|
})));
|
|
35
40
|
/**
|
|
36
41
|
* VA-359: Context tag for the resolved RunwayConfig. Provided by
|
package/dist/implement.js
CHANGED
|
@@ -44,7 +44,7 @@ export function parseImplVerdict(result) {
|
|
|
44
44
|
* completionSignal.
|
|
45
45
|
*/
|
|
46
46
|
export const runImplementLoop = (issue, deps, branch) => Effect.gen(function* () {
|
|
47
|
-
const { config, cwd, baseBranch, policy } = deps;
|
|
47
|
+
const { config, cwd, baseBranch, policy, priorFeedback } = deps;
|
|
48
48
|
const maxIters = Math.max(1, config.maxIterations);
|
|
49
49
|
let prevSummary = "";
|
|
50
50
|
let implementResult;
|
|
@@ -54,6 +54,12 @@ export const runImplementLoop = (issue, deps, branch) => Effect.gen(function* ()
|
|
|
54
54
|
issue,
|
|
55
55
|
policy,
|
|
56
56
|
previousIterations: prevSummary,
|
|
57
|
+
// VA-383: surface prior-attempt review feedback on every
|
|
58
|
+
// iteration of this run — the rejection blockers that put
|
|
59
|
+
// the issue back to Todo are equally relevant on iteration 1
|
|
60
|
+
// and iteration N+1, since the implementer keeps drifting
|
|
61
|
+
// toward the same code paths until corrected.
|
|
62
|
+
priorReviewFeedback: priorFeedback,
|
|
57
63
|
}));
|
|
58
64
|
implementResult = yield* runSandcastle({
|
|
59
65
|
agent: claudeCode("claude-opus-4-7"),
|
package/dist/linear.js
CHANGED
|
@@ -26,11 +26,92 @@ const IssueLabelNodeSchema = Schema.Struct({
|
|
|
26
26
|
const ProjectNodeSchema = Schema.Struct({
|
|
27
27
|
id: Schema.String,
|
|
28
28
|
});
|
|
29
|
+
// VA-394: minimal shape needed to ask "is this blocker still active?"
|
|
30
|
+
// — we only read the relation's `type` (filtering for `blocks`) and
|
|
31
|
+
// the related issue's workflow state `type`. Schemas stay narrow so
|
|
32
|
+
// the validation cost per candidate stays small.
|
|
33
|
+
const IssueRelationNodeSchema = Schema.Struct({
|
|
34
|
+
id: Schema.String,
|
|
35
|
+
type: Schema.String,
|
|
36
|
+
});
|
|
37
|
+
const WorkflowStateTypeNodeSchema = Schema.Struct({
|
|
38
|
+
type: Schema.String,
|
|
39
|
+
});
|
|
40
|
+
// VA-383: comments fetched for surfacing prior-attempt review feedback
|
|
41
|
+
// to the implementer. `user` is nullable on Linear's side (system /
|
|
42
|
+
// integration comments can lack an author); we map a missing user to
|
|
43
|
+
// the empty string so downstream filters can match by name without a
|
|
44
|
+
// null-guard at every call site.
|
|
45
|
+
const CommentNodeSchema = Schema.Struct({
|
|
46
|
+
id: Schema.String,
|
|
47
|
+
body: Schema.String,
|
|
48
|
+
createdAt: Schema.Union(Schema.String, Schema.DateFromSelf),
|
|
49
|
+
user: Schema.NullOr(Schema.Struct({
|
|
50
|
+
id: Schema.String,
|
|
51
|
+
name: Schema.String,
|
|
52
|
+
})),
|
|
53
|
+
});
|
|
54
|
+
const ViewerSchema = Schema.Struct({
|
|
55
|
+
id: Schema.String,
|
|
56
|
+
name: Schema.String,
|
|
57
|
+
});
|
|
29
58
|
const decodeIssueNode = Schema.decodeUnknownSync(IssueNodeSchema);
|
|
30
59
|
const decodeWorkflowStateNode = Schema.decodeUnknownSync(WorkflowStateNodeSchema);
|
|
31
60
|
const decodeTeamNode = Schema.decodeUnknownSync(TeamNodeSchema);
|
|
32
61
|
const decodeIssueLabelNode = Schema.decodeUnknownSync(IssueLabelNodeSchema);
|
|
33
62
|
const decodeProjectNode = Schema.decodeUnknownSync(ProjectNodeSchema);
|
|
63
|
+
const decodeCommentNode = Schema.decodeUnknownSync(CommentNodeSchema);
|
|
64
|
+
const decodeViewer = Schema.decodeUnknownSync(ViewerSchema);
|
|
65
|
+
const decodeIssueRelationNode = Schema.decodeUnknownSync(IssueRelationNodeSchema);
|
|
66
|
+
const decodeWorkflowStateTypeNode = Schema.decodeUnknownSync(WorkflowStateTypeNodeSchema);
|
|
67
|
+
// VA-394: Linear workflow state `type` values that mean "the blocker
|
|
68
|
+
// has resolved." Anything else (`triage`, `backlog`, `unstarted`,
|
|
69
|
+
// `started`) still gates the blocked issue from runway pickup.
|
|
70
|
+
const TERMINAL_STATE_TYPES = new Set(["completed", "canceled"]);
|
|
71
|
+
const isTerminalStateType = (type) => TERMINAL_STATE_TYPES.has(type);
|
|
72
|
+
/**
|
|
73
|
+
* VA-394: returns true when `issue` carries `hitlLabel`. Runway writes
|
|
74
|
+
* this label on HITL escapes, and triage may apply it to flag
|
|
75
|
+
* human-only work — either way the contract is "do not pick up." Uses
|
|
76
|
+
* the SDK's chained `labels()` call rather than asking for labels
|
|
77
|
+
* inline at the candidate-fetch site so the schema for `IssueNode`
|
|
78
|
+
* stays narrow.
|
|
79
|
+
*/
|
|
80
|
+
async function hasHitlLabel(issue, hitlLabel) {
|
|
81
|
+
const labels = await issue.labels();
|
|
82
|
+
for (const raw of labels.nodes) {
|
|
83
|
+
if (decodeIssueLabelNode(raw).name === hitlLabel)
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* VA-394: returns true when `issue` has at least one `inverseRelations`
|
|
90
|
+
* record of type `blocks` whose blocker is in a non-terminal workflow
|
|
91
|
+
* state. Other relation types (`duplicate`, `related`) do not gate
|
|
92
|
+
* pickup. A blocker with a missing or undecodable state is treated as
|
|
93
|
+
* inactive (best-effort: drift in one relation should not stop the
|
|
94
|
+
* queue), but the decoder still throws `ParseError` on outright
|
|
95
|
+
* malformed relation nodes — caught upstream as `LinearSchemaError`.
|
|
96
|
+
*/
|
|
97
|
+
async function hasActiveBlocker(issue) {
|
|
98
|
+
const relations = await issue.inverseRelations();
|
|
99
|
+
for (const rawRel of relations.nodes) {
|
|
100
|
+
const rel = decodeIssueRelationNode(rawRel);
|
|
101
|
+
if (rel.type !== "blocks")
|
|
102
|
+
continue;
|
|
103
|
+
const blocker = await rawRel.issue;
|
|
104
|
+
if (!blocker)
|
|
105
|
+
continue;
|
|
106
|
+
const blockerState = await blocker.state;
|
|
107
|
+
if (!blockerState)
|
|
108
|
+
continue;
|
|
109
|
+
const stateType = decodeWorkflowStateTypeNode(blockerState).type;
|
|
110
|
+
if (!isTerminalStateType(stateType))
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
34
115
|
export class LinearNotFound extends Data.TaggedError("LinearNotFound") {
|
|
35
116
|
}
|
|
36
117
|
export class LinearUnauthorized extends Data.TaggedError("LinearUnauthorized") {
|
|
@@ -219,15 +300,30 @@ export function createLinearGateway(config, limiter = null) {
|
|
|
219
300
|
// VA-360: validate every issue node through the schema —
|
|
220
301
|
// a single drifted issue surfaces as `LinearSchemaError`
|
|
221
302
|
// instead of a downstream `cannot read property X`.
|
|
222
|
-
|
|
303
|
+
//
|
|
304
|
+
// VA-394: per-candidate eligibility — skip issues carrying
|
|
305
|
+
// `config.hitlLabel` (triage's "human-only" marker, also
|
|
306
|
+
// applied by runway itself on prior HITL escapes), and
|
|
307
|
+
// skip issues with at least one active `blocks` relation
|
|
308
|
+
// pointing at them. The candidate set is the Todo queue,
|
|
309
|
+
// typically a handful of issues; the N+1 SDK calls per
|
|
310
|
+
// candidate go through the rate limiter and the retry
|
|
311
|
+
// policy alongside everything else.
|
|
312
|
+
const eligible = [];
|
|
313
|
+
for (const raw of issues.nodes) {
|
|
223
314
|
const i = decodeIssueNode(raw);
|
|
224
|
-
|
|
315
|
+
if (await hasHitlLabel(raw, config.hitlLabel))
|
|
316
|
+
continue;
|
|
317
|
+
if (await hasActiveBlocker(raw))
|
|
318
|
+
continue;
|
|
319
|
+
eligible.push({
|
|
225
320
|
id: i.id,
|
|
226
321
|
identifier: i.identifier,
|
|
227
322
|
title: i.title,
|
|
228
323
|
description: i.description ?? "",
|
|
229
|
-
};
|
|
230
|
-
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return eligible;
|
|
231
327
|
},
|
|
232
328
|
catch: (err) => classifyLinearError(err, "fetchReady"),
|
|
233
329
|
}), { call: "fetchReady" }));
|
|
@@ -293,6 +389,42 @@ export function createLinearGateway(config, limiter = null) {
|
|
|
293
389
|
catch: (err) => classifyLinearError(err, "comment"),
|
|
294
390
|
}), { call: "comment" }));
|
|
295
391
|
},
|
|
392
|
+
fetchComments(issueId) {
|
|
393
|
+
return gate(applyLinearPolicy(Effect.tryPromise({
|
|
394
|
+
try: async () => {
|
|
395
|
+
// Linear SDK paginates by default — use the issue-scoped
|
|
396
|
+
// accessor and let the SDK's first page suffice for the
|
|
397
|
+
// common case (Runway's HITL + reviewer flow rarely
|
|
398
|
+
// produces more than a handful per attempt). If issues
|
|
399
|
+
// start blowing past the page limit, fold in pagination.
|
|
400
|
+
const issue = await client.issue(issueId);
|
|
401
|
+
const comments = await issue.comments();
|
|
402
|
+
const decoded = comments.nodes.map((raw) => {
|
|
403
|
+
const c = decodeCommentNode(raw);
|
|
404
|
+
const createdAt = c.createdAt instanceof Date ? c.createdAt : new Date(c.createdAt);
|
|
405
|
+
return {
|
|
406
|
+
id: c.id,
|
|
407
|
+
author: c.user?.name ?? "",
|
|
408
|
+
body: c.body,
|
|
409
|
+
createdAt,
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
// VA-383: ascending by createdAt so the impl agent reads
|
|
413
|
+
// feedback in the order it was given.
|
|
414
|
+
return [...decoded].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
415
|
+
},
|
|
416
|
+
catch: (err) => classifyLinearError(err, "fetchComments"),
|
|
417
|
+
}), { call: "fetchComments" }));
|
|
418
|
+
},
|
|
419
|
+
viewer() {
|
|
420
|
+
return gate(applyLinearPolicy(Effect.tryPromise({
|
|
421
|
+
try: async () => {
|
|
422
|
+
const v = await client.viewer;
|
|
423
|
+
return decodeViewer({ id: v.id, name: v.name });
|
|
424
|
+
},
|
|
425
|
+
catch: (err) => classifyLinearError(err, "viewer"),
|
|
426
|
+
}), { call: "viewer" }));
|
|
427
|
+
},
|
|
296
428
|
};
|
|
297
429
|
}
|
|
298
430
|
export async function validateLinearConfig(config) {
|
package/dist/orchestrator.js
CHANGED
|
@@ -5,6 +5,7 @@ import { detectBaseBranch, pruneStaleAgentBranch } from "./git.js";
|
|
|
5
5
|
import { loadPolicy } from "./policy.js";
|
|
6
6
|
import { flagHitl, handleProcessFailure } from "./hitl.js";
|
|
7
7
|
import { runImplementLoop } from "./implement.js";
|
|
8
|
+
import { formatPriorFeedback } from "./prompts.js";
|
|
8
9
|
import { runReviewPass } from "./review.js";
|
|
9
10
|
import { finalize } from "./finalize.js";
|
|
10
11
|
// Re-exports so existing callers (commands/run.ts) and tests
|
|
@@ -58,10 +59,24 @@ export const drainQueue = (deps, opts = {}) => Effect.gen(function* () {
|
|
|
58
59
|
yield* Effect.logInfo("base branch resolved").pipe(Effect.annotateLogs({ baseBranch: baseBranchResolved }));
|
|
59
60
|
const policy = loadPolicy(deps.cwd, { allowPathsOverride: opts.allowPaths });
|
|
60
61
|
yield* Effect.logInfo(`policy source: ${policy.source}`);
|
|
62
|
+
// VA-383: resolve the Linear viewer once per drain to seed the
|
|
63
|
+
// default comment-author allowlist. If the operator has provided
|
|
64
|
+
// an explicit allowlist, skip the lookup. If the lookup fails,
|
|
65
|
+
// fall back to an empty allowlist — `formatPriorFeedback` will
|
|
66
|
+
// emit nothing and the run proceeds with description-only
|
|
67
|
+
// context (matching VA-383's "never blocks the run" AC).
|
|
68
|
+
const commentAuthorAllowlist = config
|
|
69
|
+
.commentAuthorAllowlist?.length
|
|
70
|
+
? config.commentAuthorAllowlist
|
|
71
|
+
: yield* linear.viewer().pipe(Effect.map((v) => [v.name]), Effect.catchAll((err) => Effect.logWarning(`VA-383: viewer() failed; prior-attempt feedback will be empty (${err.message ?? String(err)})`).pipe(Effect.as([]))));
|
|
72
|
+
yield* Effect.logInfo(`comment author allowlist: ${commentAuthorAllowlist.length === 0
|
|
73
|
+
? "(empty — prior-feedback surfacing disabled)"
|
|
74
|
+
: commentAuthorAllowlist.join(", ")}`);
|
|
61
75
|
const runDeps = {
|
|
62
76
|
...deps,
|
|
63
77
|
baseBranch: baseBranchResolved,
|
|
64
78
|
policy,
|
|
79
|
+
commentAuthorAllowlist,
|
|
65
80
|
};
|
|
66
81
|
// VA-344: never re-pick an issue in the same invocation, even if
|
|
67
82
|
// VA-342 reverted it to `Todo`. Without this, a deterministic
|
|
@@ -133,8 +148,17 @@ export const drainQueue = (deps, opts = {}) => Effect.gen(function* () {
|
|
|
133
148
|
* Linear side-effects so the phases stay independently testable.
|
|
134
149
|
*/
|
|
135
150
|
const processIssue = (issue, deps) => Effect.gen(function* () {
|
|
136
|
-
const { config, linear } = deps;
|
|
151
|
+
const { config, linear, commentAuthorAllowlist } = deps;
|
|
137
152
|
const branch = `agent/${issue.identifier.toLowerCase()}`;
|
|
153
|
+
// VA-383: fetch prior comments BEFORE posting "Runway picked up
|
|
154
|
+
// this issue" so we don't see our own bookkeeping. The allowlist
|
|
155
|
+
// resolved at drain startup filters to reviewer/repo-owner
|
|
156
|
+
// identities; `formatPriorFeedback` additionally strips known
|
|
157
|
+
// runway bookkeeping prefixes. A fetch failure logs a warning and
|
|
158
|
+
// the run proceeds with empty feedback (never blocks).
|
|
159
|
+
const priorFeedback = yield* (commentAuthorAllowlist.length === 0
|
|
160
|
+
? Effect.succeed("")
|
|
161
|
+
: linear.fetchComments(issue.id).pipe(Effect.map((cs) => formatPriorFeedback(cs, commentAuthorAllowlist)), Effect.catchAll((err) => Effect.logWarning(`${issue.identifier}: fetchComments failed; proceeding with description-only context (${err.message ?? String(err)})`).pipe(Effect.as("")))));
|
|
138
162
|
yield* linear.transition(issue.id, config.inProgressStatus);
|
|
139
163
|
yield* linear.comment(issue.id, `Runway picked up this issue. Branch: \`${branch}\`.`);
|
|
140
164
|
// VA-366: if a prior attempt left an `agent/<id>` branch behind
|
|
@@ -143,7 +167,7 @@ const processIssue = (issue, deps) => Effect.gen(function* () {
|
|
|
143
167
|
// current `baseBranch` HEAD. Preserves the branch when it carries
|
|
144
168
|
// real work. Best-effort — failures here don't abort the issue.
|
|
145
169
|
yield* pruneStaleAgentBranch(deps.cwd, deps.baseBranch, branch).pipe(Effect.catchAll((err) => Effect.logWarning(`${issue.identifier}: pruneStaleAgentBranch failed (continuing): ${err.message}`)));
|
|
146
|
-
const impl = yield* runImplementLoop(issue, deps, branch);
|
|
170
|
+
const impl = yield* runImplementLoop(issue, { ...deps, priorFeedback }, branch);
|
|
147
171
|
if (impl.kind === "hitl") {
|
|
148
172
|
yield* flagHitl(issue, deps, impl.reason);
|
|
149
173
|
return { kind: "hitl", detail: impl.reason };
|
package/dist/prompts.js
CHANGED
|
@@ -54,6 +54,7 @@ function implementVars(args) {
|
|
|
54
54
|
ISSUE_TITLE: args.issue.title,
|
|
55
55
|
ISSUE_DESCRIPTION: args.issue.description || "(no description)",
|
|
56
56
|
PREVIOUS_ITERATIONS: args.previousIterations,
|
|
57
|
+
PRIOR_REVIEW_FEEDBACK: args.priorReviewFeedback,
|
|
57
58
|
POLICY_FORBIDDEN_BULLET: renderForbiddenPathsBullet(args.policy),
|
|
58
59
|
};
|
|
59
60
|
}
|
|
@@ -66,6 +67,67 @@ function reviewVars(args) {
|
|
|
66
67
|
COMMITS: args.commits || "(no commits)",
|
|
67
68
|
};
|
|
68
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* VA-383: known orchestrator-emitted comment prefixes that are
|
|
72
|
+
* bookkeeping noise, not feedback the impl agent should learn from.
|
|
73
|
+
* Filtering by author alone is insufficient because runway and the
|
|
74
|
+
* reviewer both post under the API-key owner's identity — so
|
|
75
|
+
* "Runway picked up this issue. Branch: …" would otherwise leak into
|
|
76
|
+
* the prior-feedback block on every retry.
|
|
77
|
+
*
|
|
78
|
+
* Intentionally does NOT include `Runway flagged for human review:` —
|
|
79
|
+
* that comment carries the reviewer's rejection reason, which is the
|
|
80
|
+
* load-bearing signal VA-383 exists to surface.
|
|
81
|
+
*/
|
|
82
|
+
const RUNWAY_BOOKKEEPING_PREFIXES = [
|
|
83
|
+
"Runway picked up this issue",
|
|
84
|
+
"Runway opened a PR for review:",
|
|
85
|
+
"Runway hit a startup failure",
|
|
86
|
+
"Note: could not apply",
|
|
87
|
+
];
|
|
88
|
+
const isRunwayBookkeeping = (body) => RUNWAY_BOOKKEEPING_PREFIXES.some((p) => body.startsWith(p));
|
|
89
|
+
/**
|
|
90
|
+
* VA-383: filter + format the comment set fetched off an issue into
|
|
91
|
+
* the "Review feedback from prior attempts" block the implement
|
|
92
|
+
* prompt renders. Pure, side-effect-free, no Linear deps — the
|
|
93
|
+
* orchestrator owns I/O, this owns the shape.
|
|
94
|
+
*
|
|
95
|
+
* Comments survive the filter when **all** of:
|
|
96
|
+
* 1. author name is in `allowlist` (case-sensitive exact match)
|
|
97
|
+
* 2. body does NOT begin with a known runway bookkeeping prefix
|
|
98
|
+
* (orchestrator transitions, PR-opened announcements, etc.)
|
|
99
|
+
*
|
|
100
|
+
* Returns the empty string when nothing makes it through — the
|
|
101
|
+
* `{{PRIOR_REVIEW_FEEDBACK}}` slot expands to empty and the section
|
|
102
|
+
* disappears from the rendered prompt entirely.
|
|
103
|
+
*/
|
|
104
|
+
export function formatPriorFeedback(comments, allowlist) {
|
|
105
|
+
const allowed = new Set(allowlist);
|
|
106
|
+
const surviving = comments.filter((c) => allowed.has(c.author) && !isRunwayBookkeeping(c.body));
|
|
107
|
+
if (surviving.length === 0)
|
|
108
|
+
return "";
|
|
109
|
+
// Already sorted ascending by createdAt at the gateway boundary —
|
|
110
|
+
// re-sort here defensively in case a caller hands us an unsorted
|
|
111
|
+
// array (the test fixture, for instance, asserts ordering explicitly).
|
|
112
|
+
const ordered = [...surviving].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
113
|
+
const entries = ordered.map((c) => {
|
|
114
|
+
const when = c.createdAt.toISOString();
|
|
115
|
+
return `### ${when} — ${c.author}\n\n${c.body.trim()}`;
|
|
116
|
+
});
|
|
117
|
+
return [
|
|
118
|
+
"# Review feedback from prior attempts",
|
|
119
|
+
"",
|
|
120
|
+
"Prior runway attempts on this issue were rejected. Each block below is a",
|
|
121
|
+
"review comment to internalize **before** redoing the work — these are the",
|
|
122
|
+
"specific blockers that caused the previous attempt to be rejected. The",
|
|
123
|
+
"issue description above is the spec; the comments here are augmenting",
|
|
124
|
+
"guidance. If the two contradict, prefer the spec and call out the conflict",
|
|
125
|
+
"in your final message.",
|
|
126
|
+
"",
|
|
127
|
+
entries.join("\n\n"),
|
|
128
|
+
"",
|
|
129
|
+
].join("\n");
|
|
130
|
+
}
|
|
69
131
|
/**
|
|
70
132
|
* VA-352 (absorbed from policy.ts in VA-361): render the bullet
|
|
71
133
|
* sentence the impl prompt shows the agent. Stable formatting so a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valescoagency/runway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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": {
|
|
@@ -41,26 +41,26 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@ai-hero/sandcastle": "^0.5.10",
|
|
43
43
|
"@effect/opentelemetry": "^0.63.0",
|
|
44
|
-
"@linear/sdk": "^
|
|
44
|
+
"@linear/sdk": "^84.0.0",
|
|
45
45
|
"@opentelemetry/api": "^1.9.1",
|
|
46
46
|
"@opentelemetry/exporter-trace-otlp-http": "^0.217.0",
|
|
47
47
|
"@opentelemetry/resources": "^2.7.1",
|
|
48
48
|
"@opentelemetry/sdk-trace-base": "^2.7.1",
|
|
49
49
|
"@opentelemetry/sdk-trace-node": "^2.7.1",
|
|
50
|
-
"@opentelemetry/semantic-conventions": "^1.
|
|
50
|
+
"@opentelemetry/semantic-conventions": "^1.41.1",
|
|
51
51
|
"effect": "^3.21.2",
|
|
52
|
-
"execa": "^9.
|
|
52
|
+
"execa": "^9.6.1",
|
|
53
53
|
"yaml": "^2.9.0",
|
|
54
|
-
"zod": "^
|
|
54
|
+
"zod": "^4.4.3"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@commitlint/cli": "^21.0.
|
|
58
|
-
"@commitlint/config-conventional": "^21.0.
|
|
59
|
-
"@types/node": "^
|
|
57
|
+
"@commitlint/cli": "^21.0.1",
|
|
58
|
+
"@commitlint/config-conventional": "^21.0.1",
|
|
59
|
+
"@types/node": "^25.7.0",
|
|
60
60
|
"lefthook": "^2.1.6",
|
|
61
|
-
"tsx": "^4.
|
|
62
|
-
"typescript": "^
|
|
63
|
-
"vitest": "^4.1.
|
|
61
|
+
"tsx": "^4.21.0",
|
|
62
|
+
"typescript": "^6.0.3",
|
|
63
|
+
"vitest": "^4.1.6"
|
|
64
64
|
},
|
|
65
65
|
"engines": {
|
|
66
66
|
"node": ">=22"
|