@gr8ful/spf 0.6.0 → 0.8.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 +122 -27
- package/assets/prompts/refiner/system.md +11 -1
- package/assets/prompts/refiner/user.md +9 -3
- package/assets/skill/references/config.md +51 -13
- package/assets/templates/ts.spf.config.yaml +6 -2
- package/dist/chains/context.d.ts +26 -0
- package/dist/chains/simple_sdlc.js +9 -0
- package/dist/chains/steps.d.ts +0 -27
- package/dist/chains/steps.js +21 -2
- package/dist/cli/ask.d.ts +13 -0
- package/dist/cli/ask.js +15 -1
- package/dist/cli/commands/doctor.js +47 -9
- package/dist/cli/commands/fanout.js +49 -5
- package/dist/cli/commands/init.js +11 -3
- package/dist/cli/commands/list.d.ts +1 -1
- package/dist/cli/commands/list.js +31 -12
- package/dist/cli/commands/phases.d.ts +1 -1
- package/dist/cli/commands/phases.js +18 -4
- package/dist/cli/commands/run.js +30 -2
- package/dist/cli/commands/sessions.d.ts +1 -1
- package/dist/cli/commands/sessions.js +11 -3
- package/dist/cli/commands/watch.d.ts +8 -0
- package/dist/cli/commands/watch.js +93 -13
- package/dist/cli/index.js +4 -4
- package/dist/cli/interview.js +9 -5
- package/dist/cli/ui/fanout_dashboard.d.ts +22 -0
- package/dist/cli/ui/fanout_dashboard.js +102 -0
- package/dist/cli/ui/ink_asker.d.ts +13 -0
- package/dist/cli/ui/ink_asker.js +247 -0
- package/dist/cli/ui/reports.d.ts +30 -0
- package/dist/cli/ui/reports.js +61 -0
- package/dist/cli/ui/run_dashboard.d.ts +15 -0
- package/dist/cli/ui/run_dashboard.js +131 -0
- package/dist/cli/ui/watch_dashboard.d.ts +22 -0
- package/dist/cli/ui/watch_dashboard.js +78 -0
- package/dist/core/console.d.ts +40 -1
- package/dist/core/console.js +25 -3
- package/dist/core/data_types.d.ts +108 -5
- package/dist/core/data_types.js +50 -5
- package/dist/core/fanout.d.ts +9 -0
- package/dist/core/fanout.js +6 -2
- package/dist/core/gates.js +24 -1
- package/dist/core/issues/github_provider.d.ts +39 -5
- package/dist/core/issues/github_provider.js +103 -4
- package/dist/core/issues/jira_provider.d.ts +79 -12
- package/dist/core/issues/jira_provider.js +97 -2
- package/dist/core/issues/provider.d.ts +73 -19
- package/dist/core/issues/provider.js +24 -7
- package/dist/core/notify/channel.d.ts +1 -1
- package/dist/core/refine.d.ts +45 -8
- package/dist/core/refine.js +98 -24
- package/dist/core/runner.d.ts +5 -1
- package/dist/core/runner.js +2 -1
- package/dist/core/session.d.ts +7 -1
- package/dist/core/session.js +5 -1
- package/dist/core/watch.d.ts +86 -3
- package/dist/core/watch.js +353 -29
- package/package.json +6 -1
package/dist/core/refine.js
CHANGED
|
@@ -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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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),
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* `
|
|
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
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
parts.push(`## Blocked by\n\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>`
|
|
116
|
-
* see `WatchState`'s doc comment in
|
|
117
|
-
* via the tracker's native
|
|
118
|
-
*
|
|
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
|
|
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);
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* parsed envelope + green gates, enforced inside ph.call).
|
|
10
10
|
*/
|
|
11
11
|
import { type GitHandle } from "./git_helper.ts";
|
|
12
|
-
import { Console } from "./console.ts";
|
|
12
|
+
import { Console, type RunObserver } from "./console.ts";
|
|
13
13
|
import { Tracer } from "./tracer.ts";
|
|
14
14
|
import { type AgentCall, type EnvelopeBase, type Phase, type PhaseParams, type SFConfig } from "./data_types.ts";
|
|
15
15
|
import type { TierResolution } from "./tiering.ts";
|
|
@@ -38,6 +38,10 @@ export interface RunInit {
|
|
|
38
38
|
chainName?: string;
|
|
39
39
|
/** `null`/omitted when notifications are off (the default) or no channel resolved. */
|
|
40
40
|
notifier?: Notifier | null;
|
|
41
|
+
/** Where a printed line goes — see `Console`'s own constructor doc. Omitted everywhere except an interactive `cli/commands/run.ts` dispatch. */
|
|
42
|
+
sink?: (line: string) => void;
|
|
43
|
+
/** See `RunObserver`'s doc comment (`core/console.ts`). `null`/omitted outside an interactive dispatch. */
|
|
44
|
+
observer?: RunObserver | null;
|
|
41
45
|
}
|
|
42
46
|
export declare class Run {
|
|
43
47
|
cfg: SFConfig;
|
package/dist/core/runner.js
CHANGED
|
@@ -78,7 +78,7 @@ export class Run {
|
|
|
78
78
|
this.adw_id = init.adwId;
|
|
79
79
|
this.tracer = init.tracer;
|
|
80
80
|
this.notify = init.notifier ?? null;
|
|
81
|
-
this.console = new Console(init.tracer, init.adwId, this.notify, init.chainName || "adw");
|
|
81
|
+
this.console = new Console(init.tracer, init.adwId, this.notify, init.chainName || "adw", init.sink, init.observer);
|
|
82
82
|
this.engineer = init.engineer;
|
|
83
83
|
this.seq = init.tracer.maxPhaseSeq(init.adwId);
|
|
84
84
|
this.repo_root = init.repoRoot;
|
|
@@ -100,6 +100,7 @@ export class Run {
|
|
|
100
100
|
this.tokens += tokens;
|
|
101
101
|
this.cost += cost;
|
|
102
102
|
this.tracer.sessionAddUsage(this.adw_id, tokens, cost);
|
|
103
|
+
this.console.notifyUsage(this.tokens, this.cost);
|
|
103
104
|
}
|
|
104
105
|
// ── the phase primitive ─────────────────────────────────────────────────
|
|
105
106
|
async phase(params, fn) {
|
package/dist/core/session.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* minted and printed so the next ADW can pick it up.
|
|
7
7
|
*/
|
|
8
8
|
import { Run } from "./runner.ts";
|
|
9
|
+
import type { RunObserver } from "./console.ts";
|
|
9
10
|
import type { SFConfig } from "./data_types.ts";
|
|
10
11
|
/**
|
|
11
12
|
* The symmetric teardown for `finalizeWhenKilled()` above: drop `adwId` from
|
|
@@ -43,4 +44,9 @@ export declare function activeRunIdsForTest(): string[];
|
|
|
43
44
|
* longer a `process.argv[1]` basename that means anything. Direct callers
|
|
44
45
|
* that have no chain of their own fall back to `"adw"`.
|
|
45
46
|
*/
|
|
46
|
-
export declare function ensure(cfg: SFConfig, adwId?: string | null, cwd?: string, chainName?: string
|
|
47
|
+
export declare function ensure(cfg: SFConfig, adwId?: string | null, cwd?: string, chainName?: string,
|
|
48
|
+
/** See `RunObserver`'s doc comment (`core/console.ts`). Omitted for every caller except an interactive `cli/commands/run.ts` dispatch — a `spf watch` per-issue run, `spf fanout`'s per-attempt runs, and every test all continue to build a plain, unobserved `Console`. */
|
|
49
|
+
renderHooks?: {
|
|
50
|
+
sink?: (line: string) => void;
|
|
51
|
+
observer?: RunObserver | null;
|
|
52
|
+
}): Run;
|
package/dist/core/session.js
CHANGED
|
@@ -138,7 +138,9 @@ export function activeRunIdsForTest() {
|
|
|
138
138
|
* longer a `process.argv[1]` basename that means anything. Direct callers
|
|
139
139
|
* that have no chain of their own fall back to `"adw"`.
|
|
140
140
|
*/
|
|
141
|
-
export function ensure(cfg, adwId, cwd, chainName
|
|
141
|
+
export function ensure(cfg, adwId, cwd, chainName,
|
|
142
|
+
/** See `RunObserver`'s doc comment (`core/console.ts`). Omitted for every caller except an interactive `cli/commands/run.ts` dispatch — a `spf watch` per-issue run, `spf fanout`'s per-attempt runs, and every test all continue to build a plain, unobserved `Console`. */
|
|
143
|
+
renderHooks) {
|
|
142
144
|
const id = adwId || newId(8);
|
|
143
145
|
const anchor = paths.resolveAnchor(cwd);
|
|
144
146
|
const dataPaths = paths.resolveDataPaths(anchor, cfg.defaults.data_dir, cfg.observability.db);
|
|
@@ -161,6 +163,8 @@ export function ensure(cfg, adwId, cwd, chainName) {
|
|
|
161
163
|
dataDir: dataPaths.data_dir,
|
|
162
164
|
chainName: chainName || "adw",
|
|
163
165
|
notifier: resolveNotifier(cfg),
|
|
166
|
+
sink: renderHooks?.sink,
|
|
167
|
+
observer: renderHooks?.observer,
|
|
164
168
|
});
|
|
165
169
|
const scriptPath = process.argv[1] || "adw";
|
|
166
170
|
tracer.sessionStart(id, run.engineer, chainName || "adw");
|
package/dist/core/watch.d.ts
CHANGED
|
@@ -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 `
|
|
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
|
-
/**
|
|
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
|