Skip to content

Commit 79225fc

Browse files
committed
Merge remote-tracking branch 'origin/main' into blove/founder-email-campaign-0cdecc
2 parents 45f2486 + 51dd0a1 commit 79225fc

35 files changed

Lines changed: 2753 additions & 26 deletions

‎.github/workflows/ci.yml‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ jobs:
4242
posthog: ${{ steps.scope.outputs.posthog }}
4343
scripts_tests: ${{ steps.scope.outputs.scripts_tests }}
4444
growth_lifecycle: ${{ steps.scope.outputs.growth_lifecycle }}
45+
growth_research: ${{ steps.scope.outputs.growth_research }}
4546
steps:
4647
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
4748
with:
@@ -271,6 +272,24 @@ jobs:
271272
- run: npx nx run lifecycle:check
272273
- run: npx nx build lifecycle
273274

275+
growth-research:
276+
name: Growth Research — Node 24
277+
needs: ci-scope
278+
if: github.event_name == 'push' || needs.ci-scope.outputs.growth_research == 'true'
279+
runs-on: ubuntu-latest
280+
timeout-minutes: 15
281+
steps:
282+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
283+
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
284+
with:
285+
node-version: 24
286+
cache: npm
287+
- run: npm ci --ignore-scripts
288+
- run: npx nx lint growth-research
289+
- run: npx nx test growth-research
290+
- run: npx nx check growth-research
291+
- run: npx nx build growth-research
292+
274293
cockpit:
275294
name: Cockpit — build / test
276295
needs: ci-scope
@@ -868,6 +887,7 @@ jobs:
868887
- scripts-tests
869888
- growth-lifecycle
870889
- lifecycle
890+
- growth-research
871891
# `CI — required` is the only required status context. A merge queue
872892
# waits on it for each candidate, so it must report on merge_group too —
873893
# otherwise every queued merge blocks forever on a check that never runs.
@@ -895,6 +915,7 @@ jobs:
895915
RESULT_SCRIPTS_TESTS: ${{ needs.scripts-tests.result }}
896916
RESULT_GROWTH_LIFECYCLE: ${{ needs.growth-lifecycle.result }}
897917
RESULT_LIFECYCLE: ${{ needs.lifecycle.result }}
918+
RESULT_GROWTH_RESEARCH: ${{ needs.growth-research.result }}
898919
SCOPE_LIBRARY: ${{ needs.ci-scope.outputs.library }}
899920
SCOPE_ANGULAR_COMPATIBILITY: ${{ needs.ci-scope.outputs.angular_compatibility }}
900921
SCOPE_WEBSITE: ${{ needs.ci-scope.outputs.website }}
@@ -909,6 +930,7 @@ jobs:
909930
SCOPE_POSTHOG: ${{ needs.ci-scope.outputs.posthog }}
910931
SCOPE_SCRIPTS_TESTS: ${{ needs.ci-scope.outputs.scripts_tests }}
911932
SCOPE_GROWTH_LIFECYCLE: ${{ needs.ci-scope.outputs.growth_lifecycle }}
933+
SCOPE_GROWTH_RESEARCH: ${{ needs.ci-scope.outputs.growth_research }}
912934
# The preview lanes need repository secrets, so they skip on fork
913935
# PRs. Their scope keys are computed from changed files alone, so on
914936
# a fork they can be in scope yet legitimately skipped. This mirrors
@@ -998,6 +1020,7 @@ jobs:
9981020
require_scoped "scripts_tests" "Scripts — generator / proxy vitest suites" "$RESULT_SCRIPTS_TESTS" "$SCOPE_SCRIPTS_TESTS"
9991021
require_scoped "growth_lifecycle" "Growth lifecycle — Node 22" "$RESULT_GROWTH_LIFECYCLE" "$SCOPE_GROWTH_LIFECYCLE"
10001022
require_scoped "growth_lifecycle" "Lifecycle — Node 24" "$RESULT_LIFECYCLE" "$SCOPE_GROWTH_LIFECYCLE"
1023+
require_scoped "growth_research" "Growth Research — Node 24" "$RESULT_GROWTH_RESEARCH" "$SCOPE_GROWTH_RESEARCH"
10011024
10021025
if [[ "$failed" -ne 0 ]]; then
10031026
exit 1

‎apps/growth-research/README.md‎

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,101 @@
1-
# Growth research compatibility application
1+
# Growth research application
2+
3+
## Local company research pilot
4+
5+
The local pilot compares one bounded Dawn agent with the existing lifecycle enrichment
6+
generator on identical captured company evidence. It has no Growth database connection,
7+
does not resolve people or employment, and cannot send email. The managed deployment
8+
still exposes only the synthetic compatibility graph documented below. Pilot routes,
9+
operator adapters, and their generated graph are excluded from its staged artifact.
10+
11+
Use Node 24 and the existing workspace dependencies. Build before running the agent:
12+
13+
```sh
14+
npx nx build growth-research
15+
npx tsx apps/growth-research/scripts/research-pilot.mts synthetic --output /absolute/private/pilot
16+
npx tsx apps/growth-research/scripts/research-pilot.mts acquire --output /absolute/private/pilot --domains threadplane.ai,dawnai.org,neon.tech,vercel.com,resend.com,langchain.com
17+
```
18+
19+
These commands return UUIDs for immutable JSON files in the selected output directory.
20+
Acquisition records include complete, partial, empty and failed outcomes. The existing
21+
fetcher can skip unusable pages, so missing paths have an unknown reason; redirects may
22+
make the original path indeterminate. Review the captured corpus before model calls:
23+
remove personal biography/contact snippets, retain empty cases and failures, and fill
24+
expected claims/unknowns from the actual captured evidence. Save the reviewed corpus
25+
under a new name/version. Acquisition is preparation, not a human quality label.
26+
27+
Set `GROWTH_RESEARCH_PILOT_MODE=local-company-only` and configure `OPENAI_API_KEY`
28+
for the agent or `ANTHROPIC_API_KEY` for the baseline through the operator environment.
29+
Never include keys in arguments, fixtures, reports or commits. The local in-process
30+
case context is also required: an environment flag alone cannot authorize pilot tools.
31+
32+
```sh
33+
npx tsx apps/growth-research/scripts/research-pilot.mts run --output /absolute/private/pilot --corpus /absolute/private/pilot/CORPUS_UUID.json --approach agent
34+
npx tsx apps/growth-research/scripts/research-pilot.mts run --output /absolute/private/pilot --corpus /absolute/private/pilot/CORPUS_UUID.json --approach baseline
35+
npx tsx apps/growth-research/scripts/research-pilot.mts inspect --output /absolute/private/pilot --run RUN_UUID
36+
```
37+
38+
Each case/approach/repetition has a separate run ID, deadline, budget and terminal
39+
record. Runs execute sequentially. The agent permits six provider requests, six evidence
40+
reads, 1,024 output tokens per request, no provider retries, a 20-second request timeout,
41+
and a 90-second run deadline. It can use the evidence skill and plan, read only captured
42+
case evidence, and submit a candidate. It cannot delegate, use memory, fetch URLs or
43+
read arbitrary files. Candidate acceptance is structural validation, not a truth label.
44+
Explicit inspection shows company sources and candidate findings; ordinary progress
45+
prints only opaque IDs and outcome codes. Reports use restrictive atomic writes and
46+
refuse overwrites. Preserve the final index and all failed attempts when comparing runs.
47+
48+
The baseline uses its existing provider/model and 1,200-token/30-second request bounds.
49+
It receives company mode, synthetic adapter form context and zero progress score.
50+
Its raw citations are captured before production normalization. It does not return
51+
quotes: `not_provided` is distinct from failing or passing exact-quote validation.
52+
Provider failure records retain known request/usage/citation diagnostics. Missing usage
53+
and cost are unavailable, never zero. This comparison measures whole approaches with
54+
different providers/models; it does not isolate Dawn's causal contribution.
55+
56+
Raw automatic tracing is disabled for local pilot runs. The record reports
57+
`tracing: unavailable`; this slice does not claim sanitized LangSmith tracing is live.
58+
No research findings are automatically published to Growth or typed memory.
59+
60+
### Human comparison
61+
62+
Each invocation emits a blinded review packet. To combine baseline and agent results
63+
for the same corpus, pass their index UUIDs; mixed corpus hashes/classes are rejected:
64+
65+
```sh
66+
npx tsx apps/growth-research/scripts/research-pilot.mts review --output /absolute/private/pilot --indices BASELINE_INDEX_UUID,AGENT_INDEX_UUID
67+
npx tsx apps/growth-research/scripts/research-pilot.mts score --output /absolute/private/pilot --packet PACKET_UUID --labels /absolute/private/pilot/human-labels.json
68+
```
69+
70+
The packet omits model and approach labels. Reviewers inspect each claim and profile
71+
against the captured sources, including failed cases. `human-labels.json` is an array:
72+
73+
```json
74+
[{"reviewId":"RUN_UUID","supportedClaims":0,"reviewedClaims":0,"supportedFields":0,"applicableFields":0,"correctAbstentions":3,"applicableAbstentions":3,"contradictionsMissed":0}]
75+
```
76+
77+
Use actual UUIDs and counts for each case. Reviewed claim count must match the packet;
78+
applicable fields and abstentions come from its expected unknowns. Imported labels and
79+
per-approach scores are persisted as a new review artifact. Aggregate quality scores
80+
remain unavailable while reviews are incomplete, preventing success-only denominators.
81+
Human semantic review is not replaced by model grading or string matching.
82+
83+
### Dogfooding findings ledger
84+
85+
| Finding | Evidence / owning layer | Status and next verification |
86+
| --- | --- | --- |
87+
| Nullable tool fields become required strings | Dawn 0.8.24 compiler JSON schema conversion; observed generated submit schema and failed unknown-field submissions | Upstream core and LangChain conversion regression/fix in progress. Pilot uses the supported authored Zod schema export; a package upgrade must rerun the original extraction probe before declaring the upstream defect released. |
88+
| Bound model calls bypass subclass generation hooks | Real bound-model regression in this application | Guards, request counts and JSON usage capture live at the actual provider fetch boundary; generated graph tests verify it. |
89+
| Page capture yields empty, partial, or mostly navigation evidence | Company-only acquisition against the six documented domains | Outcomes retained. Evaluate extraction improvements separately; do not hide failures by swapping cases. |
90+
| Baseline provider rejects billing state | Live baseline synthetic calls returned a classified billing rejection | External provider funding/configuration required; no quality comparison can be claimed from failed calls. |
91+
| Managed interruption precedes later child checkpoint | Recorded local/cloud Agent Server 0.13.4-node24 probe | Still a live-person integration gate; local cancellation tests are not proof of managed cancellation. |
92+
| Disabled memory and shared harness persistence behavior | Earlier synthetic compatibility probe on Dawn 0.8.24 | Reproduction-needed against current Dawn before assigning a fix. Pilot has no memory and graph tests use isolated state. |
93+
94+
Keep source snapshots, generated reports and review labels outside git. The full growth
95+
funnel/contact journey and real install/runtime-triggered enrichment are subsequent
96+
slices, after supported company context and the managed data lifecycle are verified.
97+
98+
## Synthetic compatibility deployment
299

3100
Private synthetic Dawn application, separate from lifecycle and the Python cockpit.
4101
Published Dawn packages are pinned to `0.8.24`; the app and deployment require Node 24.

‎apps/growth-research/dawn.config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { candidateMemoryStore, syntheticEmbedder, trustedFixtureScope } from './
55
export default {
66
appDir: 'src/app',
77
build: { targets: ['langsmith'] },
8-
toolOutput: { noOffloadTools: ['readFixture', 'coordinatorSummary', 'readSkill', 'writeTodos', 'recall', 'remember'] },
8+
toolOutput: { noOffloadTools: ['readFixture', 'coordinatorSummary', 'readSkill', 'writeTodos', 'recall', 'remember', 'readEvidence', 'submitCandidate'] },
99
summarization: { enabled: false },
1010
memory: {
1111
store: candidateMemoryStore,

‎apps/growth-research/eslint.config.mjs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,13 @@ import baseConfig from '../../eslint.config.mjs';
33
export default [
44
{ ignores: ['**/.dawn/**', '**/.deployment/**'] },
55
...baseConfig,
6+
{
7+
// Local-only benchmark adapters exercise the exact lifecycle baseline.
8+
// They are excluded from the standalone deployment; copying it would bias comparisons.
9+
files: [
10+
'apps/growth-research/src/pilot/baseline.ts',
11+
'apps/growth-research/src/pilot/acquisition.ts',
12+
],
13+
rules: { '@nx/enforce-module-boundaries': 'off' },
14+
},
615
];

‎apps/growth-research/project.json‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,21 @@
33
"$schema": "../../node_modules/nx/schemas/project-schema.json",
44
"sourceRoot": "apps/growth-research/src",
55
"projectType": "application",
6-
"tags": ["scope:internal", "scope:growth-research", "type:app", "runtime:node24"],
6+
"tags": [
7+
"scope:internal",
8+
"scope:growth-research",
9+
"type:app",
10+
"runtime:node24"
11+
],
712
"targets": {
13+
"pilot": {
14+
"executor": "nx:run-commands",
15+
"cache": false,
16+
"options": {
17+
"command": "tsx apps/growth-research/scripts/research-pilot.mts",
18+
"forwardAllArgs": true
19+
}
20+
},
821
"smoke-langsmith": {
922
"executor": "nx:run-commands",
1023
"cache": false,
@@ -17,7 +30,9 @@
1730
"test-memory-integration": {
1831
"executor": "nx:run-commands",
1932
"cache": false,
20-
"options": { "command": "npx vitest run --config apps/growth-research/vitest.memory-integration.config.ts --reporter=verbose" }
33+
"options": {
34+
"command": "npx vitest run --config apps/growth-research/vitest.memory-integration.config.ts --reporter=verbose"
35+
}
2136
},
2237
"test": {
2338
"executor": "@nx/vitest:test",
@@ -28,7 +43,10 @@
2843
"cache": false,
2944
"options": {
3045
"cwd": "apps/growth-research",
31-
"commands": ["node scripts/dawn-cli.mts check", "node ../../node_modules/typescript/bin/tsc --noEmit -p tsconfig.json"],
46+
"commands": [
47+
"node scripts/dawn-cli.mts check",
48+
"node ../../node_modules/typescript/bin/tsc --noEmit -p tsconfig.json"
49+
],
3250
"parallel": false
3351
}
3452
},
@@ -38,7 +56,11 @@
3856
"outputs": ["{projectRoot}/.deployment"],
3957
"options": {
4058
"cwd": "apps/growth-research",
41-
"commands": ["node scripts/dawn-cli.mts build --clean", "node scripts/package-langsmith.mts", "node scripts/verify-langsmith-artifact.mts"],
59+
"commands": [
60+
"node scripts/dawn-cli.mts build --clean",
61+
"node scripts/package-langsmith.mts",
62+
"node scripts/verify-langsmith-artifact.mts"
63+
],
4264
"parallel": false
4365
}
4466
},

‎apps/growth-research/scripts/package-langsmith.mts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ async function contained(root: string, path: string): Promise<void> {
4343
async function copySource(root: string, path: string, output: string): Promise<void> {
4444
await contained(root, path);
4545
const name = basename(path);
46+
const local = relative(root, path);
47+
if (local === 'src/app/enrichment/company-pilot') return;
48+
if (local.startsWith('src/pilot/') && !['context.ts', 'contracts.ts', 'validation.ts'].includes(name)) return;
4649
if (name.startsWith('.') || name === 'node_modules' || /\.(spec|test)\.[cm]?ts$/.test(name)) return;
4750
if ((await lstat(path)).isDirectory()) {
4851
await mkdir(output, { recursive: true });
@@ -116,7 +119,9 @@ export async function stageLangSmith(appRoot: string): Promise<string> {
116119
const config = await readObject(join(root, '.dawn/build/langgraph.json'));
117120
const generatedGraphs = object(config['graphs'], 'graphs');
118121
const specialistId = '/enrichment/research/subagents/researcher#agent';
119-
if (Object.keys(generatedGraphs).some(key => key !== graphId && key !== specialistId)) throw new Error('Unexpected generated graph');
122+
const pilotId = '/enrichment/company-pilot#agent';
123+
if (Object.keys(generatedGraphs).some(key => key !== graphId && key !== specialistId && key !== pilotId)) throw new Error('Unexpected generated graph');
124+
if (pilotId in generatedGraphs && generatedGraphs[pilotId] !== './.dawn/build/enrichment-company-pilot.ts:graph') throw new Error('Unexpected pilot graph');
120125
if (specialistId in generatedGraphs) {
121126
if (generatedGraphs[specialistId] !== './.dawn/build/enrichment-research-subagents-researcher.ts:graph') throw new Error('Unexpected specialist graph entry');
122127
await validateReference(root, generatedGraphs[specialistId], 'specialist graph');
@@ -141,6 +146,7 @@ export async function stageLangSmith(appRoot: string): Promise<string> {
141146
await copyFile(join(root, 'dawn.config.ts'), join(output, 'dawn.config.ts'));
142147
const copySchemas = async (path: string, target: string): Promise<void> => {
143148
await contained(root, path);
149+
if (['.dawn/routes/enrichment/company-pilot', '.dawn/routes/enrichment-company-pilot'].includes(relative(root, path))) return;
144150
if ((await lstat(path)).isDirectory()) {
145151
await mkdir(target, { recursive: true });
146152
for (const name of await readdir(path)) await copySchemas(join(path, name), join(target, name));
@@ -151,6 +157,7 @@ export async function stageLangSmith(appRoot: string): Promise<string> {
151157
};
152158
await copySchemas(join(root, '.dawn/routes'), join(output, '.dawn/routes'));
153159
for (const name of await readdir(join(root, '.dawn/build'))) {
160+
if (name === 'enrichment-company-pilot.ts') continue;
154161
if (!name.endsWith('.ts')) continue;
155162
await contained(root, join(root, '.dawn/build', name));
156163
await copyFile(join(root, '.dawn/build', name), join(output, '.dawn/build', name));

0 commit comments

Comments
 (0)