Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
52a57ed
Merge pull request #74 from trycompai/main
carhartlewis Aug 7, 2026
1561073
chore: release release
github-actions[bot] Aug 7, 2026
5074fc4
Merge pull request #75 from trycompai/release-please--branches--release
carhartlewis Aug 7, 2026
3c20d80
Merge pull request #77 from trycompai/main
carhartlewis Aug 7, 2026
808b835
Merge pull request #79 from trycompai/main
carhartlewis Aug 7, 2026
407280a
Merge pull request #81 from trycompai/main
carhartlewis Aug 7, 2026
c26a08d
Merge pull request #84 from trycompai/main
carhartlewis Aug 7, 2026
d585dc3
Merge pull request #90 from trycompai/main
carhartlewis Aug 8, 2026
d0299d9
Merge pull request #98 from trycompai/main
carhartlewis Aug 11, 2026
7d4a573
Merge pull request #107 from trycompai/main
carhartlewis Aug 11, 2026
56f4eeb
Merge pull request #116 from trycompai/main
github-actions[bot] Aug 11, 2026
57001e6
Merge pull request #119 from trycompai/main
github-actions[bot] Aug 11, 2026
ad1d702
Merge pull request #122 from trycompai/main
github-actions[bot] Aug 11, 2026
fc0c594
Merge pull request #127 from trycompai/main
github-actions[bot] Aug 11, 2026
4ffe150
Merge pull request #130 from trycompai/main
github-actions[bot] Aug 11, 2026
14cd220
Merge pull request #135 from trycompai/main
github-actions[bot] Aug 11, 2026
f2484fb
Merge pull request #141 from trycompai/main
github-actions[bot] Aug 12, 2026
bb63520
Merge pull request #161 from trycompai/main
github-actions[bot] Aug 18, 2026
517d859
Merge pull request #165 from trycompai/main
github-actions[bot] Aug 20, 2026
b842bd6
Merge pull request #168 from trycompai/main
github-actions[bot] Aug 20, 2026
77089e4
Merge pull request #172 from trycompai/main
github-actions[bot] Aug 20, 2026
6d4793d
Merge pull request #177 from trycompai/main
github-actions[bot] Aug 21, 2026
efd44f0
feat(api): add signal ingest contracts
lumenstech Sep 4, 2026
3616bba
feat(api): add idempotent signal ingest service
lumenstech Sep 4, 2026
fd80927
feat(api): expose central signal ingest endpoint
lumenstech Sep 4, 2026
fc1fa7c
feat(api): register ingest module
lumenstech Sep 4, 2026
defcff1
feat(api): wire central signal ingest module
lumenstech Sep 4, 2026
929fb27
feat(api): add signal inbox contracts
lumenstech Sep 4, 2026
b13ebae
feat(api): add signal inbox query service
lumenstech Sep 4, 2026
07e35ad
feat(api): expose signal inbox endpoint
lumenstech Sep 4, 2026
5f6181c
feat(api): add signal company resolution contracts
lumenstech Sep 4, 2026
d04ce53
feat(api): resolve signal companies and fix mapping lookup
lumenstech Sep 4, 2026
617f711
feat(api): connect ingest resolver to companies service
lumenstech Sep 4, 2026
c712b82
feat(api): expose signal company resolution endpoints
lumenstech Sep 4, 2026
28c6969
fix(api): use production reconciliation columns without Prisma assump…
lumenstech Sep 4, 2026
4155b5b
feat(api): add signal qualification and promotion contracts
lumenstech Sep 4, 2026
3023b61
feat(api): add signal qualification and promotion service
lumenstech Sep 4, 2026
fb0dfc6
feat(api): wire signal qualification service
lumenstech Sep 4, 2026
db28d90
feat(api): expose qualification and promotion endpoints
lumenstech Sep 4, 2026
cbcb6ab
feat(api): expose intelligence inbox resolution state
lumenstech Sep 4, 2026
7a9b6c9
fix(api): keep inbox response lean
lumenstech Sep 4, 2026
130e24d
feat(app): add intelligence inbox page
lumenstech Sep 4, 2026
66d9cf5
feat(app): build intelligence inbox workflow
lumenstech Sep 4, 2026
2511adf
feat(app): add intelligence to primary navigation
lumenstech Sep 4, 2026
0443822
feat(app): prefetch intelligence inbox from navigation
lumenstech Sep 4, 2026
91f2999
fix: sync intelligence workflow with foundation schema
Sep 4, 2026
85394dc
fix: parse signal payloads at the API boundary
Sep 4, 2026
bf73c36
Add local lead worker dry-run extraction
Sep 16, 2026
6aa6e53
Add local lead campaign runner
Sep 16, 2026
8448b63
Fix local campaign failure handling
Sep 16, 2026
e08d24e
Add local campaign progress instrumentation
Sep 16, 2026
1e53e61
Tighten local lead extraction schema compliance
Sep 17, 2026
9948864
Rank local campaign source units before extraction
Sep 17, 2026
8186392
Filter weak local campaign source units
Sep 17, 2026
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
126 changes: 126 additions & 0 deletions apps/agent/agent/cli/local-campaign-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { writeFile } from "node:fs/promises";
import {
localCampaignCsv,
localCampaignJsonl,
} from "../lib/local-campaign-export";
import {
assertLocalCampaignOnly,
loadCampaignFile,
loadCampaignSeeds,
runLocalCampaign,
} from "../lib/local-campaign-runner";
import {
assertLocalCampaignPath,
defaultLocalCampaignStagingPath,
readStagedLeads,
} from "../lib/local-campaign-staging";

function argument(args: string[], flag: string): string | undefined {
const index = args.indexOf(flag);
return (
args
.find((value) => value.startsWith(`${flag}=`))
?.slice(flag.length + 1) ?? args[index + 1]

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When an option is absent, argument() returns the command instead of undefined. Normal run, list, and export invocations therefore fail while resolving omitted options. Return undefined when index < 0.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/cli/local-campaign-worker.ts, line 23:

<comment>When an option is absent, `argument()` returns the command instead of `undefined`. Normal `run`, `list`, and `export` invocations therefore fail while resolving omitted options. Return `undefined` when `index < 0`.</comment>

<file context>
@@ -0,0 +1,126 @@
+	return (
+		args
+			.find((value) => value.startsWith(`${flag}=`))
+			?.slice(flag.length + 1) ?? args[index + 1]
+	);
+}
</file context>
Fix with cubic

);
}

function numericArgument(args: string[], flag: string): number | undefined {
const value = argument(args, flag);
if (value === undefined) return undefined;
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error(`${flag} must be a positive number.`);
}
return Math.round(parsed);
}

async function main(): Promise<void> {
const args = process.argv.slice(2);
assertLocalCampaignOnly(args);
const command = args[0];
if (command === "validate") {
const campaign = await loadCampaignFile(argument(args, "--campaign") ?? "");
console.log(
JSON.stringify(
{ valid: true, campaign_id: campaign.campaign_id },
null,
2,
),
);
return;
}
if (command === "run") {
const baseCampaign = await loadCampaignFile(
argument(args, "--campaign") ?? "",
);
const campaign = {
...baseCampaign,
max_source_units_per_page:
numericArgument(args, "--max-source-units-per-page") ??
baseCampaign.max_source_units_per_page,
max_source_unit_chars:
numericArgument(args, "--max-source-unit-chars") ??
baseCampaign.max_source_unit_chars,
max_ollama_calls_per_seed:
numericArgument(args, "--max-ollama-calls-per-seed") ??
baseCampaign.max_ollama_calls_per_seed,
max_total_ollama_calls:
numericArgument(args, "--max-total-ollama-calls") ??

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: CLI overrides bypass the campaign schema's Ollama and source-unit maxima. A command such as --max-total-ollama-calls=100000 removes the configured call guardrail. Parse the merged campaign with localCampaignSchema before running.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/cli/local-campaign-worker.ts, line 68:

<comment>CLI overrides bypass the campaign schema's Ollama and source-unit maxima. A command such as `--max-total-ollama-calls=100000` removes the configured call guardrail. Parse the merged campaign with `localCampaignSchema` before running.</comment>

<file context>
@@ -0,0 +1,126 @@
+				numericArgument(args, "--max-ollama-calls-per-seed") ??
+				baseCampaign.max_ollama_calls_per_seed,
+			max_total_ollama_calls:
+				numericArgument(args, "--max-total-ollama-calls") ??
+				baseCampaign.max_total_ollama_calls,
+			per_ollama_call_timeout_ms:
</file context>
Fix with cubic

baseCampaign.max_total_ollama_calls,
per_ollama_call_timeout_ms:
numericArgument(args, "--per-ollama-call-timeout-ms") ??
baseCampaign.per_ollama_call_timeout_ms,
};
const seedFile = argument(args, "--seed-file");
const seeds = seedFile
? await loadCampaignSeeds(seedFile)
: campaign.seed_urls;
const progress = args.includes("--progress");
const result = await runLocalCampaign(campaign, seeds, {
stagingPath: argument(args, "--staging-path"),
seedStatusPath: argument(args, "--seed-status-path"),
perSeedTimeoutMs: numericArgument(args, "--per-seed-timeout-ms"),
onProgress: progress
? (event) => console.error(JSON.stringify(event))
: undefined,
});
console.log(JSON.stringify({ mode: "dry-run", ...result }, null, 2));
return;
}
if (command === "list") {
const path =
argument(args, "--staging-path") ?? defaultLocalCampaignStagingPath();
console.log(JSON.stringify(await readStagedLeads(path), null, 2));
return;
}
if (command === "export") {
const format = argument(args, "--format");
const path =
argument(args, "--staging-path") ?? defaultLocalCampaignStagingPath();
const leads = await readStagedLeads(path);
const output = argument(args, "--out");
if (!output) throw new Error("Export needs --out.");
const safeOutput = assertLocalCampaignPath(output);
if (format === "jsonl") {
await writeFile(safeOutput, localCampaignJsonl(leads), "utf8");
} else if (format === "csv") {
await writeFile(safeOutput, localCampaignCsv(leads), "utf8");
} else {
throw new Error("Export format must be csv or jsonl.");
}
console.log(
JSON.stringify(
{ exported: leads.length, output: safeOutput, format },
null,
2,
),
);
return;
}
throw new Error("Use validate, run, list, or export.");
}

main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
72 changes: 72 additions & 0 deletions apps/agent/agent/cli/local-lead-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
extractLocalLead,
fetchLocalLeadSource,
} from "../lib/local-lead-extraction";
import {
defaultLocalLeadStagingPath,
stageLocalLead,
} from "../lib/local-lead-staging";

function valueAfter(args: string[], flag: string): string | undefined {
const index = args.indexOf(flag);
return index >= 0 ? args[index + 1] : undefined;
}

function valueEquals(args: string[], flag: string): string | undefined {
const prefix = `${flag}=`;
return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
}

function argument(args: string[], flag: string): string | undefined {
return valueEquals(args, flag) ?? valueAfter(args, flag);
}

function assertDryRun(args: string[]): void {
const forbidden = args.filter((arg) =>
["--sync", "--sync-crm", "--production", "--neon"].includes(
arg.split("=", 1)[0] ?? "",
),
);
if (forbidden.length > 0) {
throw new Error(
`CRM synchronization is not implemented. Refused: ${forbidden.join(", ")}`,
);
}
}

async function main(): Promise<void> {
const args = process.argv.slice(2);
assertDryRun(args);

const text = argument(args, "--text");
const url = argument(args, "--url");
if ((text ? 1 : 0) + (url ? 1 : 0) !== 1) {
throw new Error("Provide exactly one of --text or --url.");
}

const source = url
? await fetchLocalLeadSource(url)
: {
sourceUrl:
argument(args, "--source-url") ?? "https://local.invalid/sample",
text: text as string,
};
const lead = await extractLocalLead(source.text, source.sourceUrl);
const staged = await stageLocalLead(
lead,
argument(args, "--staging-path") ?? defaultLocalLeadStagingPath(),
);

console.log(
JSON.stringify(
{ mode: "dry-run", extracted: lead, staging: staged },
null,
2,
),
);
}

main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
55 changes: 55 additions & 0 deletions apps/agent/agent/lib/local-campaign-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createHash } from "node:crypto";
import { appendFile, mkdir, readFile } from "node:fs/promises";
import { dirname } from "node:path";
import { fetchLocalLeadSource } from "./local-lead-extraction";
import { defaultLocalLeadStagingPath } from "./local-lead-staging";

export type CachedPage = {
url: string;
content_hash: string;
text: string;
fetched_at: string;
};

const cachePath = () =>
defaultLocalLeadStagingPath().replace(/leads\.jsonl$/, "pages.jsonl");

export async function readCachedPage(
url: string,
path = cachePath(),
): Promise<CachedPage | null> {
let raw = "";
try {
raw = await readFile(path, "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
throw error;
}
for (const line of raw.split("\n")) {
if (!line.trim()) continue;
const row = JSON.parse(line) as CachedPage;

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When pages.jsonl contains one truncated or invalid JSON line, readCachedPage throws before checking the requested URL, causing every seed to fail as a fetch error. Skip malformed cache lines and refetch that URL instead of treating cache corruption as a page-fetch failure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/lib/local-campaign-cache.ts, line 30:

<comment>When `pages.jsonl` contains one truncated or invalid JSON line, `readCachedPage` throws before checking the requested URL, causing every seed to fail as a fetch error. Skip malformed cache lines and refetch that URL instead of treating cache corruption as a page-fetch failure.</comment>

<file context>
@@ -0,0 +1,55 @@
+	}
+	for (const line of raw.split("\n")) {
+		if (!line.trim()) continue;
+		const row = JSON.parse(line) as CachedPage;
+		if (row.url === url) return row;
+	}
</file context>
Fix with cubic

if (row.url === url) return row;
}
return null;
}

export async function fetchCachedPage(
url: string,
fetchImpl: typeof fetch = fetch,
path = cachePath(),
): Promise<{ page: CachedPage; cached: boolean }> {
const existing = await readCachedPage(url, path);
if (existing) return { page: existing, cached: true };
const source = await fetchLocalLeadSource(url, fetchImpl);

@cubic-dev-ai cubic-dev-ai Bot Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A seed hangs indefinitely when its server stalls after response headers. Keep the fetch deadline active while reading the body.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/lib/local-campaign-cache.ts, line 43:

<comment>A seed hangs indefinitely when its server stalls after response headers. Keep the fetch deadline active while reading the body.</comment>

<file context>
@@ -0,0 +1,55 @@
+): Promise<{ page: CachedPage; cached: boolean }> {
+	const existing = await readCachedPage(url, path);
+	if (existing) return { page: existing, cached: true };
+	const source = await fetchLocalLeadSource(url, fetchImpl);
+	const page: CachedPage = {
+		url,
</file context>
Fix with cubic

const page: CachedPage = {
url,
content_hash: createHash("sha256")
.update(source.text, "utf8")
.digest("hex"),
text: source.text,
fetched_at: new Date().toISOString(),
};
await mkdir(dirname(path), { recursive: true });
await appendFile(path, `${JSON.stringify(page)}\n`, "utf8");
return { page, cached: false };
}
23 changes: 23 additions & 0 deletions apps/agent/agent/lib/local-campaign-export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { LocalStagedLead } from "./local-campaign-staging";

type CsvValue = string | string[] | number | boolean | null | undefined;

function csvValue(value: CsvValue): string {
const text = value === null || value === undefined ? "" : String(value);
return /[,"\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
}

export function localCampaignJsonl(leads: LocalStagedLead[]): string {
return `${leads.map((lead) => JSON.stringify(lead)).join("\n")}\n`;
}

export function localCampaignCsv(leads: LocalStagedLead[]): string {
const headers = leads[0] ? Object.keys(leads[0]) : [];
const rows = [
headers,
...leads.map((lead) =>
headers.map((header) => lead[header as keyof typeof lead]),
),
];
return `${rows.map((row) => row.map(csvValue).join(",")).join("\n")}\n`;
}
Loading
Loading