@valescoagency/runway 0.7.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 CHANGED
@@ -392,7 +392,7 @@ These are tractable, just not v1.
392
392
 
393
393
  ## Status
394
394
 
395
- 0.6.0 — production-shaped and dogfooded against live Linear queues.
395
+ 0.7.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/linear.js CHANGED
@@ -26,6 +26,17 @@ 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
+ });
29
40
  // VA-383: comments fetched for surfacing prior-attempt review feedback
30
41
  // to the implementer. `user` is nullable on Linear's side (system /
31
42
  // integration comments can lack an author); we map a missing user to
@@ -51,6 +62,56 @@ const decodeIssueLabelNode = Schema.decodeUnknownSync(IssueLabelNodeSchema);
51
62
  const decodeProjectNode = Schema.decodeUnknownSync(ProjectNodeSchema);
52
63
  const decodeCommentNode = Schema.decodeUnknownSync(CommentNodeSchema);
53
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
+ }
54
115
  export class LinearNotFound extends Data.TaggedError("LinearNotFound") {
55
116
  }
56
117
  export class LinearUnauthorized extends Data.TaggedError("LinearUnauthorized") {
@@ -239,15 +300,30 @@ export function createLinearGateway(config, limiter = null) {
239
300
  // VA-360: validate every issue node through the schema —
240
301
  // a single drifted issue surfaces as `LinearSchemaError`
241
302
  // instead of a downstream `cannot read property X`.
242
- return issues.nodes.map((raw) => {
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) {
243
314
  const i = decodeIssueNode(raw);
244
- return {
315
+ if (await hasHitlLabel(raw, config.hitlLabel))
316
+ continue;
317
+ if (await hasActiveBlocker(raw))
318
+ continue;
319
+ eligible.push({
245
320
  id: i.id,
246
321
  identifier: i.identifier,
247
322
  title: i.title,
248
323
  description: i.description ?? "",
249
- };
250
- });
324
+ });
325
+ }
326
+ return eligible;
251
327
  },
252
328
  catch: (err) => classifyLinearError(err, "fetchReady"),
253
329
  }), { call: "fetchReady" }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valescoagency/runway",
3
- "version": "0.7.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": {