surf-skill 2.1.1 → 4.0.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/CHANGELOG.md +264 -0
- package/README.md +119 -77
- package/SKILL.md +52 -52
- package/bin/surf-plan-skill.mjs +180 -0
- package/bin/{surf-skill.mjs → surf-search-skill.mjs} +41 -30
- package/bin/surf.mjs +314 -0
- package/logo.png +0 -0
- package/package.json +15 -5
- package/references/parallel-api.md +1 -1
- package/references/plan-workflow.md +137 -0
- package/skills/surf-plan-skill/SKILL.md +260 -0
- package/src/index.mjs +6 -3
- package/src/install/postinstall.mjs +8 -4
- package/src/lib/check-surf-skill.mjs +46 -0
- package/src/lib/dispatch.mjs +4 -4
- package/src/lib/format.mjs +1 -1
- package/src/lib/harness-install.mjs +34 -11
- package/src/lib/keys-cmd.mjs +31 -6
- package/src/lib/project-config.mjs +3 -3
- package/src/lib/setup.mjs +57 -21
- package/src/plan/plan-file.mjs +170 -0
- package/src/plan/plans-dir.mjs +46 -0
- package/src/plan/slug.mjs +55 -0
- package/src/validators/index.mjs +129 -0
package/bin/surf.mjs
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `surf` — bundle wrapper for surf-search-skill + surf-plan-skill.
|
|
3
|
+
//
|
|
4
|
+
// Running `surf` with no args launches an interactive setup that:
|
|
5
|
+
// 1. Verifies both skills are installed (symlinks present)
|
|
6
|
+
// 2. Lists currently-configured keys per provider
|
|
7
|
+
// 3. Offers an interactive menu: add / list / remove / doctor / quit
|
|
8
|
+
// 4. EVERY key added is validated LIVE against the provider's API
|
|
9
|
+
// before being saved (1-credit cost, ~1-3s per validation)
|
|
10
|
+
//
|
|
11
|
+
// This is the friendliest entry point. `surf-search-skill` and `surf-plan-skill`
|
|
12
|
+
// remain available for power users and scripts.
|
|
13
|
+
|
|
14
|
+
import readline from 'node:readline/promises';
|
|
15
|
+
import { stdin, stdout, stderr } from 'node:process';
|
|
16
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
import { loadState, saveStateAtomic, KEYS_FILE, PROVIDERS } from '../src/lib/state.mjs';
|
|
21
|
+
import { validateKey, formatValidation } from '../src/validators/index.mjs';
|
|
22
|
+
import { HARNESS_DIRS } from '../src/lib/harness-install.mjs';
|
|
23
|
+
|
|
24
|
+
const VERSION = '4.0.1';
|
|
25
|
+
|
|
26
|
+
const HELP = `surf — multi-skill setup & validation
|
|
27
|
+
|
|
28
|
+
Bundles surf-search-skill (multi-provider web search) and surf-plan-skill
|
|
29
|
+
(research-driven execution planning) into one command.
|
|
30
|
+
|
|
31
|
+
Commands:
|
|
32
|
+
(no args) Interactive setup wizard (add keys with live validation)
|
|
33
|
+
add Add a key (you'll be asked for provider + key)
|
|
34
|
+
list List configured keys (masked) + last-known state
|
|
35
|
+
validate [provider] Re-validate all keys (or just one provider's)
|
|
36
|
+
remove <provider> <i> Remove key #i from provider
|
|
37
|
+
doctor Diagnostics: skills installed? keys valid? harness symlinks?
|
|
38
|
+
--help, -h Show this help
|
|
39
|
+
--version, -v Show version
|
|
40
|
+
|
|
41
|
+
Power-user CLIs (also installed):
|
|
42
|
+
surf-search-skill ... The search engine (search/extract/crawl/map/research)
|
|
43
|
+
surf-plan-skill ... The planning skill (list/show/new/doctor)
|
|
44
|
+
|
|
45
|
+
Keys live in: ${KEYS_FILE} (chmod 600)
|
|
46
|
+
Plans live in: ~/.claude/plans/<slug>-<timestamp>.md (or ./plans/)
|
|
47
|
+
SKILL.md (search): ~/.agents/skills/surf-search-skill/SKILL.md
|
|
48
|
+
SKILL.md (planning): ~/.agents/skills/surf-plan-skill/SKILL.md
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
function out(s = '') {
|
|
52
|
+
stdout.write(s + (s.endsWith('\n') ? '' : '\n'));
|
|
53
|
+
}
|
|
54
|
+
function err(s) {
|
|
55
|
+
stderr.write(s + (s.endsWith('\n') ? '' : '\n'));
|
|
56
|
+
}
|
|
57
|
+
function mask(key) {
|
|
58
|
+
if (!key || key.length < 8) return key;
|
|
59
|
+
return key.slice(0, 5) + '…' + key.slice(-4);
|
|
60
|
+
}
|
|
61
|
+
function fmtBytes(n) {
|
|
62
|
+
if (n < 1024) return `${n}B`;
|
|
63
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
|
|
64
|
+
return `${(n / 1024 / 1024).toFixed(1)}MB`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function detectSkills() {
|
|
68
|
+
const home = os.homedir();
|
|
69
|
+
const skillsToCheck = ['surf-search-skill', 'surf-plan-skill'];
|
|
70
|
+
const found = {};
|
|
71
|
+
for (const skill of skillsToCheck) {
|
|
72
|
+
found[skill] = { dirs: [] };
|
|
73
|
+
for (const dir of HARNESS_DIRS) {
|
|
74
|
+
const link = path.join(dir, skill);
|
|
75
|
+
if (existsSync(link)) {
|
|
76
|
+
try {
|
|
77
|
+
const stat = await fs.lstat(link);
|
|
78
|
+
found[skill].dirs.push({
|
|
79
|
+
path: link,
|
|
80
|
+
isSymlink: stat.isSymbolicLink(),
|
|
81
|
+
isDir: stat.isDirectory(),
|
|
82
|
+
});
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return found;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function cmdList() {
|
|
91
|
+
const state = await loadState();
|
|
92
|
+
out(`**Keys** (config: \`${KEYS_FILE}\`, chmod 600)`);
|
|
93
|
+
out(`last_ok_provider: \`${state.last_ok_provider || 'none yet'}\``);
|
|
94
|
+
out('');
|
|
95
|
+
for (const p of PROVIDERS) {
|
|
96
|
+
const ps = state[p];
|
|
97
|
+
out(`## ${p} (${ps.keys.length} key${ps.keys.length === 1 ? '' : 's'})`);
|
|
98
|
+
if (!ps.keys.length) {
|
|
99
|
+
out(` _no keys — add with \`surf add\`_`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
for (let i = 0; i < ps.keys.length; i++) {
|
|
103
|
+
const isCur = i === ps.current ? ' (current)' : '';
|
|
104
|
+
const burned = ps.burned.find(b => b.index === i);
|
|
105
|
+
const burn = burned ? ` BURNED:${burned.reason} at ${burned.at.slice(0, 16)}` : '';
|
|
106
|
+
out(` [${i}] ${mask(ps.keys[i])}${isCur}${burn}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function cmdValidate(providerFilter) {
|
|
112
|
+
const state = await loadState();
|
|
113
|
+
let any = false;
|
|
114
|
+
for (const p of PROVIDERS) {
|
|
115
|
+
if (providerFilter && p !== providerFilter) continue;
|
|
116
|
+
const ps = state[p];
|
|
117
|
+
if (!ps.keys.length) continue;
|
|
118
|
+
any = true;
|
|
119
|
+
out(`\n## ${p}`);
|
|
120
|
+
for (let i = 0; i < ps.keys.length; i++) {
|
|
121
|
+
stdout.write(` [${i}] ${mask(ps.keys[i])} → `);
|
|
122
|
+
const r = await validateKey(p, ps.keys[i]);
|
|
123
|
+
out(formatValidation(r));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!any) out(providerFilter ? `No keys for ${providerFilter}.` : 'No keys configured. Add one with `surf add`.');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function cmdRemove(args) {
|
|
130
|
+
const [provider, indexStr] = args;
|
|
131
|
+
if (!provider || indexStr == null) {
|
|
132
|
+
err('Usage: surf remove <provider> <index>');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
if (!PROVIDERS.includes(provider)) {
|
|
136
|
+
err(`Unknown provider: ${provider}. Use: ${PROVIDERS.join('|')}`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
const idx = Number(indexStr);
|
|
140
|
+
const state = await loadState();
|
|
141
|
+
const ps = state[provider];
|
|
142
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= ps.keys.length) {
|
|
143
|
+
err(`Invalid index ${indexStr}; ${provider} has ${ps.keys.length} key${ps.keys.length === 1 ? '' : 's'} (0-${ps.keys.length - 1}).`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const removed = ps.keys.splice(idx, 1)[0];
|
|
147
|
+
ps.burned = ps.burned.filter(b => b.index !== idx).map(b => ({ ...b, index: b.index > idx ? b.index - 1 : b.index }));
|
|
148
|
+
if (ps.current >= ps.keys.length) ps.current = 0;
|
|
149
|
+
await saveStateAtomic(state);
|
|
150
|
+
out(`✓ removed ${provider} key #${idx} (${mask(removed)})`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function cmdAdd(rl) {
|
|
154
|
+
rl = rl || readline.createInterface({ input: stdin, output: stdout });
|
|
155
|
+
let closeRl = !arguments.length;
|
|
156
|
+
try {
|
|
157
|
+
out('');
|
|
158
|
+
let provider = '';
|
|
159
|
+
while (!PROVIDERS.includes(provider)) {
|
|
160
|
+
provider = (await rl.question(`Provider [${PROVIDERS.join('/')}]: `)).trim().toLowerCase();
|
|
161
|
+
if (!PROVIDERS.includes(provider)) out(` (unknown: ${provider}. Try: ${PROVIDERS.join(', ')})`);
|
|
162
|
+
}
|
|
163
|
+
const key = (await rl.question(`${provider} key: `)).trim();
|
|
164
|
+
if (!key) { out('(empty — cancelled)'); return; }
|
|
165
|
+
|
|
166
|
+
const state = await loadState();
|
|
167
|
+
const ps = state[provider];
|
|
168
|
+
if (ps.keys.includes(key)) {
|
|
169
|
+
out(` ℹ already configured at index ${ps.keys.indexOf(key)} — skipping`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
out(` validating against ${provider} API (1 credit, ~2s)…`);
|
|
174
|
+
const r = await validateKey(provider, key);
|
|
175
|
+
out(` ${formatValidation(r)}`);
|
|
176
|
+
if (!r.valid) {
|
|
177
|
+
out(' → key NOT saved. Try `surf add` again with a different key.');
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
ps.keys.push(key);
|
|
183
|
+
if (ps.keys.length === 1) ps.current = 0;
|
|
184
|
+
await saveStateAtomic(state);
|
|
185
|
+
out(`✓ saved as ${provider} key #${ps.keys.length - 1}. Total ${provider}: ${ps.keys.length}.`);
|
|
186
|
+
} finally {
|
|
187
|
+
if (closeRl) rl.close();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function cmdDoctor() {
|
|
192
|
+
out('## Skills');
|
|
193
|
+
const found = await detectSkills();
|
|
194
|
+
for (const [skill, info] of Object.entries(found)) {
|
|
195
|
+
if (!info.dirs.length) {
|
|
196
|
+
out(` ✗ ${skill}: NOT found in any harness skill dir`);
|
|
197
|
+
out(` → reinstall: npm i -g surf-skill@latest`);
|
|
198
|
+
process.exitCode = 1;
|
|
199
|
+
} else {
|
|
200
|
+
const sample = info.dirs[0];
|
|
201
|
+
out(` ✓ ${skill}: ${info.dirs.length} harness${info.dirs.length === 1 ? '' : 'es'}`);
|
|
202
|
+
for (const d of info.dirs) out(` ${d.path}${d.isSymlink ? ' (symlink)' : ''}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
out('\n## Keys');
|
|
207
|
+
const state = await loadState();
|
|
208
|
+
const totals = PROVIDERS.map(p => ({ p, n: state[p].keys.length, burned: state[p].burned.length }));
|
|
209
|
+
for (const t of totals) {
|
|
210
|
+
const status = t.n === 0 ? '⚠ no keys' : t.burned ? `${t.n} key(s), ${t.burned} burned` : `${t.n} key(s) ✓`;
|
|
211
|
+
out(` ${t.p.padEnd(10)} ${status}`);
|
|
212
|
+
}
|
|
213
|
+
if (totals.every(t => t.n === 0)) {
|
|
214
|
+
out('\n → Run `surf` to add your first key.');
|
|
215
|
+
process.exitCode = 2;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
out('\n## Plans');
|
|
219
|
+
const plansDir = path.join(os.homedir(), '.claude', 'plans');
|
|
220
|
+
if (existsSync(plansDir)) {
|
|
221
|
+
const files = (await fs.readdir(plansDir)).filter(f => f.endsWith('.md'));
|
|
222
|
+
out(` ${files.length} plan file${files.length === 1 ? '' : 's'} in ${plansDir}`);
|
|
223
|
+
} else {
|
|
224
|
+
out(` ${plansDir} not created yet`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function interactiveMenu() {
|
|
229
|
+
out('');
|
|
230
|
+
out('┌─ surf — multi-skill setup & validation ─────────────────');
|
|
231
|
+
out(`│ Skills detected:`);
|
|
232
|
+
const found = await detectSkills();
|
|
233
|
+
for (const [skill, info] of Object.entries(found)) {
|
|
234
|
+
const status = info.dirs.length ? `✓ ${info.dirs.length} harness${info.dirs.length === 1 ? '' : 'es'}` : '✗ NOT INSTALLED';
|
|
235
|
+
out(`│ ${skill.padEnd(20)} ${status}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const state = await loadState();
|
|
239
|
+
const counts = PROVIDERS.map(p => `${p} ${state[p].keys.length}`).join(', ');
|
|
240
|
+
out(`│ Keys: ${counts}`);
|
|
241
|
+
out(`│ Config: ${KEYS_FILE}`);
|
|
242
|
+
out('└──────────────────────────────────────────────────────────');
|
|
243
|
+
out('');
|
|
244
|
+
|
|
245
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
246
|
+
try {
|
|
247
|
+
while (true) {
|
|
248
|
+
out('What do you want to do?');
|
|
249
|
+
out(' [1] Add a key (with live validation)');
|
|
250
|
+
out(' [2] List + revalidate all keys');
|
|
251
|
+
out(' [3] Remove a key');
|
|
252
|
+
out(' [4] Diagnostics (skills + symlinks + dirs)');
|
|
253
|
+
out(' [q] Quit');
|
|
254
|
+
const choice = (await rl.question('> ')).trim().toLowerCase();
|
|
255
|
+
out('');
|
|
256
|
+
if (choice === '1' || choice === 'add') {
|
|
257
|
+
await cmdAdd(rl);
|
|
258
|
+
} else if (choice === '2' || choice === 'list') {
|
|
259
|
+
await cmdValidate();
|
|
260
|
+
} else if (choice === '3' || choice === 'remove') {
|
|
261
|
+
const provider = (await rl.question(`Provider [${PROVIDERS.join('/')}]: `)).trim();
|
|
262
|
+
const idx = (await rl.question('Index: ')).trim();
|
|
263
|
+
await cmdRemove([provider, idx]).catch(e => err(`✗ ${e.message}`));
|
|
264
|
+
} else if (choice === '4' || choice === 'doctor') {
|
|
265
|
+
await cmdDoctor();
|
|
266
|
+
} else if (choice === 'q' || choice === 'quit' || choice === 'exit' || !choice) {
|
|
267
|
+
out('bye 🌊');
|
|
268
|
+
return;
|
|
269
|
+
} else {
|
|
270
|
+
out(`(unknown choice: ${choice})`);
|
|
271
|
+
}
|
|
272
|
+
out('');
|
|
273
|
+
}
|
|
274
|
+
} finally {
|
|
275
|
+
rl.close();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const [, , cmd, ...rest] = process.argv;
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
if (!cmd) {
|
|
283
|
+
if (!stdin.isTTY) {
|
|
284
|
+
err(`surf requires a TTY for interactive setup. Use a subcommand:
|
|
285
|
+
surf add | list | validate | remove <provider> <i> | doctor`);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
await interactiveMenu();
|
|
289
|
+
} else if (cmd === '--help' || cmd === '-h') {
|
|
290
|
+
out(HELP);
|
|
291
|
+
} else if (cmd === '--version' || cmd === '-v') {
|
|
292
|
+
out(VERSION);
|
|
293
|
+
} else if (cmd === 'add') {
|
|
294
|
+
if (!stdin.isTTY) {
|
|
295
|
+
err('`surf add` is interactive and requires a TTY. Use `surf-search-skill keys add --provider X <key>` for scripts.');
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
await cmdAdd();
|
|
299
|
+
} else if (cmd === 'list') {
|
|
300
|
+
await cmdList();
|
|
301
|
+
} else if (cmd === 'validate') {
|
|
302
|
+
await cmdValidate(rest[0]);
|
|
303
|
+
} else if (cmd === 'remove') {
|
|
304
|
+
await cmdRemove(rest);
|
|
305
|
+
} else if (cmd === 'doctor') {
|
|
306
|
+
await cmdDoctor();
|
|
307
|
+
} else {
|
|
308
|
+
err(`Unknown command: ${cmd}. Try 'surf --help'.`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
} catch (e) {
|
|
312
|
+
err(`❌ Error: ${e.message || String(e)}`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
package/logo.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "surf-skill",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Multi-provider web
|
|
3
|
+
"version": "4.0.1",
|
|
4
|
+
"description": "Multi-skill bundle for AI coding agents: surf-search-skill (multi-provider web search via Tavily + Parallel + Brave) + surf-plan-skill (research-driven execution planning). Includes `surf` interactive setup with live key validation, automatic provider fallback, multi-key rotation, --mode tiers, per-project timeout config, and a Node library.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.mjs",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.mjs",
|
|
9
|
+
"./plan": "./src/plan/plan-file.mjs",
|
|
10
|
+
"./validators": "./src/validators/index.mjs",
|
|
9
11
|
"./package.json": "./package.json"
|
|
10
12
|
},
|
|
11
13
|
"bin": {
|
|
12
|
-
"surf
|
|
14
|
+
"surf": "./bin/surf.mjs",
|
|
15
|
+
"surf-search-skill": "./bin/surf-search-skill.mjs",
|
|
16
|
+
"surf-plan-skill": "./bin/surf-plan-skill.mjs"
|
|
13
17
|
},
|
|
14
18
|
"files": [
|
|
15
19
|
"bin/",
|
|
16
20
|
"src/",
|
|
21
|
+
"skills/",
|
|
17
22
|
"references/",
|
|
18
23
|
"SKILL.md",
|
|
19
24
|
"README.md",
|
|
20
25
|
"CHANGELOG.md",
|
|
21
|
-
"LICENSE"
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"logo.png"
|
|
22
28
|
],
|
|
23
29
|
"scripts": {
|
|
24
30
|
"postinstall": "node ./src/install/postinstall.mjs || true",
|
|
25
31
|
"preuninstall": "node ./src/install/preuninstall.mjs || true",
|
|
26
|
-
"test:syntax": "node --check bin/surf-skill.mjs && for f in src/index.mjs src/env.mjs src/install/*.mjs src/lib/*.mjs src/lib/providers/*.mjs src/lib/api/*.mjs; do node --check \"$f\" || exit 1; done && echo syntax-ok"
|
|
32
|
+
"test:syntax": "node --check bin/surf.mjs && node --check bin/surf-search-skill.mjs && node --check bin/surf-plan-skill.mjs && for f in src/index.mjs src/env.mjs src/install/*.mjs src/lib/*.mjs src/lib/providers/*.mjs src/lib/api/*.mjs src/plan/*.mjs src/validators/*.mjs; do node --check \"$f\" || exit 1; done && echo syntax-ok"
|
|
27
33
|
},
|
|
28
34
|
"engines": {
|
|
29
35
|
"node": ">=18"
|
|
@@ -35,6 +41,10 @@
|
|
|
35
41
|
"brave-search",
|
|
36
42
|
"web-search",
|
|
37
43
|
"connector",
|
|
44
|
+
"planning",
|
|
45
|
+
"spec-driven",
|
|
46
|
+
"research-driven",
|
|
47
|
+
"execution-plan",
|
|
38
48
|
"claude-code",
|
|
39
49
|
"copilot-cli",
|
|
40
50
|
"pi-coding-agent",
|
|
@@ -102,7 +102,7 @@ Returns **HTTP 202** with `{ run_id, status: "queued" \| "running", is_active, p
|
|
|
102
102
|
}
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
Processor mapping used by `surf-skill research`:
|
|
105
|
+
Processor mapping used by `surf-search-skill research`:
|
|
106
106
|
|
|
107
107
|
| `--model` | Parallel processor |
|
|
108
108
|
|---|---|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# surf-plan-skill workflow — deeper docs
|
|
2
|
+
|
|
3
|
+
This file expands on the 6-phase workflow defined in `SKILL.md`. It's
|
|
4
|
+
for humans reviewing the methodology, not for the agent (which only
|
|
5
|
+
reads SKILL.md).
|
|
6
|
+
|
|
7
|
+
## The 6 phases
|
|
8
|
+
|
|
9
|
+
### Phase 0 — preflight
|
|
10
|
+
|
|
11
|
+
Why: if `surf-search-skill` is missing, web research is impossible, and the
|
|
12
|
+
plan would just be the agent's training-time hallucinations dressed up
|
|
13
|
+
in markdown. Halt is the right move.
|
|
14
|
+
|
|
15
|
+
How: `surf-search-skill --version` exits 0 → continue. Exits non-zero or
|
|
16
|
+
command not found → halt with install instructions.
|
|
17
|
+
|
|
18
|
+
### Phase 1 — project discovery
|
|
19
|
+
|
|
20
|
+
Why: plans that skip this duplicate existing code. ~5 minutes of
|
|
21
|
+
reading saves hours of integration pain.
|
|
22
|
+
|
|
23
|
+
How:
|
|
24
|
+
1. `CLAUDE.md`, `AGENTS.md`, `README.md` — house style + constraints.
|
|
25
|
+
2. Package manifest — language, framework, deps.
|
|
26
|
+
3. Glob the source tree (2 levels deep).
|
|
27
|
+
4. Identify 2–3 existing patterns / utilities the new feature should
|
|
28
|
+
reuse.
|
|
29
|
+
5. Note relevant configs (eslint, tsconfig, docker, ci).
|
|
30
|
+
|
|
31
|
+
Output: agent's mental model of "what already exists" + file paths.
|
|
32
|
+
|
|
33
|
+
### Phase 2 — baseline web research
|
|
34
|
+
|
|
35
|
+
Why: ground the plan in 2026 best practices, not 2024 training data.
|
|
36
|
+
|
|
37
|
+
How: ONE batched `surf-search-skill search` with 3 queries. Distill:
|
|
38
|
+
- 3 dominant approaches
|
|
39
|
+
- 2–3 common mistakes
|
|
40
|
+
- 1–2 security/performance gotchas
|
|
41
|
+
|
|
42
|
+
Cost: ~6 credits (Tavily) + ~10 s. Acceptable for any non-trivial plan.
|
|
43
|
+
|
|
44
|
+
### Phase 3 — open the conversation
|
|
45
|
+
|
|
46
|
+
Why: the user needs to see you've done your homework before they
|
|
47
|
+
answer questions.
|
|
48
|
+
|
|
49
|
+
How: ≤8 lines. What you read + what the web says + how many questions
|
|
50
|
+
you have.
|
|
51
|
+
|
|
52
|
+
### Phase 4 — clarifying questions
|
|
53
|
+
|
|
54
|
+
Why: even after research, certain decisions require the user's
|
|
55
|
+
preference (e.g., "Redis or Postgres for the queue?", "do we need
|
|
56
|
+
multi-tenancy from day one?").
|
|
57
|
+
|
|
58
|
+
How:
|
|
59
|
+
- For each question: targeted `surf-search-skill search --max 2` first.
|
|
60
|
+
- AskUserQuestion with options informed by the search.
|
|
61
|
+
- Max 5 total.
|
|
62
|
+
|
|
63
|
+
Anti-pattern: asking the user to choose between options that the agent
|
|
64
|
+
just made up. Search first; pick options from real approaches.
|
|
65
|
+
|
|
66
|
+
### Phase 5 — synthesis research
|
|
67
|
+
|
|
68
|
+
Why: verify the user's choices against the very-latest state of the
|
|
69
|
+
art. Catches "you chose X but X v2 dropped support for Y last month".
|
|
70
|
+
|
|
71
|
+
How: ONE batched search with the user's chosen approach. ~6 credits.
|
|
72
|
+
If contradictions appear, flag them BEFORE writing the plan.
|
|
73
|
+
|
|
74
|
+
### Phase 6 — write the plan
|
|
75
|
+
|
|
76
|
+
Why: a plan file is a contract. It's reviewable, executable, and
|
|
77
|
+
auditable. Chat history is none of those.
|
|
78
|
+
|
|
79
|
+
How: Markdown with the structure from SKILL.md. Required sections:
|
|
80
|
+
Context, Decisions, Files, Steps, Verification, References.
|
|
81
|
+
|
|
82
|
+
Citations use Markdown footnote syntax `[^N]: [Title](URL)` — renders
|
|
83
|
+
in GitHub, GitLab, Bitbucket, Cursor, Plannotator, and most other
|
|
84
|
+
viewers.
|
|
85
|
+
|
|
86
|
+
## Cost discipline
|
|
87
|
+
|
|
88
|
+
A typical plan uses:
|
|
89
|
+
- 1 batch (Phase 2): 3 queries, ~6 credits, ~10 s
|
|
90
|
+
- 3–5 targeted (Phase 4): 1 query each, ~5 credits, ~3 s each
|
|
91
|
+
- 1 batch (Phase 5): 2–3 queries, ~5 credits, ~8 s
|
|
92
|
+
|
|
93
|
+
Total: ~15–20 credits, ~30 s of network time. On Tavily free tier
|
|
94
|
+
(1k credits/month), that's ~50 plans per month for free.
|
|
95
|
+
|
|
96
|
+
## Anti-patterns explained
|
|
97
|
+
|
|
98
|
+
**Verbose research summary section**
|
|
99
|
+
A "Research Findings" section dumping every URL with snippet is noise
|
|
100
|
+
— it inflates the plan, distracts the executor, and ages instantly.
|
|
101
|
+
Synthesize: the plan has Decisions with footnotes; that's the
|
|
102
|
+
research's role in the final document.
|
|
103
|
+
|
|
104
|
+
**Aesthetic questions**
|
|
105
|
+
"What color theme?" / "Should we name it FooBar or BarFoo?" — these
|
|
106
|
+
aren't planning questions. Defer to the execution phase.
|
|
107
|
+
|
|
108
|
+
**Single-source citations**
|
|
109
|
+
If every decision footnotes the same URL, the research was shallow.
|
|
110
|
+
Diversify: aim for 1 citation per major source category (vendor docs,
|
|
111
|
+
community blog, security advisory, benchmark, etc.).
|
|
112
|
+
|
|
113
|
+
**Plans without file paths**
|
|
114
|
+
"Update the controller layer" is not a plan, it's a wish. Phase 1
|
|
115
|
+
exists so the plan can say `src/controllers/user.ts:42`.
|
|
116
|
+
|
|
117
|
+
**Surveys (>5 questions)**
|
|
118
|
+
Beyond 5 questions, the user fatigues and starts answering "whatever".
|
|
119
|
+
If you genuinely need more, the task is too big — slice it.
|
|
120
|
+
|
|
121
|
+
## Plan file conventions
|
|
122
|
+
|
|
123
|
+
- File name: `<slug>-<YYYYMMDD-HHMM>.md`. Slug is kebab-case, ≤50 chars.
|
|
124
|
+
- Slug collision: append `-2`, `-3`, etc.
|
|
125
|
+
- Title in `# Plan: ...` matches the slug.
|
|
126
|
+
- Footnote order: in the order they're first cited.
|
|
127
|
+
- URLs: full https links, no trackers or auth tokens.
|
|
128
|
+
- Code references: `path/to/file.ext:LINE` format, parseable by most IDEs.
|
|
129
|
+
|
|
130
|
+
## What surf-plan-skill is NOT
|
|
131
|
+
|
|
132
|
+
- Not an execution engine. Once the plan is written, hand it to another
|
|
133
|
+
tool/agent/human.
|
|
134
|
+
- Not a project manager. It's per-task, not multi-task.
|
|
135
|
+
- Not a code generator. It writes a plan, not code.
|
|
136
|
+
- Not a replacement for Plan Mode. Plan Mode is more interactive but
|
|
137
|
+
ephemeral and without web research. They complement.
|