@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/console.js
CHANGED
|
@@ -100,17 +100,30 @@ export class Console {
|
|
|
100
100
|
],
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
/**
|
|
104
|
+
* `costIsEstimate` (default `false`, byte-identical to before this param
|
|
105
|
+
* existed): true when `run.cost_is_estimate` found a `claude_code` agent
|
|
106
|
+
* pointed at a non-Anthropic `ANTHROPIC_BASE_URL` (see `runner.ts`'s
|
|
107
|
+
* `Run.recordDispatch` / `agents.ts`'s `isGatewayEstimatedDispatch`) —
|
|
108
|
+
* `total_cost_usd` from `claude`'s own CLI is
|
|
109
|
+
* Anthropic's price table applied client-side, which is a fact only when
|
|
110
|
+
* Anthropic itself served the request, and a labeled guess otherwise. The
|
|
111
|
+
* label is cosmetic only: `cost` itself is unchanged (still the real sum
|
|
112
|
+
* `UsageBreakdown.total_cost` accumulated), and nothing about the budget
|
|
113
|
+
* check (`assertRunBudget`) reads this flag.
|
|
114
|
+
*/
|
|
115
|
+
async sessionFinished(ok, tokens, cost, dbPath, costIsEstimate = false) {
|
|
104
116
|
if (this.finished)
|
|
105
117
|
return;
|
|
106
118
|
this.finished = true;
|
|
107
119
|
const passed = this.results.filter((r) => r === "success").length;
|
|
108
120
|
const status = ok ? paint("green", "✓ success") : paint("red", "✗ fail");
|
|
121
|
+
const costText = costIsEstimate ? `≈ $${cost.toFixed(4)} (claude_code estimate; gateway-billed)` : `$${cost.toFixed(4)}`;
|
|
109
122
|
const rows = [
|
|
110
123
|
` ${paint("dim", "status")} ${status}`,
|
|
111
124
|
` ${paint("dim", "phases")} ${passed}/${this.results.length} passed`,
|
|
112
125
|
` ${paint("dim", "tokens")} ${tokens.toLocaleString()}`,
|
|
113
|
-
` ${paint("dim", "cost")}
|
|
126
|
+
` ${paint("dim", "cost")} ${costText}`,
|
|
114
127
|
` ${paint("dim", "adw_id")} ${this.adwId}`,
|
|
115
128
|
` ${paint("dim", "db")} ${dbPath}`,
|
|
116
129
|
` ${paint("dim", "next")} ${paint("bold", `just phases ${this.adwId}`)}`,
|
|
@@ -118,7 +131,7 @@ export class Console {
|
|
|
118
131
|
const rendered = panel(rows, "ADW complete", ok ? "green" : "red");
|
|
119
132
|
this.sink(rendered);
|
|
120
133
|
this.observer?.onSessionEnd?.(ok);
|
|
121
|
-
const plain = `session ${this.adwId} ${ok ? "success" : "fail"} · ${passed}/${this.results.length} phases · ${tokens.toLocaleString()} tokens ·
|
|
134
|
+
const plain = `session ${this.adwId} ${ok ? "success" : "fail"} · ${passed}/${this.results.length} phases · ${tokens.toLocaleString()} tokens · ${costText}`;
|
|
122
135
|
await this.tracer.event(makeEventRecord({
|
|
123
136
|
adw_id: this.adwId,
|
|
124
137
|
phase_id: this.phaseId,
|
|
@@ -134,7 +147,7 @@ export class Console {
|
|
|
134
147
|
["adw_id", this.adwId],
|
|
135
148
|
["phases", `${passed}/${this.results.length}`],
|
|
136
149
|
["tokens", tokens.toLocaleString()],
|
|
137
|
-
["cost",
|
|
150
|
+
["cost", costText],
|
|
138
151
|
],
|
|
139
152
|
});
|
|
140
153
|
}
|
|
@@ -179,9 +192,9 @@ export class Console {
|
|
|
179
192
|
async note(message) {
|
|
180
193
|
await this.emit(` ${paint("dim", `· ${clip(message)}`)}`);
|
|
181
194
|
}
|
|
182
|
-
/** `Run.addUsage()`'s only hook into `Console` — the running
|
|
183
|
-
async notifyUsage(tokens, cost) {
|
|
184
|
-
this.observer?.onUsage?.(tokens, cost);
|
|
195
|
+
/** `Run.addUsage()`'s only hook into `Console` — the running totals live on `Run`, not here, so this just forwards them to the observer. No line prints for this on its own; the totals already show up in `sessionFinished`'s panel. `billableTokens` rides alongside `tokens` so a consumer comparing against `defaults.max_run_tokens` (a billable ceiling) never has to guess which number to use — see `RunObserver.onUsage`'s own doc comment. */
|
|
196
|
+
async notifyUsage(tokens, cost, billableTokens) {
|
|
197
|
+
this.observer?.onUsage?.(tokens, cost, billableTokens);
|
|
185
198
|
}
|
|
186
199
|
// ── agents ──────────────────────────────────────────────────────────────
|
|
187
200
|
async agentStarted(name, model, sessionId) {
|
|
@@ -696,6 +696,41 @@ export declare const ConfigDefaultsSchema: v.ObjectSchema<{
|
|
|
696
696
|
readonly harness_engineering: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
697
697
|
readonly tools: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
698
698
|
readonly protected_files: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => string[]>;
|
|
699
|
+
/**
|
|
700
|
+
* Paths a READ-ONLY (or write-restricted) agent may touch WITHOUT failing
|
|
701
|
+
* the phase — `core/permissions.ts`'s `enforce()` still rolls every one of
|
|
702
|
+
* them back (an agent's claimed report must never rest on a change that
|
|
703
|
+
* didn't survive), it just does not count that rollback as a breach.
|
|
704
|
+
*
|
|
705
|
+
* WHY THIS EXISTS: a lockfile is dependency-manager BOOKKEEPING, not the
|
|
706
|
+
* repo's intent — an agent that ran `npm install` (to read a package's
|
|
707
|
+
* real shape, say) rewrites `package-lock.json` as a side effect of a
|
|
708
|
+
* read, not an edit. Observed live: a read-only scout phase failed with
|
|
709
|
+
* "scout is read-only but modified 1 path(s): factory/content/
|
|
710
|
+
* package-lock.json — rolled back" over exactly this, for work that
|
|
711
|
+
* changed nothing an operator would call "the code."
|
|
712
|
+
*
|
|
713
|
+
* The four defaults are the lockfiles of every package manager this repo
|
|
714
|
+
* already builds against (npm, pnpm, yarn, bun) — additive, not
|
|
715
|
+
* exhaustive; a repo using another one adds its own pattern here. Same
|
|
716
|
+
* glob syntax as `protected_files`/`agents[].writes` (`permissions.ts`'s
|
|
717
|
+
* `globToRegex`), where a leading "**" followed by a path separator
|
|
718
|
+
* matches at any depth INCLUDING the repo root — so the packaged
|
|
719
|
+
* defaults below match a lockfile whether it sits at the top of the repo
|
|
720
|
+
* or nested under a subdirectory.
|
|
721
|
+
*
|
|
722
|
+
* Emptying this list (`read_only_ignore: []`) restores today's strict
|
|
723
|
+
* behavior exactly — every touched path outside an agent's own allowlist
|
|
724
|
+
* fails the phase, lockfiles included.
|
|
725
|
+
*
|
|
726
|
+
* PRECEDENCE: `protected_files` always wins. A path matching
|
|
727
|
+
* `protected_files` is never ignorable via `read_only_ignore`, no matter
|
|
728
|
+
* how narrowly write-restricted the agent is — a pattern here that
|
|
729
|
+
* happens to also match a protected path is not read as "exempt this
|
|
730
|
+
* from protected_files too"; it stays a real breach. See
|
|
731
|
+
* `permissions.ts`'s `isSafeToIgnore` for the enforcement.
|
|
732
|
+
*/
|
|
733
|
+
readonly read_only_ignore: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => string[]>;
|
|
699
734
|
readonly data_dir: v.OptionalSchema<v.StringSchema<undefined>, ".spf/data">;
|
|
700
735
|
/**
|
|
701
736
|
* RUN BUDGET CEILINGS — the two knobs that bound what one adw_id may spend.
|
|
@@ -718,11 +753,20 @@ export declare const ConfigDefaultsSchema: v.ObjectSchema<{
|
|
|
718
753
|
* `agents.ts`'s `BudgetExceeded`.
|
|
719
754
|
*
|
|
720
755
|
* `max_run_cost` is USD (the same unit the provider's own usage.cost
|
|
721
|
-
* arrives in, summed by `UsageBreakdown`); `max_run_tokens` is
|
|
722
|
-
* tokens
|
|
723
|
-
*
|
|
724
|
-
* `
|
|
725
|
-
*
|
|
756
|
+
* arrives in, summed by `UsageBreakdown`); `max_run_tokens` is BILLABLE
|
|
757
|
+
* tokens — `UsageBreakdown.billable_tokens` (input + cache-write + output),
|
|
758
|
+
* checked against `Run.billable_tokens`, NOT the `total_tokens` column
|
|
759
|
+
* `sessions` also carries for display. A prompt-caching backend (Ollama
|
|
760
|
+
* Cloud's kimi models, Anthropic's own caching) re-sends the whole
|
|
761
|
+
* conversation every turn as CACHE READS, which `total_tokens` counts and
|
|
762
|
+
* this ceiling does not: cache reads are billed (when billed at all) at a
|
|
763
|
+
* small fraction of input price, sometimes free, so a ceiling measured
|
|
764
|
+
* against the bigger number trips on bulk that cost nothing — observed
|
|
765
|
+
* live, one scout phase alone reported 1,311,740 total_tokens against a
|
|
766
|
+
* gateway that billed 189,321 uncached input + 17,908 output for it.
|
|
767
|
+
* `total_tokens` is kept exactly as before for anything display-only
|
|
768
|
+
* (the sessions-panel "tokens" line, the UI) — only the budget check
|
|
769
|
+
* changed which number it reads.
|
|
726
770
|
*
|
|
727
771
|
* Both are `> 0`, not `>= 0`: a zero ceiling would mean "no agent may ever
|
|
728
772
|
* run", which is a config mistake, not a budget — it would fail the first
|
|
@@ -741,6 +785,43 @@ export declare const ConfigDefaultsSchema: v.ObjectSchema<{
|
|
|
741
785
|
*/
|
|
742
786
|
readonly max_run_cost: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
743
787
|
readonly max_run_tokens: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
788
|
+
/**
|
|
789
|
+
* REQUEST TIMEOUT — how long a single agent dispatch may run before it is
|
|
790
|
+
* aborted and settled as failed, rather than hanging on a connection that
|
|
791
|
+
* silently died mid-call with nothing to notice.
|
|
792
|
+
*
|
|
793
|
+
* FLUE-SPECIFIC, unlike every other key in this schema: it maps straight
|
|
794
|
+
* onto `AgentStatics.durability.timeoutMs` (see `@flue/runtime`'s own
|
|
795
|
+
* docs), a `flue`-backend-only mechanism. `claude_code`/`opencode` are
|
|
796
|
+
* subprocess backends with no such knob today — this field is silently
|
|
797
|
+
* ignored for them, the same way `flue_db_path` on `AgentRequest` already
|
|
798
|
+
* is. Not a bug to fix here: a subprocess backend needs its own separate
|
|
799
|
+
* process-level timeout story, which is out of scope for this key.
|
|
800
|
+
*
|
|
801
|
+
* ABSENT BY DEFAULT, and absence is a total no-op: Flue's own default
|
|
802
|
+
* applies unchanged (1 hour, 10 attempts) — the same "surprise mid-run
|
|
803
|
+
* failure on a ceiling nobody chose is worse than the spend" reasoning as
|
|
804
|
+
* `max_run_cost`/`max_run_tokens` above. Set this when a hung connection
|
|
805
|
+
* should surface as an attributable failure (and feed the normal
|
|
806
|
+
* gate-correction / `spf watch` retry loop) in minutes, not however long
|
|
807
|
+
* Flue's own default takes — e.g. `300_000` for a five-minute ceiling.
|
|
808
|
+
* NOT a precise deadline, though: manual verification against a socket
|
|
809
|
+
* that accepts a connection and then sends nothing (see
|
|
810
|
+
* `request_timeout.test.ts`'s header comment) saw Flue's own timeout check
|
|
811
|
+
* fire on a coarser periodic sweep — a 3s ceiling settled at ~15s, not 3s.
|
|
812
|
+
* Bounded-but-imprecise is still a firm improvement over unbounded.
|
|
813
|
+
*
|
|
814
|
+
* SCOPE IS ONE SUBMISSION (one agent dispatch — the first prompt, one
|
|
815
|
+
* JSON-repair retry, one gate correction), NOT the accumulated run, unlike
|
|
816
|
+
* `max_run_cost`/`max_run_tokens` above. It is still process-scoped, not
|
|
817
|
+
* per-agent: Flue's `durability` is a static on the single shared agent
|
|
818
|
+
* function `agent_flue.ts` dispatches everything through, set once before
|
|
819
|
+
* the first dispatch of the process — see that file's `ensureRuntime()`.
|
|
820
|
+
* Deliberately NOT in `loadConfig`'s per-agent back-fill list for the same
|
|
821
|
+
* reason `max_run_cost`/`max_run_tokens` aren't: a per-agent copy would
|
|
822
|
+
* read as "this agent gets its own timeout", which nothing enforces.
|
|
823
|
+
*/
|
|
824
|
+
readonly request_timeout_ms: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
744
825
|
}, undefined>;
|
|
745
826
|
export type ConfigDefaults = v.InferOutput<typeof ConfigDefaultsSchema>;
|
|
746
827
|
/**
|
|
@@ -1050,6 +1131,30 @@ export declare const WatchJiraConfigSchema: v.ObjectSchema<{
|
|
|
1050
1131
|
done?: string | undefined;
|
|
1051
1132
|
blocked?: string | undefined;
|
|
1052
1133
|
}>;
|
|
1134
|
+
/**
|
|
1135
|
+
* The Jira issue-link `type` name `refine.ts`'s `publish()` uses to
|
|
1136
|
+
* connect a freshly-published tree's ROOT issue(s) back to the spec they
|
|
1137
|
+
* were refined from (`JiraProvider.linkToSpec`) — a plain, symmetric
|
|
1138
|
+
* "issue link" (Jira's generic relate-two-issues mechanism), never the
|
|
1139
|
+
* hierarchical `parent` field `linkChild` sets: the spec's own issue type
|
|
1140
|
+
* defaults to Story (`issue_types.spec`), and a root node is often an
|
|
1141
|
+
* Epic/Task — Jira's issue-type hierarchy frequently refuses a Story as
|
|
1142
|
+
* one of those types' PARENT, so the hierarchy field is not a safe choice
|
|
1143
|
+
* here regardless of which type actually published. "Relates" is a
|
|
1144
|
+
* built-in link type on every Jira Cloud project; override this only if a
|
|
1145
|
+
* project's admin has renamed or restricted it.
|
|
1146
|
+
*
|
|
1147
|
+
* MUST NAME A SYMMETRIC LINK TYPE. `JiraProvider.linkToSpec` fixes which
|
|
1148
|
+
* side is `inwardIssue`/`outwardIssue` (the published root is always
|
|
1149
|
+
* inward, the spec always outward) and does not expose direction as a
|
|
1150
|
+
* separate knob — harmless for a symmetric type like "Relates" (Jira's UI
|
|
1151
|
+
* does not even surface a direction for one), but pointing this at a
|
|
1152
|
+
* DIRECTIONAL type (e.g. "blocks"/"is blocked by") would silently record
|
|
1153
|
+
* the opposite relationship from the one intended. Only rename this to
|
|
1154
|
+
* another symmetric type; a directional one needs code changes, not just
|
|
1155
|
+
* config.
|
|
1156
|
+
*/
|
|
1157
|
+
readonly link_type: v.OptionalSchema<v.StringSchema<undefined>, "Relates">;
|
|
1053
1158
|
}, undefined>;
|
|
1054
1159
|
export type WatchJiraConfig = v.InferOutput<typeof WatchJiraConfigSchema>;
|
|
1055
1160
|
/**
|
|
@@ -1278,6 +1383,30 @@ export declare const WatchConfigSchema: v.ObjectSchema<{
|
|
|
1278
1383
|
done?: string | undefined;
|
|
1279
1384
|
blocked?: string | undefined;
|
|
1280
1385
|
}>;
|
|
1386
|
+
/**
|
|
1387
|
+
* The Jira issue-link `type` name `refine.ts`'s `publish()` uses to
|
|
1388
|
+
* connect a freshly-published tree's ROOT issue(s) back to the spec they
|
|
1389
|
+
* were refined from (`JiraProvider.linkToSpec`) — a plain, symmetric
|
|
1390
|
+
* "issue link" (Jira's generic relate-two-issues mechanism), never the
|
|
1391
|
+
* hierarchical `parent` field `linkChild` sets: the spec's own issue type
|
|
1392
|
+
* defaults to Story (`issue_types.spec`), and a root node is often an
|
|
1393
|
+
* Epic/Task — Jira's issue-type hierarchy frequently refuses a Story as
|
|
1394
|
+
* one of those types' PARENT, so the hierarchy field is not a safe choice
|
|
1395
|
+
* here regardless of which type actually published. "Relates" is a
|
|
1396
|
+
* built-in link type on every Jira Cloud project; override this only if a
|
|
1397
|
+
* project's admin has renamed or restricted it.
|
|
1398
|
+
*
|
|
1399
|
+
* MUST NAME A SYMMETRIC LINK TYPE. `JiraProvider.linkToSpec` fixes which
|
|
1400
|
+
* side is `inwardIssue`/`outwardIssue` (the published root is always
|
|
1401
|
+
* inward, the spec always outward) and does not expose direction as a
|
|
1402
|
+
* separate knob — harmless for a symmetric type like "Relates" (Jira's UI
|
|
1403
|
+
* does not even surface a direction for one), but pointing this at a
|
|
1404
|
+
* DIRECTIONAL type (e.g. "blocks"/"is blocked by") would silently record
|
|
1405
|
+
* the opposite relationship from the one intended. Only rename this to
|
|
1406
|
+
* another symmetric type; a directional one needs code changes, not just
|
|
1407
|
+
* config.
|
|
1408
|
+
*/
|
|
1409
|
+
readonly link_type: v.OptionalSchema<v.StringSchema<undefined>, "Relates">;
|
|
1281
1410
|
}, undefined>, () => {
|
|
1282
1411
|
base_url: string;
|
|
1283
1412
|
project_key: string;
|
|
@@ -1297,6 +1426,7 @@ export declare const WatchConfigSchema: v.ObjectSchema<{
|
|
|
1297
1426
|
done?: string | undefined;
|
|
1298
1427
|
blocked?: string | undefined;
|
|
1299
1428
|
};
|
|
1429
|
+
link_type: string;
|
|
1300
1430
|
}>;
|
|
1301
1431
|
readonly github: v.OptionalSchema<v.ObjectSchema<{
|
|
1302
1432
|
readonly project_number: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, 0>;
|
|
@@ -1488,6 +1618,41 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1488
1618
|
readonly harness_engineering: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => never[]>;
|
|
1489
1619
|
readonly tools: v.OptionalSchema<v.NullableSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
|
|
1490
1620
|
readonly protected_files: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => string[]>;
|
|
1621
|
+
/**
|
|
1622
|
+
* Paths a READ-ONLY (or write-restricted) agent may touch WITHOUT failing
|
|
1623
|
+
* the phase — `core/permissions.ts`'s `enforce()` still rolls every one of
|
|
1624
|
+
* them back (an agent's claimed report must never rest on a change that
|
|
1625
|
+
* didn't survive), it just does not count that rollback as a breach.
|
|
1626
|
+
*
|
|
1627
|
+
* WHY THIS EXISTS: a lockfile is dependency-manager BOOKKEEPING, not the
|
|
1628
|
+
* repo's intent — an agent that ran `npm install` (to read a package's
|
|
1629
|
+
* real shape, say) rewrites `package-lock.json` as a side effect of a
|
|
1630
|
+
* read, not an edit. Observed live: a read-only scout phase failed with
|
|
1631
|
+
* "scout is read-only but modified 1 path(s): factory/content/
|
|
1632
|
+
* package-lock.json — rolled back" over exactly this, for work that
|
|
1633
|
+
* changed nothing an operator would call "the code."
|
|
1634
|
+
*
|
|
1635
|
+
* The four defaults are the lockfiles of every package manager this repo
|
|
1636
|
+
* already builds against (npm, pnpm, yarn, bun) — additive, not
|
|
1637
|
+
* exhaustive; a repo using another one adds its own pattern here. Same
|
|
1638
|
+
* glob syntax as `protected_files`/`agents[].writes` (`permissions.ts`'s
|
|
1639
|
+
* `globToRegex`), where a leading "**" followed by a path separator
|
|
1640
|
+
* matches at any depth INCLUDING the repo root — so the packaged
|
|
1641
|
+
* defaults below match a lockfile whether it sits at the top of the repo
|
|
1642
|
+
* or nested under a subdirectory.
|
|
1643
|
+
*
|
|
1644
|
+
* Emptying this list (`read_only_ignore: []`) restores today's strict
|
|
1645
|
+
* behavior exactly — every touched path outside an agent's own allowlist
|
|
1646
|
+
* fails the phase, lockfiles included.
|
|
1647
|
+
*
|
|
1648
|
+
* PRECEDENCE: `protected_files` always wins. A path matching
|
|
1649
|
+
* `protected_files` is never ignorable via `read_only_ignore`, no matter
|
|
1650
|
+
* how narrowly write-restricted the agent is — a pattern here that
|
|
1651
|
+
* happens to also match a protected path is not read as "exempt this
|
|
1652
|
+
* from protected_files too"; it stays a real breach. See
|
|
1653
|
+
* `permissions.ts`'s `isSafeToIgnore` for the enforcement.
|
|
1654
|
+
*/
|
|
1655
|
+
readonly read_only_ignore: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, () => string[]>;
|
|
1491
1656
|
readonly data_dir: v.OptionalSchema<v.StringSchema<undefined>, ".spf/data">;
|
|
1492
1657
|
/**
|
|
1493
1658
|
* RUN BUDGET CEILINGS — the two knobs that bound what one adw_id may spend.
|
|
@@ -1510,11 +1675,20 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1510
1675
|
* `agents.ts`'s `BudgetExceeded`.
|
|
1511
1676
|
*
|
|
1512
1677
|
* `max_run_cost` is USD (the same unit the provider's own usage.cost
|
|
1513
|
-
* arrives in, summed by `UsageBreakdown`); `max_run_tokens` is
|
|
1514
|
-
* tokens
|
|
1515
|
-
*
|
|
1516
|
-
* `
|
|
1517
|
-
*
|
|
1678
|
+
* arrives in, summed by `UsageBreakdown`); `max_run_tokens` is BILLABLE
|
|
1679
|
+
* tokens — `UsageBreakdown.billable_tokens` (input + cache-write + output),
|
|
1680
|
+
* checked against `Run.billable_tokens`, NOT the `total_tokens` column
|
|
1681
|
+
* `sessions` also carries for display. A prompt-caching backend (Ollama
|
|
1682
|
+
* Cloud's kimi models, Anthropic's own caching) re-sends the whole
|
|
1683
|
+
* conversation every turn as CACHE READS, which `total_tokens` counts and
|
|
1684
|
+
* this ceiling does not: cache reads are billed (when billed at all) at a
|
|
1685
|
+
* small fraction of input price, sometimes free, so a ceiling measured
|
|
1686
|
+
* against the bigger number trips on bulk that cost nothing — observed
|
|
1687
|
+
* live, one scout phase alone reported 1,311,740 total_tokens against a
|
|
1688
|
+
* gateway that billed 189,321 uncached input + 17,908 output for it.
|
|
1689
|
+
* `total_tokens` is kept exactly as before for anything display-only
|
|
1690
|
+
* (the sessions-panel "tokens" line, the UI) — only the budget check
|
|
1691
|
+
* changed which number it reads.
|
|
1518
1692
|
*
|
|
1519
1693
|
* Both are `> 0`, not `>= 0`: a zero ceiling would mean "no agent may ever
|
|
1520
1694
|
* run", which is a config mistake, not a budget — it would fail the first
|
|
@@ -1533,6 +1707,43 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1533
1707
|
*/
|
|
1534
1708
|
readonly max_run_cost: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
1535
1709
|
readonly max_run_tokens: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
1710
|
+
/**
|
|
1711
|
+
* REQUEST TIMEOUT — how long a single agent dispatch may run before it is
|
|
1712
|
+
* aborted and settled as failed, rather than hanging on a connection that
|
|
1713
|
+
* silently died mid-call with nothing to notice.
|
|
1714
|
+
*
|
|
1715
|
+
* FLUE-SPECIFIC, unlike every other key in this schema: it maps straight
|
|
1716
|
+
* onto `AgentStatics.durability.timeoutMs` (see `@flue/runtime`'s own
|
|
1717
|
+
* docs), a `flue`-backend-only mechanism. `claude_code`/`opencode` are
|
|
1718
|
+
* subprocess backends with no such knob today — this field is silently
|
|
1719
|
+
* ignored for them, the same way `flue_db_path` on `AgentRequest` already
|
|
1720
|
+
* is. Not a bug to fix here: a subprocess backend needs its own separate
|
|
1721
|
+
* process-level timeout story, which is out of scope for this key.
|
|
1722
|
+
*
|
|
1723
|
+
* ABSENT BY DEFAULT, and absence is a total no-op: Flue's own default
|
|
1724
|
+
* applies unchanged (1 hour, 10 attempts) — the same "surprise mid-run
|
|
1725
|
+
* failure on a ceiling nobody chose is worse than the spend" reasoning as
|
|
1726
|
+
* `max_run_cost`/`max_run_tokens` above. Set this when a hung connection
|
|
1727
|
+
* should surface as an attributable failure (and feed the normal
|
|
1728
|
+
* gate-correction / `spf watch` retry loop) in minutes, not however long
|
|
1729
|
+
* Flue's own default takes — e.g. `300_000` for a five-minute ceiling.
|
|
1730
|
+
* NOT a precise deadline, though: manual verification against a socket
|
|
1731
|
+
* that accepts a connection and then sends nothing (see
|
|
1732
|
+
* `request_timeout.test.ts`'s header comment) saw Flue's own timeout check
|
|
1733
|
+
* fire on a coarser periodic sweep — a 3s ceiling settled at ~15s, not 3s.
|
|
1734
|
+
* Bounded-but-imprecise is still a firm improvement over unbounded.
|
|
1735
|
+
*
|
|
1736
|
+
* SCOPE IS ONE SUBMISSION (one agent dispatch — the first prompt, one
|
|
1737
|
+
* JSON-repair retry, one gate correction), NOT the accumulated run, unlike
|
|
1738
|
+
* `max_run_cost`/`max_run_tokens` above. It is still process-scoped, not
|
|
1739
|
+
* per-agent: Flue's `durability` is a static on the single shared agent
|
|
1740
|
+
* function `agent_flue.ts` dispatches everything through, set once before
|
|
1741
|
+
* the first dispatch of the process — see that file's `ensureRuntime()`.
|
|
1742
|
+
* Deliberately NOT in `loadConfig`'s per-agent back-fill list for the same
|
|
1743
|
+
* reason `max_run_cost`/`max_run_tokens` aren't: a per-agent copy would
|
|
1744
|
+
* read as "this agent gets its own timeout", which nothing enforces.
|
|
1745
|
+
*/
|
|
1746
|
+
readonly request_timeout_ms: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.GtValueAction<number, 0, undefined>]>, undefined>;
|
|
1536
1747
|
}, undefined>, () => {
|
|
1537
1748
|
coding_agent: "claude_code" | "flue" | "opencode";
|
|
1538
1749
|
model: string;
|
|
@@ -1541,9 +1752,11 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1541
1752
|
harness_engineering: string[];
|
|
1542
1753
|
tools?: string[] | null | undefined;
|
|
1543
1754
|
protected_files: string[];
|
|
1755
|
+
read_only_ignore: string[];
|
|
1544
1756
|
data_dir: string;
|
|
1545
1757
|
max_run_cost?: number | undefined;
|
|
1546
1758
|
max_run_tokens?: number | undefined;
|
|
1759
|
+
request_timeout_ms?: number | undefined;
|
|
1547
1760
|
}>;
|
|
1548
1761
|
readonly observability: v.OptionalSchema<v.ObjectSchema<{
|
|
1549
1762
|
readonly db: v.OptionalSchema<v.SchemaWithPipe<readonly [v.UnknownSchema, v.RawTransformAction<unknown, string | {
|
|
@@ -1689,6 +1902,30 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1689
1902
|
done?: string | undefined;
|
|
1690
1903
|
blocked?: string | undefined;
|
|
1691
1904
|
}>;
|
|
1905
|
+
/**
|
|
1906
|
+
* The Jira issue-link `type` name `refine.ts`'s `publish()` uses to
|
|
1907
|
+
* connect a freshly-published tree's ROOT issue(s) back to the spec they
|
|
1908
|
+
* were refined from (`JiraProvider.linkToSpec`) — a plain, symmetric
|
|
1909
|
+
* "issue link" (Jira's generic relate-two-issues mechanism), never the
|
|
1910
|
+
* hierarchical `parent` field `linkChild` sets: the spec's own issue type
|
|
1911
|
+
* defaults to Story (`issue_types.spec`), and a root node is often an
|
|
1912
|
+
* Epic/Task — Jira's issue-type hierarchy frequently refuses a Story as
|
|
1913
|
+
* one of those types' PARENT, so the hierarchy field is not a safe choice
|
|
1914
|
+
* here regardless of which type actually published. "Relates" is a
|
|
1915
|
+
* built-in link type on every Jira Cloud project; override this only if a
|
|
1916
|
+
* project's admin has renamed or restricted it.
|
|
1917
|
+
*
|
|
1918
|
+
* MUST NAME A SYMMETRIC LINK TYPE. `JiraProvider.linkToSpec` fixes which
|
|
1919
|
+
* side is `inwardIssue`/`outwardIssue` (the published root is always
|
|
1920
|
+
* inward, the spec always outward) and does not expose direction as a
|
|
1921
|
+
* separate knob — harmless for a symmetric type like "Relates" (Jira's UI
|
|
1922
|
+
* does not even surface a direction for one), but pointing this at a
|
|
1923
|
+
* DIRECTIONAL type (e.g. "blocks"/"is blocked by") would silently record
|
|
1924
|
+
* the opposite relationship from the one intended. Only rename this to
|
|
1925
|
+
* another symmetric type; a directional one needs code changes, not just
|
|
1926
|
+
* config.
|
|
1927
|
+
*/
|
|
1928
|
+
readonly link_type: v.OptionalSchema<v.StringSchema<undefined>, "Relates">;
|
|
1692
1929
|
}, undefined>, () => {
|
|
1693
1930
|
base_url: string;
|
|
1694
1931
|
project_key: string;
|
|
@@ -1708,6 +1945,7 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1708
1945
|
done?: string | undefined;
|
|
1709
1946
|
blocked?: string | undefined;
|
|
1710
1947
|
};
|
|
1948
|
+
link_type: string;
|
|
1711
1949
|
}>;
|
|
1712
1950
|
readonly github: v.OptionalSchema<v.ObjectSchema<{
|
|
1713
1951
|
readonly project_number: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, 0>;
|
|
@@ -1791,6 +2029,7 @@ export declare const SFConfigSchema: v.ObjectSchema<{
|
|
|
1791
2029
|
done?: string | undefined;
|
|
1792
2030
|
blocked?: string | undefined;
|
|
1793
2031
|
};
|
|
2032
|
+
link_type: string;
|
|
1794
2033
|
};
|
|
1795
2034
|
github: {
|
|
1796
2035
|
project_number: number;
|
|
@@ -2011,21 +2250,45 @@ export interface AgentRequest {
|
|
|
2011
2250
|
output_type_name: string;
|
|
2012
2251
|
cwd: string;
|
|
2013
2252
|
flue_db_path: string;
|
|
2253
|
+
request_timeout_ms?: number;
|
|
2014
2254
|
env?: Record<string, string>;
|
|
2015
2255
|
/** Absent (the default) => local(), byte-identical to before this field existed. See sandbox.ts. */
|
|
2016
2256
|
sandbox?: SandboxSpec;
|
|
2257
|
+
/**
|
|
2258
|
+
* The run's own adw_id / the calling agent's name — set by `agents.ts`'s
|
|
2259
|
+
* `send()` from `run.adw_id`/`agent.name`, which are unconditionally in
|
|
2260
|
+
* scope there (unlike `otel` below, this pair is NOT gated on
|
|
2261
|
+
* `observability.otel` being configured: a gateway that groups calls by
|
|
2262
|
+
* `x-correlation-id`/`x-spf-agent` needs them on every call, not only when
|
|
2263
|
+
* SPF's own OTel export happens to be turned on). Consulted today only by
|
|
2264
|
+
* `agent_flue.ts`'s ollama registration (see `ollama_provider.ts`'s
|
|
2265
|
+
* `GatewayCallContext`); every other backend ignores both fields, so this
|
|
2266
|
+
* addition is byte-identical to before it existed for them.
|
|
2267
|
+
*/
|
|
2268
|
+
adw_id?: string;
|
|
2269
|
+
agent_name?: string;
|
|
2017
2270
|
/**
|
|
2018
2271
|
* Outbound OTel trace-context propagation — set by `agents.ts`'s `send()`
|
|
2019
2272
|
* from `otel.ts`'s `OtelExporter.agentCallTraceContext()` ONLY when
|
|
2020
|
-
* `observability.otel` is configured for this run; absent otherwise
|
|
2021
|
-
*
|
|
2022
|
-
*
|
|
2023
|
-
*
|
|
2024
|
-
*
|
|
2025
|
-
* `headers`/`service_name` are
|
|
2026
|
-
* carried through so `agent_flue.ts`
|
|
2027
|
-
* process-scoped — see
|
|
2028
|
-
* propagation without needing
|
|
2273
|
+
* `observability.otel` is configured for this run; absent otherwise.
|
|
2274
|
+
* `traceparent` is this call's own span context — `agent_cc.ts`'s single
|
|
2275
|
+
* `spawn()` choke point turns it (alongside `adw_id`/`agent_name` above,
|
|
2276
|
+
* which are NOT gated on this field) into `TRACEPARENT`/
|
|
2277
|
+
* `ANTHROPIC_CUSTOM_HEADERS`; `agent_opencode.ts` does the analogous thing
|
|
2278
|
+
* into its temp `opencode.json`. `endpoint`/`headers`/`service_name` are
|
|
2279
|
+
* the SAME `observability.otel` block, carried through so `agent_flue.ts`
|
|
2280
|
+
* can install its own (separate, process-scoped — see
|
|
2281
|
+
* `otel_propagation.ts`) global http/undici propagation without needing
|
|
2282
|
+
* the full `SFConfig`.
|
|
2283
|
+
*
|
|
2284
|
+
* `x_request_id` (this call's own span id) is no longer turned into any
|
|
2285
|
+
* outbound header by anything in this repo — `agent_cc.ts`/
|
|
2286
|
+
* `agent_opencode.ts` used to send it as `x-request-id` and no longer do
|
|
2287
|
+
* (BLOCKER B: Envoy/Switchyard own that header end-to-end; a client-sent
|
|
2288
|
+
* value broke their own trace sampling). Kept as a field (rather than
|
|
2289
|
+
* removed) since it is still a well-defined, harmless-to-carry value — a
|
|
2290
|
+
* future consumer that needs "this call's own span id" for something
|
|
2291
|
+
* other than a header has it available — but nothing reads it today.
|
|
2029
2292
|
*/
|
|
2030
2293
|
otel?: {
|
|
2031
2294
|
traceparent: string;
|
|
@@ -2049,6 +2312,24 @@ export declare class UsageBreakdown {
|
|
|
2049
2312
|
cache_write_tokens: number;
|
|
2050
2313
|
reasoning_tokens: number;
|
|
2051
2314
|
total_tokens: number;
|
|
2315
|
+
/**
|
|
2316
|
+
* The SPEND number, as distinct from `total_tokens` (the SIZE number).
|
|
2317
|
+
* `input_tokens + output_tokens + cache_write_tokens` — `cache_read_tokens`
|
|
2318
|
+
* excluded on purpose: a cache read is Anthropic's own prompt-caching
|
|
2319
|
+
* discount (billed at a small fraction of the input rate, sometimes free
|
|
2320
|
+
* on some gateways) for context the conversation already sent, not new
|
|
2321
|
+
* material moved. `total_tokens` re-sends (and re-counts) the whole
|
|
2322
|
+
* conversation every turn, so a long-running scout/build session's cache
|
|
2323
|
+
* reads dwarf everything else in it (observed live: 1.31M total_tokens in
|
|
2324
|
+
* one phase, of which 1.15M were cache reads the gateway did not bill as
|
|
2325
|
+
* input) — a run-budget ceiling measured against `total_tokens` trips on
|
|
2326
|
+
* cache-driven bulk that cost nothing, not on real spend. `assertRunBudget`
|
|
2327
|
+
* (`agents.ts`) checks THIS field against `defaults.max_run_tokens`;
|
|
2328
|
+
* `total_tokens` is kept, unchanged, for display (the sessions-panel
|
|
2329
|
+
* "tokens" line, the UI) because an operator sizing context occupancy
|
|
2330
|
+
* still needs the real re-send count, not the billable one.
|
|
2331
|
+
*/
|
|
2332
|
+
billable_tokens: number;
|
|
2052
2333
|
input_cost: number;
|
|
2053
2334
|
output_cost: number;
|
|
2054
2335
|
cache_read_cost: number;
|