feat(web): bulk select/clear-all leads; custom lead generates a real agent

- Select all / Clear all buttons in the Leads step toolbar - respects the
  current search filter, so filtering to "sql" then Select all only pins
  those, not all 412 leads. The per-category master switch (already
  select/deselect-all for that category, indeterminate when partial) was
  the only bulk control before; this adds the "everything" case.

- '+ Custom lead' now generates an ACTUAL specialist-agent markdown file
  (agents_md/vulns/custom_<slug>.md, same format every other agent uses)
  via the claude CLI on the operator's Anthropic subscription
  (claude-opus-4-8 by default - matches the harness's own default model),
  instead of folding free text into --focus. The new lead is immediately
  selectable and pinnable via --only like any other agent; verified the
  Rust harness's own agent loader picks it up (agent count went 435 -> 436,
  neurosploit agents confirmed it).

  Two things found and fixed while wiring this up:
  - the skip-permissions flag gave the model file/bash tool access, which
    made it try to write the file itself and narrate doing so instead of
    just returning text. Dropped the flag (pure text completion needs no
    tools) and told it explicitly not to use any.
  - Even so, defensively strip anything before the first '# ' heading
    before saving, in case a model still prepends commentary.

  Falls back to the old free-text-focus behavior if generation fails
  (claude not installed/logged in, malformed output, timeout) so the
  operator's intent isn't lost.

- New "Custom Leads" category, shown first, so generated leads have a
  visible home instead of landing in the catch-all "Other" bucket.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
This commit is contained in:
CyberSecurityUP
2026-08-23 15:43:13 -03:00
co-authored by Claude Sonnet 5
parent 8047c66e8f
commit e253b8b291
3 changed files with 151 additions and 3 deletions
+37 -3
View File
@@ -250,9 +250,43 @@ function renderCustomLeads() {
renderCustomLeads();
}));
}
$('#btnCustomLead').addEventListener('click', () => {
const text = prompt('Describe the custom lead (free text — becomes agent focus context):');
if (text && text.trim()) { state.customLeads.push(text.trim()); renderCustomLeads(); }
$('#btnCustomLead').addEventListener('click', async () => {
const text = prompt('Describe the custom lead — Claude (Opus, subscription) generates a real specialist agent for it, ready to pin:');
if (!text || !text.trim()) return;
const btn = $('#btnCustomLead');
const original = btn.textContent;
btn.disabled = true;
btn.textContent = 'Generating…';
try {
const { agent } = await api('/api/leads/generate', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ description: text.trim() }),
});
await loadAgents(); // re-read agents_md/ so the new file appears in its category
state.selected.add(agent.id);
renderBoard();
alert(`Generated and pinned: ${agent.title}`);
} catch (e) {
// Fall back to the old behavior — fold the raw text into --focus context
// — so a missing/logged-out Claude CLI doesn't lose the operator's intent.
state.customLeads.push(text.trim());
renderCustomLeads();
alert(`Couldn't generate a skill (${e.message}) — added as a focus hint instead.`);
} finally {
btn.disabled = false;
btn.textContent = original;
}
});
$('#btnSelectAll').addEventListener('click', () => {
// Respects the current search/filter — selects only what's visible, so a
// filtered view ("sql") + Select all pins just those leads, not all 412.
const visible = state.search.trim() ? allAgents().filter((a) => (a.title + ' ' + a.name).toLowerCase().includes(state.search.trim().toLowerCase())) : allAgents();
visible.forEach((a) => state.selected.add(a.id));
renderBoard();
});
$('#btnClearAll').addEventListener('click', () => {
const visible = state.search.trim() ? allAgents().filter((a) => (a.title + ' ' + a.name).toLowerCase().includes(state.search.trim().toLowerCase())) : allAgents();
visible.forEach((a) => state.selected.delete(a.id));
renderBoard();
});
// ---------------------------------------------------------------------------