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
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ jobs:
posthog: ${{ steps.scope.outputs.posthog }}
scripts_tests: ${{ steps.scope.outputs.scripts_tests }}
growth_lifecycle: ${{ steps.scope.outputs.growth_lifecycle }}
growth_research: ${{ steps.scope.outputs.growth_research }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
Expand Down Expand Up @@ -271,6 +272,24 @@ jobs:
- run: npx nx run lifecycle:check
- run: npx nx build lifecycle

growth-research:
name: Growth Research — Node 24
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.growth_research == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24
cache: npm
- run: npm ci --ignore-scripts
- run: npx nx lint growth-research
- run: npx nx test growth-research
- run: npx nx check growth-research
- run: npx nx build growth-research

cockpit:
name: Cockpit — build / test
needs: ci-scope
Expand Down Expand Up @@ -868,6 +887,7 @@ jobs:
- scripts-tests
- growth-lifecycle
- lifecycle
- growth-research
# `CI — required` is the only required status context. A merge queue
# waits on it for each candidate, so it must report on merge_group too —
# otherwise every queued merge blocks forever on a check that never runs.
Expand Down Expand Up @@ -895,6 +915,7 @@ jobs:
RESULT_SCRIPTS_TESTS: ${{ needs.scripts-tests.result }}
RESULT_GROWTH_LIFECYCLE: ${{ needs.growth-lifecycle.result }}
RESULT_LIFECYCLE: ${{ needs.lifecycle.result }}
RESULT_GROWTH_RESEARCH: ${{ needs.growth-research.result }}
SCOPE_LIBRARY: ${{ needs.ci-scope.outputs.library }}
SCOPE_ANGULAR_COMPATIBILITY: ${{ needs.ci-scope.outputs.angular_compatibility }}
SCOPE_WEBSITE: ${{ needs.ci-scope.outputs.website }}
Expand All @@ -909,6 +930,7 @@ jobs:
SCOPE_POSTHOG: ${{ needs.ci-scope.outputs.posthog }}
SCOPE_SCRIPTS_TESTS: ${{ needs.ci-scope.outputs.scripts_tests }}
SCOPE_GROWTH_LIFECYCLE: ${{ needs.ci-scope.outputs.growth_lifecycle }}
SCOPE_GROWTH_RESEARCH: ${{ needs.ci-scope.outputs.growth_research }}
# The preview lanes need repository secrets, so they skip on fork
# PRs. Their scope keys are computed from changed files alone, so on
# a fork they can be in scope yet legitimately skipped. This mirrors
Expand Down Expand Up @@ -998,6 +1020,7 @@ jobs:
require_scoped "scripts_tests" "Scripts — generator / proxy vitest suites" "$RESULT_SCRIPTS_TESTS" "$SCOPE_SCRIPTS_TESTS"
require_scoped "growth_lifecycle" "Growth lifecycle — Node 22" "$RESULT_GROWTH_LIFECYCLE" "$SCOPE_GROWTH_LIFECYCLE"
require_scoped "growth_lifecycle" "Lifecycle — Node 24" "$RESULT_LIFECYCLE" "$SCOPE_GROWTH_LIFECYCLE"
require_scoped "growth_research" "Growth Research — Node 24" "$RESULT_GROWTH_RESEARCH" "$SCOPE_GROWTH_RESEARCH"

if [[ "$failed" -ne 0 ]]; then
exit 1
Expand Down
99 changes: 98 additions & 1 deletion apps/growth-research/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,101 @@
# Growth research compatibility application
# Growth research application

## Local company research pilot

The local pilot compares one bounded Dawn agent with the existing lifecycle enrichment
generator on identical captured company evidence. It has no Growth database connection,
does not resolve people or employment, and cannot send email. The managed deployment
still exposes only the synthetic compatibility graph documented below. Pilot routes,
operator adapters, and their generated graph are excluded from its staged artifact.

Use Node 24 and the existing workspace dependencies. Build before running the agent:

```sh
npx nx build growth-research
npx tsx apps/growth-research/scripts/research-pilot.mts synthetic --output /absolute/private/pilot
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
```

These commands return UUIDs for immutable JSON files in the selected output directory.
Acquisition records include complete, partial, empty and failed outcomes. The existing
fetcher can skip unusable pages, so missing paths have an unknown reason; redirects may
make the original path indeterminate. Review the captured corpus before model calls:
remove personal biography/contact snippets, retain empty cases and failures, and fill
expected claims/unknowns from the actual captured evidence. Save the reviewed corpus
under a new name/version. Acquisition is preparation, not a human quality label.

Set `GROWTH_RESEARCH_PILOT_MODE=local-company-only` and configure `OPENAI_API_KEY`
for the agent or `ANTHROPIC_API_KEY` for the baseline through the operator environment.
Never include keys in arguments, fixtures, reports or commits. The local in-process
case context is also required: an environment flag alone cannot authorize pilot tools.

```sh
npx tsx apps/growth-research/scripts/research-pilot.mts run --output /absolute/private/pilot --corpus /absolute/private/pilot/CORPUS_UUID.json --approach agent
npx tsx apps/growth-research/scripts/research-pilot.mts run --output /absolute/private/pilot --corpus /absolute/private/pilot/CORPUS_UUID.json --approach baseline
npx tsx apps/growth-research/scripts/research-pilot.mts inspect --output /absolute/private/pilot --run RUN_UUID
```

Each case/approach/repetition has a separate run ID, deadline, budget and terminal
record. Runs execute sequentially. The agent permits six provider requests, six evidence
reads, 1,024 output tokens per request, no provider retries, a 20-second request timeout,
and a 90-second run deadline. It can use the evidence skill and plan, read only captured
case evidence, and submit a candidate. It cannot delegate, use memory, fetch URLs or
read arbitrary files. Candidate acceptance is structural validation, not a truth label.
Explicit inspection shows company sources and candidate findings; ordinary progress
prints only opaque IDs and outcome codes. Reports use restrictive atomic writes and
refuse overwrites. Preserve the final index and all failed attempts when comparing runs.

The baseline uses its existing provider/model and 1,200-token/30-second request bounds.
It receives company mode, synthetic adapter form context and zero progress score.
Its raw citations are captured before production normalization. It does not return
quotes: `not_provided` is distinct from failing or passing exact-quote validation.
Provider failure records retain known request/usage/citation diagnostics. Missing usage
and cost are unavailable, never zero. This comparison measures whole approaches with
different providers/models; it does not isolate Dawn's causal contribution.

Raw automatic tracing is disabled for local pilot runs. The record reports
`tracing: unavailable`; this slice does not claim sanitized LangSmith tracing is live.
No research findings are automatically published to Growth or typed memory.

### Human comparison

Each invocation emits a blinded review packet. To combine baseline and agent results
for the same corpus, pass their index UUIDs; mixed corpus hashes/classes are rejected:

```sh
npx tsx apps/growth-research/scripts/research-pilot.mts review --output /absolute/private/pilot --indices BASELINE_INDEX_UUID,AGENT_INDEX_UUID
npx tsx apps/growth-research/scripts/research-pilot.mts score --output /absolute/private/pilot --packet PACKET_UUID --labels /absolute/private/pilot/human-labels.json
```

The packet omits model and approach labels. Reviewers inspect each claim and profile
against the captured sources, including failed cases. `human-labels.json` is an array:

```json
[{"reviewId":"RUN_UUID","supportedClaims":0,"reviewedClaims":0,"supportedFields":0,"applicableFields":0,"correctAbstentions":3,"applicableAbstentions":3,"contradictionsMissed":0}]
```

Use actual UUIDs and counts for each case. Reviewed claim count must match the packet;
applicable fields and abstentions come from its expected unknowns. Imported labels and
per-approach scores are persisted as a new review artifact. Aggregate quality scores
remain unavailable while reviews are incomplete, preventing success-only denominators.
Human semantic review is not replaced by model grading or string matching.

### Dogfooding findings ledger

| Finding | Evidence / owning layer | Status and next verification |
| --- | --- | --- |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |

Keep source snapshots, generated reports and review labels outside git. The full growth
funnel/contact journey and real install/runtime-triggered enrichment are subsequent
slices, after supported company context and the managed data lifecycle are verified.

## Synthetic compatibility deployment

Private synthetic Dawn application, separate from lifecycle and the Python cockpit.
Published Dawn packages are pinned to `0.8.24`; the app and deployment require Node 24.
Expand Down
2 changes: 1 addition & 1 deletion apps/growth-research/dawn.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { candidateMemoryStore, syntheticEmbedder, trustedFixtureScope } from './
export default {
appDir: 'src/app',
build: { targets: ['langsmith'] },
toolOutput: { noOffloadTools: ['readFixture', 'coordinatorSummary', 'readSkill', 'writeTodos', 'recall', 'remember'] },
toolOutput: { noOffloadTools: ['readFixture', 'coordinatorSummary', 'readSkill', 'writeTodos', 'recall', 'remember', 'readEvidence', 'submitCandidate'] },
summarization: { enabled: false },
memory: {
store: candidateMemoryStore,
Expand Down
9 changes: 9 additions & 0 deletions apps/growth-research/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,13 @@ import baseConfig from '../../eslint.config.mjs';
export default [
{ ignores: ['**/.dawn/**', '**/.deployment/**'] },
...baseConfig,
{
// Local-only benchmark adapters exercise the exact lifecycle baseline.
// They are excluded from the standalone deployment; copying it would bias comparisons.
files: [
'apps/growth-research/src/pilot/baseline.ts',
'apps/growth-research/src/pilot/acquisition.ts',
],
rules: { '@nx/enforce-module-boundaries': 'off' },
},
];
30 changes: 26 additions & 4 deletions apps/growth-research/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,21 @@
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/growth-research/src",
"projectType": "application",
"tags": ["scope:internal", "scope:growth-research", "type:app", "runtime:node24"],
"tags": [
"scope:internal",
"scope:growth-research",
"type:app",
"runtime:node24"
],
"targets": {
"pilot": {
"executor": "nx:run-commands",
"cache": false,
"options": {
"command": "tsx apps/growth-research/scripts/research-pilot.mts",
"forwardAllArgs": true
}
},
"smoke-langsmith": {
"executor": "nx:run-commands",
"cache": false,
Expand All @@ -17,7 +30,9 @@
"test-memory-integration": {
"executor": "nx:run-commands",
"cache": false,
"options": { "command": "npx vitest run --config apps/growth-research/vitest.memory-integration.config.ts --reporter=verbose" }
"options": {
"command": "npx vitest run --config apps/growth-research/vitest.memory-integration.config.ts --reporter=verbose"
}
},
"test": {
"executor": "@nx/vitest:test",
Expand All @@ -28,7 +43,10 @@
"cache": false,
"options": {
"cwd": "apps/growth-research",
"commands": ["node scripts/dawn-cli.mts check", "node ../../node_modules/typescript/bin/tsc --noEmit -p tsconfig.json"],
"commands": [
"node scripts/dawn-cli.mts check",
"node ../../node_modules/typescript/bin/tsc --noEmit -p tsconfig.json"
],
"parallel": false
}
},
Expand All @@ -38,7 +56,11 @@
"outputs": ["{projectRoot}/.deployment"],
"options": {
"cwd": "apps/growth-research",
"commands": ["node scripts/dawn-cli.mts build --clean", "node scripts/package-langsmith.mts", "node scripts/verify-langsmith-artifact.mts"],
"commands": [
"node scripts/dawn-cli.mts build --clean",
"node scripts/package-langsmith.mts",
"node scripts/verify-langsmith-artifact.mts"
],
"parallel": false
}
},
Expand Down
9 changes: 8 additions & 1 deletion apps/growth-research/scripts/package-langsmith.mts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ async function contained(root: string, path: string): Promise<void> {
async function copySource(root: string, path: string, output: string): Promise<void> {
await contained(root, path);
const name = basename(path);
const local = relative(root, path);
if (local === 'src/app/enrichment/company-pilot') return;
if (local.startsWith('src/pilot/') && !['context.ts', 'contracts.ts', 'validation.ts'].includes(name)) return;
if (name.startsWith('.') || name === 'node_modules' || /\.(spec|test)\.[cm]?ts$/.test(name)) return;
if ((await lstat(path)).isDirectory()) {
await mkdir(output, { recursive: true });
Expand Down Expand Up @@ -116,7 +119,9 @@ export async function stageLangSmith(appRoot: string): Promise<string> {
const config = await readObject(join(root, '.dawn/build/langgraph.json'));
const generatedGraphs = object(config['graphs'], 'graphs');
const specialistId = '/enrichment/research/subagents/researcher#agent';
if (Object.keys(generatedGraphs).some(key => key !== graphId && key !== specialistId)) throw new Error('Unexpected generated graph');
const pilotId = '/enrichment/company-pilot#agent';
if (Object.keys(generatedGraphs).some(key => key !== graphId && key !== specialistId && key !== pilotId)) throw new Error('Unexpected generated graph');
if (pilotId in generatedGraphs && generatedGraphs[pilotId] !== './.dawn/build/enrichment-company-pilot.ts:graph') throw new Error('Unexpected pilot graph');
if (specialistId in generatedGraphs) {
if (generatedGraphs[specialistId] !== './.dawn/build/enrichment-research-subagents-researcher.ts:graph') throw new Error('Unexpected specialist graph entry');
await validateReference(root, generatedGraphs[specialistId], 'specialist graph');
Expand All @@ -141,6 +146,7 @@ export async function stageLangSmith(appRoot: string): Promise<string> {
await copyFile(join(root, 'dawn.config.ts'), join(output, 'dawn.config.ts'));
const copySchemas = async (path: string, target: string): Promise<void> => {
await contained(root, path);
if (['.dawn/routes/enrichment/company-pilot', '.dawn/routes/enrichment-company-pilot'].includes(relative(root, path))) return;
if ((await lstat(path)).isDirectory()) {
await mkdir(target, { recursive: true });
for (const name of await readdir(path)) await copySchemas(join(path, name), join(target, name));
Expand All @@ -151,6 +157,7 @@ export async function stageLangSmith(appRoot: string): Promise<string> {
};
await copySchemas(join(root, '.dawn/routes'), join(output, '.dawn/routes'));
for (const name of await readdir(join(root, '.dawn/build'))) {
if (name === 'enrichment-company-pilot.ts') continue;
if (!name.endsWith('.ts')) continue;
await contained(root, join(root, '.dawn/build', name));
await copyFile(join(root, '.dawn/build', name), join(output, '.dawn/build', name));
Expand Down
Loading
Loading