@gr8ful/spf 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.
@@ -1,5 +1,5 @@
1
1
  import type { Issue, IssueAuthoringProvider } from "./issues/provider.ts";
2
- import type { RefinedIssue, SFConfig } from "./data_types.ts";
2
+ import { type RefinedIssue, type RefinedPriority, type SFConfig } from "./data_types.ts";
3
3
  export interface PublishedIssue {
4
4
  /** The `RefinedIssue.key` this came from — a run-local id, never a tracker id. */
5
5
  key: string;
@@ -9,25 +9,62 @@ export interface PublishedIssue {
9
9
  isLeaf: boolean;
10
10
  }
11
11
  /**
12
- * `IssueAuthoringProvider` has a real implementation only on `GitHubProvider`
13
- * todaysee `jira_provider.ts`'s module comment on why Jira isn't wired up
14
- * yet. Throws rather than returning `null` so a `code` phase calling this
12
+ * `IssueAuthoringProvider` has a real implementation on `GitHubProvider` and
13
+ * `JiraProvider`any other `issue_provider` value fails here, defensively
14
+ * (the config schema's picklist already rejects it earlier). Throws rather
15
+ * than returning `null` so a `code` phase calling this
15
16
  * (`steps.publishIssues()`) fails the phase with a clear, specific reason —
16
17
  * the same "fail loudly, never silently do nothing" contract
17
18
  * `agents.validate()` uses for an unconfigured quality suite.
19
+ *
20
+ * Duplicates `cli/commands/watch.ts`'s own `resolveIssueProvider`
21
+ * construction logic for each provider — a pre-existing pattern for GitHub
22
+ * (this function has always rebuilt its own `GitHubProvider` rather than
23
+ * sharing one with the CLI layer's build-lane provider), mirrored for Jira
24
+ * rather than refactored away, to stay within this change's scope.
18
25
  */
19
26
  export declare function resolveAuthoringProvider(cfg: SFConfig): IssueAuthoringProvider;
27
+ export interface RefineMarker {
28
+ /** The parent's real issue id, or `null` for a top-level node. */
29
+ parent: string | null;
30
+ /** Real issue ids — resolved from `blocked_by` `key`s at publish time, see `renderBody`. */
31
+ blocked_by: string[];
32
+ priority: RefinedPriority;
33
+ }
34
+ /**
35
+ * Pure and exported so it's directly unit-testable without a provider —
36
+ * `core/watch.ts`'s `claimNewWork` and `rollUp` are the real callers, reading
37
+ * it straight out of the `Issue.body` a `listEligible`/`getIssue` call
38
+ * already returned. Never throws: a body with no marker (any issue not
39
+ * created by this lane, or one whose marker a human stripped while editing)
40
+ * degrades to `NO_REFINE_MARKER`, same as malformed JSON inside one.
41
+ */
42
+ export declare function parseRefineMarker(body: string): RefineMarker;
20
43
  export interface PublishOptions {
21
44
  labelPrefix: string;
22
45
  /** The originating spec issue's id, for every created issue's `## Parent` back-reference. `null`/omitted for a manual run with no source issue. */
23
46
  specIssueId?: string | null;
47
+ /**
48
+ * The spec's own priority (its `spf:priority:pN` label, read by
49
+ * `core/watch.ts`'s `runSpec`, or `spf refine`'s `--priority` flag for a
50
+ * bare manual run) — a CEILING, never a floor. Every node is clamped down
51
+ * to this if it outranks it (`clampPriority`), so a p3 "someday" spec
52
+ * cannot spawn p0 work that jumps the build lane's queue, regardless of
53
+ * what the refiner's own per-node judgment (or the gate's monotonicity
54
+ * check, which only sees the tree, never the spec) would otherwise allow.
55
+ * `null`/omitted — a bare run with nothing to inherit from — is a no-op:
56
+ * `clampPriority`'s own default.
57
+ */
58
+ priorityCeiling?: RefinedPriority | null;
24
59
  }
25
60
  /**
26
61
  * Create every node in `issues`, in dependency order, with its
27
- * `<prefix>:type:<kind>` label (plus `<prefix>:refined` on leaves only —
28
- * see `WatchState`'s doc comment in `provider.ts`), link each to its parent
29
- * via the tracker's native hierarchy, and render real `#n` references into
30
- * `## Blocked by`. Returns what it created, in creation order.
62
+ * `<prefix>:type:<kind>` and `<prefix>:priority:<pN>` labels (plus
63
+ * `<prefix>:refined` on leaves only — see `WatchState`'s doc comment in
64
+ * `provider.ts`), link each to its parent via the tracker's native
65
+ * hierarchy, and render real `#n` references into `## Blocked by` plus the
66
+ * hidden `spf-refine:` marker `parseRefineMarker` reads back. Returns what
67
+ * it created, in creation order.
31
68
  *
32
69
  * Not transactional: if a create or link call throws partway through, the
33
70
  * nodes already published stay published, orphaned from whatever hadn't run
@@ -14,18 +14,37 @@
14
14
  * `runSpec` closes that gap using what this module returns).
15
15
  */
16
16
  import { GitHubProvider } from "./issues/github_provider.js";
17
+ import { JiraProvider } from "./issues/jira_provider.js";
18
+ import { clampPriority } from "./data_types.js";
17
19
  /**
18
- * `IssueAuthoringProvider` has a real implementation only on `GitHubProvider`
19
- * todaysee `jira_provider.ts`'s module comment on why Jira isn't wired up
20
- * yet. Throws rather than returning `null` so a `code` phase calling this
20
+ * `IssueAuthoringProvider` has a real implementation on `GitHubProvider` and
21
+ * `JiraProvider`any other `issue_provider` value fails here, defensively
22
+ * (the config schema's picklist already rejects it earlier). Throws rather
23
+ * than returning `null` so a `code` phase calling this
21
24
  * (`steps.publishIssues()`) fails the phase with a clear, specific reason —
22
25
  * the same "fail loudly, never silently do nothing" contract
23
26
  * `agents.validate()` uses for an unconfigured quality suite.
27
+ *
28
+ * Duplicates `cli/commands/watch.ts`'s own `resolveIssueProvider`
29
+ * construction logic for each provider — a pre-existing pattern for GitHub
30
+ * (this function has always rebuilt its own `GitHubProvider` rather than
31
+ * sharing one with the CLI layer's build-lane provider), mirrored for Jira
32
+ * rather than refactored away, to stay within this change's scope.
24
33
  */
25
34
  export function resolveAuthoringProvider(cfg) {
35
+ if (cfg.watch.issue_provider === "jira") {
36
+ if (!cfg.watch.jira.base_url.trim() || !cfg.watch.jira.project_key.trim()) {
37
+ throw new Error(`watch.jira.base_url and watch.jira.project_key must both be set when watch.issue_provider is "jira"`);
38
+ }
39
+ const email = process.env["JIRA_EMAIL"];
40
+ const token = process.env["JIRA_API_TOKEN"];
41
+ if (!email || !token) {
42
+ throw new Error('JIRA_EMAIL and JIRA_API_TOKEN must both be set — the refine lane needs an Atlassian account email plus an API token (id.atlassian.com -> Security -> API tokens)');
43
+ }
44
+ return new JiraProvider(cfg.watch.jira.base_url, cfg.watch.jira.project_key, cfg.watch.label_prefix, email, token, cfg.watch.jira.issue_types);
45
+ }
26
46
  if (cfg.watch.issue_provider !== "github") {
27
- throw new Error(`watch.issue_provider ${JSON.stringify(cfg.watch.issue_provider)} does not support issue authoring — ` +
28
- `the refine lane needs "github" (see jira_provider.ts's module comment on why Jira isn't wired up yet)`);
47
+ throw new Error(`watch.issue_provider ${JSON.stringify(cfg.watch.issue_provider)} does not support issue authoring — the refine lane needs "github" or "jira"`);
29
48
  }
30
49
  // Issue authoring always targets the ISSUE tracker's repo — `issue_repo`
31
50
  // if set, falling back to plain `repo` (the common case: issue_provider
@@ -47,31 +66,78 @@ export function resolveAuthoringProvider(cfg) {
47
66
  function typeLabel(labelPrefix, kind) {
48
67
  return `${labelPrefix}:type:${kind}`;
49
68
  }
69
+ /** `spf:priority:p0..p3` — see `RefinedPrioritySchema`'s doc comment for what each rung means. Mirrors `typeLabel` above, and `github_provider.ts`'s own private `priorityLabel()` method (used by `ensureLabels()` to seed these) — the two aren't unified for the same reason `typeLabel` isn't: this file stays provider-agnostic, building label strings by convention rather than reaching into a concrete `GitHubProvider`. */
70
+ function priorityLabel(labelPrefix, priority) {
71
+ return `${labelPrefix}:priority:${priority}`;
72
+ }
73
+ /**
74
+ * Sentinel distinct from `github_provider.ts`'s `MARKER_RE` on purpose: that
75
+ * one matches a hidden comment (`spf watch`'s own scratch state — worktree,
76
+ * branch, PR number); this one matches a hidden block INSIDE the issue BODY
77
+ * this file renders — the graph `spf watch`'s build lane needs to schedule
78
+ * correctly (`parent`, `blocked_by`, `priority`) but that gets lost once
79
+ * `blocked_by` is flattened to the human-readable `## Blocked by` prose
80
+ * below. The two live in different places on the issue and are read by
81
+ * different code (`readMarker`'s comment scan vs. a plain `Issue.body`
82
+ * parse), so a single tracker `GET` — which already returns the body — is
83
+ * all `claimNewWork`'s ordering needs, no per-issue marker-comment fetch.
84
+ */
85
+ const REFINE_MARKER_RE = /<!--\s*spf-refine:\s*(\{.*?\})\s*-->/s;
86
+ /** What `parseRefineMarker` returns absent (or on a malformed/hand-edited) marker — exactly today's pre-priority, pre-frontier behavior: no parent, no blockers, the default priority. */
87
+ const NO_REFINE_MARKER = { parent: null, blocked_by: [], priority: "p2" };
88
+ /**
89
+ * Pure and exported so it's directly unit-testable without a provider —
90
+ * `core/watch.ts`'s `claimNewWork` and `rollUp` are the real callers, reading
91
+ * it straight out of the `Issue.body` a `listEligible`/`getIssue` call
92
+ * already returned. Never throws: a body with no marker (any issue not
93
+ * created by this lane, or one whose marker a human stripped while editing)
94
+ * degrades to `NO_REFINE_MARKER`, same as malformed JSON inside one.
95
+ */
96
+ export function parseRefineMarker(body) {
97
+ const match = REFINE_MARKER_RE.exec(body);
98
+ if (!match)
99
+ return NO_REFINE_MARKER;
100
+ try {
101
+ const parsed = JSON.parse(match[1]);
102
+ return {
103
+ parent: typeof parsed.parent === "string" ? parsed.parent : null,
104
+ blocked_by: Array.isArray(parsed.blocked_by) ? parsed.blocked_by.filter((b) => typeof b === "string") : [],
105
+ priority: ["p0", "p1", "p2", "p3"].includes(parsed.priority) ? parsed.priority : "p2",
106
+ };
107
+ }
108
+ catch {
109
+ return NO_REFINE_MARKER; // malformed marker JSON — tolerate it, same policy as github_provider.ts's own findMarkerComment
110
+ }
111
+ }
50
112
  /**
51
113
  * The body GitHub actually stores: the refiner's own `## What to build` /
52
114
  * `## Acceptance criteria` text, plus a `## Parent` back-reference to the
53
115
  * source spec (when there is one — a bare `spf refine` run with no
54
- * `--issue` has none), plus a `## Blocked by` section with real `#n`
55
- * references — `to-tickets`' own template shape, ported. Every `blocked_by`
56
- * key is guaranteed to already be in `byKey` by the time this runs:
57
- * `topoOrder` visits a node's dependencies before the node itself.
116
+ * `--issue` has none), a `## Blocked by` section with real `#n` references
117
+ * — `to-tickets`' own template shape, ported and finally the hidden
118
+ * `spf-refine:` marker `parseRefineMarker` reads back. Every `blocked_by`
119
+ * key (and `node.parent`) is guaranteed to already be in `byKey` by the time
120
+ * this runs: `topoOrder` visits a node's dependencies before the node itself.
58
121
  */
59
122
  function renderBody(node, byKey, specIssueId) {
60
123
  const parts = [node.body.trim()];
61
124
  if (specIssueId)
62
125
  parts.push(`## Parent\n\nDecomposed from #${specIssueId}.`);
63
- if (node.blocked_by.length > 0) {
64
- const refs = node.blocked_by.map((key) => {
65
- const published = byKey.get(key);
66
- // Defensive only: gates.refinementWellFormed already rejects a
67
- // blocked_by key that doesn't resolve to another node in the list.
68
- return published ? `#${published.issue.id}` : key;
69
- });
70
- parts.push(`## Blocked by\n\n${refs.map((r) => `- ${r}`).join("\n")}`);
126
+ const blockedByIds = node.blocked_by.map((key) => {
127
+ const published = byKey.get(key);
128
+ // Defensive only: gates.refinementWellFormed already rejects a
129
+ // blocked_by key that doesn't resolve to another node in the list.
130
+ return published ? published.issue.id : key;
131
+ });
132
+ if (blockedByIds.length > 0) {
133
+ parts.push(`## Blocked by\n\n${blockedByIds.map((id) => `- #${id}`).join("\n")}`);
71
134
  }
72
135
  else {
73
136
  parts.push(`## Blocked by\n\nNone (can start immediately).`);
74
137
  }
138
+ const parentId = node.parent ? byKey.get(node.parent)?.issue.id ?? node.parent : null;
139
+ const marker = { parent: parentId, blocked_by: blockedByIds, priority: node.priority };
140
+ parts.push(`<!-- spf-refine: ${JSON.stringify(marker)} -->`);
75
141
  return parts.join("\n\n");
76
142
  }
77
143
  /**
@@ -112,10 +178,12 @@ function topoOrder(issues) {
112
178
  }
113
179
  /**
114
180
  * Create every node in `issues`, in dependency order, with its
115
- * `<prefix>:type:<kind>` label (plus `<prefix>:refined` on leaves only —
116
- * see `WatchState`'s doc comment in `provider.ts`), link each to its parent
117
- * via the tracker's native hierarchy, and render real `#n` references into
118
- * `## Blocked by`. Returns what it created, in creation order.
181
+ * `<prefix>:type:<kind>` and `<prefix>:priority:<pN>` labels (plus
182
+ * `<prefix>:refined` on leaves only — see `WatchState`'s doc comment in
183
+ * `provider.ts`), link each to its parent via the tracker's native
184
+ * hierarchy, and render real `#n` references into `## Blocked by` plus the
185
+ * hidden `spf-refine:` marker `parseRefineMarker` reads back. Returns what
186
+ * it created, in creation order.
119
187
  *
120
188
  * Not transactional: if a create or link call throws partway through, the
121
189
  * nodes already published stay published, orphaned from whatever hadn't run
@@ -129,13 +197,19 @@ export async function publish(tracker, issues, opts) {
129
197
  const childKeys = new Set(issues.filter((i) => i.parent).map((i) => i.parent));
130
198
  const byKey = new Map();
131
199
  const created = [];
132
- for (const node of ordered) {
200
+ for (const rawNode of ordered) {
201
+ // Clamp once, up front — every downstream use (the label AND the hidden
202
+ // marker's own `priority`) must agree, or `claimNewWork`'s label-based
203
+ // ordering and a human reading the marker would disagree about what this
204
+ // issue's priority actually is.
205
+ const priority = clampPriority(rawNode.priority, opts.priorityCeiling);
206
+ const node = { ...rawNode, priority };
133
207
  const isLeaf = !childKeys.has(node.key);
134
- const labels = [typeLabel(opts.labelPrefix, node.kind)];
208
+ const labels = [typeLabel(opts.labelPrefix, node.kind), priorityLabel(opts.labelPrefix, priority)];
135
209
  if (isLeaf)
136
210
  labels.push(`${opts.labelPrefix}:refined`);
137
211
  const body = renderBody(node, byKey, opts.specIssueId);
138
- const issue = await tracker.createIssue({ title: node.title, body, labels });
212
+ const issue = await tracker.createIssue({ title: node.title, body, labels, kind: node.kind });
139
213
  const published = { key: node.key, issue, kind: node.kind, isLeaf };
140
214
  byKey.set(node.key, published);
141
215
  created.push(published);
@@ -1,6 +1,7 @@
1
1
  import type { GitHandle } from "./git_helper.ts";
2
2
  import type { CodeHostProvider, Issue, IssueComment, IssueProvider, WatchMarker, WatchState } from "./issues/provider.ts";
3
3
  import type { NotifyEvent } from "./notify/channel.ts";
4
+ import { type RefinedPriority } from "./data_types.ts";
4
5
  export interface ChainRunResult {
5
6
  accepted: boolean;
6
7
  adwId: string;
@@ -18,7 +19,7 @@ export interface ChainRunResult {
18
19
  /** Whether the chain that ran declares a "reviewer" in its `requiredAgents` — distinguishes "reviewer approved" from "nothing reviewed this change" when `reviewSummary` is absent. */
19
20
  reviewRequired?: boolean;
20
21
  }
21
- /** One issue the refine lane created — enough for `finishSpec`'s summary comment and the marker's idempotency record. */
22
+ /** One issue the refine lane created — enough for `announceRefined`'s summary comment and the marker's idempotency record. */
22
23
  export interface RefinedIssueRef {
23
24
  id: string;
24
25
  title: string;
@@ -110,6 +111,18 @@ export interface WatchDeps {
110
111
  adwId: string;
111
112
  chainOptions: Record<string, string>;
112
113
  }) => Promise<ChainRunResult>;
114
+ /**
115
+ * `IssueAuthoringProvider.listChildren`'s read-back, injected as a bound
116
+ * function rather than a whole provider object — same reasoning as
117
+ * `runChain`/`runRefine`/`linkDataDir`: this module drives whatever it's
118
+ * given without importing a concrete provider type. `undefined` on any
119
+ * tracker that doesn't implement `IssueAuthoringProvider` (Jira today —
120
+ * see `jira_provider.ts`'s module comment); `rollUp` treats that as a
121
+ * logged no-op, not a failure — the build lane still functions without
122
+ * container roll-up, unlike the refine lane, which cannot function
123
+ * without authoring at all (see `cli/commands/watch.ts`'s startup check).
124
+ */
125
+ listChildren?: (parent: Issue) => Promise<Issue[]>;
113
126
  log: (message: string) => void;
114
127
  /**
115
128
  * Structured push, alongside `log`'s plain string — a required field, like
@@ -134,6 +147,20 @@ export interface WatchRunState {
134
147
  * that isn't why this is separate — the budgets are what require it.
135
148
  */
136
149
  refining: Set<string>;
150
+ /**
151
+ * Issue id -> its container's real issue id, recorded the moment
152
+ * `claimNewWork` claims a leaf whose hidden `spf-refine:` marker names a
153
+ * parent, deleted in the same `.finally()` that clears `inflight`. Zero
154
+ * API cost — no tracker read needed to populate it — and it's the whole
155
+ * basis of sibling affinity in `orderEligible`: a sibling of something
156
+ * already in flight sorts ahead of an equal-priority issue from an
157
+ * unrelated feature, so a feature already underway tends to finish before
158
+ * the daemon starts a new one instead of interleaving both. This holds
159
+ * only while a sibling is ACTUALLY in flight — a daemon restart begins
160
+ * with this empty, so ordering falls back to priority + created-asc until
161
+ * the in-memory picture rebuilds itself over the next few ticks.
162
+ */
163
+ inflightParents: Map<string, string>;
137
164
  }
138
165
  export declare function createWatchState(): WatchRunState;
139
166
  export declare function branchNameFor(issue: Issue): string;
@@ -146,6 +173,29 @@ export declare function refineBranchNameFor(issue: Issue): string;
146
173
  * merged) PR; otherwise retry up to `MAX_ORPHAN_ATTEMPTS`, then give up.
147
174
  */
148
175
  export declare function reconcileOrphans(deps: WatchDeps, state: WatchRunState): Promise<void>;
176
+ /**
177
+ * Poll every `spec-in-progress` spec: once every id `WatchMarker.refined`
178
+ * recorded — every issue the refiner produced, leaf or container — carries
179
+ * `<prefix>:done`, the spec's own decomposed work is actually finished, and
180
+ * only then does this move it `-> done`. This is the whole point of
181
+ * `announceRefined` landing on `spec-in-progress` rather than `done`
182
+ * straight away: the spec's status is what a product manager reads to know
183
+ * whether the work is finished, and "done" the instant a tree gets PUBLISHED
184
+ * would be a lie — the work hasn't started yet, let alone finished.
185
+ *
186
+ * A container in the refined list is done exactly when `rollUp` (see
187
+ * `finishReviews`) has already rolled it up — by the time every id here is
188
+ * `<prefix>:done`, every leaf beneath every container is too, transitively,
189
+ * with no need to walk the hierarchy again from this side.
190
+ *
191
+ * A referenced id that 404s (deleted from the tracker) is treated as
192
+ * satisfied — same policy as `frontierBlockedOn`'s blockers: a removed issue
193
+ * must not wedge the spec's completion forever. A spec with no marker, or an
194
+ * empty `refined` list, is left alone with a log line rather than assumed
195
+ * done — data that shouldn't exist given the gate's at-least-one-leaf rule,
196
+ * but never silently marked complete on that assumption.
197
+ */
198
+ export declare function finishTrackedSpecs(deps: WatchDeps): Promise<void>;
149
199
  /**
150
200
  * Render the spec issue's comment thread for the refiner's prompt, replacing
151
201
  * the previous bare `title\n\nbody` — the whole reason a human's answer
@@ -161,9 +211,17 @@ export declare function reconcileOrphans(deps: WatchDeps, state: WatchRunState):
161
211
  * `MAX_THREAD_CHARS`, dropping the oldest comments first — an explicit
162
212
  * "N earlier comment(s) omitted" line, never a silent truncation.
163
213
  *
214
+ * `priority` — the spec's own `<prefix>:priority:pN` label, read by
215
+ * `runSpec` below — renders as its own `## Priority` section right after the
216
+ * header, present or absent independent of whether there's any comment
217
+ * thread at all: a spec with no priority label (the common case today) omits
218
+ * the section entirely, exactly the prompt this function produced before
219
+ * priority existed. `core/refine.ts`'s `publish()` is what actually ENFORCES
220
+ * the ceiling this section only asks for — see its own doc comment.
221
+ *
164
222
  * Exported and pure (no provider, no I/O) so it's directly unit-testable.
165
223
  */
166
- export declare function buildSpecPrompt(issue: Issue, comments: IssueComment[], feedback?: WatchMarker["feedback"]): string;
224
+ export declare function buildSpecPrompt(issue: Issue, comments: IssueComment[], feedback?: WatchMarker["feedback"], priority?: RefinedPriority | null): string;
167
225
  /**
168
226
  * The refine lane's own `reconcileOrphans` — a `refining`-labeled spec this
169
227
  * process isn't tracking is one of three things: a completed publish that
@@ -179,7 +237,32 @@ export declare function buildSpecPrompt(issue: Issue, comments: IssueComment[],
179
237
  export declare function reconcileRefining(deps: WatchDeps, state: WatchRunState): Promise<void>;
180
238
  /** Poll every `review`-labeled issue's PR for merged (-> done) or closed-without-merging (-> blocked). */
181
239
  export declare function finishReviews(deps: WatchDeps): Promise<void>;
182
- /** Claim as many `ready` issues as the concurrency budget allows, and kick off `runIssue` for each in the background. */
240
+ /**
241
+ * Sort `issues` the way `claimNewWork` walks them: priority first (p0 ahead
242
+ * of p2 regardless of creation order), then sibling affinity (a leaf whose
243
+ * hidden marker names a parent already in `inflightParents.values()` sorts
244
+ * ahead of an equal-priority leaf from an unrelated feature — the mechanism
245
+ * that tends to finish one feature before starting the next, without giving
246
+ * up the one-PR-per-story design), then creation order (oldest first,
247
+ * matching `listByLabel`'s own `sort=created&direction=asc` — the final,
248
+ * stable tiebreaker when priority and affinity both tie).
249
+ *
250
+ * Pure and exported so it's directly unit-testable without a provider, same
251
+ * spirit as `buildSpecPrompt` below. Two honest limits, both already true of
252
+ * what feeds it: affinity only reflects a sibling ACTUALLY in flight in this
253
+ * process right now — a daemon restart begins with `inflightParents` empty,
254
+ * so ordering degrades to priority + created-asc until it rebuilds itself
255
+ * over the next few ticks; and priority is read from the label, so an issue
256
+ * a human just relabeled sorts by its NEW priority starting next tick, never
257
+ * retroactively re-ordering claims a previous tick already made.
258
+ */
259
+ export declare function orderEligible(issues: Issue[], inflightParents: Map<string, string>, labelPrefix: string): Issue[];
260
+ /**
261
+ * Claim as many `ready` issues as the concurrency budget allows, in priority
262
+ * + sibling-affinity + created-asc order (`orderEligible`), skipping any
263
+ * whose `blocked_by` isn't fully `<prefix>:done` yet (`frontierBlockedOn`),
264
+ * and kick off `runIssue` for each claimed one in the background.
265
+ */
183
266
  export declare function claimNewWork(deps: WatchDeps, state: WatchRunState): Promise<void>;
184
267
  /**
185
268
  * Claim as many specs in `from` as `refineConcurrency` allows, and kick off