@gr8ful/spf 0.11.2 → 0.12.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/assets/defaults/spf.config.yaml +2 -0
- package/assets/prompts/refiner/system.md +1 -0
- package/assets/prompts/scout/system.md +1 -0
- package/assets/skill/references/config.md +7 -1
- package/dist/core/agent_cc.js +9 -0
- package/dist/core/agent_flue.d.ts +41 -1
- package/dist/core/agent_flue.js +99 -1
- package/package.json +1 -1
|
@@ -246,6 +246,7 @@ agents:
|
|
|
246
246
|
- ls
|
|
247
247
|
- bash
|
|
248
248
|
- write
|
|
249
|
+
- webfetch # check current docs for a stack/tool the repo uses, rather than guessing from training data
|
|
249
250
|
|
|
250
251
|
# No tester agent: running the suite is a known command, so it is a kind="code"
|
|
251
252
|
# phase over core/quality.ts. See SKILL.md hard rule 8.
|
|
@@ -265,6 +266,7 @@ agents:
|
|
|
265
266
|
- ls
|
|
266
267
|
- bash
|
|
267
268
|
- write
|
|
269
|
+
- webfetch # look up a possible tech stack/tool/3rd-party API's current docs before decomposing around it
|
|
268
270
|
|
|
269
271
|
- name: reviewer
|
|
270
272
|
model: openai/gpt-5.6-terra
|
|
@@ -11,6 +11,7 @@ Decompose a product spec into a feature/story-or-bug tree of vertical slices the
|
|
|
11
11
|
- You inherit the operator's shell environment — their PATH, toolchains and credentials are already live. Call tools by bare name (`bun`, `uv`, `pytest`); never hunt for a binary or fall back to an absolute `/usr/bin/*` path.
|
|
12
12
|
- Judge any command you run by its exit status, never by scanning its output for words. `error` or `not found` inside passing output is text, not a failure.
|
|
13
13
|
- Write your working notes to `<context_handoff_dir>/refine_plan.md` before emitting your Report JSON.
|
|
14
|
+
- `webfetch` fetches a URL's current content — use it to check a specific library/API/tool's actual current docs when a slice depends on one you're not certain about, rather than decomposing around stale training-data assumptions. It fetches a page you name; it cannot search the web for one.
|
|
14
15
|
|
|
15
16
|
## Grounding: tie every slice to real code
|
|
16
17
|
|
|
@@ -12,6 +12,7 @@ Find and report where things live. Change nothing.
|
|
|
12
12
|
- Judge any command you run by its exit status, never by scanning its output for words. `error` or `not found` inside passing output is text, not a failure.
|
|
13
13
|
- Write your findings to `<context_handoff_dir>/scout_findings.md` for agents that follow.
|
|
14
14
|
- If you find nothing, say so plainly — an empty finding is a valid finding.
|
|
15
|
+
- `webfetch` fetches a URL's current content when the repo touches a library, API, or tool you're not certain about — use it to check, don't rely on stale training-data knowledge of a fast-moving stack. Not for general research: fetch a specific doc/reference page, not a search query.
|
|
15
16
|
|
|
16
17
|
## Subagents
|
|
17
18
|
|
|
@@ -564,11 +564,17 @@ session** — a joined run starts that agent fresh instead of resuming.
|
|
|
564
564
|
| `grep` | search file contents |
|
|
565
565
|
| `glob` (alias: `find`) | find files by pattern |
|
|
566
566
|
| `ls` | recognized name, **no built-in on either backend** — harmless to list, never mounts |
|
|
567
|
+
| `webfetch` | fetch a URL over HTTP(S), return its content as plain text — e.g. a library/API's current docs |
|
|
567
568
|
|
|
568
569
|
These names are canonical across backends — a roster entry never says
|
|
569
570
|
which; each backend module (`agent_flue.ts`, `agent_cc.ts`) maps them to
|
|
570
571
|
its own tool vocabulary (Flue's lowercase functions, Claude Code's
|
|
571
|
-
capitalized `Read`/`Bash`/...).
|
|
572
|
+
capitalized `Read`/`Bash`/...). `webfetch` maps to Claude Code's own native
|
|
573
|
+
`WebFetch` on that backend; Flue has no such built-in, so on `flue` it's a
|
|
574
|
+
custom tool that runs `curl`/`wget` through the agent's own `Sandbox.exec()`
|
|
575
|
+
— which means a remote sandbox's `egress` policy (see `sandbox` below)
|
|
576
|
+
governs it exactly like any other `bash`-issued network call, with no
|
|
577
|
+
separate rule to configure.
|
|
572
578
|
|
|
573
579
|
**Resolution order:** an agent's own `tools` wins → else `defaults.tools` →
|
|
574
580
|
else unset (all tools usable). An empty list is a tool-less agent, and it
|
package/dist/core/agent_cc.js
CHANGED
|
@@ -213,6 +213,15 @@ const TOOL_NAME_MAP = {
|
|
|
213
213
|
bash: "Bash",
|
|
214
214
|
grep: "Grep",
|
|
215
215
|
glob: "Glob",
|
|
216
|
+
// CC's own built-in — fetches a URL and returns its content as markdown,
|
|
217
|
+
// already runs headlessly under --dangerously-skip-permissions like every
|
|
218
|
+
// other tool here. No sandbox/egress layer of SPF's own to route through:
|
|
219
|
+
// CC's own process makes the request directly, same as a bash `curl` an
|
|
220
|
+
// agent with `bash` could already issue — see agent_flue.ts's
|
|
221
|
+
// `createWebFetchTool` for the equivalent on the flue backend, where no
|
|
222
|
+
// native fetch tool exists and one has to route through the Sandbox
|
|
223
|
+
// instead.
|
|
224
|
+
webfetch: "WebFetch",
|
|
216
225
|
};
|
|
217
226
|
const TOOL_ALIASES = { find: "glob" };
|
|
218
227
|
const DROPPED_TOOLS = new Set(["ls"]);
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* `run()`'s signature deliberately mirrors the old agent_pi.ts `run()` so
|
|
23
23
|
* agents.ts's `send()` closure changes only its imports and field names.
|
|
24
24
|
*/
|
|
25
|
-
import
|
|
25
|
+
import * as v from "valibot";
|
|
26
|
+
import { type ConversationStreamChunk, type Sandbox } from "@flue/runtime";
|
|
26
27
|
import type { AgentRequest, AgentResult } from "./data_types.ts";
|
|
27
28
|
/**
|
|
28
29
|
* Folds Flue's `tool-input` + `tool-output`/`tool-output-error` chunk pair
|
|
@@ -37,6 +38,45 @@ export declare class ToolCallTracker {
|
|
|
37
38
|
observe(chunk: ConversationStreamChunk): Record<string, any> | null;
|
|
38
39
|
private finish;
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Strip `<script>`/`<style>` blocks and tags, decode the handful of entities
|
|
43
|
+
* a real docs page actually uses, and collapse whitespace. NOT a real
|
|
44
|
+
* HTML-to-markdown renderer — Claude Code's native `WebFetch` (the
|
|
45
|
+
* claude_code backend) does that; Flue has nothing equivalent built in, and
|
|
46
|
+
* a full renderer is more than this needs. A JSON/plain-text response
|
|
47
|
+
* passes through essentially unchanged, since none of these patterns match
|
|
48
|
+
* it — same "minimal, not a full parser" trade `jira_provider.ts`'s
|
|
49
|
+
* `adfToText` makes for ADF.
|
|
50
|
+
*/
|
|
51
|
+
export declare function stripHtml(text: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* The flue backend's stand-in for Claude Code's native `WebFetch` tool —
|
|
54
|
+
* `@flue/runtime` ships no such built-in (its only tool factories are
|
|
55
|
+
* read/write/edit/bash/grep/glob), so this is a custom `defineTool()`.
|
|
56
|
+
*
|
|
57
|
+
* Runs the actual request THROUGH `env.exec()` rather than calling Node's
|
|
58
|
+
* own `fetch()` directly — deliberately: `exec()` is the one universal
|
|
59
|
+
* primitive every `Sandbox` implements (`local`, and the remote
|
|
60
|
+
* `opensandbox`/`cloudflare` backends via `core/sandbox.ts`), so a command
|
|
61
|
+
* run through it executes INSIDE whichever sandbox the agent is actually
|
|
62
|
+
* using. A remote sandbox's own `egress` policy (`sandbox_opensandbox.ts`'s
|
|
63
|
+
* `networkPolicy`) then governs this exactly as it already governs every
|
|
64
|
+
* `bash` call — no separate egress rule needed for this tool. Calling
|
|
65
|
+
* `fetch()` here instead would silently bypass that policy by making the
|
|
66
|
+
* request from SPF's own orchestrator process rather than the sandbox.
|
|
67
|
+
*
|
|
68
|
+
* `curl`, with a `wget` fallback in the SAME command for portability across
|
|
69
|
+
* whatever base image a remote sandbox happens to ship — both are close to
|
|
70
|
+
* universal, but neither is guaranteed; a sandbox image with neither
|
|
71
|
+
* surfaces that plainly as a failed tool call (exit code + stderr handed
|
|
72
|
+
* back to the model), not a hang. Only `http`/`https` are accepted — this is
|
|
73
|
+
* also what stops a `file://` URL from turning "fetch a page" into "read an
|
|
74
|
+
* arbitrary local file" on the `local` sandbox, where `exec()` runs with the
|
|
75
|
+
* operator's own full filesystem access.
|
|
76
|
+
*/
|
|
77
|
+
export declare function createWebFetchTool(env: Sandbox): import("@flue/runtime").ToolDefinition<v.ObjectSchema<{
|
|
78
|
+
readonly url: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.NonEmptyAction<string, "url is required">]>;
|
|
79
|
+
}, undefined>, undefined, false, false>;
|
|
40
80
|
/** Used by agents.validate() so a typo'd tool name fails before anything spawns. */
|
|
41
81
|
export declare function isKnownToolName(name: string): boolean;
|
|
42
82
|
/**
|
package/dist/core/agent_flue.js
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* `run()`'s signature deliberately mirrors the old agent_pi.ts `run()` so
|
|
23
23
|
* agents.ts's `send()` closure changes only its imports and field names.
|
|
24
24
|
*/
|
|
25
|
-
import
|
|
25
|
+
import * as v from "valibot";
|
|
26
|
+
import { AgentRunError, createBashTool, createEditTool, createGlobTool, createGrepTool, createReadTool, createWriteTool, defineTool, init, observe, useDataWriter, useModel, useSandbox, useTool, } from "@flue/runtime";
|
|
26
27
|
import { local, sqlite, start } from "@flue/runtime/node";
|
|
27
28
|
import { UsageBreakdown, makeAgentResult } from "./data_types.js";
|
|
28
29
|
import { registerOllamaModel } from "./ollama_provider.js";
|
|
@@ -127,6 +128,102 @@ export class ToolCallTracker {
|
|
|
127
128
|
return record;
|
|
128
129
|
}
|
|
129
130
|
}
|
|
131
|
+
// ── webfetch: Flue's stand-in for Claude Code's native WebFetch ─────────────
|
|
132
|
+
const WEBFETCH_MAX_BYTES = 200_000; // a bounded chunk of a page, not the whole thing — matches RESULT_SNIPPET_CHARS's spirit
|
|
133
|
+
const WEBFETCH_TIMEOUT_MS = 20_000;
|
|
134
|
+
const WebFetchParams = v.object({ url: v.pipe(v.string(), v.nonEmpty("url is required")) });
|
|
135
|
+
/** POSIX-safe single-quoting for a shell argument: closes and reopens the quote around any embedded `'`. */
|
|
136
|
+
function shQuote(value) {
|
|
137
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Strip `<script>`/`<style>` blocks and tags, decode the handful of entities
|
|
141
|
+
* a real docs page actually uses, and collapse whitespace. NOT a real
|
|
142
|
+
* HTML-to-markdown renderer — Claude Code's native `WebFetch` (the
|
|
143
|
+
* claude_code backend) does that; Flue has nothing equivalent built in, and
|
|
144
|
+
* a full renderer is more than this needs. A JSON/plain-text response
|
|
145
|
+
* passes through essentially unchanged, since none of these patterns match
|
|
146
|
+
* it — same "minimal, not a full parser" trade `jira_provider.ts`'s
|
|
147
|
+
* `adfToText` makes for ADF.
|
|
148
|
+
*/
|
|
149
|
+
export function stripHtml(text) {
|
|
150
|
+
const cleaned = text
|
|
151
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
152
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
153
|
+
.replace(/<[^>]+>/g, " ")
|
|
154
|
+
.replace(/ /g, " ")
|
|
155
|
+
.replace(/&/g, "&")
|
|
156
|
+
.replace(/</g, "<")
|
|
157
|
+
.replace(/>/g, ">")
|
|
158
|
+
.replace(/"/g, '"')
|
|
159
|
+
.replace(/'/g, "'")
|
|
160
|
+
.replace(/[ \t]+/g, " ");
|
|
161
|
+
// Line-by-line trim BEFORE collapsing blank runs: a tag->" " substitution
|
|
162
|
+
// (above) routinely leaves a stray leading/trailing space on the line that
|
|
163
|
+
// used to hold a block-level tag's boundary — collapsing blank lines first
|
|
164
|
+
// would miss those, since they're not yet blank.
|
|
165
|
+
return cleaned
|
|
166
|
+
.split("\n")
|
|
167
|
+
.map((line) => line.trim())
|
|
168
|
+
.join("\n")
|
|
169
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
170
|
+
.trim();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The flue backend's stand-in for Claude Code's native `WebFetch` tool —
|
|
174
|
+
* `@flue/runtime` ships no such built-in (its only tool factories are
|
|
175
|
+
* read/write/edit/bash/grep/glob), so this is a custom `defineTool()`.
|
|
176
|
+
*
|
|
177
|
+
* Runs the actual request THROUGH `env.exec()` rather than calling Node's
|
|
178
|
+
* own `fetch()` directly — deliberately: `exec()` is the one universal
|
|
179
|
+
* primitive every `Sandbox` implements (`local`, and the remote
|
|
180
|
+
* `opensandbox`/`cloudflare` backends via `core/sandbox.ts`), so a command
|
|
181
|
+
* run through it executes INSIDE whichever sandbox the agent is actually
|
|
182
|
+
* using. A remote sandbox's own `egress` policy (`sandbox_opensandbox.ts`'s
|
|
183
|
+
* `networkPolicy`) then governs this exactly as it already governs every
|
|
184
|
+
* `bash` call — no separate egress rule needed for this tool. Calling
|
|
185
|
+
* `fetch()` here instead would silently bypass that policy by making the
|
|
186
|
+
* request from SPF's own orchestrator process rather than the sandbox.
|
|
187
|
+
*
|
|
188
|
+
* `curl`, with a `wget` fallback in the SAME command for portability across
|
|
189
|
+
* whatever base image a remote sandbox happens to ship — both are close to
|
|
190
|
+
* universal, but neither is guaranteed; a sandbox image with neither
|
|
191
|
+
* surfaces that plainly as a failed tool call (exit code + stderr handed
|
|
192
|
+
* back to the model), not a hang. Only `http`/`https` are accepted — this is
|
|
193
|
+
* also what stops a `file://` URL from turning "fetch a page" into "read an
|
|
194
|
+
* arbitrary local file" on the `local` sandbox, where `exec()` runs with the
|
|
195
|
+
* operator's own full filesystem access.
|
|
196
|
+
*/
|
|
197
|
+
export function createWebFetchTool(env) {
|
|
198
|
+
return defineTool({
|
|
199
|
+
name: "webfetch",
|
|
200
|
+
description: "Fetch a URL over HTTP(S) and return its content as plain text (HTML tags stripped). " +
|
|
201
|
+
"Use it to check current documentation for a library, API, or tool before relying on prior " +
|
|
202
|
+
"knowledge that may be stale or version-specific. GET only, no custom headers/auth, response " +
|
|
203
|
+
"truncated to a safe size.",
|
|
204
|
+
input: WebFetchParams,
|
|
205
|
+
run: async ({ data }) => {
|
|
206
|
+
let parsed;
|
|
207
|
+
try {
|
|
208
|
+
parsed = new URL(data.url);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return `webfetch: ${JSON.stringify(data.url)} is not a valid URL`;
|
|
212
|
+
}
|
|
213
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
214
|
+
return `webfetch: unsupported scheme ${JSON.stringify(parsed.protocol)} — only http/https are allowed`;
|
|
215
|
+
}
|
|
216
|
+
const url = shQuote(parsed.toString());
|
|
217
|
+
const command = `(curl -sS -L --max-time 20 --max-redirs 5 -A "spf-webfetch/1.0" ${url} ` +
|
|
218
|
+
`|| wget -qO- --timeout=20 ${url}) | head -c ${WEBFETCH_MAX_BYTES}`;
|
|
219
|
+
const result = await env.exec(command, { timeoutMs: WEBFETCH_TIMEOUT_MS });
|
|
220
|
+
if (result.exitCode !== 0 || !result.stdout.trim()) {
|
|
221
|
+
return `webfetch: fetching ${parsed.toString()} failed (exit ${result.exitCode}): ${(result.stderr || "no output").trim().slice(0, 2000)}`;
|
|
222
|
+
}
|
|
223
|
+
return stripHtml(result.stdout);
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
130
227
|
// ── tool-name resolution ─────────────────────────────────────────────────────
|
|
131
228
|
const BUILTIN_TOOLS = {
|
|
132
229
|
read: createReadTool,
|
|
@@ -135,6 +232,7 @@ const BUILTIN_TOOLS = {
|
|
|
135
232
|
bash: createBashTool,
|
|
136
233
|
grep: createGrepTool,
|
|
137
234
|
glob: createGlobTool,
|
|
235
|
+
webfetch: createWebFetchTool,
|
|
138
236
|
};
|
|
139
237
|
// pi's vocabulary -> Flue's. "ls" has no Flue built-in (bash/glob cover it);
|
|
140
238
|
// it is a KNOWN name that resolves to nothing, not an unknown one.
|