From a556a75cd9b8779f46d411bf54611bb5c45f3d71 Mon Sep 17 00:00:00 2001 From: Dan Levy Date: Wed, 26 Aug 2026 16:54:28 -0400 Subject: [PATCH 1/3] Add research campaign admission contract --- CONTEXT.md | 4 + docs/architecture.md | 1 + src/server/research/campaign-admission.ts | 325 ++++++++++++++++++ .../research-campaign-admission.test.ts | 167 +++++++++ 4 files changed, 497 insertions(+) create mode 100644 src/server/research/campaign-admission.ts create mode 100644 tests/integration/research-campaign-admission.test.ts diff --git a/CONTEXT.md b/CONTEXT.md index 9b3a118a4..3f33f9b6f 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -60,6 +60,9 @@ _Avoid_: autonomous plan, agent hunch, task queue A versioned decision contract that controls scheduling, approval consumption, budget stops, and recovery deduplication for a research campaign without creating target authorization or approval authority. _Avoid_: YOLO mode, blanket approval, autonomous permission +**Research Campaign Definition**: +A frozen, human-selected plan connecting one ranked ecosystem opportunity, admitted Target Recipe, authorization scope, autonomy policy, thread topology, harness admissions, budgets, exit criteria, honesty mode, recovery policy, and stop conditions before launch. +_Avoid_: agent plan, benchmark manifest, target authorization **Shared Terminal Session**: A project/thread-scoped interactive shell session whose input, output, resize events, interrupts, approvals, and actor attribution are visible to both the researcher and approved agent automation. _Avoid_: generic shell bridge, hidden agent shell, human terminal takeover @@ -161,6 +164,7 @@ _Avoid_: hidden gold, judge assertion - A **Research Observation** may preserve several external identifiers and versioned score assessments; each remains attributable to the Observation's citations and time. - A **Research Observation** may cite a message from another project thread when that discussion materially supports or contextualizes it. - A **Campaign Autonomy Policy** may consume an already matching durable approval, but it never mints one, widens target scope, extends an expired decision, overrides a denial, or blindly repeats an unknown side effect. +- A **Research Campaign Definition** may become ready for the execution gate only after human selection and admission checks; it never launches a target, creates execution authority, or imports a global vulnerability claim. - An **Investigation Entity** references a canonical project record when one exists instead of copying that record into the **Investigation Graph**. - An **Investigation Assertion** may be supported, contradicted, derived, revised, rejected, or left unresolved without changing the canonical record it discusses. - An **Investigation Citation** identifies why an **Investigation Assertion** exists; an **Artifact** remains the durable evidence object. diff --git a/docs/architecture.md b/docs/architecture.md index 0ba302cf4..8f89734a8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -142,6 +142,7 @@ The project Investigation Graph is an assertion layer over existing records, not Campaign autonomy is a scheduling and recovery policy, not an authorization source. Manual, bounded, and fully automated modes share the same durable target ledger and exact-intent enforcement. Only active probes, downloads, and shell commands may consume an explicitly declared exact preauthorization in a non-manual mode; credential tests, browser mutations, writes, exploit validation, and patching require a fresh exact decision. Denied, expired, mismatched, or consumed approvals stop the transition. Recovery reuses successful side effects and pauses to reconcile unknown outcomes before any replay. Every mode obeys conjunctive active-time, wall-time, cost, and action ceilings and preserves the policy/mode, normalized intent, authorization and approval references, Tool Run/effect fingerprint, budget transition, and raw redacted evidence. +A Research Campaign Definition freezes the selection boundary without launching work. It joins one human-selected ranked opportunity to a current-supported Target Recipe for organic discovery (or a separately labeled historical control), target authorization scope, campaign-autonomy policy, project-memory-scoped planning/research/validation/reporting threads, admitted harness manifests, conjunctive budgets, exit criteria, honesty boundaries, recovery deduplication, and stop conditions. Admission requires lifecycle/reset/teardown evidence and complete coverage, rejected-lead, impact-validation, patch, disclosure, reusable-method, accounting, terminal, and cleanup dispositions. The admitted definition exposes immutable references for the later execution gate but never creates an approval, target mutation, or vulnerability assertion. A Research Priority is an unresolved, citation-backed current assertion ranked for follow-up. Its deterministic score weights objective relevance (25%), evidence gap (20%), expected information gain (20%), target importance (15%), inverse predicate cost (8%), inverse predicate risk (7%), and authorization readiness (5%). Authorization readiness comes from the durable target ledger. Deliberately turning a Research Priority into a Task uses the existing Task workflow and a unique assertion-task receipt; it never schedules work, creates an approval, runs a tool, or promotes a Finding. The former generic security-graph repository is retired. Historical database tables may remain so existing local data is not destructively dropped, but no product path writes them and they are not authoritative. The only graph ownership boundaries are the global Security Knowledge Graph and each project's Investigation Graph. diff --git a/src/server/research/campaign-admission.ts b/src/server/research/campaign-admission.ts new file mode 100644 index 000000000..67a5b7b50 --- /dev/null +++ b/src/server/research/campaign-admission.ts @@ -0,0 +1,325 @@ +import type { CampaignAutonomyMode } from "./campaign-autonomy-policy"; + +export const RESEARCH_CAMPAIGN_DEFINITION_VERSION = + "research-campaign-definition-v1"; + +export type ResearchCampaignDefinition = { + version: typeof RESEARCH_CAMPAIGN_DEFINITION_VERSION; + campaignId: string; + projectId: string; + studyLane: "organic-discovery" | "historical-control"; + selection: { + decisionTicketId: string; + selectedBy: string; + selectedAt: string; + opportunitySnapshotDigest: string; + opportunityCandidateId: string; + opportunityStatus: "ranked" | "held" | "rejected"; + sourceSignalIds: string[]; + vulnerabilityClaimImported: boolean; + }; + targetRecipe: { + recipeId: string; + recipeDigest: string; + revisionLane: "current-supported" | "historical-control"; + targetLocator: string; + configurationId: string; + lifecycleEvidenceArtifactIds: string[]; + resetVerified: boolean; + teardownVerified: boolean; + }; + authorization: { + targetAuthorizationIds: string[]; + scopeDigest: string; + networkProfile: "offline" | "approved-targets"; + approvalPolicyVersion: string; + }; + autonomy: { + policyVersion: "campaign-autonomy-policy-v1"; + mode: CampaignAutonomyMode; + }; + threads: Array<{ + threadId: string; + role: "planning" | "research" | "validation" | "reporting"; + projectMemoryScopeId: string; + artifactHandoffRequired: boolean; + }>; + harnesses: Array<{ + harnessId: string; + admissionRecordId: string; + manifestDigest: string; + admitted: boolean; + stage: "sentinel" | "campaign" | "reporting-only"; + }>; + budgets: { + activeWorkingMs: number; + wallMs: number; + candidateCostUsd: number; + targetActionLimit: number; + contextTokens: number; + maxOutputTokens: number; + }; + exitCriteria: { + coverageArtifactRequired: boolean; + rejectedLeadRecordsRequired: boolean; + impactValidationProtocol: "impact-validation-v1"; + validatedFindingEvidenceRequired: boolean; + patchEvidenceForValidatedFindings: boolean; + disclosureReadinessDispositionRequired: boolean; + reusableMethodCandidateDispositionRequired: boolean; + costAndTimeLedgerRequired: boolean; + targetResetAndTeardownRequired: boolean; + terminalStatusRequired: boolean; + stopWhenEvidenceSufficient: boolean; + }; + honesty: { + knownAnswersCandidateVisible: false; + hiddenOracleCandidateVisible: false; + priorTargetFindingsCandidateVisible: false; + historicalControlsSeparated: boolean; + runModeLabel: "organic-hunt" | "historical-control"; + }; + recovery: { + sideEffectFingerprintsRequired: boolean; + unknownEffectsRequireReconciliation: boolean; + continuationArtifactRequiredOnPause: boolean; + }; + stopConditions: string[]; +}; + +export type ResearchCampaignAdmission = { + version: typeof RESEARCH_CAMPAIGN_DEFINITION_VERSION; + campaignId: string; + status: "ready-for-execution-gate" | "blocked"; + failures: string[]; + executionAuthorityCreated: false; + launchPerformed: false; + frozenReferences?: { + opportunitySnapshotDigest: string; + targetRecipeDigest: string; + scopeDigest: string; + harnessManifestDigests: string[]; + }; +}; + +const REQUIRED_THREAD_ROLES: ResearchCampaignDefinition["threads"][number]["role"][] = + ["planning", "research", "validation", "reporting"]; + +export function admitResearchCampaign( + definition: ResearchCampaignDefinition, +): ResearchCampaignAdmission { + const failures = [ + ...validateIdentity(definition), + ...validateSelection(definition), + ...validateRecipe(definition), + ...validateAuthorization(definition), + ...validateThreads(definition), + ...validateHarnesses(definition), + ...validateBudgets(definition), + ...validateExitCriteria(definition), + ...validateHonestyAndRecovery(definition), + ]; + return { + version: RESEARCH_CAMPAIGN_DEFINITION_VERSION, + campaignId: definition.campaignId, + status: failures.length === 0 ? "ready-for-execution-gate" : "blocked", + failures, + executionAuthorityCreated: false, + launchPerformed: false, + ...(failures.length === 0 + ? { + frozenReferences: { + opportunitySnapshotDigest: + definition.selection.opportunitySnapshotDigest, + targetRecipeDigest: definition.targetRecipe.recipeDigest, + scopeDigest: definition.authorization.scopeDigest, + harnessManifestDigests: definition.harnesses.map( + (item) => item.manifestDigest, + ), + }, + } + : {}), + }; +} + +function validateIdentity(definition: ResearchCampaignDefinition) { + const failures: string[] = []; + if (definition.version !== RESEARCH_CAMPAIGN_DEFINITION_VERSION) { + failures.push("campaign_definition_version_unsupported"); + } + for (const [name, value] of Object.entries({ + campaignId: definition.campaignId, + projectId: definition.projectId, + decisionTicketId: definition.selection.decisionTicketId, + selectedBy: definition.selection.selectedBy, + })) { + if (!value.trim()) failures.push(`${name}_missing`); + } + if (!Number.isFinite(Date.parse(definition.selection.selectedAt))) { + failures.push("selection_timestamp_invalid"); + } + if ( + definition.stopConditions.length === 0 || + definition.stopConditions.some((item) => !item.trim()) + ) { + failures.push("stop_conditions_missing"); + } + return failures; +} + +function validateSelection(definition: ResearchCampaignDefinition) { + const failures: string[] = []; + if (definition.selection.opportunityStatus !== "ranked") { + failures.push("opportunity_not_ranked"); + } + if (!digest(definition.selection.opportunitySnapshotDigest)) { + failures.push("opportunity_snapshot_digest_invalid"); + } + if (!definition.selection.opportunityCandidateId.trim()) + failures.push("opportunity_candidate_missing"); + if (definition.selection.sourceSignalIds.length === 0) + failures.push("source_signal_references_missing"); + if (definition.selection.vulnerabilityClaimImported) { + failures.push("global_signal_cannot_import_vulnerability_claim"); + } + return failures; +} + +function validateRecipe(definition: ResearchCampaignDefinition) { + const failures: string[] = []; + for (const [name, value] of Object.entries({ + recipeId: definition.targetRecipe.recipeId, + targetLocator: definition.targetRecipe.targetLocator, + configurationId: definition.targetRecipe.configurationId, + })) { + if (!value.trim()) failures.push(`${name}_missing`); + } + if (!digest(definition.targetRecipe.recipeDigest)) + failures.push("target_recipe_digest_invalid"); + if (definition.targetRecipe.lifecycleEvidenceArtifactIds.length === 0) { + failures.push("target_lifecycle_evidence_missing"); + } + if (!definition.targetRecipe.resetVerified) + failures.push("target_reset_not_verified"); + if (!definition.targetRecipe.teardownVerified) + failures.push("target_teardown_not_verified"); + if ( + definition.studyLane === "organic-discovery" && + definition.targetRecipe.revisionLane !== "current-supported" + ) { + failures.push("organic_discovery_requires_current_supported_revision"); + } + if ( + definition.studyLane === "historical-control" && + definition.targetRecipe.revisionLane !== "historical-control" + ) { + failures.push("historical_study_requires_control_revision"); + } + return failures; +} + +function validateAuthorization(definition: ResearchCampaignDefinition) { + const failures: string[] = []; + if (definition.authorization.targetAuthorizationIds.length === 0) { + failures.push("target_authorization_missing"); + } + if (!digest(definition.authorization.scopeDigest)) + failures.push("authorization_scope_digest_invalid"); + if (!definition.authorization.approvalPolicyVersion.trim()) { + failures.push("approval_policy_version_missing"); + } + if (definition.autonomy.policyVersion !== "campaign-autonomy-policy-v1") { + failures.push("campaign_autonomy_policy_mismatch"); + } + return failures; +} + +function validateThreads(definition: ResearchCampaignDefinition) { + const failures: string[] = []; + const ids = new Set(); + for (const thread of definition.threads) { + if (!thread.threadId.trim() || ids.has(thread.threadId)) + failures.push("thread_identity_invalid"); + ids.add(thread.threadId); + if (thread.projectMemoryScopeId !== definition.projectId) { + failures.push(`thread_memory_scope_mismatch:${thread.role}`); + } + if (!thread.artifactHandoffRequired) + failures.push(`artifact_handoff_not_required:${thread.role}`); + } + for (const role of REQUIRED_THREAD_ROLES) { + if (!definition.threads.some((thread) => thread.role === role)) { + failures.push(`thread_role_missing:${role}`); + } + } + return failures; +} + +function validateHarnesses(definition: ResearchCampaignDefinition) { + if (definition.harnesses.length === 0) return ["admitted_harness_missing"]; + const failures: string[] = []; + for (const harness of definition.harnesses) { + if (!harness.harnessId.trim() || !harness.admissionRecordId.trim()) { + failures.push("harness_identity_missing"); + } + if (!digest(harness.manifestDigest)) + failures.push("harness_manifest_digest_invalid"); + if (!harness.admitted) + failures.push(`harness_not_admitted:${harness.harnessId}`); + } + return failures; +} + +function validateBudgets(definition: ResearchCampaignDefinition) { + const failures: string[] = []; + for (const [name, value] of Object.entries(definition.budgets)) { + if (!Number.isFinite(value) || value <= 0) + failures.push(`campaign_budget_invalid:${name}`); + } + return failures; +} + +function validateExitCriteria(definition: ResearchCampaignDefinition) { + const { impactValidationProtocol, ...booleanCriteria } = + definition.exitCriteria; + const missing = Object.entries(booleanCriteria) + .filter(([, value]) => value !== true) + .map(([name]) => `exit_criterion_missing:${name}`); + if (impactValidationProtocol !== "impact-validation-v1") { + missing.push("impact_validation_protocol_mismatch"); + } + return missing; +} + +function validateHonestyAndRecovery(definition: ResearchCampaignDefinition) { + const failures: string[] = []; + if ( + definition.studyLane === "organic-discovery" && + definition.honesty.runModeLabel !== "organic-hunt" + ) { + failures.push("organic_run_mode_label_mismatch"); + } + if ( + definition.studyLane === "historical-control" && + definition.honesty.runModeLabel !== "historical-control" + ) { + failures.push("historical_run_mode_label_mismatch"); + } + if (!definition.honesty.historicalControlsSeparated) { + failures.push("historical_controls_not_separated"); + } + if (!definition.recovery.sideEffectFingerprintsRequired) { + failures.push("side_effect_fingerprints_not_required"); + } + if (!definition.recovery.unknownEffectsRequireReconciliation) { + failures.push("unknown_effect_reconciliation_not_required"); + } + if (!definition.recovery.continuationArtifactRequiredOnPause) { + failures.push("continuation_artifact_not_required"); + } + return failures; +} + +function digest(value: string) { + return /^sha256:[0-9a-f]{64}$/.test(value); +} diff --git a/tests/integration/research-campaign-admission.test.ts b/tests/integration/research-campaign-admission.test.ts new file mode 100644 index 000000000..697834608 --- /dev/null +++ b/tests/integration/research-campaign-admission.test.ts @@ -0,0 +1,167 @@ +import { describe, expect, it } from "vitest"; + +import { + admitResearchCampaign, + type ResearchCampaignDefinition, +} from "../../src/server/research/campaign-admission"; + +const sha = (character: string) => `sha256:${character.repeat(64)}`; + +function campaign(): ResearchCampaignDefinition { + return { + version: "research-campaign-definition-v1", + campaignId: "campaign-1", + projectId: "project-1", + studyLane: "organic-discovery", + selection: { + decisionTicketId: "issue-135", + selectedBy: "human-reviewer", + selectedAt: "2026-08-26T16:00:00.000Z", + opportunitySnapshotDigest: sha("a"), + opportunityCandidateId: "candidate-1", + opportunityStatus: "ranked", + sourceSignalIds: ["signal-1"], + vulnerabilityClaimImported: false, + }, + targetRecipe: { + recipeId: "recipe-1", + recipeDigest: sha("b"), + revisionLane: "current-supported", + targetLocator: "recipe:project/variant@revision", + configurationId: "configuration-1", + lifecycleEvidenceArtifactIds: ["artifact-lifecycle"], + resetVerified: true, + teardownVerified: true, + }, + authorization: { + targetAuthorizationIds: ["authorization-1"], + scopeDigest: sha("c"), + networkProfile: "approved-targets", + approvalPolicyVersion: "durable-exact-intent-v1", + }, + autonomy: { policyVersion: "campaign-autonomy-policy-v1", mode: "manual" }, + threads: ["planning", "research", "validation", "reporting"].map( + (role) => ({ + threadId: `thread-${role}`, + role: role as "planning" | "research" | "validation" | "reporting", + projectMemoryScopeId: "project-1", + artifactHandoffRequired: true, + }), + ), + harnesses: [ + { + harnessId: "native-harness", + admissionRecordId: "admission-1", + manifestDigest: sha("d"), + admitted: true, + stage: "sentinel", + }, + ], + budgets: { + activeWorkingMs: 600_000, + wallMs: 900_000, + candidateCostUsd: 10, + targetActionLimit: 100, + contextTokens: 65_536, + maxOutputTokens: 16_384, + }, + exitCriteria: { + coverageArtifactRequired: true, + rejectedLeadRecordsRequired: true, + impactValidationProtocol: "impact-validation-v1", + validatedFindingEvidenceRequired: true, + patchEvidenceForValidatedFindings: true, + disclosureReadinessDispositionRequired: true, + reusableMethodCandidateDispositionRequired: true, + costAndTimeLedgerRequired: true, + targetResetAndTeardownRequired: true, + terminalStatusRequired: true, + stopWhenEvidenceSufficient: true, + }, + honesty: { + knownAnswersCandidateVisible: false, + hiddenOracleCandidateVisible: false, + priorTargetFindingsCandidateVisible: false, + historicalControlsSeparated: true, + runModeLabel: "organic-hunt", + }, + recovery: { + sideEffectFingerprintsRequired: true, + unknownEffectsRequireReconciliation: true, + continuationArtifactRequiredOnPause: true, + }, + stopConditions: [ + "authorization changes", + "evidence is sufficient", + "target health changes", + ], + }; +} + +describe("research campaign admission", () => { + it("freezes a human-selected current-release campaign without launching it", () => { + expect(admitResearchCampaign(campaign())).toMatchObject({ + campaignId: "campaign-1", + status: "ready-for-execution-gate", + failures: [], + executionAuthorityCreated: false, + launchPerformed: false, + frozenReferences: { + opportunitySnapshotDigest: sha("a"), + targetRecipeDigest: sha("b"), + scopeDigest: sha("c"), + harnessManifestDigests: [sha("d")], + }, + }); + }); + + it("does not turn a held signal or imported vulnerability claim into a campaign", () => { + const input = campaign(); + input.selection.opportunityStatus = "held"; + input.selection.vulnerabilityClaimImported = true; + expect(admitResearchCampaign(input)).toMatchObject({ + status: "blocked", + failures: expect.arrayContaining([ + "opportunity_not_ranked", + "global_signal_cannot_import_vulnerability_claim", + ]), + }); + }); + + it("keeps historical controls out of organic discovery", () => { + const input = campaign(); + input.targetRecipe.revisionLane = "historical-control"; + expect(admitResearchCampaign(input).failures).toContain( + "organic_discovery_requires_current_supported_revision", + ); + }); + + it("blocks incomplete exit criteria and unadmitted harnesses", () => { + const input = campaign(); + input.exitCriteria.patchEvidenceForValidatedFindings = false; + input.harnesses[0].admitted = false; + expect(admitResearchCampaign(input)).toMatchObject({ + status: "blocked", + failures: expect.arrayContaining([ + "exit_criterion_missing:patchEvidenceForValidatedFindings", + "harness_not_admitted:native-harness", + ]), + }); + }); + + it("requires artifact handoffs across project-memory-scoped thread roles", () => { + const input = campaign(); + input.threads = input.threads.filter( + (thread) => thread.role !== "validation", + ); + input.threads[0].projectMemoryScopeId = "another-project"; + input.threads[1].artifactHandoffRequired = false; + expect(admitResearchCampaign(input).failures).toEqual( + expect.arrayContaining([ + "thread_role_missing:validation", + "thread_memory_scope_mismatch:planning", + "artifact_handoff_not_required:research", + ]), + ); + }); +}); From a1cbdd5d3b55774ef0365f7a12869d3d5998060d Mon Sep 17 00:00:00 2001 From: Dan Levy Date: Wed, 26 Aug 2026 17:00:05 -0400 Subject: [PATCH 2/3] Fingerprint admitted campaign definitions --- src/server/research/campaign-admission.ts | 17 +++++++++++++++++ .../research-campaign-admission.test.ts | 6 +++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/server/research/campaign-admission.ts b/src/server/research/campaign-admission.ts index 67a5b7b50..509a70f3d 100644 --- a/src/server/research/campaign-admission.ts +++ b/src/server/research/campaign-admission.ts @@ -1,3 +1,5 @@ +import { createHash } from "node:crypto"; + import type { CampaignAutonomyMode } from "./campaign-autonomy-policy"; export const RESEARCH_CAMPAIGN_DEFINITION_VERSION = @@ -94,6 +96,7 @@ export type ResearchCampaignAdmission = { failures: string[]; executionAuthorityCreated: false; launchPerformed: false; + definitionDigest?: `sha256:${string}`; frozenReferences?: { opportunitySnapshotDigest: string; targetRecipeDigest: string; @@ -119,6 +122,8 @@ export function admitResearchCampaign( ...validateExitCriteria(definition), ...validateHonestyAndRecovery(definition), ]; + const definitionDigest = + `sha256:${createHash("sha256").update(stableJson(definition)).digest("hex")}` as const; return { version: RESEARCH_CAMPAIGN_DEFINITION_VERSION, campaignId: definition.campaignId, @@ -128,6 +133,7 @@ export function admitResearchCampaign( launchPerformed: false, ...(failures.length === 0 ? { + definitionDigest, frozenReferences: { opportunitySnapshotDigest: definition.selection.opportunitySnapshotDigest, @@ -323,3 +329,14 @@ function validateHonestyAndRecovery(definition: ResearchCampaignDefinition) { function digest(value: string) { return /^sha256:[0-9a-f]{64}$/.test(value); } + +function stableJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`; + if (value && typeof value === "object") { + return `{${Object.entries(value) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, child]) => `${JSON.stringify(key)}:${stableJson(child)}`) + .join(",")}}`; + } + return JSON.stringify(value) ?? "null"; +} diff --git a/tests/integration/research-campaign-admission.test.ts b/tests/integration/research-campaign-admission.test.ts index 697834608..4aac8e2a7 100644 --- a/tests/integration/research-campaign-admission.test.ts +++ b/tests/integration/research-campaign-admission.test.ts @@ -100,7 +100,9 @@ function campaign(): ResearchCampaignDefinition { describe("research campaign admission", () => { it("freezes a human-selected current-release campaign without launching it", () => { - expect(admitResearchCampaign(campaign())).toMatchObject({ + const first = admitResearchCampaign(campaign()); + const second = admitResearchCampaign(campaign()); + expect(first).toMatchObject({ campaignId: "campaign-1", status: "ready-for-execution-gate", failures: [], @@ -113,6 +115,8 @@ describe("research campaign admission", () => { harnessManifestDigests: [sha("d")], }, }); + expect(first.definitionDigest).toMatch(/^sha256:[0-9a-f]{64}$/); + expect(second.definitionDigest).toBe(first.definitionDigest); }); it("does not turn a held signal or imported vulnerability claim into a campaign", () => { From c9213ca5862a3952b24b6eb9b51ea4d8a2162a64 Mon Sep 17 00:00:00 2001 From: Dan Levy Date: Thu, 27 Aug 2026 00:28:43 -0400 Subject: [PATCH 3/3] Export research campaign admission --- src/server/research/index.ts | 1 + tests/integration/research-campaign-admission.test.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/research/index.ts b/src/server/research/index.ts index e33dc2d8c..587805811 100644 --- a/src/server/research/index.ts +++ b/src/server/research/index.ts @@ -1,4 +1,5 @@ export * from "./campaign-autonomy-policy"; +export * from "./campaign-admission"; export * from "./resource-aware-execution"; export { type FinishResearchInput, diff --git a/tests/integration/research-campaign-admission.test.ts b/tests/integration/research-campaign-admission.test.ts index 4aac8e2a7..5e1bee611 100644 --- a/tests/integration/research-campaign-admission.test.ts +++ b/tests/integration/research-campaign-admission.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { admitResearchCampaign, type ResearchCampaignDefinition, -} from "../../src/server/research/campaign-admission"; +} from "../../src/server/research"; const sha = (character: string) => `sha256:${character.repeat(64)}`;