@valescoagency/runway 0.6.0 → 0.7.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 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
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,31 @@ const IssueLabelNodeSchema = Schema.Struct({
26
26
  const ProjectNodeSchema = Schema.Struct({
27
27
  id: Schema.String,
28
28
  });
29
+ // VA-383: comments fetched for surfacing prior-attempt review feedback
30
+ // to the implementer. `user` is nullable on Linear's side (system /
31
+ // integration comments can lack an author); we map a missing user to
32
+ // the empty string so downstream filters can match by name without a
33
+ // null-guard at every call site.
34
+ const CommentNodeSchema = Schema.Struct({
35
+ id: Schema.String,
36
+ body: Schema.String,
37
+ createdAt: Schema.Union(Schema.String, Schema.DateFromSelf),
38
+ user: Schema.NullOr(Schema.Struct({
39
+ id: Schema.String,
40
+ name: Schema.String,
41
+ })),
42
+ });
43
+ const ViewerSchema = Schema.Struct({
44
+ id: Schema.String,
45
+ name: Schema.String,
46
+ });
29
47
  const decodeIssueNode = Schema.decodeUnknownSync(IssueNodeSchema);
30
48
  const decodeWorkflowStateNode = Schema.decodeUnknownSync(WorkflowStateNodeSchema);
31
49
  const decodeTeamNode = Schema.decodeUnknownSync(TeamNodeSchema);
32
50
  const decodeIssueLabelNode = Schema.decodeUnknownSync(IssueLabelNodeSchema);
33
51
  const decodeProjectNode = Schema.decodeUnknownSync(ProjectNodeSchema);
52
+ const decodeCommentNode = Schema.decodeUnknownSync(CommentNodeSchema);
53
+ const decodeViewer = Schema.decodeUnknownSync(ViewerSchema);
34
54
  export class LinearNotFound extends Data.TaggedError("LinearNotFound") {
35
55
  }
36
56
  export class LinearUnauthorized extends Data.TaggedError("LinearUnauthorized") {
@@ -293,6 +313,42 @@ export function createLinearGateway(config, limiter = null) {
293
313
  catch: (err) => classifyLinearError(err, "comment"),
294
314
  }), { call: "comment" }));
295
315
  },
316
+ fetchComments(issueId) {
317
+ return gate(applyLinearPolicy(Effect.tryPromise({
318
+ try: async () => {
319
+ // Linear SDK paginates by default — use the issue-scoped
320
+ // accessor and let the SDK's first page suffice for the
321
+ // common case (Runway's HITL + reviewer flow rarely
322
+ // produces more than a handful per attempt). If issues
323
+ // start blowing past the page limit, fold in pagination.
324
+ const issue = await client.issue(issueId);
325
+ const comments = await issue.comments();
326
+ const decoded = comments.nodes.map((raw) => {
327
+ const c = decodeCommentNode(raw);
328
+ const createdAt = c.createdAt instanceof Date ? c.createdAt : new Date(c.createdAt);
329
+ return {
330
+ id: c.id,
331
+ author: c.user?.name ?? "",
332
+ body: c.body,
333
+ createdAt,
334
+ };
335
+ });
336
+ // VA-383: ascending by createdAt so the impl agent reads
337
+ // feedback in the order it was given.
338
+ return [...decoded].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
339
+ },
340
+ catch: (err) => classifyLinearError(err, "fetchComments"),
341
+ }), { call: "fetchComments" }));
342
+ },
343
+ viewer() {
344
+ return gate(applyLinearPolicy(Effect.tryPromise({
345
+ try: async () => {
346
+ const v = await client.viewer;
347
+ return decodeViewer({ id: v.id, name: v.name });
348
+ },
349
+ catch: (err) => classifyLinearError(err, "viewer"),
350
+ }), { call: "viewer" }));
351
+ },
296
352
  };
297
353
  }
298
354
  export async function validateLinearConfig(config) {
@@ -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.6.0",
3
+ "version": "0.7.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": {
@@ -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": "^41.0.0",
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.40.0",
50
+ "@opentelemetry/semantic-conventions": "^1.41.1",
51
51
  "effect": "^3.21.2",
52
- "execa": "^9.5.2",
52
+ "execa": "^9.6.1",
53
53
  "yaml": "^2.9.0",
54
- "zod": "^3.23.8"
54
+ "zod": "^4.4.3"
55
55
  },
56
56
  "devDependencies": {
57
- "@commitlint/cli": "^21.0.0",
58
- "@commitlint/config-conventional": "^21.0.0",
59
- "@types/node": "^22.10.0",
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.19.2",
62
- "typescript": "^5.7.2",
63
- "vitest": "^4.1.5"
61
+ "tsx": "^4.21.0",
62
+ "typescript": "^6.0.3",
63
+ "vitest": "^4.1.6"
64
64
  },
65
65
  "engines": {
66
66
  "node": ">=22"
@@ -6,6 +6,8 @@ You are an autonomous coding agent working on a single Linear issue.
6
6
 
7
7
  {{ISSUE_DESCRIPTION}}
8
8
 
9
+ {{PRIOR_REVIEW_FEEDBACK}}
10
+
9
11
  {{PREVIOUS_ITERATIONS}}
10
12
 
11
13
  # Repository context