@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.
Files changed (48) hide show
  1. package/README.md +8 -0
  2. package/assets/skill/references/config.md +1 -0
  3. package/dist/cli/commands/doctor.js +25 -3
  4. package/dist/cli/commands/estimate.d.ts +22 -6
  5. package/dist/cli/commands/estimate.js +32 -10
  6. package/dist/cli/commands/loop.d.ts +20 -0
  7. package/dist/cli/commands/loop.js +20 -1
  8. package/dist/cli/commands/ui.js +2 -1
  9. package/dist/cli/commands/watch.js +1 -1
  10. package/dist/cli/index.js +2 -2
  11. package/dist/cli/interview.js +13 -0
  12. package/dist/cli/ui/run_dashboard.js +13 -7
  13. package/dist/core/agent_cc.d.ts +19 -3
  14. package/dist/core/agent_cc.js +38 -18
  15. package/dist/core/agent_flue.js +51 -14
  16. package/dist/core/agent_opencode.d.ts +62 -25
  17. package/dist/core/agent_opencode.js +71 -30
  18. package/dist/core/agents.d.ts +51 -4
  19. package/dist/core/agents.js +79 -4
  20. package/dist/core/console.d.ts +24 -4
  21. package/dist/core/console.js +20 -7
  22. package/dist/core/data_types.d.ts +300 -19
  23. package/dist/core/data_types.js +134 -5
  24. package/dist/core/issues/jira_provider.d.ts +51 -1
  25. package/dist/core/issues/jira_provider.js +69 -1
  26. package/dist/core/issues/provider.d.ts +23 -0
  27. package/dist/core/loop.d.ts +39 -1
  28. package/dist/core/loop.js +33 -2
  29. package/dist/core/ollama_provider.d.ts +96 -13
  30. package/dist/core/ollama_provider.js +172 -26
  31. package/dist/core/otel.js +10 -1
  32. package/dist/core/otel_propagation.d.ts +168 -24
  33. package/dist/core/otel_propagation.js +219 -43
  34. package/dist/core/permissions.d.ts +16 -1
  35. package/dist/core/permissions.js +91 -3
  36. package/dist/core/providers.js +8 -3
  37. package/dist/core/refine.js +13 -1
  38. package/dist/core/runner.d.ts +33 -2
  39. package/dist/core/runner.js +40 -5
  40. package/dist/core/tiering.js +7 -3
  41. package/dist/core/tracer.d.ts +7 -1
  42. package/dist/core/tracer.js +15 -3
  43. package/dist/ui/server/db.d.ts +8 -1
  44. package/dist/ui/server/db.js +21 -4
  45. package/dist/ui/server/serve.d.ts +7 -0
  46. package/dist/ui/server/serve.js +10 -7
  47. package/dist/ui/shared/types.d.ts +16 -0
  48. package/package.json +1 -1
@@ -100,17 +100,30 @@ export class Console {
100
100
  ],
101
101
  });
102
102
  }
103
- async sessionFinished(ok, tokens, cost, dbPath) {
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")} $${cost.toFixed(4)}`,
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 · $${cost.toFixed(4)}`;
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", `$${cost.toFixed(4)}`],
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 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
- 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 TOTAL
722
- * tokens, i.e. the spend number — every turn re-sends the whole
723
- * conversation, so this counts cached re-reads too, exactly like the
724
- * `total_tokens` column in `sessions` (see `ui/server/db.ts`'s `usage()`
725
- * for why that number is much larger than "material moved").
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 TOTAL
1514
- * tokens, i.e. the spend number — every turn re-sends the whole
1515
- * conversation, so this counts cached re-reads too, exactly like the
1516
- * `total_tokens` column in `sessions` (see `ui/server/db.ts`'s `usage()`
1517
- * for why that number is much larger than "material moved").
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, and
2021
- * every backend that ignores it (`opencode` today) is byte-identical to
2022
- * before this field existed. `traceparent`/`x_request_id` are this call's
2023
- * own span context (`agent_cc.ts`'s single `spawn()` choke point turns
2024
- * them into `TRACEPARENT`/`ANTHROPIC_CUSTOM_HEADERS`); `endpoint`/
2025
- * `headers`/`service_name` are the SAME `observability.otel` block,
2026
- * carried through so `agent_flue.ts` can install its own (separate,
2027
- * process-scoped — see `otel_propagation.ts`) global http/undici
2028
- * propagation without needing the full `SFConfig`.
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;