@gr8ful/spf 0.7.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/dist/chains/context.d.ts +26 -0
- package/dist/chains/simple_sdlc.js +9 -0
- package/dist/chains/steps.js +1 -1
- package/dist/cli/ask.d.ts +13 -0
- package/dist/cli/ask.js +15 -1
- package/dist/cli/commands/doctor.js +43 -7
- package/dist/cli/commands/fanout.js +49 -5
- package/dist/cli/commands/init.js +7 -2
- 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 +21 -0
- package/dist/cli/commands/sessions.d.ts +1 -1
- package/dist/cli/commands/sessions.js +11 -3
- package/dist/cli/commands/watch.js +38 -6
- package/dist/cli/index.js +3 -3
- 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/fanout.d.ts +9 -0
- package/dist/core/fanout.js +6 -2
- package/dist/core/issues/github_provider.js +16 -1
- package/dist/core/issues/jira_provider.js +13 -1
- 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/package.json +6 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A live status line for `spf watch` — the exact same log lines
|
|
4
|
+
* `WatchDeps.log()` already prints (the daemon banner, each tick's
|
|
5
|
+
* claim/finish lines), in a `<Static>` history, plus one live line below
|
|
6
|
+
* it: how many issues are in flight, how many are being refined, and a
|
|
7
|
+
* countdown to the next poll.
|
|
8
|
+
*
|
|
9
|
+
* `core/watch.ts` is untouched — this only ever swaps `cli/commands/
|
|
10
|
+
* watch.ts`'s own `WatchDeps.log` callback and reads `WatchRunState`'s
|
|
11
|
+
* public `.size` after each `tick()` returns, exactly the seam the plan
|
|
12
|
+
* called for ("no core changes"). `notify` keeps going to the real
|
|
13
|
+
* `Notifier` unchanged (Slack/Teams/webhook delivery has nothing to do
|
|
14
|
+
* with terminal rendering); this only ALSO mirrors a one-line summary of
|
|
15
|
+
* each event into the same log history, for visibility.
|
|
16
|
+
*
|
|
17
|
+
* One persistent Ink instance for the whole `spf watch` invocation — same
|
|
18
|
+
* shape as `run_dashboard.tsx`/`fanout_dashboard.tsx`. `spf watch`'s own
|
|
19
|
+
* SIGINT handling exits immediately on a second Ctrl-C (`process.exit(130)`
|
|
20
|
+
* in `commands/watch.ts`'s `stop()`), so `unmountNow()` exists here for the
|
|
21
|
+
* same best-effort, no-flush-wait reason `fanout_dashboard.tsx`'s does.
|
|
22
|
+
*/
|
|
23
|
+
import { useEffect, useRef, useState } from "react";
|
|
24
|
+
import { render, Static, Box, Text } from "ink";
|
|
25
|
+
function Countdown({ deadlineMs }) {
|
|
26
|
+
const [now, setNow] = useState(() => Date.now());
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
29
|
+
return () => clearInterval(id);
|
|
30
|
+
}, []);
|
|
31
|
+
const remaining = Math.max(0, Math.round((deadlineMs - now) / 1000));
|
|
32
|
+
return _jsxs(Text, { dimColor: true, children: ["next poll in ", remaining, "s"] });
|
|
33
|
+
}
|
|
34
|
+
function WatchRoot(props) {
|
|
35
|
+
const [log, setLog] = useState([]);
|
|
36
|
+
const [counts, setCounts] = useState({ inflight: 0, refining: 0 });
|
|
37
|
+
const [nextPollAt, setNextPollAt] = useState(null);
|
|
38
|
+
const keyRef = useRef(0); // a ref, not state — see ink_asker.tsx's identical comment on why two pushes in one tick must not collide
|
|
39
|
+
props.handleRef.current = {
|
|
40
|
+
pushLog(text) {
|
|
41
|
+
const key = keyRef.current++;
|
|
42
|
+
setLog((prev) => [...prev, { key, text }]);
|
|
43
|
+
},
|
|
44
|
+
setCounts: (inflight, refining) => setCounts({ inflight, refining }),
|
|
45
|
+
setNextPollAt,
|
|
46
|
+
};
|
|
47
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: log, children: (line) => _jsx(Text, { children: line.text }, line.key) }), _jsx(Box, { children: _jsxs(Text, { children: [props.repo, " ", _jsxs(Text, { dimColor: true, children: ["label \"", props.labelPrefix, ":*\" \u00B7 chain \"", props.chain, "\" \u00B7 concurrency ", props.concurrency] }), props.refineChain ? (_jsxs(Text, { dimColor: true, children: [" ", "\u00B7 refine \"", props.refineChain, "\" concurrency ", props.refineConcurrency] })) : null, props.dryRun ? _jsx(Text, { color: "yellow", children: " (dry run)" }) : null] }) }), _jsxs(Box, { children: [_jsxs(Text, { children: [_jsxs(Text, { color: counts.inflight > 0 ? "cyan" : undefined, children: [counts.inflight, " in flight"] }), counts.refining > 0 ? _jsxs(Text, { color: "magenta", children: [", ", counts.refining, " refining"] }) : null, " · "] }), nextPollAt !== null ? _jsx(Countdown, { deadlineMs: nextPollAt }) : _jsx(Text, { dimColor: true, children: "ticking\u2026" })] })] }));
|
|
48
|
+
}
|
|
49
|
+
export function mountWatchDashboard(opts) {
|
|
50
|
+
const handleRef = { current: null };
|
|
51
|
+
const app = render(_jsx(WatchRoot, { repo: opts.repo, labelPrefix: opts.labelPrefix, chain: opts.chain, concurrency: opts.concurrency, refineChain: opts.refineChain, refineConcurrency: opts.refineConcurrency, dryRun: opts.dryRun, handleRef: handleRef }),
|
|
52
|
+
// `interactive: true` overrides Ink's own CI auto-detection — see
|
|
53
|
+
// `ink_asker.tsx`'s identical `render()` call for why: the caller
|
|
54
|
+
// (`commands/watch.ts`) only reaches this file after its own
|
|
55
|
+
// `isInteractive()` check has already passed.
|
|
56
|
+
{ patchConsole: false, interactive: true });
|
|
57
|
+
return {
|
|
58
|
+
log(message) {
|
|
59
|
+
handleRef.current?.pushLog(message);
|
|
60
|
+
},
|
|
61
|
+
mirrorNotify(event) {
|
|
62
|
+
handleRef.current?.pushLog(`[notify] ${event.title}${event.detail ? ` — ${event.detail}` : ""}`);
|
|
63
|
+
},
|
|
64
|
+
setCounts(inflight, refining) {
|
|
65
|
+
handleRef.current?.setCounts(inflight, refining);
|
|
66
|
+
},
|
|
67
|
+
setNextPollAt(deadlineMs) {
|
|
68
|
+
handleRef.current?.setNextPollAt(deadlineMs);
|
|
69
|
+
},
|
|
70
|
+
async close() {
|
|
71
|
+
await app.waitUntilRenderFlush();
|
|
72
|
+
app.unmount();
|
|
73
|
+
},
|
|
74
|
+
unmountNow() {
|
|
75
|
+
app.unmount();
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
package/dist/core/console.d.ts
CHANGED
|
@@ -15,6 +15,21 @@ export declare function paint(style: string, text: string): string;
|
|
|
15
15
|
interface Tracer {
|
|
16
16
|
event(record: EventRecord): string;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* A run-scoped hook for a live TTY view (`cli/ui/run_dashboard.tsx`) to
|
|
20
|
+
* track phase state and spend without re-parsing `Console`'s formatted
|
|
21
|
+
* lines — deliberately narrow (phase transitions + usage, not every method
|
|
22
|
+
* below) since that's the whole surface a "which phase is running, how
|
|
23
|
+
* long, how much so far" dashboard needs. Every hook is optional and every
|
|
24
|
+
* `Console` call site that doesn't pass one behaves exactly as before this
|
|
25
|
+
* existed.
|
|
26
|
+
*/
|
|
27
|
+
export interface RunObserver {
|
|
28
|
+
onPhaseStart?(phase: Phase): void;
|
|
29
|
+
onPhaseEnd?(phase: Phase, seconds: number): void;
|
|
30
|
+
onUsage?(tokens: number, cost: number): void;
|
|
31
|
+
onSessionEnd?(ok: boolean): void;
|
|
32
|
+
}
|
|
18
33
|
/** Bound to one run's tracer. Reachable as `run.console` everywhere. */
|
|
19
34
|
export declare class Console {
|
|
20
35
|
private tracer;
|
|
@@ -23,6 +38,17 @@ export declare class Console {
|
|
|
23
38
|
private notifier;
|
|
24
39
|
/** The CLI chain name (`"plan-build-test"`), for a notification's title — see session.ts. */
|
|
25
40
|
private chainName;
|
|
41
|
+
/**
|
|
42
|
+
* Where a printed line actually goes — defaults to `console.log`, exactly
|
|
43
|
+
* the prior behavior. `cli/commands/run.ts` is the only caller that ever
|
|
44
|
+
* passes something else: on a real TTY, a sink that feeds a live Ink
|
|
45
|
+
* dashboard's scrollback instead of writing straight to stdout. The
|
|
46
|
+
* tracer side of `emit()` below is UNCHANGED either way — this only
|
|
47
|
+
* repoints the print half of "one narrative, two destinations".
|
|
48
|
+
*/
|
|
49
|
+
private sink;
|
|
50
|
+
/** See `RunObserver`'s own doc comment. `null` outside an interactive `cli/commands/run.ts` dispatch. */
|
|
51
|
+
private observer;
|
|
26
52
|
private phaseId;
|
|
27
53
|
private phaseName;
|
|
28
54
|
private results;
|
|
@@ -31,7 +57,18 @@ export declare class Console {
|
|
|
31
57
|
/** `null` when notifications are off — every call site below guards with `?.`. */
|
|
32
58
|
notifier?: Notifier | null,
|
|
33
59
|
/** The CLI chain name (`"plan-build-test"`), for a notification's title — see session.ts. */
|
|
34
|
-
chainName?: string
|
|
60
|
+
chainName?: string,
|
|
61
|
+
/**
|
|
62
|
+
* Where a printed line actually goes — defaults to `console.log`, exactly
|
|
63
|
+
* the prior behavior. `cli/commands/run.ts` is the only caller that ever
|
|
64
|
+
* passes something else: on a real TTY, a sink that feeds a live Ink
|
|
65
|
+
* dashboard's scrollback instead of writing straight to stdout. The
|
|
66
|
+
* tracer side of `emit()` below is UNCHANGED either way — this only
|
|
67
|
+
* repoints the print half of "one narrative, two destinations".
|
|
68
|
+
*/
|
|
69
|
+
sink?: (line: string) => void,
|
|
70
|
+
/** See `RunObserver`'s own doc comment. `null` outside an interactive `cli/commands/run.ts` dispatch. */
|
|
71
|
+
observer?: RunObserver | null);
|
|
35
72
|
private emit;
|
|
36
73
|
sessionStarted(adwId: string, engineer: string): void;
|
|
37
74
|
sessionFinished(ok: boolean, tokens: number, cost: number, dbPath: string): void;
|
|
@@ -39,6 +76,8 @@ export declare class Console {
|
|
|
39
76
|
phaseEnded(phase: Phase, seconds: number): void;
|
|
40
77
|
/** Free-form detail inside the current phase — what `ph.log()` recorded. */
|
|
41
78
|
note(message: string): void;
|
|
79
|
+
/** `Run.addUsage()`'s only hook into `Console` — the running total lives on `Run`, not here, so this just forwards it to the observer. No line prints for this on its own; the totals already show up in `sessionFinished`'s panel. */
|
|
80
|
+
notifyUsage(tokens: number, cost: number): void;
|
|
42
81
|
agentStarted(name: string, model: string, sessionId: string): void;
|
|
43
82
|
agentFinished(name: string, tokens: number, cost: number): void;
|
|
44
83
|
retry(name: string, attempt: number, limit: number, reason: string): void;
|
package/dist/core/console.js
CHANGED
|
@@ -47,6 +47,8 @@ export class Console {
|
|
|
47
47
|
adwId;
|
|
48
48
|
notifier;
|
|
49
49
|
chainName;
|
|
50
|
+
sink;
|
|
51
|
+
observer;
|
|
50
52
|
phaseId = ""; // current lane — log events attach to it
|
|
51
53
|
phaseName = "";
|
|
52
54
|
results = []; // phase statuses, for the summary
|
|
@@ -55,15 +57,28 @@ export class Console {
|
|
|
55
57
|
/** `null` when notifications are off — every call site below guards with `?.`. */
|
|
56
58
|
notifier = null,
|
|
57
59
|
/** The CLI chain name (`"plan-build-test"`), for a notification's title — see session.ts. */
|
|
58
|
-
chainName = "adw"
|
|
60
|
+
chainName = "adw",
|
|
61
|
+
/**
|
|
62
|
+
* Where a printed line actually goes — defaults to `console.log`, exactly
|
|
63
|
+
* the prior behavior. `cli/commands/run.ts` is the only caller that ever
|
|
64
|
+
* passes something else: on a real TTY, a sink that feeds a live Ink
|
|
65
|
+
* dashboard's scrollback instead of writing straight to stdout. The
|
|
66
|
+
* tracer side of `emit()` below is UNCHANGED either way — this only
|
|
67
|
+
* repoints the print half of "one narrative, two destinations".
|
|
68
|
+
*/
|
|
69
|
+
sink = console.log,
|
|
70
|
+
/** See `RunObserver`'s own doc comment. `null` outside an interactive `cli/commands/run.ts` dispatch. */
|
|
71
|
+
observer = null) {
|
|
59
72
|
this.tracer = tracer;
|
|
60
73
|
this.adwId = adwId;
|
|
61
74
|
this.notifier = notifier;
|
|
62
75
|
this.chainName = chainName;
|
|
76
|
+
this.sink = sink;
|
|
77
|
+
this.observer = observer;
|
|
63
78
|
}
|
|
64
79
|
// ── the one helper: print AND trace, always together ────────────────────
|
|
65
80
|
emit(line, level = "info") {
|
|
66
|
-
|
|
81
|
+
this.sink(line);
|
|
67
82
|
this.tracer.event(makeEventRecord({
|
|
68
83
|
adw_id: this.adwId,
|
|
69
84
|
phase_id: this.phaseId,
|
|
@@ -101,7 +116,8 @@ export class Console {
|
|
|
101
116
|
` ${paint("dim", "next")} ${paint("bold", `just phases ${this.adwId}`)}`,
|
|
102
117
|
];
|
|
103
118
|
const rendered = panel(rows, "ADW complete", ok ? "green" : "red");
|
|
104
|
-
|
|
119
|
+
this.sink(rendered);
|
|
120
|
+
this.observer?.onSessionEnd?.(ok);
|
|
105
121
|
const plain = `session ${this.adwId} ${ok ? "success" : "fail"} · ${passed}/${this.results.length} phases · ${tokens.toLocaleString()} tokens · $${cost.toFixed(4)}`;
|
|
106
122
|
this.tracer.event(makeEventRecord({
|
|
107
123
|
adw_id: this.adwId,
|
|
@@ -133,10 +149,12 @@ export class Console {
|
|
|
133
149
|
if (p.description)
|
|
134
150
|
line += ` ${paint("dim", clip(p.description))}`;
|
|
135
151
|
this.emit(line);
|
|
152
|
+
this.observer?.onPhaseStart?.(phase);
|
|
136
153
|
}
|
|
137
154
|
phaseEnded(phase, seconds) {
|
|
138
155
|
const ok = phase.status === "success";
|
|
139
156
|
this.results.push(phase.status);
|
|
157
|
+
this.observer?.onPhaseEnd?.(phase, seconds);
|
|
140
158
|
let line = ` ${ok ? paint("green", "✓") : paint("red", "✗")} ${phase.params.name} ${paint("dim", `${seconds.toFixed(1)}s`)}`;
|
|
141
159
|
if (!ok && phase.error)
|
|
142
160
|
line += ` ${paint("red", clip(phase.error))}`;
|
|
@@ -161,6 +179,10 @@ export class Console {
|
|
|
161
179
|
note(message) {
|
|
162
180
|
this.emit(` ${paint("dim", `· ${clip(message)}`)}`);
|
|
163
181
|
}
|
|
182
|
+
/** `Run.addUsage()`'s only hook into `Console` — the running total lives on `Run`, not here, so this just forwards it to the observer. No line prints for this on its own; the totals already show up in `sessionFinished`'s panel. */
|
|
183
|
+
notifyUsage(tokens, cost) {
|
|
184
|
+
this.observer?.onUsage?.(tokens, cost);
|
|
185
|
+
}
|
|
164
186
|
// ── agents ──────────────────────────────────────────────────────────────
|
|
165
187
|
agentStarted(name, model, sessionId) {
|
|
166
188
|
this.emit(` ${paint("magenta", "▸")} ${name} ${paint("dim", model)} ${paint("dim", `session ${sessionId}`)}`);
|
package/dist/core/fanout.d.ts
CHANGED
|
@@ -214,6 +214,15 @@ export interface FanoutDeps {
|
|
|
214
214
|
* ABORT GRANULARITY note.
|
|
215
215
|
*/
|
|
216
216
|
firstSuccess?: boolean;
|
|
217
|
+
/**
|
|
218
|
+
* Fires once per attempt the moment it settles (success, fail, error, or
|
|
219
|
+
* skipped) — the same record that lands in the final `attempts` array, just
|
|
220
|
+
* as it happens instead of only once every worker has finished. Purely
|
|
221
|
+
* observational: nothing here changes selection or cleanup. `cli/commands/
|
|
222
|
+
* fanout.ts` uses it to update a live results table row by row on a TTY;
|
|
223
|
+
* omitted (the default) everywhere else, including every test.
|
|
224
|
+
*/
|
|
225
|
+
onAttempt?: (attempt: FanoutAttempt) => void;
|
|
217
226
|
}
|
|
218
227
|
/**
|
|
219
228
|
* Run `n` attempts of one prompt, select deterministically, clean up the
|
package/dist/core/fanout.js
CHANGED
|
@@ -284,10 +284,14 @@ export async function runBestOf(deps) {
|
|
|
284
284
|
return;
|
|
285
285
|
if (decided) {
|
|
286
286
|
deps.log(`fanout: attempt ${index} skipped — an earlier attempt already succeeded`);
|
|
287
|
-
|
|
287
|
+
const attempt = skipped(index);
|
|
288
|
+
attempts.push(attempt);
|
|
289
|
+
deps.onAttempt?.(attempt);
|
|
288
290
|
continue;
|
|
289
291
|
}
|
|
290
|
-
|
|
292
|
+
const attempt = await runOne(index);
|
|
293
|
+
attempts.push(attempt);
|
|
294
|
+
deps.onAttempt?.(attempt);
|
|
291
295
|
}
|
|
292
296
|
};
|
|
293
297
|
const workers = Array.from({ length: Math.max(1, Math.min(deps.concurrency, deps.n)) }, () => worker());
|
|
@@ -12,7 +12,22 @@ const STATES = [
|
|
|
12
12
|
"continue-refinement",
|
|
13
13
|
"spec-in-progress",
|
|
14
14
|
];
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* GREEDY capture, not lazy: `WatchMarker.feedback` nests its own object
|
|
17
|
+
* (`{rounds, asked_at}`), so a lazy `\{.*?\}` stops at the FIRST `}` it
|
|
18
|
+
* finds — which is `feedback`'s own closing brace, not the marker's outer
|
|
19
|
+
* one — capturing an unbalanced, unparseable fragment the instant a spec
|
|
20
|
+
* escalates even once. `JSON.parse` then throws, is silently caught, and
|
|
21
|
+
* `readMarker` returns `null` forever after that point: the round counter
|
|
22
|
+
* can never advance past 1, a fresh marker comment gets posted every tick
|
|
23
|
+
* instead of the existing one being edited in place, and — worst of all —
|
|
24
|
+
* `buildSpecPrompt` never receives a valid `asked_at`, so a human's answer
|
|
25
|
+
* is never recognized as one, just dumped into undifferentiated "earlier
|
|
26
|
+
* discussion." A greedy match backtracks from the END of the string to find
|
|
27
|
+
* the LAST `}` — the marker's own outer brace — which is exactly right
|
|
28
|
+
* here since nothing meaningful follows it but the closing `-->`.
|
|
29
|
+
*/
|
|
30
|
+
const MARKER_RE = /<!--\s*spf-watch:\s*(\{.*\})\s*-->/s;
|
|
16
31
|
/** `listByLabel`'s pagination bound — see its own doc comment. */
|
|
17
32
|
const MAX_LIST_PAGES = 5;
|
|
18
33
|
/** The refine lane's leaf/container taxonomy — see `data_types.ts`'s `RefinedIssueSchema.kind`. Not a `WatchState`: these never appear on the left of a `transition()` call, so `transition()` never strips them. */
|
|
@@ -11,7 +11,19 @@ const STATES = [
|
|
|
11
11
|
"continue-refinement",
|
|
12
12
|
"spec-in-progress",
|
|
13
13
|
];
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* GREEDY capture, not lazy — same fix and same reasoning as
|
|
16
|
+
* `github_provider.ts`'s own `MARKER_RE`: `WatchMarker.feedback` nests its
|
|
17
|
+
* own object, so a lazy `\{.*?\}` stops at `feedback`'s own closing brace
|
|
18
|
+
* instead of the marker's outer one, producing unparseable JSON the moment
|
|
19
|
+
* a spec escalates even once — `readMarker` then returns `null` forever,
|
|
20
|
+
* `writeMarker` can never find the existing marker to edit in place (a new
|
|
21
|
+
* one gets posted every tick instead), and `buildSpecPrompt` never sees a
|
|
22
|
+
* valid `asked_at`, so a human's answer never gets flagged as one. Greedy
|
|
23
|
+
* backtracks from the end of the comment body to the true last `}` — safe
|
|
24
|
+
* here because nothing follows the JSON in this format at all.
|
|
25
|
+
*/
|
|
26
|
+
const MARKER_RE = /\[spf-watch-marker\]\s*(\{.*\})/s;
|
|
15
27
|
function toAdf(text) {
|
|
16
28
|
return {
|
|
17
29
|
type: "doc",
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gr8ful/spf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Super Portable Factory — a global CLI for repeatable agents-plus-code workflows (ADWs)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,8 +44,11 @@
|
|
|
44
44
|
"@earendil-works/pi-ai": "0.83.0",
|
|
45
45
|
"@flue/runtime": "2.0.3",
|
|
46
46
|
"@hono/node-server": "^2.1.1",
|
|
47
|
+
"@inkjs/ui": "^2.0.0",
|
|
47
48
|
"@valibot/to-json-schema": "^1.7.1",
|
|
48
49
|
"hono": "^4.13.3",
|
|
50
|
+
"ink": "^7.1.1",
|
|
51
|
+
"react": "^19.2.8",
|
|
49
52
|
"valibot": "^1.4.2",
|
|
50
53
|
"yaml": "^2.5.1"
|
|
51
54
|
},
|
|
@@ -54,6 +57,8 @@
|
|
|
54
57
|
},
|
|
55
58
|
"devDependencies": {
|
|
56
59
|
"@types/node": "^22.10.0",
|
|
60
|
+
"@types/react": "^19.2.18",
|
|
61
|
+
"ink-testing-library": "^4.0.0",
|
|
57
62
|
"lefthook": "^2.1.10",
|
|
58
63
|
"typescript": "^7.0.2"
|
|
59
64
|
}
|