Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/settings-ui/src/connections-pinned-by.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ describe("connections pinned-by", () => {
);
});

test("exa pins both web-search workflows", () => {
test("exa pins every web-search workflow", () => {
expect(CONNECTOR_PINNED_WORKFLOWS.exa).toEqual([
"last-30-days-research",
"exa-topic-watch",
"diligence-brief",
]);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/settings-ui/src/connections-pinned-by.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const CONNECTOR_PINNED_WORKFLOWS: Readonly<
"collateral-generation",
],
linear: ["morning-brief", "collateral-generation"],
exa: ["last-30-days-research", "exa-topic-watch"],
exa: ["last-30-days-research", "exa-topic-watch", "diligence-brief"],
attio: ["attio-task-agent"],
scrapecreators: ["reddit-opportunity-scanner"],
github: ["code-review"],
Expand Down
35 changes: 35 additions & 0 deletions packages/workflow-catalog/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,41 @@ export const WORKFLOW_CATALOG: readonly WorkflowCatalogEntry[] = [
},
],
},
{
assetName: "diligence-brief",
displayName: "Diligence brief",
automatable: false,
conversational: false,
deliveryMode: "workbench",
whatItDoes:
"Researches a company across web search and firm memory, and writes a cited diligence brief across five fixed sections, held for approval before it's saved.",
requiredConnections: ["exa"],
exampleOutput:
"Diligence brief: 5 sections, 2 flagged as insufficient evidence",
typicalDuration: "1-2 minutes, plus review and approval time",
// Mirrors workflows/diligence-brief/src/index.ts's system prompt
// exactly: "the trigger carries a `company` and an optional `focus`" —
// company is required (the prompt refuses to draft with no subject),
// focus narrows which angle to dig into and is skippable.
triggerFields: [
{
key: "company",
kind: "text",
label: "Company",
placeholder: "Acme Corp",
required: true,
help: "The company this brief is about.",
},
{
key: "focus",
kind: "text",
label: "Focus",
placeholder: "Founder track record",
required: false,
help: "Optional — narrows which angle to dig into.",
},
],
},
{
assetName: "exa-topic-watch",
displayName: "Web topic watch",
Expand Down
176 changes: 176 additions & 0 deletions workflows/diligence-brief/LICENSE

Large diffs are not rendered by default.

128 changes: 128 additions & 0 deletions workflows/diligence-brief/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# @corbits/diligence-brief-workflow

A single-step, mail-triggered workflow that researches a company and
writes a cited diligence brief as a markdown Library artifact, held for
one human approval (CL-6499, ported from scout's
`workflows/diligence-brief`).

## What ported cleanly

The evidence discipline: ground every claim in a specific tool result,
never invent a founder, funding round, metric, or competitor, and say
"insufficient evidence" plainly rather than padding a thin section. The
research order — firm memory first, then the live web — mirrors scout's
own "tool policy" (artifact refs, then knowledge-search, then web
research), minus the artifact-refs step (this deployment has no
per-brief source-artifact trigger yet). The finalize-then-approve
delivery shape is the same one every other artifact-producing workflow
in this catalog uses (`last-30-days-research`, `pain-point-collateral`,
`collateral-generation`).

## What did not port, and what this rebinds to instead

Scout's tools compile into scout's own sidecar and cannot be pinned here
(`packages/scout/README.md`, CL-5179), so every tool call is rebound to
a workbench-published package:

- Web research -> `@corbits/web-search-tools` (`web_search`, Exa-backed)
- Firm memory -> `@corbits/memory-tools` (`memory_search`)
- Artifact persistence -> `@corbits/artifacts-hub`'s workflow-artifacts
surface (`createWorkflowArtifact`, via this package's own
`finalize-tool.ts`/`artifact-client.ts` — duplicated rather than
imported, same convention every sibling workflow package uses).
That surface offers create + list-recent only, no search-by-field or
read-by-id — the same limit the sibling `@corbits/scout-agent` port
hit — so this workflow only ever writes one artifact per run; it
never reads a prior brief back by id.

Scout's own artifact-read/knowledge-search tools (reading a founder deck
or prior source document by locator) have no workbench equivalent yet:
this port has no source-artifact input at all, so that gap is moot for
now rather than silently dropped.

## What was trimmed, and why (read before comparing to scout's original)

This is a materially narrower brief than scout's. Be blunt about this
with anyone who has used the original:

- **No per-section parallel steps.** Scout drafts the brief as one
outline step plus one PARALLEL step per section (team, product,
revenue, capital, growth, market, risks), each well under a safe
output-token ceiling, then deterministically assembles them
(`assemble-brief`). This repo's execution host has never wired the
`action` primitive that parallel/assembled step graph assumes (the
same gap `last-30-days-research`'s own header documents). Folding
seven sections into seven _serial_ reasoning steps would multiply run
time and per-step timeout risk for a first port, so this port instead
follows `code-review`'s single-step shape: one agent, one turn,
everything — research, drafting, all five sections — in one pass.
- **Five sections, not seven.** `DILIGENCE_BRIEF_SECTIONS` folds scout's
separate Revenue & Business Model / Capital & Runway / Growth &
Traction sections into one "Traction & Funding" section, and Market &
Competition into "Product & Market" — a single-turn brief cannot
sustain three sections' worth of genuinely distinct evidence without
either padding or fabricating.
- **No scoring.** Scout scores seven weighted dimensions
(`SCORE_WEIGHTS`, `dims`/`dimensionReads`) and produces a structured
`DiligenceBrief` object. This port produces prose-only markdown, no
scores, no structured JSON. Scoring is real product value and cutting
it is a deliberate scope trim, not an oversight — restoring it is the
natural next step once the parallel-step machinery above lands here.
- **No source-artifact intake.** Scout's research step opens every
`sourceArtifactRefs` locator with artifact-read before searching the
web. This port has no equivalent trigger field, so research starts
from firm memory and the open web only.

## Verified end to end

`bun test` (package-scoped, stubbing every tool call — no live Exa, no
live inference) passes: the finalize tool persists a stubbed artifact
and returns `persisted: true` on success, returns an honest `isError:
true` naming the failure on a broken persist call rather than a
fabricated success or a silent empty brief, and rejects malformed
arguments without throwing. The definition itself is asserted to name
the exact approval-gated finalize tool, commit to always finalizing
(even on the no-data path), and survive the workflow-asset JSON
round-trip. `bun run typecheck` passes clean.

## Registration

Registered in `packages/workflow-catalog`'s `WORKFLOW_CATALOG` (asset
name `diligence-brief`) alongside this package's own `corbits.workflow`
block, so it shows up wherever that catalog is read: the seed step and
the web Routines picker. See [`workflows/README.md`](../README.md#status-note)
for what registration/automatable/seeded mean — this one is
`automatable: false` (gated behind a human-supplied company name per
run, same as `last-30-days-research`) and not seeded by default.

## Usage

```ts
import {
buildDiligenceBriefWorkflow,
serializeDiligenceBriefWorkflow,
} from "@corbits/diligence-brief-workflow";

const definition = buildDiligenceBriefWorkflow({
triggerAddress: "diligence-brief@tenant.example",
inferencePreferences: [{ provider: "anthropic", model: "claude-sonnet-5" }],
turnTimeoutMs: 600_000,
});

const json = serializeDiligenceBriefWorkflow(definition);
```

Pin `@corbits/web-search-tools` (needs an Exa key, handle `exa`) and
`@corbits/memory-tools` on the deployment for research to reach real
data; without them the brief honestly reports both as unreachable and
finalizes a status-note instead of a real brief.

### Trigger fields (`company`, `focus`)

Every run is triggered by mail to the deployment's address
(`triggerAddress` above); the one reasoning step reads two fields off
the trigger payload:

- `company` (required) — the company the brief is about.
- `focus` (optional) — narrows which angle to dig into (e.g.
"go-to-market" or "founder track record").
32 changes: 32 additions & 0 deletions workflows/diligence-brief/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@corbits/diligence-brief-workflow",
"private": true,
"description": "Single-step, mail-triggered workflow that researches a company and writes a cited diligence brief, held for approval",
"version": "0.0.1",
"license": "LGPL-2.1-or-later",
"type": "module",
"corbits": {
"workflow": {
"assetName": "diligence-brief",
"displayName": "Diligence brief",
"automatable": false
}
},
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit",
"test": "bun test"
},
"dependencies": {
"@intx/agent": "0.3.0",
"@intx/types": "0.3.0",
"@intx/workflow": "workspace:*",
"arktype": "catalog:"
},
"devDependencies": {
"@types/bun": "catalog:",
"typescript": "catalog:"
}
}
60 changes: 60 additions & 0 deletions workflows/diligence-brief/src/artifact-client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { expect, test } from "bun:test";

import { createWorkflowArtifact } from "./artifact-client";

const CONFIG = {
hubArtifactsUrl: "https://hub.example.com",
sidecarToken: "sc-token",
runAddress: "run_1@workflow",
};

test("posts to the workflow-artifacts endpoint and returns id/version", async () => {
const captured: { url: string; init: RequestInit | undefined } = {
url: "",
init: undefined,
};
const fetchImpl = (async (input: URL | string, init?: RequestInit) => {
captured.url = String(input);
captured.init = init;
return new Response(JSON.stringify({ data: { id: "art_1", version: 1 } }), {
status: 201,
});
}) as unknown as typeof fetch;

const result = await createWorkflowArtifact(
{ ...CONFIG, fetchImpl },
{ title: "Acme Corp", kind: "text", content: "# Diligence brief" },
);

expect(result).toEqual({ id: "art_1", version: 1 });
expect(captured.url).toBe("https://hub.example.com/api/workflow-artifacts/");
const headers = captured.init?.headers as Record<string, string>;
expect(headers["authorization"]).toBe("Bearer sc-token");
expect(headers["x-workflow-run-address"]).toBe("run_1@workflow");
});

test("throws on a non-ok HTTP response", async () => {
const fetchImpl = (async () =>
new Response("nope", { status: 500 })) as unknown as typeof fetch;

await expect(
createWorkflowArtifact(
{ ...CONFIG, fetchImpl },
{ title: "Acme Corp", kind: "text", content: "# Diligence brief" },
),
).rejects.toThrow(/500/);
});

test("throws when the response doesn't match the expected shape", async () => {
const fetchImpl = (async () =>
new Response(JSON.stringify({ data: {} }), {
status: 201,
})) as unknown as typeof fetch;

await expect(
createWorkflowArtifact(
{ ...CONFIG, fetchImpl },
{ title: "Acme Corp", kind: "text", content: "# Diligence brief" },
),
).rejects.toThrow(/did not match the expected shape/);
});
68 changes: 68 additions & 0 deletions workflows/diligence-brief/src/artifact-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// A minimal client for the sanctioned workflow-artifacts HTTP surface
// (`@corbits/artifacts-hub`'s `createWorkflowArtifactRoutes`, CL-6000).
// Deliberately duplicated from `@corbits/artifact-tools`' `client.ts`
// (and from every sibling workflow package's own copy, e.g.
// `last-30-days-research`) rather than imported: this package ships as
// installable data on the native workflow contract (see `./index.ts`'s
// header comment and `test/boundary.test.ts`), so its shipped sources
// import only `@intx/*`, `arktype`, and relative specifiers — never
// another `@corbits/*` package. Authenticates with the sidecar's own
// bearer token plus the run's own mailbox address (both already reach a
// workflow-process child's tool env), never a database handle.
import { type } from "arktype";

export interface WorkflowArtifactClientConfig {
readonly hubArtifactsUrl: string;
readonly sidecarToken: string;
readonly runAddress: string;
/** Override for tests; defaults to the global `fetch`. */
readonly fetchImpl?: typeof fetch;
}

export type CreateWorkflowArtifactInput = {
readonly title: string;
readonly kind: string;
readonly content: string;
};

export type CreatedWorkflowArtifact = {
readonly id: string;
readonly version: number;
};

const CreatedWorkflowArtifactResponse = type({
data: { id: "string", version: "number" },
});

/** Persists one artifact. Throws on any transport, HTTP, or shape failure. */
export async function createWorkflowArtifact(
config: WorkflowArtifactClientConfig,
input: CreateWorkflowArtifactInput,
): Promise<CreatedWorkflowArtifact> {
const doFetch = config.fetchImpl ?? fetch;
const response = await doFetch(
`${config.hubArtifactsUrl}/api/workflow-artifacts/`,
{
method: "POST",
headers: {
authorization: `Bearer ${config.sidecarToken}`,
"x-workflow-run-address": config.runAddress,
"content-type": "application/json",
},
body: JSON.stringify(input),
},
);
if (!response.ok) {
throw new Error(
`Workflow artifact create failed: ${response.status} ${response.statusText}`,
);
}
const body: unknown = await response.json();
const parsed = CreatedWorkflowArtifactResponse(body);
if (parsed instanceof type.errors) {
throw new Error(
`Workflow artifact create response did not match the expected shape: ${parsed.summary}`,
);
}
return parsed.data;
}
Loading
Loading