@gr8ful/spf 0.19.0 → 0.19.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 +8 -0
- package/assets/skill/references/config.md +1 -0
- package/dist/cli/commands/doctor.js +25 -3
- package/dist/cli/commands/estimate.d.ts +22 -6
- package/dist/cli/commands/estimate.js +32 -10
- package/dist/cli/commands/loop.d.ts +20 -0
- package/dist/cli/commands/loop.js +20 -1
- package/dist/cli/commands/ui.js +2 -1
- package/dist/cli/commands/watch.js +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/interview.js +13 -0
- package/dist/cli/ui/run_dashboard.js +13 -7
- package/dist/core/agent_cc.d.ts +19 -3
- package/dist/core/agent_cc.js +38 -18
- package/dist/core/agent_flue.js +51 -14
- package/dist/core/agent_opencode.d.ts +62 -25
- package/dist/core/agent_opencode.js +71 -30
- package/dist/core/agents.d.ts +51 -4
- package/dist/core/agents.js +79 -4
- package/dist/core/console.d.ts +24 -4
- package/dist/core/console.js +20 -7
- package/dist/core/data_types.d.ts +300 -19
- package/dist/core/data_types.js +134 -5
- package/dist/core/issues/jira_provider.d.ts +51 -1
- package/dist/core/issues/jira_provider.js +69 -1
- package/dist/core/issues/provider.d.ts +23 -0
- package/dist/core/loop.d.ts +39 -1
- package/dist/core/loop.js +33 -2
- package/dist/core/ollama_provider.d.ts +96 -13
- package/dist/core/ollama_provider.js +172 -26
- package/dist/core/otel.js +10 -1
- package/dist/core/otel_propagation.d.ts +168 -24
- package/dist/core/otel_propagation.js +219 -43
- package/dist/core/permissions.d.ts +16 -1
- package/dist/core/permissions.js +91 -3
- package/dist/core/providers.js +8 -3
- package/dist/core/refine.js +13 -1
- package/dist/core/runner.d.ts +33 -2
- package/dist/core/runner.js +40 -5
- package/dist/core/tiering.js +7 -3
- package/dist/core/tracer.d.ts +7 -1
- package/dist/core/tracer.js +15 -3
- package/dist/ui/server/db.d.ts +8 -1
- package/dist/ui/server/db.js +21 -4
- package/dist/ui/server/serve.d.ts +7 -0
- package/dist/ui/server/serve.js +10 -7
- package/dist/ui/shared/types.d.ts +16 -0
- package/package.json +1 -1
package/dist/core/runner.js
CHANGED
|
@@ -66,6 +66,30 @@ export class Run {
|
|
|
66
66
|
phases = [];
|
|
67
67
|
tokens = 0;
|
|
68
68
|
cost = 0;
|
|
69
|
+
/** The BILLABLE half of `tokens` — see `UsageBreakdown.billable_tokens`'s doc comment. What `assertRunBudget` actually checks `defaults.max_run_tokens` against; `tokens` stays the display total. */
|
|
70
|
+
billable_tokens = 0;
|
|
71
|
+
/**
|
|
72
|
+
* True once this run has DISPATCHED at least one `claude_code` agent
|
|
73
|
+
* while `ANTHROPIC_BASE_URL` pointed somewhere other than Anthropic's own
|
|
74
|
+
* API — i.e. a gateway/proxy stood in for Anthropic on a call that
|
|
75
|
+
* actually happened. Read by `Console.sessionFinished` to label the run's
|
|
76
|
+
* printed cost as an estimate rather than a fact: `claude`'s own
|
|
77
|
+
* `total_cost_usd` is ANTHROPIC's price table applied to whatever the CLI
|
|
78
|
+
* thinks it called, which is honest only when Anthropic itself served the
|
|
79
|
+
* request.
|
|
80
|
+
*
|
|
81
|
+
* Starts `false` and is flipped by `recordDispatch()` below, called once
|
|
82
|
+
* per agent dispatch (`agents.ts`'s `execute()`, right before the real
|
|
83
|
+
* coding-agent call) — computed from what this run actually DISPATCHED,
|
|
84
|
+
* never from the roster's static shape (see `agents.ts`'s
|
|
85
|
+
* `isGatewayEstimatedDispatch` vs. `isGatewayEstimatedCost` doc comments
|
|
86
|
+
* for why that distinction matters: a chain can configure a `claude_code`
|
|
87
|
+
* agent it never actually calls this run, and labeling a real cost as
|
|
88
|
+
* "estimated" because the roster merely CONTAINS such an agent would be
|
|
89
|
+
* its own kind of dishonesty). Sticky: once a qualifying dispatch has
|
|
90
|
+
* happened, a later non-qualifying one must never flip it back to false.
|
|
91
|
+
*/
|
|
92
|
+
cost_is_estimate = false;
|
|
69
93
|
repo_root; // where every agent is spawned to work — always absolute
|
|
70
94
|
/** Every git operation for this run, bound to repo_root. Never call git_helper directly. */
|
|
71
95
|
git;
|
|
@@ -107,12 +131,23 @@ export class Run {
|
|
|
107
131
|
this.agent_map[agent] = entry;
|
|
108
132
|
writeFileSync(this.agentMapPath, JSON.stringify(this.agent_map, null, 2));
|
|
109
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Called once per agent dispatch, right before the real coding-agent call
|
|
136
|
+
* (`agents.ts`'s `execute()`) — see `cost_is_estimate`'s own doc comment
|
|
137
|
+
* for why this, not the roster, is what decides the label. Sticky: only
|
|
138
|
+
* ever flips `cost_is_estimate` from false to true, never back.
|
|
139
|
+
*/
|
|
140
|
+
recordDispatch(agent) {
|
|
141
|
+
if (agents.isGatewayEstimatedDispatch(agent))
|
|
142
|
+
this.cost_is_estimate = true;
|
|
143
|
+
}
|
|
110
144
|
// ── usage (run totals mirror what the tracer accumulates in the trace db) ─
|
|
111
|
-
async addUsage(tokens, cost) {
|
|
145
|
+
async addUsage(tokens, cost, billableTokens) {
|
|
112
146
|
this.tokens += tokens;
|
|
113
147
|
this.cost += cost;
|
|
114
|
-
|
|
115
|
-
await this.
|
|
148
|
+
this.billable_tokens += billableTokens;
|
|
149
|
+
await this.tracer.sessionAddUsage(this.adw_id, tokens, cost, billableTokens);
|
|
150
|
+
await this.console.notifyUsage(this.tokens, this.cost, this.billable_tokens);
|
|
116
151
|
}
|
|
117
152
|
// ── the phase primitive ─────────────────────────────────────────────────
|
|
118
153
|
async phase(params, fn) {
|
|
@@ -157,7 +192,7 @@ export class Run {
|
|
|
157
192
|
await this.tracer.phaseUpsert(phase);
|
|
158
193
|
await this.tracer.sessionFinish(this.adw_id, false);
|
|
159
194
|
await this.console.phaseEnded(phase, (performance.now() - clock) / 1000);
|
|
160
|
-
await this.console.sessionFinished(false, this.tokens, this.cost, describeObservabilityDb(this.cfg.observability.db));
|
|
195
|
+
await this.console.sessionFinished(false, this.tokens, this.cost, describeObservabilityDb(this.cfg.observability.db), this.cost_is_estimate);
|
|
161
196
|
throw error;
|
|
162
197
|
}
|
|
163
198
|
}
|
|
@@ -185,7 +220,7 @@ export class Run {
|
|
|
185
220
|
await this.console.note(`not accepted: ${note}`);
|
|
186
221
|
}
|
|
187
222
|
await this.tracer.sessionFinish(this.adw_id, ok);
|
|
188
|
-
await this.console.sessionFinished(ok, this.tokens, this.cost, describeObservabilityDb(this.cfg.observability.db));
|
|
223
|
+
await this.console.sessionFinished(ok, this.tokens, this.cost, describeObservabilityDb(this.cfg.observability.db), this.cost_is_estimate);
|
|
189
224
|
return ok ? 0 : 1;
|
|
190
225
|
}
|
|
191
226
|
}
|
package/dist/core/tiering.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* stands. `effectiveAgent` is the one place that turns a resolution into an
|
|
26
26
|
* actual `AgentConfig` — overriding `model` and NOTHING else.
|
|
27
27
|
*/
|
|
28
|
-
import { ollamaBaseUrl } from "./ollama_provider.js";
|
|
28
|
+
import { ollamaApiKey, ollamaBaseUrl } from "./ollama_provider.js";
|
|
29
29
|
// ── the classifier (design doc §2) ──────────────────────────────────────────
|
|
30
30
|
/**
|
|
31
31
|
* One explicit `name -> weight` table entry per BUILT-IN chain
|
|
@@ -184,9 +184,13 @@ async function fetchServedOllamaTags() {
|
|
|
184
184
|
try {
|
|
185
185
|
// ollamaBaseUrl() is the SAME default-substitution a real dispatch
|
|
186
186
|
// uses, including treating a set-but-EMPTY OLLAMA_BASE_URL as unset —
|
|
187
|
-
// never re-derive that default here.
|
|
187
|
+
// never re-derive that default here. Same for ollamaApiKey(): against a
|
|
188
|
+
// gateway (not bare Ollama) this endpoint 401s without a bearer — send
|
|
189
|
+
// the SAME dummy-or-real bearer a real dispatch sends (byte-identical
|
|
190
|
+
// to doctor.ts's own OLLAMA_BASE_URL reachability probe), or this probe
|
|
191
|
+
// fails closed (401 -> null) even when a real dispatch would succeed.
|
|
188
192
|
const url = ollamaBaseUrl().replace(/\/+$/, "") + "/models";
|
|
189
|
-
const res = await fetch(url, { signal: controller.signal });
|
|
193
|
+
const res = await fetch(url, { signal: controller.signal, headers: { authorization: `Bearer ${ollamaApiKey()}` } });
|
|
190
194
|
if (res.status !== 200)
|
|
191
195
|
return null;
|
|
192
196
|
const body = (await res.json());
|
package/dist/core/tracer.d.ts
CHANGED
|
@@ -87,7 +87,13 @@ export declare class Tracer {
|
|
|
87
87
|
sessionStart(adwId: string, engineer: string, adwName?: string | null): Promise<void>;
|
|
88
88
|
sessionRequest(adwId: string, request: string): Promise<void>;
|
|
89
89
|
sessionFinish(adwId: string, ok: boolean): Promise<void>;
|
|
90
|
-
|
|
90
|
+
/**
|
|
91
|
+
* `tokens`/`cost` are the DISPLAY totals (`total_tokens`, unchanged from
|
|
92
|
+
* before `billable_tokens` existed); `billableTokens` is the new column —
|
|
93
|
+
* see `UsageBreakdown.billable_tokens`'s doc comment for what it excludes
|
|
94
|
+
* and why. Both accumulate in the same row, same as before.
|
|
95
|
+
*/
|
|
96
|
+
sessionAddUsage(adwId: string, tokens: number, cost: number, billableTokens: number): Promise<void>;
|
|
91
97
|
/**
|
|
92
98
|
* Record a live process for this run.
|
|
93
99
|
*
|
package/dist/core/tracer.js
CHANGED
|
@@ -123,6 +123,12 @@ const MIGRATIONS = [
|
|
|
123
123
|
["agent_sessions", "context_tokens", "INTEGER"],
|
|
124
124
|
["agent_sessions", "context_window", "INTEGER"],
|
|
125
125
|
["sessions", "archived", "INTEGER DEFAULT 0"],
|
|
126
|
+
// The BILLABLE half of `total_tokens` — see `UsageBreakdown.billable_tokens`'s
|
|
127
|
+
// own doc comment (`data_types.ts`) for why the two diverge. `total_tokens`
|
|
128
|
+
// is untouched (still every re-sent token, cache reads included); this is
|
|
129
|
+
// the new column `sessionAddUsage` also increments, so a trace db from
|
|
130
|
+
// before this existed just gets a column full of 0 until its next run.
|
|
131
|
+
["sessions", "billable_tokens", "INTEGER DEFAULT 0"],
|
|
126
132
|
];
|
|
127
133
|
export class Tracer {
|
|
128
134
|
db;
|
|
@@ -285,10 +291,16 @@ export class Tracer {
|
|
|
285
291
|
if (processesError)
|
|
286
292
|
throw processesError;
|
|
287
293
|
}
|
|
288
|
-
|
|
294
|
+
/**
|
|
295
|
+
* `tokens`/`cost` are the DISPLAY totals (`total_tokens`, unchanged from
|
|
296
|
+
* before `billable_tokens` existed); `billableTokens` is the new column —
|
|
297
|
+
* see `UsageBreakdown.billable_tokens`'s doc comment for what it excludes
|
|
298
|
+
* and why. Both accumulate in the same row, same as before.
|
|
299
|
+
*/
|
|
300
|
+
async sessionAddUsage(adwId, tokens, cost, billableTokens) {
|
|
289
301
|
await this.db
|
|
290
|
-
.query("UPDATE sessions SET total_tokens=total_tokens+?, total_cost=total_cost+? WHERE adw_id=?")
|
|
291
|
-
.run(tokens, cost, adwId);
|
|
302
|
+
.query("UPDATE sessions SET total_tokens=total_tokens+?, total_cost=total_cost+?, billable_tokens=billable_tokens+? WHERE adw_id=?")
|
|
303
|
+
.run(tokens, cost, billableTokens, adwId);
|
|
292
304
|
}
|
|
293
305
|
// ── processes (adw_id → pid, so a hung run can be found and killed) ─────
|
|
294
306
|
/**
|
package/dist/ui/server/db.d.ts
CHANGED
|
@@ -180,10 +180,17 @@ export interface ChainHistorySession {
|
|
|
180
180
|
started_at: string | null;
|
|
181
181
|
total_tokens: number;
|
|
182
182
|
total_cost: number;
|
|
183
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* In `phases.seq` order; one entry per phase iteration (e.g. `fix_1`,
|
|
185
|
+
* `fix_2`), not yet loop-normalized. `tokens` is the display total;
|
|
186
|
+
* `billable_tokens` is what `spf estimate`'s ceiling projection
|
|
187
|
+
* (`findCutoffPhase`) actually walks against `defaults.max_run_tokens` —
|
|
188
|
+
* see that field's own doc comment above for the old-row fallback.
|
|
189
|
+
*/
|
|
184
190
|
phases: {
|
|
185
191
|
name: string;
|
|
186
192
|
seq: number;
|
|
187
193
|
tokens: number;
|
|
194
|
+
billable_tokens: number;
|
|
188
195
|
}[];
|
|
189
196
|
}
|
package/dist/ui/server/db.js
CHANGED
|
@@ -237,7 +237,7 @@ export class SfDb {
|
|
|
237
237
|
const rows = await this.db
|
|
238
238
|
.query(`SELECT adw_id, ${await this.optionalColumn("sessions", "adw_name")}, request,
|
|
239
239
|
status, engineer, started_at, ended_at,
|
|
240
|
-
total_tokens, total_cost,
|
|
240
|
+
total_tokens, total_cost, ${await this.optionalColumn("sessions", "billable_tokens")},
|
|
241
241
|
${await this.optionalColumn("sessions", "archived")}
|
|
242
242
|
FROM sessions
|
|
243
243
|
WHERE COALESCE(${(await this.hasColumn("sessions", "archived")) ? "archived" : "0"}, 0) = 0
|
|
@@ -282,7 +282,7 @@ export class SfDb {
|
|
|
282
282
|
return ((await this.db
|
|
283
283
|
.query(`SELECT adw_id, ${await this.optionalColumn("sessions", "adw_name")}, request,
|
|
284
284
|
status, engineer, started_at, ended_at,
|
|
285
|
-
total_tokens, total_cost
|
|
285
|
+
total_tokens, total_cost, ${await this.optionalColumn("sessions", "billable_tokens")}
|
|
286
286
|
FROM sessions WHERE adw_id = ?`)
|
|
287
287
|
.get(adwId)) ?? null);
|
|
288
288
|
}
|
|
@@ -497,7 +497,7 @@ export class SfDb {
|
|
|
497
497
|
const phaseRows = await this.chunked(ids, (chunk) => {
|
|
498
498
|
const placeholders = chunk.map(() => "?").join(", ");
|
|
499
499
|
return this.db
|
|
500
|
-
.query(`SELECT p.adw_id, p.name, p.seq, e.tokens
|
|
500
|
+
.query(`SELECT p.adw_id, p.name, p.seq, e.tokens, e.payload_json
|
|
501
501
|
FROM phases p LEFT JOIN events e ON e.phase_id = p.phase_id AND e.type = 'agent_end'
|
|
502
502
|
WHERE p.adw_id IN (${placeholders})
|
|
503
503
|
ORDER BY p.seq ASC`)
|
|
@@ -506,7 +506,24 @@ export class SfDb {
|
|
|
506
506
|
const phasesByAdw = new Map();
|
|
507
507
|
for (const row of phaseRows) {
|
|
508
508
|
const list = phasesByAdw.get(row.adw_id);
|
|
509
|
-
const
|
|
509
|
+
const totalTokens = row.tokens ?? 0;
|
|
510
|
+
// Derived from the payload, same "parse it in JS" approach `usage()`
|
|
511
|
+
// above already uses (never a stored column) — so a phase from before
|
|
512
|
+
// `billable_tokens` existed just falls back to the total, exactly
|
|
513
|
+
// like `Session.billable_tokens`'s own doc comment (`ui/shared/types.ts`)
|
|
514
|
+
// describes for the session-level number.
|
|
515
|
+
let billableTokens = totalTokens;
|
|
516
|
+
if (row.payload_json) {
|
|
517
|
+
try {
|
|
518
|
+
const usage = JSON.parse(row.payload_json).usage;
|
|
519
|
+
if (usage && typeof usage.billable_tokens === "number")
|
|
520
|
+
billableTokens = usage.billable_tokens;
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
/* a payload written by an older tracer simply falls back to the total */
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const entry = { name: row.name, seq: row.seq, tokens: totalTokens, billable_tokens: billableTokens };
|
|
510
527
|
if (list)
|
|
511
528
|
list.push(entry);
|
|
512
529
|
else
|
|
@@ -7,6 +7,13 @@ export interface UiOptions {
|
|
|
7
7
|
webDir: string;
|
|
8
8
|
/** Explicit port. Omit to try the default and probe upward on collision. */
|
|
9
9
|
port?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Bind address. Omit for the safe default (`127.0.0.1`, loopback-only).
|
|
12
|
+
* Set explicitly (e.g. `"0.0.0.0"`, or a specific interface IP) to make
|
|
13
|
+
* this reachable off-box — a deliberate widening, not something a caller
|
|
14
|
+
* should ever get by accident. See the module doc comment above.
|
|
15
|
+
*/
|
|
16
|
+
host?: string;
|
|
10
17
|
open?: boolean;
|
|
11
18
|
}
|
|
12
19
|
export interface UiHandle {
|
package/dist/ui/server/serve.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* `spf ui` bootstrap: bind the app to a real port, print the URL, optionally
|
|
3
3
|
* open a browser, and shut down cleanly on SIGINT/SIGTERM.
|
|
4
4
|
*
|
|
5
|
-
* Binds loopback-only (127.0.0.1) deliberately — this server can
|
|
6
|
-
* database column and reveal agent prompts, and a companion CLI has
|
|
7
|
-
* reason to listen on every interface
|
|
5
|
+
* Binds loopback-only (127.0.0.1) by DEFAULT, deliberately — this server can
|
|
6
|
+
* flip a database column and reveal agent prompts, and a companion CLI has
|
|
7
|
+
* no reason to listen on every interface unless someone explicitly asks for
|
|
8
|
+
* it via `options.host` (`--host` on the CLI). Widening the bind is an
|
|
9
|
+
* opt-in the caller states on purpose, not a default — see issue #90.
|
|
8
10
|
*/
|
|
9
11
|
import { spawn } from "node:child_process";
|
|
10
12
|
import { serve } from "@hono/node-server";
|
|
@@ -15,9 +17,9 @@ const PORT_PROBE_ATTEMPTS = 10;
|
|
|
15
17
|
function isAddrInUse(error) {
|
|
16
18
|
return typeof error === "object" && error !== null && error.code === "EADDRINUSE";
|
|
17
19
|
}
|
|
18
|
-
function listen(app, port) {
|
|
20
|
+
function listen(app, port, hostname) {
|
|
19
21
|
return new Promise((resolvePromise, reject) => {
|
|
20
|
-
const server = serve({ fetch: app.fetch, port, hostname
|
|
22
|
+
const server = serve({ fetch: app.fetch, port, hostname }, () => resolvePromise(server));
|
|
21
23
|
server.on("error", reject);
|
|
22
24
|
});
|
|
23
25
|
}
|
|
@@ -37,6 +39,7 @@ function openUrl(url) {
|
|
|
37
39
|
export async function runUi(options) {
|
|
38
40
|
const db = await SfDb.open(options.db, options.sessionsDir);
|
|
39
41
|
const app = createApp(db, options.webDir);
|
|
42
|
+
const host = options.host ?? "127.0.0.1";
|
|
40
43
|
const explicit = options.port !== undefined;
|
|
41
44
|
const startPort = options.port ?? DEFAULT_PORT;
|
|
42
45
|
let server;
|
|
@@ -45,7 +48,7 @@ export async function runUi(options) {
|
|
|
45
48
|
for (let i = 0; i < attempts; i++) {
|
|
46
49
|
port = startPort + i;
|
|
47
50
|
try {
|
|
48
|
-
server = await listen(app, port);
|
|
51
|
+
server = await listen(app, port, host);
|
|
49
52
|
break;
|
|
50
53
|
}
|
|
51
54
|
catch (error) {
|
|
@@ -61,7 +64,7 @@ export async function runUi(options) {
|
|
|
61
64
|
}
|
|
62
65
|
const info = server.address();
|
|
63
66
|
const boundPort = info?.port ?? port;
|
|
64
|
-
const url = `http
|
|
67
|
+
const url = `http://${host}:${boundPort}`;
|
|
65
68
|
const shouldOpen = options.open !== false && process.stdout.isTTY && !process.env.CI && !process.env.SSH_CONNECTION;
|
|
66
69
|
if (shouldOpen)
|
|
67
70
|
openUrl(url);
|
|
@@ -24,6 +24,22 @@ export interface Session {
|
|
|
24
24
|
ended_at: string | null;
|
|
25
25
|
total_tokens: number | null;
|
|
26
26
|
total_cost: number | null;
|
|
27
|
+
/**
|
|
28
|
+
* The BILLABLE half of `total_tokens` — see `UsageBreakdown.billable_tokens`'s
|
|
29
|
+
* doc comment (`core/data_types.ts`) for why the two diverge.
|
|
30
|
+
*
|
|
31
|
+
* Two ways a row can predate real tracking, both meaning "fall back to
|
|
32
|
+
* `total_tokens`" (see `cli/commands/loop.ts`'s readback):
|
|
33
|
+
* - `null`: this db has never run the migration that adds the column at
|
|
34
|
+
* all (`optionalColumn` in `db.ts` substitutes `NULL AS billable_tokens`
|
|
35
|
+
* against it) — only possible for a db a writer Tracer has never opened
|
|
36
|
+
* since this column shipped.
|
|
37
|
+
* - `0` while `total_tokens > 0`: the column exists (a Tracer's
|
|
38
|
+
* `ALTER TABLE ... DEFAULT 0` ran) but this SPECIFIC row was written
|
|
39
|
+
* before that, so it was backfilled to 0 rather than to the true
|
|
40
|
+
* (unknowable, after the fact) billable figure.
|
|
41
|
+
*/
|
|
42
|
+
billable_tokens: number | null;
|
|
27
43
|
/** 1 once archived out of the review list. Review state, not run state. */
|
|
28
44
|
archived: number | null;
|
|
29
45
|
}
|