Skip to content

Commit 72cf25d

Browse files
mpstatonclaude
andcommitted
new(didi-crawl, prompt-runner, org-workbench, search-and-add): the v1.2 crawl — three targets, relevance brief, staged team ingest
One organization.crawl capability in prompt-runner: a single model turn (Sonnet + Anthropic server-side web_search, the pause_turn loop already in runPrompt) composes queries, searches, filters by the per-workspace relevance brief, and returns candidates — never writes. Org context and the brief are fetched over NATS so the button and chat doors share one source of truth. Proven live against the Aspen safe target: 65s, 8 correctly-kinded deduped link candidates, including a brief-steered find (the Education & Society Program page). The relevance brief: relevance_briefs table behind client.brief.get/set, edited in place via the workbench header's BriefPanel — topical scope + people policy, per workspace client. Doors: 🤖 header buttons on the links and streams lists launch search-and-add's new crawl mode (the scan-mode precedent — no term bar, same ResultsList, per-row ➕ carrying the model's kind and a stream's real name into the write); 🤖 on People fires the team crawl, whose candidates stage in StagedPeople with the step-4 discipline: accept → person.candidates → no-candidate rows flow (apply create + affiliate, team-page URL as observation source), ambiguous rows open the gate. The policy filter is never silent (filtered_note rendered). Chat-legal: organization.crawl joins CHAT_CAPABILITY_NAMES with a WORKBENCH_CHAT_VERBS slab; focused-org context plumbing stays with the didi-chat plan. Closes #33. Plan: context-v/plans/Didi-Crawl-Three-Targets-Relevance-Brief-And-Staged-Team-Ingest.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UvYzx7vDWeafnkAi2nEQeb
1 parent dc676d4 commit 72cf25d

18 files changed

Lines changed: 840 additions & 7 deletions

File tree

apps/org-workbench/src/AdditiveList.svelte

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
nameable = false,
2121
onadd,
2222
onsearch,
23+
oncrawl,
2324
onedit,
2425
entryaction,
2526
}: {
@@ -31,6 +32,9 @@
3132
onadd: (url: string, kind?: string, name?: string) => Promise<void>;
3233
// Optional 🔍 — launches search-and-add pre-scoped to this list (Phase 3).
3334
onsearch?: () => void;
35+
// Optional 🤖 — didi's crawl for this whole list (v1.2): header-level,
36+
// because the list (not one entry) is the crawl's subject.
37+
oncrawl?: () => void;
3438
// Optional per-entry patch (kind/name matched by URL server-side) —
3539
// presence turns on the in-place editor.
3640
onedit?: (entry: Entry, patch: { kind?: string; name?: string }) => Promise<void>;
@@ -138,6 +142,11 @@
138142
<h3 class="ow-list-title">{title} <span class="ow-list-count">{entries.length}</span></h3>
139143
<span class="ow-list-actions">
140144
{#if justAdded}<span class="ow-added">added ✓</span>{/if}
145+
{#if oncrawl}
146+
<button type="button" class="ow-plus" title="didi: crawl the web for {title}" onclick={oncrawl}>
147+
🤖
148+
</button>
149+
{/if}
141150
{#if onsearch}
142151
<button type="button" class="ow-plus" title="Search the web for {title}" onclick={onsearch}>
143152
🔍

apps/org-workbench/src/App.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import OrgCard from './OrgCard.svelte';
1313
import OrgCreateInline from './OrgCreateInline.svelte';
1414
import OrgRoster from './OrgRoster.svelte';
15+
import BriefPanel from './BriefPanel.svelte';
1516
import { fetchOrgDetail } from './lib/org-client';
1617
import type { OrgDetail, OrgSuggestion } from './lib/types';
1718
@@ -122,6 +123,7 @@
122123
>
123124
{creating ? '×' : '+ New organization'}
124125
</button>
126+
<BriefPanel {client} />
125127
</div>
126128
{#if creating}
127129
<OrgCreateInline
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<script lang="ts">
2+
// The relevance brief — the operator's standing intent, per workspace
3+
// client: the topical scope ("what's relevant") and the people policy
4+
// ("who from a team page is worth ingesting"). Every didi crawl loads it
5+
// server-side; this panel is the view/edit door. State-Inspector ethos:
6+
// what the agent believes should be visible and editable.
7+
// Per context-v/specs/Augment-From-DB-Flow.md §v1.2.
8+
9+
import { fetchBrief, saveBrief } from './lib/org-client';
10+
11+
let { client }: { client: string } = $props();
12+
13+
let open = $state(false);
14+
let text = $state('');
15+
let loadedFor = $state<string | null>(null);
16+
let updatedAt = $state<string | null>(null);
17+
let busy = $state(false);
18+
let saved = $state(false);
19+
let error = $state<string | null>(null);
20+
21+
async function load(c: string) {
22+
busy = true;
23+
error = null;
24+
try {
25+
const r = await fetchBrief(c);
26+
text = r.brief ?? '';
27+
updatedAt = r.updated_at;
28+
loadedFor = c;
29+
} catch (err) {
30+
error = err instanceof Error ? err.message : String(err);
31+
} finally {
32+
busy = false;
33+
}
34+
}
35+
36+
$effect(() => {
37+
if (open && loadedFor !== client) void load(client);
38+
});
39+
40+
async function save() {
41+
busy = true;
42+
error = null;
43+
try {
44+
await saveBrief(client, text);
45+
saved = true;
46+
setTimeout(() => (saved = false), 2000);
47+
} catch (err) {
48+
error = err instanceof Error ? err.message : String(err);
49+
} finally {
50+
busy = false;
51+
}
52+
}
53+
</script>
54+
55+
<div class="ow-brief">
56+
<button type="button" class="ow-add-go" onclick={() => (open = !open)}>
57+
{open ? '× Relevance brief' : '📋 Relevance brief'}
58+
</button>
59+
{#if open}
60+
<div class="ow-brief-panel">
61+
<p class="ow-brief-hint">
62+
didi loads this into every crawl for <strong>{client}</strong> — topical scope (what's
63+
relevant) and the people policy (who from a team page is worth ingesting).
64+
{#if updatedAt}<span class="ow-brief-date">last saved {updatedAt.slice(0, 10)}</span>{/if}
65+
</p>
66+
<textarea
67+
class="ow-brief-text"
68+
rows="6"
69+
placeholder="e.g. Relevant: US higher-education and workforce-development funders, their education-adjacent publication streams. People policy: all major leadership, plus all team members covering Education & Workforce Development and related strategies/topics."
70+
bind:value={text}
71+
disabled={busy}
72+
></textarea>
73+
<span class="ow-addperson-actions">
74+
{#if saved}<span class="ow-added">saved ✓</span>{/if}
75+
<button type="button" class="ow-add-go" onclick={save} disabled={busy}>
76+
{busy ? '' : 'Save brief'}
77+
</button>
78+
</span>
79+
{#if error}<div class="ow-error">{error}</div>{/if}
80+
</div>
81+
{/if}
82+
</div>

apps/org-workbench/src/OrgCard.svelte

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@
5858
seed_term: seed(displayName),
5959
});
6060
}
61+
62+
// 🤖 — didi's crawl for a whole list (v1.2): same envelope, crawl flag on;
63+
// search-and-add's crawl mode fires organization.crawl instead of a term.
64+
function makeCrawl(target: 'links' | 'streams') {
65+
return () =>
66+
requestSearch({
67+
entity: { type: 'organization', org_slug: org.slug, display_name: displayName },
68+
target,
69+
seed_term: '',
70+
crawl: true,
71+
});
72+
}
6173
</script>
6274

6375
<article class="ow-card">
@@ -88,6 +100,7 @@
88100
kindHint="kind (auto: website/linkedin/x/…)"
89101
onadd={makeAdd(addOrgLink)}
90102
onsearch={makeSearch('links', (n) => `"${n}" LinkedIn`)}
103+
oncrawl={makeCrawl('links')}
91104
/>
92105
<AdditiveList
93106
title="Pulse streams"
@@ -97,6 +110,7 @@
97110
onadd={addStream}
98111
onedit={editStream}
99112
onsearch={makeSearch('streams', (n) => `"${n}" blog`)}
113+
oncrawl={makeCrawl('streams')}
100114
entryaction={{
101115
label: 'scan',
102116
fn: (stream) =>

apps/org-workbench/src/PeopleReveal.svelte

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
import { onMount } from 'svelte';
1010
import PersonCard from './PersonCard.svelte';
1111
import AddPersonInline from './AddPersonInline.svelte';
12-
import { fetchOrgAffiliations } from './lib/org-client';
12+
import StagedPeople from './StagedPeople.svelte';
13+
import { fetchOrgAffiliations, crawlTeam, type CrawledPerson } from './lib/org-client';
1314
import type { AffiliatedPerson } from './lib/types';
1415
1516
let {
@@ -29,6 +30,31 @@
2930
let error = $state<string | null>(null);
3031
let expanded = $state<string | null>(null); // person_uuid
3132
33+
// didi's team crawl (v1.2) — staged candidates, never auto-written.
34+
let crawling = $state(false);
35+
let crawlError = $state<string | null>(null);
36+
let crawlGen = $state(0); // bumps per crawl so StagedPeople remounts fresh
37+
let staged = $state<{
38+
people: CrawledPerson[];
39+
filtered_note: string;
40+
source_urls: string[];
41+
} | null>(null);
42+
43+
async function crawl() {
44+
crawling = true;
45+
crawlError = null;
46+
try {
47+
const r = await crawlTeam(org_slug, client);
48+
staged = r;
49+
crawlGen += 1;
50+
if (!open) toggle();
51+
} catch (err) {
52+
crawlError = err instanceof Error ? err.message : String(err);
53+
} finally {
54+
crawling = false;
55+
}
56+
}
57+
3258
async function load() {
3359
loading = true;
3460
error = null;
@@ -60,6 +86,8 @@
6086
people = [];
6187
loaded = false;
6288
expanded = null;
89+
staged = null;
90+
crawlError = null;
6391
if (open) void load();
6492
});
6593
@@ -76,7 +104,20 @@
76104
{open ? '' : ''} People{#if loaded}&nbsp;<span class="ow-list-count">{people.length}</span>{/if}
77105
</button>
78106
</h3>
107+
<span class="ow-list-actions">
108+
<button
109+
type="button"
110+
class="ow-plus"
111+
title="didi: crawl the web for relevant team members (selection per the relevance brief)"
112+
disabled={crawling}
113+
onclick={crawl}
114+
>
115+
{crawling ? '' : '🤖'}
116+
</button>
117+
</span>
79118
</header>
119+
{#if crawling}<p class="ow-empty">didi is crawling for team members — this takes a minute…</p>{/if}
120+
{#if crawlError}<div class="ow-error">{crawlError}</div>{/if}
80121

81122
{#if open}
82123
{#if loading}
@@ -110,6 +151,20 @@
110151
{/each}
111152
</ul>
112153
{/if}
154+
{#if staged}
155+
{#key crawlGen}
156+
<StagedPeople
157+
{org_slug}
158+
{orgName}
159+
{client}
160+
people={staged.people}
161+
filtered_note={staged.filtered_note}
162+
source_urls={staged.source_urls}
163+
onchanged={load}
164+
onclear={() => (staged = null)}
165+
/>
166+
{/key}
167+
{/if}
113168
<AddPersonInline {org_slug} {orgName} {client} onadded={load} />
114169
{/if}
115170
{/if}

0 commit comments

Comments
 (0)